Passed
Pull Request — master (#831)
by Maxim
07:40
created

AbstractKernel::run()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 2.032

Importance

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