elegantly/laravel-media

A flexible media library for Laravel


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

This package offers an extremely flexible media library, enabling you to store any type of file along with their conversions.

It provides advanced features such as:

  • 🌐 Supports any filesystem solutions (local or cloud), such as S3, R2, Bunny.net, DO...
  • ⚡ Supports any file conversion solutions (local or cloud), such as ffmpeg, Transloadit, Cloudflare, Coconut, and others.
  • 🔄 Advanced nested media conversions
  • 🚀 Rich metadata automatically extracted
  • 🛠️ Highly flexible and customizable

I developed this package with the highest degree of flexibility possible and I have been using it in production for nearly a year, handling terabytes of files monthly.

Table of Contents

  1. Requirements

  2. Installation

  3. Basic Usage

  4. Advanced Usage

  5. Customization

Requirements

  • PHP 8.1+
  • Laravel 11.0+
  • spatie/image for image conversions
  • ffmpeg & pbmedia/laravel-ffmpeg for video/audio processing

Installation

You can install the package via composer:

composer require elegantly/laravel-media

You have to publish and run the migrations with:

php artisan vendor:publish --tag="laravel-media-migrations"
php artisan migrate

You can publish the config file with:

php artisan vendor:publish --tag="laravel-media-config"

This is the contents of the published config file:

use Elegantly\Media\Jobs\DeleteModelMediaJob;
use Elegantly\Media\Models\Media;

return [
    /**
     * The media model
     * Define your own model here by extending \Elegantly\Media\Models\Media::class
     */
    'model' => Media::class,

    /**
     * The path used to store temporary file copy for conversions
     * This will be used with storage_path() function
     */
    'temporary_storage_path' => 'app/tmp/media',

    /**
     * The default disk used for storing files
     */
    'disk' => env('MEDIA_DISK', env('FILESYSTEM_DISK', 'local')),

    /**
     * Determine if media should be deleted with the model
     * when using the HasMedia Trait
     */
    'delete_media_with_model' => true,

    /**
     * Determine if media should be deleted with the model
     * when it is soft deleted
     */
    'delete_media_with_trashed_model' => false,

    /**
     * Deleting a large number of media attached to a model can be time-consuming
     * or even fail (e.g., cloud API error, permissions, etc.)
     * For performance and monitoring, when a model with the HasMedia trait is deleted,
     * each media is individually deleted inside a job.
     */
    'delete_media_with_model_job' => DeleteModelMediaJob::class,

    /**
     * The default collection name
     */
    'default_collection_name' => 'default',

    /**
     * Prefix for the generated path of files
     * Set to null if you do not want any prefix
     * To fully customize the generated default path, extend the Media model and override the generateBasePath method
     */
    'generated_path_prefix' => null,

    /**
     * Customize the queue connection used when dispatching conversion jobs
     */
    'queue_connection' => env('QUEUE_CONNECTION', 'sync'),

    /**
     * Customize the queue used when dispatching conversion jobs
     * null will fall back to the default Laravel queue
     */
    'queue' => null,

];

Optionally, you can publish the views using

php artisan vendor:publish --tag="laravel-media-views"

Basic Usage

Defining Media Collections

Media Collections define how media are stored, transformed, and processed for a specific model. They provide granular control over file handling, accepted types, and transformations.

To associate a media collection with a Model, start by adding the InteractWithMedia interface and the HasMedia trait.

Next, define your collections in the registerMediaCollections method, as shown below:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Elegantly\Media\Concerns\HasMedia;
use Elegantly\Media\Contracts\InteractWithMedia;
use Elegantly\Media\MediaCollection;

class Channel extends Model implements InteractWithMedia
{
    use HasMedia;

    public function registerMediaCollections(): array;
    {
        return [
            new MediaCollection(
                name: 'avatar',
                single: true, // If true, only the latest file will be kept
                disk: 's3', // (optional) Specify where the file will be stored
                acceptedMimeTypes: [ // (optional) Specify accepted file types
                    'image/jpeg',
                    'image/png',
                    'image/webp'
                ]
            )
        ];
    }
}

Defining Media Conversions

Media conversions create different variants of your media files. For example, a 720p version of a 1440p video or a WebP or PNG version of an image are common types of media conversions. Interestingly, a media conversion can also have its own additional conversions.

