Skip to Content

Система событий и слушателей

Flute CMS использует систему событий Symfony для обеспечения слабой связи между компонентами. События позволяют модулям реагировать на действия других модулей без прямых зависимостей.

Архитектура событий

Диспетчер событий

Система использует стандартный EventDispatcher Symfony:

<?php use Symfony\Component\EventDispatcher\EventDispatcher; // Получение экземпляра диспетчера через helper $dispatcher = events(); // Отправка события events()->dispatch($event, $eventName); // Добавление слушателя events()->addListener($eventName, $listener);

Базовый класс события

Все события наследуются от Symfony\Contracts\EventDispatcher\Event:

<?php namespace Flute\Core\Events; use Symfony\Contracts\EventDispatcher\Event; class UserChangedEvent extends Event { public const NAME = 'flute.user.changed'; private User $user; public function __construct(User $user) { $this->user = $user; } public function getUser(): User { return $this->user; } }

Существующие события системы

Основные события Flute CMS

Система имеет несколько встроенных событий:

<?php // События рендеринга шаблонов use Flute\Core\Events\BeforeRenderEvent; use Flute\Core\Events\AfterRenderEvent; use Flute\Core\Template\Events\TemplateInitialized; // События маршрутизации use Flute\Core\Events\RoutingStartedEvent; use Flute\Core\Events\RoutingFinishedEvent; use Flute\Core\Events\OnRouteFoundEvent; // События пользователей use Flute\Core\Events\UserChangedEvent; // События модулей use Flute\Core\ModulesManager\Events\ModuleRegistered;

Дополнительно в ядре:

  • TemplateInitialized — вызывается после инициализации шаблона (полезно для регистрации вкладок, ресурсов)
  • RegisterPaymentFactoriesEvent — даёт возможность зарегистрировать платежные драйверы
  • PackageRegisteredEvent/PackageInitializedEvent — события админ-пакетов

Создание пользовательских событий

Для создания собственного события наследуйтесь от базового класса Event:

<?php namespace Flute\Modules\News\Events; use Symfony\Contracts\EventDispatcher\Event; use Flute\Modules\News\Database\Entities\News; class NewsPublishedEvent extends Event { public const NAME = 'news.published'; private News $news; public function __construct(News $news) { $this->news = $news; } public function getNews(): News { return $this->news; } public function getNewsId(): int { return $this->news->id; } public function getAuthorId(): int { return $this->news->user_id; } }

События с дополнительными данными

<?php namespace Flute\Modules\Shop\Events; use Symfony\Contracts\EventDispatcher\Event; use Flute\Modules\Shop\Database\Entities\Purchase; class PurchaseCompletedEvent extends Event { public const NAME = 'shop.purchase.completed'; private Purchase $purchase; private array $metadata; public function __construct(Purchase $purchase, array $metadata = []) { $this->purchase = $purchase; $this->metadata = $metadata; } public function getPurchase(): Purchase { return $this->purchase; } public function getMetadata(): array { return $this->metadata; } public function addMetadata(string $key, $value): void { $this->metadata[$key] = $value; } }

Регистрация слушателей

Создание слушателей

Слушатели могут быть простыми функциями или классами с методами:

