Test Failed
Pull Request — master (#1190)
by Aleksei
15:08
created

Initializer   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 290
Duplicated Lines 0 %

Test Coverage

Coverage 98.9%

Importance

Changes 0
Metric Value
wmc 53
eloc 111
c 0
b 0
f 0
dl 0
loc 290
rs 6.96
ccs 90
cts 91
cp 0.989

14 Methods

Rating   Name   Duplication   Size   Complexity  
A findBootloaderClassesInMethod() 0 11 4
A getBootloadConfigAttribute() 0 13 4
B getBootloadConfig() 0 28 11
A __construct() 0 6 1
A resolveDependencies() 0 8 2
B init() 0 42 10
A getRegistry() 0 3 1
A resolveAttributeBindings() 0 22 6
A findDependenciesInMethods() 0 15 3
A findMethodsWithPriority() 0 25 4
A initDefaultChecker() 0 8 1
A initBootloader() 0 11 3
A isBootloader() 0 3 1
A shouldBeBooted() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like Initializer 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 Initializer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Boot\BootloadManager;
6
7
use Psr\Container\ContainerExceptionInterface;
8
use Psr\Container\ContainerInterface;
9
use Psr\Container\NotFoundExceptionInterface;
10
use Spiral\Boot\Attribute\BootloadConfig;
11
use Spiral\Boot\Attribute\BootMethod;
12
use Spiral\Boot\Attribute\InitMethod;
13
use Spiral\Boot\Bootloader\BootloaderInterface;
14
use Spiral\Boot\Bootloader\DependedInterface;
15
use Spiral\Boot\BootloadManager\Checker\BootloaderChecker;
16
use Spiral\Boot\BootloadManager\Checker\BootloaderCheckerInterface;
17
use Spiral\Boot\BootloadManager\Checker\CanBootedChecker;
18
use Spiral\Boot\BootloadManager\Checker\CheckerRegistry;
19
use Spiral\Boot\BootloadManager\Checker\ClassExistsChecker;
20
use Spiral\Boot\BootloadManager\Checker\ConfigChecker;
21
use Spiral\Boot\BootloadManagerInterface;
22
use Spiral\Core\Attribute\Singleton;
23
use Spiral\Core\BinderInterface;
24
use Spiral\Core\ResolverInterface;
25
26
/**
27
 * @internal
28
 * @psalm-import-type TClass from BootloadManagerInterface
29
 * @psalm-import-type TFullBinding from BootloaderInterface
30
 */
31
#[Singleton]
32 631
class Initializer implements InitializerInterface
33
{
34
    protected ?BootloaderCheckerInterface $checker = null;
35
36
    public function __construct(
37 631
        protected readonly ContainerInterface $container,
38
        protected readonly BinderInterface $binder,
39
        protected readonly ClassesRegistry $bootloaders = new ClassesRegistry(),
40
        ?BootloaderCheckerInterface $checker = null,
41
    ) {}
42
43
    /**
44 622
     * Instantiate bootloader objects and resolve dependencies
45
     *
46 622
     * @param TClass[]|array<class-string<BootloaderInterface>, array<string,mixed>> $classes
0 ignored issues
show
Documentation Bug introduced by
The doc comment TClass[]|array<class-str...>, array<string,mixed>> at position 6 could not be parsed: Unknown type name 'class-string' at position 6 in TClass[]|array<class-string<BootloaderInterface>, array<string,mixed>>.
Loading history...
47
     * @throws ContainerExceptionInterface
48 622
     * @throws NotFoundExceptionInterface
49
     * @throws \ReflectionException
50 622
     */
51 605
    public function init(array $classes, bool $useConfig = true): \Generator
52 605
    {
53
        $this->checker ??= $this->initDefaultChecker();
54 622
55
        foreach ($classes as $bootloader => $options) {
56 622
            // default bootload syntax as simple array
57 458
            if (\is_string($options) || $options instanceof BootloaderInterface) {
58
                $bootloader = $options;
59
                $options = [];
60 601
            }
61
            $options = $useConfig ? $this->getBootloadConfig($bootloader, $options) : [];
62 601
63 600
            if (!$this->checker->canInitialize($bootloader, $useConfig ? $options : null)) {
0 ignored issues
show
Bug introduced by
The method canInitialize() does not exist on null. ( Ignorable by Annotation )

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

63
            if (!$this->checker->/** @scrutinizer ignore-call */ canInitialize($bootloader, $useConfig ? $options : null)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
It seems like $useConfig ? $options : null can also be of type array; however, parameter $config of Spiral\Boot\BootloadMana...erface::canInitialize() does only seem to accept Spiral\Boot\Attribute\BootloadConfig|null, maybe add an additional type check? ( Ignorable by Annotation )

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

63
            if (!$this->checker->canInitialize($bootloader, /** @scrutinizer ignore-type */ $useConfig ? $options : null)) {
Loading history...
64
                continue;
65
            }
66
67 601
            $this->bootloaders->register($bootloader instanceof BootloaderInterface ? $bootloader::class : $bootloader);
68 601
69 601
            if (!$bootloader instanceof BootloaderInterface) {
70 601
                $bootloader = $this->container->get($bootloader);
71 601
            }
72
73
            $initMethods = $this->findMethodsWithPriority(
74
                $bootloader,
75 9
                [0 => [Methods::INIT->value]],
76
                InitMethod::class,
77 9
            );
78
79
            $bootMethods = $this->findMethodsWithPriority(
80
                $bootloader,
81
                [0 => [Methods::BOOT->value]],
82
                BootMethod::class,
83 601
            );
84
85 601
            yield from $this->resolveDependencies($bootloader, \array_unique([...$initMethods, ...$bootMethods]));
86 601
87
            $this->initBootloader($bootloader);
88
            yield $bootloader::class => [
89 601
                'bootloader' => $bootloader,
90 601
                'options' => $options instanceof BootloadConfig ? $options->args : $options,
91 601
                'init_methods' => $initMethods,
92 601
                'boot_methods' => $bootMethods,
93
            ];
94
        }
95
    }
96
97
    public function getRegistry(): ClassesRegistry
98
    {
99
        return $this->bootloaders;
100
    }
101 601
102
    protected function shouldBeBooted(\ReflectionNamedType $type): bool
103 601
    {
104 557
        /** @var TClass $class */
105
        $class = $type->getName();
106
107 601
        return $this->isBootloader($class)
108 578
            && !$this->bootloaders->isBooted($class);
109
    }
110
111
    /**
112 601
     * @psalm-pure
113
     * @psalm-assert-if-true TClass $class
114 601
     */
115
    protected function isBootloader(string|object $class): bool
116 601
    {
117
        return \is_subclass_of($class, BootloaderInterface::class);
118 601
    }
119
120 601
    protected function initDefaultChecker(): BootloaderCheckerInterface
121 601
    {
122 578
        $registry = new CheckerRegistry();
123 578
        $registry->register($this->container->get(ConfigChecker::class));
124 578
        $registry->register(new ClassExistsChecker());
125
        $registry->register(new CanBootedChecker($this->bootloaders));
126
127
        return new BootloaderChecker($registry);
128 601
    }
129
130
    /**
131 578
     * Resolve all bootloader dependencies and init bindings
132
     */
133 578
    private function initBootloader(BootloaderInterface $bootloader): void
134 578
    {
135 578
        foreach ($bootloader->defineBindings() as $alias => $resolver) {
136 578
            $this->binder->bind($alias, $resolver);
137 461
        }
138
139
        foreach ($bootloader->defineSingletons() as $alias => $resolver) {
140
            $this->binder->bindSingleton($alias, $resolver);
141 578
        }
142
143
        $this->resolveAttributeBindings($bootloader);
144 578
    }
145
146
    /**
147 578
     * Returns merged config. Attribute config has lower priority.
148
     *
149 578
     * @param class-string<BootloaderInterface>|BootloaderInterface $bootloader
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<BootloaderI...ce>|BootloaderInterface at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<BootloaderInterface>|BootloaderInterface.
Loading history...
150 578
     * @throws \ReflectionException
151
     */
152
    private function getBootloadConfig(
153
        string|BootloaderInterface $bootloader,
154
        array|callable|BootloadConfig $config,
155
    ): BootloadConfig {
156
        if ($config instanceof \Closure) {
0 ignored issues
show
introduced by
$config is never a sub-type of Closure.
Loading history...
157 578
            $config = $this->container instanceof ResolverInterface
158
                ? $config(...$this->container->resolveArguments(new \ReflectionFunction($config)))
159 578
                : $config();
160
        }
161
162 622
        $attr = $this->getBootloadConfigAttribute($bootloader);
163
164 622
        $getArgument = static fn(string $key, bool $override, mixed $default = []): mixed => match (true) {
165 622
            $config instanceof BootloadConfig && $override => $config->{$key},
166 622
            $config instanceof BootloadConfig && !$override && \is_array($default) =>
167 622
                $config->{$key} + ($attr->{$key} ?? []),
168
            $config instanceof BootloadConfig && !$override && \is_bool($default) => $config->{$key},
169 622
            \is_array($config) && $config !== [] && $key === 'args' => $config,
170
            default => $attr->{$key} ?? $default,
171
        };
172
173
        $override = $config instanceof BootloadConfig ? $config->override : true;
0 ignored issues
show
introduced by
$config is never a sub-type of Spiral\Boot\Attribute\BootloadConfig.
Loading history...
174
175
        return new BootloadConfig(
176
            args: $getArgument('args', $override),
177 624
            enabled: $getArgument('enabled', $override, true),
178
            allowEnv: $getArgument('allowEnv', $override),
179
            denyEnv: $getArgument('denyEnv', $override),
180
        );
181 624
    }
182 2
183 2
    /**
184
     * This method is used to find and instantiate BootloadConfig attribute.
185
     *
186 624
     * @param class-string<BootloaderInterface>|BootloaderInterface $bootloader
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<BootloaderI...ce>|BootloaderInterface at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<BootloaderInterface>|BootloaderInterface.
Loading history...
187
     * @throws \ReflectionException
188 624
     */
189 624
    private function getBootloadConfigAttribute(string|BootloaderInterface $bootloader): ?BootloadConfig
190 606
    {
191 3
        $attribute = null;
192 606
        if ($bootloader instanceof BootloaderInterface || \class_exists($bootloader)) {
193 603
            $ref = new \ReflectionClass($bootloader);
194 624
            $attribute = $ref->getAttributes(BootloadConfig::class)[0] ?? null;
195 606
        }
196
197 624
        if ($attribute === null) {
198
            return null;
199 624
        }
200 624
201 624
        return $attribute->newInstance();
202 624
    }
203 624
204 624
    /**
205
     * This method is used to find methods with InitMethod or BootMethod attributes.
206
     *
207
     * @param class-string<InitMethod|BootMethod> $attribute
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<InitMethod|BootMethod> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<InitMethod|BootMethod>.
Loading history...
208
     * @param list<non-empty-string[]> $initialMethods
209
     * @return list<non-empty-string>
210 624
     */
211
    private function findMethodsWithPriority(
212 624
        BootloaderInterface $bootloader,
213 624
        array $initialMethods,
214 623
        string $attribute,
215 623
    ): array {
216
        $methods = $initialMethods;
217
218 624
        $refl = new \ReflectionClass($bootloader);
219 605
        foreach ($refl->getMethods() as $method) {
220
            if ($method->isStatic()) {
221
                continue;
222 22
            }
223
224
            $attrs = $method->getAttributes($attribute);
225
            if (\count($attrs) === 0) {
226
                continue;
227
            }
228
            /** @var InitMethod|BootMethod $attr */
229
            $attr = $attrs[0]->newInstance();
230
            $methods[$attr->priority][] = $method->getName();
231
        }
232
233
        \ksort($methods);
234
235
        return \array_merge(...$methods);
236
    }
237
238
    /**
239
     * This method is used to resolve bindings from attributes.
240
     *
241
     * @throws \ReflectionException
242
     */
243
    private function resolveAttributeBindings(BootloaderInterface $bootloader): void
244
    {
245
        if (!$this->container->has(AttributeResolver::class)) {
246
            return;
247
        }
248
249
        /** @var AttributeResolver $attributeResolver */
250
        $attributeResolver = $this->container->get(AttributeResolver::class);
251
252
        $availableAttributes = $attributeResolver->getResolvers();
253
254
        $refl = new \ReflectionClass($bootloader);
255
        foreach ($refl->getMethods() as $method) {
256
            if ($method->isStatic()) {
257
                continue;
258
            }
259
260
            foreach ($availableAttributes as $attributeClass) {
261
                $attrs = $method->getAttributes($attributeClass);
262
                foreach ($attrs as $attr) {
263
                    $instance = $attr->newInstance();
264
                    $attributeResolver->resolve($instance, $bootloader, $method);
265
                }
266
            }
267
        }
268
    }
269
270
    /**
271
     * @param non-empty-string[] $bootloaderMethods
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string[] at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string[].
Loading history...
272
     * @throws ContainerExceptionInterface
273
     * @throws NotFoundExceptionInterface
274
     * @throws \ReflectionException
275
     */
276
    private function resolveDependencies(BootloaderInterface $bootloader, array $bootloaderMethods): iterable
277
    {
278
        $deps = $this->findDependenciesInMethods($bootloader, $bootloaderMethods);
279
        if ($bootloader instanceof DependedInterface) {
280
            $deps = [...$deps, ...$bootloader->defineDependencies()];
281
        }
282
283
        yield from $this->init(\array_values(\array_unique($deps)));
284
    }
285
286
    /**
287
     * @param non-empty-string[] $methods
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string[] at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string[].
Loading history...
288
     * @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...
289
     */
290
    private function findDependenciesInMethods(BootloaderInterface $bootloader, array $methods): array
291
    {
292
        $reflectionClass = new \ReflectionClass($bootloader);
293
294
        $methodsDeps = [];
295
296
        foreach ($methods as $method) {
297
            if ($reflectionClass->hasMethod($method)) {
298
                $methodsDeps[] = $this->findBootloaderClassesInMethod(
299
                    $reflectionClass->getMethod($method),
300
                );
301
            }
302
        }
303
304
        return \array_merge(...$methodsDeps);
305
    }
306
307
    /**
308
     * @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...
309
     */
310
    private function findBootloaderClassesInMethod(\ReflectionMethod $method): array
311
    {
312
        $args = [];
313
        foreach ($method->getParameters() as $parameter) {
314
            $type = $parameter->getType();
315
            if ($type instanceof \ReflectionNamedType && $this->shouldBeBooted($type)) {
316
                $args[] = $type->getName();
317
            }
318
        }
319
320
        return $args;
321
    }
322
}
323