jolicode/media-bundle
JoliCode MediaBundle is a Symfony bundle for handling images, videos, and audio. It offers a provider system, on-the-fly filters like resize and crop, responsive image helpers, and local or cloud storage integration, making media consistent and easy to reuse across your application.
First, the honest framing: this is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no Blade directive, no Eloquent integration, and no config/media.php. Its value is concentrated in things that only exist inside a Symfony kernel: a Doctrine-mapped Media entity, DI-configured "libraries" over Flysystem, on-demand image variation generation, a browser controller/route, and EasyAdmin/SonataAdmin/Sylius field integrations. In a pure Laravel codebase you will not "leverage this package day-to-day" — you'll either avoid it or read it as a reference implementation.
If you do have a Symfony app in the estate (common in mixed shops):
composer require jolicode/media-bundle — requires PHP 8.2+, Symfony 6.4/7.x, Doctrine ORM, league/flysystem-bundle, and GD or Imagick. As of v0.10.0, imagine/imagine is a default (required) dependency — image manipulation now goes through Imagine's ImagineInterface rather than a bundle-internal convert step, so installs get it whether or not they configured it before.config/packages/jolicode_media.yaml).original storage pointing at a Flysystem filesystem, and a cache storage for generated variations.bin/console doctrine:migrations:diff && bin/console doctrine:migrations:migrate for the media table.bin/console assets:install / rebuild your front-end — v0.10.0 replaced Dropzone with an internal uploader, so the shipped JS/CSS for the media widget is now the bundle's own; don't expect your old Dropzone overrides to still apply.Article entity with a ManyToOne to the bundle's Media, expose it in EasyAdmin with the bundle's media field, upload an image, render a named variation in Twig.Where to look first: the README's configuration reference (libraries → storages → variations), then src/Library/, src/Storage/, src/Form/, src/Twig/, and — new in 0.10 — the srcset Resolver that now owns what used to live in the Twig extension, plus whatever backs temporary urls. Start with the Storage and Library classes — that's the whole architecture in two files — then read the Resolver before touching any responsive-image templates.
First use case worth shipping: "editor uploads a hero image through the admin, front end renders a responsive srcset from pre-declared variations." That's the 20-minute win, and it's the only reason to adopt the bundle over hand-rolling.
default, avatars, blog libraries each with their own Flysystem storage — conceptually identical to Laravel disks. This is the cleanest way to separate public CDN assets from private uploads, and it maps 1:1 to how you'd configure filesystems.disks in Laravel.Article; you add a ManyToOne to the media table. That's what enables reuse ("pick an existing image" instead of re-uploading) and metadata queries (alt text, dimensions) without touching the filesystem.thumb, card, hero etc. defined once; templates never compute resize parameters. Variations are generated lazily on first access and then cached — the pattern is the bundle's CachedStorage sitting in front of the original storage. In 0.10 the srcset widths are assembled by a dedicated Resolver rather than in Twig, so the responsive logic is testable and reusable outside templates (useful if you expose media over JSON/API). Mirror this in Laravel with a controller route that checks Storage::disk('media-cache'), generates via intervention/image, streams, and returns a long-cache response — and put the width-set calculation in a small service, not a Blade component with inline loops.temporary urls for private libraries. v0.10.0 adds expiring/temporary URL support — this is the correct answer for the private uploads library rather than hand-rolling a signed route. In Laravel terms it's Storage::disk('s3')->temporaryUrl(...); the point is the bundle now gives you that seam, so don't invent one.filename ends with .jpg hack — it's the bundle's own notion of "is this an image we can convert."spatie/laravel-medialibrary gives you the DB entity + filesystem + registerMediaConversions(); add a signed on-demand conversion route to replicate the lazy-cache behavior, intervention/image v3 for the actual processing, and a small SrcsetResolver service mirroring the bundle's Resolver. That combination is roughly the Laravel-shaped version of this bundle minus the admin UI.srcset generation moved from Twig to a Resolver (if you overrode the Twig extension, replaced templates, or called the old function directly, re-check your output and markup); (b) imagine/imagine is now required, and the convert step moved into core — any custom convert/adapter wiring or service overrides you wrote around the old arrangement needs re-verification. Also re-check anything that assumed Dropzone was present.trash_path normalization alone. 0.10 fixes a path-handling bug where the trash directory could be deleted. If you rely on soft-deleted media surviving, don't sit on 0.9.fa icon class, Sylius widget/upload-link fixes, upload markers). If you're not on any of those admin stacks, what's left is "a media entity plus Flysystem sugar" — and that's a weekend of work.srcset widths triggers a lot of work. Prewarm in CI/deploy and watch memory for large originals. The Resolver move makes it easier to see exactly which widths a template will request — use that list as your prewarm input.storage:link on ephemeral containers.bin/console debug:config jolicode_media to see resolved libraries, debug:container for the storage services and the image/Imagine services, and the bundle's own log channel for variation generation failures. In Laravel terms: php artisan config:show and a dedicated log channel.StorageInterface (logging, virus scan, metrics), swap the path generator if you want hash-based paths instead of slug-based, override form theme blocks for the admin widget, and now swap/extend the srcset Resolver if your breakpoints differ from the default. These are the same seams you'd want in a Laravel media package — which is the strongest argument for reading this source even if you never install it.How can I help you explore Laravel packages today?