Event dispatcher (PSR-14 events)

The event dispatcher system was added to extend TYPO3's Core behaviour in TYPO3 v10.0. In the past, this was done via Extbase's signal/slot and TYPO3's custom hook system. The event dispatcher system is a fully-capable replacement for new code in TYPO3, as well as a possibility to migrate away from previous TYPO3 solutions.

Don't get hooked, listen to events! PSR-14 within TYPO3 v10.

-- Benni Mack @ TYPO3 Developer Days 2019

For a basic example on listening to an event, see the chapter Listen to an event in the extension development how-to section.

Quick start

Dispatching an event

This quick start section shows how to create your own event class and dispatch it. If you just want to listen on an existing event, see section Implementing an event listener in your extension.

  1. Create an event class.

    An event class is basically a plain PHP object with getters for immutable properties and setters for mutable properties. It contains a constructor for all properties:

    EXT:my_extension/Classes/Event/DoingThisAndThatEvent.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension\Event;
    
    final class DoingThisAndThatEvent
    {
        public function __construct(
            private string $mutableProperty,
            private readonly int $immutableProperty,
        ) {}
    
        public function getMutableProperty(): string
        {
            return $this->mutableProperty;
        }
    
        public function setMutableProperty(string $mutableProperty): void
        {
            $this->mutableProperty = $mutableProperty;
        }
    
        public function getImmutableProperty(): int
        {
            return $this->immutableProperty;
        }
    }
    
    Copied!

    Read more about implementing event classes.

  2. Inject the event dispatcher

    If you are in a controller, the event dispatcher has already been injected, and in this case you can omit this step.

    If the event dispatcher is not yet available, you need to inject it:

    EXT:my_extension/Classes/SomeClass.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension;
    
    use Psr\EventDispatcher\EventDispatcherInterface;
    
    final class SomeClass
    {
        public function __construct(
            private readonly EventDispatcherInterface $eventDispatcher,
        ) {}
    }
    
    Copied!
  3. Dispatch the event

    Create an event object with the data that should be passed to the listeners. Use the data of mutable properties as it suits your business logic:

    EXT:my_extension/Classes/SomeClass.php
    <?php
    
    declare(strict_types=1);
    
    namespace MyVendor\MyExtension;
    
    use MyVendor\MyExtension\Event\DoingThisAndThatEvent;
    use Psr\EventDispatcher\EventDispatcherInterface;
    
    final class SomeClass
    {
        public function __construct(
            private readonly EventDispatcherInterface $eventDispatcher,
        ) {}
    
        public function doSomething(): void
        {
            // ..
    
            /** @var DoingThisAndThatEvent $event */
            $event = $this->eventDispatcher->dispatch(
                new DoingThisAndThatEvent('foo', 2),
            );
            $someChangedValue = $event->getMutableProperty();
    
            // ...
        }
    }
    
    Copied!

Description of PSR-14 in the context of TYPO3

PSR-14 is a lean solution that builds upon wide-spread solutions for hooking into existing PHP code (Frameworks, CMS, and the like).

PSR-14 consists of the following four components:

The event dispatcher object

The EventDispatcher object is used to trigger an event. TYPO3 has a custom event dispatcher implementation. In PSR-14 all event dispatchers of all frameworks are implementing \Psr\EventDispatcher\EventDispatcherInterface, thus it is possible to replace the event dispatcher with another. The EventDispatcher's main method dispatch() is called in TYPO3 Core or extensions. It receives a PHP object which will then be handed to all available listeners.

The listener provider

A ListenerProvider object that contains all listeners which have been registered for all events. TYPO3 has a custom listener provider that collects all listeners during compile time. This component is only used internally inside of TYPO3's Core Framework.

The events

An Event object can be any PHP object and is called from TYPO3 Core or an extension ("emitter") containing all information to be transported to the listeners. By default, all registered listeners get triggered by an event, however, if an event has the interface \Psr\EventDispatcher\StoppableEventInterface implemented, a listener can stop further execution of other event listeners. This is especially useful, if the listeners are candidates to provide information to the emitter. This allows to finish event dispatching, once this information has been acquired.

If an event allows modifications, appropriate methods should be available, although due to PHP's nature of handling objects and the PSR-14 listener signature, it cannot be guaranteed to be immutable.

The listeners

Extensions and PHP packages can add listeners that are registered via YAML. They are usually associated to Event objects by the fully-qualified class name of the event to be listened on. It is the task of the listener provider to provide configuration mechanisms to represent this relationship.

If multiple event listeners for a specific event are registered, their order can be configured or an existing event listener can also be overridden with a different one.

The System > Configuration > Event Listeners (PSR-14) backend module (requires the system extension lowlevel) reveals an overview of all registered event listeners, see Debugging event handling.

Advantages of the event dispatcher over hooks

