Введение в событийную систему Symfony/EventDispatcher
Событийная система (Event Dispatcher) — это один из ключевых компонентов экосистемы Symfony, который реализует шаблон проектирования "Наблюдатель" (Observer) и "Медиатор" (Mediator). Библиотека symfony/event-dispatcher предоставляет легковесный, но мощный механизм для создания слабосвязанной архитектуры приложения. Вместо того чтобы жёстко связывать компоненты прямыми вызовами методов, вы можете "генерировать" события, а любые заинтересованные части приложения (слушатели/подписчики) могут на них реагировать. Это кардинально упрощает расширение функциональности, тестирование и поддержку кода.
Зачем это нужно? Представьте, что вам нужно добавить логирование каждого действия пользователя, отправку email-уведомления после регистрации и обновление кэша после изменения данных. Без событийной системы вам пришлось бы вставлять вызовы соответствующих сервисов прямо в методы контроллера или модели. Это создаёт жёсткую связь (coupling) и делает код "хрупким". С EventDispatcher вы просто генерируете событие UserRegistered, а отдельные слушатели (логгер, почтовик, кэшер) подписываются на него и выполняют свою работу. Добавление нового функционала сводится к написанию нового слушателя, без изменения существующего кода.
Библиотека используется не только в полноценном фреймворке Symfony, но и в микрофреймворках (Silex, Laravel — имеет свою реализацию, но концепция та же), а также в любых PHP-проектах, где требуется гибкая система хуков и плагинов. Она является стандартом де-факто для событийно-ориентированного программирования в PHP.
Установка
Установка производится через Composer — стандартный менеджер пакетов для PHP. Откройте терминал в корне вашего проекта и выполните команду:
composer require symfony/event-dispatcher
Если вы планируете использовать контейнер внедрения зависимостей (Dependency Injection) и автоматическую регистрацию слушателей, рекомендуется также установить пакет для работы с DependencyInjection (обычно идёт в комплекте с Symfony Framework). Для автономного использования достаточно только symfony/event-dispatcher.
Быстрый старт: минимальный рабочий пример
Давайте создадим простейшую систему: будем генерировать событие "order.placed" (заказ размещён) и слушать его.
<?php
// 1. Подключаем автозагрузчик Composer
require_once 'vendor/autoload.php';
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\GenericEvent;
// 2. Создаём диспетчер
$dispatcher = new EventDispatcher();
// 3. Добавляем слушатель (замыкание) на событие 'order.placed'
$dispatcher->addListener('order.placed', function (GenericEvent $event) {
$orderData = $event->getSubject();
echo "Слушатель 1: Заказ {$orderData['id']} размещён. Сумма: {$orderData['total']}\n";
});
// 4. Создаём событие и передаём в него данные
$event = new GenericEvent(['id' => 123, 'total' => 2500.00]);
// 5. Диспетчеризуем (генерируем) событие
$dispatcher->dispatch($event, 'order.placed');
// Вывод: Слушатель 1: Заказ 123 размещён. Сумма: 2500
Этот пример демонстрирует базовую механику: создание диспетчера, регистрацию слушателя и отправку события. Обратите внимание, что слушатель получает объект события, из которого можно извлечь любые данные.
Основные методы и классы
Рассмотрим 8 ключевых элементов библиотеки, которые покрывают 90% сценариев использования.
1. EventDispatcher::addListener()
Сигнатура: addListener(string $eventName, callable $listener, int $priority = 0): void
Что делает: Регистрирует слушатель (callable) для указанного имени события. Можно указать приоритет: чем выше число, тем раньше будет вызван слушатель.
$dispatcher->addListener('user.login', function ($event) {
echo "Пользователь вошёл в систему.\n";
}, 10); // Приоритет 10
2. EventDispatcher::removeListener()
Сигнатура: removeListener(string $eventName, callable $listener): void
Что делает: Удаляет ранее зарегистрированный слушатель для указанного события. Полезно для динамического управления подписками.
$listener = function ($event) { /* ... */ };
$dispatcher->addListener('some.event', $listener);
// ... позже ...
$dispatcher->removeListener('some.event', $listener);
3. EventDispatcher::dispatch()
Сигнатура: dispatch(object $event, string $eventName = null): object
Что делает: Генерирует событие. Передаёт объект события всем зарегистрированным слушателям. Возвращает модифицированный объект события (слушатели могут его изменять).
$event = new GenericEvent(['key' => 'value']);
$dispatcher->dispatch($event, 'my.custom.event');
4. EventDispatcher::getListeners()
Сигнатура: getListeners(string $eventName = null): array
Что делает: Возвращает массив всех слушателей для указанного события (или для всех событий, если имя не указано). Слушатели отсортированы по приоритету.
$listeners = $dispatcher->getListeners('order.placed');
foreach ($listeners as $listener) {
// $listener - это callable
}
5. EventDispatcher::hasListeners()
Сигнатура: hasListeners(string $eventName = null): bool
Что делает: Проверяет, есть ли хотя бы один слушатель для указанного события (или для любого события, если имя не указано).
if ($dispatcher->hasListeners('user.deleted')) {
// Есть подписчики на удаление пользователя
}
6. EventDispatcher::addSubscriber()
Сигнатура: addSubscriber(EventSubscriberInterface $subscriber): void
Что делает: Регистрирует подписчика — объект класса, реализующего интерфейс EventSubscriberInterface. Подписчик может объявить несколько методов для обработки разных событий, что удобно для группировки логики.
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class OrderSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [
'order.placed' => 'onOrderPlaced',
'order.cancelled' => 'onOrderCancelled',
];
}
public function onOrderPlaced($event) { echo "Order placed\n"; }
public function onOrderCancelled($event) { echo "Order cancelled\n"; }
}
$subscriber = new OrderSubscriber();
$dispatcher->addSubscriber($subscriber);
7. GenericEvent::getSubject()
Сигнатура: getSubject(): mixed
Что делает: Извлекает "субъект" события — те данные, которые были переданы в конструктор GenericEvent. Обычно это массив или объект, представляющий контекст события.
$event = new GenericEvent($userObject);
$user = $event->getSubject(); // $userObject
8. EventDispatcher::getListenerPriority()
Сигнатура: getListenerPriority(string $eventName, callable $listener): ?int
Что делает: Возвращает приоритет конкретного слушателя для указанного события. Возвращает null, если слушатель не найден.
$priority = $dispatcher->getListenerPriority('order.placed', $myListener);
echo "Приоритет: " . ($priority ?? 'не найден');
Реальный практический пример (150+ строк кода)
Разработаем мини-систему обработки заказов с логированием, отправкой email и обновлением статистики. Создадим три отдельных файла для наглядности.
Файл: events.php (определения событий)
<?php
// events.php
namespace App\Event;
use Symfony\Component\EventDispatcher\GenericEvent;
class OrderEvents {
const PLACED = 'order.placed';
const CANCELLED = 'order.cancelled';
}
class OrderPlacedEvent extends GenericEvent {
public function __construct(array $orderData) {
parent::__construct($orderData);
}
public function getOrderId(): int {
return $this->getSubject()['id'];
}
public function getTotal(): float {
return $this->getSubject()['total'];
}
}
Файл: listeners.php (слушатели)
<?php
// listeners.php
namespace App\Listener;
use App\Event\OrderPlacedEvent;
class LoggerListener {
public function onOrderPlaced(OrderPlacedEvent $event): void {
$log = sprintf(
"[LOG] Заказ #%d размещён на сумму %.2f\n",
$event->getOrderId(),
$event->getTotal()
);
file_put_contents('orders.log', $log, FILE_APPEND);
}
}
class EmailListener {
public function onOrderPlaced(OrderPlacedEvent $event): void {
// Имитация отправки email
echo "Email отправлен: Заказ #{$event->getOrderId()} подтверждён.\n";
}
}
class StatsListener {
private int $totalOrders = 0;
private float $revenue = 0.0;
public function onOrderPlaced(OrderPlacedEvent $event): void {
$this->totalOrders++;
$this->revenue += $event->getTotal();
echo sprintf("Статистика: всего заказов: %d, выручка: %.2f\n",
$this->totalOrders, $this->revenue);
}
}
Файл: app.php (главный скрипт)
<?php
// app.php
require_once 'vendor/autoload.php';
use Symfony\Component\EventDispatcher\EventDispatcher;
use App\Event\OrderPlacedEvent;
use App\Event\OrderEvents;
use App\Listener\LoggerListener;
use App\Listener\EmailListener;
use App\Listener\StatsListener;
// Инициализация диспетчера
$dispatcher = new EventDispatcher();
// Регистрация слушателей с разными приоритетами
$logger = new LoggerListener();
$emailer = new EmailListener();
$stats = new StatsListener();
// Логгер должен отработать первым (приоритет 100)
$dispatcher->addListener(OrderEvents::PLACED, [$logger, 'onOrderPlaced'], 100);
// Статистика — второй (приоритет 50)
$dispatcher->addListener(OrderEvents::PLACED, [$stats, 'onOrderPlaced'], 50);
// Email — последним (приоритет 0)
$dispatcher->addListener(OrderEvents::PLACED, [$emailer, 'onOrderPlaced'], 0);
// Создаём заказ
$orderData = [
'id' => rand(1000, 9999),
'total' => round(mt_rand(100, 10000) / 100, 2),
'items' => ['Товар A', 'Товар B']
];
// Генерируем событие
$event = new OrderPlacedEvent($orderData);
$dispatcher->dispatch($event, OrderEvents::PLACED);
echo "\nЗаказ успешно обработан!\n";
// Пример снятия слушателя
$dispatcher->removeListener(OrderEvents::PLACED, [$emailer, 'onOrderPlaced']);
echo "Слушатель email удалён. Повторная диспетчеризация:\n";
$dispatcher->dispatch(new OrderPlacedEvent($orderData), OrderEvents::PLACED);
Этот пример показывает, как легко можно управлять слушателями, менять их приоритеты и динамически добавлять/удалять их, не трогая основную бизнес-логику.
Советы по производительности и типичные ошибки
Производительность
- Избегайте избыточного количества слушателей. Хотя EventDispatcher оптимизирован, сотни слушателей на одно событие могут замедлить выполнение. Группируйте логику в подписчиках.
- Используйте приоритеты разумно. Не назначайте всем слушателям приоритет 0. Если какой-то слушатель должен выполниться строго до или после другого, используйте приоритеты (например, 100 для валидации, -100 для логирования).
- Ленивая загрузка слушателей. В больших проектах регистрируйте слушатели через контейнер зависимостей (DIC), чтобы они создавались только при необходимости. Symfony DIC поддерживает тегирование сервисов как слушателей.
- Не передавайте тяжёлые объекты в событиях. Если событие содержит, например, полный объект Doctrine Entity, это может привести к неожиданным
Также в библиотеке
Elasticsearch клиент для C++: elasticlient и полное руководство
Лучшие библиотеки логирования для Go (Golang): обзор и сравнение
NHibernate для C# (.NET): полное руководство с примерами
Сериализация объектов в Java: полное руководство для начинающих