Passed
Pull Request — master (#855)
by Alexander
06:24
created

AbstractKernel::addDispatcher()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
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 340
    protected function __construct(
69
        protected readonly Container $container,
70
        protected readonly ExceptionHandlerInterface $exceptionHandler,
71
        protected readonly BootloadManagerInterface $bootloader,
72
        array $directories
73
    ) {
74 340
        $container->bindSingleton(ExceptionHandlerInterface::class, $exceptionHandler);
75 340
        $container->bindSingleton(ExceptionRendererInterface::class, $exceptionHandler);
76 340
        $container->bindSingleton(ExceptionReporterInterface::class, $exceptionHandler);
77 340
        $container->bindSingleton(ExceptionHandler::class, $exceptionHandler);
78 340
        $container->bindSingleton(KernelInterface::class, $this);
79
80 340
        $container->bindSingleton(self::class, $this);
81 340
        $container->bindSingleton(static::class, $this);
82
83 340
        $container->bindSingleton(
84 340
            DirectoriesInterface::class,
85 340
            new Directories($this->mapDirectories($directories))
86 340
        );
87
88 339
        $this->finalizer = new Finalizer();
89 339
        $container->bindSingleton(FinalizerInterface::class, $this->finalizer);
90
91 339
        $this->bootloader->bootload($this->defineSystemBootloaders());
92
    }
93
94
    /**
95
     * Terminate the application.
96
     */
97
    public function __destruct()
98
    {
99
        $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 340
    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 340
        $exceptionHandler ??= ExceptionHandler::class;
117
118 340
        if (\is_string($exceptionHandler)) {
119 340
            $exceptionHandler = $container->make($exceptionHandler);
120
        }
121
122 340
        if ($handleErrors) {
123 37
            $exceptionHandler->register();
124
        }
125
126 340
        if (!$container->has(InitializerInterface::class)) {
127 339
            $container->bind(InitializerInterface::class, Initializer::class);
128
        }
129 340
        if (!$container->has(InvokerStrategyInterface::class)) {
130 339
            $container->bind(InvokerStrategyInterface::class, DefaultInvokerStrategy::class);
131
        }
132
133 340
        if ($bootloadManager instanceof Autowire) {
134 1
            $bootloadManager = $bootloadManager->resolve($container);
135
        }
136 340
        $bootloadManager ??= $container->make(StrategyBasedBootloadManager::class);
137 340
        \assert($bootloadManager instanceof BootloadManagerInterface);
138 340
        $container->bind(BootloadManagerInterface::class, $bootloadManager);
139
140 340
        return new static(
141 340
            $container,
142 340
            $exceptionHandler,
143 340
            $bootloadManager,
144 340
            $directories
145 340
        );
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 334
    public function run(?EnvironmentInterface $environment = null): ?self
161
    {
162 334
        $environment ??= new Environment();
163 334
        $this->container->bindSingleton(EnvironmentInterface::class, $environment);
164
165 334
        $this->fireCallbacks($this->runningCallbacks);
166
167
        try {
168
            // will protect any against env overwrite action
169 334
            $this->container->runScope(
170 334
                [EnvironmentInterface::class => $environment],
171 334
                function (): void {
172 334
                    $this->bootload();
173 334
                    $this->bootstrap();
174
175 334
                    $this->fireCallbacks($this->bootstrappedCallbacks);
176 334
                }
177 334
            );
178
        } catch (\Throwable $e) {
179
            $this->exceptionHandler->handleGlobalException($e);
180
181
            return null;
182
        }
183
184 334
        $this->getEventDispatcher()?->dispatch(new Bootstrapped($this));
185
186 334
        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 300
    public function booting(Closure ...$callbacks): void
213
    {
214 300
        foreach ($callbacks as $callback) {
215 300
            $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 334
    public function booted(Closure ...$callbacks): void
228
    {
229 334
        foreach ($callbacks as $callback) {
230 334
            $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 299
    public function bootstrapped(Closure ...$callbacks): void
244
    {
245 299
        foreach ($callbacks as $callback) {
246 299
            $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 302
    public function addDispatcher(DispatcherInterface $dispatcher): self
255
    {
256 302
        $this->dispatchers[] = $dispatcher;
257
258 302
        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 339
    protected function defineSystemBootloaders(): array
306
    {
307 339
        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 334
    protected function defineBootloaders(): array
316
    {
317 334
        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 334
    protected function fireCallbacks(array &$callbacks): void
324
    {
325 334
        if ($callbacks === []) {
326 334
            return;
327
        }
328
329
        do {
330 334
            $this->container->invoke(\current($callbacks));
331 334
        } while (\next($callbacks));
332
333 334
        $callbacks = [];
334
    }
335
336
    /**
337
     * Bootload all registered classes using BootloadManager.
338
     */
339 334
    private function bootload(): void
340
    {
341 334
        $self = $this;
342 334
        $this->bootloader->bootload(
343 334
            $this->defineBootloaders(),
344 334
            [
345 334
                static function () use ($self): void {
346 334
                    $self->fireCallbacks($self->bootingCallbacks);
347 334
                },
348 334
            ]
349 334
        );
350
351 334
        $this->fireCallbacks($this->bootedCallbacks);
352
    }
353
354 334
    private function getEventDispatcher(): ?EventDispatcherInterface
355
    {
356 334
        return $this->container->has(EventDispatcherInterface::class)
357 2
            ? $this->container->get(EventDispatcherInterface::class)
358 334
            : null;
359
    }
360
}
361