laravel-media maintained by jegex
jegex/laravel-media
Powerful media management package for Laravel applications. Handle file uploads, image conversions, responsive images, and video thumbnails with ease. Inspired by spatie/laravel-medialibrary.
Table of Contents
- Installation
- Quick Start
- Usage
- Image Conversions
- Responsive Images
- Queue System
- Vapor Uploads
- ZIP Export
- Facade Usage
- Configuration
- Environment Variables
- Advanced Usage
- Testing
- Changelog
- License
Installation
Install the package via Composer:
composer require jegex/laravel-media
Post-Installation Steps
Step 1: Publish Configuration & Migration
# Publish config file to config/media.php
php artisan vendor:publish --tag="media-config"
# Publish migration file
php artisan vendor:publish --tag="media-migrations"
Step 2: Run Migration
php artisan migrate
Step 4: Configure Storage Disk (Optional)
In config/media.php or your .env file, set the default storage disk:
MEDIA_DISK=public
Then create the symbolic link for public disk:
php artisan storage:link
Step 5: Configure Queue (Optional)
If you want image conversions to run asynchronously:
QUEUE_CONNECTION=redis
QUEUE_CONVERSIONS_BY_DEFAULT=true
QUEUE_CONVERSIONS_AFTER_DB_COMMIT=true
Quick Start
Add the HasMedia trait to your model:
use Jegex\Media\MediaCollections\Concerns\HasMedia;
class Post extends Model
{
use HasMedia;
// ...
}
Now you can attach media:
$post = Post::create(['title' => 'My Post']);
$post->addMedia('/path/to/image.jpg')
->toMediaCollection('images');
$post->getMedia('images')->first()->getUrl();
Usage
Adding Media via Model
Add the HasMedia trait to your model:
use Jegex\Media\MediaCollections\Concerns\HasMedia;
class Post extends Model
{
use HasMedia;
// ...
}
Now you can attach media:
$post = Post::create(['title' => 'My Post']);
$post->addMedia('/path/to/image.jpg')
->toMediaCollection('images');
$post->getMedia('images')->first()->getUrl();
Adding Media Directly (Standalone)
Since the model_type and model_id columns are nullable, you can create media without associating it to any model:
use Jegex\Media\MediaCollections\Models\Media;
// From a file path
$media = Media::createFromFile('/path/to/image.jpg');
// From string content
$media = Media::createFromString($imageContent, 'photo.jpg');
// From base64
$media = Media::createFromBase64($base64String, 'avatar.png');
// From a URL
$media = Media::createFromUrl('https://example.com/image.jpg');
// With custom options
$media = Media::createFromFile('/path/to/image.jpg', [
'collection_name' => 'uploads',
'name' => 'Custom Name',
'disk' => 's3',
'custom_properties' => ['source' => 'api'],
]);
// Access the media
echo $media->getUrl();
echo $media->toHtml();
This is useful for global media libraries, CDN assets, or when you don't need to associate media with a specific model.
Retrieving Media
// Get all media in a collection
$media = $model->getMedia('photos');
// Get first media
$media = $model->getFirstMedia('photos');
// Get media URL
$url = $media->getUrl();
// Get conversion URL
$url = $media->getUrl('thumb');
// Get temporary URL (S3)
$url = $media->getTemporaryUrl(now()->addMinutes(10));
Media Properties
$media->getName(); // 'my-image'
$media->getAltTxt(); // 'Alt text for accessibility'
$media->getCaption(); // 'Short caption'
$media->getDescription(); // 'Full description'
$media->file_name; // 'my-image.jpg'
$media->mime_type; // 'image/jpeg'
$media->size; // 102400 (bytes)
Custom Properties
$media->setCustomProperty('credits', 'John Doe');
$media->getCustomProperty('credits'); // 'John Doe'
$media->hasCustomProperty('credits'); // true
Rendering Media
// Convert to HTML string
echo $media->toHtml();
// <img src="..." alt="..." loading="lazy">
// Blade component
<x-media :media="$media" class="w-full" :loading="'lazy'" />
// With conversion
<x-media :media="$media" conversion="thumb" class="thumbnail" />
Image Conversions
Define conversions in your model:
use Jegex\Media\MediaCollections\Concerns\HasMedia;
class Post extends Model
{
use HasMedia;
public function registerMediaConversions(?Media $media = null): void
{
$this->addMediaConversion('thumb')
->width(200)
->height(200)
->optimize()
->queued();
$this->addMediaConversion('preview')
->width(800)
->height(600)
->optimize();
}
}
Retrieve converted images:
$thumbUrl = $media->getUrl('thumb');
$previewUrl = $media->getUrl('preview');
Conversion Methods
| Method | Description |
|---|---|
width($px) |
Set width |
height($px) |
Set height |
fit(Fit $fit) |
Set fit mode (contain, cover, fill, etc.) |
format($format) |
Set output format (webp, avif, jpg, png) |
quality($quality) |
Set output quality (1-100) |
brightness($value) |
Adjust brightness (-100 to 100) |
contrast($value) |
Adjust contrast |
blur($value) |
Apply blur effect |
gamma($value) |
Adjust gamma |
flip($direction) |
Flip image (h, v) |
optimize() |
Optimize output image |
queued() |
Process conversion on queue |
nonQueued() |
Process conversion synchronously |
manipulate($name, $value) |
Add custom manipulation |
Image Generators
The package supports multiple file types out of the box:
- GenericImage — JPEG, PNG, GIF, BMP
- Webp — WebP images
- Avif — AVIF images
- Pdf — PDF files (thumbnail extraction)
- Svg — SVG files
- Video — Video files (thumbnail extraction via FFMPEG)
Image Optimizers
Optimizers are applied automatically when optimize() is called:
| Format | Optimizer |
|---|---|
| JPEG | Jpegoptim |
| PNG | Pngquant, Optipng |
| SVG | Svgo |
| GIF | Gifsicle |
| WebP | Cwebp |
| AVIF | Avifenc |
Responsive Images
Responsive images are generated automatically for image media files.
// Enable responsive images for a collection
$model->addMedia('/path/to/image.jpg')
->withResponsiveImages()
->toMediaCollection('photos');
The package calculates optimal widths using FileSizeOptimizedWidthCalculator (30% smaller per variation) and generates a blurred tiny placeholder for progressive loading.
Queue System
Conversions and responsive images can be processed asynchronously.
Configuration
// config/media.php
'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'),
'queue_name' => env('MEDIA_QUEUE', ''),
'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true),
'queue_conversions_after_database_commit' => env('QUEUE_CONVERSIONS_AFTER_DB_COMMIT', true),
Per-Conversion Queue Setting
$this->addMediaConversion('thumb')
->width(200)
->queued(); // Process on queue
$this->addMediaConversion('preview')
->width(800)
->nonQueued(); // Process immediately
Jobs
| Job | Description |
|---|---|
PerformConversionsJob |
Processes image conversions |
GenerateResponsiveImagesJob |
Generates responsive image variations |
Vapor Uploads
For Laravel Vapor deployments, enable the upload route:
// config/media.php
'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false),
'vapor_route_prefix' => 'media-vapor',
'vapor_route_middleware' => ['web', 'auth'],
Routes
| Method | Route | Description |
|---|---|---|
| POST | /media-vapor |
Store new media from Vapor |
| POST | /media-vapor/finished/{mediaId} |
Mark media upload as finished |
| POST | /media-vapor/parameters |
Get upload parameters for S3 direct upload |
ZIP Export
Export media collections as ZIP archives for bulk downloads.
Export from Model
// Stream ZIP directly to browser
return $post->getMediaCollectionZip('photos')->download('photos.zip');
// Save ZIP to a disk
$zipPath = $post->getMediaCollectionZip('photos')->saveToDisk('public', 'exports/photos.zip');
Export from Single Media
use Jegex\Media\MediaCollections\Models\Media;
// Get ZIP for a single media item
$zip = $media->getZip('archive.zip');
// Or retrieve media by ID first
$media = Media::find(1);
$zip = $media->getZip('archive.zip');
Filter by Conversion
// Only include 'thumb' conversions in the ZIP
return $post->getMediaCollectionZip('photos', function ($zip, $media) {
$zip->add($media, 'thumb');
})->download('thumbs.zip');
The ZIP export uses maennchen/zipstream-php for memory-efficient streaming. Files are added directly to the stream without loading them entirely into memory.
Facade Usage
The package provides a LaravelMedia facade for convenient access to media operations without needing a model:
use Jegex\Media\Facades\LaravelMedia;
// Create media from file
$media = LaravelMedia::createFromFile('/path/to/image.jpg');
// Create media from string content
$media = LaravelMedia::createFromString($imageContent, 'photo.jpg');
// Create media from base64
$media = LaravelMedia::createFromBase64($base64String, 'avatar.png');
// Create media from URL
$media = LaravelMedia::createFromUrl('https://example.com/image.jpg');
// Retrieve media
$media = LaravelMedia::getMediaById(1);
$media = LaravelMedia::getMediaByIds([1, 2, 3]);
$collection = LaravelMedia::getMediaByCollection('avatars');
// Delete media
LaravelMedia::deleteMedia(1);
// Get package info
$maxSize = LaravelMedia::getMaxFileSize(); // 10485760 (10MB)
$defaultDisk = LaravelMedia::getDefaultDisk(); // 'public'
Configuration
The full configuration file (config/media.php):
return [
// Default storage disk
'disk_name' => env('MEDIA_DISK', 'public'),
// Maximum file size (10MB default)
'max_file_size' => 1024 * 1024 * 10,
// Queue settings
'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'),
'queue_name' => env('MEDIA_QUEUE', ''),
'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true),
'queue_conversions_after_database_commit' => env('QUEUE_CONVERSIONS_AFTER_DB_COMMIT', true),
// File naming and path generation
'file_namer' => DefaultFileNamer::class,
'path_generator' => DefaultPathGenerator::class,
'file_remover_class' => DefaultFileRemover::class,
'url_generator' => DefaultUrlGenerator::class,
// URL versioning
'version_urls' => false,
// Image driver: gd, imagick, vips
'image_driver' => env('IMAGE_DRIVER', 'gd'),
// FFMPEG settings
'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'),
'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'),
'ffmpeg_timeout' => env('FFMPEG_TIMEOUT', 900),
'ffmpeg_threads' => env('FFMPEG_THREADS', 0),
// Downloads
'media_downloader' => DefaultDownloader::class,
'media_downloader_ssl' => env('MEDIA_DOWNLOADER_SSL', true),
// Temporary URL lifetime (minutes)
'temporary_url_default_lifetime' => env('MEDIA_TEMPORARY_URL_DEFAULT_LIFETIME', 5),
// S3 upload headers
'remote' => [
'extra_headers' => [
'CacheControl' => 'max-age=604800',
],
],
// Responsive images
'responsive_images' => [
'width_calculator' => FileSizeOptimizedWidthCalculator::class,
'use_tiny_placeholders' => true,
'tiny_placeholder_generator' => Blurred::class,
],
// Loading attribute: 'lazy', 'eager', 'auto', or null
'default_loading_attribute_value' => null,
// Storage prefix
'prefix' => env('MEDIA_PREFIX', ''),
// Force lazy loading
'force_lazy_loading' => env('FORCE_MEDIA_LIBRARY_LAZY_LOADING', true),
// Vapor uploads
'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false),
'vapor_route_prefix' => 'media-vapor',
'vapor_route_middleware' => ['web', 'auth'],
];
Environment Variables
| Variable | Default | Description |
|---|---|---|
MEDIA_DISK |
public |
Default storage disk |
MEDIA_QUEUE |
'' |
Queue name |
QUEUE_CONNECTION |
sync |
Queue connection |
QUEUE_CONVERSIONS_BY_DEFAULT |
true |
Queue conversions by default |
QUEUE_CONVERSIONS_AFTER_DB_COMMIT |
true |
Run after database commit |
IMAGE_DRIVER |
gd |
Image processing driver |
FFMPEG_PATH |
/usr/bin/ffmpeg |
FFMPEG binary path |
FFPROBE_PATH |
/usr/bin/ffprobe |
FFProbe binary path |
FFMPEG_TIMEOUT |
900 |
FFMPEG timeout (seconds) |
FFMPEG_THREADS |
0 |
FFMPEG thread count |
MEDIA_DOWNLOADER_SSL |
true |
SSL verification for downloads |
MEDIA_TEMPORARY_URL_DEFAULT_LIFETIME |
5 |
Temporary URL lifetime (minutes) |
MEDIA_PREFIX |
'' |
Storage path prefix |
FORCE_MEDIA_LIBRARY_LAZY_LOADING |
true |
Force lazy loading |
ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS |
false |
Enable Vapor upload routes |
Advanced Usage
Custom Path Generator
Create a custom path generator:
namespace App\Support;
use Jegex\Media\Support\PathGenerator\DefaultPathGenerator;
use Jegex\Media\MediaCollections\Models\Media;
class CustomPathGenerator extends DefaultPathGenerator
{
public function getPath(Media $media): string
{
return $media->model_type.'/'.date('Y/m/d').'/'.$media->id;
}
}
Register it in config:
'path_generator' => App\Support\CustomPathGenerator::class,
Or per-model:
'custom_path_generators' => [
App\Models\Post::class => App\Support\PostPathGenerator::class,
],
Media Lifecycle Events
The MediaObserver handles:
- creating: Sets highest order number
- created: Dispatches conversion and responsive image jobs
- updating: Handles file renaming (if
moves_media_on_updateis true) - deleting: Removes all associated files from disk
Blade Component
{{-- Basic usage --}}
<x-media :media="$media" />
{{-- With custom class and loading --}}
<x-media :media="$media" class="w-full rounded-lg" loading="lazy" />
{{-- With conversion --}}
<x-media :media="$media" conversion="thumb" alt="Thumbnail" />
{{-- Override loading attribute --}}
<x-media :media="$media" :loading="null" />
Testing
composer test
Run with coverage:
composer test-coverage
Run a specific test:
vendor/bin/pest --filter="test name"
Changelog
Please see CHANGELOG for more information on what has changed recently.
License
The MIT License (MIT). Please see License File for more information.