About Sharp for Laravel — Part 6.

Uploads are painful, but Sharp can bring some light.

Philippe Lonchampt
code16

--

See Part 1, Part 2, Part 3, Part 4 and Part 5 of this series.

Uploads are a difficult part in content management. Sharp provides a great help on this topic: first, and as usual, the front part is handled—meaning file selection, async upload with progress bar, errors, image thumbnails and so on. But it can also be very helpful on the backend side: let’s review how in this article.

The upload Model

In our little dummy project we’re building for this series, we want to add the possibility to upload a picture for a Player. Sharp’s proposal (it’s an option) is to use a special Model for all uploads, linked to any Model via an Eloquent Morph relation. I think this is a clever solution because:

  • adding an upload to any Model is as easy as writing the relation (no migration needed),
  • we can use a MorphMany relation to handle multiple uploads,
  • and finally with json field we can easily add custom attributes to our upload, as shown below.

Again, let’s be clear: using this special Model is an option, to make it easier. You are not required to use this solution with Sharp.

First step, we create the new Model for our uploads, that we chose to name “Media”:

use Code16\Sharp\Form\Eloquent\Uploads\SharpUploadModel;

class Media extends SharpUploadModel
{
protected $table = "medias";
}

Note that it must extend the SharpUploadModel base Model. Next, we run a Sharp artisan command to create the migration file:

php artisan sharp:create_uploads_migration medias

This command creates a migration like this one:

class CreateMediasTable extends Migration
{
public function up()
{
Schema::create('medias', function (Blueprint $table) {
$table->increments('id');
$table->morphs('model');
$table->string('model_key')->nullable();
$table->string('file_name')->nullable();
$table->string('mime_type')->nullable();
$table->string('disk')->default('local')->nullable();
$table->unsignedInteger('size')->nullable();
$table->text('custom_properties')->nullable();
$table->unsignedInteger('order')->nullable();
$table->timestamps();
});
}
}

And finally, we declare the new relation in the Player model:

class Player extends Model
{
[...]
public function picture()
{
return $this->morphOne(Media::class, "model")
->where("model_key", "picture");
}
[...]
}

Note that we added a model_key constraint which must be aligned with the method name (“picture”, in our case): this is needed to allow multiple relations in the Model; for instance, we could add a stats() relation to a PDF file, or something.

The first part is done, and is not related to Sharp alone; you can now use this relation in your code base to display the Player’s picture, even with some thumbnails / filters helper also provided. This topic is not the core of this article, so I leave you with the documentation.

Use this in Sharp for update

This is of course the goal here: how can we plug this to our Sharp form? Well, quite easily:

First, add the field in the form:

class PlayerSharpForm extends SharpForm
{
use WithSharpFormEloquentUpdater;

function buildFormFields()
{
$this->addField(
[...]
)->addField(
SharpFormUploadField::make("picture")
->setLabel("Picture")
->setFileFilterImages()
->setStorageDisk("local")
->setStorageBasePath("data/Players")
);
}

function buildFormLayout()
{
$this->addColumn(6, function(FormLayoutColumn $column) {
$column->withSingleField("name")
->withFields("team_id|6", "ratings|6");
})->addColumn(6, function(FormLayoutColumn $column) {
$column->withSingleField("picture");
});
}

[...]
}

The SharpFormUploadField field has many options, listed in the documentation. Here we simply set a file filter to only accept image files, and declared the storage configuration.

We are almost done here: we must finally update the find() method to add a custom transformer:

class PlayerSharpForm extends SharpForm
{
[...]

function find($id): array
{
return $this->setCustomTransformer(
"picture",
new FormUploadModelTransformer()
)->transform(
Player::with("picture")->findOrFail($id)
);
}

[...]
}

The FormUploadModelTransformer class is built in Sharp, and as any transformer in Sharp its role is to handle the data transfer to and from the front side.
Notice we also ensure that Eloquent loaded the picture related Model.

And for the real final step before a screenshot, we must… break a Sharp rule 😬. See, Sharp was developed to stay away from the functional project code, but we need in this case to add a tiny method in our Player Model in order to make the full Sharp / Eloquent auto update magic work. Here the related code:

class Player extends Model
{
[...]

public function getDefaultAttributesFor($attribute)
{
return $attribute == "picture"
? ["model_key" => $attribute]
: [];
}

}

