beeyev/thumbor-php
A Laravel-friendly PHP client for Thumbor, the open-source image processing service. Build signed and unsigned Thumbor URLs fluently, with support for resizing, cropping, filters, and security keys. Works cleanly with Laravel and plain PHP projects.
Requirements: PHP 8.0+ (2.1.0 dropped 7.x — pin ^2.0.3 if you're stuck on 7.4), Composer, and a reachable Thumbor server with a SECURITY_KEY. The package is a URL builder only; it never talks to Thumbor over HTTP, so nothing works until a Thumbor instance (usually Docker) is running.
composer require beeyev/thumbor-php
Where to look first:
Thumbor client class — constructor takes the security key and the server base URL.$thumbor->get($source) — this is where all your day-to-day work happens (resize, fitIn, smartCrop, addFilter, format, quality).getUrl() (and __toString()), which returns the finished signed URL.Minimal first use case — turning a stored image path into a signed, resized thumbnail:
use Beeyev\Thumbor\Thumbor;
$thumbor = new Thumbor(
config('services.thumbor.key'),
config('services.thumbor.url'), // https://img.example.com
);
$url = $thumbor->get('https://cdn.example.com/products/123.jpg')
->resize(600, 400)
->format('webp')
->quality(80)
->getUrl();
Put the two values in config/services.php (or a dedicated config/thumbor.php) and source them from .env:
'thumbor' => [
'url' => env('THUMBOR_URL', 'http://localhost:8888'),
'key' => env('THUMBOR_KEY', ''),
],
Then bind it once as a singleton in AppServiceProvider::register() so you inject a typed dependency instead of new Thumbor(...) in every controller:
$this->app->singleton(Thumbor::class, fn () => new Thumbor(
config('services.thumbor.key'),
config('services.thumbor.url'),
));
First real feature to ship: product/post thumbnails in a Blade view, because it proves the full chain (config → signature → thumbor → source reachability) in one go.
Wrap it in a small service, not in views. The package API is a fluent URL grammar; if you sprinkle it through Blade templates you've coupled your whole app to it. A thin App\Support\ImageUrl class that accepts (string $source, int $width, int $height, array $options = []) and returns a string pays for itself the first time you change CDN or add a default filter.
Blade usage via a helper or component. Inject the wrapper into a <x-thumb-image> Blade component so templates only speak in image + size:
<x-thumb-image :src="$product->image_url" width="600" height="400" class="rounded" />
Responsive images / srcset. The builder is deterministic, so generating a set of breakpoints is a loop, not a query:
$srcset = collect([320, 640, 1280])
->map(fn ($w) => $this->imageUrl->resize($w, null) . " {$w}w")
->implode(', ');
This is the single highest-value pattern for Laravel apps — one source image in the DB, N derivatives at render time.
API resources. Emit derived URLs from your JsonResource classes rather than storing them. Store the source path only. Signed URLs are cheap to rebuild and would go stale if you rotate THUMBOR_KEY or move servers.
Default policy object. Thumbor URL params are order-sensitive for caching; centralise them. A ThumborPreset enum-ish set of methods (thumbnail(), hero(), avatar()) that applies fitIn + format('webp') + quality(80) consistently stops every developer inventing their own crop behaviour and fragmenting your CDN cache.
Queues and jobs. Never pass the client or builder into a job payload — pass the source string and scalar params, and rebuild inside the job. The builder holds state and won't survive a sane serialization boundary.
Testing. You're testing strings, not I/O. Assert on the generated URL (snapshot or substring assertions on filter segments). One integration smoke test that Http::get($url)->successful() against a real/dev Thumbor catches filter-name typos that the builder happily accepts.
Local dev. Add a Thumbor service to docker-compose.yml, mount an uploads volume so the server can reach your sample images, and set THUMBOR_URL/THUMBOR_KEY to match its SECURITY_KEY. Also set them in .env.testing so CI doesn't need a live server for URL assertions.
composer.json on any app still on 7.4 before bumping.SECURITY_KEY differ, the URL builds fine and thumbor returns 400/404. Classic causes: trailing whitespace in .env, the key quoted in .env but the quotes surviving, or an empty key on one side only (empty key = unsigned URLs, which work until you enable signing).localhost, relative paths, and private S3 objects will 404 even though they work in your browser. If your images live on a private bucket, put Thumbor behind signed S3 URLs or object storage it can read.urlencoded URL produces a valid-looking but permanently 400ing link — and it's the most common "it works in Postman" bug with this package.addFilter('brigthness', 60) is accepted by the builder and rejected by Thumbor at fetch time. Keep one real smoke test, or validate filter names against a known list in your wrapper."0" and 0 mean different things in Thumbor's grammar (0 = auto/original in several params). Cast to int and validate ranges before they hit the builder.env() outside config files is a trap, as always: once php artisan config:cache runs, a runtime env('THUMBOR_KEY') returns null and you get silently unsigned URLs. Read only from config().https:// in production (mixed-content blocking is silent until you open devtools), and drop the trailing slash.vendor:publish; write the six-line binding above.Preset/ImageUrl service as described above. That decorator is also where you'd swap CDN hostnames per environment.curl -I it directly, and compare against an unsigned URL (empty key both sides) if it 4xxs. If the unsigned one works, the problem is always encoding or key — never the builder.How can I help you explore Laravel packages today?