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

Tinypng Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • The framework mismatch is the whole assessment. Adding this bundle to a Laravel app gives you a dead dependency with an unmaintained 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.
  • It throws, it doesn't return 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.
  • Re-compressing an already-optimized image is pure quota waste and gains roughly nothing. Track optimized_at per asset and skip files already below your size threshold.
  • 429 is not a transient error you should hammer. Back off and let the queue redeliver; cap concurrent image workers well below TinyPNG's rate limits so a burst of workers doesn't exhaust the month's quota in an hour.
  • Set explicit HTTP timeouts (5–10s) — the SDK's default can hang a queue worker on a stalled upstream, and then you're debugging a backed-up queue instead of a compression bug.
  • Verify the Symfony constraint if you do have a Symfony target. Unmaintained bundles pin you to an older Symfony minor and silently block a framework upgrade. Check 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.
  • Keep the key out of dumped parameters. Use %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.
  • Extension points are thin by design. The bundle's only value is DI registration; key rotation, retries, metrics, and fallbacks are all yours to write regardless of path. That's the strongest argument for just owning the provider binding.
  • Zero stars / zero dependents is a supply-chain signal, not a code-quality one. No issue history means no free failure-mode research — you'll discover the 429 and 415 behaviours in production. If you must pin this bundle for a Symfony service, pin the exact version, add a lockfile comment explaining why, and put an integration test in CI that boots the kernel.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings