Passed
Pull Request — master (#20)
by Dmitriy
01:23
created

Provider::detach()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Yiisoft\EventDispatcher\Provider;
4
5
use Psr\EventDispatcher\ListenerProviderInterface;
6
7
/**
8
 * Provider is a listener provider that registers event listeners for interfaces used in callable type-hints
9
 * and gives out a list of handlers by event interface provided for further use with Dispatcher.
10
 *
11
 * ```php
12
 * $provider = new Yiisoft\EventDispatcher\Provider\Provider();
13
 *
14
 * // adding some listeners
15
 * $provider->attach(function (AfterDocumentProcessed $event) {
16
 *    $document = $event->getDocument();
17
 *    // do something with document
18
 * });
19
 * ```
20
 */
21
final class Provider extends AbstractProviderConfigurator implements ListenerProviderInterface
22
{
23
    /**
24
     * @var callable[]
25
     */
26
    private array $listeners = [];
27
28
    public function getListenersForEvent(object $event): iterable
29
    {
30
        yield from $this->listenersFor(get_class($event));
31
        yield from $this->listenersFor(...array_values(class_parents($event)));
32
        yield from $this->listenersFor(...array_values(class_implements($event)));
33
    }
34
35
    /**
36
     * Attaches listener to corresponding event based on the type-hint used for the event argument.
37
     *
38
     * Method signature should be the following:
39
     *
40
     * ```
41
     *  function (MyEvent $event): void
42
     * ```
43
     *
44
     * Any callable could be used be it a closure, invokable object or array referencing a class or object.
45
     *
46
     * @param callable $listener
47
     * @param string $eventClassName
48
     */
49
    protected function attach(callable $listener, string $eventClassName = ''): void
50
    {
51
        if ($eventClassName === '') {
52
            $eventClassName = $this->getParameterType($listener);
53
        }
54
55
        $this->listeners[$eventClassName][] = $listener;
56
    }
57
58
    /**
59
     * Detach all event handlers registered for an event
60
     *
61
     * @param string $eventClassName
62
     */
63
    public function detach(string $eventClassName): void
64
    {
65
        unset($this->listeners[$eventClassName]);
66
    }
67
68
    /**
69
     * Derives the interface type of the first argument of a callable.
70
     *
71
     * @param callable $callable The callable for which we want the parameter type.
72
     * @return string The interface the parameter is type hinted on.
73
     */
74
    private function getParameterType(callable $callable): string
75
    {
76
        // This try-catch is only here to keep OCD linters happy about uncaught reflection exceptions.
77
        try {
78
            switch (true) {
79
                // See note on isClassCallable() for why this must be the first case.
80
                case $this->isClassCallable($callable):
0 ignored issues
show
Unused Code introduced by
$this->isClassCallable($callable) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
81
                    $reflect = new \ReflectionClass($callable[0]);
82
                    $params = $reflect->getMethod($callable[1])->getParameters();
83
                    break;
84
                case $this->isFunctionCallable($callable):
85
                case $this->isClosureCallable($callable):
86
                    $reflect = new \ReflectionFunction($callable);
87
                    $params = $reflect->getParameters();
88
                    break;
89
                case $this->isObjectCallable($callable):
90
                    $reflect = new \ReflectionObject($callable[0]);
91
                    $params = $reflect->getMethod($callable[1])->getParameters();
92
                    break;
93
                case $this->isInvokable($callable):
94
                    $params = (new \ReflectionMethod($callable, '__invoke'))->getParameters();
95
                    break;
96
                default:
97
                    throw new \InvalidArgumentException('Not a recognized type of callable');
98
            }
99
100
            $reflectedType = isset($params[0]) ? $params[0]->getType() : null;
101
            if ($reflectedType === null) {
102
                throw new \InvalidArgumentException('Listeners must declare an object type they can accept.');
103
            }
104
            $type = $reflectedType->getName();
105
        } catch (\ReflectionException $e) {
106
            throw new \RuntimeException('Type error registering listener.', 0, $e);
107
        }
108
109
        return $type;
110
    }
111
112
    /**
113
     * Determines if a callable represents a function.
114
     *
115
     * Or at least a reasonable approximation, since a function name may not be defined yet.
116
     *
117
     * @param callable $callable
118
     * @return True if the callable represents a function, false otherwise.
119
     */
120
    private function isFunctionCallable(callable $callable): bool
121
    {
122
        // We can't check for function_exists() because it may be included later by the time it matters.
123
        return is_string($callable);
0 ignored issues
show
Bug Best Practice introduced by
The expression return is_string($callable) returns the type boolean which is incompatible with the documented return type true.
Loading history...
124
    }
125
126
    /**
127
     * Determines if a callable represents a closure/anonymous function.
128
     *
129
     * @param callable $callable
130
     * @return True if the callable represents a closure object, false otherwise.
131
     */
132
    private function isClosureCallable(callable $callable): bool
133
    {
134
        return $callable instanceof \Closure;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $callable instanceof Closure returns the type boolean which is incompatible with the documented return type true.
Loading history...
135
    }
136
137
    /**
138
     * @param callable $callable
139
     * @return True if the callable represents an invokable object, false otherwise.
140
     */
141
    private function isInvokable(callable $callable): bool
142
    {
143
        return is_object($callable);
0 ignored issues
show
Bug Best Practice introduced by
The expression return is_object($callable) returns the type boolean which is incompatible with the documented return type true.
Loading history...
144
    }
145
146
    /**
147
     * Determines if a callable represents a method on an object.
148
     *
149
     * @param callable $callable
150
     * @return True if the callable represents a method object, false otherwise.
151
     */
152
    private function isObjectCallable(callable $callable): bool
153
    {
154
        return is_array($callable) && is_object($callable[0]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return is_array($callabl...is_object($callable[0]) returns the type boolean which is incompatible with the documented return type true.
Loading history...
155
    }
156
157
    /**
158
     * Determines if a callable represents a static class method.
159
     *
160
     * The parameter here is untyped so that this method may be called with an
161
     * array that represents a class name and a non-static method.  The routine
162
     * to determine the parameter type is identical to a static method, but such
163
     * an array is still not technically callable.  Omitting the parameter type here
164
     * allows us to use this method to handle both cases.
165
     *
166
     * Note that this method must therefore be the first in the switch statement
167
     * above, or else subsequent calls will break as the array is not going to satisfy
168
     * the callable type hint but it would pass `is_callable()`.  Because PHP.
169
     *
170
     * @param callable $callable
171
     * @return True if the callable represents a static method, false otherwise.
172
     */
173
    private function isClassCallable($callable): bool
174
    {
175
        return is_array($callable) && is_string($callable[0]) && class_exists($callable[0]);
0 ignored issues
show
Bug Best Practice introduced by
The expression return is_array($callabl...ss_exists($callable[0]) returns the type boolean which is incompatible with the documented return type true.
Loading history...
176
    }
177
178
    /**
179
     * @param string ...$eventClassNames
180
     * @return iterable<callable>
181
     */
182
    private function listenersFor(string ...$eventClassNames): iterable
183
    {
184
        foreach ($eventClassNames as $eventClassName) {
185
            if (isset($this->listeners[$eventClassName])) {
186
                yield from $this->listeners[$eventClassName];
187
            }
188
        }
189
    }
190
}
191