Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Imagine Bundle Laravel Package

liip/imagine-bundle

LiipImagineBundle is an image manipulation toolkit for Symfony. Define filter sets in YAML or XML to apply thumbnails, scaling, cropping, flips, and more, with post-processors, caching, and responsive image support.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer; the Symfony Flex recipe wires up the config file and the media/cache route for you.

composer require liip/imagine-bundle
# Flex creates config/packages/liip_imagine.yaml and config/routes/liip_imagine.yaml

If Flex is not in play, import the bundle routing (@LiipImagineBundle/Resources/config/routing.xml) or every generated image URL will 404.

The minimum viable configuration is a loader (where source images live), a resolver (where derivatives are written and how their URLs are built), and at least one filter set:

# config/packages/liip_imagine.yaml
liip_imagine:
    driver: gd

    loaders:
        default:
            filesystem:
                data_root: '%kernel.project_dir%/public'

    resolvers:
        default:
            web_path:
                web_root: '%kernel.project_dir%/public'
                cache_prefix: media/cache

    filter_sets:
        thumb_sm:
            quality: 82
            filters:
                auto_rotate: ~
                thumbnail: { size: [160, 160], mode: outbound }
                strip: ~

Your first use case is almost always a thumbnail in a template:

<img src="{{ 'uploads/products/42/photo.jpg'|imagine_filter('thumb_sm') }}" alt="Product">

Look first at bin/console debug:config liip_imagine to confirm the loader/resolver wiring, then at the filter reference (thumbnail, scale, crop, background, watermark, strip, auto_rotate, interlace) and the post-processor reference (jpegoptim, mozjpeg, optipng, pngquant).

Understand the request lifecycle before you debug anything: the Twig helper only computes a URL (/media/cache/thumb_sm/uploads/products/42/photo.jpg). Nothing is generated until a browser actually requests that URL; the bundle controller then applies the filters, stores the result via the resolver, and redirects to the static file. Subsequent hits never touch PHP.

Implementation Patterns

Treat filter sets as a design system, not ad-hoc parameters. Define a small, named catalogue once and reference it everywhere: avatar_xs, thumb_sm, thumb_md, card, hero, og_image. Every filter set name is part of your URLs and therefore part of your cache keys — renaming one invalidates the world.

Responsive images with srcset. Multiple filter sets beat runtime config for anything a template renders repeatedly:

<img src="{{ image.path|imagine_filter('thumb_md') }}"
     srcset="{{ image.path|imagine_filter('thumb_sm') }} 160w,
             {{ image.path|imagine_filter('thumb_md') }} 480w"
     sizes="(max-width: 600px) 160px, 480px"
     width="480" height="480" alt="{{ image.alt }}">

Use imagine_filter_path() when you want a root-relative path without the asset machinery (useful for <picture> sources, JSON payloads, or CSS custom properties). Reserve format: webp for a dedicated filter set and serve it through <picture> with the JPEG set as a fallback.

Build URLs in PHP, not in Twig, when the source is an entity. A tiny service keeps templates dumb and makes the endpoint reusable from an API:

final class ImageUrlBuilder
{
    private const SETS = ['thumb_sm', 'thumb_md', 'hero'];

    public function __construct(private CacheManager $cacheManager) {}

    /** @return array<string, string> */
    public function urls(string $path): array
    {
        $urls = [];
        foreach (self::SETS as $filter) {
            $urls[$filter] = $this->cacheManager->getBrowserPath($path, $filter);
        }
        return $urls;
    }

    public function isReady(string $path, string $filter): bool
    {
        return $this->cacheManager->isStored($path, $filter);
    }
}

getBrowserPath() is cheap and side-effect free; isStored() is what you want for "show a placeholder until the derivative exists" flows.

Version your paths and invalidate aggressively. When a user replaces photo.jpg, the derivative at the old cache path is stale forever. The two workable patterns are (a) content-addressed paths — uploads/products/42/photo-{sha1}.jpg — so a new file is a new URL, and (b) explicit purge on write:

$this->cacheManager->remove('uploads/products/42/photo.jpg', ['thumb_sm', 'thumb_md', 'hero']);

or from the CLI:

php bin/console liip:imagine:cache:remove uploads/products/42/photo.jpg --filter=thumb_sm

Warm the cache at deploy time. First-hit generation is slow and, on a cold CDN, users see it. Add a deploy hook that resolves every filter for every path (or only for recently changed content):

php bin/console liip:imagine:cache:resolve uploads/products/42/photo.jpg --filter=thumb_md

Use runtime config for one-off admin previews only. It is excellent for a "preview these crop values" screen, and terrible for anything user-facing and repeatable:

