intervention/image-symfony
Symfony integration for Intervention Image, providing a ready-to-use image manager service for resizing, cropping, and applying filters. Configure drivers and options once, then inject the manager anywhere to handle uploads and thumbnails with clean, expressive code.
Install and wire it up
4.0.0 is a major release that exists solely to track Intervention Image v4. Install both majors together so Composer can't resolve a mismatched pair:
composer require intervention/image-symfony:^4.0 intervention/image:^4.0
Symfony Flex should register the bundle automatically; if not, add it to config/bundles.php:
Intervention\Image\Symfony\ImageSymfonyBundle::class => ['all' => true],
Create config/packages/intervention_image.yaml with the minimum:
intervention_image:
driver: gd # or imagick
Both drivers require the corresponding PHP extension (ext-gd / ext-imagick). Pick gd for portability and CI, imagick when you need better format support or quality control. Check the composer.json of both packages for the new PHP floor — image majors typically raise it, and a failed resolution on PHP version is the most common install error here.
Where to look first
DependencyInjection/Configuration.php and *Extension.php — still the authoritative list of config keys. The 4.0.0 release note only mentions the underlying library bump, so you should not assume any config schema change; confirm by diffing Configuration.php against what you have.Intervention\Image v4 docs. The Symfony package is a thin DI wrapper; 90% of what you'll do day-to-day is the underlying library's API, and v4 is a fresh major — re-read the upgrade guide even if you knew v3 well.bin/console debug:container ImageManager to confirm the service ID and autowiring alias survived the bump.First use case
Inject the manager and process an upload in a controller:
use Intervention\Image\ImageManager;
public function upload(Request $request, ImageManager $manager): Response
{
$file = $request->files->get('photo');
$image = $manager->read($file->getPathname())
->cover(1200, 630)
->toWebp(82);
$image->save($this->getParameter('kernel.project_dir').'/public/uploads/hero.webp');
return $this->redirectToRoute('home');
}
Verify each method name against the v4 docs before copying this in — read(), cover(), and the encoder methods are the surface most likely to have been renamed or re-signatured in a new major. If it autowires and encodes, you're done; that's the whole integration surface for most projects.
Constructor injection everywhere
The manager is stateless and safe to inject into controllers, services, message handlers, and console commands. Favor injecting it over calling static helpers so tests can swap it. If v4 changed the manager's constructor signature, the bundle's service definition is where that's absorbed — your injection points should not need to change.
HTTP responses
For on-the-fly resizing (thumbnails, avatars), read from disk or a stream and return the encoded image:
$image = $manager->read($path)->scale(width: 200);
return new Response($image->toJpeg(), 200, ['Content-Type' => 'image/jpeg']);
Add Cache-Control / ETag headers yourself — the bundle does not give you an HTTP caching layer out of the box.
Background processing
Image work is CPU- and memory-bound. Move it to a Symfony Messenger handler:
#[AsMessageHandler]
class GenerateThumbnailHandler
{
public function __construct(private ImageManager $manager, private FilesystemOperator $storage) {}
public function __invoke(GenerateThumbnail $msg): void
{
$image = $this->manager->read($this->storage->read($msg->path))->cover(400, 400);
$this->storage->write($msg->thumbPath, $image->toWebp()->toString());
}
}
This pairs naturally with Flysystem — read the source stream, write the derivative stream, never touch local temp files explicitly. A major-version rollover is a good moment to do the upgrade behind the queue: deploy workers first, let them drain, then ship the web tier.
Per-environment driver configuration
Use when@test to force GD in tests and when@prod to use Imagick with tuned options:
when@test:
intervention_image:
driver: gd
when@prod:
intervention_image:
driver: imagick
options:
autoOrientation: true
Deriving variants in a service
Wrap the manager in your own Thumbnailer service so resize rules, quality, and naming live in one place rather than being scattered across controllers. This is also where you'd add cache-key logic (see below) — and where a v4 encoder change only has to be absorbed once.
Custom defaults via decoration
If you need project-wide defaults (e.g., always strip metadata, always encode WebP at quality 80), decorate the manager:
App\Image\ConfiguredImageManager:
decorates: Intervention\Image\ImageManager
Or simply expose your own facade service and forbid direct injection via code review — for a package this thin, a thin wrapper is often cleaner than fighting DI. A decorator also gives you a single seam to shim any v4 API renames.
Testing
Use real fixture images and assert on dimensions/format, not byte equality (drivers differ across versions). Treat this upgrade as a fixture reset: v4 encoders may emit different bytes for the same input, so any golden-file or snapshot assertion on encoded output will fail for reasons unrelated to your code. Regenerate fixtures once, commit them, and keep asserting on dimensions, format, and rough file-size bands.
This is a compatibility major, not a feature major. The 4.0.0 note contains exactly one line: support for Intervention Image v4.0.0. Expect no new config keys, no new services, no new extension points. Budget the upgrade as "chase the underlying library's breaking changes," not "learn a new bundle."
You cannot mix majors. The bundle's composer.json will require intervention/image:^4.0. If a transitive dependency pins v3, resolution fails. Diagnose with:
composer why-not intervention/image 4.0
composer why-not intervention/image-symfony 4.0
Run this before editing composer.json so you know whether a third-party image plugin is the blocker.
v4 API vs. every tutorial you'll find. Most blog posts and Stack Overflow answers still show v2 (Image::make($path), ->resize(300, null)) and a growing pile show v3 ($manager->read(), named args). v4 has reset that surface again. If something "doesn't exist," check which major the answer was written for before assuming the install is broken — and prefer the upgrade guide over snippets.
The integration is thin — expect to read source. With a tiny star count and no visible dependents, issues and examples are sparse. When config keys or service IDs are unclear, read the bundle's Configuration class and services.yaml rather than trusting the README. Keep intervention/image and intervention/image-symfony pinned to compatible minors; a floating ^4.0 on both is fine, a floating constraint on one and a pinned one on the other is not.
Invalidate your derivative cache on deploy. If you cache generated images keyed on source-hash-plus-transform-params, a v4 encoder change produces visually similar but byte-different output under identical keys. Bump a version prefix in the cache key as part of the 4.0.0 rollout rather than waiting for users to report stale images.
Imagick policy.xml will bite you in containers. Docker base images often ship an imagick policy that blocks PDFs, large dimensions, or certain coders. If operations silently fail or throw — especially after a major bump where the failing call may have moved — check /etc/ImageMagick-*/policy.xml before blaming the bundle.
Memory and timeouts. A 4000×3000 JPEG decoded into GD or Imagick can consume hundreds of MB. Set memory_limit explicitly for queue workers and consider ->scale() before ->cover() for very large inputs. Never do heavy processing synchronously on a user request path.
EXIF orientation. Photos from phones will appear rotated unless autoOrientation is enabled in the options config. It's off by default in the underlying library; turn it on globally. Re-verify the option name in v4 — it is exactly the kind of setting that gets renamed in a major.
Animated formats. GIF/WebP animation decoding is opt-in (decodeAnimation). If frames vanish after processing, that's the option you forgot. Same caveat: confirm the v4 spelling.
Filesystem permissions for cache/temp. If your config points at a writable directory (cache, temp), ensure it exists and is writable by the PHP user — especially in read-only container filesystems where only /var/cache and /tmp are mounted writable.
Cache invalidation. If you add your own derivative cache, key on the source file's hash plus the transform parameters, not on mtime alone — deploys and rsync can reset mtimes.
Multiple managers / explicit aliases. If you register additional ImageManager instances (e.g., different drivers per feature), autowiring by type becomes ambiguous. Define named aliases and inject by argument name or explicit service ID. After a major bump, re-run debug:container to confirm the aliases still exist before you assume your autowiring is fine.
Extension points are Symfony-native, not package-native. The package offers no events or plugin hooks, and 4.0.0 adds none. Use service decoration, compiler passes, and your own wrapper services — all standard Symfony DI — rather than looking for integration-specific extension APIs.
Upgrade path. 3.x → 4.0 is driven entirely by intervention/image 3.x → 4.0. Read the underlying library's changelog and upgrade guide first; the Symfony package will rarely be the source of a breaking change, and its release notes will rarely tell you what actually broke. Pin the major on both, upgrade in a branch, run your image test suite, and expect the failures to be in method names and encoder output — not in the bundle wiring.
How can I help you explore Laravel packages today?