Passed
Push — master ( 641be3...0c1911 )
by butschster
06:48
created

AbstractKernel::booted()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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