diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index e6940e4..4c1f9dd 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -8,8 +8,8 @@ on: name: static analysis jobs: - psalm: - uses: spiral/gh-actions/.github/workflows/psalm.yml@master - with: - os: >- - ['ubuntu-latest'] + psalm: + uses: spiral/gh-actions/.github/workflows/psalm.yml@master + with: + os: >- + ['ubuntu-latest'] diff --git a/composer.json b/composer.json index 1f49803..1104a89 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "spiral/tokenizer": "^3.0", "spiral/scaffolder": "^3.0", "spiral/roadrunner-bridge": "^2.0 || ^3.0", - "temporal/sdk": "^1.3 || ^2.0" + "temporal/sdk": "^2.7" }, "require-dev": { "spiral/framework": "^3.0", diff --git a/src/Bootloader/PrototypeBootloader.php b/src/Bootloader/PrototypeBootloader.php index 29bbb0e..3e65690 100644 --- a/src/Bootloader/PrototypeBootloader.php +++ b/src/Bootloader/PrototypeBootloader.php @@ -8,7 +8,7 @@ use Spiral\Prototype\Bootloader\PrototypeBootloader as BasePrototypeBootloader; use Temporal\Client\WorkflowClientInterface; -class PrototypeBootloader extends Bootloader +final class PrototypeBootloader extends Bootloader { public function defineDependencies(): array { diff --git a/src/Bootloader/TemporalBridgeBootloader.php b/src/Bootloader/TemporalBridgeBootloader.php index ed90f71..deecddd 100644 --- a/src/Bootloader/TemporalBridgeBootloader.php +++ b/src/Bootloader/TemporalBridgeBootloader.php @@ -8,10 +8,10 @@ use Spiral\Boot\AbstractKernel; use Spiral\Boot\Bootloader\Bootloader; use Spiral\Boot\EnvironmentInterface; -use Spiral\Boot\FinalizerInterface; use Spiral\Config\ConfiguratorInterface; use Spiral\Config\Patch\Append; use Spiral\Console\Bootloader\ConsoleBootloader; +use Spiral\Core\Container\Autowire; use Spiral\Core\FactoryInterface; use Spiral\RoadRunnerBridge\Bootloader\RoadRunnerBootloader; use Spiral\TemporalBridge\Commands; @@ -19,20 +19,30 @@ use Spiral\TemporalBridge\DeclarationLocator; use Spiral\TemporalBridge\DeclarationLocatorInterface; use Spiral\TemporalBridge\Dispatcher; +use Spiral\TemporalBridge\WorkerFactory; +use Spiral\TemporalBridge\WorkerFactoryInterface; use Spiral\TemporalBridge\WorkersRegistry; use Spiral\TemporalBridge\WorkersRegistryInterface; use Spiral\Tokenizer\ClassesInterface; -use Temporal\Client\ClientOptions; use Temporal\Client\GRPC\ServiceClient; use Temporal\Client\WorkflowClient; use Temporal\Client\WorkflowClientInterface; use Temporal\DataConverter\DataConverter; use Temporal\DataConverter\DataConverterInterface; +use Temporal\Interceptor\PipelineProvider; +use Temporal\Interceptor\SimplePipelineProvider; +use Temporal\Internal\Interceptor\Interceptor; use Temporal\Worker\Transport\Goridge; -use Temporal\Worker\WorkerFactoryInterface; +use Temporal\Worker\WorkerFactoryInterface as TemporalWorkerFactoryInterface; use Temporal\Worker\WorkerOptions; -use Temporal\WorkerFactory; +use Temporal\WorkerFactory as TemporalWorkerFactory; +use Temporal\Client\ScheduleClient; +use Temporal\Client\ScheduleClientInterface; +use Temporal\Client\GRPC\ServiceClientInterface; +/** + * @psalm-import-type TInterceptor from TemporalConfig + */ class TemporalBridgeBootloader extends Bootloader { public function defineDependencies(): array @@ -46,29 +56,33 @@ public function defineDependencies(): array public function defineSingletons(): array { return [ - WorkerFactoryInterface::class => [self::class, 'initWorkerFactory'], + TemporalWorkerFactoryInterface::class => [self::class, 'initWorkerFactory'], + WorkerFactoryInterface::class => WorkerFactory::class, DeclarationLocatorInterface::class => [self::class, 'initDeclarationLocator'], WorkflowClientInterface::class => [self::class, 'initWorkflowClient'], - WorkersRegistryInterface::class => [self::class, 'initWorkersRegistry'], + WorkersRegistryInterface::class => WorkersRegistry::class, + ScheduleClientInterface::class => [self::class, 'initScheduleClient'], DataConverterInterface::class => [self::class, 'initDataConverter'], + PipelineProvider::class => [self::class, 'initPipelineProvider'], + ServiceClientInterface::class => [self::class, 'initServiceClient'], ]; } public function __construct( private readonly ConfiguratorInterface $config, + private readonly FactoryInterface $factory, ) { } 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); + $kernel->addDispatcher($this->factory->make(Dispatcher::class)); } public function addWorkerOptions(string $worker, WorkerOptions $options): void @@ -76,6 +90,32 @@ public function addWorkerOptions(string $worker, WorkerOptions $options): void $this->config->modify(TemporalConfig::CONFIG, new Append('workers', $worker, $options)); } + /** + * Register a new Temporal interceptor. + * + * @param TInterceptor $interceptor + */ + public function addInterceptor(string|Interceptor|Autowire $interceptor): void + { + if (\is_string($interceptor)) { + $interceptor = $this->factory->make($interceptor); + } elseif ($interceptor instanceof Autowire) { + $interceptor = $interceptor->resolve($this->factory); + } + + if (!$interceptor instanceof Interceptor) { + throw new \InvalidArgumentException( + \sprintf( + 'Interceptor must be an instance of `%s`, `%s` given.', + Interceptor::class, + \get_class($interceptor), + ), + ); + } + + $this->config->modify(TemporalConfig::CONFIG, new Append('interceptors', null, $interceptor)); + } + protected function initConfig(EnvironmentInterface $env): void { $this->config->setDefaults( @@ -83,8 +123,12 @@ protected function initConfig(EnvironmentInterface $env): void [ 'address' => $env->get('TEMPORAL_ADDRESS', '127.0.0.1:7233'), 'namespace' => 'App\\Endpoint\\Temporal\\Workflow', - 'defaultWorker' => (string)$env->get('TEMPORAL_TASK_QUEUE', WorkerFactoryInterface::DEFAULT_TASK_QUEUE), + 'defaultWorker' => (string)$env->get( + 'TEMPORAL_TASK_QUEUE', + TemporalWorkerFactoryInterface::DEFAULT_TASK_QUEUE, + ), 'workers' => [], + 'clientOptions' => null, ], ); } @@ -92,11 +136,14 @@ protected function initConfig(EnvironmentInterface $env): void protected function initWorkflowClient( TemporalConfig $config, DataConverterInterface $dataConverter, + PipelineProvider $pipelineProvider, + ServiceClientInterface $serviceClient, ): WorkflowClientInterface { - return WorkflowClient::create( - serviceClient: ServiceClient::create($config->getAddress()), - options: (new ClientOptions())->withNamespace($config->getTemporalNamespace()), + return new WorkflowClient( + serviceClient: $serviceClient, + options: $config->getClientOptions(), converter: $dataConverter, + interceptorProvider: $pipelineProvider, ); } @@ -105,29 +152,51 @@ protected function initDataConverter(): DataConverterInterface return DataConverter::createDefault(); } - protected function initWorkerFactory( - DataConverterInterface $dataConverter, - ): WorkerFactoryInterface { - return new WorkerFactory( + protected function initWorkerFactory(DataConverterInterface $dataConverter,): TemporalWorkerFactoryInterface + { + return new TemporalWorkerFactory( dataConverter: $dataConverter, rpc: Goridge::create(), ); } - protected function initDeclarationLocator( - ClassesInterface $classes, - ): DeclarationLocatorInterface { + protected function initDeclarationLocator(ClassesInterface $classes,): DeclarationLocatorInterface + { return new DeclarationLocator( classes: $classes, reader: new AttributeReader(), ); } - protected function initWorkersRegistry( - WorkerFactoryInterface $workerFactory, - FinalizerInterface $finalizer, + protected function initPipelineProvider(TemporalConfig $config, FactoryInterface $factory): PipelineProvider + { + /** @var Interceptor[] $interceptors */ + $interceptors = \array_map( + static fn(mixed $interceptor) => match (true) { + \is_string($interceptor) => $factory->make($interceptor), + $interceptor instanceof Autowire => $interceptor->resolve($factory), + default => $interceptor + }, + $config->getInterceptors(), + ); + + return new SimplePipelineProvider($interceptors); + } + + protected function initServiceClient(TemporalConfig $config): ServiceClientInterface + { + return ServiceClient::create($config->getAddress()); + } + + protected function initScheduleClient( TemporalConfig $config, - ): WorkersRegistryInterface { - return new WorkersRegistry($workerFactory, $finalizer, $config); + DataConverterInterface $dataConverter, + ServiceClientInterface $serviceClient, + ): ScheduleClientInterface { + return new ScheduleClient( + serviceClient: $serviceClient, + options: $config->getClientOptions(), + converter: $dataConverter, + ); } } diff --git a/src/Config/TemporalConfig.php b/src/Config/TemporalConfig.php index 78a28b4..9853921 100644 --- a/src/Config/TemporalConfig.php +++ b/src/Config/TemporalConfig.php @@ -4,10 +4,32 @@ namespace Spiral\TemporalBridge\Config; +use Spiral\Core\Container\Autowire; use Spiral\Core\InjectableConfig; +use Temporal\Client\ClientOptions; +use Temporal\Exception\ExceptionInterceptorInterface; +use Temporal\Internal\Interceptor\Interceptor; use Temporal\Worker\WorkerFactoryInterface; use Temporal\Worker\WorkerOptions; +/** + * @psalm-type TInterceptor = Interceptor|class-string|Autowire + * @psalm-type TExceptionInterceptor = ExceptionInterceptorInterface|class-string|Autowire + * @psalm-type TWorker = array{ + * options?: WorkerOptions, + * exception_interceptor?: TExceptionInterceptor + * } + * + * @property array{ + * address: non-empty-string, + * namespace: non-empty-string, + * temporalNamespace: non-empty-string, + * defaultWorker: non-empty-string, + * workers: array, + * interceptors?: TInterceptor[], + * clientOptions?: ClientOptions + * } $config + */ final class TemporalConfig extends InjectableConfig { public const CONFIG = 'temporal'; @@ -18,31 +40,60 @@ final class TemporalConfig extends InjectableConfig 'temporalNamespace' => 'default', 'defaultWorker' => WorkerFactoryInterface::DEFAULT_TASK_QUEUE, 'workers' => [], + 'interceptors' => [], + 'clientOptions' => null, ]; + /** + * @return non-empty-string + */ public function getDefaultNamespace(): string { return $this->config['namespace']; } + /** + * @return non-empty-string + */ public function getTemporalNamespace(): string { return $this->config['temporalNamespace']; } + /** + * @return non-empty-string + */ public function getAddress(): string { return $this->config['address']; } + /** + * @return non-empty-string + */ public function getDefaultWorker(): string { return $this->config['defaultWorker']; } - /** @psalm-return array */ + /** + * @return array + */ public function getWorkers(): array { - return (array) $this->config['workers']; + return $this->config['workers'] ?? []; + } + + /** + * @return TInterceptor[] + */ + public function getInterceptors(): array + { + return $this->config['interceptors'] ?? []; + } + + public function getClientOptions(): ClientOptions + { + return $this->config['clientOptions'] ?? (new ClientOptions())->withNamespace($this->getTemporalNamespace()); } } diff --git a/src/WorkerFactory.php b/src/WorkerFactory.php new file mode 100644 index 0000000..f9ec100 --- /dev/null +++ b/src/WorkerFactory.php @@ -0,0 +1,90 @@ + */ + private array $workers = []; + + public function __construct( + private readonly TemporalWorkerFactory $workerFactory, + private readonly FinalizerInterface $finalizer, + private readonly FactoryInterface $factory, + private readonly PipelineProvider $pipelineProvider, + private readonly TemporalConfig $config, + ) { + $this->workers = $this->config->getWorkers(); + } + + /** + * @param non-empty-string $name + */ + public function create(string $name): WorkerInterface + { + /** @psalm-suppress TooManyArguments */ + $worker = $this->workerFactory->newWorker( + $name, + $this->getWorkerOptions($name), + $this->getExceptionInterceptor($name), + $this->pipelineProvider, + ); + $worker->registerActivityFinalizer(fn () => $this->finalizer->finalize()); + + return $worker; + } + + /** + * @param non-empty-string $name + */ + private function getWorkerOptions(string $name): ?WorkerOptions + { + $worker = $this->workers[$name] ?? null; + + return match (true) { + $worker instanceof WorkerOptions => $worker, + isset($worker['options']) && $worker['options'] instanceof WorkerOptions => $worker['options'], + default => null + }; + } + + /** + * @param non-empty-string $name + */ + private function getExceptionInterceptor(string $name): ?ExceptionInterceptorInterface + { + $worker = $this->workers[$name] ?? null; + if (!\is_array($worker) || !isset($worker['exception_interceptor'])) { + return null; + } + + $exceptionInterceptor = $this->wire($worker['exception_interceptor']); + \assert($exceptionInterceptor instanceof ExceptionInterceptorInterface); + + return $exceptionInterceptor; + } + + private function wire(mixed $alias): object + { + return match (true) { + \is_string($alias) => $this->factory->make($alias), + $alias instanceof Autowire => $alias->resolve($this->factory), + default => $alias + }; + } +} diff --git a/src/WorkerFactoryInterface.php b/src/WorkerFactoryInterface.php new file mode 100644 index 0000000..34aa9c8 --- /dev/null +++ b/src/WorkerFactoryInterface.php @@ -0,0 +1,17 @@ + $options */ public function __construct( - private readonly WorkerFactoryInterface $workerFactory, + private readonly WorkerFactoryInterface|TemporalWorkerFactory $workerFactory, private readonly FinalizerInterface $finalizer, private readonly TemporalConfig $config, ) { @@ -30,22 +30,26 @@ 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) ); } - $this->workers[$name] = $this->workerFactory->newWorker($name, $options); - $this->workers[$name]->registerActivityFinalizer(fn() => $this->finalizer->finalize()); + if ($this->workerFactory instanceof WorkerFactoryInterface) { + $this->workers[$name] = $this->workerFactory->create($name); + } else { + $this->workers[$name] = $this->workerFactory->newWorker($name, $options); + $this->workers[$name]->registerActivityFinalizer(fn() => $this->finalizer->finalize()); + } } public function get(string $name): WorkerInterface { \assert($name !== ''); - $options = $this->config->getWorkers(); + $options = $this->config->getWorkers()[$name] ?? null; - if (!$this->has($name)) { - $this->register($name, $options[$name] ?? null); + if (! $this->has($name)) { + $this->register($name, $options instanceof WorkerOptions ? $options : null); } return $this->workers[$name]; diff --git a/tests/app/src/SomeInterceptor.php b/tests/app/src/SomeInterceptor.php new file mode 100644 index 0000000..ba447ca --- /dev/null +++ b/tests/app/src/SomeInterceptor.php @@ -0,0 +1,15 @@ +create(); @@ -21,7 +22,7 @@ public function testAssignWorkerAttribute(\ReflectionClass $class, ?AssignWorker $this->assertEquals($expected, $reader->firstClassMetadata($class, AssignWorker::class)); } - public function assignWorkerDataProvider(): \Traversable + public static function assignWorkerDataProvider(): \Traversable { yield [new \ReflectionClass(SomeActivity::class), new AssignWorker('worker1')]; yield [new \ReflectionClass(SomeWorkflow::class), new AssignWorker('worker2')]; diff --git a/tests/src/Bootloader/PrototypeBootloaderTest.php b/tests/src/Bootloader/PrototypeBootloaderTest.php deleted file mode 100644 index 0d082e7..0000000 --- a/tests/src/Bootloader/PrototypeBootloaderTest.php +++ /dev/null @@ -1,29 +0,0 @@ -getContainer()->get(PrototypeRegistry::class); - - $this->assertInstanceOf( - $expected, - $this->getContainer()->get($registry->resolveProperty($property)->type->name()) - ); - } - - public function propertiesDataProvider(): \Traversable - { - yield [WorkflowClientInterface::class, 'workflow']; - } -} diff --git a/tests/src/Bootloader/TemporalBridgeBootloaderTest.php b/tests/src/Bootloader/TemporalBridgeBootloaderTest.php index f3e5604..5a4b7e7 100644 --- a/tests/src/Bootloader/TemporalBridgeBootloaderTest.php +++ b/tests/src/Bootloader/TemporalBridgeBootloaderTest.php @@ -4,6 +4,9 @@ namespace Spiral\TemporalBridge\Tests\Bootloader; +use Mockery as m; +use Spiral\Core\Container\Autowire; +use Spiral\Core\FactoryInterface; use Spiral\TemporalBridge\Bootloader\TemporalBridgeBootloader; use Spiral\TemporalBridge\Config\TemporalConfig; use Spiral\Config\ConfigManager; @@ -11,19 +14,52 @@ use Spiral\TemporalBridge\DeclarationLocator; use Spiral\TemporalBridge\DeclarationLocatorInterface; use Spiral\TemporalBridge\Tests\TestCase; +use Spiral\TemporalBridge\WorkerFactory; +use Spiral\TemporalBridge\WorkerFactoryInterface; use Spiral\TemporalBridge\WorkersRegistry; use Spiral\TemporalBridge\WorkersRegistryInterface; +use Temporal\Client\GRPC\ServiceClient; +use Temporal\Client\GRPC\ServiceClientInterface; +use Temporal\Client\ScheduleClient; +use Temporal\Client\ScheduleClientInterface; use Temporal\Client\WorkflowClient; use Temporal\Client\WorkflowClientInterface; use Temporal\DataConverter\DataConverter; use Temporal\DataConverter\DataConverterInterface; -use Temporal\Worker\WorkerFactoryInterface; +use Temporal\Interceptor\SimplePipelineProvider; +use Temporal\Interceptor\PipelineProvider; +use Temporal\Internal\Interceptor\Interceptor; +use Temporal\Worker\WorkerFactoryInterface as TemporalWorkerFactoryInterface; use Temporal\Worker\WorkerOptions; -use Temporal\WorkerFactory; +use Temporal\WorkerFactory as TemporalWorkerFactory; class TemporalBridgeBootloaderTest extends TestCase { - public function testWorkerFactory() + public function testServiceClient(): void + { + $this->assertContainerBoundAsSingleton( + ServiceClientInterface::class, + ServiceClient::class, + ); + } + + public function testScheduleClient(): void + { + $this->assertContainerBoundAsSingleton( + ScheduleClientInterface::class, + ScheduleClient::class, + ); + } + + public function testTemporalWorkerFactory(): void + { + $this->assertContainerBoundAsSingleton( + TemporalWorkerFactoryInterface::class, + TemporalWorkerFactory::class, + ); + } + + public function testWorkerFactory(): void { $this->assertContainerBoundAsSingleton( WorkerFactoryInterface::class, @@ -31,27 +67,27 @@ public function testWorkerFactory() ); } - public function testDataConverter() + public function testDataConverter(): void { $this->assertContainerBoundAsSingleton( DataConverterInterface::class, - DataConverter::class + DataConverter::class, ); } - public function testDeclarationLocator() + public function testDeclarationLocator(): void { $this->assertContainerBoundAsSingleton( DeclarationLocatorInterface::class, - DeclarationLocator::class + DeclarationLocator::class, ); } - public function testWorkflowClient() + public function testWorkflowClient(): void { $this->assertContainerBoundAsSingleton( WorkflowClientInterface::class, - WorkflowClient::class + WorkflowClient::class, ); } @@ -59,7 +95,15 @@ public function testWorkersRegistry(): void { $this->assertContainerBoundAsSingleton( WorkersRegistryInterface::class, - WorkersRegistry::class + WorkersRegistry::class, + ); + } + + public function testPipelineProvider(): void + { + $this->assertContainerBound( + PipelineProvider::class, + SimplePipelineProvider::class, ); } @@ -68,13 +112,77 @@ public function testAddWorkerOptions(): void $configs = new ConfigManager($this->createMock(LoaderInterface::class)); $configs->setDefaults(TemporalConfig::CONFIG, ['workers' => []]); - $bootloader = new TemporalBridgeBootloader($configs); + $bootloader = new TemporalBridgeBootloader($configs, $this->getContainer()); $bootloader->addWorkerOptions('first', $first = WorkerOptions::new()); $bootloader->addWorkerOptions('second', $second = WorkerOptions::new()); $this->assertSame( ['first' => $first, 'second' => $second], - $configs->getConfig(TemporalConfig::CONFIG)['workers'] + $configs->getConfig(TemporalConfig::CONFIG)['workers'], + ); + } + + public function testAddInterceptor(): void + { + $configs = new ConfigManager($this->createMock(LoaderInterface::class)); + $configs->setDefaults(TemporalConfig::CONFIG, ['interceptors' => []]); + + $bootloader = new TemporalBridgeBootloader($configs, $this->getContainer()); + + $bootloader->addInterceptor($iterceptor = m::mock(Interceptor::class)); + + $this->assertSame( + [$iterceptor], + $configs->getConfig(TemporalConfig::CONFIG)['interceptors'], ); } + + public function testStringableInterceptor(): void + { + $configs = new ConfigManager($this->createMock(LoaderInterface::class)); + $configs->setDefaults(TemporalConfig::CONFIG, ['interceptors' => []]); + + $bootloader = new TemporalBridgeBootloader($configs, $factory = m::mock(FactoryInterface::class)); + + $factory->shouldReceive('make')->with('foo')->andReturn($iterceptor = m::mock(Interceptor::class)); + + $bootloader->addInterceptor('foo'); + + $this->assertSame( + [$iterceptor], + $configs->getConfig(TemporalConfig::CONFIG)['interceptors'], + ); + } + + public function testAutowireInterceptor(): void + { + $configs = new ConfigManager($this->createMock(LoaderInterface::class)); + $configs->setDefaults(TemporalConfig::CONFIG, ['interceptors' => []]); + + $bootloader = new TemporalBridgeBootloader($configs, $factory = m::mock(FactoryInterface::class)); + + $factory->shouldReceive('make')->with('foo', ['bar' => 'baz'])->andReturn($iterceptor = m::mock(Interceptor::class)); + + $bootloader->addInterceptor(new Autowire('foo', ['bar' => 'baz'])); + + $this->assertSame( + [$iterceptor], + $configs->getConfig(TemporalConfig::CONFIG)['interceptors'], + ); + } + + public function testInvalidInterceptor(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Interceptor must be an instance of `Temporal\Internal\Interceptor\Interceptor`, `stdClass` given.'); + + $configs = new ConfigManager($this->createMock(LoaderInterface::class)); + $configs->setDefaults(TemporalConfig::CONFIG, ['interceptors' => []]); + + $bootloader = new TemporalBridgeBootloader($configs, $factory = m::mock(FactoryInterface::class)); + + $factory->shouldReceive('make')->with('foo')->andReturn(new \StdClass()); + + $bootloader->addInterceptor('foo'); + } } diff --git a/tests/src/Config/TemporalConfigTest.php b/tests/src/Config/TemporalConfigTest.php index 60068e2..67bf00a 100644 --- a/tests/src/Config/TemporalConfigTest.php +++ b/tests/src/Config/TemporalConfigTest.php @@ -6,6 +6,7 @@ use Spiral\TemporalBridge\Config\TemporalConfig; use Spiral\TemporalBridge\Tests\TestCase; +use Temporal\Client\ClientOptions; use Temporal\Worker\WorkerFactoryInterface; use Temporal\Worker\WorkerOptions; @@ -79,7 +80,25 @@ public function testGetsWorkers(): void { $workers = [ 'first' => WorkerOptions::new(), - 'second' => WorkerOptions::new() + 'second' => WorkerOptions::new(), + 'withOptions' => [ + 'options' => WorkerOptions::new(), + ], + 'withInterceptors' => [ + 'interceptors' => [ + 'foo' + ], + ], + 'withExceptionInterceptor' => [ + 'exception_interceptor' => 'bar' + ], + 'all' => [ + 'options' => WorkerOptions::new(), + 'interceptors' => [ + 'foo' + ], + 'exception_interceptor' => 'bar' + ], ]; $config = new TemporalConfig([ @@ -95,4 +114,24 @@ public function testGetsWorkersIfItNotSet(): void $this->assertSame([], $config->getWorkers()); } + + public function testGetsUndefinedClientOptions(): void + { + $config = new TemporalConfig([ + 'temporalNamespace' => 'foo', + ]); + + $options = $config->getClientOptions(); + + $this->assertSame('foo', $options->namespace); + } + + public function testGetsClientOptions(): void + { + $config = new TemporalConfig([ + 'clientOptions' => $options = new ClientOptions(), + ]); + + $this->assertSame($options, $config->getClientOptions()); + } } diff --git a/tests/src/WorkerFactoryTest.php b/tests/src/WorkerFactoryTest.php new file mode 100644 index 0000000..f195017 --- /dev/null +++ b/tests/src/WorkerFactoryTest.php @@ -0,0 +1,181 @@ +temporalWorkerFactory = $this->createMock(TemporalWorkerFactory::class); + } + + public function testCreateWithoutAnyOptions(): void + { + $this->temporalWorkerFactory + ->expects($this->once()) + ->method('newWorker') + ->with('without-any-options') + ->willReturn($worker = $this->createMock(WorkerInterface::class)); + + $factory = $this->createWorkerFactory($this->temporalWorkerFactory); + + $this->assertSame($worker, $factory->create('without-any-options')); + } + + public function testCreateWithOptionsAsValue(): void + { + $this->temporalWorkerFactory + ->expects($this->once()) + ->method('newWorker') + ->with('with-options-as-value', $this->equalTo(WorkerOptions::new()->withEnableSessionWorker())) + ->willReturn($worker = $this->createMock(WorkerInterface::class)); + + $factory = $this->createWorkerFactory($this->temporalWorkerFactory); + + $this->assertSame($worker, $factory->create('with-options-as-value')); + } + + public function testCreateWithOptionsInArray(): void + { + $this->temporalWorkerFactory + ->expects($this->once()) + ->method('newWorker') + ->with('with-options-in-array', $this->equalTo(WorkerOptions::new()->withEnableSessionWorker())) + ->willReturn($worker = $this->createMock(WorkerInterface::class)); + + $factory = $this->createWorkerFactory($this->temporalWorkerFactory); + + $this->assertSame($worker, $factory->create('with-options-in-array')); + } + + #[DataProvider(methodName: 'exceptionInterceptorsDataProvider')] + public function testCreateWithExceptionInterceptor(string $name): void + { + $this->temporalWorkerFactory + ->expects($this->once()) + ->method('newWorker') + ->with($name, null, $this->equalTo(new ExceptionInterceptor([]))) + ->willReturn($worker = $this->createMock(WorkerInterface::class)); + + $factory = $this->createWorkerFactory($this->temporalWorkerFactory); + + $this->assertSame($worker, $factory->create($name)); + } + + public function testCreateWithInterceptors(): void + { + $expectedInterceptors = new SimplePipelineProvider([ + new SomeInterceptor(), + new SomeInterceptor(), + new SomeInterceptor(), + ]); + + $this->temporalWorkerFactory + ->expects($this->once()) + ->method('newWorker') + ->with('with-interceptors', null, null, $this->equalTo($expectedInterceptors)) + ->willReturn($worker = $this->createMock(WorkerInterface::class)); + + $factory = $this->createWorkerFactory($this->temporalWorkerFactory, $expectedInterceptors); + + $this->assertSame($worker, $factory->create('with-interceptors')); + } + + public function testCreateWithAllOptions(): void + { + $expectedInterceptors = new SimplePipelineProvider([ + new SomeInterceptor(), + new SomeInterceptor(), + new SomeInterceptor(), + ]); + + $this->temporalWorkerFactory + ->expects($this->once()) + ->method('newWorker') + ->with( + 'all', + $this->equalTo(WorkerOptions::new()->withEnableSessionWorker()), + $this->equalTo(new ExceptionInterceptor([])), + $this->equalTo($expectedInterceptors), + ) + ->willReturn($worker = $this->createMock(WorkerInterface::class)); + + $factory = $this->createWorkerFactory($this->temporalWorkerFactory, $expectedInterceptors); + + $this->assertSame($worker, $factory->create('all')); + } + + public static function exceptionInterceptorsDataProvider(): \Traversable + { + yield ['with-exception-interceptor-as-string']; + yield ['with-exception-interceptor-as-autowire']; + yield ['with-exception-interceptor-as-instance']; + } + + private function createWorkerFactory( + TemporalWorkerFactory $workerFactory, + PipelineProvider $pipelineProvider = new SimplePipelineProvider(), + ): + WorkerFactory { + $container = new Container(); + $container->bind(PipelineProvider::class, SimplePipelineProvider::class); + $container->bind(ExceptionInterceptor::class, new ExceptionInterceptor([])); + + $interceptors = [ + SomeInterceptor::class, + new SomeInterceptor(), + new Autowire(SomeInterceptor::class), + ]; + + return new WorkerFactory( + $workerFactory, + $this->createMock(FinalizerInterface::class), + $container, + $pipelineProvider, + new TemporalConfig([ + 'workers' => [ + 'with-options-as-value' => WorkerOptions::new()->withEnableSessionWorker(), + 'with-options-in-array' => [ + 'options' => WorkerOptions::new()->withEnableSessionWorker(), + ], + 'with-interceptors' => [ + 'interceptors' => $interceptors, + ], + 'with-exception-interceptor-as-string' => [ + 'exception_interceptor' => ExceptionInterceptor::class, + ], + 'with-exception-interceptor-as-autowire' => [ + 'exception_interceptor' => new Autowire(ExceptionInterceptor::class, []), + ], + 'with-exception-interceptor-as-instance' => [ + 'exception_interceptor' => new ExceptionInterceptor([]), + ], + 'all' => [ + 'options' => WorkerOptions::new()->withEnableSessionWorker(), + 'interceptors' => $interceptors, + 'exception_interceptor' => ExceptionInterceptor::class, + ], + ], + ]) + ); + } +}