Completed
Push — master ( b9492e...31cd44 )
by Alexander
11:15
created

Provider::getParameterType()   B

Complexity

Conditions 8
Paths 35

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
nc 35
nop 1
dl 0
loc 37
rs 8.0835
c 0
b 0
f 0
1
<?php
2
namespace Yiisoft\EventDispatcher\Provider;
3
4
use Psr\EventDispatcher\ListenerProviderInterface;
5
6
/**
7
 * Provider is a listener provider that registers event listeners for interfaces used in callable type-hints
8
 * and gives out a list of handlers by event interface provided for further use with Dispatcher.
9
 */
10
final class Provider implements ListenerProviderInterface
11
{
12
    private $listeners = [];
13
14
    public function getListenersForEvent(object $event): iterable
15
    {
16
        $className = get_class($event);
17
        if (isset($this->listeners[$className])) {
18
            yield from $this->listeners[$className];
19
        }
20
21
        foreach (class_parents($event) as $parent) {
22
            if (isset($this->listeners[$parent])) {
23
                yield from $this->listeners[$parent];
24
            }
25
        }
26
27
        foreach (class_implements($event) as $interface) {
28
            if (isset($this->listeners[$interface])) {
29
                yield from $this->listeners[$interface];
30
            }
31
        }
32
    }
33
34
    /**
35
     * Attaches listener to corresponding event based on the type-hint used for the event argument.
36
     *
37
     * Method signature should be the following:
38
     *
39
     * ```
40
     *  function (MyEvent $event): void
41
     * ```
42
     *
43
     * Any callable could be used be it a closure, invokable object or array referencing a class or object.
44
     *
45
     * @param callable $listener
46
     */
47
    public function attach(callable $listener): void
48
    {
49
        $this->listeners[$this->getParameterType($listener)][] = $listener;
50
    }
51
52
    /**
53
     * Detach all event handlers registered for an interface
54
     *
55
     * @param string $interface
56
     */
57
    public function detach(string $interface): void
58
    {
59
        unset($this->listeners[$interface]);
60
    }
61
62
    /**
63
     * Derives the interface type of the first argument of a callable.
64
     *
65
     * @param callable $callable The callable for which we want the parameter type.
66
     * @return string The interface the parameter is type hinted on.
67
     */
68
    private function getParameterType(callable $callable): string
69
    {
70
        // This try-catch is only here to keep OCD linters happy about uncaught reflection exceptions.
71
        try {
72
            switch (true) {
73
                // See note on isClassCallable() for why this must be the first case.
74
                case $this->isClassCallable($callable):
75
                    $reflect = new \ReflectionClass($callable[0]);
76
                    $params = $reflect->getMethod($callable[1])->getParameters();
77
                    break;
78
                case $this->isFunctionCallable($callable):
79
                case $this->isClosureCallable($callable):
80
                    $reflect = new \ReflectionFunction($callable);
81
                    $params = $reflect->getParameters();
82
                    break;
83
                case $this->isObjectCallable($callable):
84
                    $reflect = new \ReflectionObject($callable[0]);
85
                    $params = $reflect->getMethod($callable[1])->getParameters();
86
                    break;
87
                case $this->isInvokable($callable):
88
                    $params = (new \ReflectionMethod($callable, '__invoke'))->getParameters();
89
                    break;
90
                default:
91
                    throw new \InvalidArgumentException('Not a recognized type of callable');
92
            }
93
94
            $reflectedType = $params[0]->getType();
95
            if ($reflectedType === null) {
96
                throw new \InvalidArgumentException('Listeners must declare an object type they can accept.');
97
            }
98
            $type = $reflectedType->getName();
99
        } catch (\ReflectionException $e) {
100
            throw new \RuntimeException('Type error registering listener.', 0, $e);
101
        }
102
103
        return $type;
104
    }
105
106
    /**
107
     * Determines if a callable represents a function.
108
     *
109
     * Or at least a reasonable approximation, since a function name may not be defined yet.
110
     *
111
     * @param callable $callable
112
     * @return True if the callable represents a function, false otherwise.
113
     */
114
    private function isFunctionCallable(callable $callable): bool
115
    {
116
        // We can't check for function_exists() because it may be included later by the time it matters.
117
        return is_string($callable);
118
    }
119
120
    /**
121
     * Determines if a callable represents a closure/anonymous function.
122
     *
123
     * @param callable $callable
124
     * @return True if the callable represents a closure object, false otherwise.
125
     */
126
    private function isClosureCallable(callable $callable): bool
127
    {
128
        return $callable instanceof \Closure;
129
    }
130
131
    /**
132
     * @param callable $callable
133
     * @return True if the callable represents an invokable object, false otherwise.
134
     */
135
    private function isInvokable(callable $callable): bool
136
    {
137
        return is_object($callable);
138
    }
139
140
    /**
141
     * Determines if a callable represents a method on an object.
142
     *
143
     * @param callable $callable
144
     * @return True if the callable represents a method object, false otherwise.
145
     */
146
    private function isObjectCallable(callable $callable): bool
147
    {
148
        return is_array($callable) && is_object($callable[0]);
149
    }
150
151
    /**
152
     * Determines if a callable represents a static class method.
153
     *
154
     * The parameter here is untyped so that this method may be called with an
155
     * array that represents a class name and a non-static method.  The routine
156
     * to determine the parameter type is identical to a static method, but such
157
     * an array is still not technically callable.  Omitting the parameter type here
158
     * allows us to use this method to handle both cases.
159
     *
160
     * Note that this method must therefore be the first in the switch statement
161
     * above, or else subsequent calls will break as the array is not going to satisfy
162
     * the callable type hint but it would pass `is_callable()`.  Because PHP.
163
     *
164
     * @param callable $callable
165
     * @return True if the callable represents a static method, false otherwise.
166
     */
167
    private function isClassCallable($callable): bool
168
    {
169
        return is_array($callable) && is_string($callable[0]) && class_exists($callable[0]);
170
    }
171
}
172