Passed
Push — master ( 1a1904...88f463 )
by Aleksei
36:26 queued 25:05
created

Initializer::initDefaultChecker()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Boot\BootloadManager;
6
7
use Psr\Container\ContainerInterface;
8
use Spiral\Boot\Attribute\BootloadConfig;
9
use Spiral\Boot\Bootloader\BootloaderInterface;
10
use Spiral\Boot\Bootloader\DependedInterface;
11
use Spiral\Boot\BootloadManager\Checker\BootloaderChecker;
12
use Spiral\Boot\BootloadManager\Checker\BootloaderCheckerInterface;
13
use Spiral\Boot\BootloadManager\Checker\CanBootedChecker;
14
use Spiral\Boot\BootloadManager\Checker\CheckerRegistry;
15
use Spiral\Boot\BootloadManager\Checker\ClassExistsChecker;
16
use Spiral\Boot\BootloadManager\Checker\ConfigChecker;
17
use Spiral\Boot\BootloadManagerInterface;
18
use Spiral\Core\Attribute\Singleton;
19
use Spiral\Core\BinderInterface;
20
use Spiral\Core\ResolverInterface;
21
22
/**
23
 * @internal
24
 * @psalm-import-type TClass from BootloadManagerInterface
25
 * @psalm-import-type TFullBinding from BootloaderInterface
26
 */
