Initializer::findMethodsWithPriority()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 25
ccs 13
cts 13
cp 1
rs 9.8666
cc 4
nc 4
nop 3
crap 4
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
class Initializer implements InitializerInterface
33
{
34
    protected ?BootloaderCheckerInterface $checker = null;
35
36 643
    public function __construct(
37
        protected readonly ContainerInterface $container,
38
        protected readonly BinderInterface $binder,
39
        protected readonly ClassesRegistry $bootloaders = new ClassesRegistry(),
40
        ?BootloaderCheckerInterface $checker = null,
41 643
    ) {}
42
43
    /**
44
     * Instantiate bootloader objects and resolve dependencies
45
     *
46
     * @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
     * @throws NotFoundExceptionInterface
49
     * @throws \ReflectionException
50
     */
51 634
    public function init(array $classes, bool $useConfig = true): \Generator
52
    {
53 634
        $this->checker ??= $this->initDefaultChecker();
54
55 634
        foreach ($classes as $bootloader => $options) {
56
            // default bootload syntax as simple array
57 634
            if (\is_string($options) || $options instanceof BootloaderInterface) {
58 617
                $bootloader = $options;
59 617
                $options = [];
60
            }
61 634
            $options = $useConfig ? $this->getBootloadConfig($bootloader, $options) : [];
62
63 634
            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 457
                continue;
65
            }
66
67 613
            $this->bootloaders->register($bootloader instanceof BootloaderInterface ? $bootloader::class : $bootloader);
68
69 613
            if (!$bootloader instanceof BootloaderInterface) {
70 612
                $bootloader = $this->container->get($bootloader);
71
            }
72
73 613
            $initMethods = $this->findMethodsWithPriority(
74 613
                $bootloader,
75 613
                [0 => [Methods::INIT->value]],
76 613
                InitMethod::class,
77 613
            );
78
79 613
            $bootMethods = $this->findMethodsWithPriority(
80 613
                $bootloader,
81 613
                [0 => [Methods::BOOT->value]],
82 613
                BootMethod::class,
83 613
            );
84
85 613
            yield from $this->resolveDependencies($bootloader, \array_unique([...$initMethods, ...$bootMethods]));
86
87 613
            $this->initBootloader($bootloader);
88 609
            yield $bootloader::class => [
89 609
                'bootloader' => $bootloader,
90 609
                'options' => $options instanceof BootloadConfig ? $options->args : $options,
91 609
                'init_methods' => $initMethods,
92 609
                'boot_methods' => $bootMethods,
93 609
            ];
94
        }
95
    }
96
97 9
    public function getRegistry(): ClassesRegistry
98
    {
99 9
        return $this->bootloaders;
100
    }
101
102 583
    protected function shouldBeBooted(\ReflectionNamedType $type): bool
103
    {
104
        /** @var TClass $class */
105 583
        $class = $type->getName();
106
107 583
        return $this->isBootloader($class)
108 583
            && !$this->bootloaders->isBooted($class);
109
    }
110
111
    /**
112
     * @psalm-pure
113
     * @psalm-assert-if-true TClass $class
114
     */
115 583
    protected function isBootloader(string|object $class): bool
116
    {
117 583
        return \is_subclass_of($class, BootloaderInterface::class);
118
    }
119
120 634
    protected function initDefaultChecker(): BootloaderCheckerInterface
121
    {
122 634
        $registry = new CheckerRegistry();
123 634
        $registry->register($this->container->get(ConfigChecker::class));
124 634
        $registry->register(new ClassExistsChecker());
125 634
        $registry->register(new CanBootedChecker($this->bootloaders));
126
127 634
        return new BootloaderChecker($registry);
128
    }
129
130
    /**
131
     * Resolve all bootloader dependencies and init bindings
132
     */
133 613
    private function initBootloader(BootloaderInterface $bootloader): void
134
    {
135 613
        foreach ($bootloader->defineBindings() as $alias => $resolver) {
136 559
            $this->binder->bind($alias, $resolver);
137
        }
138
139 613
        foreach ($bootloader->defineSingletons() as $alias => $resolver) {
140 580
            $this->binder->bindSingleton($alias, $resolver);
141
        }
142
143 613
        $this->resolveAttributeBindings($bootloader);
144
    }
145
146
    /**
147
     * Returns merged config. Attribute config has lower priority.
148
     *
149
     * @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
     * @throws \ReflectionException
151
     */
