Skip to Content
ModulesEvents

Event System and Listeners

Flute CMS uses the Symfony Event System to ensure loose coupling between components. Events allow modules to react to actions of other modules without direct dependencies.

Event Architecture

Event Dispatcher

The system uses the standard Symfony EventDispatcher:

<?php use Symfony\Component\EventDispatcher\EventDispatcher; // Get dispatcher instance via helper $dispatcher = events(); // Dispatch event events()->dispatch($event, $eventName); // Add listener events()->addListener($eventName, $listener);

Base Event Class

All events inherit from 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; } }

Existing System Events

Main Flute CMS Events

The system has several built-in events:

<?php // Template rendering events use Flute\Core\Events\BeforeRenderEvent; use Flute\Core\Events\AfterRenderEvent; use Flute\Core\Template\Events\TemplateInitialized; // Routing events use Flute\Core\Events\RoutingStartedEvent; use Flute\Core\Events\RoutingFinishedEvent; use Flute\Core\Events\OnRouteFoundEvent; // User events use Flute\Core\Events\UserChangedEvent; // Module events use Flute\Core\ModulesManager\Events\ModuleRegistered;

Additionally in core:

  • TemplateInitialized — called after template initialization (useful for registering tabs, resources)
  • RegisterPaymentFactoriesEvent — allows registering payment drivers
  • PackageRegisteredEvent/PackageInitializedEvent — admin package events

Creating Custom Events

To create your own event, inherit from the base Event class:

<?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; } }

Events with Additional Data

<?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; } }

Registering Listeners

Creating Listeners

Listeners can be simple functions or classes with methods:

<?php namespace Flute\Modules\News\Listeners; use Flute\Modules\News\Events\NewsPublishedEvent; class NewsNotificationListener { public function handle(NewsPublishedEvent $event): void { $news = $event->getNews(); // Send notifications to users $this->sendNotifications($news); // Logging logs('news')->info('News published', [ 'news_id' => $news->id, 'title' => $news->title, 'author_id' => $news->user_id ]); } private function sendNotifications($news): void { // Notification logic } }

Registering in Module Provider

Listeners are registered in the module provider:

<?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(); // Register listeners events()->addListener(ProfileRenderEvent::NAME, [ProfileListener::class, 'handle']); } }

Simple Callable Listeners

You can use simple functions as listeners:

<?php // In module provider events()->addListener('news.published', function($event) { logs('news')->info('News published: ' . $event->getNews()->title); }); // Anonymous function with additional logic events()->addListener('shop.purchase.completed', function($event) { $purchase = $event->getPurchase(); // Send email to buyer email()->to($purchase->user->email) ->subject('Purchase confirmed') ->send(); });

Real Example from 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(); // Add tab with user bans $bansTab = app(\Flute\Modules\BansManager\Tabs\BansTab::class); $bansTab->setUser($user); $tabs->add($bansTab); } }

Dispatching Events

Sending Events

Events are sent using 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'); } // Publish news $news->published = true; $news->published_at = new \DateTime(); transaction($news)->run(); // Dispatch event events()->dispatch(new NewsPublishedEvent($news), NewsPublishedEvent::NAME); } }

Real Examples from Router

The routing system actively uses events:

<?php // In Router::__construct() events()->dispatch(new RoutingStartedEvent($this), RoutingStartedEvent::NAME); // In Router::dispatch() $onRouteEvent = events()->dispatch(new OnRouteFoundEvent($request, $this->currentRoute), OnRouteFoundEvent::NAME); // After request processing $event = new RoutingFinishedEvent($response); $event = events()->dispatch($event, RoutingFinishedEvent::NAME);

FluteEventDispatcher supports addDeferredListener() and caches listeners in cache('flute.deferred_listeners') so they remain active between requests.

Dispatching Events in Modules

<?php namespace Flute\Modules\Shop\Services; use Flute\Modules\Shop\Events\PurchaseCompletedEvent; class PurchaseService { public function completePurchase(int $purchaseId): void { $purchase = $this->findPurchase($purchaseId); // Complete purchase $purchase->status = 'completed'; $purchase->completed_at = new \DateTime(); transaction($purchase)->run(); // Dispatch event with additional data $metadata = [ 'payment_method' => $purchase->payment_method, 'total_amount' => $purchase->total, 'items_count' => count($purchase->items) ]; events()->dispatch( new PurchaseCompletedEvent($purchase, $metadata), PurchaseCompletedEvent::NAME ); } }

Stopping Event Propagation

Events can stop further propagation:

<?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(); // Security check if ($this->isBlocked($request)) { $event->stopPropagation(); $event->setErrorCode(403); $event->setErrorMessage('Access denied'); } } private function isBlocked($request): bool { // Logic for checking blocking return false; } }

Deferred Listeners

<?php // Deferred listener (cached) events()->addDeferredListener( 'user.registered', [UserNotificationListener::class, 'handle'] );

Best Practices

Naming Events

  1. Use NAME constant — define event name as class constant
  2. Descriptive names — use clear names like news.published, user.registered
  3. Group by module — start name with module name (shop.purchase.completed)
  4. Consistency — use consistent naming style across all modules

Designing Events

  1. Immutability — do not change event data after creation
  2. Minimal data — pass only necessary data
  3. Typing — use strong typing for parameters
  4. Documentation — document event purpose and its data
<?php // ✅ Good — minimal data, typing class ArticlePublishedEvent extends Event { public const NAME = 'blog.article.published'; public function __construct(private Article $article) {} public function getArticle(): Article { return $this->article; } } // ❌ Bad — too much data class ArticlePublishedEvent extends Event { public $article; public $author; public $category; public $tags; public $comments; // ... }

Working with Listeners

  1. Lightweight — listeners should execute quickly
  2. Error handling — always handle exceptions in listeners
  3. Logging — add logging for important actions
  4. Conditional registration — register listeners only when necessary

Examples of Good Code

<?php // ✅ Good — simple and clear listener events()->addListener('news.published', function($event) { logs('news')->info('News published', ['id' => $event->getNews()->id]); }); // ✅ Good — condition check events()->addListener(ProfileRenderEvent::NAME, function($event) { if (user()->hasRole('banned')) { $event->addTab(new BanInfoTab()); } }); // ❌ Bad — heavy operations in listener events()->addListener('user.registered', function($event) { // DO NOT DO THIS — too heavy for synchronous execution $this->sendWelcomeEmail($event->getUser()); $this->generateUserStatistics($event->getUser()); $this->updateRecommendations(); });

The Flute CMS event system is built on the proven Symfony EventDispatcher. It provides a simple and effective way to create loosely coupled components. Proper use of events makes code more flexible, testable, and extensible.