Issues (3)

src/AnnotationLoader.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Biurad opensource projects.
7
 *
8
 * PHP version 7.2 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Biurad\Annotations;
19
20
use Spiral\Attributes\ReaderInterface;
21
22
/**
23
 * This class allows loading of annotations/attributes using listeners.
24
 *
25
 * @author Divine Niiquaye Ibok <[email protected]>
26
 */
27
class AnnotationLoader
28
{
29
    /** @var ReaderInterface|null */
30
    private $reader;
31
32
    /** @var array<string,mixed> */
33
    private $loadedAttributes = [], $loadedListeners = [], $aliases = [];
34
35
    /** @var array<string,ListenerInterface> */
36
    private $listeners = [];
37
38
    /** @var string[] */
39
    private $resources = [];
40
41
    /** @var callable(string[]) */
42
    private $classLoader;
43
44
    /**
45
     * @param callable $classLoader
46
     */
47 7
    public function __construct(ReaderInterface $reader = null, callable $classLoader = null)
48
    {
49 7
        if (\PHP_VERSION_ID < 80000 && null === $reader) {
50
            throw new \RuntimeException(\sprintf('A "%s" instance to read annotations/attributes not available.', ReaderInterface::class));
51
        }
52
53 7
        $this->reader = $reader;
54 7
        $this->classLoader = $classLoader ?? [self::class, 'findClasses'];
55
    }
56
57
    /**
58
     * Attach a listener to the loader.
59
     */
60 5
    public function listener(ListenerInterface $listener, string $alias = null): void
61
    {
62 5
        $this->listeners[$name = \get_class($listener)] = $listener;
63 5
        unset($this->loadedListeners[$name]);
64
65 5
        if (null !== $alias) {
66 5
            $this->aliases[$alias] = $name;
67
        }
68
    }
69
70
    /**
71
     * Attache(s) the given resource(s) to the loader.
72
     *
73
     * @param string ...$resources type of class string, function name, file, or directory
74
     */
75 6
    public function resource(string ...$resources): void
76
    {
77 6
        foreach ($resources as $resource) {
78 6
            $this->resources[] = $resource;
79
        }
80
    }
81
82
    /**
83
     * Load annotations/attributes from the given resource(s).
84
     *
85
     * @param string ...$listener the name of class name for registered lister
86
     *                            annotation/attribute class name or listener's aliased name
87
     *
88
     * @return mixed
89
     */
90 7
    public function load(string ...$listener)
91
    {
92 7
        $loaded = [];
93
94 7
        foreach (($listener ?: $this->listeners) as $name => $value) {
95 7
            if (\is_int($name)) {
96 7
                $name = $this->aliases[$value] ?? $value;
97
98 7
                if (!isset($this->listeners[$name])) {
99 2
                    $loaded[$name] = $this->loadedAttributes[$name] ?? ($this->loadedAttributes[$name] = $this->build($name));
100 2
                    continue;
101
                }
102
103 5
                if (!isset($this->loadedListeners[$name])) {
104 2
                    $value = $this->listeners[$name];
105
                }
106
            }
107 5
            $loaded[$name] = $this->loadedListeners[$name] ?? $this->loadedListeners[$name] = $value->load($this->build(...$value->getAnnotations()));
108
        }
109
110 7
        return 1 === \count($loaded) ? \current($loaded) : $loaded;
111
    }
112
113
    /**
114
     * Builds the attributes/annotations for the given class or function.
115
     *
116
     * @return array<int,array<string, mixed>>
117
     */
118 7
    protected function build(string ...$annotationClass): array
119
    {
120 7
        $annotations = [];
121
122 7
        foreach ($this->resources as $resource) {
123 6
            $values = [];
124
125 6
            if (\is_dir($resource)) {
126 5
                $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($resource, \FilesystemIterator::CURRENT_AS_PATHNAME));
127 5
                $files = new \RegexIterator($iterator, '/\.php$/');
128
129 5
                foreach (($this->classLoader)($files) as $class) {
130 5
                    $classes = $this->fetchClassAnnotation($class, $annotationClass);
131
132 5
                    if (!empty($classes)) {
133 5
                        $annotations[] = $classes;
134
                    }
135
                }
136 6
            } elseif (\function_exists($resource)) {
137 3
                $values = $this->fetchFunctionAnnotation(new \ReflectionFunction($resource), $annotationClass);
138 3
            } elseif (\class_exists($resource)) {
139
                $values = $this->fetchClassAnnotation($resource, $annotationClass);
140
            }
141
142 6
            if (!empty($values)) {
143 3
                $annotations[] = $values;
144
            }
145
        }
146
147 7
        return $annotations;
148
    }
149
150
    /**
151
     * @param array<int,string>|string $annotation
152
     *
153
     * @return array<int,object>
154
     */
155 6
    private function getAnnotations(\Reflector $reflection, $annotation): array
