Skip to content

Storage

The Variant Store — where generated files actually live — is deliberately never called a "cache" in this codebase: cache means the HTTP/fragment cache (see Caching), a different concern entirely. A variant, once written, is treated as a durable artifact you can regenerate on demand, not an ephemeral cache entry.

Configuring it

yaml
progressive_image:
    variant_store:
        storage: 'oneup_flysystem.variants_filesystem' # any League\Flysystem\FilesystemOperator service
        prefix: ''
        public_url_prefix: '/media/pgi'
        fail_marker_ttl: 300

variant_store.storage is a service id resolving to any League\Flysystem\FilesystemOperator — local disk, S3, or anything else Flysystem supports. This is the only switch you need to move between local and cloud storage — there's a single VariantStorage implementation, FlysystemVariantStorage, and it works identically for either, because the whole storage strategy lives in which Flysystem adapter you wire up, not in a different PHP class.

yaml
# requires oneup/flysystem-bundle
oneup_flysystem:
    adapters:
        variants.adapter:
            local:
                directory: '%kernel.project_dir%/public/media/pgi'
    filesystems:
        variants_filesystem:
            adapter: variants.adapter

Leaving variant_store.storage unset keeps the bundle in "legacy" mode: no generation ever happens, and <twig:pgi:Image> only computes responsive attributes for the source path as-is.

Layout

Every path is built by VariantPath::for(id, source, format) — the single source of truth used consistently by storage, the serving controller, and the URL generator:

{format}/{first 2 chars of id}/{full id}/{source-path}.{ext}

e.g. a webp variant of uploads/hero.jpg might live at webp/ab/abCdEf0123.../uploads/hero.jpg.webp. The format-first layout means deleting an entire format's worth of variants (e.g. after dropping avif support) is one directory delete; the 2-character shard prevents any single directory from accumulating unbounded entries as more sources/specs get generated.

FlysystemVariantStorage::publicPath() percent-encodes each path segment and prefixes it with variant_store.public_url_prefix — that's the URL your web server or CDN needs to be able to serve directly from the underlying storage, without touching PHP.

Fail markers

If generation throws, GenerateVariantHandler writes a sibling <path>.failed file containing a Unix timestamp instead of the image itself. While that marker is "fresh" (younger than variant_store.fail_marker_ttl seconds, default 300), further generation attempts for the same VariantPath short-circuit immediately instead of retrying a source that's currently broken — this throttles repeated failures (e.g. a temporarily-unreachable remote source) without needing a separate circuit breaker. A corrupted or empty marker file is treated as "no marker" rather than silently parsed as epoch 0, so throttling can never get permanently stuck on.

Atomic writes

Both the variant itself and its fail marker are written atomically: bytes go to a random <path>.tmp-<random> name first, then move()d into place, so a concurrent reader never sees a partially-written file. If the move fails, the orphaned temp file is cleaned up on a best-effort basis (and the cleanup failure itself is logged, never silently swallowed) before the original error is rethrown.

Implementing your own VariantStorage

You normally don't need to — swap the underlying FilesystemOperator instead. If you genuinely need a non-Flysystem backend, see Custom Storage Backend, which also covers the shared VariantStorageContractTest suite every implementation (including the built-in one) is verified against.

Released under the MIT License.