Passed
Pull Request — master (#1190)
by Aleksei
20:19 queued 07:41
created

AbstractKernel::addDispatcher()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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

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

340
        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...
341
    }
342
343
    /**
344
     * Get list of defined kernel bootloaders
345
     *
346
     * @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...
347
     */
348 101
    protected function defineBootloaders(): array
349
    {
350 101
        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

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