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
<?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
<?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
<?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?
| Approach | When it fits |
|---|---|
| Direct method call | When there's EXACTLY ONE, fixed recipient and that relationship is unlikely to EVER change. |
| Custom event | When 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.