Search by

mozex / iminify-laravel

mozex

Laravel wrapper for the Iminify image optimization API, with webhooks and an optimizer for Spatie's Media Library

Package info

github.com/mozex/iminify-laravel

Homepage

pkg:composer/mozex/iminify-laravel

Fund package maintenance!

mozex

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-11-04 12:35 UTC

This package is auto-updated.

Last update: 2026-11-04 13:49:20 UTC


README

Iminify Laravel

Iminify Laravel

Latest Version on Packagist GitHub Checks Workflow Status Docs License Total Downloads

Iminify for Laravel. It puts the PHP client in your app's container behind an Iminify facade, and plugs Iminify into Spatie's Media Library and laravel-image-optimizer, so conversions and uploads come out smaller with no jpegoptim, pngquant or cwebp on the server. It also has a webhook route that checks each signature and a fake for your tests.

Read the full documentation at www.iminify.com: every guide and reference page, updated with each release.

Table of Contents

Support This Project

I maintain this package along with several other open-source PHP packages used by thousands of developers every day.

If my packages save you time or help your business, consider sponsoring my work on GitHub Sponsors. Your support lets me keep these packages updated, respond to issues quickly, and ship new features.

Business sponsors get logo placement in package READMEs. See sponsorship tiers →

Installation

Requires PHP 8.2+ - see all version requirements

composer require mozex/iminify-laravel

Then publish config/iminify.php with the install command:

php artisan iminify:install

Make an API key under Settings, API Tokens on www.iminify.com, and put it in your .env:

IMINIFY_API_KEY=your-key

Calling the API

The Iminify facade is the client from mozex/iminify-php, made once from your config, so every call that package documents works here:

use Mozex\Iminify\Laravel\Facades\Iminify;

$image = Iminify::images()->upload(storage_path('app/photos/team.jpg'), ['format' => 'webp']);
$image = Iminify::images()->wait($image);

if ($image->hasResult()) {
    Iminify::images()->download($image)->saveTo(storage_path('app/photos/team.webp'));
}

You can also type-hint Mozex\Iminify\Client in a constructor or job and the container hands you the same client. Every request it sends carries an Iminify-Client: laravel/<version> header, which Iminify uses only to count which clients are in use.

One request may take IMINIFY_TIMEOUT seconds, 30 by default. A wait is many requests and has its own limit, wait()'s second argument.

Media Library

Put the optimizer in image_optimizers in config/media-library.php, in place of jpegoptim, pngquant, optipng, gifsicle and cwebp, and keep svgo and avifenc for the SVG and AVIF files Iminify doesn't read:

'image_optimizers' => [
    Mozex\Iminify\Laravel\IminifyOptimizer::class => [],
    Spatie\ImageOptimizer\Optimizers\Svgo::class => [
        '--disable=cleanupIDs',
    ],
    Spatie\ImageOptimizer\Optimizers\Avifenc::class => [
        '-a cq-level=23',
        '-j all',
        '--min 0',
        '--max 63',
        '--minalpha 0',
        '--maxalpha 63',
        '-a end-usage=q',
        '-a tune=ssim',
    ],
],

Keep at least one of Spatie's own optimizers in that list in any case. Media Library builds the chain with spatie/image-optimizer's factory, which only recognizes Spatie's seven built-in classes, and a list holding none of them gets the default binaries instead, without a word.

Every JPEG, PNG, WebP and GIF conversion then goes through Iminify and comes back in its own format. Queued conversions work as they are: the optimizer carries only its options onto the queue, and takes the API key from your config when it runs. An api_key in a list's options is ignored, so it never lands in a queued job. Media Library never optimizes the uploaded file itself, only its conversions.

To use Iminify for some conversions and leave the rest on the binaries, keep the config as it is and give those conversions their own chain:

use Mozex\Iminify\Laravel\IminifyOptimizer;

public function registerMediaConversions(?Media $media = null): void
{
    $this->addMediaConversion('thumb')
        ->width(400)
        ->optimize(IminifyOptimizer::chain());
}

The package supports Media Library 11.23 and newer.

Responsive images

Media Library's responsive images ignore image_optimizers and always run the default binaries. Turn this on to run your image_optimizers list on each width instead, so an AVIF width still reaches avifenc. When the list lacks Iminify, it takes the place of jpegoptim, pngquant, optipng, gifsicle and cwebp there, so no width is compressed twice:

IMINIFY_RESPONSIVE_IMAGES=true

The package then binds its own ResponsiveImageGenerator, which is Media Library's with the optimizer chain swapped.

What it costs

Each file the optimizer sends counts as one image against your plan's daily images, the same as an upload on the website. An upload to a model with a thumb and a preview conversion counts two. With responsive images on, every width counts one more, and Media Library usually makes several widths per image. Iminify::account()->get(), the API's GET /account, tells you what's left today.

Optimizing again later

For media you already have, or after changing the config, have Media Library build the conversions again:

php artisan media-library:regenerate

Each regenerated conversion counts again, so narrow the run where you can: --only=thumb, a model name as the first argument, or --ids=.

laravel-image-optimizer

Put the optimizer in optimizers in config/image-optimizer.php, in place of jpegoptim, pngquant, optipng, gifsicle and cwebp:

