Skip to content

On-the-Fly Resolve Route

For most apps, the <twig:pgi:Image> component and pgi_filter() are enough — both trigger generation as a side effect of rendering a page. But some consumers can't render a Twig template at all: a headless frontend hitting the same media store, another app sharing the same storage, or a straight URL you want to hand out (e.g. a CDN origin-pull target). For those, /media/pgi/resolve/{filterSet}/{path} behaves like LiipImagineBundle's classic /media/cache/resolve/<filter>/<path>: hit it with a GET, it generates the variant synchronously if needed, and redirects to the result.

GET /media/pgi/resolve/thumb_small/uploads/hero.jpg
→ 302 Location: /media/pgi/jpeg/ab/ab12.../uploads/hero.jpg

The route is registered automatically wherever config/routes.php is imported (the same place pgi_variant_serve, the existing "wait" endpoint, comes from) — no extra config needed beyond having variant_store.storage and at least one filter_sets entry configured.

How it differs from the wait endpoint

pgi_variant_resolve (this route)pgi_variant_serve (the wait endpoint)
URL shape/media/pgi/resolve/{filterSet}/{path}/media/pgi/wait?source=...&width=...&height=...&...
Signed?NoYes — every request must carry a valid signature
PurposeA general "give me this filter of this image" URL you can hand out or link to directlyThe fallback_while_pending: wait redirect target, only ever built internally by QueryPendingUrlBuilder
GenerationAlways synchronous, in-request, regardless of generation.strategyAlways synchronous, in-request (same behavior)

Both always redirect and never stream bytes themselves — the next hit goes straight to the storage's own public URL (local disk served by nginx, or directly from S3/CDN) without touching PHP again, exactly like every other variant in this bundle.

Why it's safe to leave unsigned

The wait endpoint requires a signature because its query string is fully attacker-shaped — an unsigned version would let anyone request generation of arbitrary (width, height, filter, context) combinations, an easy way to force expensive, uncached generation work. This route avoids that by construction: {filterSet} must be the name of an entry you already defined in filter_sets (VariantSpecFactory rejects anything else with InvalidFilterDefinition), so a request can never do anything other than apply a filter set you already chose to expose, on whatever source path it names. That's the same trust boundary LiipImagineBundle's own resolve route has always had.

{path} itself is still caller-controlled, so generation can be triggered for any path — put this route behind the same caching layer (nginx/CDN) you'd put any other public endpoint behind if that's a concern for your traffic profile; once a given (filterSet, path) pair has been generated once, subsequent requests are storage hits and cost nothing beyond a redirect.

Released under the MIT License.