daun/statamic-placeholders
Statamic Placeholders lets you use dynamic placeholders in your Antlers templates, replacing them with values from entries, config, or custom resolvers. A lightweight addon for cleaner, more flexible content.
This package is the Laravel bridge for Intervention Image. It registers an ImageManager singleton in the container and exposes an Image facade, so you can read, transform, and encode images in a few lines instead of wiring up GD/Imagick by hand. It also generates placeholder images (returned as inline data URIs) for missing media. 5.0.0 is a major release: placeholders now default to WebP, and the driver layer is open enough to register custom Intervention drivers such as libvips. The explicit intervention/image requirement introduced in 4.2.0 still stands — the underlying library is a first-class dependency.
Install and verify versions first. A major bump will not arrive via ^4.2, so require it explicitly and check what resolves:
composer require <vendor>/<package>:^5.0
composer show intervention/image # confirm 3.x vs 4.x
composer why intervention/image # see who else constrains it
composer update --dry-run # read the diff before merging
If another package (e.g. a media library or an older image manipulator) still pins intervention/image ^3, Composer will resolve you to an older release of this package. That's the single most common reason "the new version won't install."
Where to look first:
config/image.php (publish with php artisan vendor:publish --tag=image-config) — the driver key, default options, and the placeholder settings (format, quality, size) that changed in 5.0.0.Image facade root — Image::read(), Image::create(), Image::animate(), Image::placeholder().Intervention\Image\ImageManager binding if you prefer constructor injection, and the driver registration point added in 5.0.0 if you want something other than gd/imagick.First use case — resize an upload and store it:
use Intervention\Image\Laravel\Facades\Image;
use Illuminate\Support\Facades\Storage;
$path = Storage::disk('public')->path('uploads/'.$filename);
Image::read($path)
->scale(width: 1200)
->toWebp(80)
->save(Storage::disk('public')->path("derived/{$filename}.webp"));
Then verify the driver actually matches production — GD and Imagick produce visibly different output, GD lacks some format support, and a custom driver (libvips) adds its own extension requirement. Run php -m | grep -E 'gd|imagick|vips' on every environment you deploy to.
Upgrading from 4.x — the one thing to check before you ship: every placeholder data URI your app emits changes from PNG (or whatever you had configured) to WebP. If you assert on placeholder strings, cache them, store them in the database, or embed them in HTML email, audit those paths first.
Inject the manager, not the facade, in long-lived or testable code. The facade is fine in controllers and Blade-adjacent helpers; prefer the container binding in services, jobs, and anything你 unit test:
public function __construct(private ImageManager $images) {}
public function handle(): void
{
$this->images->read($this->sourcePath)
->cover(400, 400)
->toJpeg(85)
->save($this->targetPath);
}
Keep transformations in a dedicated service or a value object. A ThumbnailProfile (width, height, format, quality, crop mode) passed to an ImageTransformer keeps controllers thin and gives you one place to bump quality defaults globally when you change config.
Queue anything larger than a few hundred KB. Image decoding is CPU-heavy and will blow through PHP-FPM timeouts on uploads. Use a job with an explicit $timeout and $tries = 1 (retrying a partial write leaves orphaned files), and dispatch after the upload is committed to disk:
ProcessImage::dispatch($media)->onQueue('images')->timeout(120);
Chain derivatives off one read. Decoding is the expensive part; generating three sizes from a single read() is far cheaper than three reads. Build the source once, then reuse it per variant:
$source = Image::read($path);
$source->scale(width: 1600)->toWebp(82)->save($largePath);
$source->scale(width: 800)->toWebp(80)->save($mediumPath);
Integrate with Storage rather than raw paths. For local disks use ->path(); for S3 and other remote disks never use ->path() — it doesn't exist or returns a temp path. Read the stream instead and write the encoded bytes back:
$bytes = Image::read(Storage::disk('s3')->get($key))->scale(width: 800)->toWebp();
Storage::disk('s3')->put("derived/{$key}.webp", (string) $bytes);
Use placeholders as a fallback, and cache the result. 5.0.0 makes placeholder generation cheaper (no duplicate processing pass) and produces smaller WebP data URIs. That makes it viable to generate placeholders on demand for listings, but they're still CPU work — memoize or cache the data URI per size/format rather than generating one per row per request:
$placeholder = Cache::remember("ph:400x400:webp", now()->addDay(), fn () => (string) Image::placeholder(400, 400));
If your front end expects a specific mime prefix (data:image/png;base64,), update it — the default is now data:image/webp;base64,.
Custom drivers (libvips and friends) are now a supported path. Instead of fighting GD for large images, register a custom driver and let the manager use it:
// config/image.php
'driver' => \App\Image\VipsDriver::class, // or a closure/factory returning an Intervention driver
// AppServiceProvider::register()
$this->app->extend(ImageManager::class, function (ImageManager $manager, $app) {
return new ImageManager($app->make(\App\Image\VipsDriver::class));
});
libvips is a strong win for large uploads — it streams and uses far less memory than GD — but it needs ext-vips/libvips installed on every box, including CI and queue workers. Add a boot-time check so a missing extension fails fast rather than on the first upload.
Cache-bust derivative URLs. Store a hash of the transform params in the filename (photo-800x800-a1b2c3.webp) so replacing the source invalidates the URL naturally. This is the pattern that scales best with CDN caching and avoids a lookup on every request.
Validate before decoding. A FormRequest rule on mimes:jpg,jpeg,png,webp,gif plus max:10240 will not stop a decompression bomb — also cap dimensions. Decoding a 20000×20000 PNG will exhaust memory before any size check you do in PHP runs.
Test with fixtures, not generated images. Commit a few small PNG/JPEG/WebP/GIF fixtures and assert on output dimensions and mime type. Keep one real integration test per format — stubs won't catch driver-specific encode failures, and they certainly won't catch a driver swap to libvips.
The WebP placeholder default is the 5.0.0 breaking change. Placeholders are data:image/webp;base64,… now. Consequences to check:
<img> are fine under a img-src 'self' data: CSP, but if your CSP pins a specific image mime list, add data: (mime-agnostic) rather than a per-format rule.Custom drivers shift the failure mode from "wrong pixels" to "missing extension" or "unsupported op." libvips and other custom drivers don't implement every Intervention operation identically, and some formats (animated GIF, CMYK JPEG, ICC profiles) behave differently again. If you swap drivers, re-run your per-format fixture tests and keep a documented fallback (driver can be made environment-aware) so a shared-hosting dev box without ext-vips still boots.
Dependency resolution remains the headline install risk. If your app or another package requires intervention/image ^3, installing 5.0.0 can silently downgrade your app's copy or fail outright. Run composer update --dry-run and read the diff before merging.
API surface changes between v3 and v4/v5. v3 replaced the old fluent ->resize(300, null, function ($c) {...}) callback style with named methods — scale(), cover(), contain(), pad(), resize(). v4 and v5 continue that direction and drop more legacy surface. Audit for old-style ->widen(), ->heighten(), ->fit(), ->crop(), ->encode() and ->response() calls; these are the ones that break loudly. Grep before upgrading:
grep -rnE '\->(fit|widen|heighten|encode|response|insert)\(' app/ resources/
Default encode quality is not what you expect. If you omit quality, you get the library default (typically ~75), not "best." Always pass quality explicitly for JPEG/WebP. Also check whether output type is inferred from the file extension — it is for save(), but ->toWebp() + a .png path will produce mismatched bytes and mime-type confusion in the browser.
EXIF orientation. Photos from phones carry orientation metadata. GD does not auto-rotate; you must call ->orient() (or equivalent) before resizing, or portraits come out sideways. This is silent — no exception, just wrong images. Test with an actual phone photo, not a Photoshop export.
Memory limits are the number-one production failure. A 12MP JPEG decode needs roughly width × height × 4 bytes plus overhead — well past the default 128M. Set memory_limit per-job (e.g. ini_set('memory_limit', '512M') in the job) rather than raising it globally. A custom driver like libvips reduces this substantially — that alone can justify the swap for large-upload apps.
GD vs Imagick divergence. Imagick handles animated GIFs, CMYK JPEGs, and ICC profiles properly; GD flattens and mangles them. If you accept GIF uploads and use GD, "animated" becomes "first frame, static." Pick Imagick or a capable custom driver in production, or explicitly reject formats your driver can't handle. Keep the driver config environment-aware so local and production behave the same.
Facade alias and config caching. After publishing config, php artisan config:cache will bake in the driver, placeholder format, and quality values. Changing them in .env on a running production box does nothing until you re-cache — a classic cause of "I switched to WebP placeholders and nothing changed" and, conversely, of a config-cache-baked PNG default masking your 5.0.0 upgrade.
Extension point — the manager binding. If you need custom driver options or a decorated manager, rebind Intervention\Image\ImageManager in a service provider rather than wrapping the facade everywhere. That's the clean seam for injecting a configured Imagick or libvips driver, registering decoders/encoders, or swapping in a stub during tests. 5.0.0's custom-driver support makes this the intended integration point rather than a hack.
Debugging strategy. When output looks wrong, dump before encoding — write the intermediate to a temp file with ->save('/tmp/debug.png') and inspect. When it throws, catch and log $e->getMessage() plus the source mime and byte size; the library's exceptions rarely tell you which input caused the failure, and in a queue worker you'll otherwise get an anonymous stack trace. For placeholders, log the resolved driver and format once at boot — it's the fastest way to confirm a driver/format change actually took effect.
Watch for orphaned files. save() is not atomic and it overwrites by default. Write to a temp path, then Storage::move() into place, so a crash mid-encode doesn't leave a truncated image served to users.
How can I help you explore Laravel packages today?