Skip to content

Add SelfHandlingOptimizer for optimizers without a binary - #242

Merged
freekmurze merged 2 commits into
spatie:mainfrom
mathiasgrimm:feature/self-handling-optimizer
Jun 29, 2026
Merged

Add SelfHandlingOptimizer for optimizers without a binary#242
freekmurze merged 2 commits into
spatie:mainfrom
mathiasgrimm:feature/self-handling-optimizer

Conversation

@mathiasgrimm

@mathiasgrimm mathiasgrimm commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Add SelfHandlingOptimizer for optimizers without a binary

Problem

OptimizerChain assumes every optimizer produces a getCommand() string that it runs through a Symfony Process. There is no way to write an optimizer that has no binary and no shell command, for example one that sends the image to an external optimization API.

Solution

A new SelfHandlingOptimizer interface (extending Optimizer) with a single handle() method. When the chain encounters one, it delegates execution to handle() instead of building and running a process, passing the chain's logger so the optimizer can log its own progress:

use Psr\Log\LoggerInterface;
use Spatie\ImageOptimizer\Image;
use Spatie\ImageOptimizer\Optimizers\BaseSelfHandlingOptimizer;

class ApiOptimizer extends BaseSelfHandlingOptimizer
{
    public function canHandle(Image $image): bool
    {
        return $image->mime() === 'image/jpeg';
    }

    public function handle(Image $image, LoggerInterface $logger): void
    {
        // Optimize $image->path() however you like (e.g. call an API),
        // write the result back to that path, and throw on failure.
        // Use $logger to record progress.
    }
}

Add it to a chain with addOptimizer() like any other optimizer. BaseSelfHandlingOptimizer reduces it to implementing just canHandle() and handle().

Backwards compatibility

Fully additive, no breaking changes:

  • SelfHandlingOptimizer extends Optimizer, so instances satisfy every existing Optimizer type hint.
  • OptimizerChain only gains a branch inside runOptimizer()'s body. No method signature changes, so any subclass overriding the protected methods (applyOptimizer, runOptimizer, handleException) stays compatible.
  • The Optimizer interface, BaseOptimizer, the bundled optimizers, and composer.json are untouched.

The only cost of staying non-breaking: a SelfHandlingOptimizer inherits getCommand()/binaryName() from Optimizer. They are never called (the chain branches before getCommand()), and BaseSelfHandlingOptimizer implements them as documented no-ops.

Logging & error handling

The self-handling flow mirrors the binary flow's logging. For each optimizer the chain logs the class name, then an "executing" line, followed by the optimizer's own log lines:

Using optimizer: `App\ApiOptimizer`
Executing `App\ApiOptimizer`
... your own log lines from handle() ...

A failing handle() emits an error: line, just like a failing binary, and flows through the same throws() mechanism from #241: by default it is logged and the chain continues; throws() (or a callable) lets you abort or inspect it.

Tests

Adds tests/SelfHandlingOptimizerTest.php (8 tests): delegation + image/logger passed, skip when canHandle() is false, default-continue, throws() abort, custom-handler routing, temp-file cleanup (including when handle() throws), and a mixed binary + self-handling chain proving the binary optimizer flow is unchanged. Full suite: 40 passing.

🤖 Generated with Claude Code

mathiasgrimm and others added 2 commits June 26, 2026 16:29
Some optimizers have no binary and no shell command, for example one that
sends the image to an external optimization API. Add a SelfHandlingOptimizer
interface (extending Optimizer) with a handle(Image, LoggerInterface) method;
the chain delegates execution to it instead of building and running a Process,
passing the chain's logger so the optimizer can log its own progress.

The change is fully additive: SelfHandlingOptimizer extends Optimizer so
instances still satisfy every existing type hint, and OptimizerChain only
gains a branch inside runOptimizer()'s body, with no method signatures
changed (subclasses overriding the protected methods stay compatible).

Failures flow through the existing throws() handling from the previous
release, and a failing handle() is logged like a failing binary. A
BaseSelfHandlingOptimizer helper lets implementers write only canHandle()
and handle().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BaseSelfHandlingOptimizer reimplemented the options, image path and
tmp path state, the constructor and their accessors byte-for-byte from
BaseOptimizer. Extend BaseOptimizer instead and keep only the no-op
binaryName()/getCommand() overrides and the abstract handle().
@freekmurze
freekmurze merged commit 333c039 into spatie:main Jun 29, 2026
30 checks passed
@freekmurze

Copy link
Copy Markdown
Member

Thanks for the contribution, @mathiasgrimm! Merged and released in 1.10.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants