Passed
Push — master ( 477224...3259b8 )
by Aleksei
06:12 queued 18s
created

AbstractKernel::initBootloaderRegistry()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Boot;
6
7
use Psr\EventDispatcher\EventDispatcherInterface;
8
use Spiral\Attribute\DispatcherScope;
9
use Spiral\Boot\Bootloader\BootloaderRegistry;
10
use Spiral\Boot\Bootloader\BootloaderRegistryInterface;
11
use Spiral\Boot\Bootloader\CoreBootloader;
12
use Spiral\Boot\BootloadManager\AttributeResolver;
13
use Spiral\Boot\BootloadManager\AttributeResolverRegistryInterface;
14
use Spiral\Boot\BootloadManager\StrategyBasedBootloadManager;
15
use Spiral\Boot\BootloadManager\DefaultInvokerStrategy;
16
use Spiral\Boot\BootloadManager\Initializer;
17
use Spiral\Boot\BootloadManager\InitializerInterface;
18
use Spiral\Boot\BootloadManager\InvokerStrategyInterface;
19
use Spiral\Boot\Event\Bootstrapped;
20
use Spiral\Boot\Event\DispatcherFound;
21
use Spiral\Boot\Event\DispatcherNotFound;
22
use Spiral\Boot\Event\Serving;
23
use Spiral\Boot\Exception\BootException;
24
use Spiral\Core\Container\Autowire;
25
use Spiral\Core\Container;
26
use Spiral\Core\Scope;
27
use Spiral\Exceptions\ExceptionHandler;
28
use Spiral\Exceptions\ExceptionHandlerInterface;
29
use Spiral\Exceptions\ExceptionRendererInterface;
30
use Spiral\Exceptions\ExceptionReporterInterface;
31
32
/**
33
 * Core responsible for application initialization, bootloading of all required services,
34
 * environment and directory management, exception handling.
35
 */