The main benefits of the event dispatcher approach over hooks is an implementation which helps extension authors to better understand the possibilities by having a strongly typed system based on PHP. In addition, it serves as a bridge to also incorporate other events provided by frameworks that support PSR-14.

Impact on TYPO3 Core development in the future

TYPO3's event dispatcher serves as the basis to replace all hooks in the future. However, for the time being, hooks work the same way as before, unless migrated to an event dispatcher-like code, whereas a PHP E_USER_DEPRECATED error can be triggered.

Some hooks might not be replaced 1:1 to event dispatcher, but rather superseded with a more robust or future-proof API.

Implementing an event listener in your extension

New in version 13.0

A PHP attribute\TYPO3\CMS\Core\Attribute\AsEventListener is available to autoconfigure a class as an event listener. If the PHP attribute is used, the configuration of the event listener via the Configuration/Services.yaml file is not necessary anymore.

The event listener class

An example listener, which hooks into the Mailer API to modify mailer settings to not send any emails, could look like this:

EXT:my_extension/Classes/EventListener/NullMailer.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent;

#[AsEventListener(
    identifier: 'my-extension/null-mailer',
    before: 'someIdentifier, anotherIdentifier',
)]
final readonly class NullMailer
{
    public function __invoke(AfterMailerInitializationEvent $event): void
    {
        $event->getMailer()->injectMailSettings(['transport' => 'null']);
    }
}
Copied!

An extension can define multiple listeners. The attribute can be used on class and method level. The PHP attribute is repeatable, which allows to register the same class to listen for different events.

Once the emitter is triggering an event, this listener is called automatically. Be sure to inspect the event's PHP class to fully understand the capabilities provided by an event.

The PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener supports the following properties (which are all optional):

identifier
A unique identifier should be declared which identifies the event listener, and orderings can be build upon the identifier. If this property is not explicitly defined, the service name is automatically used instead.
before
This property allows a custom sorting of registered listeners. The listener is then dispatched before the given listener. The value is the identifier of another event listener. Also, multiple event identifiers can be entered here, separated by a comma.
after
This property allows a custom sorting of registered listeners. The listener is then dispatched after the given listener. The value is the identifier of another event listener. Also, multiple event identifiers can be entered here, separated by a comma.
event
The fully-qualified class name (FQCN) of the dispatched event, that the listener wants to react to. It is recommended to not specify this property, but to use the FQCN as type declaration of the argument within the dispatched method (usually __invoke(EventName $event)).
method
The method to be called. If this property is not given, the listener class is treated as invokable, thus its __invoke() method is called.

The PHP attribute is repeatable, which allows to register the same class to listen for different events, for example:

EXT:my_extension/Classes/EventListener/NullMailer.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;

#[AsEventListener(
    identifier: 'my-extension/null-mailer-initialization',
    event: AfterMailerInitializationEvent::class,
)]
#[AsEventListener(
    identifier: 'my-extension/null-mailer-sent-message',
    event: BeforeMailerSentMessageEvent::class,
)]
final readonly class NullMailer
{
    public function __invoke(
        AfterMailerInitializationEvent | BeforeMailerSentMessageEvent $event,
    ): void {
        $event->getMailer()->injectMailSettings(['transport' => 'null']);
    }
}
Copied!

The PHP attribute can also be used on a method level. The above example can also be written as:

EXT:my_extension/Classes/EventListener/NullMailer.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;

final readonly class NullMailer
{
    #[AsEventListener(
        identifier: 'my-extension/null-mailer-initialization',
        event: AfterMailerInitializationEvent::class,
    )]
    #[AsEventListener(
        identifier: 'my-extension/null-mailer-sent-message',
        event: BeforeMailerSentMessageEvent::class,
    )]
    public function __invoke(
        AfterMailerInitializationEvent | BeforeMailerSentMessageEvent $event,
    ): void {
        $event->getMailer()->injectMailSettings(['transport' => 'null']);
    }
}
Copied!

Registering the event listener via Services.yaml

New in version 13.0

If using the PHP attribute \TYPO3\CMS\Core\Attribute\AsEventListener to configure an event listener, the registration in the Configuration/Services.yaml file is not necessary anymore.

If an extension author wants to provide a custom event listener, an according entry with the tag event.listener can be added to the Configuration/Services.yaml file of that extension.

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  MyVendor\MyExtension\EventListener\NullMailer:
    tags:
      - name: event.listener
        method: handleEvent
        identifier: 'my-extension/null-mailer'
        before: 'someIdentifier, anotherIdentifier'
Copied!

Read how to configure dependency injection in extensions.

The tag name event.listener identifies that a listener should be registered.

The custom PHP class \MyVendor\MyExtension\EventListener\NullMailer serves as the listener whose handleEvent() method is called, once the event is dispatched. The name of the listened event is specified as a typed argument to that dispatch method. handleEvent(\TYPO3\CMS\Core\Mail\Event\AfterMailerInitializationEvent $event) will for example listen on the event AfterMailerInitializationEvent.

