Passed
Pull Request — master (#840)
by Maxim
10:02 queued 03:19
created

AbstractKernel   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 309
Duplicated Lines 0 %

Test Coverage

Coverage 96.59%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 28
eloc 92
c 4
b 1
f 0
dl 0
loc 309
ccs 85
cts 88
cp 0.9659
rs 10

14 Methods

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