loevgaard/tinypng-bundle
Symfony bundle that integrates the TinyPNG/TinyJPG API to compress PNG and JPEG images, cutting file sizes while preserving visual quality. Configure your API key and optimize images through a simple service, with support for local files and remote URLs.
First, verify the ecosystem. loevgaard/tinypng-bundle is a Symfony bundle, not a Laravel package. In a Laravel app it provides nothing: no package auto-discovery, no service provider, no publishable config, no facade. It is type: symfony-bundle and depends on symfony/framework-bundle; bundles.php, Extension and Configuration classes are inert under Laravel. So the honest day-one step for a Laravel developer is:
composer show loevgaard/tinypng-bundle # read composer.json: type, symfony/* and tinify/tinify constraints
If that output says symfony-bundle, stop there and install the underlying SDK instead — the compression logic lives entirely in tinify/tinify, which is framework-agnostic.
Minimal Laravel setup (the path you will actually use):
composer require tinify/tinify
# .env
TINYPNG_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// config/services.php
'tinypng' => [
'key' => env('TINYPNG_API_KEY'),
'enabled' => env('TINYPNG_ENABLED', true),
],
// app/Providers/ImageServiceProvider.php
public function register(): void
{
$this->app->singleton(ImageOptimizer::class, function () {
Tinify\setKey(config('services.tinypng.key')); // global static — call once
return new TinifyOptimizer();
});
}
// the first use case: shrink an uploaded avatar
$optimized = $this->optimizer->optimize($file->getContent(), $file->getMimeType());
Storage::put($path, $optimized);
Where to look first. The tinify/tinify README is the real documentation — it covers fromFile/fromBuffer/fromUrl, toFile/toBuffer, resize, store and preserve. If you genuinely are on a Symfony target, then read the bundle's DependencyInjection/Configuration.php (the config tree keys) and Resources/config/services.xml (service IDs), then:
# config/packages/loevgaard_tinypng.yaml
loevgaard_tinypng:
api_key: '%env(TINYPNG_API_KEY)%'
Best first use case is not the upload request — it's an artisan command backfilling existing assets in public/ or storage/app/public/. You get measurable savings immediately, it runs out-of-band, and it validates your API key and quota without risking a user-facing path.
Wrap it behind an interface, immediately. Tinify's API is static (Tinify\fromBuffer(...)), which makes it untestable and unswappable. One interface, one implementation, one binding:
interface ImageOptimizer
{
public function optimize(string $bytes, string $mime = 'image/jpeg'): string;
}
Fake it in feature tests; swap in a local encoder later without touching call sites.
Prefer fromBuffer()->toBuffer() over fromFile()->toFile() in request paths. It avoids filesystem coupling (works with S3/Spaces without a tempfile dance), keeps the original intact, and is trivially fakeable in tests. Reserve fromFile/toFile for CLI batch jobs.
Always run compression on a queue. An HTTP round-trip to TinyPNG (5–10s worst case) has no business inside an upload request:
// in the upload controller
OptimizeImage::dispatch($asset->id)->onQueue('images');
// in the job
$bytes = Storage::get($asset->original_path);
$optimized = $this->optimizer->optimize($bytes, $asset->mime);
Storage::put($asset->optimized_path, $optimized);
$asset->update(['optimized_at' => now()]);
Deduplicate by content hash before you spend quota. A sha256 column plus a small lookup table means re-uploading the same image costs zero compressions:
$hash = hash('sha256', $bytes);
if ($cached = OptimizedImage::where('hash', $hash)->first()) {
return Storage::get($cached->path);
}
Batch/CI pattern. A command that walks a directory with a manifest file, only sends files whose hash isn't already recorded, writes results alongside originals, and exits non-zero if the API key is missing. Wire it into a deploy hook or build step.
Use store() to skip the write-back. Tinify\fromBuffer($b)->store(['service' => 's3', ...]) hands you a CDN URL directly, useful when you'd otherwise download-and-reupload. Credentials for the target bucket live in the options array (and in your TinyPNG account config) — treat them as secrets.
Resize only as a convenience. ->resize(['method' => 'fit', 'width' => 800]) exists, but this is a compressor, not a transformation layer. Keep crop/format negotiation/srcset in Intervention Image or spatie/image-optimizer.
Kill switch + graceful fallback. Check config('services.tinypng.enabled') and catch quota/auth failures in the optimizer, returning the original bytes. An outage should degrade to "unoptimized image served," never a failed upload.
Retry taxonomy. Catch Tinify\Exception\ConnectionException and ServerException (5xx) and let the queue retry with backoff. Never retry ClientException (4xx, corrupt/unsupported source) or AccountException (401/429) — those burn attempts for nothing.
Instrument quota. Log the compression count the SDK exposes after each run and alert at ~80% of your plan tier. 500 free compressions/month disappears fast across avatars and product imagery.
symfony/* constraint. You are not "using" it; you're carrying it. The wiring it saves is ~20 lines you can own.Tinify\setKey() is global static state. Call it once at boot (provider or a bootstrap callback), never per-call in a loop, and be aware there's no public reset — which makes parallel tests against different keys awkward. phpunit.xml with a dummy key plus an interface fake sidesteps this.null. Four exception types: AccountException (auth/quota), ClientException (4xx — bad image, unsupported type), ServerException, ConnectionException. Catch the base Tinify\Exception at the job boundary and branch.toFile() overwrites the source. TinyPNG is lossy and irreversible. Always keep the original bytes so a vendor change or plan downgrade is recoverable.optimized_at per asset and skip files already below your size threshold.composer.json for Symfony 5.4/6.x/7.x and PHP 8.2/8.3 support before assuming Flex auto-configuration works — then confirm the service ID with bin/console debug:container | grep -i tinify because it's not documented.%env(TINYPNG_API_KEY)% on Symfony or env() behind config() in Laravel, and confirm it doesn't surface in config:dump-reference, container dumps, or the debug toolbar.How can I help you explore Laravel packages today?