Media
This package provides a unit tested API to manage different types of files which can be related to Eloquent models (many to many relationships). Each type of file can be associated to its own resource for additional processing and manipulation. For instance, images are processed with the Image resource and manipulated with Intervention Image. API's are provides for creating your own resources.
Features
Media Eloquent Model and Migrations.
Mediable trait to relate any eloquent model to the Media model.
Resource API to add support for additional types of media.
Plugin API which adds optional features, like color extraction for images.
- Uses File Storage to manage files.
- Uses Intervention Image to manage and manipulate images.
- Fully tested with Pest at 100% code coverage.
Requirements
- PHP 8.3, 8.4, or 8.5
- Laravel 11, 12, or 13
- Intervention Image 2.x
- GD or Imagick PHP extension (plus
exif and fileinfo for image metadata)
Getting Started
Install via Composer
composer require actengage/media
The service provider, Media facade, and Resource facade are auto-discovered, and the package's migrations are loaded automatically — no extra setup is required to start storing media.
Optionally publish the config file
php artisan vendor:publish --tag=media-config
Optionally publish the migrations (only needed if you want to customize the schema; they are loaded automatically otherwise)
php artisan vendor:publish --tag=media-migrations
Resource Factory
The resource factory provides a unified interface for creating a variety of resources. You can even create your own resource classes and register them in config/media.php. By default, there are two types of resources: Image and File. For instance, you could define an Audio or Video resource. Resources are processed in the order they are defined.
use Actengage\Media\Facades\Resource;
use Illuminate\Http\UploadedFile;
$resource = Resource::path('some/file/path/image.jpeg');
$resource = Resource::request('image');
$resource = Resource::make(
new UploadedFile('some/file/path/image.jpeg', 'image.jpeg')
);
Resource Methods
Every resource share a set of standard methods. And each type of resource may define its own method. Methods are chainable.
use Actengage\Media\Facades\Resource;
$resource = Resource::request('image')
->disk('public')
->directory('images')
->filename('renamed.jpeg')
->title('Hello World!')
->caption('This is my first resource.')
->greyscale();
$media = $resource->save();
Conditional Resource Methods
Sometimes you may not know what kind of resource you are creating. You can use the is() method to conditionally chain methods to a specific resource types. Use the key configuration in config/media.php for matching or fully qualified class name.
use Actengage\Media\Facades\Resource;
use Actengage\Media\Resources\Image;
$resource = Resource::request('image')
->is('image', function($resource) {
$resource->greyscale();
})
->is(Image::class, function($resource) {
$resource->greyscale();
})
->is('file', function($resource) {
});
You may also need to check for true and false values before executing code on a resource. For these scenarios, you may use when() and not() methods to conditionally chain methods to the resource.
use Actengage\Media\Facades\Resource;
use Actengage\Media\Resources\Image;
$resource = Resource::request('image')
->when(true, function($resource) {
})
->when(function($resource) {
return true;
}, function($resource) {
})
->not(false, function($resource) {
})
->not(function($resource) {
return false;
}, function($resource) {
});
Resource Context, Meta, Tags
Contextual data can be used to search and filter Media models.
use Actengage\Media\Facades\Resource;
$resource = Resource::request('image');
$resource->context('image');
$resource->meta([
'some_key' => 'Some value goes here.'
]);
$resource->meta('another_key', 'Another key goes here.');
$resource->tags(['a', 'b', 'c']);
$resource->tags('d', 'e', 'f');
Events
Similar to Eloquent events, Resource event handlers work the same way. There are two ways to bind events, globally to a Resource class, or on the instance of a resource. The difference is global event binding is handled for all resources, whereas the instance methods are only fired for that instance.
The observable events fired during a resource's lifecycle are, in order: initialized, saving, saved, storing, and stored.
Global Methods
use Actengage\Media\Resources\Image;
Image::saving(function($resource, $model) {
});
Image::stored(function($resource, $model) {
});
Instance Methods
$resource = Resource::request('image')
->saving(function($resource, $model) {
})
->stored(function($resource, $model) {
});
Query Scopes
The Media model provides some convenient scopes for searching.
use Actengage\Media\Media;
Media::caption('Some Caption');
Media::caption('Some Caption', 'Another Caption');
Media::caption(['Some Caption', 'Another Caption']);
Media::context('Some Context');
Media::context('Some Context', 'Another Context');
Media::context(['Some Context', 'Another Context']);
Media::disk('public');
Media::disk('public', 's3');
Media::disk(['public', 's3']);
Media::extension('jpeg');
Media::extension('jpeg', 'jpg');
Media::extension(['jpeg', 'jpg']);
Media::filename('a.jpeg');
Media::filename('a.jpeg', 'b.jpeg');
Media::filename(['a.jpeg', 'b.jpeg']);
Media::filesize(2500);
Media::filesize(2500, 3500);
Media::filesize([2500, 3500]);
Media::meta([
'a' => 1,
'b' => 2,
'c' => 3
]);
Media::mime('text/plain');
Media::mime('text/plain', 'text/html');
Media::mime(['text/plain', 'text/html']);
Media::tag('a');
Media::tag('a', 'b');
Media::tag(['a', 'b', 'c']);
Media::tags('a', 'b');
Media::title('Some Title');
Media::title('Some Title', 'Another Title');
Media::title(['Some Title', 'Another Title']);
Media::withoutTag('a');
Media::withoutTag('a', 'b');
Media::withoutTag(['a', 'b', 'c']);
Media::withoutTags('a', 'b');
Mediable Trait
The Mediable trait is used to associate Media models to your custom models. Media models are related using morphToMany relationships.
Document.php
<?php
namespace App;
use Actengage\Media\Mediable;
use Illuminate\Database\Eloquent\Model;
class Document extends Model
{
use Mediable;
}
Basic Usage
$media = Resource::request('image')
->disk('public')
->save();
$document = new Document();
$document->content = 'Hello World!';
$document->save();
$document->media()->sync($media);
dd($document->media);
dd($document->medium);
Plugins
Plugins are used to add additional functionality to resources not provided by the core API's. This is an example of the ExtractImageColors plugin that extracts the common colors from an image and stores them in the model. A plugin has instance methods which apply to the specific resource that is being manipulated. Or as event handlers.
A Plugin class can be used in one of two ways. The first way is to bind it statically to the resource it should be used with. The second way is by defining it in the config/media.php file. In the config, plugins can be bound to specific resources or globally for all resources.
Static Binding
use Actengage\Media\Resources\Image;
use Actengage\Media\Plugins\ExtractImageColors;
use Actengage\Media\Plugins\HashFilename;
Image::register([
// This is a plugin without any options defined.
HashFilename::class,
// This is a plugin with options defined.
[ExtractImageColors::class, [
'colorCount' => 3,
'quality' => 10
]],
]);
config/media.php
<?php
use Actengage\Media\Media;
use Actengage\Media\Plugins\ExtractImageColors;
use Actengage\Media\Plugins\HashDirectory;
use Actengage\Media\Plugins\HashFilename;
use Actengage\Media\Resources\File;
use Actengage\Media\Resources\Image;
return [
'resources' => [
'image' => Image::class,
'file' => File::class
],
'plugins' => [
HashFilename::class,
'image' => [
[ExtractImageColors::class, [
'colorCount' => 3,
'quality' => 10
]],
],
'file' => [
HashDirectory::class
]
]
];
Plugins/ExtractImageColors.php
<?php
namespace Actengage\Media\Plugins;
use Actengage\Media\Contracts\Resource;
use Actengage\Media\Media;
use Actengage\Media\Resources\Image;
use ColorThief\ColorThief;
use Illuminate\Support\Collection;
class ExtractImageColors extends Plugin
{
protected static array $compatibleResources = [
Image::class,
];
public function saving(Resource $resource, Media $model): void
{
if ($resource instanceof Image) {
$model->colors = $resource->palette(
(int) $this->options->get('colorCount', 10),
(int) $this->options->get('quality', 10)
);
}
}
public static function boot(Collection $options): void
{
Image::macro('palette', function (
int $colorCount = 10,
int $quality = 10,
?array $area = null,
string $outputFormat = 'obj',
$adapter = null
): Collection {
return new Collection(ColorThief::getPalette(
$this->image->getCore(),
$colorCount,
$quality,
$area,
$outputFormat,
$adapter
));
});
}
}
Development
This package uses Pest for testing, Larastan/PHPStan for static analysis, Rector for automated refactoring, and Pint for code style. Continuous integration runs all four against a PHP 8.3, 8.4, and 8.5 matrix.
composer test
vendor/bin/pest --coverage --min=100
vendor/bin/phpstan analyse
vendor/bin/rector --dry-run
vendor/bin/rector
vendor/bin/pint --test
vendor/bin/pint
Releases
Versioning is managed with changesets. When you make a change that should be released, add a changeset describing it:
pnpm changeset
Merging the resulting "Version Packages" pull request tags the release automatically.