bvtterfly/lio
Lio is a Laravel package by bvtterfly that provides a clean, Eloquent-friendly API with sensible defaults and minimal configuration, making it easy to integrate into existing Laravel applications and workflows.
Install and publish the config
composer require approached/laravel-image-optimizer
php artisan vendor:publish --tag=laravel-image-optimizer
Auto-discovery registers the service provider; there is nothing to bind manually unless you want to swap implementations in tests.
Look at the config file first. config/laravel-image-optimizer.php is where all day-to-day behaviour lives. The optimizers array is an ordered chain — each entry is a class handling specific MIME types, and the chain runs until every supported type has been attempted. The default set mixes local binaries (jpegoptim, optipng, pngquant, gifsicle, svgo) with API-based optimizers (reSmush, tinyPNG, kraken).
First use case — optimize an uploaded avatar after validation:
use Approached\LaravelImageOptimizer\ImageOptimizer;
public function store(Request $request, ImageOptimizer $optimizer)
{
$path = $request->file('avatar')->store('avatars', 'public');
$full = Storage::disk('public')->path($path);
$optimizer->optimizeImage($full);
return back()->with('status', 'Uploaded');
}
Choosing your first optimizer. If you are on shared hosting or Docker without image tooling, start with reSmush alone — it is pure HTTP, no binaries. If you control the server, start with the local lossless pair (jpegoptim + optipng) instead; it is faster, deterministic, and has no rate limits.
On 2.0.1 specifically: this is a patch release fixing bugs in the reSmush optimizer. There is no API change, so upgrading from 2.0.0 is safe — but do upgrade if reSmush is enabled, because the fixed bugs were in the path you would otherwise be silently relying on. Pin ^2.0.1 rather than ^2.0.
Never optimize inside the request/response cycle. Large PNGs plus a remote API call will blow past max_execution_time and hold a PHP-FPM worker. Dispatch a job:
OptimizeImage::dispatch($fullPath)->onQueue('images');
Give that job $tries = 3 and a backoff(), and treat exhaustion as non-fatal — the image still exists, just unoptimized.
Order the chain lossless-then-lossy. The config order is the execution order. A practical chain: jpegoptim --strip-all → optipng -o2 → pngquant/reSmush for the last mile. Putting a lossy optimizer first means later steps re-compress already-degraded pixels for no gain.
'optimizers' => [
Approached\LaravelImageOptimizer\Optimizers\JpegOptimOptimizer::class,
Approached\LaravelImageOptimizer\Optimizers\OptiPngOptimizer::class,
Approached\LaravelImageOptimizer\Optimizers\PngQuantOptimizer::class,
],
Custom optimizers are the extension point. Extend the package's abstract optimizer (or implement its optimizer contract), return the supported MIME types, and shell out via Symfony Process. Then register the class in the config array — nothing else needs to change. This is how you add cwebp/AVIF conversion or an internal image service.
Preserve originals, then compare. The optimizer mutates files in place. Copy to a originals/ disk first, then keep whichever is smaller:
$before = filesize($full);
$optimizer->optimizeImage($full);
if (filesize($full) >= $before) {
Storage::disk('originals')->put($path, file_get_contents($fullPath)); // restore
}
Wire it to model events, not controllers. A ImageObserver on saved/created keeps every ingestion path (API, Nova, imports, seeders) covered instead of just the web form.
Backfill existing media with an artisan command. php artisan images:optimize --disk=public --chunk=100 walking the disk is the standard one-off migration. Log the byte savings per file so you can report the actual win.
Testing. Never let a real binary or a real API call run in CI. Bind a pass-through fake into the container or point the config at a no-op optimizer class in phpunit.xml, and assert your job was dispatched rather than asserting on file bytes.
Docker/CI image. Local optimizers need jpegoptim optipng pngquant gifsicle from the distro package manager and svgo from npm. Add a smoke test that runs which jpegoptim in the built image — the failure otherwise shows up only at runtime on a user's upload.
try/catch, log the failure, and let the upload succeed with the original file. Optimization is best-effort, never a hard dependency.optipng on a freshly provisioned server breaks the first upload, not the deploy. Add a php artisan image-optimizer:check style health probe or a scheduled check.php artisan config:cache after every config edit; a stale cache is the most common "my new optimizer isn't running" cause.retry_after on the queue above your worst-case job duration.--strip-all / reSmush remove metadata including orientation and GPS. That is usually what you want for GDPR and bytes — but confirm you are not shipping sideways phone photos, and that you strip after any orientation read you need.filesize() before and after and keep the smaller copy.jpegoptim --version, curl the reSmush endpoint manually), then confirm the optimizer actually claims the file's MIME type, then confirm the chain order. Most "it does nothing" reports are a MIME mismatch or a cached config.How can I help you explore Laravel packages today?