This package provides common converions to simplify your work:

  • MediaConversionImage: This conversion optimizes, resizes, or converts any image using spatie/image.
  • MediaConversionVideo: This conversion optimizes, resizes, or converts any video using pbmedia/laravel-ffmpeg.
  • MediaConversionAudio: This conversion optimizes, resizes, converts or extract any audio using pbmedia/laravel-ffmpeg.
  • MediaConversionPoster: This conversion extracts a poster using pbmedia/laravel-ffmpeg.
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Elegantly\Media\Concerns\HasMedia;
use Elegantly\Media\Contracts\InteractWithMedia;
use Elegantly\Media\MediaCollection;
use Elegantly\Media\Definitions\MediaConversionImage;

class Channel extends Model implements InteractWithMedia
{
    use HasMedia;

    public function registerMediaCollections(): array;
    {
        return [
            new MediaCollection(
                name: 'videos',
                conversions: [
                    new MediaConversionPoster(
                        name: 'poster',
                        conversions: [
                            new MediaConversionImage(
                                name: '360p',
                                width: 360
                            ),
                        ],
                    ),
                    new MediaConversionVideo(
                        name: '720p',
                        width: 720
                    ),
                ]
            )
        ];
    }
}

Adding Media

Add media to your model from various sources:

From a Controller

public function store(Request $request, Channel $channel)
{
    $channel->addMedia(
        file: $request->file('avatar'),
        collectionName: 'avatar',
        name: "{$channel->name}-avatar"
    );
}

From a Livewire Component

use Livewire\WithFileUploads;

class ImageUploader extends Component
{
    use WithFileUploads;

    public function save()
    {
        $this->channel->addMedia(
            file: $this->avatar->getRealPath(),
            collectionName: 'avatar',
            name: "{$this->channel->name}-avatar"
        );
    }
}

Retreiving Media

Retrieve media from your model:

// Get all media from a specific collection
$avatars = $channel->getMedia('avatar');

// Get the first media from a collection
$avatar = $channel->getFirstMedia('avatar');

// Check if media exists
$hasAvatar = $channel->hasMedia('avatar');

Media properties

Each media item provides rich metadata automatically:

$media = $channel->getFirstMedia('avatar');

// File properties
$media->name; // file_name without the extension
$media->file_name;
$media->extension;
$media->mime_type;
$media->size; // in bytes
$media->humanReadableSize();

// Image/Video specific properties
$media->width;       // in pixels
$media->height;      // in pixels
$media->aspect_ratio;
$media->duration;    // for video/audio

You can use dot notation to access either the root properties or a specific conversion:

// Get the original media URL
$originalUrl = $media->getUrl();

// Get a specific conversion URL
$thumbnailUrl = $media->getUrl(
    conversion: '360p',
    fallback: true // Falls back to original if conversion doesn't exist
);

// Use the same logic with other properties such as
$media->getPath();
$media->getWith();
// ...

Access Media Conversions

To directly access conversions, use:

// Check if a conversion exists
$hasThumbnail = $media->hasConversion('avatar.100p');

// Get a specific conversion
$thumbnailConversion = $media->getConversion('avatar.100p');

// Get the 'avatar' conversion
$media->getParentConversion('avatar.360p');

// Only get children conversions of avatar
$media->getChildrenConversions('avatar');

Blade components

The package also provides blade components.

<!-- fallback to the root media url if the conversion doesn't exist -->
<!-- allows you to specify query parameters -->
<x-media::img
    :media="$user->getFirstMedia('avatar')"
    conversion="avatar"
    fallback
    parameters="['foo'=>'bar']"
    alt="User avatar"
/>
<!-- fallback to the root media url if the conversion doesn't exist -->
<!-- allows you to specify query parameters -->
<x-media::video
    :media="$user->getFirstMedia('videos')"
    conversion="720"
    fallback
    muted
    playsinline
    autoplay
    loop
/>

Advanced Usage

Async vs. Sync Conversions

When adding new media, its conversions can be either dispatched asynchronously or generated synchronously.

You can configure the strategy in the conversion definition using the queued and queue parameters:

new MediaCollection(
    name: 'avatar',
    conversions: [
        new MediaConversionImage(
            name: '360',
            width: 360,
            queued: true,  // (default) Dispatch as a background job
            queue: 'slow' // (optional) Specify a custom queue
        )
    ]
)

Synchronous conversions can be particularly useful in specific use cases, such as generating a poster immediately upon upload.

Delayed Conversions

There are scenarios where you might want to define conversions that should not be generated immediately. For instance, if a conversion is resource-intensive or not always required, you can defer its generation to a later time.

To achieve this, configure the conversion with the immediate parameter set to false. This allows you to generate the conversion manually when needed:

new MediaCollection(
    name: 'avatar',
    conversions: [
        new MediaConversionImage(
            name: '360',
            width: 360,
            immediate: false, // Conversion will not be generated at upload time
        )
    ]
)

