Skip to content

The derivative saved event

ai_disclosure_image dispatches AiDisclosureImageEvents::DERIVATIVE_SAVED (ai_disclosure_image.derivative_saved) after an image style has written a derivative file. Core has none: effects run before the save, and the toolkit saves pixels only, so a module that marks a finished derivative needs this moment.

The event is a Drupal\ai_disclosure_image\Event\DerivativeSavedEvent:

Method Returns
getImageStyle() the ImageStyleInterface that built the derivative
getOriginalUri() the URI of the original image
getDerivativeUri() the URI of the derivative that was saved

It is dispatched once per derivative, and only when the save succeeded. A source file that does not exist, a directory that cannot be made or a toolkit that fails dispatch nothing.

An example subscriber

namespace Drupal\example\EventSubscriber;

use Drupal\Core\Queue\QueueFactory;
use Drupal\ai_disclosure_image\Event\AiDisclosureImageEvents;
use Drupal\ai_disclosure_image\Event\DerivativeSavedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class SignDerivativeSubscriber implements EventSubscriberInterface {

  public function __construct(private readonly QueueFactory $queueFactory) {}

  public static function getSubscribedEvents(): array {
    return [AiDisclosureImageEvents::DERIVATIVE_SAVED => 'onDerivativeSaved'];
  }

  public function onDerivativeSaved(DerivativeSavedEvent $event): void {
    // Only a note is made here. The slow work runs in the queue worker.
    $this->queueFactory->get('example_sign_derivative')->createItem([
      'style' => $event->getImageStyle()->id(),
      'original' => $event->getOriginalUri(),
      'derivative' => $event->getDerivativeUri(),
    ]);
  }

}

Register it as a service tagged event_subscriber.

Keep listeners fast

Derivatives are built on the first request for the style URL, so a listener runs inside the request of a visitor who waits for the image. Do the cheap part in the listener and queue the slow part, for example the signing of a C2PA manifest. The file is already on disk when the event is dispatched, and a queued job can read it later. A listener that throws makes that request fail, even though the derivative exists.

How the event is dispatched

The submodule swaps the entity class of image_style in hook_entity_type_alter(). The subclass calls core's createDerivative() and dispatches the event when it returned TRUE. It returns core's value unchanged and does nothing else, so paths, URLs and flushing stay as they were. Entities cannot take constructor injection, so the subclass fetches the event dispatcher with \Drupal::service(), as core's own ImageStyle does for the file system and the logger.

Only one entity class can be in place at a time:

  • If the class is core's, the submodule sets its own.
  • If the class is the one of Image Optimize (imageapi_optimize), the submodule sets a subclass of it, so both keep working.
  • If any other module has replaced the class, the submodule leaves it alone. No event is dispatched, and the status report shows a warning.

When core gets an event

A native event in core would make the swap unnecessary. A feature request for it is planned. When it lands, the submodule drops the class swap and dispatches the same event name from core's event, so subscribers need no change.