Passed
Push — master ( 2ba06e...0c2849 )
by butschster
06:56
created

AbstractKernel   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 328
Duplicated Lines 0 %

Test Coverage

Coverage 97.25%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 30
eloc 95
c 4
b 1
f 0
dl 0
loc 328
ccs 106
cts 109
cp 0.9725
rs 10

15 Methods

Rating   Name   Duplication   Size   Complexity  
A bootstrapped() 0 4 2
A running() 0 4 2
A booted() 0 4 2
A fireCallbacks() 0 11 3
A defineSystemBootloaders() 0 3 1
A addDispatcher() 0 5 1
A getEventDispatcher() 0 5 2
A booting() 0 4 2
A __destruct() 0 3 1
A defineBootloaders() 0 3 1
A create() 0 36 6
A bootload() 0 13 1
A __construct() 0 24 1
A serve() 0 20 3
A run() 0 27 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\Boot\Bootloader\CoreBootloader;
10
use Spiral\Boot\BootloadManager\StrategyBasedBootloadManager;
11
use Spiral\Boot\BootloadManager\DefaultInvokerStrategy;
12
use Spiral\Boot\BootloadManager\Initializer;
13
use Spiral\Boot\BootloadManager\InitializerInterface;
14
use Spiral\Boot\BootloadManager\InvokerStrategyInterface;
15
use Spiral\Boot\Event\Bootstrapped;
16
use Spiral\Boot\Event\DispatcherFound;
17
use Spiral\Boot\Event\DispatcherNotFound;
18
use Spiral\Boot\Event\Serving;
19
use Spiral\Boot\Exception\BootException;
20
use Spiral\Core\Container\Autowire;
21
use Spiral\Core\Container;
22
use Spiral\Exceptions\ExceptionHandler;
23
use Spiral\Exceptions\ExceptionHandlerInterface;
24
use Spiral\Exceptions\ExceptionRendererInterface;
25
use Spiral\Exceptions\ExceptionReporterInterface;
26
27
/**
28
 * Core responsible for application initialization, bootloading of all required services,
29
 * environment and directory management, exception handling.
30
 */
