ListenerClassFile::resolveListeners()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 2
nop 0
dl 0
loc 21
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of slick/event package
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace Slick\Event\Application;
11
12
use Psr\Container\ContainerExceptionInterface;
13
use Psr\Container\ContainerInterface;
14
use Psr\Container\NotFoundExceptionInterface;
15
use ReflectionAttribute;
16
use ReflectionClass;
17
use ReflectionMethod;
18
use RuntimeException;
19
use Slick\Event\Application\Attribute\AsEventListener;
20
use Slick\Event\EventListener;
21
22
/**
23
 * ListenerClassFile
24
 *
25
 * @package Slick\Event\Application
26
 */
27
final class ListenerClassFile
28
{
29
    private readonly string $className;
30
    /** @var array<array{source: string, event: string, method: string, priority: int}> */
31
    private array $listenerBindings = [];
32
33
    public function __construct(
34
        private readonly string $filePath,
35
        private ?ContainerInterface $container = null
36
    ) {
37
        $this->analyze();
38
    }
39
40
    /**
41
     * Sets the dependency injection container
42
     *
43
     * @param ContainerInterface $container The container to be set
44
     * @return self Returns an instance of the class with the updated container
45
     */
46
    public function withContainer(ContainerInterface $container): self
47
    {
48
        $this->container = $container;
49
        return $this;
50
    }
51
52
    /**
53
     * Check if the object has any listener bindings.
54
     *
55
     * @return bool True if the object has listener bindings, false otherwise.
56
     */
57
    public function isListener(): bool
58
    {
59
        return !empty($this->listenerBindings);
60
    }
61
62
    /**
63
     * Returns the listener bindings of the Symfony application.
64
     *
65
     * @return array<array{source: string, event: string, method: string, priority: int}> The listener bindings array
66
     */
67
    public function bindings(): array
68
    {
69
        return $this->listenerBindings;
70
    }
71
72
    /**
73
     * Resolves the listeners for the Symfony application.
74
     *
75
     * @return array<array{event: string, listener: EventListener|CallableEventListener, priority: int}>
76
     *     The resolved listeners array
77
     * @throws ContainerExceptionInterface
78
     * @throws NotFoundExceptionInterface
79
     * @throws RuntimeException When the container is not available
80
     */
81
    public function resolveListeners(): array
82
    {
83
        if (!$this->container) {
84
            throw new RuntimeException("Container not available.");
85
        }
86
87
        $instance = $this->container->get($this->className);
88
89
        return array_map(function ($binding) use ($instance) {
90
            $method = $binding['method'];
91
92
            $listener = $instance instanceof EventListener
93
                ? $instance
94
                : new CallableEventListener(fn(object $event) => $instance->{$method}($event));
95
96
            return [
97
                'event' => $binding['event'],
98
                'listener' => $listener,
99
                'priority' => $binding['priority'],
100
            ];
101
        }, $this->listenerBindings);
102
    }
103
104
    public function className(): ?string
105
    {
106
        return $this->className;
107
    }
108
109
    public function filePath(): string
110
    {
111
        return $this->filePath;
112
    }
113
114
    public function __serialize(): array
115
    {
116
        return [
117
            'className' => $this->className,
118
            'listenerBindings' => $this->listenerBindings,
119
        ];
120
    }
121
122
    public function __unserialize(array $data): void
123
    {
124
        $this->listenerBindings = $data['listenerBindings'] ?? [];
125
        $this->className = $data['className'] ?? null;
0 ignored issues
show
Bug introduced by
The property className is declared read-only in Slick\Event\Application\ListenerClassFile.
Loading history...
126
    }
127
128
    private function analyze(): void
129
    {
130
        $className = $this->getClassFromFile($this->filePath);
131
        if (!$className || !class_exists($className)) {
132
            return;
133
        }
134
135
        $this->className = $className;
0 ignored issues
show
Bug introduced by
The property className is declared read-only in Slick\Event\Application\ListenerClassFile.
Loading history...
136
        $refClass = new ReflectionClass($className);
137
138
        // Class-level attributes
139
        $attributes = $refClass->getAttributes(AsEventListener::class, ReflectionAttribute::IS_INSTANCEOF);
140
        foreach ($attributes as $attr) {
141
            /** @var AsEventListener $instance */
142
            $instance = $attr->newInstance();
143
            $isInterface = $refClass->implementsInterface(EventListener::class);
144
            $this->listenerBindings[] = [
145
                'event' => $instance->event,
146
                'method' => $isInterface ? 'handle' : $instance->method ?? '__invoke',
147
                'priority' => $instance->priority ?? 0,
148
                'source' => $isInterface ? 'interface' : 'class'
149
            ];
150
        }
151
152
        // Method-level attributes
153
        foreach ($refClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
154
            foreach ($method->getAttributes(AsEventListener::class, ReflectionAttribute::IS_INSTANCEOF) as $attr) {
155
                /** @var AsEventListener $instance */
156
                $instance = $attr->newInstance();
157
                $this->listenerBindings[] = [
158
                    'event' => $instance->event,
159
                    'method' => $method->getName(),
160
                    'priority' => $instance->priority ?? 0,
161
                    'source' => 'method'
162
                ];
163
            }
164
165
            // EventListener interface
166
            if (count($attributes) <= 0 && $refClass->implementsInterface(EventListener::class)) {
167
                $this->listenerBindings[] = [
168
                    'event' => $className,
169
                    'method' => 'handle',
170
                    'priority' => 0,
171
                    'source' => 'interface'
172
                ];
173
            }
174
        }
175
    }
176
177
    /**
178
     * Retrieves the class name from a PHP file based on its namespace declaration.
179
     *
180
     * @param string $filePath The path to the PHP file
181
     * @return string|null The fully qualified class name or null if class is not found
182
     */
183
    private function getClassFromFile(string $filePath): ?string
184
    {
185
        $src = file_get_contents($filePath);
186
        $tokens = token_get_all($src);
187
188
        $namespace = '';
189
        $class = '';
190
191
        for ($i = 0, $count = count($tokens); $i < $count; $i++) {
192
            $token = $tokens[$i];
193
194
            // Namespace (T_NAME_QUALIFIED)
195
            if (is_array($token) && $token[0] === T_NAMESPACE) {
196
                $i++;
197
                while (isset($tokens[$i]) && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
198
                    $i++;
199
                }
200
                if (isset($tokens[$i]) && is_array($tokens[$i]) && $tokens[$i][0] === T_NAME_QUALIFIED) {
201
                    $namespace = $tokens[$i][1];
202
                }
203
            }
204
205
            // Class name (after T_CLASS)
206
            if (is_array($token) && $token[0] === T_CLASS) {
207
                $i++;
208
                while (isset($tokens[$i]) && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
209
                    $i++;
210
                }
211
                if (isset($tokens[$i]) && is_array($tokens[$i]) && $tokens[$i][0] === T_STRING) {
212
                    $class = $tokens[$i][1];
213
                    break;
214
                }
215
            }
216
        }
217
218
        return $class ? ($namespace ? $namespace . '\\' . $class : $class) : null;
219
    }
220
}
221