Passed
Push — master ( dd46fb...92bcfa )
by butschster
06:23 queued 19s
created

AbstractKernel::canServe()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 7
ccs 3
cts 4
cp 0.75
rs 10
cc 2
nc 2
nop 1
crap 2.0625
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\Attribute\DispatcherScope;
10
use Spiral\Boot\Bootloader\BootloaderRegistry;
11
use Spiral\Boot\Bootloader\BootloaderRegistryInterface;
12
use Spiral\Boot\Bootloader\CoreBootloader;
13
use Spiral\Boot\BootloadManager\StrategyBasedBootloadManager;
14
use Spiral\Boot\BootloadManager\DefaultInvokerStrategy;
15
use Spiral\Boot\BootloadManager\Initializer;
16
use Spiral\Boot\BootloadManager\InitializerInterface;
17
use Spiral\Boot\BootloadManager\InvokerStrategyInterface;
18
use Spiral\Boot\Event\Bootstrapped;
19
use Spiral\Boot\Event\DispatcherFound;
20
use Spiral\Boot\Event\DispatcherNotFound;
21
use Spiral\Boot\Event\Serving;
22
use Spiral\Boot\Exception\BootException;
23
use Spiral\Core\Container\Autowire;
24
use Spiral\Core\Container;
25
use Spiral\Core\Scope;
26
use Spiral\Exceptions\ExceptionHandler;
27
use Spiral\Exceptions\ExceptionHandlerInterface;
28
use Spiral\Exceptions\ExceptionRendererInterface;
29
use Spiral\Exceptions\ExceptionReporterInterface;
30
31
/**
32
 * Core responsible for application initialization, bootloading of all required services,
33
 * environment and directory management, exception handling.
34
 */