This method will ensure that the model_key attribute of our Media Model is updated on the save process, as Sharp Eloquent updater class will call this when needed.

OK. Screenshot time:

Yes, Daniel Narcisse is the least we could do.

All the rest is Sharp business: browsing files, temporary upload, thumbnail, image download, image editing (crop, rotate), and of course the Update process which just works. Yes, we’re done!

Some tweaks

If you want more, from here, we can:

Add a crop ratio constraint:

class PlayerSharpForm extends SharpForm
{
use WithSharpFormEloquentUpdater;

function buildFormFields()
{
$this->addField(
[...]
)->addField(
SharpFormUploadField::make("picture")
->setLabel("Picture")
->setFileFilterImages()
->setCropRatio("1:1")
->setStorageDisk("local")
->setStorageBasePath("data/Players")
);
}

[...]
}
All picture will now be cropped as squares when uploading and editing

Manage upload folders

Sure you can configure the thumbnails folder, but it’s also possible to add the Model id in the storage path, which is always useful:

class PlayerSharpForm extends SharpForm
{
use WithSharpFormEloquentUpdater;

function buildFormFields()
{
$this->addField(
[...]
)->addField(
SharpFormUploadField::make("picture")
->setLabel("Picture")
->setFileFilterImages()
->setCropRatio("1:1")
->setStorageDisk("local")
->setStorageBasePath("data/Players/{id}")
);
}

[...]
}

Sharp’s Eloquent updater is clever enough to handle this special {id} trick even at creation step, when the id isn’t already set, by deferring the related code.

Add a custom attribute

Say you want to attach its copyright to the picture. It couldn’t be more easy with Upload’s custom attributes feature:

class PlayerSharpForm extends SharpForm
{
use WithSharpFormEloquentUpdater;

function buildFormFields()
{
$this->addField(
[...]
)->addField(
SharpFormTextField::make("picture:copyright")
->setLabel("Copyright")
);
}

function buildFormLayout()
{
$this->addColumn(6, function(FormLayoutColumn $column) {
$column->withSingleField("name")
->withFields("team_id|6", "ratings|6");
})->addColumn(6, function(FormLayoutColumn $column) {
$column->withSingleField("picture")
->withSingleField("picture:copyright");
});
}
[...]
}

With the : notation, seen in a previous article in this series, we can declare a sub-attribute—here the copyright attribute of picture, which is our Upload Model. And since the copyright attribute does not exists, it will be treated by the model as a custom one, stored in a json-based column. So, with this tiny code, we get this working:

I’m thinking that the “Picture” part of the form could be visually enhanced with a Fieldset

The copyright will be updated and stored in the medias table, and can be retrieved with $player->picture->copyright.

Display the picture in the Entity List

Before closing this article, let’s see how we can display this new picture in our Players list in Sharp:

class PlayerSharpList extends SharpEntityList
{

function buildListDataContainers()
{
$this->addDataContainer(
EntityListDataContainer::make("picture")
->setLabel("")
)->addDataContainer(
[...]
);
}

[...]

function buildListLayout()
{
$this->addColumn("picture", 1)
->addColumn("name", 4)
->addColumn("team:name", 4)
->addColumn("ratings", 3);
}

function getListData(EntityListQueryParams $params)
{
[...]

return $this
->setCustomTransformer(...)
->setCustomTransformer(
"picture",
new SharpUploadModelAttributeTransformer(100)
)

->transform(
$players->with("team", "picture")->paginate(30)
);
}
}

We’ve first added a column “picture” in the data containers and in the layout (see the very first article for more on this), and then declared a Sharp built-in custom transformer to handle the thumbnail creation.

And voilà.

Conclusion

This was a quite long ride, becaise I wanted to be exhaustive, and still we avoid some subjects like uploads lists; but to me the most important to understand is that Sharp provides a great help with files for the uploading part, update, storage and even custom attributes and thumbnail creation, (almost) without adding code adherence to the functional code. Some of these features are options (you can only take the front part, if you want to), but as many times with Laravel, in my opinion, the best option is maybe to fully embrace the package.

Sharp is an open source project available on Github, and as always in this series the full code described in this article is too. I don’t really know right now where the next part will lead us… Maybe an advanced topic like custom form field creation, or how to add feature tests to Sharp forms or commands.

--

--

Philippe Lonchampt
code16
Editor for

Developer and founder of Code 16, maintainer of Laravel Sharp.