Sharp built-in solution for uploads

Uploads are painful.

Sharp provide a very opinionated and totally optional solution to handle if you are using Eloquent and the WithSharpFormEloquentUpdater trait (see related documentation).

The proposal is to use a special Sharp Model for all your uploads, and to link them to your Models with Eloquent's Morph relationships.

Use SharpUploadModel

The base Model class is Code16\Sharp\Form\Eloquent\Uploads\SharpUploadModel. Just create your own Model class and make it extends this base class.

You'll have to define the Eloquent $table attribute to indicate the table name. So for instance, let's say your Model name choice is Media, here's the class code:

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

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

Generator

php artisan sharp:make:media <model_name> --table=<table_name>

Create the migration

Sharp provides an artisan command for that: sharp:create_uploads_migration <table_name>

Pass your specific table name in the table_name argument ("medias" in our example).

This command will create a migration file 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();
        });
    }

    public function down()
    {
        Schema::dropIfExists('medias');
    }
}

Now, you need to define the relationships. Let's say you have a Book model, and you want the user to be able to upload its cover and PDF version.

    class Book extends Model
    {
	public function cover()
        {
            return $this->morphOne(Media::class, "model")
                ->where("model_key", "cover");
        }

        public function pdf()
        {
            return $this->morphOne(Media::class, "model")
                ->where("model_key", "pdf");
        }
    }

Use it!

Let's pretend you already have data in this new table, here how to handle it.

Properties

By default, you can get the file_name, but also mime_type and file's size.

Custom properties

You can add whatever property you need through custom properties, by setting it:

$book->cover->author = "Tomi Ungerer";

Custom properties will be stored in the custom_properties column, as JSON.

You can retreive the value the same way:

$author = $book->cover->author

Thumbnails

Thumbnail creation, for image, is built-in, with this function: thumbnail($width=null, $height=null, $filters=[])

You must first define the thumbnail directory, in Sharp's config:

    // config/sharp.php

    "uploads" => [
        "thumbnails_dir" => "thumbnails",
    ],

This path is relative to the public directory.

Then you can call $thumb = $book->cover->thumbnail(150) to have a full URL to a 150px (width) thumbnail.

Filters

The third argument is for Filters. For now, only two are available:

  • greyscale ->thumbnail(150, null, ["greyscale" => []])

  • fit: this one has 2 params, w for width and h for height, and will center-fit the image in those constraints. ->thumbnail(150, null, ["fit" => ["w"=>150, "h"=>100]])

But of course you can provide here a custom one. You'll need for that to first create a Filter class that extends Code16\Sharp\Form\Eloquent\Uploads\Thumbnails\ThumbnailFilter, implementing:

  • function applyFilter(Intervention\Image\Image $image): apply you filter, using the great Intervention API.
  • function resized(): (optional, default to false) Return true if the resize is part of the applyFilter() code.

Once the class is created, pass the full class path as filter name:

    return $this->thumbnail($size, $size, [
        CustomThumbnailFilter::class => ["w"=>$w, 'fill'=>'#ffffff']
    ]);

Update with Sharp

The best part is this: Sharp will take care of everything related to update and store.

First declare your upload, like usual:

    function buildFormFields()
    {
        $this->addField(
            SharpFormUploadField::make("cover")
                ->setLabel("Cover")
                ->setFileFilterImages()
                ->setCropRatio("1:1")
                ->setStorageDisk("local")
                ->setStorageBasePath("data/Books")
            )
        );
    }

Then add a customTransformer:

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

The full path of this transformer is Code16\Sharp\Form\Eloquent\Transformers\FormUploadModelTransformer.

And finally, and this is a sad exception to the "don't touch the applicative code for Sharp", add this in your Model that declares an upload relationship (Book, in our example):

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

This will tell SharpEloquentUpdater to add the necessary model_keyattribute when creating a new upload.

And... voilà! From there, Sharp will handle the rest.

Updating custom attributes

So we want to add an author custom attribute to our cover field: for this we add the field in the Sharp Entity Form, using the : separator to designate a related attribute:

    $this->addField(
        SharpFormTextField::make("cover:author")
            ->setLabel("Author")
    );

Here we intend to update the author attribute of the cover relation.

What about upload lists?

So let's say we want to add pictures of inner pages, for our Book. It can be easily done by creating a morphMany relation in the Book Model:

    public function pictures()
    {
        return $this->morphMany(Media::class, "model")
            ->where("model_key", "pictures")
            ->orderBy("order");
    }

And then add the field in the Sharp Entity Form:

    $this->addField(
        SharpFormListField::make("pictures")
            ->setLabel("Additional pictures")
            ->setAddable()->setAddText("Add a picture")
            ->setRemovable()
            ->setSortable()
            ->setOrderAttribute("order")
            ->addItemField(
                SharpFormUploadField::make("file")
                    ->setFileFilterImages()
                    ->setStorageDisk("local")
                    ->setStorageBasePath("data/Books/Pictures")
            )
        )
    );

Note that we use the a special file key for the SharpFormUploadField in the item.

You'll have next to update your Model special getDefaultAttributesFor() function:

    public function getDefaultAttributesFor($attribute)
    {
        return in_array($attribute, ["cover","pictures"])
            ? ["model_key" => $attribute]
            : [];
    }

All set.

Updating custom attributes in upload lists

    $this->addField(
        SharpFormListField::make("pictures")
            [...]
            ->addItemField(
                SharpFormUploadField::make("file")
                    [...]
            )->addItemField(
                SharpFormTextField::make("legend")
            )
        )
    );

In this code, the legend designates a custom attribute.