league/glide-symfony
Glide adapter for Symfony integrates the Glide image manipulation library into Symfony applications, enabling on-the-fly image resizing and cropping via a simple URL-based API.
First: confirm your framework. This is the single most important step, and for a Laravel developer it happens before any install. league/glide-symfony is not framework-agnostic glue — it is a Symfony bundle. It expects:
symfony/framework-bundle (HttpKernel) at runtimeGlideBundle registration in config/bundles.phpconfig/packages/glide.yaml for source/cache filesystem configconfig/routes.yaml mounting the adapter's controllerIn a Laravel app none of those exist, and composer require only pulls dead weight. Run php artisan --version first; if it prints a Laravel version, stop here and read the "Laravel parity" note below.
If you are on Symfony (5.4–8):
composer require league/glide-symfony
config/bundles.php.config/packages/glide.yaml (Flysystem-backed; env vars for S3 credentials).config/routes.yaml:glide:
resource: "@GlideBundle/Resources/config/routing.xml"
prefix: /glide
Your first use case. Drop photo.jpg into the source filesystem, then hit:
/glide/photo.jpg?w=600&h=400&fit=crop
If you get a WebP/JPEG back with sensible headers, the wiring is correct. Everything after that is URL generation from Twig or a controller.
Laravel parity (the honest advice). The concept transfers; the package does not. Depend on league/glide directly — it is framework-neutral — and write ~40 lines of glue:
Route::get('/glide/{path}', function (string $path) {
$server = \League\Glide\ServerFactory::create([
'source' => storage_path('app/glide-source'),
'cache' => storage_path('app/glide-cache'),
'driver' => 'gd',
]);
return $server->getImageResponse($path, request()->only([
'w', 'h', 'fit', 'fm', 'q', 'orient', 'bri', 'con', 'filt',
]));
})->where('path', '.*');
Where to look first. The adapter README is a pointer, not documentation — the real docs live at glide.thephpleague.com and cover the parameter vocabulary (w, h, fit, fm, q, filt, bri, con, gam, sharp, blur, watermark, …). After that, read DependencyInjection/Configuration.php inside the bundle to see exactly which keys are configurable, and the routing XML to see the controller contract.
Config-driven filesystems, not hardcoded paths. Both the Symfony and Laravel paths should resolve source and cache through Flysystem/Storage rather than raw paths. That is what makes the local→S3 migration a config change instead of a rewrite. In Symfony this is the bundle's glide.yaml; in Laravel mirror it with a dedicated disk:
// config/filesystems.php
'glide_source' => ['driver' => 'local', 'root' => storage_path('app/glide-source')],
'glide_cache' => ['driver' => 'local', 'root' => storage_path('app/glide-cache')],
One catch-all route, extension required. Glide picks the output encoder from the source file extension. Route the whole path through a single endpoint and require a trailing extension:
->where('path', '.*\.(jpe?g|png|gif|webp|avif)')
Never let templates invent parameters. Define a small named preset set in config — thumb, card, hero, og — and expose a single helper that expands a preset name into a query string. This keeps cache hit rates high, keeps transforms reviewable, and makes it trivial to swap fm=webp in later.
{# Symfony #}
<img src="{{ glide_url(asset_path, 'card') }}" srcset="..." alt="">
// Laravel equivalent
function glide_url(string $path, string $preset = 'card'): string
{
return url('/glide/'.$path.'?'.http_build_query(config("images.presets.$preset")));
}
Sign your URLs. Glide supports setSignKey(). In the Symfony bundle this is a config value; in the Laravel glue you add it to ServerFactory::create(). With signing enabled, arbitrary transforms from the internet return 403 SignatureException — only your app can mint valid URLs. This is the cheapest possible defence against the /glide endpoint becoming a CPU amplifier.
Cache invalidation by versioning, not by purging. Deriving URLs from ?v=<filemtime|content-hash> avoids ever needing to walk and delete the cache tree when a source image is updated. Purging a Flysystem cache directory is slow and error-prone; cache-busting the URL is one string concat. The derivative for the old version simply ages out of the CDN.
Push caching to the edge. getImageResponse() already emits Last-Modified/ETag derived from the source file. Add Cache-Control: public, max-age=31536000, immutable on top and put a CDN in front of /glide. The PHP worker should serve each derivative exactly once per cache generation.
Integration with the upload path. Both ecosystems have the same shape: upload bundle stores the original (never the derivative), the database stores the original's path, and the Glide URL is computed at render time. In Symfony this composes with VichUploaderBundle; in Laravel it's the standard storage disk + Storage::url(). Do not persist Glide URLs — they're derived data, and persisting them couples your schema to a transform preset.
Functional test the endpoint, not the library. One test that hits /glide/test.jpg?w=100&h=100&fit=crop, asserts Content-Type: image/* and status 200, catches 90% of misconfigurations (unwritable cache, missing extension, bad source root, wrong driver). Add a second test asserting a signed-but-tampered URL returns 403.
The framework mismatch is the headline risk. Everything else on this list is a footnote next to it. A Laravel team adopting this package will spend hours fighting a boot process that will never work, then rewrite against league/glide anyway. Budget that time against the 40-line glue instead.
Version matrix — check before you pin. 2.1.0 → Symfony 8, PHP 8.3/8.4, Glide 3. 2.0.1 → Symfony 7, PHP 8.2. 2.0.0 → Symfony 6, PHP 8.1, Glide 2. 1.0.x → Symfony 4/5. The Glide 2→3 bump changes the ServerFactory/Server surface, so adapter major and Glide major must move together. Pin ^2.1 and let composer validate run in CI.
Extensions are load-bearing. Glide infers the output format from the source filename's extension, and Flysystem infers the mime type again on read. A path without an extension, a double extension (photo.jpg.webp), or an uppercased extension (.JPG) is a routine source of 400s and blank responses. Normalize extensions on upload.
The cache directory must be shared across nodes. This is the single most common production failure. With two app servers and a local cache, users see a derivative on one request and a cache-miss timeout on the next. Object storage for the cache, or a shared volume — no exceptions.
Keep the source out of public/. If originals are web-served, users can bypass /glide entirely and you lose both the transform and the cache — and on Laravel you also risk the cache directory being publicly browsable. Originals belong in a private disk.
Memory and dimensions. A 6000×4000 JPEG under a wide resize can exhaust a 128M memory_limit and take the worker down. Set an explicit ceiling on w/h (validation before the request reaches Glide), raise memory_limit for the image route specifically if you're on php-fpm pools, and prefer gd only if Imagick isn't available — GD's memory profile on large inputs is worse.
Path traversal. Glide's own path validation is decent, but validate at the edge anyway: reject .., reject absolute paths, whitelist the extension, and never interpolate a raw user string into a Storage::path() call. The route regex above is the first line of defence, not the only one.
Watermark and font paths resolve against the base path, not the source root. This trips people up constantly — the filter silently fails or throws "file not found" while the rest of the transform works. Use absolute paths for overlays if you can.
Debugging: curl -I first. Headers tell you whether you're looking at a Glide response, a framework 404, or a PHP fatal. Symfony's profiler stops at the controller boundary — the actual transform work happens inside league/glide, so latency investigations need Xdebug or Blackfire, not the profiler timeline. In Laravel, Log::debug() around getImageResponse() and a dd() on the resolved Server config gets you there faster.
Extension points are in Glide, not the adapter. Custom manipulation APIs, custom manipulators, and a custom ServerFactory all live in league/glide. The Symfony bundle wires a fixed server from YAML and gives you limited room to substitute pieces; if you need a custom Api with your own manipulator, you'll be fighting the bundle's DI definitions. In Laravel's hand-rolled glue you simply pass your own Api into ServerFactory::create() — another point in favour of the thin-glue route.
Governance posture. 104 stars, zero declared dependents, effectively single-maintainer. Treat this as convenience wiring, not a platform dependency: pin the constraint, review at each Symfony LTS bump, and keep the escape hatch (drop the bundle, keep league/glide) documented in the runbook. The blast radius of a stalled release is small precisely because the adapter is thin — but only if you never let the bundle's config format leak into your application code.
How can I help you explore Laravel packages today?