Passed
Push — master ( 73f763...c93144 )
by Alexander
02:03
created

ListenerCollection::getParameterType()   A

Complexity

Conditions 4
Paths 9

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4.016

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 10
c 2
b 0
f 0
dl 0
loc 17
ccs 9
cts 10
cp 0.9
rs 9.9332
cc 4
nc 9
nop 1
crap 4.016
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\EventDispatcher\Provider;
6
7
/**
8
 * Listener collection stores listeners and is used to configure provider.
9
 *
10
 * @see Provider
11
 */
12
final class ListenerCollection
13
{
14
    /**
15
     * @var callable[]
16
     */
17
    private array $listeners = [];
18
19
    /**
20
     * @param string ...$eventClassNames
21
     * @return iterable<callable>
22
     */
23 7
    public function getForEvents(string ...$eventClassNames): iterable
24
    {
25 7
        foreach ($eventClassNames as $eventClassName) {
26 7
            if (isset($this->listeners[$eventClassName])) {
27 7
                yield from $this->listeners[$eventClassName];
28
            }
29
        }
30 7
    }
31
32
    /**
33
     * Attaches listener to corresponding event based on the type-hint used for the event argument.
34
     *
35
     * Method signature should be the following:
36
     *
37
     * ```
38
     *  function (MyEvent $event): void
39
     * ```
40
     *
41
     * Any callable could be used be it a closure, invokable object or array referencing a class or object.
42
     *
43
     * @param callable $listener
44
     * @param string $eventClassName
45
     * @return self
46
     */
47 8
    public function add(callable $listener, string $eventClassName = ''): self
48
    {
49 8
        $new = clone $this;
50
51 8
        if ($eventClassName === '') {
52 7
            $eventClassName = $this->getParameterType($listener);
53
        }
54
55 7
        $new->listeners[$eventClassName][] = $listener;
56 7
        return $new;
57
    }
58
59
    /**
60
     * Derives the interface type of the first argument of a callable.
61
     *
62
     * @suppress PhanUndeclaredMethod
63
     *
64
     * @param callable $callable The callable for which we want the parameter type.
65
     * @return string The interface the parameter is type hinted on.
66
     */
67 7
    private function getParameterType(callable $callable): string
68
    {
69
        // This try-catch is only here to keep OCD linters happy about uncaught reflection exceptions.
70
        try {
71 7
            $closure = new \ReflectionFunction(\Closure::fromCallable($callable));
72 7
            $params = $closure->getParameters();
73
74 7
            $reflectedType = isset($params[0]) ? $params[0]->getType() : null;
75 7
            if ($reflectedType === null) {
76 1
                throw new \InvalidArgumentException('Listeners must declare an object type they can accept.');
77
            }
78 6
            $type = $reflectedType->getName();
79 1
        } catch (\ReflectionException $e) {
80
            throw new \RuntimeException('Type error registering listener.', 0, $e);
81
        }
82
83 6
        return $type;
84
    }
85
86
    /**
87
     * Determines if a callable represents a function.
88
     *
89
     * Or at least a reasonable approximation, since a function name may not be defined yet.
90
     *
91
     * @param callable $callable
92
     * @return True if the callable represents a function, false otherwise.
93
     */
94
    private function isFunctionCallable(callable $callable): bool
0 ignored issues
show
Unused Code introduced by
The method isFunctionCallable() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
95
    {
96
        // We can't check for function_exists() because it may be included later by the time it matters.
97
        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...
98
    }
99
100
    /**
101
     * Determines if a callable represents a closure/anonymous function.
102
     *
103
     * @param callable $callable
104
     * @return True if the callable represents a closure object, false otherwise.
105
     */
106
    private function isClosureCallable(callable $callable): bool
0 ignored issues
show
Unused Code introduced by
The method isClosureCallable() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
107
    {
108
        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...
109
    }
110
111
    /**
112
     * @param callable $callable
113
     * @return True if the callable represents an invokable object, false otherwise.
114
     */
115
    private function isInvokable(callable $callable): bool
0 ignored issues
show
Unused Code introduced by
The method isInvokable() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
116
    {
117
        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...
118
    }
119
120
    /**
121
     * Determines if a callable represents a method on an object.
122
     *
123
     * @param callable $callable
124
     * @return True if the callable represents a method object, false otherwise.
125
     */
126
    private function isObjectCallable(callable $callable): bool
0 ignored issues
show
Unused Code introduced by
The method isObjectCallable() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
127
    {
128
        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...
129
    }
130
131
    /**
132
     * Determines if a callable represents a static class method.
133
     *
134
     * The parameter here is untyped so that this method may be called with an
135
     * array that represents a class name and a non-static method.  The routine
136
     * to determine the parameter type is identical to a static method, but such
137
     * an array is still not technically callable.  Omitting the parameter type here
138
     * allows us to use this method to handle both cases.
139
     *
140
     * Note that this method must therefore be the first in the switch statement
141
     * above, or else subsequent calls will break as the array is not going to satisfy
142
     * the callable type hint but it would pass `is_callable()`.  Because PHP.
143
     *
144
     * @param callable $callable
145
     * @return True if the callable represents a static method, false otherwise.
146
     */
147
    private function isClassCallable($callable): bool
0 ignored issues
show
Unused Code introduced by
The method isClassCallable() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
148
    {
149
        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...
150
    }
151
}
152