156
    {
157 6
        $annotations = [];
158
159 6
        if (\is_array($annotation)) {
160 6
            foreach ($annotation as $annotationClass) {
161 6
                $annotations = \array_merge($annotations, $this->getAnnotations($reflection, $annotationClass));
162
            }
163
164 6
            return $annotations;
165
        }
166
167 6
        if (null === $this->reader) {
168 2
            return \array_map(static function (\ReflectionAttribute $attribute): object {
169 2
                return $attribute->newInstance();
170 2
            }, $reflection->getAttributes($annotation));
171
        }
172
173 6
        if ($reflection instanceof \ReflectionClass) {
174 5
            $annotations = $this->reader->getClassMetadata($reflection, $annotation);
175 6
        } elseif ($reflection instanceof \ReflectionFunctionAbstract) {
176 6
            $annotations = $this->reader->getFunctionMetadata($reflection, $annotation);
177 6
        } elseif ($reflection instanceof \ReflectionProperty) {
178 5
            $annotations = $this->reader->getPropertyMetadata($reflection, $annotation);
179 6
        } elseif ($reflection instanceof \ReflectionClassConstant) {
180 2
            $annotations = $this->reader->getConstantMetadata($reflection, $annotation);
181 6
        } elseif ($reflection instanceof \ReflectionParameter) {
182 6
            $annotations = $this->reader->getParameterMetadata($reflection, $annotation);
183
        }
184
185 6
        return $annotations instanceof \Traversable ? \iterator_to_array($annotations) : $annotations;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $annotations inst...tations) : $annotations could return the type iterable which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
186
    }
187
188
    /**
189
     * Finds annotations in the given resource.
190
     *
191
     * @param class-string|string $resource
192
     * @param array<int,string>   $annotationClass
193
     *
194
     * @return array<string,mixed>
195
     */
196 5
    private function fetchClassAnnotation(string $resource, array $annotationClass): array
197
    {
198 5
        $classReflection = new \ReflectionClass($resource);
199
200 5
        if ($classReflection->isAbstract()) {
201 3
            return [];
202
        }
203
204 5
        $classRefCount = 0;
205 5
        $constants = $properties = $methods = [];
206 5
        $reflections = \array_merge($classReflection->getMethods(), $classReflection->getProperties(), $classReflection->getConstants());
207
208 5
        foreach ($reflections as $name => $reflection) {
209 5
            if (\is_string($name)) {
210 2
                $reflection = new \ReflectionClassConstant($classReflection->name, $name);
211
            }
212
213 5
            if ($reflection instanceof \ReflectionMethod) {
214 5
                $method = $this->fetchFunctionAnnotation($reflection, $annotationClass);
215
216 5
                if (!empty($method)) {
217 5
                    $methods[] = $method;
218 5
                    ++$classRefCount;
219
                }
220 5
            } elseif (!empty($annotations = $this->getAnnotations($reflection, $annotationClass))) {
221 5
                if ($reflection instanceof \ReflectionProperty) {
222 5
                    $properties[] = ['attributes' => $annotations, 'type' => $reflection];
223 5
                    ++$classRefCount;
224 2
                } elseif ($reflection instanceof \ReflectionClassConstant) {
225 2
                    $constants[] = ['attributes' => $annotations, 'type' => $reflection];
226 2
                    ++$classRefCount;
227
                }
228
            }
229
        }
230
231 5
        if (empty($class = $this->getAnnotations($classReflection, $annotationClass)) && 0 === $classRefCount) {
232 3
            return [];
233
        }
234
235 5
        return ['attributes' => $class] + \compact('constants', 'properties', 'methods') + ['type' => $classReflection];
236
    }
237
238
    /**
239
     * @return array<string,mixed>
240
     */
241 6
    private function fetchFunctionAnnotation(\ReflectionFunctionAbstract $reflection, array $annotationClass)
242
    {
243 6
        $parameters = [];
244 6
        $annotations = $this->getAnnotations($reflection, $annotationClass);
245
246 6
        if (empty($annotations)) {
247 3
            return [];
248
        }
249
250 6
        foreach ($reflection->getParameters() as $parameter) {
251 6
            $attributes = $this->getAnnotations($parameter, $annotationClass);
252
253 6
            if (!empty($attributes)) {
254 3
                $parameters[] = ['attributes' => $attributes, 'type' => $parameter];
255
            }
256
        }
257
258 6
        return ['attributes' => $annotations, 'parameters' => $parameters, 'type' => $reflection];
259
    }
260
261
    /**
262
     * Finds classes in the given resource directory.
263
     *
264
     * @param \Traversable<int,string> $files
265
     *
266
     * @return array>int,string>
267
     */
268 1
    private static function findClasses(\Traversable $files): array
269
    {
270 1
        $declared = \get_declared_classes();
271 1
        $classes = [];
272
273 1
        foreach ($files as $file) {
274 1
            require_once $file;
275
        }
276
277 1
        return \array_merge($classes, \array_diff(\get_declared_classes(), $declared));
278
    }
279
}
280