Passed
Pull Request — master (#1190)
by butschster
11:23
created

Initializer::initBindings()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 4
nop 2
crap 3
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 642
    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 642
    ) {}
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 633
    public function init(array $classes, bool $useConfig = true): \Generator
52
    {
53 633
        $this->checker ??= $this->initDefaultChecker();
54
55 633
        foreach ($classes as $bootloader => $options) {
56
            // default bootload syntax as simple array
57 633
            if (\is_string($options) || $options instanceof BootloaderInterface) {
58 616
                $bootloader = $options;
59 616
                $options = [];
60
            }
61 633
            $options = $useConfig ? $this->getBootloadConfig($bootloader, $options) : [];
62
63 633
            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 458
                continue;
65
            }
66
67 612
            $this->bootloaders->register($bootloader instanceof BootloaderInterface ? $bootloader::class : $bootloader);
68
69 612
            if (!$bootloader instanceof BootloaderInterface) {
70 611
                $bootloader = $this->container->get($bootloader);
71
            }
72
73 612
            $initMethods = $this->findMethodsWithPriority(
74 612
                $bootloader,
75 612
                [0 => [Methods::INIT->value]],
76 612
                InitMethod::class,
77 612
            );
78
79 612
            $bootMethods = $this->findMethodsWithPriority(
80 612
                $bootloader,
81 612
                [0 => [Methods::BOOT->value]],
82 612
                BootMethod::class,
83 612
            );
84
85 612
            yield from $this->resolveDependencies($bootloader, \array_unique([...$initMethods, ...$bootMethods]));
86
87 612
            $this->initBootloader($bootloader);
88 608
            yield $bootloader::class => [
89 608
                'bootloader' => $bootloader,
90 608
                'options' => $options instanceof BootloadConfig ? $options->args : $options,
91 608
                'init_methods' => $initMethods,
92 608
                'boot_methods' => $bootMethods,
93 608
            ];
94
        }
95
    }
96
97 9
    public function getRegistry(): ClassesRegistry
98
    {
99 9
        return $this->bootloaders;
100
    }
101
102
    /**
103
     * Resolve all bootloader dependencies and init bindings
104
     */
105 612
    private function initBootloader(BootloaderInterface $bootloader): void
106
    {
107 612
        foreach ($bootloader->defineBindings() as $alias => $resolver) {
108 558
            $this->binder->bind($alias, $resolver);
109
        }
110
111 612
        foreach ($bootloader->defineSingletons() as $alias => $resolver) {
112 579
            $this->binder->bindSingleton($alias, $resolver);
113
        }
114
115 612
        $this->resolveAttributeBindings($bootloader);
116
    }
117
118 582
    protected function shouldBeBooted(\ReflectionNamedType $type): bool
119
    {
120
        /** @var TClass $class */
121 582
        $class = $type->getName();
122
123 582
        return $this->isBootloader($class)
124 582
            && !$this->bootloaders->isBooted($class);
125
    }
126
127
    /**
128
     * @psalm-pure
129
     * @psalm-assert-if-true TClass $class
130
     */
131 582
    protected function isBootloader(string|object $class): bool
132
    {
133 582
        return \is_subclass_of($class, BootloaderInterface::class);
134
    }
135
136 633
    protected function initDefaultChecker(): BootloaderCheckerInterface