{% set runtime = { thumbnail: { size: [50, 50], mode: 'outbound' } } %}
<img src="{{ path|imagine_filter('thumb_sm', runtime) }}">

Split resolvers when you outgrow the local filesystem. One web_path resolver for local dev, one Flysystem/S3 resolver (with an S3-backed root_url or CloudFront domain) for production, selected per filter set (resolver: s3) or per call (imagine_filter(path, 'hero', {}, 's3')). The generation path stays identical; only storage and URL construction change.

Custom filters are a 20-line class. Implement LoaderInterface, tag it, and it becomes configurable like a built-in:

final class BlurLoader implements LoaderInterface
{
    public function load(ImageInterface $image, array $options = []): ImageInterface
    {
        return $image->effects()->blur((float) ($options['radius'] ?? 3));
    }
}
services:
    App\Imagine\BlurLoader:
        tags: [{ name: liip_imagine.filter.loader, loader: my_blur }]

The same pattern applies to post-processors (PostProcessorInterface + tag liip_imagine.filter.post_processor), loaders (liip_imagine.binary.loader), and cache resolvers (ResolverInterface + tag liip_imagine.cache.resolver).

Gotchas and Tips

intercept_redirects breaks images in dev. The controller redirects to the generated file; the web profiler swallows that redirect and you get broken images. Set web_profiler.intercept_redirects: false in config_dev.yaml — this is the single most common "the bundle doesn't work" report.

Do not wrap the path in asset(). The loader resolves paths relative to data_root, not relative to your asset manifest. asset() may append a version query string (?v=3) that pollutes the generated URL while leaving the cache key unchanged — a silent stale-image bug. Pass the raw relative path ('uploads/foo.jpg'), and let the resolver produce the public URL.

Runtime config does not reliably change the cache path. The resolver keys derivatives on filter name + source path, so tweaking runtime options can keep serving the old file. Clear the cache after changing runtime options, and promote anything permanent into its own named filter set.

Re-uploading the same filename serves the old thumbnail. Cached derivatives are never invalidated by the source file changing. Either version the path or purge on update — pick one before launch, not after.

Filter order is execution order. YAML maps are ordered, so watermark before thumbnail and watermark after thumbnail produce very different images. Also note mode: outbound crops to fill, mode: inset pads to fit; add upscale: false if you don't want small sources blown up.

Add auto_rotate: ~ to every set that handles user uploads. EXIF-rotated phone photos otherwise render sideways, and the complaint arrives as "your site rotates my images", not as a stack trace. Pair it with strip: ~ to drop EXIF metadata (privacy plus a few KB per file).

Watch PHP memory on large sources. A 6000×4000 JPEG can need well over 100 MB per manipulation with GD. Prefer Imagick with explicit policy.xml limits for big uploads, or reject oversized inputs before they reach the bundle. Quality post-processors additionally require the binaries (jpegoptim, pngquant, optipng, cwebp) to be present in the container image — missing binaries mean silently unoptimized output or a hard failure, depending on version.

Multi-server deployments need shared storage. The web_path resolver writes to the local filesystem; behind a load balancer, half your requests hit a box without the derivative and regenerate it, and purging only touches one node. Move to a Flysystem/S3 resolver + CDN, or guarantee a shared mount, before scaling out.

The cache: ~ line in the README boilerplate is a legacy artifact. It defines a filter set literally named cache with no filters. Ignore it; you only need resolvers, loaders, and filter_sets.

Debugging checklist. Confirm the route exists (bin/console debug:router | grep imagine), confirm config (debug:config liip_imagine), confirm the source path is right relative to data_root (a wrong path surfaces as a 404 from the controller, not from the template), check var/log/dev.log for image-open errors, and try the CLI resolve to isolate "generation failed" from "routing/URL failed". Ensure public/media/cache is writable by the web user and gitignored.

Treat filter names and runtime config as server-side only. Never accept a filter name or a runtime config array from user input — it lets anonymous callers trigger arbitrary, expensive image operations. Keep the source path constrained (IDs, not raw strings) and prefer a fixed set of named filters exposed by your own code.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
thomas-0816/doctrine-dbal-duckdb
moyisoft/laravel-admin
oxygen/mod-pages
oxygen/mod-import-export
oxygen/mod-media
ksoft/laravel-contentbuilderjs
ourgoldpackages/exchange
mikkimike/laravel-exchange1c
wandesnet/mercadopago-laravel
tsfcorp/email
larafony/core
laravel-enso/currencies
tartan/laravel-online-payment
wedevbr/celcoin-laravel
surazdott/nepali-date
devsquad-cockpit/laravel
coding-socks/laravel-upload-handler
artisanbarista/laravel-shield
instance-code/repository
flooris/prestashop-models