Passed
Push — master ( 9cc41a...e2f21b )
by Divine Niiquaye
02:42
created

AnnotationLoader::fetchClassAnnotation()   B

Complexity

Conditions 11
Paths 27

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 11

Importance

Changes 0
Metric Value
eloc 26
c 0
b 0
f 0
dl 0
loc 46
ccs 27
cts 27
cp 1
rs 7.3166
cc 11
nc 27
nop 2
crap 11

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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[])|null */
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;
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 7
    public function load(string ...$listener)
89
    {
90 7
        $loadedAnnotation = [];
91
92 7
        if (empty($listener)) {
93 3
            if (empty($this->loadedListeners)) {
94 3
                foreach ($this->listeners as $name => $value) {
95 3
                    $this->loadedListeners[$name] = $value->load($this->build(...$value->getAnnotations()));
96
                }
97
            }
98
99 3
            $loadedAnnotation = $this->loadedListeners;
100
        } else {
101 7
            foreach ($listener as $name) {
102 7
                $name = $this->aliases[$name] ?? $name;
103 7
                $loaded = ($this->loadedListeners[$name] ?? $this->loadedAttributes[$name] ?? null);
104
105 7
                if (null === $loaded) {
106 4
                    $l = $this->listeners[$name] ?? null;
107
108 4
                    if ($l instanceof ListenerInterface) {
109 2
                        $this->loadedListeners[$name] = $l->load($this->build(...$l->getAnnotations()));
110
                    }
111
112 4
                    $loaded = $this->loadedListeners[$name] ?? ($this->loadedAttributes[$name] = $this->build($name));
113
                }
114
115 7
                $loadedAnnotation[$name] = $loaded;
116
            }
117
        }
118
119 7
        return 1 === \count($loadedAnnotation) ? \current($loadedAnnotation) : $loadedAnnotation;
120
    }
121
122
    /**
123
     * Builds the attributes/annotations for the given class or function.
124
     *
125
     * @return array<int,array<string, mixed>>
126
     */
127 7
    protected function build(string ...$annotationClass): array
128
    {
129 7
        $annotations = [];
130
131 7
        foreach ($this->resources as $resource) {
132 6
            $values = [];
133
134 6
            if (\is_dir($resource)) {
135 5
                $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($resource, \FilesystemIterator::CURRENT_AS_PATHNAME));
136 5
                $files = new \RegexIterator($iterator, '/\.php$/');
137
138 5
                if (\iterator_count($files) > 0) {
139 5
                    $values = ($this->classLoader ?? [$this, 'findClasses'])($files);
140
141 5
                    foreach ($values as $class) {
142 5
                        $classes = $this->fetchClassAnnotation($class, $annotationClass);
143
144 5
                        if (!empty($classes)) {
145 5
                            $annotations[] = $classes;
146
                        }
147
                    }
148
                }
149 5
                continue;
150
            }
151
152 6
            if (\function_exists($resource)) {
153 3
                $values = $this->fetchFunctionAnnotation(new \ReflectionFunction($resource), $annotationClass);
154 3
            } elseif (\class_exists($resource)) {
155
                $values = $this->fetchClassAnnotation($resource, $annotationClass);
156
            }
157
158 6
            if (!empty($values)) {
159 3
                $annotations[] = $values;
160
            }
161
        }
162
163 7
        return $annotations;
164
    }
165
166
    /**
167
     * @param array<int,string>|string $annotation
168
     *
169
     * @return array<int,object>
170
     */
171 6
    private function getAnnotations(\Reflector $reflection, $annotation): array
172
    {
173 6
        $annotations = [];
174
175 6
        if (\is_array($annotation)) {
176 6
            foreach ($annotation as $annotationClass) {
177 6
                $annotations = \array_merge($annotations, $this->getAnnotations($reflection, $annotationClass));
178
            }
179
180 6
            return $annotations;
181
        }
182
183 6
        if (null === $this->reader) {
184 2
            return \array_map(static function (\ReflectionAttribute $attribute): object {
185 2
                return $attribute->newInstance();
186 2
            }, $reflection->getAttributes($annotation));
0 ignored issues
show
Bug introduced by
The method getAttributes() does not exist on Reflector. It seems like you code against a sub-type of said class. However, the method does not exist in ReflectionExtension or ReflectionZendExtension. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

186
            }, $reflection->/** @scrutinizer ignore-call */ getAttributes($annotation));
