mini\Hooks namespace

Hooks - Event Dispatcher System

Lightweight event dispatchers for extending framework behavior and building event-driven applications.

Dispatcher Classes

Event

Multi-fire dispatcher where all listeners are notified on each trigger.

use mini\Hooks\Event;

/** @var Event<string> */
$onMessage = new Event('message');

$onMessage->listen(function(string $msg) {
    echo "Received: $msg\n";
});

$onMessage->once(function(string $msg) {
    echo "First message only: $msg\n";
});

$onMessage->trigger('Hello');  // Both listeners called
$onMessage->trigger('World');  // Only first listener called

Methods: trigger(mixed ...$args), listen(Closure ...$listeners), once(Closure ...$listeners), off(Closure ...$listeners)

Trigger

One-time event with memory. Late subscribers receive the original payload immediately.

use mini\Hooks\Trigger;

/** @var Trigger<array> */
$onReady = new Trigger('app-ready');

$onReady->listen(fn($config) => setup($config));  // Waits for trigger

$onReady->trigger(['db' => 'mysql']);  // Fires once

$onReady->listen(fn($config) => lateSetup($config));  // Called immediately!

$onReady->wasTriggered();  // true

Methods: trigger(mixed ...$args), listen(Closure ...$listeners), off(Closure ...$listeners), wasTriggered(): bool

Handler

Chain of responsibility - first non-null response wins, remaining listeners skipped.

use mini\Hooks\Handler;

/** @var Handler<\Throwable, string> */
$errorHandler = new Handler('error');

$errorHandler->listen(function(\Throwable $e): ?string {
    if ($e instanceof ValidationException) {
        return "Validation failed: {$e->getMessage()}";
    }
    return null;  // Can't handle, try next
});

$errorHandler->listen(fn(\Throwable $e) => "Error: {$e->getMessage()}");  // Fallback

$response = $errorHandler->trigger(new \RuntimeException('Oops'));

Methods: trigger(mixed $data, mixed ...$args): mixed, listen(Closure ...$listeners), off(Closure ...$listeners)

Filter

Transform data through a pipeline. Each listener receives and must return the value.

use mini\Hooks\Filter;

/** @var Filter<string> */
$htmlFilter = new Filter('html-output');

$htmlFilter->listen(fn(string $html) => trim($html));
$htmlFilter->listen(fn(string $html) => "<!-- Generated -->\n$html");

$output = $htmlFilter->filter('<p>Hello</p>');

Methods: filter(mixed $value, mixed ...$args): mixed, listen(Closure ...$listeners), off(Closure ...$listeners)

StateMachine

Managed state transitions with validation and lifecycle hooks.

use mini\Hooks\StateMachine;

enum Status { case Draft; case Published; case Archived; }

/** @var StateMachine<Status> */
$workflow = new StateMachine([
    [Status::Draft, Status::Published],           // Draft → Published
    [Status::Published, Status::Archived],        // Published → Archived
    [Status::Archived],                           // Terminal
]);

$workflow->onEnteringState(Status::Published, fn($from, $to) => validate());
$workflow->onEnteredState(Status::Published, fn($from, $to) => notify());

$workflow->trigger(Status::Published);
$workflow->getCurrentState();  // Status::Published

Transition hooks: onEnteringState(), onEnteredState(), onExitingState(), onExitedState(), onExitCurrentState()

Methods: trigger(TState $state), listen(Closure ...$listeners), off(Closure ...$listeners), getCurrentState(): TState

PerItemTriggers

One-time triggers per unique source (string or object). Late subscribers for triggered sources are called immediately.

use mini\Hooks\PerItemTriggers;

/** @var PerItemTriggers<string, array> */
$onUserLoaded = new PerItemTriggers('user-loaded');

// Global listener for all users
$onUserLoaded->listen(fn($id, $data) => cache($id, $data));

// Trigger for specific user
$onUserLoaded->triggerFor('user:42', ['name' => 'John']);

// Late subscriber - called immediately since user:42 already triggered
$onUserLoaded->listenFor('user:42', fn($id, $data) => log($data));

$onUserLoaded->wasTriggeredFor('user:42');  // true

Methods: triggerFor(string|object $source, mixed ...$data), listen(Closure ...$listeners), listenFor(string|object $source, Closure ...$listeners), off(Closure ...$listeners), wasTriggeredFor(string|object $source): bool

Typed Event Properties Pattern

Expose dispatchers as typed readonly properties for IDE support and discoverability:

class UserService {
    /** @var Event<User> */
    public readonly Event $onUserCreated;

    /** @var Event<User> */
    public readonly Event $onUserDeleted;

    public function __construct() {
        $this->onUserCreated = new Event('user-created');
        $this->onUserDeleted = new Event('user-deleted');
    }
}

// Usage with full IDE autocompletion
$service->onUserCreated->listen(fn(User $user) => sendWelcome($user));

Base Class: Dispatcher

All dispatchers extend Dispatcher which provides:

  • Exception handling - Configure via Dispatcher::configure() for async event loops
  • Debug info - getDescription(), getFile(), getLine() for tracing dispatcher origins
  • Deferred execution - Listeners are queued and run after the trigger call completes

Classes (7)

Dispatcher

Abstract event dispatcher - root class for Mini's hooks system

abstract
Event

Event dispatcher for events that can trigger multiple times

Filter

Chain of listeners that transform a value

Handler

Event where first non-null response wins

PerItemTriggers

Event dispatcher that triggers once per source (string or object)

StateMachine

A state machine which validates that states only transition to legal target states.

Trigger

Event that can only trigger ONCE