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