Caching
There are two, deliberately separate, caching concerns in this bundle. Don't confuse them: the Variant Store (Storage) holds the generated image files themselves and is never called a cache in this codebase. This page is about the other one — caching the rendered HTML of <twig:pgi:Image>.
Enabling fragment caching
progressive_image:
image_cache_enabled: true
image_cache_service: 'cache.my_cache' # must be a tag-aware pool
ttl: 3600# config/packages/framework.yaml
framework:
cache:
pools:
cache.my_cache:
tags: trueimage_cache_service must resolve to a Symfony\Contracts\Cache\TagAwareCacheInterface (or carry the cache.taggable tag) — this is checked at compile time (CheckCacheInterfacePass), so a misconfigured pool breaks cache:clear, not a live request.
How it works
TransparentImageCacheSubscriber hooks into Symfony UX Twig Component's own render lifecycle (PreCreateForRenderEvent/PreRenderEvent) and transparently wraps everypgi:Image render, without any change to your templates:
onPreCreate— computes a cache key from an md5 hash of the component's raw input props (src,sizes,filter,context, ...) and checks the cache. On a hit, the cached HTML is used directly and the component never even mounts.onPreRender(only reached on a miss) — swaps the template fortemplates/cache_wrapper.html.twig, which includes the real component output through thepgi_cache_saveTwig filter (TransparentCacheExtension). That filter stores the rendered HTML under the same key, taggedpgi_tag_<md5(src)>— so invalidating a specific source's cached HTML iscache.my_cache->invalidateTags(['pgi_tag_'.md5($src)]).
Both stages key off the component's raw input props (not post-mount defaults), so two images with the same effective config but props supplied in a different order still hit the same cache key deterministically (props are always the same associative structure).
Pending variants are never cached
If a render dispatched a variant generation that's still pending (PendingGenerationTracker::hasPending()), both cache paths skip it:
pgi_cache_savereturns the content unsaved — caching now would freeze the original-image fallback into the fragment cache long after the real variant becomes ready.ResponseCacheOverrideListener(akernel.responselistener at priority-1024, so it runs after Symfony's own#[Cache]attribute listener and wins) forces the whole HTTP response toCache-Control: no-store, no-cache, must-revalidate, private, max-age=0+Surrogate-Control: no-store, and stripsETag/Last-Modified/Expires— regardless of what your controller or#[Cache]attribute said. This protects browser caches, reverse proxies, and CDNs alike from caching a page that's currently showing a stand-in image.
This is exactly why Storage and this page are separate concepts: a "pending" state only exists in the Variant pipeline, but it needs to reach into this HTML/HTTP caching layer to avoid freezing a stale render.