Passed
Push — master ( 3aa2bb...992f44 )
by Divine Niiquaye
36:29 queued 06:55
created

AnnotationLoader   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 267
Duplicated Lines 0 %

Test Coverage

Coverage 95.24%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 43
eloc 93
c 3
b 0
f 0
dl 0
loc 267
ccs 100
cts 105
cp 0.9524
rs 8.96

11 Methods

Rating   Name   Duplication   Size   Complexity  
A attach() 0 4 2
A __construct() 0 3 1
A findFiles() 0 7 1
A findClasses() 0 9 2
B fetchAnnotations() 0 33 9
A getMethodParameter() 0 7 5
B build() 0 34 7
A load() 0 7 2
A findAnnotations() 0 26 3
B getAnnotations() 0 35 9
A attachListener() 0 4 2

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