jegex / laravel-media
Laravel media library — upload, image conversions, responsive images, video thumbnails, ZIP export, and more
Fund package maintenance!
Requires
- php: ^8.4
- illuminate/contracts: ^11.0||^12.0||^13.0
- maennchen/zipstream-php: ^3.1
- spatie/image: ^3.3.2
- spatie/laravel-package-tools: ^1.16
- spatie/temporary-directory: ^2.2
Requires (Dev)
- ext-imagick: *
- ext-pdo_sqlite: *
- ext-zip: *
- guzzlehttp/guzzle: ^7.8.1
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^10.0.0||^9.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
Suggests
- php-ffmpeg/php-ffmpeg: Required for video thumbnail generation via FFMPEG
This package is auto-updated.
Last update: 2026-05-13 22:39:34 UTC
README
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.