To generate the conversion later, you can use the following methods:

// Generate the conversion synchronously
$media->executeConversion(
    conversion: '360',
    force: false // Skips execution if the conversion already exists
);

// Dispatch the conversion as a background job
$media->dispatchConversion(
    conversion: '360',
    force: false // Skips execution if the conversion already exists
);

Custom Conversions

Conversions can be anything—a variant of a file, a transcription of a video, a completely new file, or even just a string.

You can use built-in presets or define your own custom conversion. To create a custom conversion, use the MediaConversionDefinition class:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Elegantly\Media\Concerns\HasMedia;
use Elegantly\Media\Contracts\InteractWithMedia;
use Elegantly\Media\MediaCollection;
use Elegantly\Media\Definitions\MediaConversionDefinition;

class Channel extends Model implements InteractWithMedia
{
    use HasMedia;

    public function registerMediaCollections(): array
    {
        return [
            new MediaCollection(
                name: 'videos',
                conversions: [
                    // Using a custom conversion definition
                    new MediaConversionDefinition(
                        name: 'webp',
                        when: fn($media, $parent) => $media->type === MediaType::Image,
                        handle: function($media, $parent, $file, $filesystem, $temporaryDirectory) {
                            $target = $filesystem->path("{$media->name}.webp");

                            Image::load($filesystem->path($file))
                                ->optimize($this->optimizerChain)
                                ->save($target);

                            return $media->addConversion(
                                file: $target,
                                conversionName: $this->name,
                                parent: $parent,
                            );
                        }
                    ),
                ]
            ),
        ];
    }
}

The handle method of MediaConversionDefinition is where the logic for the conversion is implemented. It provides the following parameters:

  • $media: The Media model.
  • $parent: The MediaConversion model, if the conversion is nested.
  • $file: A local copy of the file associated with either $media or $parent.
  • $filesystem: An instance of the local filesystem where the file copy is stored.
  • $temporaryDirectory: An instance of TemporaryDirectory where the file copy is temporarily stored.

You don’t need to worry about cleaning up the files, as the $temporaryDirectory will be deleted automatically when the process completes.

To finalize the conversion, ensure you save it by calling $media->addConversion or returning a MediaConversion instance at the end of the handle method.

Manually Generate Conversions

You can manage your media conversions programmatically using the following methods:

// Store a new file as a conversion
$media->addConversion(
    file: $file, // Can be an HTTP File, URL, or file path
    conversionName: 'transcript',
    parent: $mediaConversion // (Optional) Specify a parent conversion
    // Additional parameters...
);

// Replace an existing conversion safely
$media->replaceConversion(
    conversion: $mediaConversion
);

// Safely delete a specific conversion and all its children
$media->deleteConversion('360');

// Safely delete only the child conversions of a parent conversion
$media->deleteChildrenConversions('360');

// Dispatch or execute a conversion
$media->dispatchConversion('360'); // Runs asynchronously as a job
$media->executeConversion('360'); // Executes synchronously
$media->getOrExecuteConversion('360'); // Retrieves or generates the conversion

// Retrieve conversion information
$media->getConversion('360'); // Fetch a specific conversion
$media->hasConversion('360'); // Check if a conversion exists
$media->getParentConversion('360'); // Retrieve the parent of a conversion
$media->getChildrenConversions('360'); // Retrieve child conversions

Additionally, you can use an Artisan command to generate conversions with various options:

php artisan media:generate-conversions

This provides a convenient way to process conversions in bulk or automate them within your workflows.

Customization

Custom Media Model

You can define your own Media model to use with the library.

First, create your own model class:

namespace App\Models;

use Elegantly\Media\Models\Media as ElegantlyMedia;

class Media extends ElegantlyMedia
{
    // ...
}

Then, update the config file:

use App\Models\Media;

return [

    'model' => Media::class,

    // ...

];

The library is typed with generics, so you can use your own Media model seamlessly:

namespace App\Models;

use App\Models\Media;
use Elegantly\Media\Concerns\HasMedia;
use Elegantly\Media\Contracts\InteractWithMedia;

/**
 * @implements InteractWithMedia<Media>
 */
class Post extends Model implements InteractWithMedia
{
    /** @use HasMedia<Media> **/
    use HasMedia;

    // ...
}

Testing

composer test

Changelog

Please see the CHANGELOG for more information on recent changes.

Contributing

Feel free to open an issue or a discussion.

Security Vulnerabilities

Please contact me to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see the License File for more information.