Loading history...
187
        }
188
189 6
        if ($reflection instanceof \ReflectionClass) {
190 5
            $annotations = $this->reader->getClassMetadata($reflection, $annotation);
191 6
        } elseif ($reflection instanceof \ReflectionFunctionAbstract) {
192 6
            $annotations = $this->reader->getFunctionMetadata($reflection, $annotation);
193 6
        } elseif ($reflection instanceof \ReflectionProperty) {
194 5
            $annotations = $this->reader->getPropertyMetadata($reflection, $annotation);
195 6
        } elseif ($reflection instanceof \ReflectionClassConstant) {
196 2
            $annotations = $this->reader->getConstantMetadata($reflection, $annotation);
197 6
        } elseif ($reflection instanceof \ReflectionParameter) {
198 6
            $annotations = $this->reader->getParameterMetadata($reflection, $annotation);
199
        }
200
201 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...
202
    }
203
204
    /**
205
     * Finds annotations in the given resource.
206
     *
207
     * @param class-string|string $resource
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|string.
Loading history...
208
     * @param array<int,string>   $annotationClass
209
     *
210
     * @return array<string,mixed>
211
     */
212 5
    private function fetchClassAnnotation(string $resource, array $annotationClass): array
213
    {
214 5
        $classReflection = new \ReflectionClass($resource);
215
216 5
        if ($classReflection->isAbstract()) {
217 3
            return [];
218
        }
219
220 5
        $classRefCount = 0;
221 5
        $constants = $properties = $methods = [];
222 5
        $reflections = \array_merge($classReflection->getMethods(), $classReflection->getProperties(), $classReflection->getConstants());
223
224 5
        foreach ($reflections as $name => $reflection) {
225 5
            if (\is_string($name)) {
226 2
                $reflection = new \ReflectionClassConstant($classReflection->name, $name);
227
            }
228
229 5
            if ($reflection instanceof \ReflectionMethod) {
230 5
                $method = $this->fetchFunctionAnnotation($reflection, $annotationClass);
231
232 5
                if (!empty($method)) {
233 5
                    $methods[] = $method;
234 5
                    ++$classRefCount;
235
                }
236
237 5
                continue;
238
            }
239
240 5
            if (empty($annotations = $this->getAnnotations($reflection, $annotationClass))) {
241 3
                continue;
242
            }
243
244 5
            if ($reflection instanceof \ReflectionProperty) {
245 5
                $properties[] = ['attributes' => $annotations, 'type' => $reflection];
246 5
                ++$classRefCount;
247 2
            } elseif ($reflection instanceof \ReflectionClassConstant) {
248 2
                $constants[] = ['attributes' => $annotations, 'type' => $reflection];
249 2
                ++$classRefCount;
250
            }
251
        }
252
253 5
        if (empty($class = $this->getAnnotations($classReflection, $annotationClass)) && 0 === $classRefCount) {
254 3
            return [];
255
        }
256
257 5
        return ['attributes' => $class] + \compact('constants', 'properties', 'methods') + ['type' => $classReflection];
258
    }
259
260
    /**
261
     * @return array<string,mixed>
262
     */
263 6
    private function fetchFunctionAnnotation(\ReflectionFunctionAbstract $reflection, array $annotationClass)
264
    {
265 6
        $parameters = [];
266 6
        $annotations = $this->getAnnotations($reflection, $annotationClass);
267
268 6
        if (empty($annotations)) {
269 3
            return [];
270
        }
271
272 6
        foreach ($reflection->getParameters() as $parameter) {
273 6
            $attributes = $this->getAnnotations($parameter, $annotationClass);
274
275 6
            if (!empty($attributes)) {
276 3
                $parameters[] = ['attributes' => $attributes, 'type' => $parameter];
277
            }
278
        }
279
280 6
        return ['attributes' => $annotations, 'parameters' => $parameters, 'type' => $reflection];
281
    }
282
283
    /**
284
     * Finds classes in the given resource directory.
285
     *
286
     * @param \Traversable<int,string> $files
287
     *
288
     * @return array>int,string>
289
     */
290 1
    private static function findClasses(\Traversable $files): array
291
    {
292 1
        $declared = \get_declared_classes();
293 1
        $classes = [];
294
295 1
        foreach ($files as $file) {
296 1
            require_once $file;
297
        }
298
299 1
        return \array_merge($classes, \array_diff(\get_declared_classes(), $declared));
300
    }
301
}
302