31
abstract class AbstractKernel implements KernelInterface
32
{
33
    /**
34
     * Defines list of bootloaders to be used for core initialisation and all system components.
35
     *
36
     * @deprecated since v4.0. Use {@see defineSystemBootloaders()} method instead.
37
     */
38
    protected const SYSTEM = [CoreBootloader::class];
39
40
    /**
41
     * List of bootloaders to be called on application initialization (before `serve` method).
42
     * This constant must be redefined in child application.
43
     *
44
     * @deprecated since v4.0. Use {@see defineBootloaders()} method instead.
45
     */
46
    protected const LOAD = [];
47
48
    protected FinalizerInterface $finalizer;
49
50
    /** @var DispatcherInterface[] */
51
    protected array $dispatchers = [];
52
53
    /** @var array<Closure> */
54
    private array $runningCallbacks = [];
55
56
    /** @var array<Closure> */
57
    private array $bootingCallbacks = [];
58
59
    /** @var array<Closure> */
60
    private array $bootedCallbacks = [];
61
62
    /** @var array<Closure>  */
63
    private array $bootstrappedCallbacks = [];
64
65
    /**
66
     * @throws \Throwable
67
     */
68 335
    protected function __construct(
69
        protected readonly Container $container,
70
        protected readonly ExceptionHandlerInterface $exceptionHandler,
71
        protected readonly BootloadManagerInterface $bootloader,
72
        array $directories
73
    ) {
74 335
        $container->bindSingleton(ExceptionHandlerInterface::class, $exceptionHandler);
75 335
        $container->bindSingleton(ExceptionRendererInterface::class, $exceptionHandler);
76 335
        $container->bindSingleton(ExceptionReporterInterface::class, $exceptionHandler);
77 335
        $container->bindSingleton(ExceptionHandler::class, $exceptionHandler);
78 335
        $container->bindSingleton(KernelInterface::class, $this);
79
80 335
        $container->bindSingleton(self::class, $this);
81 335
        $container->bindSingleton(static::class, $this);
82
83 335
        $container->bindSingleton(
84 335
            DirectoriesInterface::class,
85 335
            new Directories($this->mapDirectories($directories))
86 335
        );
87
88 334
        $this->finalizer = new Finalizer();
89 334
        $container->bindSingleton(FinalizerInterface::class, $this->finalizer);
90
91 334
        $this->bootloader->bootload($this->defineSystemBootloaders());
92
    }
93
94
    /**
95
     * Terminate the application.
96
     */
97 2
    public function __destruct()
98
    {
99 2
        $this->finalizer->finalize(true);
100
    }
101
102
    /**
103
     * Create an application instance.
104
     *
105
     * @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...
106
     *
107
     * @throws \Throwable
108
     */
109 335
    final public static function create(
110
        array $directories,
111
        bool $handleErrors = true,
112
        ExceptionHandlerInterface|string|null $exceptionHandler = null,
113
        Container $container = new Container(),
114
        BootloadManagerInterface|Autowire|null $bootloadManager = null
115
    ): static {
116 335
        $exceptionHandler ??= ExceptionHandler::class;
117
118 335
        if (\is_string($exceptionHandler)) {
119 335
            $exceptionHandler = $container->make($exceptionHandler);
120
        }
121
122 335
        if ($handleErrors) {
123 37
            $exceptionHandler->register();
124
        }
125
126 335
        if (!$container->has(InitializerInterface::class)) {
127 334
            $container->bind(InitializerInterface::class, Initializer::class);
128
        }
129 335
        if (!$container->has(InvokerStrategyInterface::class)) {
130 334
            $container->bind(InvokerStrategyInterface::class, DefaultInvokerStrategy::class);
131
        }
132
133 335
        if ($bootloadManager instanceof Autowire) {
134 1
            $bootloadManager = $bootloadManager->resolve($container);
135
        }
136 335
        $bootloadManager ??= $container->make(StrategyBasedBootloadManager::class);
137 335
        \assert($bootloadManager instanceof BootloadManagerInterface);
138 335
        $container->bind(BootloadManagerInterface::class, $bootloadManager);
139
140 335
        return new static(
141 335
            $container,
142 335
            $exceptionHandler,
143 335
            $bootloadManager,
144 335
            $directories
145 335
        );
146
    }
147
148
    /**
149
     * Run the application with given Environment
150
     *
151
     * $app = App::create([...]);
152
     * $app->booting(...);
153
     * $app->booted(...);
154
     * $app->bootstrapped(...);
155
     * $app->run(new Environment([
156
     *     'APP_ENV' => 'production'
157
     * ]));
158
     *
159
     */
160 329
    public function run(?EnvironmentInterface $environment = null): ?self
161
    {
162 329
        $environment ??= new Environment();
163 329
        $this->container->bindSingleton(EnvironmentInterface::class, $environment);
164
165 329
        $this->fireCallbacks($this->runningCallbacks);
166
167
        try {
168
            // will protect any against env overwrite action
169 329
            $this->container->runScope(
170 329
                [EnvironmentInterface::class => $environment],
171 329
                function (): void {
172 329
                    $this->bootload();
173 329
                    $this->bootstrap();
174
175 329
                    $this->fireCallbacks($this->bootstrappedCallbacks);
176 329
                }
177 329
            );
178
        } catch (\Throwable $e) {
179
            $this->exceptionHandler->handleGlobalException($e);
180
181
            return null;
182
        }
183
184 329
        $this->getEventDispatcher()?->dispatch(new Bootstrapped($this));
185
186 329
        return $this;
187
    }
188
189
    /**
190
     * Register a new callback, that will be fired before framework run.
191
     * (After SYSTEM bootloaders, before bootloaders in LOAD section)
192
     *
193
     * $kernel->running(static function(KernelInterface $kernel) {
194
     *     $kernel->getContainer()->...
195
     * });
196
     */
197 1
    public function running(Closure ...$callbacks): void
198
    {
199 1
        foreach ($callbacks as $callback) {
200 1
            $this->runningCallbacks[] = $callback;
201
        }
202
    }
203
204
    /**
205
     * Register a new callback, that will be fired before framework bootloaders boot.
206
     * (Before all framework bootloaders in LOAD section will be booted)
207
     *
208
     * $kernel->booting(static function(KernelInterface $kernel) {
209
     *     $kernel->getContainer()->...
210
     * });
211
     */
212 295
    public function booting(Closure ...$callbacks): void
213
    {
214 295
        foreach ($callbacks as $callback) {
215 295
            $this->bootingCallbacks[] = $callback;
216
        }
217
    }
218
219
    /**
220
     * Register a new callback, that will be fired after framework bootloaders booted.
221
     * (After booting all framework bootloaders in LOAD section)
222
     *
223
     * $kernel->booted(static function(KernelInterface $kernel) {
224
     *     $kernel->getContainer()->...
225
     * });
226
     */
227 329
    public function booted(Closure ...$callbacks): void
228
    {
229 329
        foreach ($callbacks as $callback) {
230 329
            $this->bootedCallbacks[] = $callback;
231
        }
232
    }
233
234
235
    /**
236
     * Register a new callback, that will be fired after framework bootstrapped.
237
     * (Before serving)
238
     *
239
     * $kernel->bootstrapped(static function(KernelInterface $kernel) {
240
     *     $kernel->getContainer()->...
241
     * });
242
     */
243 294
    public function bootstrapped(Closure ...$callbacks): void
244
    {
245 294
        foreach ($callbacks as $callback) {
246 294
            $this->bootstrappedCallbacks[] = $callback;
247
        }
248
    }
249
250
    /**
251
     * Add new dispatcher. This method must only be called before method `serve`
252
     * will be invoked.
253
     */
254 297
    public function addDispatcher(DispatcherInterface $dispatcher): self
255
    {
256 297
        $this->dispatchers[] = $dispatcher;
257
258 297
        return $this;
259
    }
260
261
    /**
262
     * Start application and serve user requests using selected dispatcher or throw
263
     * an exception.
264
     *
265
     * @throws BootException
266
     * @throws \Throwable
267
     */
268 5
    public function serve(): mixed
269
    {
270 5
        $eventDispatcher = $this->getEventDispatcher();
271 5
        $eventDispatcher?->dispatch(new Serving());
272
273 5
        foreach ($this->dispatchers as $dispatcher) {
274 3
            if ($dispatcher->canServe()) {
275 3
                return $this->container->runScope(
276 3
                    [DispatcherInterface::class => $dispatcher],
277 3
                    static function () use ($dispatcher, $eventDispatcher): mixed {
278 3
                        $eventDispatcher?->dispatch(new DispatcherFound($dispatcher));
279 3
                        return $dispatcher->serve();
280 3
                    }
281 3
                );
282
            }
283
        }
284
285 2
        $eventDispatcher?->dispatch(new DispatcherNotFound());
286
287 2
        throw new BootException('Unable to locate active dispatcher.');
288
    }
289
290
    /**
291
     * Bootstrap application. Must be executed before serve method.
292
     */
293
    abstract protected function bootstrap(): void;
294
295
    /**
296
     * Normalizes directory list and adds all required aliases.
297
     */
298
    abstract protected function mapDirectories(array $directories): array;
299
300
    /**
301
     * Get list of defined system bootloaders
302
     *
303
     * @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...
304
     */
305 334
    protected function defineSystemBootloaders(): array
306
    {
307 334
        return static::SYSTEM;
0 ignored issues
show
Deprecated Code introduced by
The constant Spiral\Boot\AbstractKernel::SYSTEM has been deprecated: since v4.0. Use {@see defineSystemBootloaders()} method instead. ( Ignorable by Annotation )

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

307
        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...
308
    }
309
310
    /**
311
     * Get list of defined kernel bootloaders
312
     *
313
     * @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...
314
     */
315 329
    protected function defineBootloaders(): array
316
    {
317 329
        return static::LOAD;
0 ignored issues
show
Deprecated Code introduced by
The constant Spiral\Boot\AbstractKernel::LOAD has been deprecated: since v4.0. Use {@see defineBootloaders()} method instead. ( Ignorable by Annotation )

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

317
        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...
318
    }
319
320
    /**
321
     * Call the registered booting callbacks.
322
     */
323 329
    protected function fireCallbacks(array &$callbacks): void
324
    {
325 329
        if ($callbacks === []) {
326 329
            return;
327
        }
328
329
        do {
330 329
            $this->container->invoke(\current($callbacks));
331 329
        } while (\next($callbacks));
332
333 329
        $callbacks = [];
334
    }
335
336
    /**
337
     * Bootload all registered classes using BootloadManager.
338
     */
339 329
    private function bootload(): void
340
    {
341 329
        $self = $this;
342 329
        $this->bootloader->bootload(
343 329
            $this->defineBootloaders(),
344 329
            [
345 329
                static function () use ($self): void {
346 329
                    $self->fireCallbacks($self->bootingCallbacks);
347 329
                },
348 329
            ]
349 329
        );
350
351 329
        $this->fireCallbacks($this->bootedCallbacks);
352
    }
353
354 329
    private function getEventDispatcher(): ?EventDispatcherInterface
355
    {
356 329
        return $this->container->has(EventDispatcherInterface::class)
357 2
            ? $this->container->get(EventDispatcherInterface::class)
358 329
            : null;
359
    }
360
}
361