onliner/imgproxy-php
PHP client for imgproxy that builds signed and unsigned image URLs through a fluent, chainable API. Supports resizing, cropping, gravity, format conversion, and Laravel integration, letting you serve optimized, secure images from your own imgproxy instance.
Three things must exist before the package is useful: a running imgproxy instance, a hex-encoded IMGPROXY_KEY, and a hex-encoded IMGPROXY_SALT. The library only builds and signs URLs — it never talks to the imgproxy server itself.
composer require onliner/imgproxy-php
Add credentials to config/services.php:
'imgproxy' => [
'url' => env('IMGPROXY_URL', 'http://imgproxy:8080'),
'key' => env('IMGPROXY_KEY'),
'salt' => env('IMGPROXY_SALT'),
],
First use case — a signed thumbnail URL:
use Onliner\ImgProxy\ImgProxy;
$client = new ImgProxy(
config('services.imgproxy.url'),
config('services.imgproxy.key'),
config('services.imgproxy.salt'),
);
$url = $client->url('https://cdn.example.com/products/42.jpg')
->width(800)
->height(600)
->fit('cover')
->format('webp')
->quality(80)
->build();
Where to look first:
src/ for the ImgProxy entry class and the option/URL builder — the fluent methods map almost 1:1 to imgproxy processing options, so the imgproxy docs are your second reference.key/salt as raw bytes (not the hex strings).If you only ever need one size, stop here. The rest of this assessment is about keeping signed URLs maintainable once they spread across views, jobs, and API resources.
Bind a singleton and inject it. Don't new ImgProxy(...) in controllers. Register it once so config changes and testing stay in one place:
// AppServiceProvider::register()
$this->app->singleton(ImgProxy::class, fn () => new ImgProxy(
config('services.imgproxy.url'),
config('services.imgproxy.key'),
config('services.imgproxy.salt'),
));
Centralize transformation presets. Scattering ->width(300)->format('webp') across Blade templates guarantees inconsistent cache keys and drift. Define named presets in config or an enum:
// config/images.php
'presets' => [
'thumb' => ['width' => 200, 'height' => 200, 'fit' => 'cover'],
'product' => ['width' => 1200, 'height' => 900, 'fit' => 'cover', 'format' => 'webp'],
'hero' => ['width' => 1920, 'fit' => 'cover', 'dpr' => 2],
],
Then a small application-level factory applies them, so presets stay the source of truth even if the package API changes.
Responsive srcset is the highest-leverage pattern. The library is just URL construction, so loop widths and emit a set:
$srcset = collect([320, 640, 1280, 1920])
->map(fn ($w) => $client->url($source)->width($w)->fit('cover')->format('webp')->build()." {$w}w")
->implode(', ');
Sign remote sources, not just local paths. Imgproxy accepts full upstream URLs, S3 references (s3://bucket/key), and local paths depending on how the server is configured. The package's "flexible path generation" is where this matters — pass the source exactly as imgproxy expects it, then let the library handle signing. For private buckets, prefer imgproxy-native source options (S3 auth via server-side config) over pre-signed upstream URLs, which expire and poison your CDN cache.
Blade directives / model accessors. Wrap the builder in a helper so views stay readable:
@imgproxy($product->image, 'product', ['alt' => $product->name])
For Eloquent, an imageUrl(string $preset) accessor keeps transformation logic off the model's public surface while still caching the built URL.
Job and API usage. Signed URLs are deterministic, so they are safe to generate in queued jobs and API resources. If the same URL is generated repeatedly, memoize per request; if you use signed URLs in emails or webhooks, generate them close to send time — they're cheap, but base URLs and credentials can rotate.
Testing. Assert against a fixed key/salt and snapshot the URL. Because signing is deterministic, a hash comparison catches accidental option reordering or encoding changes:
$this->assertSame($expected, $client->url($src)->width(300)->build());
IMGPROXY_KEY/IMGPROXY_SALT as hex strings. The signer needs raw bytes. If signatures fail with a 403, this is almost always why — confirm whether the package decodes hex internally or expects raw values, and don't paste the hex string straight in if it expects hex2bin().w:300/h:200 and h:200/w:300 produce different signatures and different CDN cache entries. Pick a canonical option order (a preset array, sorted) and stick to it, or you'll silently fragment your cache.//. Likewise, encoding an already-encoded source URL double-encodes it. Normalize base URL with rtrim($url, '/') and let the builder encode the source once.unsafe mode is dev-only. If the package exposes an unsigned/unsafe builder, gate it behind app()->isLocal(). Shipping it to production lets anyone generate arbitrary transformations and hotlink your origin storage.dpr=2 multiplies output dimensions server-side; if your CDN or layout assumes the requested width, images will be 2× larger than expected. Keep DPR out of presets used for fixed-size elements.webp/avif without Accept-based fallback leaves older clients with a broken image. Either generate a second signed URL in the fallback format or rely on imgproxy's auto-format (format: auto) if your server build supports it.->build(), or by composing the path yourself and using only the signing helper. Prefer that over forking — forking a 25-star package is a maintenance liability versus a thin wrapper.^ constraint in composer.json and run the test snapshot after upgrades, since option names and the signer are the surfaces most likely to shift.How can I help you explore Laravel packages today?