symfony/ux-image
Symfony UX Image offers a Twig component and PHP helpers for rendering and manipulating images in Symfony applications, simplifying responsive output, lazy loading, and integration with image processing or optimization backends.
The package is symfony/ux-image. Despite the symfony/ namespace, nothing in the bridge layer requires the Symfony framework — a Laravel app can use it as a pure URL-generation library for image CDNs. As of v3.6.0 it ships four bridges in the box: Cloudflare, KeyCDN (with Secure Token signing), imgix, and Cloudinary.
composer require symfony/ux-image. Pull it in alongside whatever you already use for uploads (intervention/image, Spatie Media Library, Flysystem). You do not need Symfony Flex, the recipe, or config/packages/ux_image.yaml.Symfony\UX\Image\Bridge\* — one class per CDN — and at the new preset support (Symfony\UX\Image\Preset\* or the config key the bridge accepts for named transforms). Those two things are 90% of what you'll touch. The Twig ux_picture component and ImageInterface/Picture classes are the Symfony-facing front end and are the part you'll most likely reimplement in Blade.config/services.php + .env (zone hash for Cloudflare, hostname + token for KeyCDN, cloud name + API key for Cloudinary, source/domain for imgix).products/abc.jpg), run it through the bridge with a named preset ('card' → 400×300, fit=crop, format=auto), and emit it in a Blade component as src plus a 2–3 entry srcset.Wrap the bridge, don't sprinkle it. Expose a single ImageUrl service with methods like url(string $path, string $preset, array $overrides = []) and srcset(string $path, array $presets). The bridge API is transform-option shaped and CDN-specific; your app should speak presets only. This is exactly what the new Add presets feature is for — define presets once in config/image.php, and swapping Cloudinary for imgix later becomes a config change plus one binding.
Preset-driven Blade component. Build a <x-image :path="$model->image" preset="hero" /> that reads the preset from config, calls the bridge, and renders srcset/sizes. Keep the bridge call in the component class, not the template, so you can cache and test it.
Resolve signed URLs at render time, not at upload time. With KeyCDN Secure Token (and Cloudinary's signed URLs), the token is time-bound. Store the asset key in the database, never the signed URL. If you use Cache::remember on rendered fragments, either exclude signed URLs from the cache or set the TTL below the token TTL.
Model the path, hide the domain. Persist products/abc.jpg, let the bridge prepend the CDN base. This keeps local dev (public disk), staging, and prod interchangeable and avoids stale absolute URLs in your database.
Layer it onto the upload pipeline. After your existing upload job writes the file, you don't need to generate derivatives — the CDN does that on first request. Use a queued "warmup" job that Http::get()s the 3–4 preset URLs for new hero images so the first visitor doesn't pay the transform cost.
Testing. Bind a trivial fake implementing your ImageUrl contract in TestCase::setUp() so feature tests assert on preset names, not on signed query strings. For unit tests of URL construction, instantiate the real bridge directly with dummy credentials — they're pure string builders and need no network.
ImageInterface/Picture machinery assume Twig plus symfony/asset for versioning. In Laravel you either install twig/twig and fight two view layers, or (recommended) ignore the component entirely and drive the bridges directly. Budget an hour to read the bridge source before committing.config/packages/ux_image.yaml if you run composer require with Flex present in a mixed repo. Keep credentials in config/services.php and a small config/image.php of your own.fit, gravity, dpr, and format negotiation are spelled differently per provider. Keep overrides to a documented subset if you care about portability.utm_*-style params after signing invalidates the URL and you get a 403 with no useful body. Log the exact unsigned URL on failure and compare byte-for-byte.srcset + signed URLs + HTTP cache. Each preset gets a different token, so cache keys explode and edge hit-rate drops. Prefer path-based signing or long TTLs on presets, and short TTLs only on genuinely private assets.IMAGE_CDN=null) so php artisan serve doesn't require network access for every page render.^3.6 and read the bridge changelog on every minor; internal bridge constructors have already churned once.How can I help you explore Laravel packages today?