Skip to content

Recipes

Muhammet Şafak edited this page May 25, 2026 · 2 revisions

Recipes

Practical, copy-pasteable patterns that combine the building blocks documented elsewhere in this wiki. Each recipe links back to the relevant reference page.

1. WordPress-style action hooks

A drop-in for the do_action / add_action mental model: a global event name, multiple subscribers, no return value coordination.

use InitPHP\Events\Events;

// Subscribers — declared anywhere in your app.
Events::on('post.published', function (array $post) {
    PingSitemap::send($post['url']);
});

Events::on('post.published', function (array $post) {
    Newsletter::queue($post['id']);
});

Events::on('post.published', function (array $post) {
    Cache::invalidate('feed');
});

// Trigger — from wherever the action happens.
Events::trigger('post.published', $post);

Reference: Events facade.

2. Veto-able pre-save hook

Validate a payload via a chain of listeners. Any listener can stop the save by returning false. Use Events::trigger() and check its return value.

use InitPHP\Events\Events;

Events::on('user.before_save', function (array &$user) {
    if (!filter_var($user['email'], FILTER_VALIDATE_EMAIL)) {
        return false; // veto
    }
});

Events::on('user.before_save', function (array &$user) {
    $user['email'] = strtolower($user['email']); // mutate
});

Events::on('user.before_save', function (array $user) {
    if (User::emailTaken($user['email'])) {
        return false; // veto
    }
});

if (Events::trigger('user.before_save', $userArray) === false) {
    throw new RuntimeException('Validation failed.');
}

User::save($userArray);

References: Stopping Propagation, passing arguments by reference (PHP listener convention).

3. Decoupled domain events with EventEmitter

When you would rather depend-inject an event bus than reach for a global, instantiate one and pass it through your DI container.

use InitPHP\Events\EventEmitter;
use InitPHP\Events\EventEmitterInterface;

final class OrderService
{
    /** @var EventEmitterInterface */
    private $events;

    public function __construct(EventEmitterInterface $events)
    {
        $this->events = $events;
    }

    public function place(array $order): void
    {
        // … persist …
        $this->events->emit('order.placed', [$order]);
    }
}

// composition root
$bus = new EventEmitter();
$bus->on('order.placed', new SendOrderConfirmation());
$bus->on('order.placed', new EnqueueShipping());

$service = new OrderService($bus);
$service->place($order);

Reference: EventEmitter.

4. One-shot bootstrap step

Run an initialiser exactly once, even if the trigger fires more than once (e.g. lazy initialisation). Both APIs expose once():

use InitPHP\Events\Events;

Events::once('app.ready', function () {
    Container::warmup();
});

Events::trigger('app.ready'); // warmup runs
Events::trigger('app.ready'); // silent no-op

Or, with the low-level emitter:

use InitPHP\Events\EventEmitter;

$bus = new EventEmitter();
$bus->once('app.ready', fn () => Container::warmup());
$bus->emit('app.ready'); // warmup runs
$bus->emit('app.ready'); // silent no-op

Reference: Events facade › once and EventEmitteronce.

5. Test isolation — start each test with a clean facade

Call Events::reset() in setUp() / tearDown() so listeners, simulate / debug flags, and the debug log from one test never bleed into the next:

use InitPHP\Events\Events;

final class MyTest extends \PHPUnit\Framework\TestCase
{
    protected function setUp(): void  { Events::reset(); }
    protected function tearDown(): void { Events::reset(); }

    public function test_save_path_is_reached(): void
    {
        $captured = null;
        Events::on('save', function ($payload) use (&$captured): void {
            $captured = $payload;
        });

        runProductionCodePath();

        $this->assertSame('expected', $captured);
    }
}

If you need a dry-run of a code path (walk the listener queue without actually firing the side effects), combine reset() with setSimulate(true) — or inject a pre-configured dispatcher:

$dryRun = (new Event())->setSimulate(true)->setDebugMode(true);
Events::setInstance($dryRun);

runProductionCodePath();

$dispatched = array_column(Events::getDebug(), 'event');
// $dispatched contains the events that *would* have fired.

Reference: Simulate & Debug Mode, Events facade › Singleton lifecycle.

6. Lightweight profiler for a hot path

Enable debug mode around a critical section and inspect per-listener timings:

use InitPHP\Events\Events;

Events::setDebugMode(true);

Events::trigger('checkout.flow', $cart);

$debug = Events::getDebug();
usort($debug, fn($a, $b) => ($b['end']-$b['start']) <=> ($a['end']-$a['start']));

foreach (array_slice($debug, 0, 5) as $row) {
    $ms = ($row['end'] - $row['start']) * 1000;
    printf("%6.2fms  %s\n", $ms, $row['event']);
}

Events::setDebugMode(false);

Reference: Simulate & Debug Mode › Debug mode.

7. Removing a listener you cannot recapture

EventEmitter::removeListener() needs the exact callable. If you can't keep a reference (e.g. the original code is a third-party plugin), nuke the entire event:

$bus->removeAllListeners('order.placed');

If you only want to drop listeners temporarily, snapshot first:

$snapshot = $bus->listeners('order.placed');
$bus->removeAllListeners('order.placed');

try {
    $bus->emit('order.placed', [$order]); // no listeners run
} finally {
    foreach ($snapshot as $listener) {
        $bus->on('order.placed', $listener);
    }
}

References: EventEmitterremoveListener, removeAllListeners.

8. Class-based listener with __invoke

Group listener logic into a class — handy when the handler needs dependencies of its own.

final class SendOrderConfirmation
{
    public function __construct(private Mailer $mailer) {}

    public function __invoke(array $order): void
    {
        $this->mailer->send($order['email'], 'Order confirmation', /* … */);
    }
}

$bus->on('order.placed', new SendOrderConfirmation($mailer));

The same trick works on the Events facade:

Events::on('order.placed', new SendOrderConfirmation($mailer));

9. Forwarding events between buses

You can wire one bus's emissions into another by registering a thin adapter listener:

$bus = new EventEmitter();
$bus->on('user.registered', function (...$args) {
    Events::trigger('user.registered', ...$args);
});

Useful while migrating from one API to the other, or when integrating two subsystems that each prefer a different style.

See also

Clone this wiki locally