From c8672f2af46629eacfb00c2c77496c5d32cb8e5c Mon Sep 17 00:00:00 2001 From: butschster Date: Fri, 22 Dec 2023 11:13:10 +0400 Subject: [PATCH] Adds temporal:info command --- .gitignore | 1 + src/Bootloader/TemporalBridgeBootloader.php | 3 +- src/Commands/InfoCommand.php | 88 ++++++++++++++++ src/DeclarationLocatorInterface.php | 8 ++ src/DeclarationWorkerResolver.php | 46 ++++++++ src/Dispatcher.php | 29 +---- src/WorkersRegistry.php | 6 +- src/WorkersRegistryInterface.php | 7 +- tests/src/Commands/InfoCommandTest.php | 111 ++++++++++++++++++++ tests/src/DeclarationWorkerResolverTest.php | 66 ++++++++++++ tests/src/DispatcherTest.php | 75 ++----------- 11 files changed, 340 insertions(+), 100 deletions(-) create mode 100644 src/Commands/InfoCommand.php create mode 100644 src/DeclarationWorkerResolver.php create mode 100644 tests/src/Commands/InfoCommandTest.php create mode 100644 tests/src/DeclarationWorkerResolverTest.php diff --git a/.gitignore b/.gitignore index 1a6ed30..29387cb 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ docs vendor node_modules .php-cs-fixer.cache +tests/runtime diff --git a/src/Bootloader/TemporalBridgeBootloader.php b/src/Bootloader/TemporalBridgeBootloader.php index 1555eab..ed90f71 100644 --- a/src/Bootloader/TemporalBridgeBootloader.php +++ b/src/Bootloader/TemporalBridgeBootloader.php @@ -38,7 +38,6 @@ class TemporalBridgeBootloader extends Bootloader public function defineDependencies(): array { return [ - ConsoleBootloader::class, RoadRunnerBootloader::class, ScaffolderBootloader::class, ]; @@ -64,10 +63,12 @@ public function init( AbstractKernel $kernel, EnvironmentInterface $env, FactoryInterface $factory, + ConsoleBootloader $console, ): void { $this->initConfig($env); $kernel->addDispatcher($factory->make(Dispatcher::class)); + $console->addCommand(Commands\InfoCommand::class); } public function addWorkerOptions(string $worker, WorkerOptions $options): void diff --git a/src/Commands/InfoCommand.php b/src/Commands/InfoCommand.php new file mode 100644 index 0000000..e0174cc --- /dev/null +++ b/src/Commands/InfoCommand.php @@ -0,0 +1,88 @@ +getDeclarations() as $type => $declaration) { + $taskQueue = $workerResolver->resolve($declaration); + + if ($type === WorkflowInterface::class) { + $prototype = $workflowReader->fromClass($declaration->getName()); + $workflows[$prototype->getID()] = [ + 'class' => $declaration->getName(), + 'file' => $declaration->getFileName(), + 'name' => $prototype->getID(), + 'task_queue' => $taskQueue, + ]; + } else { + foreach ($activityReader->fromClass($declaration->getName()) as $prototype) { + $activities[$declaration->getName()][$prototype->getID()] = [ + 'file' => $declaration->getFileName(), + 'name' => $prototype->getID(), + 'handler' => $declaration->getShortName() . '::' . $prototype->getHandler()->getName(), + 'task_queue' => $taskQueue, + ]; + } + } + } + + $rootDir = \realpath($dir->get('root')) . '/'; + + $this->output->title('Workflows'); + $table = $this->table(['Name', 'Class', 'Task Queue']); + foreach ($workflows as $workflow) { + $table->addRow([ + \sprintf('%s', $workflow['name']), + $workflow['class'] . "\n" . \sprintf('%s', \str_replace($rootDir, '', $workflow['file'])), + $workflow['task_queue'], + ]); + } + $table->render(); + + + $this->output->title('Activities'); + $table = $this->table(['Name', 'Class', 'Task Queue']); + foreach ($activities as $class => $prototypes) { + foreach ($prototypes as $prototype) { + $table->addRow([ + $prototype['name'], + $prototype['handler'], + $prototype['task_queue'], + ]); + } + if (\end($activities) !== $prototypes) { + $table->addRow(new TableSeparator()); + } + } + $table->render(); + + return self::SUCCESS; + } +} diff --git a/src/DeclarationLocatorInterface.php b/src/DeclarationLocatorInterface.php index 83256ca..22f0e2a 100644 --- a/src/DeclarationLocatorInterface.php +++ b/src/DeclarationLocatorInterface.php @@ -4,7 +4,15 @@ namespace Spiral\TemporalBridge; +use Temporal\Activity\ActivityInterface; +use Temporal\Workflow\WorkflowInterface; + interface DeclarationLocatorInterface { + /** + * List of all declarations for workflows and activities. + * + * @return iterable|class-string, \ReflectionClass> + */ public function getDeclarations(): iterable; } diff --git a/src/DeclarationWorkerResolver.php b/src/DeclarationWorkerResolver.php new file mode 100644 index 0000000..e680c1f --- /dev/null +++ b/src/DeclarationWorkerResolver.php @@ -0,0 +1,46 @@ +resolveQueueName($declaration) ?? $this->config->getDefaultWorker(); + } + + private function resolveQueueName(\ReflectionClass $declaration): ?string + { + $assignWorker = $this->reader->firstClassMetadata($declaration, AssignWorker::class); + + 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 null; + } +} diff --git a/src/Dispatcher.php b/src/Dispatcher.php index b6db7fa..b7b55e1 100644 --- a/src/Dispatcher.php +++ b/src/Dispatcher.php @@ -5,12 +5,9 @@ namespace Spiral\TemporalBridge; use ReflectionClass; -use Spiral\Attributes\ReaderInterface; use Spiral\Boot\DispatcherInterface; use Spiral\Core\Container; use Spiral\RoadRunnerBridge\RoadRunnerMode; -use Spiral\TemporalBridge\Attribute\AssignWorker; -use Spiral\TemporalBridge\Config\TemporalConfig; use Temporal\Activity\ActivityInterface; use Temporal\Worker\WorkerFactoryInterface; use Temporal\Workflow\WorkflowInterface; @@ -19,9 +16,8 @@ final class Dispatcher implements DispatcherInterface { public function __construct( private readonly RoadRunnerMode $mode, - private readonly ReaderInterface $reader, - private readonly TemporalConfig $config, private readonly Container $container, + private readonly DeclarationWorkerResolver $workerResolver, ) { } @@ -39,13 +35,15 @@ public function serve(): void $declarations = $this->container->get(DeclarationLocatorInterface::class)->getDeclarations(); // factory initiates and runs task queue specific activity and workflow workers + /** @var WorkerFactoryInterface $factory */ $factory = $this->container->get(WorkerFactoryInterface::class); + /** @var WorkersRegistryInterface $registry */ $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. - $queueName = $this->resolveQueueName($declaration) ?? $this->config->getDefaultWorker(); + $queueName = $this->workerResolver->resolve($declaration); $worker = $registry->get($queueName); @@ -71,23 +69,4 @@ public function serve(): void // start primary loop $factory->run(); } - - private function resolveQueueName(\ReflectionClass $declaration): ?string - { - $assignWorker = $this->reader->firstClassMetadata($declaration, AssignWorker::class); - - 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 null; - } } diff --git a/src/WorkersRegistry.php b/src/WorkersRegistry.php index c8e6f43..a6489ef 100644 --- a/src/WorkersRegistry.php +++ b/src/WorkersRegistry.php @@ -20,7 +20,7 @@ final class WorkersRegistry implements WorkersRegistryInterface public function __construct( private readonly WorkerFactoryInterface $workerFactory, private readonly FinalizerInterface $finalizer, - private readonly TemporalConfig $config + private readonly TemporalConfig $config, ) { } @@ -30,7 +30,7 @@ public function register(string $name, ?WorkerOptions $options): void if ($this->has($name)) { throw new WorkersRegistryException( - \sprintf('Temporal worker with given name `%s` has already been registered.', $name) + \sprintf('Temporal worker with given name `%s` has already been registered.', $name), ); } @@ -44,7 +44,7 @@ public function get(string $name): WorkerInterface $options = $this->config->getWorkers(); - if (! $this->has($name)) { + if (!$this->has($name)) { $this->register($name, $options[$name] ?? null); } diff --git a/src/WorkersRegistryInterface.php b/src/WorkersRegistryInterface.php index c176a20..0743c5b 100644 --- a/src/WorkersRegistryInterface.php +++ b/src/WorkersRegistryInterface.php @@ -11,18 +11,19 @@ interface WorkersRegistryInterface { /** - * Register a new temporal worker with given task queue and options + * Register a new temporal worker with given task queue and options. + * * @throws WorkersRegistryException */ public function register(string $name, ?WorkerOptions $options): void; /** - * Get or create worker by task queue name + * Get or create worker by task queue name. */ public function get(string $name): WorkerInterface; /** - * Check if a worker with given task queue name registered + * Check if a worker with given task queue name registered. */ public function has(string $name): bool; } diff --git a/tests/src/Commands/InfoCommandTest.php b/tests/src/Commands/InfoCommandTest.php new file mode 100644 index 0000000..097e507 --- /dev/null +++ b/tests/src/Commands/InfoCommandTest.php @@ -0,0 +1,111 @@ +mockContainer(DeclarationLocatorInterface::class); + $locator->shouldReceive('getDeclarations')->andReturnUsing(function () { + yield WorkflowInterface::class => new \ReflectionClass(Workflow::class); + yield ActivityInterface::class => new \ReflectionClass(ActivityInterfaceWithWorker::class); + yield ActivityInterface::class => new \ReflectionClass(ActivityInterfaceWithoutWorker::class); + yield WorkflowInterface::class => new \ReflectionClass(AnotherWorkflow::class); + }); + } + + public function testInfo(): void + { + $result = $this->runCommand('temporal:info'); + + $this->assertSame( + <<<'OUTPUT' + +Workflows +========= + ++-----------------+------------------------------------------------------+------------+ +| Name | Class | Task Queue | ++-----------------+------------------------------------------------------+------------+ +| fooWorkflow | Spiral\TemporalBridge\Tests\Commands\Workflow | worker2 | +| | src/Commands/InfoCommandTest.php | | +| AnotherWorkflow | Spiral\TemporalBridge\Tests\Commands\AnotherWorkflow | default | +| | src/Commands/InfoCommandTest.php | | ++-----------------+------------------------------------------------------+------------+ + +Activities +========== + ++----------------+-------------------------------------+------------+ +| Name | Class | Task Queue | ++----------------+-------------------------------------+------------+ +| fooActivity | ActivityInterfaceWithWorker::foo | worker1 | +| bar | ActivityInterfaceWithWorker::bar | worker1 | ++----------------+-------------------------------------+------------+ +| fooActivitybaz | ActivityInterfaceWithoutWorker::baz | default | ++----------------+-------------------------------------+------------+ + +OUTPUT, + $result, + ); + } +} + +#[AssignWorker(name: 'worker1')] +#[ActivityInterface] +class ActivityInterfaceWithWorker +{ + #[ActivityMethod('fooActivity')] + public function foo(): void + { + } + + #[ActivityMethod] + public function bar(): void + { + } +} + + +#[ActivityInterface('fooActivity')] +class ActivityInterfaceWithoutWorker +{ + + #[ActivityMethod] + public function baz(): void + { + } +} + +#[AssignWorker(name: 'worker2')] +#[WorkflowInterface] +class Workflow +{ + #[WorkflowMethod('fooWorkflow')] + public function handle() + { + } +} + +#[AssignWorker(name: 'default')] +#[WorkflowInterface] +class AnotherWorkflow +{ + #[WorkflowMethod] + public function handle() + { + } +} diff --git a/tests/src/DeclarationWorkerResolverTest.php b/tests/src/DeclarationWorkerResolverTest.php new file mode 100644 index 0000000..38ffd1e --- /dev/null +++ b/tests/src/DeclarationWorkerResolverTest.php @@ -0,0 +1,66 @@ +resolver = new DeclarationWorkerResolver( + new AttributeReader(), + new TemporalConfig(['defaultWorker' => 'foo']), + ); + } + + public function testResolvingQueueNameWithAttributeOnClass(): void + { + $queue = $this->resolver->resolve( + new \ReflectionClass(ActivityInterfaceWithAttribute::class), + ); + + $this->assertSame('worker1', $queue); + } + + public function testResolvingQueueNameWithAttributeOnParentClass(): void + { + $queue = $this->resolver->resolve( + new \ReflectionClass(ActivityClass::class), + ); + + $this->assertSame('worker1', $queue); + } + + public function testResolvingQueueNameWithoutAttribute(): void + { + $queue = $this->resolver->resolve( + new \ReflectionClass(ActivityInterfaceWithoutAttribute::class), + ); + + $this->assertSame('foo', $queue); + } +} + +#[AssignWorker(name: 'worker1')] +interface ActivityInterfaceWithAttribute +{ +} + + +interface ActivityInterfaceWithoutAttribute +{ +} + +class ActivityClass implements ActivityInterfaceWithAttribute +{ +} diff --git a/tests/src/DispatcherTest.php b/tests/src/DispatcherTest.php index 75f33b6..ccfdfab 100644 --- a/tests/src/DispatcherTest.php +++ b/tests/src/DispatcherTest.php @@ -6,9 +6,9 @@ use Spiral\Attributes\AttributeReader; use Spiral\RoadRunnerBridge\RoadRunnerMode; -use Spiral\TemporalBridge\Attribute\AssignWorker; use Spiral\TemporalBridge\Config\TemporalConfig; use Spiral\TemporalBridge\DeclarationLocatorInterface; +use Spiral\TemporalBridge\DeclarationWorkerResolver; use Spiral\TemporalBridge\Dispatcher; use Spiral\TemporalBridge\Tests\App\SomeWorkflow; use Spiral\TemporalBridge\WorkersRegistryInterface; @@ -18,7 +18,6 @@ final class DispatcherTest extends TestCase { - private \ReflectionMethod $method; private Dispatcher $dispatcher; protected function setUp(): void @@ -27,55 +26,16 @@ protected function setUp(): void $this->dispatcher = new Dispatcher( RoadRunnerMode::Temporal, - new AttributeReader(), - new TemporalConfig(['defaultWorker' => 'foo']), $this->getContainer(), + new DeclarationWorkerResolver( + new AttributeReader(), + new TemporalConfig(['defaultWorker' => 'foo']), + ) ); - - $ref = new \ReflectionClass($this->dispatcher); - $this->method = $ref->getMethod('resolveQueueName'); - $this->method->setAccessible(true); - } - - 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([]); @@ -89,18 +49,11 @@ public function testServeWithoutDeclarations(): void $factory = $this->mockContainer(WorkerFactoryInterface::class); $factory->shouldReceive('run')->once(); - $dispatcher->serve(); + $this->dispatcher->serve(); } public function testServeWithDeclarations(): void { - $dispatcher = new Dispatcher( - RoadRunnerMode::Temporal, - new AttributeReader(), - new TemporalConfig(), - $this->getContainer(), - ); - $locator = $this->mockContainer(DeclarationLocatorInterface::class); $locator->shouldReceive('getDeclarations')->once()->andReturn([ WorkflowInterface::class => new \ReflectionClass(SomeWorkflow::class), @@ -116,20 +69,6 @@ public function testServeWithDeclarations(): void $factory = $this->mockContainer(WorkerFactoryInterface::class); $factory->shouldReceive('run')->once(); - $dispatcher->serve(); + $this->dispatcher->serve(); } } - -#[AssignWorker(name: 'worker1')] -interface ActivityInterfaceWithAttribute -{ -} - - -interface ActivityInterfaceWithoutAttribute -{ -} - -class ActivityClass implements ActivityInterfaceWithAttribute -{ -}