Skip to content

Commit

Permalink
Merge pull request #1090 from spiral/dispatcher-scopes
Browse files Browse the repository at this point in the history
Expose Dispatcher scopes and Spiral enum
  • Loading branch information
butschster committed Feb 29, 2024
2 parents dd46fbf + 428b1d9 commit 92bcfa4
Show file tree
Hide file tree
Showing 22 changed files with 376 additions and 55 deletions.
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -135,7 +135,7 @@
"rector/rector": "0.18.1",
"spiral/code-style": "^1.1",
"spiral/nyholm-bridge": "^1.2",
"spiral/testing": "^2.4",
"spiral/testing": "^2.7",
"spiral/validator": "^1.3",
"google/protobuf": "^3.25",
"symplify/monorepo-builder": "^10.2.7",
Expand Down
58 changes: 46 additions & 12 deletions src/Boot/src/AbstractKernel.php
Expand Up @@ -6,6 +6,7 @@

use Closure;
use Psr\EventDispatcher\EventDispatcherInterface;
use Spiral\Attribute\DispatcherScope;
use Spiral\Boot\Bootloader\BootloaderRegistry;
use Spiral\Boot\Bootloader\BootloaderRegistryInterface;
use Spiral\Boot\Bootloader\CoreBootloader;
Expand All @@ -21,6 +22,7 @@
use Spiral\Boot\Exception\BootException;
use Spiral\Core\Container\Autowire;
use Spiral\Core\Container;
use Spiral\Core\Scope;
use Spiral\Exceptions\ExceptionHandler;
use Spiral\Exceptions\ExceptionHandlerInterface;
use Spiral\Exceptions\ExceptionRendererInterface;
Expand Down Expand Up @@ -49,7 +51,10 @@ abstract class AbstractKernel implements KernelInterface

protected FinalizerInterface $finalizer;

/** @var DispatcherInterface[] */
/**
* @internal
* @var array<class-string<DispatcherInterface>>
*/
protected array $dispatchers = [];