'optimizers' => [
    Mozex\Iminify\Laravel\IminifyOptimizer::class => [],
    Spatie\ImageOptimizer\Optimizers\Svgo::class => [
        '--disable=cleanupIDs',
    ],
],

ImageOptimizer::optimize($path) and the OptimizeImages middleware then go through Iminify. The middleware waits for every uploaded file inside the request, usually a few seconds each, so on a busy form you may prefer to optimize from a queued job after saving.

Your own chain

IminifyOptimizer::chain() is a Spatie OptimizerChain holding only this optimizer, which spatie/image and Browsershot take too:

use Mozex\Iminify\Laravel\IminifyOptimizer;
use Spatie\Image\Image;

Image::load($path)->width(1200)->optimize(IminifyOptimizer::chain())->save();

Configuration

config/iminify.php holds:

Key Env Default What it does
api_key IMINIFY_API_KEY none The key every request sends
base_uri IMINIFY_BASE_URI https://www.iminify.com/api/v1 Where the API answers
timeout IMINIFY_TIMEOUT 30 Seconds one request may take
optimizer.level IMINIFY_LEVEL the API's, smart none, lossless, smart or ultra
optimizer.keep_metadata false Keep EXIF and the like
optimizer.timeout 120 Seconds the optimizer waits for one file
media_library.responsive_images IMINIFY_RESPONSIVE_IMAGES false Send responsive widths through Iminify
webhooks.secret IMINIFY_WEBHOOK_SECRET none Your webhook's signing secret
webhooks.tolerance 300 Seconds a webhook's time may be off from your clock

An option in a config list overrides optimizer for that list:

Mozex\Iminify\Laravel\IminifyOptimizer::class => ['level' => 'ultra'],

The optimizer never asks for alt text, keeps each file's format, and writes back only a smaller copy. It deletes each image from your Iminify account when it's done with the file, whether or not it wrote anything back.

When a file can't be optimized

The file stays as it was. A spent daily allowance, a timeout, a refused key or an answer the optimizer won't trust leaves the image unoptimized, and nothing else breaks. Spatie's chain logs the reason to its own logger, which is off by default, and the package dispatches OptimizationSkippedEvent with the file's path and the exception:

use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Mozex\Iminify\Laravel\Events\OptimizationSkippedEvent;

Event::listen(function (OptimizationSkippedEvent $event) {
    Log::warning("Iminify skipped {$event->path}: {$event->reason->getMessage()}");
});

A listener that throws is reported to your exception handler, and never takes the place of the reason. A queued listener gets $event->reason as an IminifyException that names the original's class and message, since an exception's trace can hold things a queue can't carry.

Webhooks

Register the route in routes/web.php. It leaves out the CSRF check, which a webhook can't pass:

Route::iminifyWebhooks();

It answers at /iminify/webhooks under the name iminify.webhooks; pass another address and name as its arguments. Save the address under Settings, Webhooks on www.iminify.com, and put the signing secret it shows in your .env:

IMINIFY_WEBHOOK_SECRET=whsec_...

The route checks the signature against the raw body, answers anything that doesn't match with a 400, and dispatches WebhookReceivedEvent for the rest:

use Illuminate\Support\Facades\Event;
use Mozex\Iminify\Enums\WebhookEventType;
use Mozex\Iminify\Laravel\Events\WebhookReceivedEvent;

Event::listen(function (WebhookReceivedEvent $received) {
    if ($received->event->type === WebhookEventType::ImageCompleted) {
        $image = $received->event->image();
    }
});

$received->event->id stays the same when a failed send is sent again, so you can skip one you've handled. Iminify counts a send as failed when the answer takes longer than the limit its webhooks guide states, so a listener with slow work should implement ShouldQueue.

Faking Iminify in your tests

Iminify::fake() swaps the client for one that answers what you queue, in order, and keeps every call to assert on. The optimizer uses it too, and a wait never sleeps:

use Mozex\Iminify\Laravel\Facades\Iminify;
use Mozex\Iminify\Testing\Fixtures;

Iminify::fake([
    Fixtures::image(['id' => 'abc']),
    Fixtures::refusal('daily_limit_reached', 429),
]);

$image = Iminify::images()->get('abc');

Iminify::assertSent('GET', 'images/abc');

Fixtures holds an example answer for every record the API returns, and a queued exception is thrown in its turn. The optimizer waits out a rate limit with Laravel's Sleep, so Sleep::fake() keeps those waits out of your tests too. To test your webhook listener, sign a body with (new WebhookVerifier($secret))->sign($body) from the PHP client and post it to the route with the Iminify-Signature header.

AI coding assistants

The package ships a Laravel Boost skill, iminify-laravel, that shows an AI coding assistant how to set up the optimizer, the webhook route and the fake. Boost installs it with php artisan boost:install or boost:update, and the assistant loads it when a task touches Iminify.

These docs are also on Context7 as mozex/iminify-laravel, which indexes this README. With Context7's MCP server connected, ask for "the Context7 docs for mozex/iminify-laravel" when you want code that calls the package.

To have the assistant compress, convert or scan images itself rather than write code that does it, connect it to Iminify's own MCP server: connecting an app covers Claude, Claude Code, Cursor, VS Code and ChatGPT.

Resources

Visit the documentation at www.iminify.com for every guide and reference page, updated from this repository with each release.

License

The MIT License (MIT). Please see License File for more information.