libre-informatique/media-bundle
A Symfony bundle for managing media files and images, providing Doctrine-backed media entities, file upload and storage handling, and reusable tools for attaching pictures and documents to application content.
First, calibrate expectations: this is a Symfony bundle, and you are a Laravel developer. There is no service provider, no facade, no config/media.php, no Eloquent model. composer require libre-informatique/media-bundle into a Laravel app will install code that nothing autoloads or bootstraps. The realistic day-to-day relationship with this package is "read it, don't depend on it" — borrow the domain model, reimplement it idiomatically.
Where to look first, in this order:
composer.json — the fastest way to learn what this thing actually assumes. Look at require for the symfony/* constraints, doctrine/orm, and any filesystem/image dependency (Flysystem, Gaufrette, LiipImagineBundle, Imagine). Also check autoload PSR-4 prefixes so you can map namespaces to directories.Entity/ — the Media model and its Doctrine mapping. This is the part worth keeping: which fields exist (path, filename, mime, size, dimensions, timestamps), whether attachments are polymorphic, and how storage location is recorded.DependencyInjection/ and Resources/config/ — the service definitions. This tells you the real API surface: a media manager service, a storage/upload adapter, probably a form type or two and a Twig helper.NOASSERTION license, nothing here is battle-tested.First use case to evaluate against: the boring universal one — upload a file, persist a record pointing at it, and later serve or derive from it (thumbnail, crop, resize). Everything else in the bundle is scaffolding around that flow.
Minimal spike (in a throwaway Symfony skeleton, never in your Laravel repo):
composer create-project symfony/skeleton media-spike
cd media-spike
composer require libre-informatique/media-bundle
# register the bundle, point the storage adapter at var/media,
# run the schema diff, upload one file through the provided form type
Then verify the invariant that matters: the row and the file agree. If the spike doesn't work end-to-end in an afternoon, stop — the package hasn't earned further time. Cap the whole investigation at a few hours and write down what you learned about the entity shape.
You only get to use this pattern if you're already in a Symfony/Sylius codebase, ideally the Libre Informatique orbit.
web/uploads layout. Override with %env(MEDIA_STORAGE_PATH)% and a filesystem adapter you control.persist() the media entity directly from a controller. The manager sequences file write + DB write, and bypassing it is how you get orphaned rows and orphaned files.Media with a Doctrine association you own (ManyToOne, or a join table for a shared library). That way you can excise the bundle later without a migration archaeology project.Mirror the triad, not the code:
// database/migrations/..._create_media_table.php
$table->id();
$table->morphs('mediable'); // the thing the Symfony bundle can't give you
$table->string('disk');
$table->string('path');
$table->string('collection')->default('default');
$table->string('mime');
$table->unsignedBigInteger('size');
$table->string('original_name');
$table->json('conversions')->nullable();
$table->timestamps();
A small MediaManager service with one entry point:
public function store(Model $owner, UploadedFile $file, string $collection = 'default'): Media
{
return DB::transaction(function () use ($owner, $file, $collection) {
$path = $file->store("media/{$collection}", $owner->mediaDisk());
$media = $owner->media()->create([
'disk' => $owner->mediaDisk(),
'path' => $path,
'collection' => $collection,
'mime' => $file->getClientMimeType(),
'size' => $file->getSize(),
'original_name' => $file->getClientOriginalName(),
]);
GenerateConversions::dispatch($media)->afterCommit();
return $media;
});
}
Key patterns that carry over:
mediable morph relation instead of Doctrine associations. This is the single biggest ergonomic win and the reason to port rather than adopt.['thumb' => ['w' => 200, 'h' => 200], 'hero' => ['w' => 1600]] — so conversions are data, not bespoke code per model.store(), replace(), delete(); nothing touches Storage directly outside the manager.afterCommit() so the row exists before a worker looks for it.NOASSERTION license is a hard gate, not a nitpick. Legal cannot clear it from a metadata scan. That means: no redistribution, no embedding in shipped artifacts, and if you port the code line-for-line you may be copying unlicensed source. Read it for design, then write your own — don't cut and paste, and don't vendor the files into a proprietary repo without review.symfony/* constraint and transitive Doctrine/Imagine pins. Run composer why-not (or a resolver dry run in a throwaway skeleton) before assuming compatibility with anything modern.web/uploads, fix it at config level before your first upload, not after.composer.json. The correct dependency for a Laravel media subsystem is spatie/laravel-medialibrary (MIT, maintained, polymorphic, S3-native, queued conversions). Use this bundle's entity shape as a design checklist against it, confirm the feature gap is nil, and move on.disk column and Storage::disk() config drift apart the moment you add a second disk, and it presents as a path bug.How can I help you explore Laravel packages today?