Skip to content

Commit

Permalink
Merge pull request #1015 from spiral/feature/bootloaders-registry
Browse files Browse the repository at this point in the history
[spiral/boot] Adding `BootloaderRegistry` to improve the bootloader registration process
  • Loading branch information
butschster committed Nov 17, 2023
2 parents 6714d80 + 9b1b16e commit c618ac7
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 5 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,11 @@
# CHANGELOG

## Unreleased

- **Other Features**
- [spiral/boot] Added `Spiral\Boot\Bootloader\BootloaderRegistryInterface` and `Spiral\Boot\Bootloader\BootloaderRegistry`
to allow for easier management of bootloaders.

## 3.9.0 - 2023-10-19

- **Other Features**
Expand Down
23 changes: 18 additions & 5 deletions src/Boot/src/AbstractKernel.php
Expand Up @@ -6,6 +6,8 @@

use Closure;
use Psr\EventDispatcher\EventDispatcherInterface;
use Spiral\Boot\Bootloader\BootloaderRegistry;
use Spiral\Boot\Bootloader\BootloaderRegistryInterface;
use Spiral\Boot\Bootloader\CoreBootloader;
use Spiral\Boot\BootloadManager\StrategyBasedBootloadManager;
use Spiral\Boot\BootloadManager\DefaultInvokerStrategy;
Expand Down Expand Up @@ -135,6 +137,10 @@ final public static function create(
\assert($bootloadManager instanceof BootloadManagerInterface);
$container->bind(BootloadManagerInterface::class, $bootloadManager);

if (!$container->has(BootloaderRegistryInterface::class)) {
$container->bindSingleton(BootloaderRegistryInterface::class, [self::class, 'initBootloaderRegistry']);
}

return new static(
$container,
$exceptionHandler,
Expand Down Expand Up @@ -164,11 +170,13 @@ public function run(?EnvironmentInterface $environment = null): ?self
// will protect any against env overwrite action
$this->container->runScope(
[EnvironmentInterface::class => $environment],
function (): void {
$this->bootloader->bootload($this->defineSystemBootloaders());
function (Container $container): void {
$registry = $container->get(BootloaderRegistryInterface::class);

$this->bootloader->bootload($registry->getSystemBootloaders());
$this->fireCallbacks($this->runningCallbacks);

$this->bootload();
$this->bootload($registry->getBootloaders());
$this->bootstrap();

$this->fireCallbacks($this->bootstrappedCallbacks);
Expand Down Expand Up @@ -335,11 +343,11 @@ protected function fireCallbacks(array &$callbacks): void
/**
* Bootload all registered classes using BootloadManager.
*/
private function bootload(): void
private function bootload(array $bootloaders = []): void
{
$self = $this;
$this->bootloader->bootload(
$this->defineBootloaders(),
$bootloaders,
[
static function () use ($self): void {
$self->fireCallbacks($self->bootingCallbacks);
Expand All @@ -356,4 +364,9 @@ private function getEventDispatcher(): ?EventDispatcherInterface
? $this->container->get(EventDispatcherInterface::class)
: null;
}

private function initBootloaderRegistry(): BootloaderRegistryInterface
{
return new BootloaderRegistry($this->defineSystemBootloaders(), $this->defineBootloaders());
}
}
81 changes: 81 additions & 0 deletions src/Boot/src/Bootloader/BootloaderRegistry.php
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\Bootloader;

/**
* @psalm-import-type TClass from \Spiral\Boot\BootloadManagerInterface
*/
final class BootloaderRegistry implements BootloaderRegistryInterface
{
/**
* @param array<TClass>|array<TClass, array<string, mixed>> $systemBootloaders
* @param array<TClass>|array<TClass, array<string, mixed>> $bootloaders
*/
public function __construct(
private array $systemBootloaders = [],
private array $bootloaders = [],
) {
}

/**
* @param TClass|array<TClass, array<string, mixed>> $bootloader
*/
public function registerSystem(string|array $bootloader): void
{
if ($this->hasBootloader($bootloader)) {
return;
}

\is_string($bootloader)
? $this->systemBootloaders[] = $bootloader
: $this->systemBootloaders[\array_key_first($bootloader)] = $bootloader[\array_key_first($bootloader)]
;
}

/**
* @param TClass|array<TClass, array<string, mixed>> $bootloader
*/
public function register(string|array $bootloader): void
{
if ($this->hasBootloader($bootloader)) {
return;
}

\is_string($bootloader)
? $this->bootloaders[] = $bootloader
: $this->bootloaders[\array_key_first($bootloader)] = $bootloader[\array_key_first($bootloader)]
;
}

/**
* @return array<TClass>|array<TClass, array<string, mixed>>
*/
public function getSystemBootloaders(): array
{
return $this->systemBootloaders;
}

/**
* @return array<TClass>|array<TClass, array<string, mixed>>
*/
public function getBootloaders(): array
{
return $this->bootloaders;
}

/**
* @param TClass|array<TClass, array<string, mixed>> $bootloader
*/
private function hasBootloader(string|array $bootloader): bool
{
if (\is_array($bootloader)) {
return false;
}

return
\in_array($bootloader, $this->systemBootloaders, true) ||
\in_array($bootloader, $this->bootloaders, true);
}
}
39 changes: 39 additions & 0 deletions src/Boot/src/Bootloader/BootloaderRegistryInterface.php
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Spiral\Boot\Bootloader;

/**
* @psalm-import-type TClass from \Spiral\Boot\BootloadManagerInterface
*/
interface BootloaderRegistryInterface
{
/**
* @param TClass|array<TClass, array<string, mixed>> $bootloader
*
* Examples:
* 1. SimpleBootloader::class,
* 2. [SimpleBootloader::class => ['option' => 'value']]
*/
public function registerSystem(string|array $bootloader): void;

/**
* @param TClass|array<TClass, array<string, mixed>> $bootloader
*
* Examples:
* 1. SimpleBootloader::class,
* 2. [SimpleBootloader::class => ['option' => 'value']]
*/
public function register(string|array $bootloader): void;

/**
* @return array<TClass>|array<TClass, array<string, mixed>>
*/
public function getSystemBootloaders(): array;

/**
* @return array<TClass>|array<TClass, array<string, mixed>>
*/
public function getBootloaders(): array;
}
73 changes: 73 additions & 0 deletions src/Boot/tests/Bootloader/BootloaderRegistryTest.php
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace Spiral\Tests\Boot\Bootloader;

use PHPUnit\Framework\TestCase;
use Spiral\Boot\Bootloader\BootloaderRegistry;

final class BootloaderRegistryTest extends TestCase
{
public function testConstructor(): void
{
$registry = new BootloaderRegistry();
$this->assertSame([], $registry->getSystemBootloaders());
$this->assertSame([], $registry->getBootloaders());

$registry = new BootloaderRegistry(
['BootloaderA', 'BootloaderB'],
['BootloaderC', 'BootloaderD'],
);

$this->assertSame(['BootloaderA', 'BootloaderB'], $registry->getSystemBootloaders());
$this->assertSame(['BootloaderC', 'BootloaderD'], $registry->getBootloaders());
}

public function testSystemBootloaders(): void
{
$registry = new BootloaderRegistry();
$this->assertSame([], $registry->getSystemBootloaders());

$registry->registerSystem('BootloaderA');
$registry->registerSystem(['BootloaderB' => ['option' => 'value']]);

$this->assertSame([
'BootloaderA',
'BootloaderB' => ['option' => 'value'],
], $registry->getSystemBootloaders());
}

public function testBootloaders(): void
{
$registry = new BootloaderRegistry();
$this->assertSame([], $registry->getBootloaders());

$registry->register('BootloaderA');
$registry->register(['BootloaderB' => ['option' => 'value']]);

$this->assertSame([
'BootloaderA',
'BootloaderB' => ['option' => 'value'],
], $registry->getBootloaders());
}

public function testDuplicateBootloader(): void
{
$registry = new BootloaderRegistry();
$registry->register('BootloaderA');
$registry->register('BootloaderA');
$registry->registerSystem('BootloaderA');

$this->assertSame(['BootloaderA'], $registry->getBootloaders());
$this->assertSame([], $registry->getSystemBootloaders());

$registry = new BootloaderRegistry();
$registry->registerSystem('BootloaderA');
$registry->registerSystem('BootloaderA');
$registry->register('BootloaderA');

$this->assertSame([], $registry->getBootloaders());
$this->assertSame(['BootloaderA'], $registry->getSystemBootloaders());
}
}
2 changes: 2 additions & 0 deletions src/Framework/Framework/Kernel.php
Expand Up @@ -68,6 +68,8 @@ public function appBooted(\Closure ...$callbacks): void
* Get list of defined application bootloaders
*
* @return array<int, class-string>|array<class-string, array<non-empty-string, mixed>>
*
* @deprecated since v3.10 Use {@see defineBootloaders()} instead. Will be removed in v4.0
*/
protected function defineAppBootloaders(): array
{
Expand Down
18 changes: 18 additions & 0 deletions tests/Framework/KernelTest.php
Expand Up @@ -4,6 +4,8 @@

namespace Spiral\Tests\Framework;

use Spiral\Boot\Bootloader\BootloaderRegistry;
use Spiral\Boot\Bootloader\BootloaderRegistryInterface;
use Spiral\Boot\Exception\BootException;
use Spiral\App\TestApp;
use Spiral\Core\Container;
Expand Down Expand Up @@ -78,4 +80,20 @@ public function testRunningCallbackShouldBeFired(): void
$this->assertTrue($callback1);
$this->assertTrue($callback2);
}

public function testBootloaderRegistryShouldBeBoundAsSingleton(): void
{
$this->assertContainerBoundAsSingleton(BootloaderRegistryInterface::class, BootloaderRegistry::class);
}

public function testCustomBootloaderRegistry(): void
{
$registry = $this->createMock(BootloaderRegistryInterface::class);
$container = new Container();
$container->bindSingleton(BootloaderRegistryInterface::class, $registry);

$kernel = TestApp::create(directories: ['root' => __DIR__. '/../'], container: $container);

$this->assertSame($registry, $kernel->getContainer()->get(BootloaderRegistryInterface::class));
}
}

0 comments on commit c618ac7

Please sign in to comment.