27
#[Singleton]
28
class Initializer implements InitializerInterface
29
{
30
    protected ?BootloaderCheckerInterface $checker = null;
31
32 608
    public function __construct(
33
        protected readonly ContainerInterface $container,
34
        protected readonly BinderInterface $binder,
35
        protected readonly ClassesRegistry $bootloaders = new ClassesRegistry(),
36
        ?BootloaderCheckerInterface $checker = null,
0 ignored issues
show
Unused Code introduced by
The parameter $checker is not used and could be removed. ( Ignorable by Annotation )

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

36
        /** @scrutinizer ignore-unused */ ?BootloaderCheckerInterface $checker = null,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
37
    ) {
38 608
    }
39
40
    /**
41
     * Instantiate bootloader objects and resolve dependencies
42
     *
43
     * @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...
44
     */
45 599
    public function init(array $classes, bool $useConfig = true): \Generator
46
    {
47 599
        $this->checker ??= $this->initDefaultChecker();
48
49 599
        foreach ($classes as $bootloader => $options) {
50
            // default bootload syntax as simple array
51 599
            if (\is_string($options) || $options instanceof BootloaderInterface) {
52 582
                $bootloader = $options;
53 582
                $options = [];
54
            }
55 599
            $options = $useConfig ? $this->getBootloadConfig($bootloader, $options) : [];
56
57 599
            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

57
            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

57
            if (!$this->checker->canInitialize($bootloader, /** @scrutinizer ignore-type */ $useConfig ? $options : null)) {
Loading history...
58 436
                continue;
59
            }
60
61 578
            $this->bootloaders->register($bootloader instanceof BootloaderInterface ? $bootloader::class : $bootloader);
62
63 578
            if (!$bootloader instanceof BootloaderInterface) {
64 577
                $bootloader = $this->container->get($bootloader);
65
            }
66
67
            /** @var BootloaderInterface $bootloader */
68 578
            yield from $this->initBootloader($bootloader);
69 578
            yield $bootloader::class => [
70 578
                'bootloader' => $bootloader,
71 578
                'options' => $options instanceof BootloadConfig ? $options->args : $options,
72 578
            ];
73
        }
74
    }
75
76 9
    public function getRegistry(): ClassesRegistry
77
    {
78 9
        return $this->bootloaders;
79
    }
80
81
    /**
82
     * Resolve all bootloader dependencies and init bindings
83
     */
84 578
    protected function initBootloader(BootloaderInterface $bootloader): iterable
85
    {
86 578
        if ($bootloader instanceof DependedInterface) {
87 578
            yield from $this->init($this->getDependencies($bootloader));
88
        }
89
90 578
        $this->initBindings(
91 578
            $bootloader->defineBindings(),
92 578
            $bootloader->defineSingletons()
93 578
        );
94
    }
95
96
    /**
97
     * Bind declared bindings.
98
     *
99
     * @param TFullBinding $bindings
0 ignored issues
show
Bug introduced by
The type Spiral\Boot\BootloadManager\TFullBinding was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
100
     * @param TFullBinding $singletons
101
     */
102 578
    protected function initBindings(array $bindings, array $singletons): void
103
    {
104 578
        foreach ($bindings as $aliases => $resolver) {
105 506
            $this->binder->bind($aliases, $resolver);
106
        }
107
108 578
        foreach ($singletons as $aliases => $resolver) {
109 555
            $this->binder->bindSingleton($aliases, $resolver);
110
        }
111
    }
112
113 578
    protected function getDependencies(DependedInterface $bootloader): array
114
    {
115 578
        $deps = $bootloader->defineDependencies();
116
117 578
        $reflectionClass = new \ReflectionClass($bootloader);
118
119 578
        $methodsDeps = [];
120
121 578
        foreach (Methods::cases() as $method) {
122 578
            if ($reflectionClass->hasMethod($method->value)) {
123 555
                $methodsDeps[] = $this->findBootloaderClassesInMethod(
124 555
                    $reflectionClass->getMethod($method->value)
125 555
                );
126
            }
127
        }
128
129 578
        return \array_values(\array_unique(\array_merge($deps, ...$methodsDeps)));
130
    }
131
132 555
    protected function findBootloaderClassesInMethod(\ReflectionMethod $method): array
133
    {
134 555
        $args = [];
135 555
        foreach ($method->getParameters() as $parameter) {
136 555
            $type = $parameter->getType();
137 555
            if ($type instanceof \ReflectionNamedType && $this->shouldBeBooted($type)) {
138 459
                $args[] = $type->getName();
139
            }
140
        }
141
142 555
        return $args;
143
    }
144
145 555
    protected function shouldBeBooted(\ReflectionNamedType $type): bool
146
    {
147
        /** @var TClass $class */
148 555
        $class = $type->getName();
149
150 555
        return $this->isBootloader($class)
151 555
            && !$this->bootloaders->isBooted($class);
152
    }
153
154
    /**
155
     * @psalm-pure
156
     * @psalm-assert-if-true TClass $class
157
     */
158 555
    protected function isBootloader(string|object $class): bool
159
    {
160 555
        return \is_subclass_of($class, BootloaderInterface::class);
161
    }
162
163 599
    protected function initDefaultChecker(): BootloaderCheckerInterface
164
    {
165 599
        $registry = new CheckerRegistry();
166 599
        $registry->register($this->container->get(ConfigChecker::class));
167 599
        $registry->register(new ClassExistsChecker());
168 599
        $registry->register(new CanBootedChecker($this->bootloaders));
169
170 599
        return new BootloaderChecker($registry);
171
    }
172
173
    /**
174
     * Returns merged config. Attribute config have lower priority.
175
     *
176
     * @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...
177
     */
178 601
    private function getBootloadConfig(
179
        string|BootloaderInterface $bootloader,
180
        array|callable|BootloadConfig $config
181
    ): BootloadConfig {
182 601
        if ($config instanceof \Closure) {
0 ignored issues
show
introduced by
$config is never a sub-type of Closure.
Loading history...
183 2
            $config = $this->container instanceof ResolverInterface
184 2
                ? $config(...$this->container->resolveArguments(new \ReflectionFunction($config)))
185
                : $config();
186
        }
187 601
        $attr = $this->getBootloadConfigAttribute($bootloader);
188
189 601
        $getArgument = static fn (string $key, bool $override, mixed $default = []): mixed => match (true) {
190 601
            $config instanceof BootloadConfig && $override => $config->{$key},
191 583
            $config instanceof BootloadConfig && !$override && \is_array($default) =>
192 3
                $config->{$key} + ($attr->{$key} ?? []),
193 583
            $config instanceof BootloadConfig && !$override && \is_bool($default) => $config->{$key},
194 580
            \is_array($config) && $config !== [] && $key === 'args' => $config,
195 601
            default => $attr->{$key} ?? $default,
196 583
        };
197
198 601
        $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...
199
200 601
        return new BootloadConfig(
201 601
            args: $getArgument('args', $override),
202 601
            enabled: $getArgument('enabled', $override, true),
203 601
            allowEnv: $getArgument('allowEnv', $override),
204 601
            denyEnv: $getArgument('denyEnv', $override),
205 601
        );
206
    }
207
208
    /**
209
     * @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...
210
     */
211 601
    private function getBootloadConfigAttribute(string|BootloaderInterface $bootloader): ?BootloadConfig
212
    {
213 601
        $attribute = null;
214 601
        if ($bootloader instanceof BootloaderInterface || \class_exists($bootloader)) {
215 600
            $ref = new \ReflectionClass($bootloader);
216 600
            $attribute = $ref->getAttributes(BootloadConfig::class)[0] ?? null;
217
        }
218
219 601
        if ($attribute === null) {
220 582
            return null;
221
        }
222
223 22
        return $attribute->newInstance();
224
    }
225
}
226