137
    {
138 633
        $registry = new CheckerRegistry();
139 633
        $registry->register($this->container->get(ConfigChecker::class));
140 633
        $registry->register(new ClassExistsChecker());
141 633
        $registry->register(new CanBootedChecker($this->bootloaders));
142
143 633
        return new BootloaderChecker($registry);
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 635
    private function getBootloadConfig(
153
        string|BootloaderInterface $bootloader,
154
        array|callable|BootloadConfig $config,
155
    ): BootloadConfig {
156 635
        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 635
        $attr = $this->getBootloadConfigAttribute($bootloader);
163
164 635
        $getArgument = static fn(string $key, bool $override, mixed $default = []): mixed => match (true) {
165 635
            $config instanceof BootloadConfig && $override => $config->{$key},
166 617
            $config instanceof BootloadConfig && !$override && \is_array($default) =>
167 3
                $config->{$key} + ($attr->{$key} ?? []),
168 617
            $config instanceof BootloadConfig && !$override && \is_bool($default) => $config->{$key},
169 614
            \is_array($config) && $config !== [] && $key === 'args' => $config,
170 635
            default => $attr->{$key} ?? $default,
171 617
        };
172
173 635
        $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 635
        return new BootloadConfig(
176 635
            args: $getArgument('args', $override),
177 635
            enabled: $getArgument('enabled', $override, true),
178 635
            allowEnv: $getArgument('allowEnv', $override),
179 635
            denyEnv: $getArgument('denyEnv', $override),
180 635
        );
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 635
    private function getBootloadConfigAttribute(string|BootloaderInterface $bootloader): ?BootloadConfig
190
    {
191 635
        $attribute = null;
192 635
        if ($bootloader instanceof BootloaderInterface || \class_exists($bootloader)) {
193 634
            $ref = new \ReflectionClass($bootloader);
194 634
            $attribute = $ref->getAttributes(BootloadConfig::class)[0] ?? null;
195
        }
196
197 635
        if ($attribute === null) {
198 616
            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 612
    private function findMethodsWithPriority(
212
        BootloaderInterface $bootloader,
213
        array $initialMethods,
214
        string $attribute,
215
    ): array {
216 612
        $methods = $initialMethods;
217
218 612
        $refl = new \ReflectionClass($bootloader);
219 612
        foreach ($refl->getMethods() as $method) {
220 612
            if ($method->isStatic()) {
221 359
                continue;
222
            }
223
224 612
            $attrs = $method->getAttributes($attribute);
225 612
            if (\count($attrs) === 0) {
226 612
                continue;
227
            }
228
            /** @var InitMethod|BootMethod $attr */
229 4
            $attr = $attrs[0]->newInstance();
230 4
            $methods[$attr->priority][] = $method->getName();
231
        }
232
233 612
        \ksort($methods);
234
235 612
        return \array_merge(...$methods);
236
    }
237
238
    /**
239
     * This method is used to resolve bindings from attributes.
240
     *
241
     * @throws \ReflectionException
242
     */
243 612
    private function resolveAttributeBindings(BootloaderInterface $bootloader): void
244
    {
245 612
        if (!$this->container->has(AttributeResolver::class)) {
246 594
            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 612
    private function resolveDependencies(BootloaderInterface $bootloader, array $bootloaderMethods): iterable
277
    {
278 612
        $deps = $this->findDependenciesInMethods($bootloader, $bootloaderMethods);
279 612
        if ($bootloader instanceof DependedInterface) {
280 612
            $deps = [...$deps, ...$bootloader->defineDependencies()];
281
        }
282
283 612
        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 612
    private function findDependenciesInMethods(BootloaderInterface $bootloader, array $methods): array
291
    {
292 612
        $reflectionClass = new \ReflectionClass($bootloader);
293
294 612
        $methodsDeps = [];
295
296 612
        foreach ($methods as $method) {
297 612
            if ($reflectionClass->hasMethod($method)) {
298 582
                $methodsDeps[] = $this->findBootloaderClassesInMethod(
299 582
                    $reflectionClass->getMethod($method),
300 582
                );
301
            }
302
        }
303
304 612
        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 582
    private function findBootloaderClassesInMethod(\ReflectionMethod $method): array
311
    {
312 582
        $args = [];
313 582
        foreach ($method->getParameters() as $parameter) {
314 582
            $type = $parameter->getType();
315 582
            if ($type instanceof \ReflectionNamedType && $this->shouldBeBooted($type)) {
316 461
                $args[] = $type->getName();
317
            }
318
        }
319
320 582
        return $args;
321
    }
322
}
323