diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c16ef..4e1e8ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +Next release +------------ + +* Add run checker (check before run consumer). + v2.0.3 ------ diff --git a/src/Adapter/Amqp/Queue/AmqpQueue.php b/src/Adapter/Amqp/Queue/AmqpQueue.php index b08b617..c08bc4a 100644 --- a/src/Adapter/Amqp/Queue/AmqpQueue.php +++ b/src/Adapter/Amqp/Queue/AmqpQueue.php @@ -89,7 +89,6 @@ public function get(): ?ReceivedMessage $envelope = $this->queue->get(); if ($envelope) { - // @phpstan-ignore-next-line return new AmqpReceivedMessage($this->queue, $envelope); } diff --git a/src/Command/OutputEventHandler.php b/src/Command/OutputEventHandler.php index 2940343..48b954e 100644 --- a/src/Command/OutputEventHandler.php +++ b/src/Command/OutputEventHandler.php @@ -42,7 +42,7 @@ public function __invoke(Event $event): void 'Receive consumer timeout exceed error.', OutputInterface::VERBOSITY_VERBOSE ); - } else if (Event::StopAfterNExecutes === $event) { + } elseif (Event::StopAfterNExecutes === $event) { $this->output->writeln( 'Stop consumer after N executes.', OutputInterface::VERBOSITY_VERBOSE diff --git a/src/Command/RunConsumerCommand.php b/src/Command/RunConsumerCommand.php index c496ea2..b98c7f8 100644 --- a/src/Command/RunConsumerCommand.php +++ b/src/Command/RunConsumerCommand.php @@ -13,12 +13,15 @@ namespace FiveLab\Component\Amqp\Command; +use FiveLab\Component\Amqp\Consumer\Checker\RunConsumerCheckerRegistry; +use FiveLab\Component\Amqp\Consumer\Checker\RunConsumerCheckerRegistryInterface; use FiveLab\Component\Amqp\Consumer\ConsumerInterface; use FiveLab\Component\Amqp\Consumer\EventableConsumerInterface; use FiveLab\Component\Amqp\Consumer\Middleware\StopAfterNExecutesMiddleware; use FiveLab\Component\Amqp\Consumer\MiddlewareAwareInterface; use FiveLab\Component\Amqp\Consumer\Registry\ConsumerRegistryInterface; use FiveLab\Component\Amqp\Exception\ConsumerTimeoutExceedException; +use FiveLab\Component\Amqp\Exception\RunConsumerCheckerNotFoundException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -42,14 +45,24 @@ class RunConsumerCommand extends Command */ protected static $defaultDescription = 'Run consumer.'; + /** + * @var RunConsumerCheckerRegistryInterface + */ + private readonly RunConsumerCheckerRegistryInterface $runCheckerRegistry; + /** * Constructor. * - * @param ConsumerRegistryInterface $consumerRegistry + * @param ConsumerRegistryInterface $consumerRegistry + * @param RunConsumerCheckerRegistryInterface|null $runCheckerRegistry */ - public function __construct(private readonly ConsumerRegistryInterface $consumerRegistry) - { + public function __construct( + private readonly ConsumerRegistryInterface $consumerRegistry, + RunConsumerCheckerRegistryInterface $runCheckerRegistry = null + ) { parent::__construct(); + + $this->runCheckerRegistry = $runCheckerRegistry ?: new RunConsumerCheckerRegistry(); } /** @@ -62,7 +75,8 @@ protected function configure(): void ->addArgument('key', InputArgument::REQUIRED, 'The key of consumer.') ->addOption('read-timeout', null, InputOption::VALUE_REQUIRED, 'Set the read timeout for RabbitMQ.') ->addOption('loop', null, InputOption::VALUE_NONE, 'Loop consume (used only with read-timeout).') - ->addOption('messages', null, InputOption::VALUE_REQUIRED, 'After process number of messages process be normal exits.'); + ->addOption('messages', null, InputOption::VALUE_REQUIRED, 'After process number of messages process be normal exits.') + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Check if consumer can be run.'); } /** @@ -70,7 +84,23 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { - $consumer = $this->consumerRegistry->get((string) $input->getArgument('key')); + $consumerKey = (string) $input->getArgument('key'); + + if ($input->getOption('dry-run')) { + $checker = $this->runCheckerRegistry->get($consumerKey); + $checker->checkBeforeRun(); + + return 0; + } + + try { + $checker = $this->runCheckerRegistry->get($consumerKey); + $checker->checkBeforeRun(); + } catch (RunConsumerCheckerNotFoundException) { + // Normal flow. Checker not found. + } + + $consumer = $this->consumerRegistry->get($consumerKey); if ($consumer instanceof EventableConsumerInterface) { $closure = (new OutputEventHandler($output))(...); diff --git a/src/Consumer/Checker/ContainerRunConsumerCheckerRegistry.php b/src/Consumer/Checker/ContainerRunConsumerCheckerRegistry.php new file mode 100644 index 0000000..3656a42 --- /dev/null +++ b/src/Consumer/Checker/ContainerRunConsumerCheckerRegistry.php @@ -0,0 +1,47 @@ +container->has($key)) { + return $this->container->get($key); + } + + throw new RunConsumerCheckerNotFoundException(\sprintf( + 'The checker for consumer "%s" was not found.', + $key + )); + } +} diff --git a/src/Consumer/Checker/RunConsumerCheckerInterface.php b/src/Consumer/Checker/RunConsumerCheckerInterface.php new file mode 100644 index 0000000..300865f --- /dev/null +++ b/src/Consumer/Checker/RunConsumerCheckerInterface.php @@ -0,0 +1,29 @@ + + */ + private array $checkers = []; + + /** + * Add checker for consumer to registry + * + * @param string $key + * @param RunConsumerCheckerInterface $checker + */ + public function add(string $key, RunConsumerCheckerInterface $checker): void + { + $this->checkers[$key] = $checker; + } + + /** + * {@inheritdoc} + */ + public function get(string $key): RunConsumerCheckerInterface + { + return $this->checkers[$key] ?? throw new RunConsumerCheckerNotFoundException(\sprintf( + 'The checker for consumer "%s" was not found.', + $key + )); + } +} diff --git a/src/Consumer/Checker/RunConsumerCheckerRegistryInterface.php b/src/Consumer/Checker/RunConsumerCheckerRegistryInterface.php new file mode 100644 index 0000000..f36f355 --- /dev/null +++ b/src/Consumer/Checker/RunConsumerCheckerRegistryInterface.php @@ -0,0 +1,33 @@ +registry = $this->createMock(ConsumerRegistryInterface::class); + $this->checkerRegistry = $this->createMock(RunConsumerCheckerRegistryInterface::class); $this->connection = $this->createMock(ConnectionInterface::class); $this->channel = $this->createMock(ChannelInterface::class); $this->queue = $this->createMock(QueueInterface::class); @@ -295,4 +305,105 @@ public function shouldSuccessExecuteInLoopWithReadTimeout(bool $verbose): void throw $error; } } + + #[Test] + public function shouldSuccessExecuteIfCheckerNotFoundInRegistry(): void + { + $command = new RunConsumerCommand($this->registry, $this->checkerRegistry); + + $input = new ArrayInput([ + 'key' => 'some', + ]); + + $status = $command->run($input, new BufferedOutput()); + + self::assertEquals(0, $status); + } + + #[Test] + public function shouldFailExecuteIfCheckerThrowError(): void + { + $this->configureChecker('some', new CannotRunConsumerException('foo bar')); + + $this->registry->expects(self::never()) + ->method('get'); + + $command = new RunConsumerCommand($this->registry, $this->checkerRegistry); + + $input = new ArrayInput([ + 'key' => 'some', + ]); + + $this->expectException(CannotRunConsumerException::class); + $this->expectExceptionMessage('foo bar'); + + $command->run($input, new BufferedOutput()); + } + + #[Test] + public function shouldSuccessExecuteWithDryRun(): void + { + $this->configureChecker('some', null); + + $command = new RunConsumerCommand($this->registry, $this->checkerRegistry); + + $input = new ArrayInput([ + 'key' => 'some', + '--dry-run' => true, + ]); + + $status = $command->run($input, new BufferedOutput()); + + self::assertEquals(0, $status); + } + + #[Test] + public function shouldFailExecuteWithDryRunIfCheckerNotFound(): void + { + $command = new RunConsumerCommand($this->registry); + + $input = new ArrayInput([ + 'key' => 'some', + '--dry-run' => true, + ]); + + $this->expectException(RunConsumerCheckerNotFoundException::class); + $this->expectExceptionMessage('The checker for consumer "some" was not found.'); + + $command->run($input, new BufferedOutput()); + } + + #[Test] + public function shouldFailExecuteWithDryRunIfCheckerThrowError(): void + { + $this->configureChecker('some', new CannotRunConsumerException('bla bla')); + + $command = new RunConsumerCommand($this->registry, $this->checkerRegistry); + + $input = new ArrayInput([ + 'key' => 'some', + '--dry-run' => true, + ]); + + $this->expectException(CannotRunConsumerException::class); + $this->expectExceptionMessage('bla bla'); + + $command->run($input, new BufferedOutput()); + } + + private function configureChecker(string $key, ?\Throwable $error): void + { + $checker = $this->createMock(RunConsumerCheckerInterface::class); + + $matcher = $checker->expects(self::once())->method('checkBeforeRun'); + + if ($error) { + $matcher->willThrowException($error); + } + + $this->checkerRegistry->expects(self::once()) + ->method('get') + ->with($key) + ->willReturn($checker); + } } diff --git a/tests/Unit/Consumer/Checker/ContainerRunConsumerCheckerRegistryTest.php b/tests/Unit/Consumer/Checker/ContainerRunConsumerCheckerRegistryTest.php new file mode 100644 index 0000000..a56733f --- /dev/null +++ b/tests/Unit/Consumer/Checker/ContainerRunConsumerCheckerRegistryTest.php @@ -0,0 +1,81 @@ + + */ + private array $checkers; + + /** + * @var ContainerRunConsumerCheckerRegistry + */ + private ContainerRunConsumerCheckerRegistry $registry; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + $this->checkers = [ + $this->createMock(RunConsumerCheckerInterface::class), + $this->createMock(RunConsumerCheckerInterface::class), + ]; + + $container = $this->createMock(ContainerInterface::class); + + $container->expects(self::any()) + ->method('has') + ->willReturnCallback(static function (string $key) { + return \in_array($key, ['consumer_1', 'consumer_2'], true); + }); + + $container->expects(self::any()) + ->method('get') + ->willReturnMap([ + ['consumer_1', $this->checkers[0]], + ['consumer_2', $this->checkers[1]], + ]); + + $this->registry = new ContainerRunConsumerCheckerRegistry($container); + } + + #[Test] + public function shouldSuccessGet(): void + { + $consumer2 = $this->registry->get('consumer_2'); + self::assertSame($this->checkers[1], $consumer2); + + $consumer1 = $this->registry->get('consumer_1'); + self::assertSame($this->checkers[0], $consumer1); + } + + #[Test] + public function shouldFailGetIfNotFound(): void + { + $this->expectException(RunConsumerCheckerNotFoundException::class); + $this->expectExceptionMessage('The checker for consumer "foo" was not found.'); + + $this->registry->get('foo'); + } +} diff --git a/tests/Unit/Consumer/Checker/RunConsumerCheckerRegistryTest.php b/tests/Unit/Consumer/Checker/RunConsumerCheckerRegistryTest.php new file mode 100644 index 0000000..8dbea16 --- /dev/null +++ b/tests/Unit/Consumer/Checker/RunConsumerCheckerRegistryTest.php @@ -0,0 +1,70 @@ +createUniqueChecker(); + $checker2 = $this->createUniqueChecker(); + $checker3 = $this->createUniqueChecker(); + + $registry = new RunConsumerCheckerRegistry(); + + $registry->add('test_1', $checker1); + $registry->add('test_2', $checker2); + $registry->add('test_3', $checker3); + + $result = $registry->get('test_2'); + + self::assertSame($checker2, $result); + } + + #[Test] + public function shouldFailIfCheckerNotFound(): void + { + $registry = new RunConsumerCheckerRegistry(); + + $registry->add('foo', $this->createUniqueChecker()); + + $this->expectException(RunConsumerCheckerNotFoundException::class); + $this->expectExceptionMessage('The checker for consumer "bar" was not found.'); + + $registry->get('bar'); + } + + #[Test] + public function shouldFailIfRegistryIsEmpty(): void + { + $registry = new RunConsumerCheckerRegistry(); + + $this->expectException(RunConsumerCheckerNotFoundException::class); + $this->expectExceptionMessage('The checker for consumer "foo" was not found.'); + + $registry->get('foo'); + } + + private function createUniqueChecker(): RunConsumerCheckerInterface + { + return $this->createMock(RunConsumerCheckerInterface::class); + } +} diff --git a/tests/Unit/Consumer/Registry/ConsumerRegistryTest.php b/tests/Unit/Consumer/Registry/ConsumerRegistryTest.php index 7816f06..a4d9908 100644 --- a/tests/Unit/Consumer/Registry/ConsumerRegistryTest.php +++ b/tests/Unit/Consumer/Registry/ConsumerRegistryTest.php @@ -36,7 +36,7 @@ public function shouldSuccessGet(): void $result = $registry->get('test_2'); - self::assertEquals($consumer2, $result); + self::assertSame($consumer2, $result); } #[Test] diff --git a/tests/Unit/Consumer/Registry/ContainerConsumerRegistryTest.php b/tests/Unit/Consumer/Registry/ContainerConsumerRegistryTest.php index 4c7b4b4..5113a78 100644 --- a/tests/Unit/Consumer/Registry/ContainerConsumerRegistryTest.php +++ b/tests/Unit/Consumer/Registry/ContainerConsumerRegistryTest.php @@ -64,10 +64,10 @@ protected function setUp(): void public function shouldSuccessGet(): void { $consumer2 = $this->registry->get('consumer_2'); - self::assertEquals($this->consumers[1], $consumer2); + self::assertSame($this->consumers[1], $consumer2); $consumer1 = $this->registry->get('consumer_1'); - self::assertEquals($this->consumers[0], $consumer1); + self::assertSame($this->consumers[0], $consumer1); } #[Test]