Passed
Pull Request — master (#840)
by Maxim
06:37
created

AbstractKernel   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 306
Duplicated Lines 0 %

Test Coverage

Coverage 96.51%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 27
eloc 90
c 4
b 1
f 0
dl 0
loc 306
ccs 83
cts 86
cp 0.9651
rs 10

14 Methods

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