liip/rokka-imagine-bundle
Symfony bundle integrating LiipImagineBundle with Rokka, so image transformations and delivery run through Rokka’s image CDN. Configure Rokka as the Imagine loader/cache to serve optimized, responsive images without local processing.
This bundle is effectively unmaintained, but no longer frozen: release 1.2.0 added Symfony 5 support after a multi-year gap. Read that narrowly — one dependency-constraint bump is a compatibility patch, not resumed maintenance. There is no roadmap, no PHP 8.x statement, no LiipImagineBundle major bump, and no Flex recipe. Treat it as legacy code you are inheriting, not something to reach for on greenfield work.
Before writing any application code, do three things in order:
liip/imagine-bundle 2.x — a different config shape from the Symfony 3/4-era README. Run composer why-not against your symfony/framework-bundle, liip/imagine-bundle, and php versions. If you are on Symfony 6 or 7, expect a conflict; stop and go straight to the migration path in Gotchas. If you are on Symfony 5, note that 5.4 left security support in November 2024, so "supported" here means "compatible with an EOL major."DependencyInjection/Configuration.php is the authoritative source for config keys — check whether the 1.2.0 tree moved to the resolvers/loaders naming or kept the older cache/data_loader keys, because the README examples likely predate the change. Cache/Resolver/RokkaResolver.php (or similarly named) tells you exactly what URL it emits and whether it uploads, caches, or just links. Also check whether the resolver implements Liip's newer ResolverInterface signature (getBrowserPath) or the legacy resolve — that determines whether it works at all on Liip 2.x.config/bundles.php), set ROKKA_API_KEY / ROKKA_ORGANIZATION, point the Liip cache resolver at the rokka implementation, and render a single known-good hash from a stack you created in the rokka dashboard. Do this on a scratch branch so a failed install doesn't block anything.The first genuinely useful use case is unchanged: you already have LiipImagineBundle filter sets (thumbnails, banners, responsive variants) and you want the rendered imagine_filter URLs to point at rokka's CDN instead of web/media/cache. That's the whole value proposition. Everything else — stack management, uploads, invalidation — you will still wire up yourself.
# config/packages/liip_imagine.yaml (shape varies by Liip version — verify)
liip_imagine:
resolvers:
rokka:
rokka:
api_key: '%env(ROKKA_API_KEY)%'
organization: '%env(ROKKA_ORGANIZATION)%'
stack: 'thumbnails'
cache: rokka
filter_sets:
thumb:
filters:
thumbnail: { size: [120, 90], mode: outbound }
Twig usage is unchanged — that's the point:
<img src="{{ image.hash|imagine_filter('thumb') }}">
If the generated URL is not https://<org>.rokka.io/<stack>/<hash>?..., you are not actually hitting the resolver; debug the service wiring before debugging anything else.
One stack per filter set. Rokka stacks are named, ordered lists of operations. The cleanest mapping is a 1:1 relationship between a Liip filter set and a rokka stack name, so thumb → stack thumb, banner → stack banner. This keeps the Liip config readable, makes cache invalidation per-variant obvious, and lets you change a stack's operations server-side without redeploying the application.
Keep filter definitions minimal and let rokka do the work. The 1.2.0 bump did not add filter translation, so the converter is still the old one. Anything exotic (custom Imagine filters, PHP closures, watermarking with local fonts) either won't translate or will silently diverge from what you'd get locally. Prefer standard operations — resize, crop, autoformat, dpr, quality — and verify visual output against a local Imagine render at least once per filter set.
Treat upload as a separate concern. A resolver emits URLs; it does not put bytes in rokka. The data loader path is fragile (hash must match rokka's SHA-1-of-contents expectation), and on Liip 2.x the data-loader interface changed, so don't count on it. In practice, upload explicitly on write:
final class UploadImageToRokka
{
public function __construct(private RokkaClient $client) {}
public function __invoke(string $localPath, string $name): string
{
$image = $this->client->getImageService()
->upload($localPath, $name, $this->client->getUser(), true /* override */);
return $image->getHash();
}
}
Do this from a Doctrine listener, a Messenger handler, or an admin action — anywhere that has the file and can fail loudly. Store the returned hash on the entity and render from that. This is the single most important integration decision: do not depend on the bundle to upload lazily.
Persist hashes, not paths. Once images live in rokka, the hash is your stable identifier. Entity columns like image_hash (string, indexed) let you build URLs without filesystem access, which is what makes the CDN worth using. Keep the original filename in a separate column for humans.
Invalidate by hash, not by URL. Rokka caches by content hash and stack. Changing a stack's operations changes the URL query string, so old variants naturally fall out of use. To force a real purge, use the rokka client directly (deleteImage, or re-upload with override), not the bundle's cache-removal command — the Liip liip:imagine:cache:remove semantics assume a filesystem resolver and may no-op.
Test the URL contract, not the image. Unit tests should assert the resolver produces the expected https://<org>.rokka.io/<stack>/<hash> shape for given config. Visual regression against live rokka is flaky and rate-limited; run it in a scheduled job, not in CI on every PR.
Scope the API key. Rokka keys can be read-only or read-write per organization. Create two: a write key for the upload path (server, never exposed) and a read-only key for anything that only generates URLs. Rotate via secret manager, not .env committed to the repo.
php constraint (a Symfony 5 bump often rides on ^7.2, which would leave PHP 8.2/8.3 untested), and whether the rokka/client and liip/imagine-bundle constraints moved with it. Pin exact versions once you've found a working combination and vendor the diff if you need a one-line fix — one release does not mean a maintainer is watching the issue tracker.resolvers/loaders/cache/data_loader across majors and changed the resolver and data-loader interfaces. If you copy README config onto Liip 2.x, keys get silently ignored and you'll get local filesystem cache paths while assuming the resolver is broken. Read Configuration.php and mirror the method signatures in the resolver class.401 almost always means wrong API key or wrong organization. 404 on an image means the hash doesn't exist in that org — usually because the upload path wasn't run, or the hash was computed locally and differs. Log the full URL and the hash on failure; the bundle's exceptions are terse.mode: outbound semantics differ between Imagine and rokka operations. Pick one source of truth per filter and spot-check portrait-oriented photos with EXIF rotation.liip:imagine:cache:resolve / :remove are misleading here. They're built around Liip's filesystem cache. With the rokka resolver they may partially work or no-op. Don't build deploy pipelines around them; pre-warm by hitting the URLs or uploading explicitly.config/bundles.php and confirm the service IDs (liip_imagine.cache.resolver.rokka or similar) with debug:container --show-private | grep -i rokka. If the service isn't registered, the config was wrong upstream — most often a key name from the Liip 1.x era.decorates:) and rewrite the returned URL — for example to add format negotiation (?fm=auto) or signing. A compiler pass can also replace the stack-operation converter if you need a filter it doesn't understand.How can I help you explore Laravel packages today?