Symfony/EventDispatcher для PHP: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в событийную систему 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, это может привести к неожиданным

Рекомендаций