1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core;
4:
5: use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface;
6: use Psr\EventDispatcher\ListenerProviderInterface as PsrListenerProviderInterface;
7: use Psr\EventDispatcher\StoppableEventInterface as PsrStoppableEventInterface;
8: use Salient\Contract\Core\HasName;
9: use Salient\Contract\Core\Instantiable;
10: use Salient\Utility\Reflect;
11: use Salient\Utility\Str;
12: use LogicException;
13:
14: /**
15: * Dispatches events to listeners
16: *
17: * Implements PSR-14 (Event Dispatcher) interfaces.
18: *
19: * @api
20: */
21: final class EventDispatcher implements
22: PsrEventDispatcherInterface,
23: PsrListenerProviderInterface,
24: Instantiable
25: {
26: /**
27: * Listener ID => list of events
28: *
29: * @var array<int,string[]>
30: */
31: private array $Listeners = [];
32:
33: /**
34: * Event => [ listener ID => listener ]
35: *
36: * @var array<string,array<int,callable>>
37: */
38: private array $EventListeners = [];
39:
40: private int $NextListenerId = 0;
41: private PsrListenerProviderInterface $ListenerProvider;
42:
43: /**
44: * Creates a new EventDispatcher object
45: *
46: * If a listener provider is given, calls to methods other than
47: * {@see EventDispatcher::dispatch()} will fail with a
48: * {@see LogicException}.
49: */
50: public function __construct(?PsrListenerProviderInterface $listenerProvider = null)
51: {
52: $this->ListenerProvider = $listenerProvider ?? $this;
53: }
54:
55: /**
56: * Register an event listener with the dispatcher
57: *
58: * Returns a listener ID that can be passed to
59: * {@see EventDispatcher::removeListener()}.
60: *
61: * @template TEvent of object
62: *
63: * @param callable(TEvent): mixed $listener
64: * @param string[]|string|null $event An event or array of events. If
65: * `null`, the listener is registered to receive events accepted by its
66: * first parameter.
67: */
68: public function listen(callable $listener, $event = null): int
69: {
70: $this->assertIsListenerProvider();
71:
72: if ($event === null) {
73: $event = [];
74: foreach (Reflect::getAcceptedTypes($listener, true) as $name) {
75: if (is_string($name)) {
76: $event[] = $name;
77: }
78: }
79: }
80:
81: if ($event === []) {
82: throw new LogicException('At least one event must be given');
83: }
84:
85: $id = $this->NextListenerId++;
86: foreach ((array) $event as $event) {
87: $event = Str::lower($event);
88: $this->Listeners[$id][] = $event;
89: $this->EventListeners[$event][$id] = $listener;
90: }
91:
92: return $id;
93: }
94:
95: /**
96: * Dispatch an event to listeners registered to receive it
97: *
98: * @template TEvent of object
99: *
100: * @param TEvent $event
101: * @return TEvent
102: */
103: public function dispatch(object $event): object
104: {
105: $listeners = $this->ListenerProvider->getListenersForEvent($event);
106:
107: foreach ($listeners as $listener) {
108: if (
109: $event instanceof PsrStoppableEventInterface
110: && $event->isPropagationStopped()
111: ) {
112: break;
113: }
114:
115: $listener($event);
116: }
117:
118: return $event;
119: }
120:
121: /**
122: * @template TEvent of object
123: *
124: * @param TEvent $event
125: * @return array<callable(TEvent): mixed>
126: */
127: public function getListenersForEvent(object $event): array
128: {
129: $this->assertIsListenerProvider();
130:
131: $events = array_merge(
132: [get_class($event)],
133: class_parents($event),
134: class_implements($event),
135: );
136:
137: if ($event instanceof HasName) {
138: $eventName = $event->getName();
139: // If the event returns a name we already have, do nothing
140: if (!is_a($event, $eventName)) {
141: $events[] = $eventName;
142: }
143: }
144:
145: $listenersByEvent = array_intersect_key(
146: $this->EventListeners,
147: array_change_key_case(array_fill_keys($events, null)),
148: );
149:
150: $listeners = [];
151: foreach ($listenersByEvent as $eventListeners) {
152: $listeners += $eventListeners;
153: }
154:
155: return array_values($listeners);
156: }
157:
158: /**
159: * Remove an event listener from the dispatcher
160: *
161: * @param int $id A listener ID returned by
162: * {@see EventDispatcher::listen()}.
163: */
164: public function removeListener(int $id): void
165: {
166: $this->assertIsListenerProvider();
167:
168: if (!array_key_exists($id, $this->Listeners)) {
169: throw new LogicException('No listener with that ID');
170: }
171:
172: foreach ($this->Listeners[$id] as $event) {
173: unset($this->EventListeners[$event][$id]);
174: if (!$this->EventListeners[$event]) {
175: unset($this->EventListeners[$event]);
176: }
177: }
178:
179: unset($this->Listeners[$id]);
180: }
181:
182: private function assertIsListenerProvider(): void
183: {
184: if ($this->ListenerProvider !== $this) {
185: throw new LogicException('Not a listener provider');
186: }
187: }
188: }
189: