PerItemTriggers.php
PHP
Path: src/Hooks/PerItemTriggers.php
<?php
namespace mini\Hooks;
use Closure;
use LogicException;
use WeakMap;
/**
* Event dispatcher that triggers once per source (string or object)
* After triggering for a source, new subscribers are called immediately
*
* @template TSource of string|object The source identifier type
* @template TPayload The payload type passed to listeners
* @package mini\Hooks
*/
class PerItemTriggers extends Dispatcher {
/**
* String sources that triggered
*
* @var array<string, list<mixed>>
*/
protected array $triggeredStrings = [];
/**
* Object sources that triggered
*
* @var WeakMap<object, list<mixed>>
*/
protected readonly WeakMap $triggeredObjects;
/**
* Listeners on string sources
*
* @var array<string, list<callable(TSource, TPayload, mixed...): void>>
*/
protected array $stringListeners = [];
/**
* Listeners on object sources
*
* @var WeakMap<object, list<callable(TSource, TPayload, mixed...): void>>
*/
protected readonly WeakMap $objectListeners;
/**
* Listeners on all events
*
* @var list<callable(TSource, TPayload, mixed...): void>
*/
protected array $listeners = [];
public function __construct(?string $description = null) {
parent::__construct($description);
$this->triggeredObjects = new WeakMap();
$this->objectListeners = new WeakMap();
}
/**
* Was this event triggered for a specific source?
*
* @param TSource $source
* @return bool
*/
public function wasTriggeredFor(string|object $source): bool {
if (\is_string($source)) {
return \array_key_exists($source, $this->triggeredStrings);
} else {
return isset($this->triggeredObjects[$source]);
}
}
/**
* Trigger event for a specific source
*
* @param TSource $source
* @param TPayload $payload
* @param mixed ...$data Additional arguments
* @throws LogicException If already triggered for this source
*/
public function triggerFor(string|object $source, mixed ...$data): void {
if ($this->wasTriggeredFor($source)) {
throw new LogicException("Event already triggered for this source");
}
// Record trigger
if (\is_string($source)) {
$this->triggeredStrings[$source] = $data;
} else {
$this->triggeredObjects[$source] = $data;
}
// Invoke global listeners
$this->invokeAll($this->listeners, $source, ...$data);
// Invoke source-specific listeners
if (\is_string($source)) {
if (empty($this->stringListeners[$source])) {
return;
}
$listeners = $this->stringListeners[$source];
unset($this->stringListeners[$source]);
$this->invokeAll($listeners, $source, ...$data);
} else {
if (!isset($this->objectListeners[$source])) {
return;
}
$listeners = $this->objectListeners[$source];
unset($this->objectListeners[$source]);
$this->invokeAll($listeners, $source, ...$data);
}
}
/**
* Subscribe to all events (receives source as first arg)
*
* @param callable(TSource, TPayload, mixed...): void ...$listeners
*/
public function listen(Closure ...$listeners): void {
foreach ($listeners as $listener) {
$this->listeners[] = $listener;
}
}
/**
* Subscribe to a specific source
* If already triggered, listener is called immediately
*
* @param TSource $source
* @param callable(TSource, TPayload, mixed...): void ...$listeners
*/
public function listenFor(string|object $source, Closure ...$listeners): void {
if (\is_object($source)) {
if (isset($this->triggeredObjects[$source])) {
// Already triggered - invoke immediately
$this->invokeAll($listeners, $source, ...$this->triggeredObjects[$source]);
} else {
// Add listener
if (!isset($this->objectListeners[$source])) {
$this->objectListeners[$source] = $listeners;
} else {
$this->objectListeners[$source] = [...$this->objectListeners[$source], ...$listeners];
}
}
} else {
if (\array_key_exists($source, $this->triggeredStrings)) {
// Already triggered - invoke immediately
$this->invokeAll($listeners, $source, ...$this->triggeredStrings[$source]);
} else {
// Add listener
$this->stringListeners[$source] = [...($this->stringListeners[$source] ?? []), ...$listeners];
}
}
}
/**
* Unsubscribe from event
*
* @param callable(TSource, TPayload, mixed...): void ...$listeners
*/
public function off(Closure ...$listeners): void {
self::filterArrays($listeners, $this->listeners);
foreach ($this->stringListeners as $source => $array) {
self::filterArrays($listeners, $array);
if ($array === []) {
unset($this->stringListeners[$source]);
} else {
$this->stringListeners[$source] = $array;
}
}
foreach ($this->objectListeners as $object => $array) {
self::filterArrays($listeners, $array);
if ($array === []) {
unset($this->objectListeners[$object]);
} else {
$this->objectListeners[$object] = $array;
}
}
}
}