Passed
Push — master ( 151f28...0c78bb )
by Arnold
03:10
created

ListenerProvider::withListenerInNs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 12
ccs 7
cts 7
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Jasny\EventDispatcher;
6
7
use Closure;
8
use Improved\IteratorPipeline\Pipeline;
9
use Jasny\ReflectionFactory\ReflectionFactory;
10
use LogicException;
11
use Psr\EventDispatcher\ListenerProviderInterface;
12
use ReflectionException;
13
14
/**
15
 * Event dispatcher.
16
 * @immutable
17
 */
18
class ListenerProvider implements ListenerProviderInterface
19
{
20
    /**
21
     * @var ReflectionFactory
22
     */
23
    protected $reflectionFactory;
24
25
    /**
26
     * @var array
27
     */
28
    protected $listeners = [];
29
30
31
    /**
32
     * ListenerProvider constructor.
33
     *
34
     * @param ReflectionFactory|null $reflectionFactory
35
     */
36 15
    public function __construct(ReflectionFactory $reflectionFactory = null)
37
    {
38 15
        $this->reflectionFactory = $reflectionFactory ?? new ReflectionFactory();
39 15
    }
40
41
42
    /**
43
     * Bind a handler for an event.
44
     *
45
     * @param callable $listener
46
     * @return static
47
     * @throws LogicException if listener is invalid
48
     */
49 15
    public function withListener(callable $listener): self
50
    {
51 15
        return $this->withListenerInNs('', $listener);
52
    }
53
54
    /**
55
     * Bind a handler for an event.
56
     *
57
     * @param string   $ns
58
     * @param callable $listener
59
     * @return static
60
     * @throws LogicException if listener is invalid
61
     */
62 15
    public function withListenerInNs(string $ns, callable $listener): self
63
    {
64 15
        if (strpos($ns, '*') !== false) {
65 1
            throw new \InvalidArgumentException("Invalid event ns '$ns': illegal character '*'");
66
        }
67
68 15
        $class = $this->getEventClassForListener($listener);
69
70 15
        $clone = clone $this;
71 15
        $clone->listeners[] = ['ns' => $ns, 'class' => $class, 'listener' => $listener];
72
73 15
        return $clone;
74
    }
75
76
    /**
77
     * Use reflection to get the event class from the first argument
78
     *
79
     * @param callable $listener
80
     * @return string
81
     * @throws LogicException
82
     */
83 15
    protected function getEventClassForListener(callable $listener): string
84
    {
85 15
        if (!$listener instanceof Closure && is_object($listener)) {
86 1
            $listener = [$listener, '__invoke']; // Type hint means we're sure that the object is callable
87
        }
88
89
        try {
90 15
            $reflFn = is_array($listener)
91 2
                ? $this->reflectionFactory->reflectMethod($listener[0], $listener[1])
92 15
                : $this->reflectionFactory->reflectFunction($listener);
93 1
        } catch (ReflectionException $exception) {
94 1
            throw new LogicException("Invalid event listener: " . $exception->getMessage());
95
        }
96
97 15
        if ($reflFn->getNumberOfParameters() === 0) {
98 1
            throw new LogicException("Invalid event listener: No parameters defined");
99
        }
100
101 15
        [$reflParam] = $reflFn->getParameters();
102 15
        $class = $reflParam->getType();
103
104 15
        if ($class === null) {
105 1
            throw new LogicException(sprintf(
106 1
                'Invalid event listener: No type hint for parameter $%s',
107 1
                $reflParam->getName()
108
            ));
109
        }
110
111 15
        return $class->getName();
112
    }
113
114
    /**
115
     * Remove all listeners of the specified namespace.
116
     *
117
     * @param string $ns  Namespace, optionally with wildcards
118
     * @return static
119
     */
120 4
    public function withoutNs(string $ns): self
121
    {
122 4
        $listeners = Pipeline::with($this->listeners)
123
            ->filter(function ($trigger) use ($ns) {
124 4
                return !fnmatch($ns, $trigger['ns'], FNM_NOESCAPE)
125 4
                    && !fnmatch("$ns.*", $trigger['ns'], FNM_NOESCAPE);
126 4
            })
127 4
            ->values()
128 4
            ->toArray();
129
130 4
        if (count($listeners) === count($this->listeners)) {
131 1
            return $this;
132
        }
133
134 4
        $clone = clone $this;
135 4
        $clone->listeners = $listeners;
136
137 4
        return $clone;
138
    }
139
140
141
    /**
142
     * Get the relevant listeners for the given event.
143
     *
144
     * @param object $event
145
     * @return callable[]
146
     */
147 8
    public function getListenersForEvent(object $event): iterable
148
    {
149 8
        return Pipeline::with($this->listeners)
150
            ->filter(function ($trigger) use ($event) {
151 8
                return $this->reflectionFactory->isA($event, $trigger['class']);
152 8
            })
153 8
            ->column('listener')
154 8
            ->values()
155 8
            ->toArray();
156
    }
157
}
158