Passed
Pull Request — master (#31)
by Alexander
02:20
created

ListenerCollection::isClassCallable()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

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