Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Custom Events and Listeners

Custom Events and Listeners

~17 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026

Now let's build the concrete feature from chapter 5: a notification once a task gets assigned to a user – with a CUSTOM, named event, following EXACTLY the pattern of the kernel events from chapter 35.

Creating an event class

src/Event/TaskAssignedEvent.php
<?php

declare(strict_types=1);

namespace App\Event;

use App\Entity\Task;
use App\Entity\User;
use Symfony\Contracts\EventDispatcher\Event;

class TaskAssignedEvent extends Event
{
    public function __construct(
        private readonly Task $task,
        private readonly User $assignedUser,
    ) {
    }

    public function getTask(): Task
    {
        return $this->task;
    }

    public function getAssignedUser(): User
    {
        return $this->assignedUser;
    }
}

The base class Symfony\Contracts\EventDispatcher\Event is deliberately kept MINIMAL – an event object is, at its core, a simple, immutable data carrier for an event's CONTEXT (WHICH task, WHICH user).

Dispatching the event in the service

src/Service/TaskAssignmentService.php
<?php

declare(strict_types=1);

namespace App\Service;

use App\Entity\Task;
use App\Entity\User;
use App\Event\TaskAssignedEvent;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class TaskAssignmentService
{
    public function __construct(
        private readonly EntityManagerInterface $entityManager,
        private readonly EventDispatcherInterface $eventDispatcher,
    ) {
    }

    public function assign(Task $task, User $user): void
    {
        $task->setAssignedUser($user);
        $this->entityManager->flush();

        $this->eventDispatcher->dispatch(
            new TaskAssignedEvent($task, $user)
        );
    }
}

EventDispatcherInterface is – EXACTLY like LoggerInterface – a service provided by Symfony, available via autowiring. dispatch() fires the event: ALL registered listeners for TaskAssignedEvent run IMMEDIATELY, SYNCHRONOUSLY, in order of their priority (chapter 35).

Achtung: TaskAssignmentService DOESN'T KNOW which (if any) listeners react to this event – that's INTENTIONAL, not a lack of information. This decoupling is exactly the core benefit: chapter 38 adds an email listener WITHOUT touching TaskAssignmentService AT ALL.

A listener for our own event

src/EventListener/TaskAssignedListener.php
<?php

declare(strict_types=1);

namespace App\EventListener;

use App\Event\TaskAssignedEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener]
class TaskAssignedListener
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {
    }

    public function __invoke(TaskAssignedEvent $event): void
    {
        $this->logger->info(sprintf(
            'Task "%s" was assigned to %s.',
            $event->getTask()->getTitle(),
            $event->getAssignedUser()->getName(),
        ));
    }
}

WITHOUT an explicit event: '...' argument (unlike kernel.exception in chapter 35), #[AsEventListener] AUTOMATICALLY detects the event from the __invoke() parameter's type – another advantage of custom, typed event classes over the older, NAME-BASED kernel events.

Multiple listeners for the same custom event

EXACTLY like kernel events, ANY number of listeners can react to TaskAssignedEvent – a second, independent listener (e.g. for a later dashboard activity history) would SIMPLY mean another #[AsEventListener] class with the same event type, WITHOUT changing TaskAssignmentService or the FIRST listener.

Rule of thumb: when is a custom event worth it?

ApproachWhen it fits
Direct method callWhen there's EXACTLY ONE, fixed recipient and that relationship is unlikely to EVER change.
Custom eventWhen an action should trigger SEVERAL, potentially GROWING, INDEPENDENT reactions – like our task assignment, which could soon serve logging, email, push notifications, AND an activity history all at once.

Tipp: A common beginner mistake: turning EVERY method call into an event "because you can" – this makes the code flow HARDER to trace ("who actually reacts to this event?" becomes a real search). Events pay off EXACTLY when decoupling offers a REAL, foreseeable value – not as a default go-to choice.