vlabs/media-bundle
vlabs/media-bundle is a Laravel package for managing media files and attaching them to Eloquent models. It simplifies uploads, storage, image conversions, and reusable media collections with a clean API for PHP applications.
First, a reality check. vlabs/media-bundle is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/media.php to publish, and no artisan command. So "getting started" in a Laravel app really means one of three things:
symfony/form, symfony/http-foundation, Doctrine) are pulled in alongside Eloquent — which is almost always more pain than it's worth.Where to look first (in the repo):
Media entity — this is the core of the abstraction. Read its fields (name, path/filename, mime type, size, timestamps) and decide what you'd mirror in an Eloquent model.MediaType form type — shows how the bundle wires an upload widget into a form with metadata.DependencyInjection/ and Resources/config/ — the YAML service wiring that you'd translate into a Laravel service provider.Minimal Symfony steps (for reference):
composer require vlabs/media-bundle
// config/bundles.php
Vlabs\MediaBundle\VlabsMediaBundle::class => ['all' => true],
Then configure the filesystem root/adapter, run the Doctrine migration for the media table, and embed MediaType in your own form.
Minimal Laravel equivalent (the realistic path):
composer require spatie/laravel-medialibrary
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
Spatie's HasMedia trait + addMedia() is the direct Laravel-native analogue of what this bundle gives Symfony, and it's battle-tested at a scale this bundle (44 stars, 0 dependents) is not.
First use case to target either way: a single "attach one file to one record, store the path, keep the mime type and size" flow. Nail that before touching galleries, conversions, or multi-disk setups.
Media entityclass Media extends Model
{
protected $fillable = ['disk', 'path', 'filename', 'mime_type', 'size'];
public function mediable(): MorphTo
{
return $this->morphTo();
}
}
trait HasMedia
{
public function media(): MorphMany
{
return $this->morphMany(Media::class, 'mediable');
}
public function attachUpload(UploadedFile $file, string $disk = 'public'): Media
{
$path = $file->store('media', $disk);
return $this->media()->create([
'disk' => $disk,
'path' => $path,
'filename' => $file->getClientOriginalName(),
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
]);
}
}
The bundle's best idea is treating storage as an injected abstraction rather than hard-coding a path. In Laravel, push that into a custom cast or a small MediaStore service:
class MediaStore
{
public function __construct(private string $disk) {}
public function put(UploadedFile $file, string $dir = 'media'): Media
{
return Media::create([
'disk' => $this->disk,
'path' => $file->store($dir, $this->disk),
// ...
]);
}
public function url(Media $media): string
{
return Storage::disk($media->disk)->url($media->path);
}
}
Bind it in a service provider so tests can swap fake() disks. This is the closest Laravel analogue to the bundle's adapter wiring.
The bundle leans on Symfony's Form component; Laravel leans on FormRequests + StoreMediaRequest rules. Map the bundle's form type to:
class StoreMediaRequest extends FormRequest
{
public function rules(): array
{
return [
'file' => ['required', 'file', 'mimes:jpg,png,pdf', 'max:10240'],
];
}
}
If you're genuinely running Symfony forms inside Laravel (rare), you're re-implementing laravelcollective/html and the CSRF bridge — don't.
The bundle hooks entity persist/remove to clean up files. In Laravel, mirror that in model events:
protected static function booted(): void
{
static::deleting(fn (Media $m) => Storage::disk($m->disk)->delete($m->path));
}
Be careful — deleting a mediable should cascade to media rows and files; use a deleted observer on the parent too.
The bundle typically stores a path and lets you build a URL. In Laravel, either use the public disk + storage:link, or stream through a signed route for private files:
Route::get('/media/{media}', fn (Media $media) =>
Storage::disk($media->disk)->download($media->path, $media->filename)
)->middleware('signed');
Kernel/bundles.php registration. composer require will pull Symfony packages into your app and can conflict with Laravel's pinned symfony/* versions. Check composer why-not symfony/http-foundation before attempting.config/packages/vlabs_media.yaml in Symfony, not config/media.php. There's no php artisan vendor:publish.DB::transaction, and clean up on failure. Conversely, deleting the row should delete the blob — do it in a deleting hook, not in a controller.UploadedFile::getMimeType() trusts the client sniff; getMimeType() on UploadedFile uses PHP's finfo and is safer. The bundle delegates to the filesystem/HTTP layer, so verify what your adapter actually reports — especially for PDFs and HEIC.disk on the row, not in the config, so records survive a storage migration (public → S3).path column vs Storage::disk()->path(''), (2) confirm storage:link exists for public URLs, (3) check disk permissions for the PHP-FPM user, (4) Storage::disk()->exists($media->path) from php artisan tinker.FormRequest and a MediaStore service bound to an interface. Everything else (the entity, the URL building) is thin glue you should reimplement natively.laravel-medialibrary for the Laravel path, resist also pulling in this bundle "for the form bits" — you'll end up with two media tables and two cleanup strategies.How can I help you explore Laravel packages today?