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

AbstractKernel   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Test Coverage

Coverage 96.88%

Importance

Changes 0
Metric Value
wmc 37
eloc 112
c 0
b 0
f 0
dl 0
loc 375
ccs 124
cts 128
cp 0.9688
rs 9.44

17 Methods

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

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

343
        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...
344
    }
345
346
    /**
347
     * Get list of defined kernel bootloaders
348
     *
349
     * @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...
350
     */
351 101
    protected function defineBootloaders(): array
352
    {
353 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

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