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

Imgproxy Laravel Package

hosmelq/imgproxy

A Laravel package for generating signed imgproxy URLs on the fly. Configure presets, resizing, cropping, and format options, then build secure image transformation links from a fluent API for models or storage disks.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up. The package is framework-agnostic PHP (not a Laravel package — no service provider, no auto-discovery), so the first step is composer require hosmelq/imgproxy-php followed by a small binding of your own. It targets modern PHP (8.1+), so it will drop into any current Laravel app cleanly.

Where to look first, in this order:

  1. README.md — the canonical list of supported processing options and the builder entry point.
  2. src/Imgproxy.php (or equivalent root class) — the configuration/factory surface.
  3. src/Options/ — one class per imgproxy processing option (Resize, Size, Gravity, Dpr, Quality, Format, Background, Blur, Watermark, Trim, Rotate, Expires, Filename, Cachebuster, Preset, …). This directory is the API; skim it before writing anything custom.
  4. The signer/encryption class — worth reading once so you understand exactly what bytes get HMAC'd.

Config. Add a config/imgproxy.php and mirror the imgproxy server's own env vars:

// config/imgproxy.php
return [
    'base_url' => env('IMGPROXY_BASE_URL', 'http://localhost:8080'),
    // hex strings, exactly as given to IMGPROXY_KEY / IMGPROXY_SALT
    'key'  => env('IMGPROXY_KEY'),
    'salt' => env('IMGPROXY_SALT'),
];

First use case — a thumbnail in Blade. Bind a configured instance once, then build URLs anywhere:

// app/Providers/AppServiceProvider.php
$this->app->singleton(Imgproxy::class, fn () => Imgproxy::configure()
    ->withBaseUrl(config('imgproxy.base_url'))
    ->withKey(config('imgproxy.key'))
    ->withSalt(config('imgproxy.salt'))
);
$url = app(Imgproxy::class)
    ->url($post->cover_url)
    ->withOptions(Resize::make(width: 600, height: 400))
    ->build();
<img src="{{ $url }}" alt="{{ $post->title }}" width="600" height="400">

If that renders an image, everything else is just adding option objects. If it 403s, jump straight to the Gotchas section — it is almost always key/salt encoding.

Implementation Patterns

One container binding, injected everywhere. Don't call Imgproxy::configure() at call sites — you'll duplicate config and drift. Bind a singleton and inject Imgproxy into controllers, API resources, Blade components, and jobs. Because the instance is stateless per-URL (the builder is what carries state), a singleton is safe.

A thin app-level wrapper is worth the ten lines. Wrap raw URL building in something like App\Support\ImageUrl with domain methods (thumbnail(), hero(), avatar()) so the rest of the codebase never touches imgproxy option classes. This is your seam for swapping CDN hostnames, adding expires, or changing default quality in one place.

Presets beat long option chains. If your imgproxy server is configured with IMGPROXY_PRESETS, prefer emitting the preset name over re-sending every option. Shorter URLs, fewer signature bytes, and the tuning lives in server config where ops can change it without a deploy.

Signed vs. unsigned by environment. Use real key/salt in staging and production; use imgproxy's insecure/plain mode (or a local unsigned instance) in tests and local dev. Abstract this behind the environment so the same call site produces /{sig}/... in prod and the insecure form locally — otherwise every feature test needs secrets baked into phpunit.xml.

expires is the pattern for anything user-facing. For image URLs embedded in emails, PDFs, or anything that leaves your origin, add Expires::make(now()->addDays(7)->timestamp). imgproxy returns 403 past the deadline, which is exactly the behaviour you want for leaked URLs — it degrades gracefully to a broken image rather than a permanent hotlink.

cachebuster for content that changes in place. If a user re-uploads avatar.jpg to the same path, the imgproxy URL is byte-identical and your CDN keeps serving the old render. Push a version (updated_at timestamp, file hash) into Cachebuster and bust it deliberately rather than by hoping.

Generate srcset from one source, not five. imgproxy's Dpr option plus a loop over widths gives you a responsive set without storing derivatives:

$builder = $imgproxy->url($post->cover_url);
$srcset = collect([400, 800, 1200, 1600])
    ->map(fn ($w) => $builder->withOptions(Resize::make(width: $w))->build()." {$w}w")
    ->implode(', ');