<?php namespace Flute\Modules\News\Listeners; use Flute\Modules\News\Events\NewsPublishedEvent; class NewsNotificationListener { public function handle(NewsPublishedEvent $event): void { $news = $event->getNews(); // Отправка уведомлений пользователям $this->sendNotifications($news); // Логирование logs('news')->info('News published', [ 'news_id' => $news->id, 'title' => $news->title, 'author_id' => $news->user_id ]); } private function sendNotifications($news): void { // Логика отправки уведомлений } }

Регистрация в провайдере модуля

Слушатели регистрируются в провайдере модуля:

<?php namespace Flute\Modules\BansManager\Providers; use Flute\Core\Support\ModuleServiceProvider; use Flute\Core\Modules\Profile\Events\ProfileRenderEvent; use Flute\Modules\BansManager\Listeners\ProfileListener; class BansManagerProvider extends ModuleServiceProvider { public function boot(\DI\Container $container): void { $this->bootstrapModule(); // Регистрация слушателей events()->addListener(ProfileRenderEvent::NAME, [ProfileListener::class, 'handle']); } }

Простые callable слушатели

Можно использовать простые функции как слушатели:

<?php // В провайдере модуля events()->addListener('news.published', function($event) { logs('news')->info('News published: ' . $event->getNews()->title); }); // Анонимная функция с дополнительной логикой events()->addListener('shop.purchase.completed', function($event) { $purchase = $event->getPurchase(); // Отправка email покупателю email()->to($purchase->user->email) ->subject('Покупка подтверждена') ->send(); });

Реальный пример из BansManager

<?php namespace Flute\Modules\BansManager\Listeners; use Flute\Core\Modules\Profile\Events\ProfileRenderEvent; class ProfileListener { public function handle(ProfileRenderEvent $event): void { $user = $event->getUser(); $tabs = $event->getTabs(); // Добавление вкладки с банами пользователя $bansTab = app(\Flute\Modules\BansManager\Tabs\BansTab::class); $bansTab->setUser($user); $tabs->add($bansTab); } }

Диспетчеризация событий

Отправка событий

События отправляются с помощью events()->dispatch():

<?php namespace Flute\Modules\News\Services; use Flute\Modules\News\Events\NewsPublishedEvent; class NewsService { public function publishNews(int $newsId): void { $news = rep(\Flute\Modules\News\Database\Entities\News::class)->findByPK($newsId); if (!$news) { throw new \Exception('News not found'); } // Публикация новости $news->published = true; $news->published_at = new \DateTime(); transaction($news)->run(); // Отправка события events()->dispatch(new NewsPublishedEvent($news), NewsPublishedEvent::NAME); } }

Реальные примеры из Router

Система маршрутизации активно использует события:

<?php // В Router::__construct() events()->dispatch(new RoutingStartedEvent($this), RoutingStartedEvent::NAME); // В Router::dispatch() $onRouteEvent = events()->dispatch(new OnRouteFoundEvent($request, $this->currentRoute), OnRouteFoundEvent::NAME); // После обработки запроса $event = new RoutingFinishedEvent($response); $event = events()->dispatch($event, RoutingFinishedEvent::NAME);

FluteEventDispatcher поддерживает addDeferredListener() и кеширует слушателей в cache('flute.deferred_listeners'), чтобы они оставались активными между запросами.

Отправка событий в модулях

<?php namespace Flute\Modules\Shop\Services; use Flute\Modules\Shop\Events\PurchaseCompletedEvent; class PurchaseService { public function completePurchase(int $purchaseId): void { $purchase = $this->findPurchase($purchaseId); // Завершение покупки $purchase->status = 'completed'; $purchase->completed_at = new \DateTime(); transaction($purchase)->run(); // Отправка события с дополнительными данными $metadata = [ 'payment_method' => $purchase->payment_method, 'total_amount' => $purchase->total, 'items_count' => count($purchase->items) ]; events()->dispatch( new PurchaseCompletedEvent($purchase, $metadata), PurchaseCompletedEvent::NAME ); } }

Остановка распространения события

События могут остановить дальнейшее распространение:

<?php namespace Flute\Modules\Security\Listeners; use Flute\Core\Events\OnRouteFoundEvent; class SecurityListener { public function handle(OnRouteFoundEvent $event): void { $route = $event->getRoute(); $request = $event->getRequest(); // Проверка безопасности if ($this->isBlocked($request)) { $event->stopPropagation(); $event->setErrorCode(403); $event->setErrorMessage('Access denied'); } } private function isBlocked($request): bool { // Логика проверки блокировки return false; } }

Deferred Listeners

<?php // Отложенный слушатель (кешируется) events()->addDeferredListener( 'user.registered', [UserNotificationListener::class, 'handle'] );

Лучшие практики

Именование событий

  1. Используйте константу NAME — определяйте имя события как константу класса
  2. Описательные имена — используйте понятные имена типа news.published, user.registered
  3. Группировка по модулям — начинайте имя с названия модуля (shop.purchase.completed)
  4. Единообразие — используйте одинаковый стиль именования во всех модулях

Проектирование событий

  1. Неизменяемость — не изменяйте данные события после создания
  2. Минимальные данные — передавайте только необходимые данные
  3. Типизация — используйте строгую типизацию для параметров
  4. Документирование — документируйте назначение события и его данные
<?php // ✅ Хорошо — минимальные данные, типизация class ArticlePublishedEvent extends Event { public const NAME = 'blog.article.published'; public function __construct(private Article $article) {} public function getArticle(): Article { return $this->article; } } // ❌ Плохо — слишком много данных class ArticlePublishedEvent extends Event { public $article; public $author; public $category; public $tags; public $comments; // ... }

Работа со слушателями

  1. Легковесность — слушатели должны выполняться быстро
  2. Обработка ошибок — всегда обрабатывайте исключения в слушателях
  3. Логирование — добавляйте логирование важных действий
  4. Условная регистрация — регистрируйте слушателей только при необходимости

Примеры хорошего кода

<?php // ✅ Хорошо — простой и понятный слушатель events()->addListener('news.published', function($event) { logs('news')->info('News published', ['id' => $event->getNews()->id]); }); // ✅ Хорошо — проверка условий events()->addListener(ProfileRenderEvent::NAME, function($event) { if (user()->hasRole('banned')) { $event->addTab(new BanInfoTab()); } }); // ❌ Плохо — тяжелые операции в слушателе events()->addListener('user.registered', function($event) { // НЕ ДЕЛАЙТЕ ТАК — слишком тяжело для синхронного выполнения $this->sendWelcomeEmail($event->getUser()); $this->generateUserStatistics($event->getUser()); $this->updateRecommendations(); });

Система событий Flute CMS построена на основе проверенного EventDispatcher Symfony. Она предоставляет простой и эффективный способ создания слабо связанных компонентов. Правильное использование событий делает код более гибким, тестируемым и расширяемым.