laravel-medialibrary is a powerful package that can help handle media in a Laravel application. It can organise your files across multiple filesystems, generate thumbnails, optimize images and much much more. At Spatie we use this package in nearly every project.

The last few months our team has been hard at work to create a new major version of the package. I'm happy to share that this week I released the new v7 on stage at the Laravel Live conference in New Delhi. In this blogpost I'd like to show you some of the new features.

General introduction #

To get a feeling of the basic features of the medialibrary, you should watch this video.

In case you don't have time to view the entire video, here are some quick examples to get you up to speed. In essence to package allow you to associate files with Eloquent models.

$yourModel
    ->addMedia($filePath)
    ->toMediaCollection();

This will move the file at $filePath to the medialibrary and associate a new Media model with $yourModel. You can add as many files as you'd like.

To get all associated media you can do this:

$yourModel->getMedia();

This will return a Collection of associated Media models. Because I found that the first associated media is often need there's a getFirstMedia method that does exactly what it's name implies.

A media object you can determine the url and path to it's associated file.

$yourModel->getFirstMedia()->getUrl() // returns an url
$yourModel->getFirstMedia()->getPath() // returns the full path

If you delete models that have associated media, the medialibrary will also clean up the files. You don't need to clean up after yourself.

// all related files will be deleted from the filesystem
$yourModel->delete();

Media can be put in so called media collections. It's really nothing more the putting a name on a group of media so you can retrieve them together.

$yourModel
    ->addMedia($pathToImage)
    ->toMediaCollection('images');
    
$yourModel
    ->addMedia($pathToAnotherImage)
    ->toMediaCollection('images');
    
$yourModel
    ->addMedia($pathToPdf)
    ->toMediaCollection('downloads');
    
$yourModel
    ->addMedia($pathToAnotherPdf)
    ->toMediaCollection('downloads');
    
$yourModel->getMedia('images'); // only get media in the images collection

$yourModel->getMedia('downloads'); // only get media in the downloads collection

The medialibrary can also store things on external filesystems. Under the hood Laravel's native cloud filesystem is used. So you can store your media on all filesystem for which there is a FlySystem driver (S3, Dropbox, Google Drive, SFTP, ...)

Storing a media on a remote filesystem is laughably easy. Just use the disk name as the second parameter of toMediaCollection.

// the image will be uploaded to s3
$yourModel
    ->addMedia($pathToImage)
    ->toMediaCollection('images', 's3');
    
$yourModel->getFirstMedia('images')->getUrl(); // returns an url to the file on s3

The medialibrary can also generate derived images. Image you want to create a small thumbnail of an image. That's easy! On your eloquent model you should define a media conversion.

// in your model
 public function registerMediaConversions()
{
    $this
		    ->addMediaConversion('thumb')
        ->width(50)
        ->height(50);
 }

Whenever you associate media with your model a derived image will be created that will fit in a 50x50 box, aspect ratio will be respected.

$yourModel
    ->addMedia($pathToImage)
    ->toMediaCollection();
    
$yourModel->getFirstMedia()->getUrl() // url to original image
$yourModel->getFirstMedia()->getUrl('thumb') // url to the thumbnail version.

By default all image conversions are queued. You can have as many image conversions on a model as you'd like. Under the hood our image package is used, so you can any of it's many supported manipulations.

The above examples are only a tip of the iceberg. If you want to know more about all available features, go read the extensive documentation we've written for this package.

With this general introduction out of the way, let's focus on the new features that have been added to the shiny new v7 of the package.

New in v7: progressive image loading and responsive images #

One of the most important additions in v7 is the support for responsive images. Websites are viewed on various devices with widely differing screen sizes and connection speeds. When serving images it's best not to use the same image for all devices. A large image might be fine on a desktop computer with a fast internet connection, but on a small mobile device with limited bandwhith, the download might take a long time.

The most common way to display a picture is by using an img element with a src attribute.

<img src="my-image.jpg">

Using this markup the browser will always display my-image.jpg regardless of screen size.

As described in the HTML specification you can also use a srcset attribute to indicate different versions of your image and their respective width.

