Events and event handlers

Definitions (adapted from PSR-14)

  • Event - An Event is a message produced by an Emitter. Usually denoting a state change.
  • Listener - A Listener is any PHP callable that expects to be passed an Event. Zero or more Listeners may be passed the same Event. A Listener MAY enqueue some other asynchronous behavior if it so chooses.
  • Emitter - An Emitter is any arbitrary code that wishes to dispatch an Event. This is also known as the "calling code".
  • Dispatcher - The Dispatcher is given an Event by an Emitter. The Dispatcher is responsible for ensuring that the Event is passed to all relevant Listeners.

Pattern

We implement the Observer pattern using the Mediator pattern.

The key is that the emitter should not know what is listening to its events. The dispatcher avoids modules communicate directly but instead through a mediator. This helps the Single Responsibility principle by allowing communication to be offloaded to a class that just handles communication.

How does it work? The dispatcher, the central object of the event dispatcher system, notifies listeners of an event dispatched to it. Put another way: your code dispatches an event to the dispatcher, the dispatcher notifies all registered listeners for the event, and each listener does whatever it wants with the event.

Example 1: Adding elements to the core UI

An emitter in a core twig template

{% for block in handle_event('ViewAttachment' ~ attachment.getMimetypeMajor() | capitalize , {'attachment': attachment, 'thumbnail_parameters': thumbnail_parameters}) %}
    {{ block | raw }}
{% endfor %}

Listener

/**
 * Generates the view for attachments of type Image
 *
 * @param array $vars Input from the caller/emitter
 * @param array $res I/O parameter used to accumulate or return values from the listener to the emitter
 *
 * @return \EventResult true if not handled or if the handling should be accumulated with other listeners,
 *              false if handled well enough and no other listeners are needed
 */
public function onViewAttachmentImage(array $vars, array &$res): \EventResult
{
    $res[] = Formatting::twigRenderFile('imageEncoder/imageEncoderView.html.twig', ['attachment' => $vars['attachment'], 'thumbnail_parameters' => $vars['thumbnail_parameters']]);
    return Event::stop;
}

Some things to note about this example:

  • The parameters of the handler onViewAttachmentImage are defined by the emitter;
  • Every handler must return a bool stating what is specified in the example docblock.

Example 2: Informing the core about an handler

Event emitted in the core

Event::handle('ResizerAvailable', [&$event_map]);

Event lister in a plugin

/**
 * @param array $event_map output
 *
 * @return \EventResult event hook
 */
public function onResizerAvailable(array &$event_map): \EventResult
{
    $event_map['image'] = 'ResizeImagePath';
    return Event::next;
}

Example 3: Default action

An event can be emited to perform an action, but still have a fallback as such:

Event emitter

if (Event::handle('EventName', $args) !== Event::stop): \EventResult
{
    // Do default action, as no-one claimed authority on handling this event
}