Dispatcher.php
PHP
Path: src/Hooks/Dispatcher.php
<?php
namespace mini\Hooks;
use Closure;
use LogicException;
use Throwable;
/**
* Abstract event dispatcher - root class for Mini's hooks system
*
* @package mini\Hooks
*/
abstract class Dispatcher {
private static int $queueFirstIndex = 0;
private static int $queueLastIndex = 0;
private static array $queuedListeners = [];
private static array $queuedArgs = [];
private static array $queuedDispatchers = [];
/**
* Stack trace of where this event dispatcher was constructed
*
* @var array
*/
private array $constructTrace = [];
/**
* Closure invoked whenever an event listener throws an exception
*
* @var Closure `function(Throwable $exception, Closure $listener, Dispatcher $event): void`
*/
private static ?Closure $exceptionHandler = null;
/**
* Closure which schedules a function to be invoked asynchronously
*
* @var null|Closure
*/
private static ?Closure $deferFunction = null;
/**
* Closure which runs all scheduled events
*
* @var null|Closure
*/
private static ?Closure $runEventsFunction = null;
public function __construct(
private readonly ?string $description = null,
) {
// Store construction location for debugging
$this->constructTrace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
}
/**
* Get a description of the event dispatcher
*
* @return null|string
*/
public final function getDescription(): ?string {
return $this->description;
}
/**
* Get the filename where this event dispatcher was created
*
* @return string
*/
public final function getFile(): string {
return $this->constructTrace['file'] ?? 'unknown';
}
/**
* Get the line number where this event dispatcher was created
*
* @return int
*/
public final function getLine(): int {
return $this->constructTrace['line'] ?? 0;
}
/**
* Configure event loop integration
*
* @param Closure $deferFunction
* @param Closure $runEventsFunction
* @param Closure $exceptionHandler
* @throws LogicException
*/
public final static function configure(Closure $deferFunction, Closure $runEventsFunction, Closure $exceptionHandler): void {
if (self::$deferFunction !== null) {
throw new LogicException("Event loop already configured");
}
self::$deferFunction = $deferFunction;
self::$runEventsFunction = $runEventsFunction;
self::$exceptionHandler = $exceptionHandler;
}
/**
* Invoke all listeners in an array
*
* @param Closure[] $listeners Array of closures
* @param mixed ...$args Arguments to pass to listeners
*/
protected function invokeAll(array $listeners, mixed ...$args): void {
foreach ($listeners as $listener) {
self::defer($this, $listener, $args);
}
self::runEvents();
}
/**
* Handle an exception from a listener
*
* @param Throwable $exception
* @param null|Closure $listener
* @param null|Dispatcher $source
* @throws Throwable
*/
protected static function handleException(Throwable $exception, ?Closure $listener, ?Dispatcher $source): void {
if (self::$exceptionHandler !== null) {
try {
(self::$exceptionHandler)(exception: $exception, listener: $listener, event: $source);
} catch (\Throwable $e) {
// Fatal: exception handler itself threw
error_log("Hook exception handler failed: " . $e->getMessage());
throw $e;
}
} else {
throw $exception;
}
}
/**
* Filter array to remove specific values
*
* @param array $array Source array
* @param mixed $valueToRemove Value to remove
* @param null|int $count Number of elements removed
* @param int|null $limit Limit removals
* @return array
*/
protected static function filterArray(array $array, mixed $valueToRemove, ?int &$count = 0, ?int $limit = null): array {
$result = [];
foreach ($array as $v) {
if ($limit !== $count && $v === $valueToRemove) {
++$count;
continue;
}
$result[] = $v;
}
return $result;
}
/**
* Schedule a function to run when runEvents() is called
*
* @param Dispatcher $dispatcher
* @param Closure $listener
* @param array $args
*/
protected static function defer(Dispatcher $dispatcher, Closure $listener, array $args): void {
if (self::$deferFunction) {
(self::$deferFunction)(self::invoke(...), $dispatcher, $listener, $args);
} else {
self::$queuedListeners[self::$queueLastIndex] = $listener;
self::$queuedArgs[self::$queueLastIndex] = $args;
self::$queuedDispatchers[self::$queueLastIndex] = $dispatcher;
++self::$queueLastIndex;
}
}
/**
* Run all scheduled events
*
* @throws Throwable
*/
protected static function runEvents(): void {
if (self::$runEventsFunction) {
(self::$runEventsFunction)();
return;
}
// Run only events queued before this call (prevent infinite loops)
$runUntil = self::$queueLastIndex;
while (self::$queueFirstIndex < $runUntil) {
$listener = self::$queuedListeners[self::$queueFirstIndex];
$args = self::$queuedArgs[self::$queueFirstIndex];
$source = self::$queuedDispatchers[self::$queueFirstIndex];
unset(
self::$queuedListeners[self::$queueFirstIndex],
self::$queuedArgs[self::$queueFirstIndex],
self::$queuedDispatchers[self::$queueFirstIndex]
);
++self::$queueFirstIndex;
try {
$listener(...$args);
} catch (Throwable $exception) {
self::handleException($exception, $listener, $source);
}
}
}
/**
* Remove values from multiple arrays by reference
*
* @param array $values
* @param array $arrays
*/
protected function filterArrays(array $values, array &...$arrays): void {
foreach ($arrays as &$array) {
foreach ($values as $value) {
foreach ($array as $k => $v) {
if ($v === $value) {
unset($array[$k]);
}
}
}
}
}
/**
* Invoke a listener with exception handling
*
* @param Dispatcher $dispatcher
* @param Closure $listener
* @param array $args
* @return mixed
*/
protected static function invoke(Dispatcher $dispatcher, Closure $listener, array $args): mixed {
try {
return $listener(...$args);
} catch (\Throwable $e) {
self::handleException($e, $listener, $dispatcher);
return null;
}
}
}