<img src="large.jpg" srcset="large.jpg 2400w, medium.jpg 1200w, small.jpg 600w">

When using srcset, the browser will automatically figure out which image is best to use.

Responsive images are nice addition. But of course to display an image it needs to come over the wire first. On bad network condition even a small image download can take a while. When an image finally is downloaded the layout of the page is potentially rearrenged, which can be distracting.

In order to solve this problem support for progressive image loading was added to the medialibrary. You might have seen this technique before at [Medium] blogs (here's an example. You see a blurred image first. After a while that images gets replaced by the real one.

The medialibrary can generate both the blurred placeholder and all responsive versions of the image (these go in the srcset attribute) for you. Simply use the withResponsiveImages function when adding media to the medialibrary.

$yourModel
   ->addMedia($yourImageFile)
   ->withResponsiveImages()
   ->toMediaCollection();

By default the medialibrary uses an algorithm which produces responsive images that have ±70% of the filesize of the previous variation. It will keep generating variations until the predicted file size is lower then 10 Kb or the target width is less than 20 pixels. So for an image with large dimensions the medialibrary will generate more variations than for an image with smaller dimensions.

In order to display responsive images you can simply output an instance of media in Blade view.

// we use a closure route here, normally you'd do this in a controller

Route::get('showing-responsive-images', function() {
    $yourModel->getFirstMedia();

    return view('showing-responsive-images', compact('media'));
});

The blade view could look like this:

{{-- Yup, there's nothing more to it --}}
{{ $media }}

This will output html that will look like:

<img
	 srcset="
	 	 /media/1/responsive-images/test___medialibrary_original_2048_1536.jpg 2048w,
	 	 /media/1/responsive-images/test___medialibrary_original_1713_1284.jpg 1713w,
	 	 /media/1/responsive-images/test___medialibrary_original_1433_1074.jpg 1433w,
	 	 /media/1/responsive-images/test___medialibrary_original_1199_899.jpg 1199w,
	 	 /media/1/responsive-images/test___medialibrary_original_1003_752.jpg 1003w, 
	 	 /media/1/responsive-images/test___medialibrary_original_839_629.jpg 839w, 
	 	 /media/1/responsive-images/test___medialibrary_original_702_526.jpg 702w,
	 	 /media/1/responsive-images/test___medialibrary_original_587_440.jpg 587w,
	 	 /media/1/responsive-images/test___medialibrary_original_491_368.jpg 491w,
	 	 /media/1/responsive-images/test___medialibrary_original_411_308.jpg 411w,
	 	 /media/1/responsive-images/test___medialibrary_original_344_258.jpg 344w,
	 	 /media/1/responsive-images/test___medialibrary_original_287_215.jpg 287w,
	 	 /media/1/responsive-images/test___medialibrary_original_240_180.jpg 240w, 
	 	 data:image/svg+xml;base64,PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHg9IjAiCiB5PSIwIiB2aWV3Qm94PSIwIDAgMjA0OCAxNTM2Ij4KCTxpbWFnZSB3aWR0aD0iMjA0OCIgaGVpZ2h0PSIxNTM2IiB4bGluazpocmVmPSJkYXRhOmltYWdlL2pwZWc7YmFzZTY0LC85ai80QUFRU2taSlJnQUJBUUVBWUFCZ0FBRC8vZ0E3UTFKRlFWUlBVam9nWjJRdGFuQmxaeUIyTVM0d0lDaDFjMmx1WnlCSlNrY2dTbEJGUnlCMk9UQXBMQ0J4ZFdGc2FYUjVJRDBnT1RBSy85c0FRd0FEQWdJREFnSURBd01EQkFNREJBVUlCUVVFQkFVS0J3Y0dDQXdLREF3TENnc0xEUTRTRUEwT0VRNExDeEFXRUJFVEZCVVZGUXdQRnhnV0ZCZ1NGQlVVLzlzQVF3RURCQVFGQkFVSkJRVUpGQTBMRFJRVUZCUVVGQlFVRkJRVUZCUVVGQlFVRkJRVUZCUVVGQlFVRkJRVUZCUVVGQlFVRkJRVUZCUVVGQlFVRkJRVUZCUVUvOEFBRVFnQUdBQWdBd0VpQUFJUkFRTVJBZi9FQUI4QUFBRUZBUUVCQVFFQkFBQUFBQUFBQUFBQkFnTUVCUVlIQ0FrS0MvL0VBTFVRQUFJQkF3TUNCQU1GQlFRRUFBQUJmUUVDQXdBRUVRVVNJVEZCQmhOUllRY2ljUlF5Z1pHaENDTkNzY0VWVXRId0pETmljb0lKQ2hZWEdCa2FKU1luS0NrcU5EVTJOemc1T2tORVJVWkhTRWxLVTFSVlZsZFlXVnBqWkdWbVoyaHBhbk4wZFhaM2VIbDZnNFNGaG9lSWlZcVNrNVNWbHBlWW1acWlvNlNscHFlb3FhcXlzN1MxdHJlNHVickN3OFRGeHNmSXljclMwOVRWMXRmWTJkcmg0dVBrNWVibjZPbnE4Zkx6OVBYMjkvajUrdi9FQUI4QkFBTUJBUUVCQVFFQkFRRUFBQUFBQUFBQkFnTUVCUVlIQ0FrS0MvL0VBTFVSQUFJQkFnUUVBd1FIQlFRRUFBRUNkd0FCQWdNUkJBVWhNUVlTUVZFSFlYRVRJaktCQ0JSQ2thR3h3UWtqTTFMd0ZXSnkwUW9XSkRUaEpmRVhHQmthSmljb0tTbzFOamM0T1RwRFJFVkdSMGhKU2xOVVZWWlhXRmxhWTJSbFptZG9hV3B6ZEhWMmQzaDVlb0tEaElXR2g0aUppcEtUbEpXV2w1aVptcUtqcEtXbXA2aXBxckt6dExXMnQ3aTV1c0xEeE1YR3g4akp5dExUMU5YVzE5aloydUxqNU9YbTUranA2dkx6OVBYMjkvajUrdi9hQUF3REFRQUNFUU1SQUQ4QStFanBkeG9Gd2wwWTJDNTY0cjE3d0pIZjZyRkhkTEorNlBHMnZxU1g5bXZSOWIrSG9NMEtpZmFTSHgzcjU2c2RKdlBoanJjMWpOSHZ0MWJFZWF2TDhTc1p6Y3NXckcyT3djcVVVMXJjeVBHVnZwbW1TRjdvK1ZKakl6NjE0OTR0dVQ0amxXTzNPNUY0elhzWHhDOEY2aDR6a2p1bGpaVWJvb0ZlT2VNdkMrcStBcGxTZU5vMWZrWkZkdGFqWnR0bkRCT0s1V2o3bDFqOXBJYWI0VVd6aSs4QjFyNXQrSWZ4RHZOZnUwdTFmNXQzRkZGZUxsODVVYWxvUGRuME9JZDZXcFkwZjR3NmhvQzIwdDZxeVc2WU9DSzV2NHkvRVdINHBQRExGQ3NRakdPQlJSWDJGU2pDcXB6bHVqNTlTYmR6LzlrPSI+Cgk8L2ltYWdlPgo8L3N2Zz4= 32w" 
	 onload="this.onload=null;this.sizes=Math.ceil(this.getBoundingClientRect().width/window.innerWidth*100)+'vw';" 
	 sizes="1px" 
	 src="/media/1/test.jpg"
	 width="2048">

You can see all the image variations in the srcset attribute. The last item in that attribute is an inlined version of the blurred tiny version of the image. It's inlined so that it can be displayed immediately, no extra network request needed.

Notice that we set the sizes attribute to 1px. This will make the browser pick the blurred version by default. The little bit of JavaScript in onload is responsible for replacing the blurred version with the real version once it has been downloaded. A cool side effect of this is that if you make your browser window larger, it will try to download an image for the new size of the window. It wil show that bigger image as soon as it has been downloaded.

If you want to see an example of this behaviour, point your browser to this page in our docs.

The medialibrary makes it incredibly easy to get started with responsive images + progressive images loading. The only things you have to do is to use withResponsiveImages() when adding media and then ouput a media object in a Blade view. I hope this feature really can make a difference. I can't wait to see it being used in the wild.

New in v7: multi file downloads #

In the medialibrary every file is associated with a Media model. To download a single file you can do this in a controller:

public function download(Media $media) 
{
   return response()->download($media->getPath());
}

Because Media implements Laravel's Responsable interface you can also write that a bit shorter:

public function download(Media $media) 
{
   return $media;
}

Pretty sweet! But what if you want to download multiple files at once?

Medialibrary v7 includes a new ZipStreamResponse class that allows you to respond with a stream. Files will be zipped on the fly and you can even include files from multiple filesystems.

Let's take a look at an example on how to use ZipStreamResponse. We're going to create two routes. Visiting add-files will add some files to our medialibrary, visiting download-files will download them. I'm using routes here for demonstration purposes, in a real world app you'd probably use ZipStreamResponse in a controller.

Route::get('add-files', function() {
    //create a regular model
    $article = Article::create();

    // add a file to the downloads collection on the local disk
    $article
        ->addMedia($pathToAFile)
        ->toMediaCollection('downloads');

    // add a file to the downloads collection on the s3 disk
    $article
    ->addMedia($pathToABigFile)
    ->toMediaCollection('downloads', 's3');

    return 'files added!';
});

Route::get('download-files', function() {
    //get all files in the download collection
    $allMedia = Article::first()->getMedia('downloads');

    // download them in a streamed way, so no prob if your files are very large
    return ZipStreamResponse::create('my-files.zip')->addMedia($allMedia);
});

That last line is the most important one. It's kinda cool that the zip is created on the fly and that it pulls data from both the local disk and s3.

Coding ZipStreamResponse up was easier than I thought it would be. The maennchen/zipstream-php does the hard work of creating a zip stream. All I need to do was to integrate the provided ZipStream class in our medialibrary. Here's the entire source code of Spatie\MediaLibrary\ZipStreamResponse:

namespace Spatie\MediaLibrary;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Contracts\Support\Responsable;
use Spatie\MediaLibrary\Media;
use Symfony\Component\HttpFoundation\StreamedResponse;
use ZipStream\ZipStream;

class ZipStreamResponse implements Responsable
{
    /** string */
    protected $zipName;

    /** Illuminate\Support\Collection */
    protected $mediaItems;

    public static function create(string $zipName)
    {
        return new static($zipName);
    }

    public function __construct(string $zipName)
    {
        $this->zipName = $zipName;
    }

    public function addMedia($mediaItems)
    {
        $this->mediaItems = $mediaItems;

        return $this;
    }

    public function toResponse($request)
    {
        return new StreamedZipResponse(function () {
            $zip = new ZipStream($this->zipName);
            
            $this->mediaItems->each(function (Media $media) use ($zip) {
                $zip->addFileFromStream($media->file_name, $media->stream());
            });
            
            $zip->finish();
        });
    }
}

Of course there are situations (eg. when the same assets get downloaded over and over again, or when download speed is very important) where you still want to create a zip file locally and store it for later use. But I believe StreamedZipResponse does provide a good solution for the proverbial 80%.

New in v7: supercharged media collections #

Media collections already exist in the current version of the medialibrary. They allow you to put different types of files in their own collection.

Let's associate some media:

$yourModel
   ->addMedia($pathToImage)
   ->toMediaCollection('images');

$yourModel
   ->addMedia($pathToAnotherImage)
   ->toMediaCollection('images');
   
$newsItem
   ->addMedia($pathToPdfFile)
   ->toMediaCollection('downloads');
   
$newsItem
  ->addMedia($pathToAnExcelFile)
  ->toMediaCollection('downloads');

All media in a specific collection can be retrieved like this:

// will return media instances for all files in the images collection
$yourModel->getMedia('images');

// will return media instances for all files in the downloads collection
$yourModel->getMedia('downloads');

In v7 a media collection can be more than just a name to group files. By defining a media collection in your model you can add certain behaviours to collections.

To get started with media collections add a function called registerMediaCollections to your prepared model. Inside that function you can use addMediaCollection to start a media collection.

// in your model

public function registerMediaCollections()
{
    $this->addMediaCollection('my-collection')
        //add options
        ...

    // you can define as much collections as needed
    $this->addMediaCollection('my-other-collection')
        //add options
        ...
}

You can pass a callback to acceptsFile that will check if a file is allowed into the collection. In this example we only accept jpeg files:

use Spatie\MediaLibrary\File;
...
public function registerMediaCollections()
{
    $this
        ->addMediaCollection('only-jpegs-please')
        ->acceptsFile(function (File $file) {
            return $file->mimeType === 'image/jpeg';
        });
}

This will succeed:

$yourModel
  ->addMedia('beautiful.jpg')
  ->toMediaCollection('only-jpegs-please');

This will throw a Spatie\MediaLibrary\Exceptions\FileCannotBeAdded\FileUnacceptableForCollection exception:

$yourModel
   ->addMedia('ugly.ppt')
   ->toMediaCollection('only-jpegs-please');

You can use media collections to ensure that files added to a collection are automatically added to a certain disk.

// in your model

public function registerMediaCollections()
{
    $this
       ->addMediaCollection('big-files')
       ->useDisk('s3');
}

When adding a file to my-collection it will be stored on the s3 disk.

$yourModel->addMedia($pathToFile)->toMediaCollection('big-files');

You can still specify the disk name manually when adding media. In this example the file will be stored on alternative-disk instead of s3.

$yourModel
   ->addMedia($pathToFile)
   ->toMediaCollection('big-files', 'alternative-disk');

If you want a collection to hold only one file you can use singleFile on the collection. A good use case for this would be an avatar collection on a User model. In most cases you'd want to have a user to only have one avatar.

// in your model

public function registerMediaCollections()
{
    $this
        ->addMediaCollection('avatar')
        ->singleFile();
}

The first time you add a file to the collection it will be stored as usual.

$yourModel
   ->add($pathToImage)
   ->toMediaCollection('avatar');
   
$yourModel->getMedia('avatar')->count(); // returns 1

$yourModel->getFirstUrl('avatar'); // will return an url to the `$pathToImage` file

When adding another file to a single file collection the first one will be deleted.

// this will remove other files in the collection
$yourModel
   ->add($anotherPathToImage)
   ->toMediaCollection('avatar');
   
$yourModel->getMedia('avatar')->count(); // returns 1

$yourModel->getFirstUrl('avatar'); // will return an url to the `$anotherPathToImage` file

In the general introduction I've mentioned that you can use registerMediaConversions to define media conversions. However, images conversions can also be registered inside media collections.

public function registerMediaCollections()
{
    $this
        ->addMediaCollection('my-collection')
        ->registerMediaConversions(function (Media $media) {
            $this
                ->addMediaConversion('thumb')
                ->width(100)
                ->height(100);
        });
}

When adding an image to my-collection a thumbnail that fits inside 100x100 will be created.

$yourModel->add($pathToImage)->toMediaCollection('my-collection');

$yourModel->getFirstMediaUrl('thumb') // returns an url to a 100x100 version of the added image.

In closing #

In addition to the features described above we did some refactoring of the code, improved the testsuite and polished some functionality:

  • files will now be lowercased when adding them to the medialibrary.
  • for seo purposes converted images will now included the filename of the original file in their filename.
  • move and copy methods have been added to Media so you can easily move media between models.

There are a lot of features not touched upon in this blogpost, such as image generators, using a custom directory structures, optimizing images, using your own model, ... Head over to the documentation of the package to learn it all.

In the next couple of months we're planning to add some Vue components to the package. These components will make it easy to upload stuff into the medialibrary. We'll also add components that are able to administer uploaded media in a collection.

Originally these components were going to be part of v7. We need more time to finish them. Probably we'll be able to add the components without having to introduce breaking changes.

I hope you liked this rundown of the new features in v7. Personally I can't wait to use it in our new projects.

Medialibrary is not the only package our team at Spatie has made. Be sure to check out all other packages listed on our company site. Don't forget to send us a postcard if any of our stuff makes in into your production environment.