diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index caff662..18a5eae 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -14,7 +14,5 @@ jobs: extensions: sockets, grpc os: >- ['ubuntu-latest'] - php: >- - ['8.1', '8.2'] stability: >- ['prefer-stable'] diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index df570fa..e6940e4 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -13,5 +13,3 @@ jobs: with: os: >- ['ubuntu-latest'] - php: >- - ['8.1'] diff --git a/composer.json b/composer.json index fac723c..1f49803 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "require-dev": { "spiral/framework": "^3.0", "spiral/testing": "^2.6", - "vimeo/psalm": "^5.0" + "vimeo/psalm": "^5.17" }, "autoload": { "psr-4": { diff --git a/psalm.xml b/psalm.xml index 615df74..d4a2702 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,9 +1,11 @@ diff --git a/src/Attribute/AssignWorker.php b/src/Attribute/AssignWorker.php index 6b2691d..b7f8028 100644 --- a/src/Attribute/AssignWorker.php +++ b/src/Attribute/AssignWorker.php @@ -4,11 +4,16 @@ namespace Spiral\TemporalBridge\Attribute; -#[\Attribute(\Attribute::TARGET_CLASS)] +use Spiral\Attributes\NamedArgumentConstructor; + +/** + * @psalm-suppress DeprecatedClass + */ +#[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor] final class AssignWorker { public function __construct( - public readonly string $name + public readonly string $name, ) { } } diff --git a/src/Dispatcher.php b/src/Dispatcher.php index 208c0e3..b6db7fa 100644 --- a/src/Dispatcher.php +++ b/src/Dispatcher.php @@ -21,7 +21,7 @@ public function __construct( private readonly RoadRunnerMode $mode, private readonly ReaderInterface $reader, private readonly TemporalConfig $config, - private readonly Container $container + private readonly Container $container, ) { } @@ -42,9 +42,12 @@ public function serve(): void $factory = $this->container->get(WorkerFactoryInterface::class); $registry = $this->container->get(WorkersRegistryInterface::class); + $hasDeclarations = false; foreach ($declarations as $type => $declaration) { // Worker that listens on a task queue and hosts both workflow and activity implementations. - $worker = $registry->get($this->resolveQueueName($declaration)); + $queueName = $this->resolveQueueName($declaration) ?? $this->config->getDefaultWorker(); + + $worker = $registry->get($queueName); if ($type === WorkflowInterface::class) { // Workflows are stateful. So you need a type to create instances. @@ -55,23 +58,36 @@ public function serve(): void // Workflows are stateful. So you need a type to create instances. $worker->registerActivity( $declaration->getName(), - fn(ReflectionClass $class): object => $this->container->make($class->getName()) + fn(ReflectionClass $class): object => $this->container->make($class->getName()), ); } + $hasDeclarations = true; + } + + if (!$hasDeclarations) { + $registry->get(WorkerFactoryInterface::DEFAULT_TASK_QUEUE); } // start primary loop $factory->run(); } - private function resolveQueueName(\ReflectionClass $declaration): string + private function resolveQueueName(\ReflectionClass $declaration): ?string { $assignWorker = $this->reader->firstClassMetadata($declaration, AssignWorker::class); - if ($assignWorker === null) { - return $this->config->getDefaultWorker(); + if ($assignWorker !== null) { + return $assignWorker->name; + } + + $parents = $declaration->getInterfaceNames(); + foreach ($parents as $parent) { + $queueName = $this->resolveQueueName(new \ReflectionClass($parent)); + if ($queueName !== null) { + return $queueName; + } } - return $assignWorker->name; + return null; } } diff --git a/tests/src/DispatcherTest.php b/tests/src/DispatcherTest.php index 29ed66c..75f33b6 100644 --- a/tests/src/DispatcherTest.php +++ b/tests/src/DispatcherTest.php @@ -8,34 +8,115 @@ use Spiral\RoadRunnerBridge\RoadRunnerMode; use Spiral\TemporalBridge\Attribute\AssignWorker; use Spiral\TemporalBridge\Config\TemporalConfig; +use Spiral\TemporalBridge\DeclarationLocatorInterface; use Spiral\TemporalBridge\Dispatcher; +use Spiral\TemporalBridge\Tests\App\SomeWorkflow; +use Spiral\TemporalBridge\WorkersRegistryInterface; +use Temporal\Worker\WorkerFactoryInterface; +use Temporal\Worker\WorkerInterface; +use Temporal\Workflow\WorkflowInterface; final class DispatcherTest extends TestCase { - public function testResolvingQueueName(): void + private \ReflectionMethod $method; + private Dispatcher $dispatcher; + + protected function setUp(): void { - $dispatcher = new Dispatcher( + parent::setUp(); + + $this->dispatcher = new Dispatcher( RoadRunnerMode::Temporal, new AttributeReader(), new TemporalConfig(['defaultWorker' => 'foo']), $this->getContainer(), ); - $ref = new \ReflectionClass($dispatcher); - $method = $ref->getMethod('resolveQueueName'); - $method->setAccessible(true); + $ref = new \ReflectionClass($this->dispatcher); + $this->method = $ref->getMethod('resolveQueueName'); + $this->method->setAccessible(true); + } - $queue = $method->invoke( - $dispatcher, - new \ReflectionClass(ActivityInterfaceWithAttribute::class) + public function testResolvingQueueNameWithAttributeOnClass(): void + { + $queue = $this->method->invoke( + $this->dispatcher, + new \ReflectionClass(ActivityInterfaceWithAttribute::class), ); + $this->assertSame('worker1', $queue); + } + + public function testResolvingQueueNameWithAttributeOnParentClass(): void + { + $queue = $this->method->invoke( + $this->dispatcher, + new \ReflectionClass(ActivityClass::class), + ); + + $this->assertSame('worker1', $queue); + } + + public function testResolvingQueueNameWithoutAttribute(): void + { + $queue = $this->method->invoke( + $this->dispatcher, + new \ReflectionClass(ActivityInterfaceWithoutAttribute::class), + ); + + $this->assertNull($queue); + } + + public function testServeWithoutDeclarations(): void + { + $dispatcher = new Dispatcher( + RoadRunnerMode::Temporal, + new AttributeReader(), + new TemporalConfig(), + $this->getContainer(), + ); + + $locator = $this->mockContainer(DeclarationLocatorInterface::class); + $locator->shouldReceive('getDeclarations')->once()->andReturn([]); + + $registry = $this->mockContainer(WorkersRegistryInterface::class); + $registry + ->shouldReceive('get') + ->once() + ->with(WorkerFactoryInterface::DEFAULT_TASK_QUEUE) + ->andReturn($this->createMock(WorkerInterface::class)); - $queue = $method->invoke( - $dispatcher, - new \ReflectionClass(ActivityInterfaceWithoutAttribute::class) + $factory = $this->mockContainer(WorkerFactoryInterface::class); + $factory->shouldReceive('run')->once(); + + $dispatcher->serve(); + } + + public function testServeWithDeclarations(): void + { + $dispatcher = new Dispatcher( + RoadRunnerMode::Temporal, + new AttributeReader(), + new TemporalConfig(), + $this->getContainer(), ); - $this->assertSame('foo', $queue); + + $locator = $this->mockContainer(DeclarationLocatorInterface::class); + $locator->shouldReceive('getDeclarations')->once()->andReturn([ + WorkflowInterface::class => new \ReflectionClass(SomeWorkflow::class), + ]); + + $registry = $this->mockContainer(WorkersRegistryInterface::class); + $registry + ->shouldReceive('get') + ->once() + ->with('worker2') + ->andReturn($this->createMock(WorkerInterface::class)); + + $factory = $this->mockContainer(WorkerFactoryInterface::class); + $factory->shouldReceive('run')->once(); + + $dispatcher->serve(); } } @@ -48,3 +129,7 @@ interface ActivityInterfaceWithAttribute interface ActivityInterfaceWithoutAttribute { } + +class ActivityClass implements ActivityInterfaceWithAttribute +{ +} diff --git a/tests/src/WorkersRegistryTest.php b/tests/src/WorkersRegistryTest.php index 1b5879a..e725998 100644 --- a/tests/src/WorkersRegistryTest.php +++ b/tests/src/WorkersRegistryTest.php @@ -40,7 +40,7 @@ public function testRegisterWorker(): void public function testRegisterWorkerWithExistsName(): void { $this->expectException(WorkersRegistryException::class); - $this->expectErrorMessage('Temporal worker with given name `foo` has already been registered.'); + $this->expectExceptionMessage('Temporal worker with given name `foo` has already been registered.'); $registry = new WorkersRegistry( $this->createMock(WorkerFactoryInterface::class),