Lines 100.00% 18 / 18
Methods 100.00% 3 / 3
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 listen 100.00% 5 / 5 100.00% 1 / 1 1
 getListenersForEvent 100.00% 8 / 8 100.00% 1 / 1 3
 classesAndInterfaces 100.00% 5 / 5 100.00% 1 / 1 2
27class ListenerProvider implements ListenerProviderInterface
28{
29    /**
30     * Registered listeners keyed by event class name.
31     *
32     * Each entry holds the listener (a callable or a class-string resolved
33     * through the container at dispatch time), its priority, and its
34     * registration order for stable sorting.
35     *
36     * @var array<string, list<array{listener: callable|string, priority: int, order: int}>>
37     */
38    private array $listeners = [];
39
40    /**
41     * Monotonic counter used to keep registration order stable.
42     *
43     * @var int
44     */
45    private int $order = 0;
46
47    /**
48     * Register a listener for an event class.
49     *
50     * The listener may be a callable (closure, invokable object, function
51     * name) or a class-string. Class-strings are resolved through the
52     * container when the event is dispatched, so their constructor may
53     * receive dependencies.
54     *
55     * @param class-string $eventClass Event class (or parent class / interface) to listen for
56     * @param callable|string $listener Callable, or class-string of an invokable listener
57     * @param int $priority Higher priorities run first; defaults to 0
58     * @return void
59     */
60    public function listen(string $eventClass, callable|string $listener, int $priority = 0): void
61    {
62        $this->listeners[$eventClass][] = [
63            'listener' => $listener,
64            'priority' => $priority,
65            'order' => $this->order++,
66        ];
67    }
68
69    /**
70     * Get the listeners applicable to an event.
71     *
72     * Collects listeners registered for the event's own class, every parent
73     * class, and every implemented interface, then sorts them by priority
74     * (descending) and registration order (ascending).
75     *
76     * @param object $event The event to find listeners for
77     * @return iterable<callable> Applicable listeners in invocation order
78     */
79    public function getListenersForEvent(object $event): iterable
80    {
81        $matches = [];
82
83        foreach ($this->classesAndInterfaces($event) as $class) {
84            foreach ($this->listeners[$class] ?? [] as $entry) {
85                $matches[] = $entry;
86            }
87        }
88
89        usort($matches, static function (array $a, array $b): int {
90            return [$b['priority'], $a['order']] <=> [$a['priority'], $b['order']];
91        });
92
93        return array_column($matches, 'listener');
94    }
95
96    /**
97     * Resolve the event's class hierarchy and implemented interfaces.
98     *
99     * @param object $event The event to inspect
100     * @return list<string> Class names, from the event's own class upward
101     */
102    private function classesAndInterfaces(object $event): array
103    {
104        $reflection = new ReflectionClass($event);
105
106        $classes = [];
107        for ($current = $reflection; $current; $current = $current->getParentClass()) {
108            $classes[] = $current->getName();
109        }
110
111        return array_merge($classes, $reflection->getInterfaceNames());
112    }
113}