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.
- PHP 8.1+
- Laravel 11.0+
spatie/image
for image conversionsffmpeg
&pbmedia/laravel-ffmpeg
for video/audio processing
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"
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'
]
)
];
}
}
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 usingspatie/image
.MediaConversionVideo
: This conversion optimizes, resizes, or converts any video usingpbmedia/laravel-ffmpeg
.MediaConversionAudio
: This conversion optimizes, resizes, converts or extract any audio usingpbmedia/laravel-ffmpeg
.MediaConversionPoster
: This conversion extracts a poster usingpbmedia/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
),
]
)
];
}
}
Add media to your model from various sources:
public function store(Request $request, Channel $channel)
{
$channel->addMedia(
file: $request->file('avatar'),
collectionName: 'avatar',
name: "{$channel->name}-avatar"
);
}
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"
);
}
}
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');
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();
// ...
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');
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
/>
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.
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
);
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 ofTemporaryDirectory
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.
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.
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;
// ...
}
composer test
Please see the CHANGELOG for more information on recent changes.
Feel free to open an issue or a discussion.
Please contact me to report security vulnerabilities.
The MIT License (MIT). Please see the License File for more information.