ListenerCollection   A
last analyzed

Complexity

Total Complexity 10

Size/Duplication

Total Lines 85
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 10
eloc 25
c 4
b 0
f 0
dl 0
loc 85
ccs 26
cts 26
cp 1
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
A getForEvents() 0 5 3
A getParameterType() 0 26 4
A add() 0 12 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\EventDispatcher\Provider;
6
7
use Closure;
8
use InvalidArgumentException;
9
use ReflectionFunction;
10
use ReflectionNamedType;
11
use ReflectionUnionType;
12
13
/**
14
 * Listener collection stores listeners and is used to configure provider.
15
 *
16
 * @see Provider
17
 */
18
final class ListenerCollection
19
{
20
    /**
21
     * @var callable[][]
22
     */
23
    private array $listeners = [];
24
25
    /**
26
     * Get listeners for event class names specified.
27
     *
28
     * @param string ...$eventClassNames Event class names.
29
     *
30
     * @return iterable<callable> Listeners.
31
     */
32 9
    public function getForEvents(string ...$eventClassNames): iterable
33
    {
34 9
        foreach ($eventClassNames as $eventClassName) {
35 9
            if (isset($this->listeners[$eventClassName])) {
36 9
                yield from $this->listeners[$eventClassName];
37
            }
38
        }
39
    }
40
41
    /**
42
     * Attaches listener to corresponding event based on the type-hint used for the event argument.
43
     *
44
     * Method signature should be the following:
45
     *
46
     * ```
47
     *  function (MyEvent $event): void
48
     * ```
49
     *
50
     * Any callable could be used be it a closure, invokable object or array referencing a class or object.
51
     *
52
     * @throws InvalidArgumentException If callable is invalid.
53
     */
54 12
    public function add(callable $listener, string ...$eventClassNames): self
55
    {
56 12
        $new = clone $this;
57
58 12
        if ($eventClassNames === []) {
59 10
            $eventClassNames = $this->getParameterType($listener);
60
        }
61
62 10
        foreach ($eventClassNames as $eventClassName) {
63 10
            $new->listeners[$eventClassName][] = $listener;
64
        }
65 10
        return $new;
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
     *
73
     * @throws InvalidArgumentException If callable is invalid.
74
     *
75
     * @return string[] Interfaces the parameter is type hinted on.
76
     */
77 10
    private function getParameterType(callable $callable): array
78
    {
79 10
        $closure = new ReflectionFunction(Closure::fromCallable($callable));
80 10
        $params = $closure->getParameters();
81
82 10
        if (isset($params[0])) {
83 9
            $reflectedType = $params[0]->getType();
84
        } else {
85 1
            throw new InvalidArgumentException('Listeners must accept an event object.');
86
        }
87
88 9
        if ($reflectedType instanceof ReflectionNamedType) {
89 7
            return [$reflectedType->getName()];
90
        }
91
92
        /** @psalm-suppress UndefinedClass,TypeDoesNotContainType */
93 2
        if ($reflectedType instanceof ReflectionUnionType) {
94
            /** @var ReflectionNamedType[] */
95 1
            $types = $reflectedType->getTypes();
96 1
            return array_map(
97 1
                static fn (ReflectionNamedType $type) => $type->getName(),
98 1
                $types
99 1
            );
100
        }
101
102 1
        throw new InvalidArgumentException('Listeners must declare an object type they can accept.');
103
    }
104
}
105