35
abstract class AbstractKernel implements KernelInterface
36
{
37
    /**
38
     * Defines list of bootloaders to be used for core initialisation and all system components.
39
     *
40
     * @deprecated Use {@see defineSystemBootloaders()} method instead. Will be removed in v4.0
41
     */
42
    protected const SYSTEM = [CoreBootloader::class];
43
44
    /**
45
     * List of bootloaders to be called on application initialization (before `serve` method).
46
     * This constant must be redefined in child application.
47
     *
48
     * @deprecated Use {@see defineBootloaders()} method instead. Will be removed in v4.0
49
     */
50
    protected const LOAD = [];
51
52
    protected FinalizerInterface $finalizer;
53
54
    /**
55
     * @internal
56
     * @var array<class-string<DispatcherInterface>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string<DispatcherInterface>> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string<DispatcherInterface>>.
Loading history...
57
     */
58
    protected array $dispatchers = [];
59
60
    /** @var array<Closure> */
61
    private array $runningCallbacks = [];
62
63
    /** @var array<Closure> */
64
    private array $bootingCallbacks = [];
65
66
    /** @var array<Closure> */
67
    private array $bootedCallbacks = [];
68
69
    /** @var array<Closure>  */
70
    private array $bootstrappedCallbacks = [];
71
72
    /**
73
     * @throws \Throwable
74
     */
75 436
    protected function __construct(
76
        protected readonly Container $container,
77
        protected readonly ExceptionHandlerInterface $exceptionHandler,
78
        protected readonly BootloadManagerInterface $bootloader,
79
        array $directories
80
    ) {
81 436
        $container->bindSingleton(ExceptionHandlerInterface::class, $exceptionHandler);
82 436
        $container->bindSingleton(ExceptionRendererInterface::class, $exceptionHandler);
83 436
        $container->bindSingleton(ExceptionReporterInterface::class, $exceptionHandler);
84 436
        $container->bindSingleton(ExceptionHandler::class, $exceptionHandler);
85 436
        $container->bindSingleton(KernelInterface::class, $this);
86
87 436
        $container->bindSingleton(self::class, $this);
88 436
        $container->bindSingleton(static::class, $this);
89
90 436
        $container->bindSingleton(
91 436
            DirectoriesInterface::class,
92 436
            new Directories($this->mapDirectories($directories))
93 436
        );
94
95 435
        $this->finalizer = new Finalizer();
96 435
        $container->bindSingleton(FinalizerInterface::class, $this->finalizer);
97
    }
98
99
    /**
100
     * Terminate the application.
101
     */
102 7
    public function __destruct()
103
    {
104 7
        $this->finalizer->finalize(true);
105
    }
106
107
    /**
108
     * Create an application instance.
109
     *
110
     * @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...
111
     *
112
     * @throws \Throwable
113
     */
114 436
    final public static function create(
115
        array $directories,
116
        bool $handleErrors = true,
117
        ExceptionHandlerInterface|string|null $exceptionHandler = null,
118
        Container $container = new Container(),
119
        BootloadManagerInterface|Autowire|null $bootloadManager = null
120
    ): static {
121 436
        $exceptionHandler ??= ExceptionHandler::class;
122
123 436
        if (\is_string($exceptionHandler)) {
124 436
            $exceptionHandler = $container->make($exceptionHandler);
125
        }
126
127 436
        if ($handleErrors) {
128 41
            $exceptionHandler->register();
129
        }
130
131 436
        if (!$container->has(InitializerInterface::class)) {
132 435
            $container->bind(InitializerInterface::class, Initializer::class);
133
        }
134 436
        if (!$container->has(InvokerStrategyInterface::class)) {
135 435
            $container->bind(InvokerStrategyInterface::class, DefaultInvokerStrategy::class);
136
        }
137
138 436
        if ($bootloadManager instanceof Autowire) {
139 1
            $bootloadManager = $bootloadManager->resolve($container);
140
        }
141 436
        $bootloadManager ??= $container->make(StrategyBasedBootloadManager::class);
142 436
        \assert($bootloadManager instanceof BootloadManagerInterface);
143 436
        $container->bind(BootloadManagerInterface::class, $bootloadManager);
144
145 436
        if (!$container->has(BootloaderRegistryInterface::class)) {
146 436
            $container->bindSingleton(BootloaderRegistryInterface::class, [self::class, 'initBootloaderRegistry']);
147
        }
148
149 436
        return new static(
150 436
            $container,
151 436
            $exceptionHandler,
152 436
            $bootloadManager,
153 436
            $directories
154 436
        );
155
    }
156
157
    /**
158
     * Run the application with given Environment
159
     *
160
     * $app = App::create([...]);
161
     * $app->booting(...);
162
     * $app->booted(...);
163
     * $app->bootstrapped(...);
164
     * $app->run(new Environment([
165
     *     'APP_ENV' => 'production'
166
     * ]));
167
     *
168
     */
169 430
    public function run(?EnvironmentInterface $environment = null): ?self
170
    {
171 430
        $environment ??= new Environment();
172 430
        $this->container->bindSingleton(EnvironmentInterface::class, $environment);
173
174
        try {
175
            // will protect any against env overwrite action
176 430
            $this->container->runScope(
177 430
                [EnvironmentInterface::class => $environment],
178 430
                function (Container $container): void {
179 430
                    $registry = $container->get(BootloaderRegistryInterface::class);
180
181
                    /** @psalm-suppress TooManyArguments */
182 430
                    $this->bootloader->bootload($registry->getSystemBootloaders(), [], [], false);
0 ignored issues
show
Unused Code introduced by
The call to Spiral\Boot\BootloadManagerInterface::bootload() has too many arguments starting with false. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

182
                    $this->bootloader->/** @scrutinizer ignore-call */ 
183
                                       bootload($registry->getSystemBootloaders(), [], [], false);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
183 430
                    $this->fireCallbacks($this->runningCallbacks);
184
185 430
                    $this->bootload($registry->getBootloaders());
186 430
                    $this->bootstrap();
187
188 430
                    $this->fireCallbacks($this->bootstrappedCallbacks);
189 430
                }
190 430
            );
191
        } catch (\Throwable $e) {
192
            $this->exceptionHandler->handleGlobalException($e);
193
194
            return null;
195
        }
196
197 430
        $this->getEventDispatcher()?->dispatch(new Bootstrapped($this));
198
199 430
        return $this;
200
    }
201
202
    /**
203
     * Register a new callback, that will be fired before framework run.
204
     * (After SYSTEM bootloaders, before bootloaders in LOAD section)
205
     *
206
     * $kernel->running(static function(KernelInterface $kernel) {
207
     *     $kernel->getContainer()->...
208
     * });
209
     */
210 1
    public function running(Closure ...$callbacks): void
211
    {
212 1
        foreach ($callbacks as $callback) {
213 1
            $this->runningCallbacks[] = $callback;
214
        }
215
    }
216
217
    /**
218
     * Register a new callback, that will be fired before framework bootloaders boot.
219
     * (Before all framework bootloaders in LOAD section will be booted)
220
     *
221
     * $kernel->booting(static function(KernelInterface $kernel) {
222
     *     $kernel->getContainer()->...
223
     * });
224
     */
225 430
    public function booting(Closure ...$callbacks): void
226
    {
227 430
        foreach ($callbacks as $callback) {
228 430
            $this->bootingCallbacks[] = $callback;
229
        }
230
    }
231
232
    /**
233
     * Register a new callback, that will be fired after framework bootloaders booted.
234
     * (After booting all framework bootloaders in LOAD section)
235
     *
236
     * $kernel->booted(static function(KernelInterface $kernel) {
237
     *     $kernel->getContainer()->...
238
     * });
239
     */
240 428
    public function booted(Closure ...$callbacks): void
241
    {
242 428
        foreach ($callbacks as $callback) {
243 428
            $this->bootedCallbacks[] = $callback;
244
        }
245
    }
246
247
248
    /**
249
     * Register a new callback, that will be fired after framework bootstrapped.
250
     * (Before serving)
251
     *
252
     * $kernel->bootstrapped(static function(KernelInterface $kernel) {
253
     *     $kernel->getContainer()->...
254
     * });
255
     */
256 391
    public function bootstrapped(Closure ...$callbacks): void
257
    {
258 391
        foreach ($callbacks as $callback) {
259 391
            $this->bootstrappedCallbacks[] = $callback;
260
        }
261
    }
262
263
    /**
264
     * Add new dispatcher. This method must only be called before method `serve`
265
     * will be invoked.
266
     *
267
     * @param class-string<DispatcherInterface>|DispatcherInterface $dispatcher The class name or instance
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<DispatcherI...ce>|DispatcherInterface at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<DispatcherInterface>|DispatcherInterface.
Loading history...
268
     * of the dispatcher. Since v4.0, it will only accept the class name.
269
     */
270 395
    public function addDispatcher(string|DispatcherInterface $dispatcher): self
271
    {
272 395
        if (\is_object($dispatcher)) {
273 4
            $dispatcher = $dispatcher::class;
274
        }
275
276 395
        $this->dispatchers[] = $dispatcher;
277
278 395
        return $this;
279
    }
280
281
    /**
282
     * Start application and serve user requests using selected dispatcher or throw
283
     * an exception.
284
     *
285
     * @throws BootException
286
     * @throws \Throwable
287
     */
288 13
    public function serve(): mixed
289
    {
290 13
        $eventDispatcher = $this->getEventDispatcher();
291 13
        $eventDispatcher?->dispatch(new Serving());
292
293 13
        $serving = $servingScope = null;
294 13
        foreach ($this->dispatchers as $dispatcher) {
295 11
            $reflection = new \ReflectionClass($dispatcher);
296
297 11
            $scope = ($reflection->getAttributes(DispatcherScope::class)[0] ?? null)?->newInstance()->scope;
298 11
            $this->container->getBinder($scope)->bind($dispatcher, $dispatcher);
299
300 11
            if ($serving === null && $this->canServe($reflection)) {
301 11
                $serving = $dispatcher;
302 11
                $servingScope = $scope;
303
            }
304
        }
305
306 13
        if ($serving === null) {
307 2
            $eventDispatcher?->dispatch(new DispatcherNotFound());
308 2
            throw new BootException('Unable to locate active dispatcher.');
309
        }
310
311 11
        return $this->container->runScope(
312 11
            new Scope(name: $servingScope, bindings: [DispatcherInterface::class => $serving]),
313 11
            static function (DispatcherInterface $dispatcher) use ($eventDispatcher): mixed {
314 11
                $eventDispatcher?->dispatch(new DispatcherFound($dispatcher));
315 11
                return $dispatcher->serve();
316 11
            }
317 11
        );
318
    }
319
320
    /**
321
     * Bootstrap application. Must be executed before serve method.
322
     */
323
    abstract protected function bootstrap(): void;
324
325
    /**
326
     * Normalizes directory list and adds all required aliases.
327
     */
328
    abstract protected function mapDirectories(array $directories): array;
329
330
    /**
331
     * Get list of defined system bootloaders
332
     *
333
     * @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...
334
     */
335 430
    protected function defineSystemBootloaders(): array
336
    {
337 430
        return static::SYSTEM;
0 ignored issues
show
Deprecated Code introduced by
The constant Spiral\Boot\AbstractKernel::SYSTEM has been deprecated: Use {@see defineSystemBootloaders()} method instead. Will be removed in v4.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

337
        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...
338
    }
339
340
    /**
341
     * Get list of defined kernel bootloaders
342
     *
343
     * @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...
344
     */
345 108
    protected function defineBootloaders(): array
346
    {
347 108
        return static::LOAD;
0 ignored issues
show
Deprecated Code introduced by
The constant Spiral\Boot\AbstractKernel::LOAD has been deprecated: Use {@see defineBootloaders()} method instead. Will be removed in v4.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

347
        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...
348
    }
349
350
    /**
351
     * Call the registered booting callbacks.
352
     */
353 430
    protected function fireCallbacks(array &$callbacks): void
354
    {
355 430
        if ($callbacks === []) {
356 430
            return;
357
        }
358
359
        do {
360 430
            $this->container->invoke(\current($callbacks));
361 430
        } while (\next($callbacks));
362
363 430
        $callbacks = [];
364
    }
365
366
    /**
367
     * Bootload all registered classes using BootloadManager.
368
     */
369 430
    private function bootload(array $bootloaders = []): void
370
    {
371 430
        $self = $this;
372 430
        $this->bootloader->bootload(
373 430
            $bootloaders,
374 430
            [
375 430
                static function () use ($self): void {
376 430
                    $self->fireCallbacks($self->bootingCallbacks);
377 430
                },
378 430
            ]
379 430
        );
380
381 430
        $this->fireCallbacks($this->bootedCallbacks);
382
    }
383
384 430
    private function getEventDispatcher(): ?EventDispatcherInterface
385
    {
386 430
        return $this->container->has(EventDispatcherInterface::class)
387 2
            ? $this->container->get(EventDispatcherInterface::class)
388 430
            : null;
389
    }
390
391 430
    private function initBootloaderRegistry(): BootloaderRegistryInterface
392
    {
393 430
        return new BootloaderRegistry($this->defineSystemBootloaders(), $this->defineBootloaders());
394
    }
395
396
    /**
397
     * @throws BootException
398
     */
399 11
    private function canServe(\ReflectionClass $reflection): bool
400
    {
401 11
        if (!$reflection->hasMethod('canServe')) {
402
            throw new BootException('Dispatcher must implement static `canServe` method.');
403
        }
404
405 11
        return $this->container->invoke([$reflection->getName(), 'canServe']);
406
    }
407
}
408