-
Notifications
You must be signed in to change notification settings - Fork 0
Event System
Razy provides two distinct event systems for different use cases:
-
Module Event System — Decoupled inter-module communication within a Distributor, using
Agent::listen()andController::trigger(). -
PSR-14 Event Dispatcher — A standalone, standards-compliant event dispatcher with priority and stoppable events.
The module event system enables modules to fire events that other modules can listen and respond to, without direct dependencies between them. Events are scoped to a single Distributor — modules in different Distributors cannot listen to each other's events.
Event names use the format vendor/module_code:event_name:
'vendor/module_code:event_name'
The vendor and module code identify the emitting module (the one that fires the event). The event name identifies the specific event within that module.
Register listeners in __onInit() using $agent->listen(). The method signature is:
Agent::listen(mixed $event, null|string|callable $path = null): bool|arrayParameters:
| Parameter | Type | Description |
| --- | --- | --- |
| $event | string\|array | Event name or array of event names to listen to |
| $path | null\|string\|callable | Inline closure, file path to handler, or null for batch registration |
Return value: bool → true if the target module is loaded (available for events), false otherwise. When an array is passed, returns an associative array of results.
The simplest approach — pass a closure directly:
public function __onInit(Agent $agent): bool
{
$agent->listen('demo/event_demo:user_registered', function ($userData) {
// Process the event and optionally return data
return [
'status' => 'received',
'user' => $userData['name'],
'receiver' => 'event_receiver',
];
});
return true;
}Point to a PHP file that returns a closure. The file is loaded via ClosureLoader and bound to the controller instance:
public function __onInit(Agent $agent): bool
{
// Loads from the module's controller/ directory
$agent->listen('demo/event_demo:user_registered', 'onUserRegistered');
return true;
}The handler file controller/onUserRegistered.php:
// controller/onUserRegistered.php
return function ($userData) {
// $this refers to the controller instance
return ['handled' => true, 'by' => $this->getModuleInfo()->getCode()];
};Pass an array of event-to-handler mappings:
public function __onInit(Agent $agent): bool
{
$results = $agent->listen([
'demo/event_demo:user_registered' => function ($userData) {
return ['status' => 'ok'];
},
'demo/event_demo:user_deleted' => 'onUserDeleted',
]);
// $results = ['demo/event_demo:user_registered' => true, ...]
return true;
}Fire events from within a controller method using $this->trigger():
Controller::trigger(string $event, ?callable $callback = null): EventEmitterParameters:
| Parameter | Type | Description |
| --- | --- | --- |
| $event | string | Event name (without vendor prefix — automatically prepended) |
| $callback | ?callable | Optional callback invoked with each listener's response |
// In a route handler or controller method
$emitter = $this->trigger('user_registered');
$emitter->resolve($userData);
$responses = $emitter->getAllResponse();The EventEmitter object returned by trigger() provides two methods:
| Method | Return | Description |
| --- | --- | --- |
| resolve(...$args): self | EventEmitter | Broadcasts args to all registered listeners, collecting their return values |
| getAllResponse(): array | array | Returns an array of all listener return values (raw values, no envelope) |
⚠️ Important:resolve()returns$thisfor chaining. Each listener receives the same arguments and their return value is collected. There is no standard response envelope —getAllResponse()returns the raw return values from each handler.
The complete event lifecycle follows four stages:
1. Registration Module B calls $agent->listen('vendor/A:some_event', handler)
→ during __onInit()
2. Creation Module A calls $this->trigger('some_event')
→ returns EventEmitter with registered listeners
3. Resolution $emitter->resolve($arg1, $arg2, ...)
→ each listener is invoked, return values collected
4. Collection $emitter->getAllResponse()
returns array of all listener return values
Module A → Event Emitter (demo/event_demo):
// controller/main.php → route handler
return function () {
$userData = [
'name' => 'Alice',
'email' => 'alice@example.com',
];
$emitter = $this->trigger('user_registered');
$emitter->resolve($userData);
$responses = $emitter->getAllResponse();
// $responses contains return values from all listeners
};Module B → Event Listener (demo/event_receiver):
// Controller class
public function __onInit(Agent $agent): bool
{
$agent->listen('demo/event_demo:user_registered', function ($userData) {
return [
'status' => 'received',
'user' => $userData['name'],
'receiver' => 'event_receiver',
];
});
return true;
}If a listener throws an exception during resolve(), the error is caught and passed to the emitting module's __onError hook. Other listeners continue to execute normally.
public function __onError(string $path, Throwable $exception): void
{
// Log the error or take corrective action
error_log("Event error at {$path}: " . $exception->getMessage());
}-
Events are scoped to a single Distributor — modules loaded in different distributors cannot communicate via events.
-
There is no priority mechanism — listeners execute in registration order.
-
There is no propagation stopping — all listeners always execute (errors are caught, not propagated).
-
Event names do not support wildcards or pattern matching.
Razy also includes a standalone PSR-14 compliant event system in the Razy\Event namespace. This system is independent of the module event system and can be used anywhere.
| Class | Description |
| --- | --- |
| Razy\Event\EventDispatcher | Dispatches event objects to registered listeners |
| Razy\Event\ListenerProvider | Stores listeners with optional priority ordering |
| Razy\Event\StoppableEvent | Base class for events that support propagation stopping |
use Razy\Event\EventDispatcher;
use Razy\Event\ListenerProvider;
$provider = new ListenerProvider();
// Register listeners with optional priority (higher = earlier)
$provider->addListener(UserRegistered::class, function (UserRegistered $event) {
// Handle event
}, priority: 10);
$provider->addListener(UserRegistered::class, function (UserRegistered $event) {
// Lower priority, runs after
}, priority: 0);
$dispatcher = new EventDispatcher($provider);
// Dispatch returns the event object
$event = $dispatcher->dispatch(new UserRegistered($user));ListenerProvider::addListener(string $eventClass, callable $listener, int $priority = 0): static| Parameter | Type | Description |
| --- | --- | --- |
| $eventClass | string | Fully-qualified class name of the event |
| $listener | callable | Listener callback receiving the event object |
| $priority | int | Execution priority (higher runs first, default 0) |
Extend StoppableEvent to allow listeners to halt propagation:
use Razy\Event\StoppableEvent;
class UserRegistered extends StoppableEvent
{
public function __construct(public readonly array $user) {}
}
// In a listener
$provider->addListener(UserRegistered::class, function (UserRegistered $event) {
$event->stopPropagation();
// No further listeners will be called
});| Feature | Module Events | PSR-14 Events |
| --- | --- | --- |
| Scope | Within a Distributor | Anywhere in PHP |
| Event identity | String name (vendor/code:event) | Class name (FQCN) |
| Priority | No (registration order) | Yes (numeric) |
| Stop propagation | No | Yes (StoppableEvent) |
| Response collection | Yes (getAllResponse()) | Via event object mutation |
| Auto-wiring | Bound to controller context | Independent |
| Use case | Inter-module communication | General-purpose eventing |
| Scenario | Use | Why |
| --- | --- | --- |
| One module notifying many | Module Events (fireEvent) | Broadcast to all listeners; responses aggregated |
| Calling a specific module's function | Cross-Module API (fire) | Direct call with known module_code:action |
| Need to stop processing early | PSR-14 Events | StoppableEvent with stopPropagation() |
| Collecting responses from all listeners | Module Events | getAllResponse() aggregates return values |
| Need priority ordering | PSR-14 Events | Supports numeric priority on listeners |
| Decoupled plugin architecture | Module Events | Listeners don't need to know emitter identity |