152 636
    private function getBootloadConfig(
153
        string|BootloaderInterface $bootloader,
154
        array|callable|BootloadConfig $config,
155
    ): BootloadConfig {
156 636
        if ($config instanceof \Closure) {
0 ignored issues
show
introduced by
$config is never a sub-type of Closure.
Loading history...
157 2
            $config = $this->container instanceof ResolverInterface
158 2
                ? $config(...$this->container->resolveArguments(new \ReflectionFunction($config)))
159
                : $config();
160
        }
161
162 636
        $attr = $this->getBootloadConfigAttribute($bootloader);
163
164 636
        $getArgument = static fn(string $key, bool $override, mixed $default = []): mixed => match (true) {
165 636
            $config instanceof BootloadConfig && $override => $config->{$key},
166 618
            $config instanceof BootloadConfig && !$override && \is_array($default) =>
167 3
                $config->{$key} + ($attr->{$key} ?? []),
168 618
            $config instanceof BootloadConfig && !$override && \is_bool($default) => $config->{$key},
169 615
            \is_array($config) && $config !== [] && $key === 'args' => $config,
170 636
            default => $attr->{$key} ?? $default,
171 618
        };
172
173 636
        $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 636
        return new BootloadConfig(
176 636
            args: $getArgument('args', $override),
177 636
            enabled: $getArgument('enabled', $override, true),
178 636
            allowEnv: $getArgument('allowEnv', $override),
179 636
            denyEnv: $getArgument('denyEnv', $override),
180 636
        );
181
    }
182
183
    /**
184
     * This method is used to find and instantiate BootloadConfig attribute.
185
     *
186
     * @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
     */
189 636
    private function getBootloadConfigAttribute(string|BootloaderInterface $bootloader): ?BootloadConfig
190
    {
191 636
        $attribute = null;
192 636
        if ($bootloader instanceof BootloaderInterface || \class_exists($bootloader)) {
193 635
            $ref = new \ReflectionClass($bootloader);
194 635
            $attribute = $ref->getAttributes(BootloadConfig::class)[0] ?? null;
195
        }
196
197 636
        if ($attribute === null) {
198 617
            return null;
199
        }
200
201 22
        return $attribute->newInstance();
202
    }
203
204
    /**
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
     */
211 613
    private function findMethodsWithPriority(
212
        BootloaderInterface $bootloader,
213
        array $initialMethods,
214
        string $attribute,
215
    ): array {
216 613
        $methods = $initialMethods;
217
218 613
        $refl = new \ReflectionClass($bootloader);
219 613
        foreach ($refl->getMethods() as $method) {
220 613
            if ($method->isStatic()) {
221 358
                continue;
222
            }
223
224 613
            $attrs = $method->getAttributes($attribute);
225 613
            if (\count($attrs) === 0) {
226 613
                continue;
227
            }
228
            /** @var InitMethod|BootMethod $attr */
229 4
            $attr = $attrs[0]->newInstance();
230 4
            $methods[$attr->priority][] = $method->getName();
231
        }
232
233 613
        \ksort($methods);
234
235 613
        return \array_merge(...$methods);
236
    }
237
238
    /**
239
     * This method is used to resolve bindings from attributes.
240
     *
241
     * @throws \ReflectionException
242
     */
243 613
    private function resolveAttributeBindings(BootloaderInterface $bootloader): void
244
    {
245 613
        if (!$this->container->has(AttributeResolver::class)) {
246 595
            return;
247
        }
248
249
        /** @var AttributeResolver $attributeResolver */
250 18
        $attributeResolver = $this->container->get(AttributeResolver::class);
251
252 18
        $availableAttributes = $attributeResolver->getResolvers();
253
254 18
        $refl = new \ReflectionClass($bootloader);
255 18
        foreach ($refl->getMethods() as $method) {
256 18
            if ($method->isStatic()) {
257
                continue;
258
            }
259
260 18
            foreach ($availableAttributes as $attributeClass) {
261 18
                $attrs = $method->getAttributes($attributeClass);
262 18
                foreach ($attrs as $attr) {
263 11
                    $instance = $attr->newInstance();
264 11
                    $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 613
    private function resolveDependencies(BootloaderInterface $bootloader, array $bootloaderMethods): iterable
277
    {
278 613
        $deps = $this->findDependenciesInMethods($bootloader, $bootloaderMethods);
279 613
        if ($bootloader instanceof DependedInterface) {
280 613
            $deps = [...$deps, ...$bootloader->defineDependencies()];
281
        }
282
283 613
        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 613
    private function findDependenciesInMethods(BootloaderInterface $bootloader, array $methods): array
291
    {
292 613
        $reflectionClass = new \ReflectionClass($bootloader);
293
294 613
        $methodsDeps = [];
295
296 613
        foreach ($methods as $method) {
297 613
            if ($reflectionClass->hasMethod($method)) {
298 583
                $methodsDeps[] = $this->findBootloaderClassesInMethod(
299 583
                    $reflectionClass->getMethod($method),
300 583
                );
301
            }
302
        }
303
304 613
        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 583
    private function findBootloaderClassesInMethod(\ReflectionMethod $method): array
311
    {
312 583
        $args = [];
313 583
        foreach ($method->getParameters() as $parameter) {
314 583
            $type = $parameter->getType();
315 583
            if ($type instanceof \ReflectionNamedType && $this->shouldBeBooted($type)) {
316 460
                $args[] = $type->getName();
317
            }
318
        }
319
320 583
        return $args;
321
    }
322
}
323