Skip to content

Event System

Ray Fung edited this page Feb 26, 2026 · 4 revisions

Event System

Razy provides two distinct event systems for different use cases:

  1. Module Event System — Decoupled inter-module communication within a Distributor, using Agent::listen() and Controller::trigger().

  2. PSR-14 Event Dispatcher — A standalone, standards-compliant event dispatcher with priority and stoppable events.

Module Event System

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 Name Format

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.

Registering Listeners

Register listeners in __onInit() using $agent->listen(). The method signature is:

Agent::listen(mixed $event, null|string|callable $path = null): bool|array

Parameters:

| 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: booltrue if the target module is loaded (available for events), false otherwise. When an array is passed, returns an associative array of results.

Inline Closure Handler

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;

}

File-Based Handler

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()];

};

Batch Registration

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;

}

Firing Events

Fire events from within a controller method using $this->trigger():

Controller::trigger(string $event, ?callable $callback = null): EventEmitter

Parameters:

| 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();

EventEmitter API

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 $this for 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.

Event Flow

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

Complete Dual-Module Example

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;

}

Error Handling

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());

}

Scoping Rules

  • 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.


PSR-14 Event Dispatcher

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.

Components

| 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 |

Basic Usage

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 API

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) |

Stoppable Events

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

});

Module vs PSR-14 Comparison

| 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 |


Decision Guide — Events vs Cross-Module API

| 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 |

← PreviousRouting

Next → Cross-Module API

Clone this wiki locally