Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
EventSubscriberListenerProvider
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
4 / 4
22
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getListenersForEvent
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 addSubscriber
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
14
 addListener
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of php-fast-forward/event-dispatcher.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2025-2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/event-dispatcher
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\EventDispatcher\ListenerProvider;
20
21use SplPriorityQueue;
22use FastForward\EventDispatcher\Event\NamedEvent;
23use FastForward\EventDispatcher\Exception\InvalidArgumentException;
24use Psr\EventDispatcher\ListenerProviderInterface;
25use Symfony\Component\EventDispatcher\EventSubscriberInterface;
26
27/**
28 * Adapt Symfony event subscribers to a PSR-14 listener provider.
29 *
30 * Subscribers are indexed by event name and yielded in descending priority order.
31 */
32 class EventSubscriberListenerProvider implements ListenerProviderInterface
33{
34    /**
35     * Registered subscribers indexed by event name.
36     *
37     * @var array<string, SplPriorityQueue>
38     */
39    private array $subscribedEvents = [];
40
41    /**
42     * Register the initial event subscribers.
43     *
44     * @param EventSubscriberInterface|string ...$eventSubscribers Subscriber instances or subscriber class names.
45     */
46    public function __construct(EventSubscriberInterface|string ...$eventSubscribers)
47    {
48        foreach ($eventSubscribers as $eventSubscriber) {
49            $this->addSubscriber($eventSubscriber);
50        }
51    }
52
53    /**
54     * Yield listeners for the provided event.
55     *
56     * @param object $event event instance used for listener lookup
57     *
58     * @return iterable<callable(object): void> listeners that accept the resolved event instance
59     */
60    public function getListenersForEvent(object $event): iterable
61    {
62        $eventName = $event instanceof NamedEvent ? $event->getName() : $event::class;
63
64        if (! isset($this->subscribedEvents[$eventName])) {
65            return [];
66        }
67
68        foreach ($this->subscribedEvents[$eventName] as [$eventSubscriber, $method]) {
69            yield static function (object $event) use ($eventSubscriber, $method): void {
70                $eventSubscriber->{$method}($event instanceof NamedEvent ? $event->getEvent() : $event);
71            };
72        }
73    }
74
75    /**
76     * Register a subscriber and index its declared listeners.
77     *
78     * @param EventSubscriberInterface|string $eventSubscriber subscriber instance or subscriber class name
79     *
80     * @throws InvalidArgumentException thrown when the provided class name is not a Symfony event subscriber
81     */
82    public function addSubscriber(EventSubscriberInterface|string $eventSubscriber): void
83    {
84        if (\is_string($eventSubscriber) && ! is_subclass_of($eventSubscriber, EventSubscriberInterface::class)) {
85            throw InvalidArgumentException::forInvalidEventSubscriber(
86                $eventSubscriber,
87                EventSubscriberInterface::class,
88            );
89        }
90
91        $subscribedEvents = \call_user_func([$eventSubscriber, 'getSubscribedEvents']);
92
93        foreach ($subscribedEvents as $eventName => $method) {
94            if (\is_string($method)) {
95                $this->addListener($eventSubscriber, $eventName, $method);
96
97                continue;
98            }
99
100            if (\is_array($method) && isset($method[0]) && \is_string($method[0])) {
101                $this->addListener($eventSubscriber, $eventName, $method[0], $method[1] ?? 0);
102
103                continue;
104            }
105
106            if (\is_array($method) && \is_array($method[0])) {
107                foreach ($method as $item) {
108                    if (\is_array($item) && isset($item[0]) && \is_string($item[0])) {
109                        $this->addListener($eventSubscriber, $eventName, $item[0], $item[1] ?? 0);
110                    }
111                }
112            }
113        }
114    }
115
116    /**
117     * Attach one subscriber method to the given event name.
118     *
119     * @param EventSubscriberInterface $eventSubscriber subscriber instance
120     * @param string $eventName event class name or string identifier
121     * @param string $method subscriber method to invoke
122     * @param int $priority Listener priority. Higher values are yielded first.
123     */
124    private function addListener(
125        EventSubscriberInterface $eventSubscriber,
126        string $eventName,
127        string $method,
128        int $priority = 0,
129    ): void {
130        $this->subscribedEvents[$eventName] ??= new SplPriorityQueue();
131        $this->subscribedEvents[$eventName]->insert([$eventSubscriber, $method], $priority);
132    }
133}