Custom Modifier
Modifiers let a sizes breakpoint token trigger arbitrary config changes via |<modifier> pipe segments — e.g. lg:4@square|circle — without hand-writing a whole filter_sets entry for every combination you might want in a template.
ModifierInterface
namespace Tito10047\ProgressiveImageBundle\Modifier;
interface ModifierInterface
{
public function supports(string $modifier): bool;
/**
* @param array<string, mixed> $context
* @return array<string, mixed>
*/
public function modify(string $modifier, array $context): array;
}ModifierProvider::applyModifiers() runs every |-separated modifier string in a breakpoint token through every registered ModifierInterface whose supports() returns true, threading the (mutated) context array through each in turn. That resulting context is what eventually reaches VariantSpecFactory::create() as its per-call layer — see Filters, Formats & Quality.
The bundle registers one built-in modifier at low priority (-100), BaseFilterModifier, which supports() everything and, if context['filter'] isn't already set, sets it to the modifier string itself — this is what makes |circle in lg:4@square|circle shorthand for context: { filter: circle }, i.e. "apply the filter_sets.circle entry". Because it's low priority, any modifier you register runs before it and can set a different context['filter'] (or anything else) that BaseFilterModifier will then leave alone.
Example: a modifier that toggles a watermark
namespace App\Image;
use Tito10047\ProgressiveImageBundle\Modifier\ModifierInterface;
final class WatermarkModifier implements ModifierInterface
{
public function supports(string $modifier): bool
{
return 'watermarked' === $modifier;
}
public function modify(string $modifier, array $context): array
{
$context['filters']['watermark'] = [
'image' => 'images/watermark.png',
'position' => 'bottom_right',
];
return $context;
}
}<twig:pgi:Image src="{{ asset('images/hero.jpg') }}" sizes="lg:6|watermarked" alt="Hero" />Wiring it in
ModifierInterface is autoconfigured — implementing it is enough, no explicit tagging or config entry needed:
// config/services.php
$container->services()->set(App\Image\WatermarkModifier::class);ProgressiveImageExtension::load() calls $container->registerForAutoconfiguration(ModifierInterface::class)->addTag('progressive_image.modifier'), and ModifierProvider collects every tagged service via a TaggedIteratorArgument. Priority matters if more than one modifier could supports() the same string — use #[AsTaggedItem] or an explicit addTag('progressive_image.modifier', ['priority' => N]) to control ordering relative to the built-in BaseFilterModifier (priority -100).
FilterModifierInterface
A second, narrower interface exists for rewriting a single already-resolved filter's raw options — after the filter_sets → image_configs → context merge, right before that one filter is turned into a typed Filter object — rather than the whole context array:
namespace Tito10047\ProgressiveImageBundle\Modifier;
interface FilterModifierInterface
{
public function supports(string $filterName): bool;
/**
* @param array<string, mixed> $currentOptions
* @return array<string, mixed>
*/
public function modify(string $filterName, array $currentOptions): array;
}Use this when what you need to change depends on the filter's own options, not just the breakpoint/modifier string — ModifierInterface only ever sees the modifier string itself and the context built so far, with no visibility into what a specific filter's merged options ended up being.
Example: clamping a watermark's opacity
namespace App\Image;
use Tito10047\ProgressiveImageBundle\Modifier\FilterModifierInterface;
final class ClampWatermarkOpacity implements FilterModifierInterface
{
public function supports(string $filterName): bool
{
return 'watermark' === $filterName;
}
public function modify(string $filterName, array $currentOptions): array
{
$currentOptions['opacity'] = min(50, $currentOptions['opacity'] ?? 100);
return $currentOptions;
}
}Wiring it in
Also autoconfigured — implementing the interface is enough, no explicit tagging needed:
// config/services.php
$container->services()->set(App\Image\ClampWatermarkOpacity::class);VariantSpecFactory collects every service tagged pgi.filter_modifier (the tag ProgressiveImageExtension::load() autoconfigures FilterModifierInterface into) and, for every raw (filterName, options) pair in the merged config, runs it through every modifier whose supports($filterName) returns true — in registration order — before handing the (possibly rewritten) options to FilterFactory::create(). This runs for both create() (the responsive/breakpoint path) and createFromFilterSet() (pgi_filter(), the on-the-fly resolve route) alike.