From 8d6707be4d18dc9e48d1b5c8348c5151f5c1b8de Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Wed, 10 Jan 2024 16:20:02 +0200 Subject: [PATCH 01/15] Add ScopeName attribute --- src/Core/src/Attribute/Scope.php | 8 +++++--- src/Core/src/Container.php | 5 ++++- src/Core/src/Scope.php | 4 ++-- src/Framework/Framework/ScopeName.php | 16 ++++++++++++++++ 4 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 src/Framework/Framework/ScopeName.php diff --git a/src/Core/src/Attribute/Scope.php b/src/Core/src/Attribute/Scope.php index e2e814189..33ac1836b 100644 --- a/src/Core/src/Attribute/Scope.php +++ b/src/Core/src/Attribute/Scope.php @@ -12,8 +12,10 @@ #[\Attribute(\Attribute::TARGET_CLASS)] final class Scope implements Plugin { - public function __construct( - public string $name, - ) { + public string $name; + + public function __construct(string|\BackedEnum $name) + { + $this->name = $name instanceof \BackedEnum ? $name->value : $name; } } diff --git a/src/Core/src/Container.php b/src/Core/src/Container.php index 28801f4bc..20b3bb104 100644 --- a/src/Core/src/Container.php +++ b/src/Core/src/Container.php @@ -379,7 +379,10 @@ private function closeScope(): void private function runIsolatedScope(Scope $config, callable $closure): mixed { // Open scope - $container = new self($this->config, $config->name); + $container = new self( + $this->config, + $config->name instanceof \BackedEnum ? $config->name->value : $config->name + ); // Configure scope $container->scope->setParent($this, $this->scope); diff --git a/src/Core/src/Scope.php b/src/Core/src/Scope.php index 0a8a40b9f..717a06ba2 100644 --- a/src/Core/src/Scope.php +++ b/src/Core/src/Scope.php @@ -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 $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, ) { diff --git a/src/Framework/Framework/ScopeName.php b/src/Framework/Framework/ScopeName.php new file mode 100644 index 000000000..e504b9cdc --- /dev/null +++ b/src/Framework/Framework/ScopeName.php @@ -0,0 +1,16 @@ + Date: Wed, 10 Jan 2024 16:24:20 +0200 Subject: [PATCH 02/15] Allow \BackedEnum in getBinder method --- src/Core/src/Container.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Core/src/Container.php b/src/Core/src/Container.php index 20b3bb104..234eb9d3d 100644 --- a/src/Core/src/Container.php +++ b/src/Core/src/Container.php @@ -153,13 +153,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 = $scope instanceof \BackedEnum ? $scope->value : $scope; + return $scope === null ? $this->binder : new StateBinder($this->config->scopedBindings->getState($scope)); From b385fb3965f77cd641c9073fece34b2e052fcc66 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Wed, 10 Jan 2024 17:19:50 +0200 Subject: [PATCH 03/15] Add unit tests --- src/Core/tests/Attribute/ScopeTest.php | 29 ++++++++++++++++++++++++++ src/Core/tests/Fixtures/ScopeEnum.php | 10 +++++++++ src/Core/tests/Scope/UseCaseTest.php | 22 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 src/Core/tests/Attribute/ScopeTest.php create mode 100644 src/Core/tests/Fixtures/ScopeEnum.php diff --git a/src/Core/tests/Attribute/ScopeTest.php b/src/Core/tests/Attribute/ScopeTest.php new file mode 100644 index 000000000..81c443b48 --- /dev/null +++ b/src/Core/tests/Attribute/ScopeTest.php @@ -0,0 +1,29 @@ +assertSame($expected, $scope->name); + } + + public static function scopeNameDataProvider(): \Traversable + { + yield ['foo', 'foo']; + yield [ScopeName::HttpRequest, 'http.request']; + yield [ScopeEnum::A, 'a']; + } +} diff --git a/src/Core/tests/Fixtures/ScopeEnum.php b/src/Core/tests/Fixtures/ScopeEnum.php new file mode 100644 index 000000000..8f3a4e77e --- /dev/null +++ b/src/Core/tests/Fixtures/ScopeEnum.php @@ -0,0 +1,10 @@ +make('foo'); } + + #[DataProvider('scopeEnumDataProvider')] + public function testScopeWithEnum(\BackedEnum $scope): void + { + $root = new Container(); + $root->getBinder($scope)->bindSingleton('foo', SampleClass::class); + + $root->runScope(new Scope($scope), function (Container $container) { + $this->assertTrue($container->has('foo')); + $this->assertInstanceOf(SampleClass::class, $container->get('foo')); + }); + $this->assertFalse($root->has('foo')); + } + + public static function scopeEnumDataProvider(): \Traversable + { + yield [ScopeName::HttpRequest, 'http.request']; + yield [ScopeEnum::A, 'a']; + } } From 6f846e86970b78eef69232dee30985c12de472a2 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Wed, 10 Jan 2024 18:13:00 +0200 Subject: [PATCH 04/15] Add scope names --- src/Core/src/Attribute/Scope.php | 4 ++-- src/Core/src/Container.php | 4 ++-- src/Framework/Framework/ScopeName.php | 7 +++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Core/src/Attribute/Scope.php b/src/Core/src/Attribute/Scope.php index 33ac1836b..a406b1205 100644 --- a/src/Core/src/Attribute/Scope.php +++ b/src/Core/src/Attribute/Scope.php @@ -12,10 +12,10 @@ #[\Attribute(\Attribute::TARGET_CLASS)] final class Scope implements Plugin { - public string $name; + public readonly string $name; public function __construct(string|\BackedEnum $name) { - $this->name = $name instanceof \BackedEnum ? $name->value : $name; + $this->name = $name instanceof \BackedEnum ? (string) $name->value : $name; } } diff --git a/src/Core/src/Container.php b/src/Core/src/Container.php index 234eb9d3d..c33f57a23 100644 --- a/src/Core/src/Container.php +++ b/src/Core/src/Container.php @@ -160,7 +160,7 @@ public function has(string $id): bool */ public function getBinder(string|\BackedEnum|null $scope = null): BinderInterface { - $scope = $scope instanceof \BackedEnum ? $scope->value : $scope; + $scope = $scope instanceof \BackedEnum ? (string) $scope->value : $scope; return $scope === null ? $this->binder @@ -383,7 +383,7 @@ private function runIsolatedScope(Scope $config, callable $closure): mixed // Open scope $container = new self( $this->config, - $config->name instanceof \BackedEnum ? $config->name->value : $config->name + $config->name instanceof \BackedEnum ? (string) $config->name->value : $config->name ); // Configure scope diff --git a/src/Framework/Framework/ScopeName.php b/src/Framework/Framework/ScopeName.php index e504b9cdc..564af59c8 100644 --- a/src/Framework/Framework/ScopeName.php +++ b/src/Framework/Framework/ScopeName.php @@ -6,11 +6,18 @@ enum ScopeName: string { + case Http = 'http'; case HttpRequest = 'http.request'; + case Queue = 'queue'; case QueueTask = 'queue.task'; + case Temporal = 'temporal'; case TemporalActivity = 'temporal.activity'; + case Grpc = 'grpc'; case GrpcRequest = 'grpc.request'; + case Centrifugo = 'centrifugo'; case CentrifugoRequest = 'centrifugo.request'; + case Tcp = 'tcp'; case TcpPacket = 'tcp.packet'; + case Console = 'console'; case ConsoleCommand = 'console.command'; } From dd2766f7b9f7e235b49dcf7fc1428affe2d8c0fd Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Wed, 10 Jan 2024 18:26:10 +0200 Subject: [PATCH 05/15] Replace instanceof with is_object --- src/Core/src/Attribute/Scope.php | 2 +- src/Core/src/Container.php | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Core/src/Attribute/Scope.php b/src/Core/src/Attribute/Scope.php index a406b1205..308df4e3b 100644 --- a/src/Core/src/Attribute/Scope.php +++ b/src/Core/src/Attribute/Scope.php @@ -16,6 +16,6 @@ final class Scope implements Plugin public function __construct(string|\BackedEnum $name) { - $this->name = $name instanceof \BackedEnum ? (string) $name->value : $name; + $this->name = \is_object($name) ? (string) $name->value : $name; } } diff --git a/src/Core/src/Container.php b/src/Core/src/Container.php index c33f57a23..20270a25a 100644 --- a/src/Core/src/Container.php +++ b/src/Core/src/Container.php @@ -160,7 +160,7 @@ public function has(string $id): bool */ public function getBinder(string|\BackedEnum|null $scope = null): BinderInterface { - $scope = $scope instanceof \BackedEnum ? (string) $scope->value : $scope; + $scope = \is_object($scope) ? (string) $scope->value : $scope; return $scope === null ? $this->binder @@ -381,10 +381,7 @@ private function closeScope(): void private function runIsolatedScope(Scope $config, callable $closure): mixed { // Open scope - $container = new self( - $this->config, - $config->name instanceof \BackedEnum ? (string) $config->name->value : $config->name - ); + $container = new self($this->config, \is_object($config->name) ? (string) $config->name->value : $config->name); // Configure scope $container->scope->setParent($this, $this->scope); From ddd105ed703a1998a066d2f7cd8e1f2056556e8f Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Thu, 11 Jan 2024 10:57:30 +0200 Subject: [PATCH 06/15] Allow BackedEnum in constructor --- src/Core/src/Container.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Core/src/Container.php b/src/Core/src/Container.php index 20270a25a..bdf6f03a6 100644 --- a/src/Core/src/Container.php +++ b/src/Core/src/Container.php @@ -59,8 +59,12 @@ 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, ) { + if (\is_object($scopeName)) { + $scopeName = (string) $scopeName->value; + } + $this->initServices($this, $scopeName); /** @psalm-suppress RedundantPropertyInitializationCheck */ @@ -381,7 +385,7 @@ private function closeScope(): void private function runIsolatedScope(Scope $config, callable $closure): mixed { // Open scope - $container = new self($this->config, \is_object($config->name) ? (string) $config->name->value : $config->name); + $container = new self($this->config, $config->name); // Configure scope $container->scope->setParent($this, $this->scope); From 80733185f8f29d78aa2e260d893438163995d4fc Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Thu, 11 Jan 2024 16:34:51 +0200 Subject: [PATCH 07/15] Add DispatcherScope --- src/Boot/src/AbstractKernel.php | 11 ++++++++++- src/Framework/Attribute/DispatcherScope.php | 17 +++++++++++++++++ src/Framework/Console/ConsoleDispatcher.php | 3 +++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/Framework/Attribute/DispatcherScope.php diff --git a/src/Boot/src/AbstractKernel.php b/src/Boot/src/AbstractKernel.php index 3ae0b6f0f..ab347d2c2 100644 --- a/src/Boot/src/AbstractKernel.php +++ b/src/Boot/src/AbstractKernel.php @@ -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; @@ -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; @@ -281,7 +283,7 @@ public function serve(): mixed foreach ($this->dispatchers as $dispatcher) { if ($dispatcher->canServe()) { return $this->container->runScope( - [DispatcherInterface::class => $dispatcher], + new Scope($this->getDispatcherScope($dispatcher), [DispatcherInterface::class => $dispatcher]), static function () use ($dispatcher, $eventDispatcher): mixed { $eventDispatcher?->dispatch(new DispatcherFound($dispatcher)); return $dispatcher->serve(); @@ -370,4 +372,11 @@ private function initBootloaderRegistry(): BootloaderRegistryInterface { return new BootloaderRegistry($this->defineSystemBootloaders(), $this->defineBootloaders()); } + + private function getDispatcherScope(DispatcherInterface $dispatcher): string|\BackedEnum|null + { + $reflection = new \ReflectionObject($dispatcher); + + return ($reflection->getAttributes(DispatcherScope::class)[0] ?? null)?->newInstance()->scope; + } } diff --git a/src/Framework/Attribute/DispatcherScope.php b/src/Framework/Attribute/DispatcherScope.php new file mode 100644 index 000000000..2f7e85542 --- /dev/null +++ b/src/Framework/Attribute/DispatcherScope.php @@ -0,0 +1,17 @@ + Date: Mon, 15 Jan 2024 13:55:36 +0200 Subject: [PATCH 08/15] Add unit tests --- src/Boot/src/AbstractKernel.php | 5 +- src/Boot/tests/KernelTest.php | 71 +++++-------------- .../Attribute/DispatcherScopeTest.php | 38 ++++++++++ .../app/src/Dispatcher/AbstractDispatcher.php | 27 +++++++ .../Dispatcher/DispatcherWithCustomEnum.php | 12 ++++ .../Dispatcher/DispatcherWithScopeName.php | 13 ++++ .../Dispatcher/DispatcherWithStringScope.php | 12 ++++ tests/app/src/Dispatcher/Scope.php | 10 +++ 8 files changed, 133 insertions(+), 55 deletions(-) create mode 100644 tests/Framework/Attribute/DispatcherScopeTest.php create mode 100644 tests/app/src/Dispatcher/AbstractDispatcher.php create mode 100644 tests/app/src/Dispatcher/DispatcherWithCustomEnum.php create mode 100644 tests/app/src/Dispatcher/DispatcherWithScopeName.php create mode 100644 tests/app/src/Dispatcher/DispatcherWithStringScope.php create mode 100644 tests/app/src/Dispatcher/Scope.php diff --git a/src/Boot/src/AbstractKernel.php b/src/Boot/src/AbstractKernel.php index ab347d2c2..2d4fc3fa4 100644 --- a/src/Boot/src/AbstractKernel.php +++ b/src/Boot/src/AbstractKernel.php @@ -283,7 +283,10 @@ public function serve(): mixed foreach ($this->dispatchers as $dispatcher) { if ($dispatcher->canServe()) { return $this->container->runScope( - new Scope($this->getDispatcherScope($dispatcher), [DispatcherInterface::class => $dispatcher]), + new Scope( + name: $this->getDispatcherScope($dispatcher), + bindings: [DispatcherInterface::class => $dispatcher], + ), static function () use ($dispatcher, $eventDispatcher): mixed { $eventDispatcher?->dispatch(new DispatcherFound($dispatcher)); return $dispatcher->serve(); diff --git a/src/Boot/tests/KernelTest.php b/src/Boot/tests/KernelTest.php index 9266ab284..ff0ccc6c2 100644 --- a/src/Boot/tests/KernelTest.php +++ b/src/Boot/tests/KernelTest.php @@ -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(); } @@ -47,28 +45,14 @@ public function testKernelException(): void */ public function testDispatcher(): void { - $kernel = TestCore::create([ - 'root' => __DIR__, - ])->run(); - - $d = new class() implements DispatcherInterface { - public $fired = false; - - public function canServe(): bool - { - return true; - } - - public function serve(): void - { - $this->fired = true; - } - }; - $kernel->addDispatcher($d); - $this->assertFalse($d->fired); + $kernel = TestCore::create(['root' => __DIR__])->run(); + + $d = $this->createMock(DispatcherInterface::class); + $d->expects($this->once())->method('canServe')->willReturn(true); + $d->expects($this->once())->method('serve'); + $kernel->addDispatcher($d); $kernel->serve(); - $this->assertTrue($d->fired); } /** @@ -76,21 +60,11 @@ public function serve(): void */ public function testDispatcherReturnCode(): void { - $kernel = TestCore::create([ - 'root' => __DIR__, - ])->run(); - - $d = new class() implements DispatcherInterface { - public function canServe(): bool - { - return true; - } - - public function serve(): int - { - return 1; - } - }; + $kernel = TestCore::create(['root' => __DIR__])->run(); + + $d = $this->createMock(DispatcherInterface::class); + $d->expects($this->once())->method('canServe')->willReturn(true); + $d->expects($this->once())->method('serve')->willReturn(1); $kernel->addDispatcher($d); $result = $kernel->serve(); @@ -102,9 +76,7 @@ public function serve(): int */ public function testEnv(): void { - $kernel = TestCore::create([ - 'root' => __DIR__, - ])->run(); + $kernel = TestCore::create(['root' => __DIR__])->run(); $this->assertSame( 'VALUE', @@ -114,9 +86,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'); @@ -153,16 +123,9 @@ public function testBootingCallbacks() public function testEventsShouldBeDispatched(): void { - $testDispatcher = new class implements DispatcherInterface { - public function canServe(): bool - { - return true; - } - - public function serve(): void - { - } - }; + $testDispatcher = $this->createMock(DispatcherInterface::class); + $testDispatcher->expects($this->once())->method('canServe')->willReturn(true); + $testDispatcher->expects($this->once())->method('serve'); $container = new Container(); $kernel = TestCore::create(directories: ['root' => __DIR__,], container: $container) diff --git a/tests/Framework/Attribute/DispatcherScopeTest.php b/tests/Framework/Attribute/DispatcherScopeTest.php new file mode 100644 index 000000000..e7195e27c --- /dev/null +++ b/tests/Framework/Attribute/DispatcherScopeTest.php @@ -0,0 +1,38 @@ +beforeBooting(function (AbstractKernel $kernel, Container $container) use ($dispatcher, $scope): void { + $kernel->addDispatcher($container->get($dispatcher)); + $container->getBinder($scope)->bind('foo', new \stdClass()); + }); + + $app = $this->makeApp(); + + $this->assertInstanceOf(\stdClass::class, $app->serve()); + } + + public static function dispatchersDataProvider(): \Traversable + { + yield [DispatcherWithScopeName::class, ScopeName::Console]; + yield [DispatcherWithCustomEnum::class, Scope::Custom]; + yield [DispatcherWithStringScope::class, 'test']; + } +} diff --git a/tests/app/src/Dispatcher/AbstractDispatcher.php b/tests/app/src/Dispatcher/AbstractDispatcher.php new file mode 100644 index 000000000..82ddf42da --- /dev/null +++ b/tests/app/src/Dispatcher/AbstractDispatcher.php @@ -0,0 +1,27 @@ +container->get('foo'); + } +} diff --git a/tests/app/src/Dispatcher/DispatcherWithCustomEnum.php b/tests/app/src/Dispatcher/DispatcherWithCustomEnum.php new file mode 100644 index 000000000..62912d43a --- /dev/null +++ b/tests/app/src/Dispatcher/DispatcherWithCustomEnum.php @@ -0,0 +1,12 @@ + Date: Wed, 17 Jan 2024 15:17:09 +0200 Subject: [PATCH 09/15] Creating dispatchers in scope --- src/Boot/src/AbstractKernel.php | 55 ++++++++++------ src/Boot/src/DispatcherInterface.php | 6 +- src/Boot/tests/KernelTest.php | 63 ++++++++++++++++--- .../Attribute/DispatcherScopeTest.php | 8 ++- tests/Framework/KernelTest.php | 44 +++++++++++++ .../app/src/Dispatcher/AbstractDispatcher.php | 14 +++-- .../src/Dispatcher/DispatcherWithoutScope.php | 9 +++ 7 files changed, 159 insertions(+), 40 deletions(-) create mode 100644 tests/app/src/Dispatcher/DispatcherWithoutScope.php diff --git a/src/Boot/src/AbstractKernel.php b/src/Boot/src/AbstractKernel.php index 2d4fc3fa4..6d8f7d6a9 100644 --- a/src/Boot/src/AbstractKernel.php +++ b/src/Boot/src/AbstractKernel.php @@ -51,7 +51,10 @@ abstract class AbstractKernel implements KernelInterface protected FinalizerInterface $finalizer; - /** @var DispatcherInterface[] */ + /** + * @internal + * @var array> + */ protected array $dispatchers = []; /** @var array */ @@ -261,8 +264,12 @@ public function bootstrapped(Closure ...$callbacks): void * Add new dispatcher. This method must only be called before method `serve` * will be invoked. */ - 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; @@ -280,24 +287,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( - new Scope( - name: $this->getDispatcherScope($dispatcher), - bindings: [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(); + } + ); } /** @@ -376,10 +390,15 @@ private function initBootloaderRegistry(): BootloaderRegistryInterface return new BootloaderRegistry($this->defineSystemBootloaders(), $this->defineBootloaders()); } - private function getDispatcherScope(DispatcherInterface $dispatcher): string|\BackedEnum|null + /** + * @throws BootException + */ + private function canServe(\ReflectionClass $reflection): bool { - $reflection = new \ReflectionObject($dispatcher); + if (!$reflection->hasMethod('canServe')) { + throw new BootException('Dispatcher must implement static `canServe` method.'); + } - return ($reflection->getAttributes(DispatcherScope::class)[0] ?? null)?->newInstance()->scope; + return $this->container->invoke([$reflection->getName(), 'canServe']); } } diff --git a/src/Boot/src/DispatcherInterface.php b/src/Boot/src/DispatcherInterface.php index 496b7ad5d..dde920049 100644 --- a/src/Boot/src/DispatcherInterface.php +++ b/src/Boot/src/DispatcherInterface.php @@ -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(EnvironmentInterface $env) */ interface DispatcherInterface { - /** - * Must return true if dispatcher expects to handle requests in a current environment. - */ - public function canServe(): bool; - /** * Start request execution. * diff --git a/src/Boot/tests/KernelTest.php b/src/Boot/tests/KernelTest.php index ff0ccc6c2..0b0915b4e 100644 --- a/src/Boot/tests/KernelTest.php +++ b/src/Boot/tests/KernelTest.php @@ -47,12 +47,40 @@ public function testDispatcher(): void { $kernel = TestCore::create(['root' => __DIR__])->run(); - $d = $this->createMock(DispatcherInterface::class); - $d->expects($this->once())->method('canServe')->willReturn(true); - $d->expects($this->once())->method('serve'); + $d = new class() implements DispatcherInterface { + 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(): bool + { + return true; + } + }; $kernel->addDispatcher($d); - $kernel->serve(); + + $this->assertTrue($kernel->serve()); } /** @@ -62,9 +90,17 @@ public function testDispatcherReturnCode(): void { $kernel = TestCore::create(['root' => __DIR__])->run(); - $d = $this->createMock(DispatcherInterface::class); - $d->expects($this->once())->method('canServe')->willReturn(true); - $d->expects($this->once())->method('serve')->willReturn(1); + $d = new class() implements DispatcherInterface { + public static function canServe(EnvironmentInterface $env): bool + { + return true; + } + + public function serve(): int + { + return 1; + } + }; $kernel->addDispatcher($d); $result = $kernel->serve(); @@ -123,9 +159,16 @@ public function testBootingCallbacks() public function testEventsShouldBeDispatched(): void { - $testDispatcher = $this->createMock(DispatcherInterface::class); - $testDispatcher->expects($this->once())->method('canServe')->willReturn(true); - $testDispatcher->expects($this->once())->method('serve'); + $testDispatcher = new class implements DispatcherInterface { + public static function canServe(EnvironmentInterface $env): bool + { + return true; + } + + public function serve(): void + { + } + }; $container = new Container(); $kernel = TestCore::create(directories: ['root' => __DIR__,], container: $container) diff --git a/tests/Framework/Attribute/DispatcherScopeTest.php b/tests/Framework/Attribute/DispatcherScopeTest.php index e7195e27c..8d9a5f733 100644 --- a/tests/Framework/Attribute/DispatcherScopeTest.php +++ b/tests/Framework/Attribute/DispatcherScopeTest.php @@ -20,13 +20,15 @@ final class DispatcherScopeTest extends BaseTestCase public function testDispatcherScope(string $dispatcher, string|\BackedEnum $scope): void { $this->beforeBooting(function (AbstractKernel $kernel, Container $container) use ($dispatcher, $scope): void { - $kernel->addDispatcher($container->get($dispatcher)); - $container->getBinder($scope)->bind('foo', new \stdClass()); + $kernel->addDispatcher($dispatcher); + $container->getBinder($scope)->bind('foo', \stdClass::class); }); $app = $this->makeApp(); - $this->assertInstanceOf(\stdClass::class, $app->serve()); + $this->assertInstanceOf(\stdClass::class, $app->serve()['foo']); + $this->assertInstanceOf($dispatcher, $app->serve()['dispatcher']); + $this->assertSame(is_object($scope) ? $scope->value : $scope, $app->serve()['scope']); } public static function dispatchersDataProvider(): \Traversable diff --git a/tests/Framework/KernelTest.php b/tests/Framework/KernelTest.php index cf5a9ae81..669f224d4 100644 --- a/tests/Framework/KernelTest.php +++ b/tests/Framework/KernelTest.php @@ -4,11 +4,19 @@ namespace Spiral\Tests\Framework; +use PHPUnit\Framework\Attributes\DataProvider; +use Spiral\App\Dispatcher\DispatcherWithCustomEnum; +use Spiral\App\Dispatcher\DispatcherWithoutScope; +use Spiral\App\Dispatcher\DispatcherWithScopeName; +use Spiral\App\Dispatcher\DispatcherWithStringScope; +use Spiral\App\Dispatcher\Scope; +use Spiral\Boot\AbstractKernel; use Spiral\Boot\Bootloader\BootloaderRegistry; use Spiral\Boot\Bootloader\BootloaderRegistryInterface; use Spiral\Boot\Exception\BootException; use Spiral\App\TestApp; use Spiral\Core\Container; +use Spiral\Framework\ScopeName; use stdClass; class KernelTest extends BaseTestCase @@ -96,4 +104,40 @@ public function testCustomBootloaderRegistry(): void $this->assertSame($registry, $kernel->getContainer()->get(BootloaderRegistryInterface::class)); } + + public function testDispatcherWithoutNamedScope(): void + { + $this->beforeBooting(function (AbstractKernel $kernel): void { + $kernel->addDispatcher(DispatcherWithoutScope::class); + }); + + $app = $this->makeApp(); + + $this->assertInstanceOf(DispatcherWithoutScope::class, $app->serve()['dispatcher']); + $this->assertSame('root', $app->serve()['scope']); + + $this->assertTrue($app->getContainer()->has(DispatcherWithoutScope::class)); + } + + #[DataProvider('dispatchersDataProvider')] + public function testDispatchersShouldBeBoundInCorrectScope(string $dispatcher, string $scope): void + { + $this->beforeBooting(function (AbstractKernel $kernel) use ($dispatcher): void { + $kernel->addDispatcher($dispatcher); + }); + + $app = $this->makeApp(); + + $this->assertInstanceOf($dispatcher, $app->serve()['dispatcher']); + $this->assertSame($scope, $app->serve()['scope']); + + $this->assertFalse($app->getContainer()->has($dispatcher)); + } + + public static function dispatchersDataProvider(): \Traversable + { + yield [DispatcherWithScopeName::class, ScopeName::Console->value]; + yield [DispatcherWithCustomEnum::class, Scope::Custom->value]; + yield [DispatcherWithStringScope::class, 'test']; + } } diff --git a/tests/app/src/Dispatcher/AbstractDispatcher.php b/tests/app/src/Dispatcher/AbstractDispatcher.php index 82ddf42da..d07b0ba99 100644 --- a/tests/app/src/Dispatcher/AbstractDispatcher.php +++ b/tests/app/src/Dispatcher/AbstractDispatcher.php @@ -6,22 +6,28 @@ use Psr\Container\ContainerInterface; use Spiral\Boot\DispatcherInterface; -use Spiral\Core\Attribute\Proxy; +use Spiral\Boot\EnvironmentInterface; abstract class AbstractDispatcher implements DispatcherInterface { public function __construct( - #[Proxy] private readonly ContainerInterface $container, + private readonly ContainerInterface $container, ) { } - public function canServe(): bool + public static function canServe(EnvironmentInterface $env): bool { return true; } public function serve(): mixed { - return $this->container->get('foo'); + $scope = (new \ReflectionProperty($this->container, 'scope'))->getValue($this->container); + + return [ + 'dispatcher' => $this->container->get(static::class), + 'foo' => $this->container->has('foo') ? $this->container->get('foo') : null, + 'scope' => (new \ReflectionProperty($scope, 'scopeName'))->getValue($scope) + ]; } } diff --git a/tests/app/src/Dispatcher/DispatcherWithoutScope.php b/tests/app/src/Dispatcher/DispatcherWithoutScope.php new file mode 100644 index 000000000..eb90d0b97 --- /dev/null +++ b/tests/app/src/Dispatcher/DispatcherWithoutScope.php @@ -0,0 +1,9 @@ + Date: Wed, 17 Jan 2024 18:22:17 +0200 Subject: [PATCH 10/15] Adding ConsoleDispatcher as class name instead of object --- src/Console/src/Bootloader/ConsoleBootloader.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Console/src/Bootloader/ConsoleBootloader.php b/src/Console/src/Bootloader/ConsoleBootloader.php index ff4a402b8..e522837ec 100644 --- a/src/Console/src/Bootloader/ConsoleBootloader.php +++ b/src/Console/src/Bootloader/ConsoleBootloader.php @@ -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; @@ -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( From ff6f8f1955c7eb0aa8d89760096276b5fc479b9f Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Wed, 17 Jan 2024 18:38:42 +0200 Subject: [PATCH 11/15] Add static method canServe to ConsoleDispatcher --- src/Framework/Console/ConsoleDispatcher.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Framework/Console/ConsoleDispatcher.php b/src/Framework/Console/ConsoleDispatcher.php index 794788717..34f6b6803 100644 --- a/src/Framework/Console/ConsoleDispatcher.php +++ b/src/Framework/Console/ConsoleDispatcher.php @@ -26,17 +26,16 @@ final class ConsoleDispatcher implements DispatcherInterface { public function __construct( - private readonly EnvironmentInterface $env, private readonly FinalizerInterface $finalizer, private readonly ContainerInterface $container, private readonly ExceptionHandlerInterface $errorHandler, ) { } - public function canServe(): bool + public static function canServe(EnvironmentInterface $env): bool { // only run in pure CLI more, ignore under RoadRunner - return (PHP_SAPI === 'cli' && $this->env->get('RR_MODE') === null); + return (PHP_SAPI === 'cli' && $env->get('RR_MODE') === null); } public function serve(InputInterface $input = null, OutputInterface $output = null): int From ad0491643f1059724598e46947404168ddf75136 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Thu, 18 Jan 2024 10:38:37 +0200 Subject: [PATCH 12/15] Remove EnvironmentInterface from canServe method --- src/Boot/src/DispatcherInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Boot/src/DispatcherInterface.php b/src/Boot/src/DispatcherInterface.php index dde920049..2d0119e14 100644 --- a/src/Boot/src/DispatcherInterface.php +++ b/src/Boot/src/DispatcherInterface.php @@ -7,7 +7,7 @@ /** * 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(EnvironmentInterface $env) + * @method static bool canServe() */ interface DispatcherInterface { From 9a8e7ae67b9e56897163166cb6d8596b45555c54 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Mon, 22 Jan 2024 12:19:35 +0200 Subject: [PATCH 13/15] Add descriptions for canServe method, dispatcher parameter --- src/Boot/src/AbstractKernel.php | 3 +++ src/Boot/src/DispatcherInterface.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Boot/src/AbstractKernel.php b/src/Boot/src/AbstractKernel.php index 6d8f7d6a9..efec3626e 100644 --- a/src/Boot/src/AbstractKernel.php +++ b/src/Boot/src/AbstractKernel.php @@ -263,6 +263,9 @@ 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 $dispatcher The class name or instance + * of the dispatcher. Since v4.0, it will only accept the class name. */ public function addDispatcher(string|DispatcherInterface $dispatcher): self { diff --git a/src/Boot/src/DispatcherInterface.php b/src/Boot/src/DispatcherInterface.php index 2d0119e14..95b4f13ac 100644 --- a/src/Boot/src/DispatcherInterface.php +++ b/src/Boot/src/DispatcherInterface.php @@ -7,7 +7,7 @@ /** * 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() + * @method static bool canServe() Must return true if the dispatcher expects to handle requests in a current environment */ interface DispatcherInterface { From c717a48c076f4cd884e6968cd6556948d8f98deb Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Mon, 22 Jan 2024 14:47:04 +0200 Subject: [PATCH 14/15] Up min version of spiral/testing --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d40537baf..969596a35 100644 --- a/composer.json +++ b/composer.json @@ -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", From 428b1d99a9252a15f9faec8a7790c746bcc97f4a Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 29 Feb 2024 14:01:52 +0400 Subject: [PATCH 15/15] Rename ScopeName to Spiral and mark it internal --- src/Core/tests/Attribute/ScopeTest.php | 4 ++-- src/Core/tests/Scope/UseCaseTest.php | 4 ++-- src/Framework/Console/ConsoleDispatcher.php | 4 ++-- src/Framework/Framework/{ScopeName.php => Spiral.php} | 5 ++++- tests/Framework/Attribute/DispatcherScopeTest.php | 4 ++-- tests/Framework/KernelTest.php | 4 ++-- tests/app/src/Dispatcher/DispatcherWithScopeName.php | 4 ++-- 7 files changed, 16 insertions(+), 13 deletions(-) rename src/Framework/Framework/{ScopeName.php => Spiral.php} (93%) diff --git a/src/Core/tests/Attribute/ScopeTest.php b/src/Core/tests/Attribute/ScopeTest.php index 81c443b48..f88e3247f 100644 --- a/src/Core/tests/Attribute/ScopeTest.php +++ b/src/Core/tests/Attribute/ScopeTest.php @@ -7,7 +7,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Spiral\Core\Attribute\Scope; -use Spiral\Framework\ScopeName; +use Spiral\Framework\Spiral; use Spiral\Tests\Core\Fixtures\ScopeEnum; final class ScopeTest extends TestCase @@ -23,7 +23,7 @@ public function testScope(string|\BackedEnum $name, string $expected): void public static function scopeNameDataProvider(): \Traversable { yield ['foo', 'foo']; - yield [ScopeName::HttpRequest, 'http.request']; + yield [Spiral::HttpRequest, 'http.request']; yield [ScopeEnum::A, 'a']; } } diff --git a/src/Core/tests/Scope/UseCaseTest.php b/src/Core/tests/Scope/UseCaseTest.php index 88ea5bf13..1943a5857 100644 --- a/src/Core/tests/Scope/UseCaseTest.php +++ b/src/Core/tests/Scope/UseCaseTest.php @@ -11,7 +11,7 @@ use Spiral\Core\Config\Shared; use Spiral\Core\Container; use Spiral\Core\Scope; -use Spiral\Framework\ScopeName; +use Spiral\Framework\Spiral; use Spiral\Tests\Core\Fixtures\Bucket; use Spiral\Tests\Core\Fixtures\Factory; use Spiral\Tests\Core\Fixtures\SampleClass; @@ -406,7 +406,7 @@ public function testHasInParentScopeWithScopeAttribute(): void public static function scopeEnumDataProvider(): \Traversable { - yield [ScopeName::HttpRequest, 'http.request']; + yield [Spiral::HttpRequest, 'http.request']; yield [ScopeEnum::A, 'a']; } } diff --git a/src/Framework/Console/ConsoleDispatcher.php b/src/Framework/Console/ConsoleDispatcher.php index 34f6b6803..e8b60b36b 100644 --- a/src/Framework/Console/ConsoleDispatcher.php +++ b/src/Framework/Console/ConsoleDispatcher.php @@ -12,7 +12,7 @@ use Spiral\Console\Logger\DebugListener; use Spiral\Exceptions\ExceptionHandlerInterface; use Spiral\Exceptions\Verbosity; -use Spiral\Framework\ScopeName; +use Spiral\Framework\Spiral; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; @@ -22,7 +22,7 @@ /** * Manages Console commands and exception. Lazy loads console service. */ -#[DispatcherScope(scope: ScopeName::Console)] +#[DispatcherScope(scope: Spiral::Console)] final class ConsoleDispatcher implements DispatcherInterface { public function __construct( diff --git a/src/Framework/Framework/ScopeName.php b/src/Framework/Framework/Spiral.php similarity index 93% rename from src/Framework/Framework/ScopeName.php rename to src/Framework/Framework/Spiral.php index 564af59c8..3b1e510b1 100644 --- a/src/Framework/Framework/ScopeName.php +++ b/src/Framework/Framework/Spiral.php @@ -4,7 +4,10 @@ namespace Spiral\Framework; -enum ScopeName: string +/** + * @internal + */ +enum Spiral: string { case Http = 'http'; case HttpRequest = 'http.request'; diff --git a/tests/Framework/Attribute/DispatcherScopeTest.php b/tests/Framework/Attribute/DispatcherScopeTest.php index 8d9a5f733..addf705a1 100644 --- a/tests/Framework/Attribute/DispatcherScopeTest.php +++ b/tests/Framework/Attribute/DispatcherScopeTest.php @@ -11,7 +11,7 @@ use Spiral\App\Dispatcher\Scope; use Spiral\Boot\AbstractKernel; use Spiral\Core\Container; -use Spiral\Framework\ScopeName; +use Spiral\Framework\Spiral; use Spiral\Tests\Framework\BaseTestCase; final class DispatcherScopeTest extends BaseTestCase @@ -33,7 +33,7 @@ public function testDispatcherScope(string $dispatcher, string|\BackedEnum $scop public static function dispatchersDataProvider(): \Traversable { - yield [DispatcherWithScopeName::class, ScopeName::Console]; + yield [DispatcherWithScopeName::class, Spiral::Console]; yield [DispatcherWithCustomEnum::class, Scope::Custom]; yield [DispatcherWithStringScope::class, 'test']; } diff --git a/tests/Framework/KernelTest.php b/tests/Framework/KernelTest.php index 669f224d4..efe16d99d 100644 --- a/tests/Framework/KernelTest.php +++ b/tests/Framework/KernelTest.php @@ -16,7 +16,7 @@ use Spiral\Boot\Exception\BootException; use Spiral\App\TestApp; use Spiral\Core\Container; -use Spiral\Framework\ScopeName; +use Spiral\Framework\Spiral; use stdClass; class KernelTest extends BaseTestCase @@ -136,7 +136,7 @@ public function testDispatchersShouldBeBoundInCorrectScope(string $dispatcher, s public static function dispatchersDataProvider(): \Traversable { - yield [DispatcherWithScopeName::class, ScopeName::Console->value]; + yield [DispatcherWithScopeName::class, Spiral::Console->value]; yield [DispatcherWithCustomEnum::class, Scope::Custom->value]; yield [DispatcherWithStringScope::class, 'test']; } diff --git a/tests/app/src/Dispatcher/DispatcherWithScopeName.php b/tests/app/src/Dispatcher/DispatcherWithScopeName.php index 47abf6326..e3cbd30d7 100644 --- a/tests/app/src/Dispatcher/DispatcherWithScopeName.php +++ b/tests/app/src/Dispatcher/DispatcherWithScopeName.php @@ -5,9 +5,9 @@ namespace Spiral\App\Dispatcher; use Spiral\Attribute\DispatcherScope; -use Spiral\Framework\ScopeName; +use Spiral\Framework\Spiral; -#[DispatcherScope(scope: ScopeName::Console)] +#[DispatcherScope(scope: Spiral::Console)] final class DispatcherWithScopeName extends AbstractDispatcher { }