Upload multiple files, create thumbnails with Spatie MediaLibrary

Spatie Laravel-Medialibrary is a very powerful package that lets you associate files (not just images) with your Eloquent models.

Whilst being a great package, I found that on my first time using it, I was struggling to figure out how to use it. I had to read a fair few blogs and guides to get started.

In this post, i'll cover the following:

  • How to upload and optimise multiple images
  • Store the images on S3 and local storage
  • Create a thumbnail conversion
  • Fetch the images and the thumbnails

First of all we'll need a simple form to attach our images to. In your blade file, /resources/events/create.blade.php create a form element like below: (Note, this form is basic for demo purposes. It also uses Bootstrap 5 for styling. Amend as needed)

<form action="/events/store" method="post" id="create-event" enctype="multipart/form-data">
    @csrf
    <div class="form-group mb-3">
        <label for="images">Select Images - Max: 10</label>
        <input type="file" name="images[]" class="form-control" multiple="" accept="image/png, image/jpeg">
    </div>
    <button type="submit" class="my-3 btn btn-lg btn-primary">Create Event</button>
</form>

Next we should prepare the model we want to associate the files to.

In my example I'm working with an Event model, replace Event with your own.

Make sure your model implements the HasMedia inteface and also make sure to import the 4 classes: use Spatie\Image\Manipulations; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; and use Spatie\MediaLibrary\MediaCollections\Models\Media;

And finally, add the InteractsWithMedia trait to the class: use HasFactory, InteractsWithMedia;

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;

class Event extends Model implements HasMedia
{
    use HasFactory, InteractsWithMedia;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'slug',
        'title',
    ];

    public function registerMediaConversions(Media $media = null): void
    {
        $this->addMediaConversion('thumb')
            ->width(368)
            ->height(232)
            ->sharpen(10)
            ->quality(60)
            ->performOnCollections('images');

    }
}

Looking at the model you can see that there is a method called registerMediaConversions(). This is where we can add multiple conversion that will happen when media is uploaded to our app. In this example you can see that i can set the width and height, sharpen and set the image quality - All of these enhancements use the Spatie Image package. Have a look at the documentation as there are many more you can use!

Please Note: In order for the conversions to work, you must have a queue worker running. By default the package offloads conversion to the queue. To start up a queue locally, run: php artisan queue:listen

The next step is to create our controller and populate the store method. php artisan make:controller EventController --model=Event Notice the --model=Event flag, this will create a controller with the standard C.R.U.D methods that you would generally use.

Open up the controller and navigate to the create() method and in here we're going to return the view we created in the first step resources/events/create.blade.php - in case you missed it. Add the following code:

/**
 * Show the form for creating a new resource.
 *
 * @return Response
 */
public function create()
{
    return view('events.create');
}

Next we want to populate the store() method in the same controller:

/**
 * Store a newly created resource in storage.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return RedirectResponse
 */
public function store(Request $request)
{
    $request->validate([
        'images' => 'max:10',
        'images.*' => ['image', 'max:4000'],
        // Add your own fields in here too
    ])
    
    $event = Event::create($request->only([
        'images',
        // Any other fields here too
    ]));

    if ($request->has('images')) {
        foreach ($request->images as $image) {
            Image::load($image->getPathName())
                ->quality(60)
                ->save();

            $event
                ->addMedia($image) 
                ->toMediaCollection('images');

            // Optional - If you want to upload your images to a cloud provider such as AWS S3
            $event
                ->addMedia($image) //starting method
                ->toMediaCollection('images', 's3');
        }
    }

    return redirect()->route('events')->with('message', 'Event Created!');
}

As i've commented in the validation section, you can add your own fields - (don't forget to add them to your view / form also). What's happening is, we are first checking if there are any images in the request and if so looping over each one, setting the quality, saving the image as-is and then assigning it to our $event model and also adding it to an images collection. Finally we redirect to our events view, which we are going to create now.

Optional Step - Uploading images to AWS S3

If you choose to upload your images to a cloud provider, you'll need to first make sure your credentials are set in your .env file. By default, the images you upload will have their visibility set as private, meaning you can only access them with a signed URL. If you want to upload your images and make them publically accessible, you'll need to add the following line of code to your config/media-library.php config file.

If you don't see this file, you can generate it with this command: php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="config".

There is a 'remote' key and you want to add this onto the next line:

'remote' => [
    /*
     * Any extra headers that should be included when uploading media to
     * a remote disk. Even though supported headers may vary between
     * different drivers, a sensible default has been provided.
     *
     * Supported by S3: CacheControl, Expires, StorageClass,
     * ServerSideEncryption, Metadata, ACL, ContentEncoding
     */
    'extra_headers' => [
        'CacheControl' => 'max-age=604800',
        'visibility' => 'public', // Add this line here to make images public by default
    ],
],


Displaying the saved images

The last step is to now show the images we just saved. Find the index() method in the controller and add the following:

public function index()
{
    $events = Event::with('media')->get();
    return view('events.index', ['events' => $events]);
}

Were fetching all the events and also loading in the media (images) to avoid the N+1 problem of fetching the images for each event separately. This will help reduce database queries and improve performance.

After this we need to make a new blade file in resources/events/index.blade.php this is where we'll list out each event and its images.

Add the follwing code to the view:

<section>
    <div class="container">
        <div class="row">
        @foreach($events as $event)
            <div class="col-md-12">
                <h1>{{ $event->title }}</h1>
                // If you only want to show the first image - useful for showing each event with an image
                <img src="{{ $event->getFirstMediaUrl('images', 'thumb') }}" alt="{{ $event->title }}" class="img-fluid">
                // Show all the images from the collection
                @foreach($event->media as $image)
                    <img src="{{ $image->getUrl() }}" alt="{{ $event->title }}">
                @endforeach
            </div>
        @endforeach
        </div>
    </div>
</section>

As you can see i've given two examples of how to retreive your media, the first one allows you to show the first item in the media collection, useful if you want to only show 1 image. Also notice, i've included 'thumb' as the second parameter. E.g. $event->getFirstMediaUrl('images', 'thumb'). You can pass in the name of the conversion (See the Event model for reference) and the package will use that conversion. If you want to show the full size image, just remove the second parameter.

The other example loops over each image in the collection and fetches back the full URL to the image. Again, if you want the thumbnail or another conversion, just pass that in. E.g. $image->getUrl('thumb').

Finally, we need to create our routes. In routes/web.php add the following:

Route::get('/events', 'App\Http\Controllers\EventController@index')->name('events');
Route::get('/events/create', 'App\Http\Controllers\EventController@create')->name('events.create');
Route::post('/events/store', 'App\Http\Controllers\EventController@store')->name('events.store');

Go test it out!

More Posts