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 driversPackageRegisteredEvent/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
- Use NAME constant — define event name as class constant
- Descriptive names — use clear names like
news.published,user.registered - Group by module — start name with module name (
shop.purchase.completed) - Consistency — use consistent naming style across all modules
Designing Events
- Immutability — do not change event data after creation
- Minimal data — pass only necessary data
- Typing — use strong typing for parameters
- 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
- Lightweight — listeners should execute quickly
- Error handling — always handle exceptions in listeners
- Logging — add logging for important actions
- 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.