Completed
Push — master ( bb497c...039325 )
by Alexander
10:30
created

Provider::isInvokable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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