donia-shaker / media-library
Requires
- intervention/image: ^3.6
- ivoglent/ffmpeg-composer-bin: ^1.3
- laravel/framework: >=10.0.0 <13.0.0
Requires (Dev)
- laravel/pint: ^1.15
- orchestra/testbench: ^9.0
- phpunit/phpunit: ^11.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
This package provides a set of functions for handling media files, including images, audio, video, PDF files, and other documents.
- Media Library
Features
- Public and Private Storage: Store media in publicly accessible storage or protected private storage.
- Image Conversion: Convert and store images in multiple formats.
- Automatic Thumbnail Generation: Automatically generate thumbnails when enabled.
- Universal File Storage: Store images, documents, PDFs, videos, audio files, and other file types.
- Soft Delete Support: Restore accidentally deleted media records when needed.
- Public and Protected URL Generation: Generate direct URLs for public media and signed authenticated URLs for private media.
- Image Upload Rules: Apply customizable dimensions, size, and format validation rules.
- Authentication Support: Protect private media using configurable Laravel authentication guards.
- Frontend Independent: Private media can be consumed from Vue, React, Angular, mobile applications, or any client capable of sending authenticated HTTP requests.
Installation
To install the DoniaShaker\MediaLibrary package, follow these steps.
1. Install the package
composer require donia-shaker/media-library
2. Publish migrations and config
php artisan vendor:publish --tag=media-library-migrations php artisan vendor:publish --tag=media-library-config
3. Configure the default image format
Edit:
config/media.php
Example:
'default_image_format' => 'webp',
4. Configure environment variables
Required: Set APP_URL in .env to your project's actual base URL, including http:// or https://. The library uses this value to generate media URLs, so an incorrect value will produce incorrect file links.
APP_URL=https://your-project.com MEDIA_USE_STORAGE=true MEDIA_CREATE_THUMBNAILS=true
For local development, use your project's local URL, for example APP_URL=http://localhost/my-project. Do not include /media or /storage/media; the library adds the media path automatically.
Private media uses the sanctum authentication guard by default.
If your project uses another authentication guard, such as JWT with an api guard:
MEDIA_PRIVATE_AUTH_GUARD=api
5. Run migrations
php artisan migrate
Usage
Initialization
Import the controller:
use DoniaShaker\MediaLibrary\MediaController;
Create an instance:
$media_controller = new MediaController();
The $format parameter can be null. In this case, the package uses the configured default_image_format.
Save Image
Saves an image, optionally resizes it, controls image quality, and generates a thumbnail when enabled.
$media_controller->saveImage($model, $model_id, $file, $format, $maxWidth, $maxHeight, $quality);
$format, $maxWidth, $maxHeight, and $quality are optional.
Queued Image Processing
Existing installations keep processing images immediately. Queued processing is optional and does not require changes to calls to saveImage() or convertTempImage(), or new media migrations.
Enable it in .env:
MEDIA_QUEUE_IMAGES=true QUEUE_CONNECTION=database
The library uses the application's connection from config/queue.php, including database, redis, and other Laravel queue connections. Prepare that connection as usual (for example, the jobs table for database, or a configured Redis connection). It does not create or replace the project's queue configuration.
You can optionally select a different connection and queue name in config/media.php:
'queueImages' => env('MEDIA_QUEUE_IMAGES', false), 'queueConnection' => env('MEDIA_QUEUE_CONNECTION'), // null: use queue.default 'queueName' => env('MEDIA_QUEUE_NAME'), // null: use the connection's queue
For example:
MEDIA_QUEUE_IMAGES=true MEDIA_QUEUE_CONNECTION=redis MEDIA_QUEUE_NAME=media-images
Run a worker for the selected connection and queue:
php artisan queue:work redis --queue=media-images --tries=3
When using the application's default connection and queue, run php artisan queue:work --tries=3. Refresh cached configuration after changing these settings if your application uses config:cache.
With a queued connection, the HTTP request stores the source and a media record, then dispatches ProcessImage after the database transaction commits. The worker resizes the image, converts it to WebP using the existing image component, and creates the thumbnail when enabled. Uploaded files and local paths are copied without decoding; already decoded in-memory images are staged as PNG to preserve their contents.
The returned array keeps its existing keys. Image paths refer to the output that the worker will create, so they become available when processing finishes; the returned thumbnail is initially null. The media record has is_active = 0 until processing succeeds, then is_active = 1 and the appropriate has_thumb value. Public and private storage follow the existing visibility settings.
The source is stored under media.privatePath/processing, outside public media directories, and retained between retry attempts. When Laravel marks the job as finally failed (for example, after exhausting --tries=3), its failed() callback saves the original image bytes without resizing, conversion, or thumbnail generation. It updates the same media record to the source's actual format, sets is_active = 1 and has_thumb = 0, and removes any incomplete processed output. The staged source is retained to allow conversion to be retried later. Read the updated media->url to get the original image's URL, since its extension may differ from the initially returned WebP path. If the source is corrupt or even copying fails, the record remains inactive, the source is retained, and the fallback error is reported. HTTP and queue workers must share access to the configured media paths.
After fixing the conversion error, retry failed jobs using Laravel's existing commands:
php artisan queue:failed php artisan queue:retry --queue=media-images
Replace media-images with your configured queue name. To retry just one job, use php artisan queue:retry <job-id>. Failed-job storage must be enabled in the project's queue configuration, and the worker for the selected connection and queue must be running. When deploying a code fix to long-running workers, restart them using the project's normal deployment process (php artisan queue:restart for standard workers).
Retried jobs use their original dimensions, quality, visibility, target format, connection, and queue. A retry converts the retained source and updates the same media record; the published original remains available until conversion succeeds. After success, the job removes the staged source and the previous original file when its path differs from the converted output. Repeated failure preserves the original and source for another retry. Older JPEG/PNG fallback jobs whose staged source was already deleted can recover it from the published original. If you permanently discard failed jobs instead of retrying them, remove their unused staged source files too. Retries are explicit; fixing code does not automatically requeue failed jobs.
QUEUE_CONNECTION=sync executes inside the current request and provides no background processing. The library preserves immediate processing for connections whose driver is sync (including custom connection names) or null, even when MEDIA_QUEUE_IMAGES=true. With MEDIA_QUEUE_IMAGES=false (the default), existing behavior remains unchanged regardless of the project's queue connection. See Laravel's queue documentation for connection and worker setup.
Create Temporary Image
Creates a temporary image.
$media_controller->saveTempImage($model, $model_id, $file);
Convert Temporary Image
Converts a temporary image into a normal media file and associates it with a model.
$media_controller->convertTempImage($model, $model_id, $media->id);
Delete Temporary Image
Deletes a temporary image and its media record.
$media_controller->deleteTemp();
Save Audio File
Saves an audio file and associates it with a model.
$media_controller->audio($model, $model_id, $file);
Save Video File
Saves a video file and associates it with a model.
$media_controller->video($model, $model_id, $file);
Save Document File
Saves a document or other uploaded file and associates it with a model.
$media_controller->uploadFile($model, $model_id, $file);
Private Media
Media can be stored as either:
public
or:
private
The default visibility is:
public
Existing package usage therefore remains backward compatible.
Private files are stored outside the publicly accessible storage directory and are served through an authenticated signed route.
To save private media, pass:
'private'
as the visibility parameter.
Save Private Image
$media_controller->saveImage($model, $model_id, $file, $format, $maxWidth, $maxHeight, $quality, 'private');
Example:
$media_controller->saveImage('order', $order->id, $file, null, 1600, 1600, 75, 'private');
Save Private Document
$media_controller->uploadFile($model, $model_id, $file, 'private');
Example:
$media_controller->uploadFile('order_receipt_invoice', $order->id, $file, 'private');
Save Private Audio
$media_controller->audio($model, $model_id, $file, 'private');
Save Private Video
$media_controller->video($model, $model_id, $file, 'private');
Private Media Authentication Guards
Private media authentication is controlled by the configured authentication guards.
By default, the package uses the sanctum guard:
'privateAuthGuards' => env('MEDIA_PRIVATE_AUTH_GUARDS', 'sanctum'),
If the application already uses Sanctum, no additional configuration is required.
Single Guard
For a JWT-based application using the api guard:
MEDIA_PRIVATE_AUTH_GUARDS=api
For another custom guard:
MEDIA_PRIVATE_AUTH_GUARDS=delegate
Multiple Guards
The package also supports multiple authentication guards.
Configure them as a comma-separated list:
MEDIA_PRIVATE_AUTH_GUARDS=sanctum,api,delegate
The package checks the configured guards in order and uses the first guard that has an authenticated user.
Conceptually:
$guards = explode( ',', config('media.privateAuthGuards', 'sanctum') ); foreach ($guards as $guard) { $user = Auth::guard(trim($guard))->user(); if ($user) { return $user; } }
This allows the same media library to work with applications that have multiple authentication systems, such as:
sanctum
api
admin
delegate
The frontend does not need to know which guard is being used.
The frontend only needs to send the authentication credentials required by the application, such as a Bearer token or the configured authentication mechanism.
The package handles guard resolution internally.
Private Media URL
Private media URLs are generated automatically from the url attribute.
$media->url;
Example response:
https://example.com/api/media/private/10?auth_id=1&signature=...
The package automatically generates:
- Media ID
- Authenticated user ID
- Signed URL signature
The application should not manually create or modify auth_id or signature.
For example:
'image' => $media->url,
may return:
{
"image": "https://example.com/api/media/private/10?auth_id=1&signature=..."
}
The signed URL is bound to the authenticated user for whom it was generated.
When the private media endpoint is requested, the package verifies:
- The signed URL is valid.
- The media record is private.
- The request contains a valid authenticated user.
- The authenticated user's ID matches the signed
auth_id. - The physical private file exists.
If any verification fails, the endpoint returns:
404 Not Found
Access Private Media From Frontend
Bearer Token Authentication
When using JWT, Sanctum API tokens, or another Bearer Token authentication system, private media must be requested with:
Authorization: Bearer YOUR_TOKEN
Do not directly use the private URL like this with JWT:
<img src="PRIVATE_MEDIA_URL">
A normal <img> request does not automatically attach a Bearer Token.
Instead, request the private URL through an authenticated HTTP request.
JavaScript Fetch
const response = await fetch(media.url, { headers: { Authorization: `Bearer ${token}` } });
Convert the response to a Blob:
const blob = await response.blob();
Create a browser URL:
const imageUrl = URL.createObjectURL(blob);
Display it:
<img src="BLOB_URL">
Complete example:
const response = await fetch(media.url, { headers: { Authorization: `Bearer ${token}` } }); const blob = await response.blob(); const imageUrl = URL.createObjectURL(blob);
Vue
const response = await fetch(media.url, { headers: { Authorization: `Bearer ${token}` } }); const blob = await response.blob(); imageUrl.value = URL.createObjectURL(blob);
Then:
<img :src="imageUrl">
React
const response = await fetch(media.url, { headers: { Authorization: `Bearer ${token}` } }); const blob = await response.blob(); const imageUrl = URL.createObjectURL(blob);
Then:
<img src={imageUrl} alt="" />
Axios
const response = await axios.get(media.url, { headers: { Authorization: `Bearer ${token}` }, responseType: 'blob' });
Then:
const imageUrl = URL.createObjectURL(response.data);
Authenticated API Client
If the application already has an HTTP client that automatically sends the authentication token, the token does not need to be manually added again.
Example:
const blob = await $api(media.url, { responseType: "blob" });
Then:
const imageUrl = URL.createObjectURL(blob);
The important requirement is that the request reaching the private media endpoint contains the application's authentication credentials.
For JWT or Bearer Token authentication, the request must contain:
Authorization: Bearer YOUR_TOKEN
Private Thumbnail
Private thumbnails are also returned as protected signed URLs.
Example:
$media->thumb_url;
A private thumbnail may return:
https://example.com/api/media/private/10?auth_id=1&thumb=1&signature=...
It should be requested using the same authenticated process as the original private media file.
Example:
const response = await fetch(media.thumb_url, { headers: { Authorization: `Bearer ${token}` } }); const blob = await response.blob(); const thumbnailUrl = URL.createObjectURL(blob);
Public vs Private Media
Public Media
Public media can be displayed directly.
<img :src="media.url">
Example URL:
https://example.com/storage/media/images/order/10-image.webp
No authenticated media request is required.
Private Media
Private media is accessed through a protected API endpoint.
media.url
↓
Authenticated HTTP Request
↓
Authorization: Bearer TOKEN
↓
Signed URL Validation
↓
User Authentication
↓
User Identity Validation
↓
Private File
↓
Blob
↓
<img>
In short:
Public -> Use media.url directly
Private -> Request media.url with authentication
Media Object
Explanation of the media object properties:
id: The unique identifier of the media object.model: The model associated with the media object.format: The file format.model_id: The ID of the associated model.order: The order of the media object when a model has multiple media files.file_name: The unique file name.has_thumb: Indicates whether a thumbnail exists.is_active: Indicates whether the media is active.is_temp: Indicates whether the media is temporary.visibility: Defines whether the media ispublicorprivate.deleted_at: The media deletion timestamp.created_at: The media creation timestamp.updated_at: The media last update timestamp.url: Public URL for public media or signed authenticated URL for private media.thumb_url: Public thumbnail URL or protected signed thumbnail URL when available.
Example:
{
"id": 10,
"model": "order",
"model_id": 500,
"format": "webp",
"visibility": "private",
"url": "https://example.com/api/media/private/10?auth_id=1&signature=...",
"thumb_url": "https://example.com/api/media/private/10?auth_id=1&thumb=1&signature=..."
}
Rules
Square Image
Use SquareImageRule to validate that an uploaded image is square.
use DoniaShaker\MediaLibrary\Rules\SquareImageRule;
Usage:
'file' => [..., new SquareImageRule],
Troubleshooting and Collaboration
If you encounter any issues or have suggestions, please feel free to open an issue on GitHub.