Passed
Push — master ( a9bf82...5141f8 )
by Divine Niiquaye
02:18
created

AnnotationLoader   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Test Coverage

Coverage 94.06%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 43
eloc 89
c 2
b 0
f 0
dl 0
loc 249
ccs 95
cts 101
cp 0.9406
rs 8.96

10 Methods

Rating   Name   Duplication   Size   Complexity  
A attach() 0 4 2
A __construct() 0 3 1
A attachListener() 0 4 2
A findFiles() 0 9 1
A findClasses() 0 10 2
B fetchAnnotations() 0 29 9
A getMethodParameter() 0 7 5
A load() 0 17 6
B findAnnotations() 0 39 6
B getAnnotations() 0 35 9

How to fix   Complexity   

Complex Class

Complex classes like AnnotationLoader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AnnotationLoader, and based on these observations, apply Extract Interface, too.

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 FilesystemIterator;
21
use RecursiveDirectoryIterator;
22
use RecursiveIteratorIterator;
23
use ReflectionClass;
24
use ReflectionClassConstant;
25
use ReflectionMethod;
26
use ReflectionParameter;
27
use ReflectionProperty;
28
use Reflector;
29
use RegexIterator;
30
use Spiral\Attributes\ReaderInterface;
31
32
class AnnotationLoader implements LoaderInterface
33
{
34
    /** @var ReaderInterface */
35
    private $annotation;
36
37
    /** @var ListenerInterface[] */
38
    private $listeners = [];
39
40
    /** @var string[] */
41
    private $resources = [];
42
43
    /**
44
     * @param ReaderInterface $reader
45
     */
46 2
    public function __construct(ReaderInterface $reader)
47
    {
48 2
        $this->annotation = $reader;
49 2
    }
50
51
    /**
52
     * {@inheritdoc}
53
     */
54 2
    public function attachListener(ListenerInterface ...$listeners): void
55
    {
56 2
        foreach ($listeners as $listener) {
57 2
            $this->listeners[] = $listener;
58
        }
59 2
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64 2
    public function attach(string ...$resources): void
65
    {
66 2
        foreach ($resources as $resource) {
67 2
            $this->resources[] = $resource;
68
        }
69 2
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74 2
    public function load(): iterable
75
    {
76 2
        $annotations = [];
77
78 2
        foreach ($this->resources as $resource) {
79 2
            if (\class_exists($resource) || \is_dir($resource)) {
80 2
                $annotations += $this->findAnnotations($resource);
81
82 2
                continue;
83
            }
84
85
            //TODO: Read annotations from functions ...
86
        }
87
88 2
        foreach ($this->listeners as $listener) {
89 2
            if (null !== $found = $listener->onAnnotation($annotations)) {
90 2
                yield $found;
91
            }
92
        }
93 2
    }
94
95
    /**
96
     * Finds annotations in the given resource
97
     *
98
     * @param string $resource
99
     *
100
     * @return array<string,array<string,mixed>>
101
     */
102 2
    private function findAnnotations(string $resource): array
103
    {
104 2
        $classes = $annotations = [];
105
106 2
        if (\is_dir($resource)) {
107 2
            $classes += $this->findClasses($resource);
108
        } elseif (\class_exists($resource)) {
109
            $classes[] = $resource;
110
        }
111
112
        /** @var class-string $class */
113 2
        foreach ($classes as $class) {
114 2
            $classReflection = new ReflectionClass($class);
115 2
            $className       = $classReflection->getName();
116
117 2
            if ($classReflection->isAbstract()) {
118
                throw new InvalidAnnotationException(\sprintf(
119
                    'Annotations from class "%s" cannot be read as it is abstract.',
120
                    $classReflection->getName()
121
                ));
122
            }
123
124 2
            foreach ($this->getAnnotations($classReflection) as $annotation) {
125 2
                $annotations[$className]['class'] = $annotation;
126
            }
127
128
            // Reflections belonging to class object.
129 2
            $reflections = \array_merge(
130 2
                $classReflection->getMethods(),
131 2
                $classReflection->getProperties(),
132 2
                $classReflection->getConstants()
133
            );
134
135 2
            $this->fetchAnnotations($className, $reflections, $annotations);
136
        }
137
138 2
        \gc_mem_caches();
139
140 2
        return $annotations;
141
    }
142
143
    /**
144
     * @param Reflector $reflection
145
     *
146
     * @return iterable<object>
147
     */
148 2
    private function getAnnotations(Reflector $reflection): iterable
149
    {
150 2
        $annotations = [];
151
152
        switch (true) {
153 2
            case $reflection instanceof ReflectionClass:
154 2
                $annotations = $this->annotation->getClassMetadata($reflection);
155
156 2
                break;
157
158 2
            case $reflection instanceof ReflectionMethod:
159 2
                $annotations = $this->annotation->getFunctionMetadata($reflection);
160
161 2
                break;
162
163 2
            case $reflection instanceof ReflectionProperty:
164 2
                $annotations = $this->annotation->getPropertyMetadata($reflection);
165
166 2
                break;
167
168 1
            case $reflection instanceof ReflectionClassConstant:
169 1
                $annotations = $this->annotation->getConstantMetadata($reflection);
170
171 1
                break;
172
173 1
            case $reflection instanceof ReflectionParameter:
174 1
                $annotations = $this->annotation->getParameterMetadata($reflection);
175
        }
176
177 2
        foreach ($annotations as $annotation) {
178 2
            foreach ($this->listeners as $listener) {
179 2
                $annotationClass = $listener->getAnnotation();
180
181 2
                if ($annotation instanceof $annotationClass) {
182 2
                    yield $annotation;
183
                }
184
            }
185
        }
186 2
    }
187
188
    /**
189
     * @param ReflectionParameter[] $parameters
190
     *
191
     * @return iterable<int,object[]>
192
     */
193 2
    private function getMethodParameter(array $parameters): iterable
194
    {
195 2
        foreach ($this->listeners as $listener) {
196 2
            foreach ($parameters as $parameter) {
197 2
                if (\in_array($parameter->getName(), $listener->getArguments(), true)) {
198 1
                    foreach ($this->getAnnotations($parameter) as $annotation) {
199 1
                        yield [$parameter, $annotation];
200
                    }
201
                }
202
            }
203
        }
204 2
    }
205
206
    /**
207
     * Fetch annotations from methods, constant, property and methods parameter
208
     *
209
     * @param string                            $className
210
     * @param Reflector[]                       $reflections
211
     * @param array<string,array<string,mixed>> $annotations
212
     */
213 2
    private function fetchAnnotations(string $className, array $reflections, array &$annotations): void
214
    {    
215 2
        foreach ($reflections as $name => $reflection) {
216 2
            if ($reflection instanceof ReflectionMethod && $reflection->isAbstract()) {
217
                continue;
218
            }
219
220 2
            if (is_string($name)) {
221 1
                $reflection = new ReflectionClassConstant($className, $name);
222
            }
223
224 2
            foreach ($this->getAnnotations($reflection) as $annotation) {
225 2
                if ($reflection instanceof ReflectionMethod) {
226 2
                    $annotations[$className]['method'][] = [$reflection, $annotation];
227
228 2
                    foreach ($this->getMethodParameter($reflection->getParameters()) as $parameter) {
229 1
                        $annotations[$className]['method_property'][] = $parameter;
230
                    }
231
232 2
                    continue;
233
                }
234
235 2
                if ($reflection instanceof ReflectionClassConstant) {
236 1
                    $annotations[$className]['constant'][] = [$reflection, $annotation];
237
238 1
                    continue;
239
                }
240
241 2
                $annotations[$className]['property'][] = [$reflection, $annotation];
242
            }
243
        }
244 2
    }
245
246
    /**
247
     * Finds classes in the given resource directory
248
     *
249
     * @param string $resource
250
     *
251
     * @return string[]
252
     */
253 2
    private function findClasses(string $resource): array
254
    {
255 2
        $files    = $this->findFiles($resource);
256 2
        $declared = \get_declared_classes();
257
258 2
        foreach ($files as $file) {
259 2
            require_once $file;
260
        }
261
262 2
        return \array_diff(\get_declared_classes(), $declared);
263
    }
264
265
    /**
266
     * Finds files in the given resource
267
     *
268
     * @param string $resource
269
     *
270
     * @return string[]
271
     */
272 2
    private function findFiles(string $resource): array
273
    {
274 2
        $flags = FilesystemIterator::CURRENT_AS_PATHNAME;
275
276 2
        $directory = new RecursiveDirectoryIterator($resource, $flags);
277 2
        $iterator  = new RecursiveIteratorIterator($directory);
278 2
        $files     = new RegexIterator($iterator, '/\.php$/');
279
280 2
        return \iterator_to_array($files);
281
    }
282
}
283