mael/intervention-image-bundle
A Symfony bundle that integrates the Intervention Image library, offering service configuration and convenient helpers for resizing, cropping, and manipulating images within Symfony applications.
What this actually is. mael/intervention-image-bundle is a ~1-file Symfony bundle whose job is to register Intervention\Image\ImageManager in the container with a configurable driver. That's it. It is not a Laravel package, it has 1 star and 0 dependents. The latest release, 1.0.1, is a deprecation-only patch ("Fix Symfony 5.1 deprecations") — no new features, no API change, no Laravel support. Read that as a signal: the package is being kept installable on Symfony 5.x, not modernised for Symfony 6/7, and it still targets Intervention Image v2. Treat it as a reference implementation of "how to wire Intervention into a DI container," not as a dependency you should composer require in a new Laravel or Symfony app.
The Laravel translation. Laravel doesn't load Symfony bundles. The direct equivalent for a Laravel developer is intervention/image-laravel (v3), which gives you the Image facade. If you're on v2, the old intervention/image package shipped its own service provider. Either way, the day-to-day API is identical to what this bundle exposes — so everything below transfers, just through a different container.
Minimal steps if you're actually in a Symfony app:
composer require mael/intervention-image-bundle — then pin intervention/image explicitly (^2.7) in your composer.json, because the bundle's own constraint is old and will resolve unpredictably.config/bundles.php (older Symfony skeletons don't auto-register):
Mael\InterventionImageBundle\InterventionImageBundle::class => ['all' => true],
ext-gd or ext-imagick is installed — the bundle does not install a driver for you.DependencyInjection/Configuration.php first. That file is the entire public API surface: it tells you the config key, the allowed driver values, and whether the ImageManager service is aliased for autowiring.thumbnails/ path alongside the original.The Laravel-side equivalent of "step 4" is just the container binding — one line in a service provider, and you never need the bundle at all:
$this->app->singleton(ImageManager::class, fn () => new ImageManager(['driver' => 'gd']));
Look at the bundle's Resources/config/services.* for the exact service id and any aliases before you start autowiring — that's the five minutes that saves an hour of Cannot autowire service errors.
1. Autowire the manager, never new ImageManager(). The whole value of the bundle (or your own binding) is a single shared, driver-configured manager. Constructor-inject it:
final class ThumbnailService
{
public function __construct(private ImageManager $images) {}
public function make(string $src, string $dest, int $width = 300): void
{
$this->images->make($src)
->orientate() // v2: EXIF rotation is NOT automatic
->resize($width, null, function ($c) {
$c->aspectRatio();
$c->upsize(); // don't blow up small images
})
->save($dest, 82);
}
}
(v3 API if you upgrade: $manager->read($src)->orientate()->scale(width: 300)->toJpeg(82)->save($dest) — note make()→read(), resize()→scale(), save()→toJpeg()/save().)
2. Upload → derivatives, but as a queued unit of work. The pattern that survives production: move the original to permanent storage in the request, dispatch a job/command that generates every derivative, persist the derivative paths (or a predictable path convention) on the model. Never loop over 6 sizes synchronously inside a 30-second PHP-FPM request.
// Laravel
Storage::disk('local')->put($path, $file);
GenerateDerivatives::dispatch($path)->onQueue('images');
3. On-the-fly resize behind a validated cache key. This is the most common "day-to-day" use and the easiest to get wrong. Validate w/h against a whitelist and a max bound before touching the filesystem:
$w = (int) $request->query('w', 300);
abort_unless(in_array($w, [64, 128, 300, 768, 1200], true), 404);
$key = sprintf('%s_%d_%s.webp', sha1($path), $w, filemtime($absolute));
$out = storage_path("app/cache/{$key}");
Include the source file's mtime (or content hash) in the key, or you will serve stale thumbnails forever after a re-upload.
4. Centralize every filter/size in one class or config array. Scattered ->resize(300) calls across controllers is how you end up with five different avatar sizes. One ImagePresets config, one apply(string $preset, Image $img) method, and preset names in the DB ("avatar", "og", "hero").
5. Twig/Blade helper for presentation. In Symfony, a Twig extension with {{ asset_path|thumb(300) }}; in Laravel, a Blade component or Image::url($path, $preset). Keep URL generation separate from rendering so the endpoint contract is testable in isolation.
6. Backfill as an artisan/console command. Ship images:regenerate {--force} on day one. You will change the OG-image dimensions three months in and need to rebuild everything idempotently.
7. Test dimensions, not pixels. Don't snapshot images. After the call, assert with getimagesize():
Storage::fake('public');
(new ThumbnailService(new ImageManager(['driver' => 'gd'])))
->make($fixture, $out = Storage::path('public/t.jpg'), 300);
[$w, $h] = getimagesize($out);
$this->assertSame(300, $w);
$this->assertLessThanOrEqual(300, $h); // aspect ratio preserved
Keep one or two small real fixtures (a portrait, a CMYK JPEG, a transparent PNG) in tests/fixtures/. Those three catch ~80% of image bugs.
make→read, resize→scale/resize, save→toJpeg/toPng/save). If Composer pulls v3, the bundle's service definition may still instantiate fine but your calls will break. Pin intervention/image:^2.7 deliberately, or skip the bundle and bind v3 yourself — a one-line singleton is cheaper than a stale wrapper.Definition-only config). If your app is on Symfony 6+, expect autowiring/config errors the bundle will never patch; write the binding yourself.mael_intervention_image vs intervention_image is a five-minute bin/console debug:config away. bin/console debug:container ImageManager tells you instantly whether autowiring will work.memory_limit=128M + two concurrent uploads = fatal error with no stack trace. Either raise the limit for the queue worker or pre-check dimensions and reject >N megapixels.->orientate() on every user-uploaded photo. v2 does not respect EXIF orientation. Without it, iPhone photos are sideways and nobody notices until the client does.->make(). Resolve against a base directory, realpath(), and confirm the result is still inside that base. On-the-fly-resize endpoints are a classic LFI vector.->save() re-encodes and strips EXIF/IPTC. Good for size and privacy, bad if you wanted to preserve GPS or copyright. Use ->save($path, $quality) and be explicit about format; letting the extension guess produces surprises.imagewebp() needs GD compiled with WebP support; Imagick needs the delegate. Detect once at boot and fall back to JPEG rather than emitting broken responses.ImageManager for logging/metrics; (3) a compiler pass/tagged-filter collection in Symfony, or a container binding in Laravel; (4) only then, forking the bundle. Given the star count and the fact that 1.0.1 is a two-line deprecation fix, forking is a realistic option and the fork is ~50 lines.intervention/image-laravel (maintained, facade, config publish) or the raw intervention/image package with a three-line provider. Adopt the patterns above — queued derivatives, preset registry, validated cache keys, dimension assertions — from this bundle; skip the dependency.How can I help you explore Laravel packages today?