/** @var array<Closure> */
Expand Down Expand Up @@ -258,9 +263,16 @@ public function bootstrapped(Closure ...$callbacks): void
/**
* Add new dispatcher. This method must only be called before method `serve`
* will be invoked.
*
* @param class-string<DispatcherInterface>|DispatcherInterface $dispatcher The class name or instance
* of the dispatcher. Since v4.0, it will only accept the class name.
*/
public function addDispatcher(DispatcherInterface $dispatcher): self
public function addDispatcher(string|DispatcherInterface $dispatcher): self
{
if (\is_object($dispatcher)) {
$dispatcher = $dispatcher::class;
}

$this->dispatchers[] = $dispatcher;

return $this;
Expand All @@ -278,21 +290,31 @@ public function serve(): mixed
$eventDispatcher = $this->getEventDispatcher();
$eventDispatcher?->dispatch(new Serving());

$serving = $servingScope = null;
foreach ($this->dispatchers as $dispatcher) {
if ($dispatcher->canServe()) {
return $this->container->runScope(
[DispatcherInterface::class => $dispatcher],
static function () use ($dispatcher, $eventDispatcher): mixed {
$eventDispatcher?->dispatch(new DispatcherFound($dispatcher));
return $dispatcher->serve();
}
);
$reflection = new \ReflectionClass($dispatcher);

$scope = ($reflection->getAttributes(DispatcherScope::class)[0] ?? null)?->newInstance()->scope;
$this->container->getBinder($scope)->bind($dispatcher, $dispatcher);

if ($serving === null && $this->canServe($reflection)) {
$serving = $dispatcher;
$servingScope = $scope;
}
}

$eventDispatcher?->dispatch(new DispatcherNotFound());
if ($serving === null) {
$eventDispatcher?->dispatch(new DispatcherNotFound());
throw new BootException('Unable to locate active dispatcher.');
}

throw new BootException('Unable to locate active dispatcher.');
return $this->container->runScope(
new Scope(name: $servingScope, bindings: [DispatcherInterface::class => $serving]),
static function (DispatcherInterface $dispatcher) use ($eventDispatcher): mixed {
$eventDispatcher?->dispatch(new DispatcherFound($dispatcher));
return $dispatcher->serve();
}
);
}

/**
Expand Down Expand Up @@ -370,4 +392,16 @@ private function initBootloaderRegistry(): BootloaderRegistryInterface
{
return new BootloaderRegistry($this->defineSystemBootloaders(), $this->defineBootloaders());
}

/**
* @throws BootException
*/
private function canServe(\ReflectionClass $reflection): bool
{
if (!$reflection->hasMethod('canServe')) {
throw new BootException('Dispatcher must implement static `canServe` method.');
}

return $this->container->invoke([$reflection->getName(), 'canServe']);
}
}
6 changes: 1 addition & 5 deletions src/Boot/src/DispatcherInterface.php
Expand Up @@ -7,14 +7,10 @@
/**
* Dispatchers are general application flow controllers, system should start them and pass exception
* or instance of snapshot into them when error happens.
* @method static bool canServe() Must return true if the dispatcher expects to handle requests in a current environment
*/
interface DispatcherInterface
{
/**
* Must return true if dispatcher expects to handle requests in a current environment.
*/
public function canServe(): bool;

/**
* Start request execution.
*
Expand Down
52 changes: 29 additions & 23 deletions src/Boot/tests/KernelTest.php
Expand Up @@ -35,9 +35,7 @@ public function testKernelException(): void
{
$this->expectException(BootException::class);

$kernel = TestCore::create([
'root' => __DIR__,
])->run();
$kernel = TestCore::create(['root' => __DIR__])->run();

$kernel->serve();
}
Expand All @@ -47,41 +45,53 @@ public function testKernelException(): void
*/
public function testDispatcher(): void
{
$kernel = TestCore::create([
'root' => __DIR__,
])->run();
$kernel = TestCore::create(['root' => __DIR__])->run();

$d = new class() implements DispatcherInterface {
public $fired = false;
public static function canServe(EnvironmentInterface $env): bool
{
return true;
}

public function serve(): bool
{
return true;
}
};
$kernel->addDispatcher($d);

$this->assertTrue($kernel->serve());
}

public function testDispatcherNonStaticServe(): void
{
$kernel = TestCore::create(['root' => __DIR__])->run();

$d = new class() implements DispatcherInterface {
public function canServe(): bool
{
return true;
}

public function serve(): void
public function serve(): bool
{
$this->fired = true;
return true;
}
};
$kernel->addDispatcher($d);
$this->assertFalse($d->fired);

$kernel->serve();
$this->assertTrue($d->fired);
$this->assertTrue($kernel->serve());
}

/**
* @throws Throwable
*/
public function testDispatcherReturnCode(): void
{
$kernel = TestCore::create([
'root' => __DIR__,
])->run();
$kernel = TestCore::create(['root' => __DIR__])->run();

$d = new class() implements DispatcherInterface {
public function canServe(): bool
public static function canServe(EnvironmentInterface $env): bool
{
return true;
}
Expand All @@ -102,9 +112,7 @@ public function serve(): int
*/
public function testEnv(): void
{
$kernel = TestCore::create([
'root' => __DIR__,
])->run();
$kernel = TestCore::create(['root' => __DIR__])->run();

$this->assertSame(
'VALUE',
Expand All @@ -114,9 +122,7 @@ public function testEnv(): void

public function testBootingCallbacks()
{
$kernel = TestCore::create([
'root' => __DIR__,
]);
$kernel = TestCore::create(['root' => __DIR__]);

$kernel->booting(static function (TestCore $core) {
$core->getContainer()->bind('abc', 'foo');
Expand Down Expand Up @@ -154,7 +160,7 @@ public function testBootingCallbacks()
public function testEventsShouldBeDispatched(): void
{
$testDispatcher = new class implements DispatcherInterface {
public function canServe(): bool
public static function canServe(EnvironmentInterface $env): bool
{
return true;
}
Expand Down
5 changes: 2 additions & 3 deletions src/Console/src/Bootloader/ConsoleBootloader.php
Expand Up @@ -17,7 +17,6 @@
use Spiral\Console\Sequence\CommandSequence;
use Spiral\Core\Attribute\Singleton;
use Spiral\Core\CoreInterceptorInterface;
use Spiral\Core\FactoryInterface;
use Spiral\Tokenizer\Bootloader\TokenizerListenerBootloader;
use Spiral\Tokenizer\TokenizerListenerRegistryInterface;

Expand All @@ -44,8 +43,8 @@ public function __construct(
public function init(AbstractKernel $kernel): void
{
// Lowest priority
$kernel->bootstrapped(static function (AbstractKernel $kernel, FactoryInterface $factory): void {
$kernel->addDispatcher($factory->make(ConsoleDispatcher::class));
$kernel->bootstrapped(static function (AbstractKernel $kernel): void {
$kernel->addDispatcher(ConsoleDispatcher::class);
});

$this->config->setDefaults(
Expand Down
8 changes: 5 additions & 3 deletions src/Core/src/Attribute/Scope.php
Expand Up @@ -12,8 +12,10 @@
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Scope implements Plugin
{
public function __construct(
public string $name,
) {
public readonly string $name;

public function __construct(string|\BackedEnum $name)
{
$this->name = \is_object($name) ? (string) $name->value : $name;
}
}
12 changes: 9 additions & 3 deletions src/Core/src/Container.php
Expand Up @@ -59,9 +59,13 @@ final class Container implements
*/
public function __construct(
private Config $config = new Config(),
?string $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
string|\BackedEnum|null $scopeName = self::DEFAULT_ROOT_SCOPE_NAME,
private Options $options = new Options(),
) {
if (\is_object($scopeName)) {
$scopeName = (string) $scopeName->value;
}

$this->initServices($this, $scopeName);

/** @psalm-suppress RedundantPropertyInitializationCheck */
Expand Down Expand Up @@ -154,13 +158,15 @@ public function has(string $id): bool
/**
* Make a Binder proxy to configure bindings for a specific scope.
*
* @param null|string $scope Scope name.
* @param null|\BackedEnum|string $scope Scope name.
* If {@see null}, binder for the current working scope will be returned.
* If {@see string}, the default binder for the given scope will be returned. Default bindings won't affect
* already created Container instances except the case with the root one.
*/
public function getBinder(?string $scope = null): BinderInterface
public function getBinder(string|\BackedEnum|null $scope = null): BinderInterface
{
$scope = \is_object($scope) ? (string) $scope->value : $scope;

return $scope === null
? $this->binder
: new StateBinder($this->config->scopedBindings->getState($scope));
Expand Down
4 changes: 2 additions & 2 deletions src/Core/src/Scope.php
Expand Up @@ -14,13 +14,13 @@
final class Scope
{
/**
* @param null|string $name Scope name. Named scopes can have individual bindings and constrains.
* @param null|string|\BackedEnum $name Scope name. Named scopes can have individual bindings and constrains.
* @param array<non-empty-string, TResolver> $bindings Custom bindings for the new scope.
* @param bool $autowire If {@see false}, closure will be invoked with just only the passed Container
* as the first argument. Otherwise, {@see InvokerInterface::invoke()} will be used to invoke the closure.
*/
public function __construct(
public readonly ?string $name = null,
public readonly string|\BackedEnum|null $name = null,
public readonly array $bindings = [],
public readonly bool $autowire = true,
) {
Expand Down
29 changes: 29 additions & 0 deletions src/Core/tests/Attribute/ScopeTest.php
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Spiral\Tests\Core\Attribute;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Spiral\Core\Attribute\Scope;
use Spiral\Framework\Spiral;
use Spiral\Tests\Core\Fixtures\ScopeEnum;

final class ScopeTest extends TestCase
{
#[DataProvider('scopeNameDataProvider')]
public function testScope(string|\BackedEnum $name, string $expected): void
{
$scope = new Scope($name);

$this->assertSame($expected, $scope->name);
}

public static function scopeNameDataProvider(): \Traversable
{
yield ['foo', 'foo'];
yield [Spiral::HttpRequest, 'http.request'];
yield [ScopeEnum::A, 'a'];
}
}
10 changes: 10 additions & 0 deletions src/Core/tests/Fixtures/ScopeEnum.php
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Spiral\Tests\Core\Fixtures;

enum ScopeEnum: string
{
case A = 'a';
}

0 comments on commit 92bcfa4

Please sign in to comment.