The identifier is a common name, so orderings can be built upon the identifier, the optional before and after attributes allow for custom sorting against the identifier of other listeners. If no identifier is specified, the service name (usually the fully-qualified class name of the listener) is automatically used.

If no attribute method is given, the class is treated as invokable, thus its __invoke() method will be called:

EXT:my_extension/Configuration/Services.yaml
services:
  # Place here the default dependency injection configuration

  MyVendor\MyExtension\EventListener\NullMailer:
    tags:
      - name: event.listener
        identifier: 'my-extension/null-mailer'
        before: 'someIdentifier, anotherIdentifier'
Copied!

Read how to configure dependency injection in extensions.

Overriding event listeners

Existing event listeners can be overridden by custom implementations. This can be performed with both methods, either by using the PHP #[AsEventListener] attribute, or via EXT:my_extension/Configuration/Services.yaml.

For example, a third-party extension listens on the event \TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent via the PHP attribute:

EXT:some_extension/Classes/EventListener/SeoEvent.php
<?php

declare(strict_types=1);

namespace SomeVendor\SomeExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;

#[AsEventListener(
    identifier: 'ext-some-extension/modify-hreflang',
    after: 'typo3-seo/hreflangGenerator',
)]
final readonly class SeoEventListener
{
    public function __invoke(ModifyHrefLangTagsEvent $event): void
    {
        // ... custom code...
    }
}
Copied!

or via Services.yaml declaration:

EXT:some_extension/Configuration/Services.yaml
SomeVendor\SomeExtension\Seo\HrefLangEventListener:
  tags:
    - name: event.listener
      identifier: 'ext-some-extension/modify-hreflang'
      after: 'typo3-seo/hreflangGenerator'
Copied!

If you want to replace this event listener with your custom implementation, your extension can achieve this by specifying the overridden identifier via the PHP attribute:

EXT:my_extension/Configuration/Classes/EventListener/MySeoEvent.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;

// Important: Use the 'identifier' of the original event here to be replaced!
#[AsEventListener(
    identifier: 'ext-some-extension/modify-hreflang',
    after: 'typo3-seo/hreflangGenerator',
)]
final readonly class MySeoEventListener
{
    public function __invoke(ModifyHrefLangTagsEvent $event): void
    {
        // ... custom code which overrides the
        // original EXT:some-extension listener ...
    }
}
Copied!

or via Services.yaml declaration:

EXT:my_extension/Configuration/Services.yaml
# Provide your custom event class:
MyVendor\MyExtension\EventListener\Seo\HrefLangEventListener:
  tags:
    - name: event.listener
      # Use the same identifier of the extension that you override!
      identifier: 'ext-some-extension/modify-hreflang'
Copied!

Make sure that you set the identifier property to exactly the string which the original implementation uses. If the identifier is not mentioned specifically in the original implementation, the service name (when unspecified, the fully-qualified name of the event listener class) is used. You can inspect that identifier in the System > Configuration > Event Listeners (PSR-14) backend module (requires the system extension lowlevel), see Debugging event handling. In this example, if identifier: 'ext-some-extension/modify-hreflang' is not defined, the identifier will be set to identifier: 'SomeVendorSomeExtensionSeoHrefLangEventListener' and you could use that identifier in your implementation.

Best practices

  • When configuring listeners, it is recommended to add one listener class per event type, and have it called via __invoke().
  • When creating a new event PHP class, it is recommended to add an Event suffix to the PHP class, and to move it into an appropriate folder like Classes/Event/ to easily discover events provided by a package. Be careful about the context that should be exposed.
  • The same applies to creating a new event listener PHP class: Add an EventListener suffix to the PHP class, and move it to a folder Classes/EventListener/.
  • Emitters (TYPO3 Core or extension authors) should always use Dependency Injection to receive the event dispatcher object as a constructor argument, where possible, by adding a type declaration for \Psr\EventDispatcher\EventDispatcherInterface.
  • A unique and descriptive identifier should be used for event listeners.

Any kind of event provided by TYPO3 Core falls under TYPO3's Core API deprecation policy, except for its constructor arguments, which may vary. Events that should only be used within TYPO3 Core, are marked as @internal, just like other non-API parts of TYPO3. Events marked as @internal should be avoided whenever technically possible.

Debugging event handling

A complete list of all registered event listeners can be viewed in the the module System > Configuration > Event Listeners (PSR-14). The lowlevel system extension has to be installed for this module to be available.

List of event listeners in the Configuration module

List of event listeners in the Configuration module

To debug all events that are actually dispatched during a frontend request you can use the admin panel:

Go to Admin Panel > Debug > Events and see all dispatched events. The adminpanel system extension has to be installed for this module to be available.

List of dispatched events in the Admin Panel

List of dispatched events in the Admin Panel