Watch out: if the builder mutates in place rather than returning a clone, this loop will accumulate options. Check whether withOptions() is immutable; if not, rebuild from $imgproxy->url(...) each iteration.

Model accessors and API resources are the natural home. $post->thumbnail_url (accessor) or a PostResource field is the right level of abstraction — controllers and views stay clean, and you get one place to add expires/cachebuster later. Avoid building URLs inside Eloquent query loops for lists; consider a small ImageUrlFactory resolved once per request.

Test the URL, not the HTTP call. These are pure string builders — assert on the path structure and on round-tripping a known key/salt/source through your signer. Snapshot-test a handful of representative builders (resize, preset, expires) so refactors to option ordering don't silently invalidate every cached URL in production.

Multi-tenant: multiple instances, not one. If you serve several brands/buckets with different imgproxy deployments or keys, register named singletons (imgproxy.brand_a) rather than threading a base URL through every call.

Gotchas and Tips

Hex vs. raw bytes is the #1 failure. imgproxy's IMGPROXY_KEY/IMGPROXY_SALT are hex strings. The signer must hex2bin() them before HMAC. If you pass the hex literal straight through, every URL 403s and the error message tells you nothing. If you hand-roll config, hex-decode at the boundary and only store raw bytes internally.

Base64 is URL-safe base64, no padding. imgproxy expects -/_ and strips =. PHP's base64_encode gives you +///= — a classic mismatch. The package handles this, but the moment you post-process or re-encode a URL yourself (e.g. urlencode() on the whole thing, or passing it through a router), you break the signature. Build it, then pass it around as an opaque string.

Signature covers the path, leading slash included. Any post-processing that alters the path — URL-encoding, collapsing //, appending a query string to the source — invalidates the signature. Keep source URLs base64-encoded to avoid escaping the source URL's own query string into the imgproxy path.

Diagnose by status code. 403 = signature/key/salt problem (or expired expires). 404 = the source URL isn't fetchable, or the host isn't in IMGPROXY_ALLOWED_SOURCES. 422 = an option name or value shape imgproxy doesn't recognise (often a version mismatch between the package's option set and your server build). Log the fully built path (with signature redacted if you're paranoid) when debugging — eyeballing the path segments finds 90% of issues.

Check package version against server version. Options are added to imgproxy over time. If an option class exists in the package but your server image is older, you get a 422. Pin the imgproxy Docker tag in docker-compose/k8s and bump it in the same PR as any new option usage.

Key rotation is a coordinated restart. imgproxy accepts a single key/salt pair. You cannot have two valid signatures simultaneously, so a rotation means a brief window where old URLs 403. Plan it: rotate during low traffic, and remember CDN-cached URLs keep working because your CDN holds the rendered bytes, not the signature. That's your safety net — but it also means you can't "flush and forget," since uncached requests will fail during the window.

Signed URLs are safe to expose; unsigned config is not. The whole point is that the signature is a capability, not a secret. Do not, however, ever ship the key/salt to the frontend — always build URLs server-side (SSR, Blade, API payloads) rather than signing in JavaScript.

Option ordering affects the URL string, not the render. imgproxy processes options order-independently, but the signature covers the literal path, so two builders emitting resize/.../dpr/... vs dpr/.../resize/... produce different URLs for identical output — and split your CDN cache. Let the package own ordering; don't hand-assemble option arrays.

width is physical pixels. Adding Dpr::make(2) doubles the output, so Resize::make(width: 400) + Dpr::make(2) yields an 800px-wide image. If you're generating srcset widths, don't also set dpr unless you specifically want retina variants of each step.

Extension point: implement the option contract. When imgproxy ships an option the package hasn't caught up with, you don't have to fork — implement the package's Option interface in an app-level class returning the correct path segment name and value. Keep these in your own namespace, and open a PR upstream. This is also the cleanest escape hatch for proprietary imgproxy builds.

AVIF/WebP via Format::auto() depends on the Accept header. imgproxy negotiates from the incoming request, so if you're generating URLs in a queued job (no request context), auto is meaningless — pin the format explicitly. And if your CDN normalises Accept, negotiation breaks silently and everyone gets JPEG.

Set IMPROXY_ALLOWED_SOURCES deliberately. The default is permissive in dev; in production, lock it to your storage hosts. Without it, imgproxy is an open image proxy and someone will use it to resize arbitrary URLs on your CPU budget.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package