36
abstract class AbstractKernel implements KernelInterface
37
{
38
    /**
39
     * Defines list of bootloaders to be used for core initialisation and all system components.
40
     *
41
     * @deprecated Use {@see defineSystemBootloaders()} method instead. Will be removed in v4.0
42
     */
43
    protected const SYSTEM = [CoreBootloader::class];
44
45
    /**
46
     * List of bootloaders to be called on application initialization (before `serve` method).
47
     * This constant must be redefined in child application.
48
     *
49
     * @deprecated Use {@see defineBootloaders()} method instead. Will be removed in v4.0
50
     */
51
    protected const LOAD = [];
52
53
    protected FinalizerInterface $finalizer;
54
55
    /**
56
     * @internal
57
     * @var array<class-string<DispatcherInterface>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<DispatcherInterface>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<DispatcherInterface>>.
Loading history...
58
     */
59
    protected array $dispatchers = [];
60
61
    /** @var array<\Closure> */
62
    private array $runningCallbacks = [];
63
64
    /** @var array<\Closure> */
65
    private array $bootingCallbacks = [];
66
67
    /** @var array<\Closure> */
68
    private array $bootedCallbacks = [];
69
70
    /** @var array<\Closure>  */
71
    private array $bootstrappedCallbacks = [];
72
73
    /**
74
     * @throws \Throwable
75
     */
76 556
    protected function __construct(
77
        protected readonly Container $container,
78
        protected readonly ExceptionHandlerInterface $exceptionHandler,
79
        protected readonly BootloadManagerInterface $bootloader,
80
        array $directories,
81
    ) {
82 556
        $container->bindSingleton(ExceptionHandlerInterface::class, $exceptionHandler);
83 556
        $container->bindSingleton(ExceptionRendererInterface::class, $exceptionHandler);
84 556
        $container->bindSingleton(ExceptionReporterInterface::class, $exceptionHandler);
85 556
        $container->bindSingleton(ExceptionHandler::class, $exceptionHandler);
86 556
        $container->bindSingleton(KernelInterface::class, $this);
87
88 556
        $container->bindSingleton(self::class, $this);
89 556
        $container->bindSingleton(static::class, $this);
90
91 556
        $container->bindSingleton(
92 556
            DirectoriesInterface::class,
93 556
            new Directories($this->mapDirectories($directories)),
94 556
        );
95
96 555
        $this->finalizer = new Finalizer();
97 555
        $container->bindSingleton(FinalizerInterface::class, $this->finalizer);
98
    }
99
100
    /**
101
     * Create an application instance.
102
     *
103
     * @param class-string<ExceptionHandlerInterface>|ExceptionHandlerInterface $exceptionHandler
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<ExceptionHa...ceptionHandlerInterface at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<ExceptionHandlerInterface>|ExceptionHandlerInterface.
Loading history...
104
     *
105
     * @throws \Throwable
106
     */
107 556
    final public static function create(
108
        array $directories,
109
        bool $handleErrors = true,
110
        ExceptionHandlerInterface|string|null $exceptionHandler = null,
111
        Container $container = new Container(),
112
        BootloadManagerInterface|Autowire|null $bootloadManager = null,
113
    ): static {
114 556
        $exceptionHandler ??= ExceptionHandler::class;
115
116 556
        if (\is_string($exceptionHandler)) {
117 556
            $exceptionHandler = $container->make($exceptionHandler);
118
        }
119
120 556
        if ($handleErrors) {
121 39
            $exceptionHandler->register();
122
        }
123
124 556
        $container->bind(AttributeResolverRegistryInterface::class, AttributeResolver::class);
125
126 556
        if (!$container->has(InitializerInterface::class)) {
127 555
            $container->bind(InitializerInterface::class, Initializer::class);
128
        }
129
130 556
        if (!$container->has(InvokerStrategyInterface::class)) {
131 555
            $container->bind(InvokerStrategyInterface::class, DefaultInvokerStrategy::class);
132
        }
133
134 556
        if ($bootloadManager instanceof Autowire) {
135 1
            $bootloadManager = $bootloadManager->resolve($container);
136
        }
137 556
        $bootloadManager ??= $container->make(StrategyBasedBootloadManager::class);
138 556
        \assert($bootloadManager instanceof BootloadManagerInterface);
139 556
        $container->bind(BootloadManagerInterface::class, $bootloadManager);
140
141 556
        if (!$container->has(BootloaderRegistryInterface::class)) {
142
            /** @psalm-suppress InvalidArgument */
143 556
            $container->bindSingleton(BootloaderRegistryInterface::class, [self::class, 'initBootloaderRegistry']);
144
        }
145
146 556
        return new static(
147 556
            $container,
148 556
            $exceptionHandler,
149 556
            $bootloadManager,
150 556
            $directories,
151 556
        );
152
    }
153
154
    /**
155
     * Run the application with given Environment
156
     *
157
     *      $app = App::create([...]);
158
     *      $app->booting(...);
159
     *      $app->booted(...);
160
     *      $app->bootstrapped(...);
161
     *      $app->run(new Environment([
162
     *          'APP_ENV' => 'production'
163
     *      ]));
164
     */
165 550
    public function run(?EnvironmentInterface $environment = null): ?self
166
    {
167 550
        $environment ??= new Environment();
168 550
        $this->container->bindSingleton(EnvironmentInterface::class, $environment);
169
170
        try {
171
            // will protect any against env overwrite action
172 550
            $this->container->runScope(
173 550
                [EnvironmentInterface::class => $environment],
174 550
                function (Container $container): void {
175 550
                    $registry = $container->get(BootloaderRegistryInterface::class);
176
177
                    /** @psalm-suppress TooManyArguments */
178 550
                    $this->bootloader->bootload($registry->getSystemBootloaders(), [], [], false);
0 ignored issues
show
Unused Code introduced by
The call to Spiral\Boot\BootloadManagerInterface::bootload() has too many arguments starting with false. ( Ignorable by Annotation )

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

178
                    $this->bootloader->/** @scrutinizer ignore-call */ 
179
                                       bootload($registry->getSystemBootloaders(), [], [], false);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
179 550
                    $this->fireCallbacks($this->runningCallbacks);
180
181 550
                    $this->bootload($registry->getBootloaders());
182 550
                    $this->bootstrap();
183
184 550
                    $this->fireCallbacks($this->bootstrappedCallbacks);
185 550
                },
186 550
            );
187
        } catch (\Throwable $e) {
188
            $this->exceptionHandler->handleGlobalException($e);
189
190
            return null;
191
        }
192
193 550
        $this->getEventDispatcher()?->dispatch(new Bootstrapped($this));
194
195 550
        return $this;
196
    }
197
198
    /**
199
     * Register a new callback, that will be fired before framework run.
200
     * (After SYSTEM bootloaders, before bootloaders in LOAD section)
201
     *
202
     * $kernel->running(static function(KernelInterface $kernel) {
203
     *     $kernel->getContainer()->...
204
     * });
205
     */
206 1
    public function running(\Closure ...$callbacks): void
207
    {
208 1
        foreach ($callbacks as $callback) {
209 1
            $this->runningCallbacks[] = $callback;
210
        }
211
    }
212
213
    /**
214
     * Register a new callback, that will be fired before framework bootloaders boot.
215
     * (Before all framework bootloaders in LOAD section will be booted)
216
     *
217
     * $kernel->booting(static function(KernelInterface $kernel) {
218
     *     $kernel->getContainer()->...
219
     * });
220
     */
221 550
    public function booting(\Closure ...$callbacks): void
222
    {
223 550
        foreach ($callbacks as $callback) {
224 550
            $this->bootingCallbacks[] = $callback;
225
        }
226
    }
227
228
    /**
229
     * Register a new callback, that will be fired after framework bootloaders booted.
230
     * (After booting all framework bootloaders in LOAD section)
231
     *
232
     * $kernel->booted(static function(KernelInterface $kernel) {
233
     *     $kernel->getContainer()->...
234
     * });
235
     */
236 486
    public function booted(\Closure ...$callbacks): void
237
    {
238 486
        foreach ($callbacks as $callback) {
239 486
            $this->bootedCallbacks[] = $callback;
240
        }
241
    }
242
243
    /**
244
     * Register a new callback, that will be fired after framework bootstrapped.
245
     * (Before serving)
246
     *
247
     * $kernel->bootstrapped(static function(KernelInterface $kernel) {
248
     *     $kernel->getContainer()->...
249
     * });
250
     */
251 429
    public function bootstrapped(\Closure ...$callbacks): void
252
    {
253 429
        foreach ($callbacks as $callback) {
254 429
            $this->bootstrappedCallbacks[] = $callback;
255
        }
256
    }
257
258
    /**
259
     * Add new dispatcher. This method must only be called before method `serve`
260
     * will be invoked.
261
     *
262
     * @param class-string<DispatcherInterface>|DispatcherInterface $dispatcher The class name or instance
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<DispatcherI...ce>|DispatcherInterface at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<DispatcherInterface>|DispatcherInterface.
Loading history...
263
     * of the dispatcher. Since v4.0, it will only accept the class name.
264
     */
265 433
    public function addDispatcher(string|DispatcherInterface $dispatcher): self
266
    {
267 433
        if (\is_object($dispatcher)) {
268 4
            $dispatcher = $dispatcher::class;
269
        }
270
271 433
        $this->dispatchers[] = $dispatcher;
272
273 433
        return $this;
274
    }
275
276
    /**
277
     * Start application and serve user requests using selected dispatcher or throw
278
     * an exception.
279
     *
280
     * @throws BootException
281
     * @throws \Throwable
282
     */
283 13
    public function serve(): mixed
284
    {
285 13
        $eventDispatcher = $this->getEventDispatcher();
286 13
        $eventDispatcher?->dispatch(new Serving());
287
288 13
        $serving = $servingScope = null;
289 13
        foreach ($this->dispatchers as $dispatcher) {
290 11
            $reflection = new \ReflectionClass($dispatcher);
291
292 11
            $scope = ($reflection->getAttributes(DispatcherScope::class)[0] ?? null)?->newInstance()->scope;
293 11
            $this->container->getBinder($scope)->bind($dispatcher, $dispatcher);
294
295 11
            if ($serving === null && $this->canServe($reflection)) {
296 11
                $serving = $dispatcher;
297 11
                $servingScope = $scope;
298
            }
299
        }
300
301 13
        if ($serving === null) {
302 2
            $eventDispatcher?->dispatch(new DispatcherNotFound());
303 2
            throw new BootException('Unable to locate active dispatcher.');
304
        }
305
306 11
        return $this->container->runScope(
307 11
            new Scope(name: $servingScope, bindings: [DispatcherInterface::class => $serving]),
308 11
            static function (DispatcherInterface $dispatcher) use ($eventDispatcher): mixed {
309 11
                $eventDispatcher?->dispatch(new DispatcherFound($dispatcher));
310 11
                return $dispatcher->serve();
311 11
            },
312 11
        );
313
    }
314
315
    /**
316
     * Terminate the application.
317
     */
318 87
    public function __destruct()
319
    {
320 87
        $this->finalizer->finalize(true);
321
    }
322
323
    /**
324
     * Bootstrap application. Must be executed before serve method.
325
     */
326
    abstract protected function bootstrap(): void;
327
328
    /**
329
     * Normalizes directory list and adds all required aliases.
330
     */
331
    abstract protected function mapDirectories(array $directories): array;
332
333
    /**
334
     * Get list of defined system bootloaders
335
     *
336
     * @return array<int, class-string>|array<class-string, array<non-empty-string, mixed>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int, class-string>...n-empty-string, mixed>> at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array<int, class-string>|array<class-string, array<non-empty-string, mixed>>.
Loading history...
337
     */
338 550
    protected function defineSystemBootloaders(): array
339
    {
340 550
        return static::SYSTEM;
0 ignored issues
show
Deprecated Code introduced by
The constant Spiral\Boot\AbstractKernel::SYSTEM has been deprecated: Use {@see defineSystemBootloaders()} method instead. Will be removed in v4.0 ( Ignorable by Annotation )

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

340
        return /** @scrutinizer ignore-deprecated */ static::SYSTEM;

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
341
    }
342
343
    /**
344
     * Get list of defined kernel bootloaders
345
     *
346
     * @return array<int, class-string>|array<class-string, array<non-empty-string, mixed>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int, class-string>...n-empty-string, mixed>> at position 4 could not be parsed: Unknown type name 'class-string' at position 4 in array<int, class-string>|array<class-string, array<non-empty-string, mixed>>.
Loading history...
347
     */
348 101
    protected function defineBootloaders(): array
349
    {
350 101
        return static::LOAD;
0 ignored issues
show
Deprecated Code introduced by
The constant Spiral\Boot\AbstractKernel::LOAD has been deprecated: Use {@see defineBootloaders()} method instead. Will be removed in v4.0 ( Ignorable by Annotation )

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

350
        return /** @scrutinizer ignore-deprecated */ static::LOAD;

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
351
    }
352
353
    /**
354
     * Call the registered booting callbacks.
355
     */
356 550
    protected function fireCallbacks(array &$callbacks): void
357
    {
358 550
        if ($callbacks === []) {
359 550
            return;
360
        }
361
362
        do {
363 550
            $this->container->invoke(\current($callbacks));
364 550
        } while (\next($callbacks));
365
366 550
        $callbacks = [];
367
    }
368
369
    /**
370
     * Bootload all registered classes using BootloadManager.
371
     */
372 550
    private function bootload(array $bootloaders = []): void
373
    {
374 550
        $self = $this;
375 550
        $this->bootloader->bootload(
376 550
            $bootloaders,
377 550
            [
378 550
                static function () use ($self): void {
379 550
                    $self->fireCallbacks($self->bootingCallbacks);
380 550
                },
381 550
            ],
382 550
        );
383
384 550
        $this->fireCallbacks($this->bootedCallbacks);
385
    }
386
387 550
    private function getEventDispatcher(): ?EventDispatcherInterface
388
    {
389 550
        return $this->container->has(EventDispatcherInterface::class)
390 2
            ? $this->container->get(EventDispatcherInterface::class)
391 550
            : null;
392
    }
393
394 550
    private function initBootloaderRegistry(): BootloaderRegistryInterface
395
    {
396 550
        return new BootloaderRegistry($this->defineSystemBootloaders(), $this->defineBootloaders());
397
    }
398
399
    /**
400
     * @throws BootException
401
     */
402 11
    private function canServe(\ReflectionClass $reflection): bool
403
    {
404 11
        if (!$reflection->hasMethod('canServe')) {
405
            throw new BootException('Dispatcher must implement static `canServe` method.');
406
        }
407
408 11
        return $this->container->invoke([$reflection->getName(), 'canServe']);
409
    }
410
}
411