From 970e6d28f7d1b1a486cd984fb8ae0aa4a899a863 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sat, 23 Nov 2024 16:58:16 -0500 Subject: [PATCH 1/6] feat(history): ability to disable monitoring by class/interface --- README.md | 19 +++-- config/storage_orm.php | 1 + .../ZenstruckMessengerMonitorExtension.php | 16 ++++- src/History/HistoryListener.php | 20 +++++- ...ZenstruckMessengerMonitorExtensionTest.php | 30 ++++++++ tests/Unit/History/HistoryListenerTest.php | 72 +++++++++++++++---- 6 files changed, 138 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 1365ea4..7676ec3 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ or the [provided tools](#history). #### Disable Monitoring -You may want to disable monitoring for certain messages. There are two ways to do this: +You may want to disable monitoring for certain messages. There are several ways to do this: 1. When dispatching the message, add the `DisableMonitoringStamp`: ```php @@ -97,7 +97,7 @@ You may want to disable monitoring for certain messages. There are two ways to d $bus->dispatch(new MyMessage(), [new DisableMonitoringStamp()]) ``` -2. Add the `DisableMonitoringStamp` as a class attribute to your message: +2. Add the `DisableMonitoringStamp` as a class attribute to your message (or parent class): ```php use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; @@ -106,8 +106,8 @@ You may want to disable monitoring for certain messages. There are two ways to d { } ``` -3. You may want to disable monitoring for messages that are dispatched without any handler. -You can do this by using the `DisableMonitoringStamp` with optional constructor argument `true`: + You may want to disable monitoring for messages that are dispatched without any handler. + You can do this by using the `DisableMonitoringStamp` with optional constructor argument `true`: ```php use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; @@ -116,6 +116,15 @@ You can do this by using the `DisableMonitoringStamp` with optional constructor { } ``` +3. Add the message class to the `exclude` config option (can be abstract/interface): + ```yaml + # config/packages/zenstruck_messenger_monitor.yaml + + zenstruck_messenger_monitor: + storage: + exclude: + - App\Message\MyMessage + ``` #### Description @@ -300,6 +309,8 @@ when@dev: ```yaml zenstruck_messenger_monitor: storage: + # Message classes to disable monitoring for (can be abstract/interface) + exclude: [] orm: # Your Doctrine entity class that extends "Zenstruck\Messenger\Monitor\History\Model\ProcessedMessage" diff --git a/config/storage_orm.php b/config/storage_orm.php index e5bbd6a..3f3d2e5 100644 --- a/config/storage_orm.php +++ b/config/storage_orm.php @@ -33,6 +33,7 @@ ->args([ service('zenstruck_messenger_monitor.history.storage'), service('.zenstruck_messenger_monitor.history.result_normalizer'), + abstract_arg('exclude_classes'), ]) ->tag('kernel.event_listener', ['method' => 'addMonitorStamp', 'event' => SendMessageToTransportsEvent::class]) ->tag('kernel.event_listener', ['method' => 'receiveMessage', 'event' => WorkerMessageReceivedEvent::class]) diff --git a/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php b/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php index 99e5793..21790e0 100644 --- a/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php +++ b/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php @@ -35,6 +35,15 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->arrayNode('storage') ->children() + ->arrayNode('exclude') + ->info('Message classes to disable monitoring for (can be abstract/interface)') + ->scalarPrototype() + ->validate() + ->ifTrue(fn($v) => !\class_exists($v) && !\interface_exists($v)) + ->thenInvalid('Class/interface does not exist.') + ->end() + ->end() + ->end() ->arrayNode('orm') ->children() ->scalarNode('entity_class') @@ -91,7 +100,12 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container if ($entity = $mergedConfig['storage']['orm']['entity_class'] ?? null) { $loader->load('storage_orm.php'); - $container->getDefinition('zenstruck_messenger_monitor.history.storage')->setArgument(1, $entity); + $container->getDefinition('zenstruck_messenger_monitor.history.storage') + ->setArgument(1, $entity) + ; + $container->getDefinition('.zenstruck_messenger_monitor.history.listener') + ->setArgument(2, $mergedConfig['storage']['exclude']) + ; if (!\class_exists(Schedule::class)) { $container->removeDefinition('.zenstruck_messenger_monitor.command.schedule_purge'); diff --git a/src/History/HistoryListener.php b/src/History/HistoryListener.php index f18fbdc..a671a77 100644 --- a/src/History/HistoryListener.php +++ b/src/History/HistoryListener.php @@ -34,8 +34,14 @@ */ final class HistoryListener { - public function __construct(private Storage $storage, private ResultNormalizer $normalizer) - { + /** + * @param class-string[] $excludedClasses + */ + public function __construct( + private Storage $storage, + private ResultNormalizer $normalizer, + private array $excludedClasses, + ) { } public function addMonitorStamp(SendMessageToTransportsEvent $event): void @@ -103,6 +109,14 @@ public function handleFailure(WorkerMessageFailedEvent $event): void private function isMonitoringDisabled(Envelope $envelope): bool { + $messageClass = $envelope->getMessage()::class; + + foreach ($this->excludedClasses as $excludedClass) { + if (\is_a($messageClass, $excludedClass, true)) { + return true; + } + } + if ($stamp = $envelope->last(DisableMonitoringStamp::class)) { if (false === $stamp->onlyWhenNoHandler) { return true; @@ -111,7 +125,7 @@ private function isMonitoringDisabled(Envelope $envelope): bool return $this->hasNoHandlers($envelope); } - $reflection = new \ReflectionClass($envelope->getMessage()); + $reflection = new \ReflectionClass($messageClass); $attributes = []; while (false !== $reflection && [] === $attributes) { diff --git a/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php b/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php index f9c0571..c250250 100644 --- a/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php +++ b/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php @@ -13,9 +13,11 @@ use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\ContainerBuilderHasAliasConstraint; +use Matthias\SymfonyDependencyInjectionTest\PhpUnit\ContainerBuilderHasServiceDefinitionConstraint; use PHPUnit\Framework\Constraint\LogicalNot; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Zenstruck\Messenger\Monitor\DependencyInjection\ZenstruckMessengerMonitorExtension; +use Zenstruck\Messenger\Monitor\History\HistoryListener; use Zenstruck\Messenger\Monitor\History\Model\ProcessedMessage; use Zenstruck\Messenger\Monitor\History\Storage; use Zenstruck\Messenger\Monitor\History\Storage\ORMStorage; @@ -38,6 +40,7 @@ public function no_config(): void $this->assertContainerBuilderHasAlias(Transports::class, 'zenstruck_messenger_monitor.transports'); $this->assertContainerBuilderHasAlias(Workers::class, 'zenstruck_messenger_monitor.workers'); $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasAliasConstraint(Storage::class))); + $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasServiceDefinitionConstraint('.zenstruck_messenger_monitor.history.listener'))); } /** @@ -52,6 +55,33 @@ public function orm_config(): void $this->assertContainerBuilderHasService('zenstruck_messenger_monitor.history.storage', ORMStorage::class); $this->assertContainerBuilderHasServiceDefinitionWithArgument('zenstruck_messenger_monitor.history.storage', 1, ProcessedMessageImpl::class); $this->assertContainerBuilderHasAlias(Storage::class, 'zenstruck_messenger_monitor.history.storage'); + $this->assertContainerBuilderHasService('.zenstruck_messenger_monitor.history.listener', HistoryListener::class); + $this->assertContainerBuilderHasServiceDefinitionWithArgument('.zenstruck_messenger_monitor.history.listener', 2, []); + } + + /** + * @test + */ + public function storage_with_excluded_classes(): void + { + $this->load(['storage' => [ + 'orm' => ['entity_class' => ProcessedMessageImpl::class], + 'exclude' => ['stdClass'] + ]]); + + $this->assertContainerBuilderHasServiceDefinitionWithArgument('.zenstruck_messenger_monitor.history.listener', 2, [\stdClass::class]); + } + + /** + * @test + */ + public function invalid_exclude_class(): void + { + $this->expectException(InvalidConfigurationException::class); + + $this->load(['storage' => [ + 'exclude' => ['invalid'] + ]]); } /** diff --git a/tests/Unit/History/HistoryListenerTest.php b/tests/Unit/History/HistoryListenerTest.php index 8d471db..ae15e92 100644 --- a/tests/Unit/History/HistoryListenerTest.php +++ b/tests/Unit/History/HistoryListenerTest.php @@ -39,7 +39,7 @@ final class HistoryListenerTest extends TestCase */ public function adds_monitor_stamp(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); $envelope = new Envelope(new \stdClass()); $event = new SendMessageToTransportsEvent($envelope, []); @@ -55,7 +55,7 @@ public function adds_monitor_stamp(): void */ public function skips_standard_messages_without_monitor_stamp(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); $envelope = new Envelope(new \stdClass()); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); @@ -71,7 +71,7 @@ public function skips_standard_messages_without_monitor_stamp(): void */ public function marks_standard_message_as_received(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); $envelope = new Envelope(new \stdClass(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); @@ -89,7 +89,7 @@ public function marks_standard_message_as_received(): void */ public function marks_scheduled_message_as_received(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); $envelope = new Envelope(new \stdClass(), [new ScheduledStamp(new MessageContext( 'default', 'id', @@ -123,7 +123,7 @@ public function handles_success(): void $this->isInstanceOf(Results::class), ); - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__), []); $listener->handleSuccess($event = new WorkerMessageHandledEvent($envelope, 'foo')); @@ -138,7 +138,7 @@ public function handles_success_invalid(): void $storage = $this->createMock(Storage::class); $storage->expects($this->never())->method('save'); - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__), []); $listener->handleSuccess(new WorkerMessageHandledEvent(new Envelope(new \stdClass()), 'foo')); $listener->handleSuccess(new WorkerMessageHandledEvent(new Envelope(new \stdClass(), [new MonitorStamp()]), 'foo')); @@ -158,7 +158,7 @@ public function handles_failure(): void $exception, ); - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__), []); $listener->handleFailure($event = new WorkerMessageFailedEvent($envelope, 'foo', $exception)); @@ -173,7 +173,7 @@ public function handles_failure_invalid(): void $storage = $this->createMock(Storage::class); $storage->expects($this->never())->method('save'); - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__), []); $listener->handleFailure(new WorkerMessageFailedEvent(new Envelope(new \stdClass()), 'foo', new \RuntimeException())); $listener->handleFailure(new WorkerMessageFailedEvent(new Envelope(new \stdClass(), [new MonitorStamp()]), 'foo', new \RuntimeException())); @@ -184,7 +184,7 @@ public function handles_failure_invalid(): void */ public function can_disable_monitoring_with_envelope_stamp(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); $envelope = new Envelope(new \stdClass(), [new MonitorStamp(), new DisableMonitoringStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); @@ -198,7 +198,7 @@ public function can_disable_monitoring_with_envelope_stamp(): void */ public function can_disable_monitoring_message_attribute(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); $envelope = new Envelope(new DisabledMonitoringMessage(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); @@ -212,7 +212,7 @@ public function can_disable_monitoring_message_attribute(): void */ public function can_disable_monitoring_message_attribute_without_handler(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); $envelope = new Envelope(new DisabledMonitoringWithoutHandlerMessage(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); @@ -226,7 +226,7 @@ public function can_disable_monitoring_message_attribute_without_handler(): void */ public function handle_disable_monitoring_message_attribute_with_handler(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__)); + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); $envelope = new Envelope( new EnabledMonitoringWithHandlerMessage(), [ @@ -240,6 +240,46 @@ public function handle_disable_monitoring_message_attribute_with_handler(): void $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } + + /** + * @test + */ + public function can_disable_monitoring_message_via_config(): void + { + $listener = new HistoryListener( + $this->createMock(Storage::class), + new ResultNormalizer(__DIR__), + [ + MessageToDisableViaConfig::class, + ] + ); + $envelope = new Envelope(new MessageToDisableViaConfig(), [new MonitorStamp()]); + $event = new WorkerMessageReceivedEvent($envelope, 'foo'); + + $listener->receiveMessage($event); + + $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); + } + + /** + * @test + */ + public function can_disable_extended_monitoring_message_via_config(): void + { + $listener = new HistoryListener( + $this->createMock(Storage::class), + new ResultNormalizer(__DIR__), + [ + MessageToDisableViaConfig::class, + ] + ); + $envelope = new Envelope(new ExtendedMessageToDisableViaConfig(), [new MonitorStamp()]); + $event = new WorkerMessageReceivedEvent($envelope, 'foo'); + + $listener->receiveMessage($event); + + $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); + } } #[DisableMonitoringStamp] @@ -257,6 +297,14 @@ class EnabledMonitoringWithHandlerMessage { } +class MessageToDisableViaConfig +{ +} + +class ExtendedMessageToDisableViaConfig extends MessageToDisableViaConfig +{ +} + class EnabledMonitoringWithHandlerMessageHandler { public function __invoke(EnabledMonitoringWithHandlerMessage $message): void From f6f61895deeddd186e56b04e25ea5d411c01c993 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sat, 23 Nov 2024 17:18:04 -0500 Subject: [PATCH 2/6] feat(history): allow adding `DisableMonitoringStamp` to interfaces --- README.md | 2 +- src/History/HistoryListener.php | 26 ++------ src/Stamp/DisableMonitoringStamp.php | 26 ++++++++ ...ZenstruckMessengerMonitorExtensionTest.php | 4 +- tests/Unit/History/HistoryListenerTest.php | 65 +++++++++++++++++++ 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 7676ec3..2c9418b 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ You may want to disable monitoring for certain messages. There are several ways $bus->dispatch(new MyMessage(), [new DisableMonitoringStamp()]) ``` -2. Add the `DisableMonitoringStamp` as a class attribute to your message (or parent class): +2. Add the `DisableMonitoringStamp` as a class attribute to your message (or parent class/interface): ```php use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; diff --git a/src/History/HistoryListener.php b/src/History/HistoryListener.php index a671a77..f06d985 100644 --- a/src/History/HistoryListener.php +++ b/src/History/HistoryListener.php @@ -117,31 +117,17 @@ private function isMonitoringDisabled(Envelope $envelope): bool } } - if ($stamp = $envelope->last(DisableMonitoringStamp::class)) { - if (false === $stamp->onlyWhenNoHandler) { - return true; - } - - return $this->hasNoHandlers($envelope); - } + $stamp = $envelope->last(DisableMonitoringStamp::class) ?? DisableMonitoringStamp::getFor($messageClass); - $reflection = new \ReflectionClass($messageClass); - $attributes = []; - - while (false !== $reflection && [] === $attributes) { - $attributes = $reflection->getAttributes(DisableMonitoringStamp::class); - $reflection = $reflection->getParentClass(); + if (!$stamp) { + return false; } - if ([] !== $attributes) { - if (false === $attributes[0]->newInstance()->onlyWhenNoHandler) { - return true; - } - - return $this->hasNoHandlers($envelope); + if ($stamp->onlyWhenNoHandler && !$this->hasNoHandlers($envelope)) { + return false; } - return false; + return true; } private function createResults(Envelope $envelope, ?HandlerFailedException $exception = null): Results diff --git a/src/Stamp/DisableMonitoringStamp.php b/src/Stamp/DisableMonitoringStamp.php index 1acf5e9..548cd59 100644 --- a/src/Stamp/DisableMonitoringStamp.php +++ b/src/Stamp/DisableMonitoringStamp.php @@ -22,4 +22,30 @@ final class DisableMonitoringStamp implements StampInterface public function __construct(public readonly bool $onlyWhenNoHandler = false) { } + + /** + * @internal + * + * @param class-string $class + */ + public static function getFor(string $class): ?self + { + $original = $reflection = new \ReflectionClass($class); + + while (false !== $reflection) { + if ($attributes = $reflection->getAttributes(self::class)) { + return $attributes[0]->newInstance(); + } + + $reflection = $reflection->getParentClass(); + } + + foreach ($original->getInterfaces() as $refInterface) { + if ($attributes = $refInterface->getAttributes(self::class)) { + return $attributes[0]->newInstance(); + } + } + + return null; + } } diff --git a/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php b/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php index c250250..1be43f6 100644 --- a/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php +++ b/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php @@ -66,7 +66,7 @@ public function storage_with_excluded_classes(): void { $this->load(['storage' => [ 'orm' => ['entity_class' => ProcessedMessageImpl::class], - 'exclude' => ['stdClass'] + 'exclude' => ['stdClass'], ]]); $this->assertContainerBuilderHasServiceDefinitionWithArgument('.zenstruck_messenger_monitor.history.listener', 2, [\stdClass::class]); @@ -80,7 +80,7 @@ public function invalid_exclude_class(): void $this->expectException(InvalidConfigurationException::class); $this->load(['storage' => [ - 'exclude' => ['invalid'] + 'exclude' => ['invalid'], ]]); } diff --git a/tests/Unit/History/HistoryListenerTest.php b/tests/Unit/History/HistoryListenerTest.php index ae15e92..c3af2e3 100644 --- a/tests/Unit/History/HistoryListenerTest.php +++ b/tests/Unit/History/HistoryListenerTest.php @@ -207,6 +207,20 @@ public function can_disable_monitoring_message_attribute(): void $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } + /** + * @test + */ + public function can_disable_monitoring_message_interface_attribute(): void + { + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $envelope = new Envelope(new DisableMonitoringViaInterface(), [new MonitorStamp()]); + $event = new WorkerMessageReceivedEvent($envelope, 'foo'); + + $listener->receiveMessage($event); + + $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); + } + /** * @test */ @@ -221,6 +235,23 @@ public function can_disable_monitoring_message_attribute_without_handler(): void $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } + /** + * @test + */ + public function can_disable_monitoring_message_without_handler(): void + { + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $envelope = new Envelope(new \stdClass(), [ + new MonitorStamp(), + new DisableMonitoringStamp(true), + ]); + $event = new WorkerMessageReceivedEvent($envelope, 'foo'); + + $listener->receiveMessage($event); + + $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); + } + /** * @test */ @@ -241,6 +272,27 @@ public function handle_disable_monitoring_message_attribute_with_handler(): void $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } + /** + * @test + */ + public function handle_disable_monitoring_message_with_handler(): void + { + $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $envelope = new Envelope( + new \stdClass(), + [ + new MonitorStamp(), + new DisableMonitoringStamp(true), + new HandledStamp(EnabledMonitoringWithHandlerMessageHandler::class, 'result'), + ], + ); + $event = new WorkerMessageReceivedEvent($envelope, 'foo'); + + $listener->receiveMessage($event); + + $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); + } + /** * @test */ @@ -297,6 +349,19 @@ class EnabledMonitoringWithHandlerMessage { } +#[DisableMonitoringStamp] +interface DisableInterface +{ +} + +abstract class ParentDisableMonitoringViaInterface implements DisableInterface +{ +} + +class DisableMonitoringViaInterface extends ParentDisableMonitoringViaInterface +{ +} + class MessageToDisableViaConfig { } From 9db1567424229be9f25d6bb6409d33d64911f177 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sun, 24 Nov 2024 09:49:27 -0500 Subject: [PATCH 3/6] minor(history): allow `Results` to contain `null` --- src/History/Model/ProcessedMessage.php | 4 +-- src/History/Model/Results.php | 12 ++++---- tests/Unit/History/Model/ResultsTest.php | 35 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 tests/Unit/History/Model/ResultsTest.php diff --git a/src/History/Model/ProcessedMessage.php b/src/History/Model/ProcessedMessage.php index 5db7c2d..3dae513 100644 --- a/src/History/Model/ProcessedMessage.php +++ b/src/History/Model/ProcessedMessage.php @@ -44,8 +44,8 @@ abstract class ProcessedMessage private ?string $failureType = null; private ?string $failureMessage = null; - /** @var Structure[]|Results */ - private array|Results $results; + /** @var Structure[]|Results|null */ + private array|Results|null $results; public function __construct(Envelope $envelope, Results $results, ?\Throwable $exception = null) { diff --git a/src/History/Model/Results.php b/src/History/Model/Results.php index dc1abf9..085ec5b 100644 --- a/src/History/Model/Results.php +++ b/src/History/Model/Results.php @@ -26,9 +26,9 @@ final class Results implements \Countable, \IteratorAggregate, \JsonSerializable /** * @internal * - * @param Structure[] $data + * @param Structure[]|null $data */ - public function __construct(private array $data) + public function __construct(private ?array $data) { } @@ -37,7 +37,7 @@ public function __construct(private array $data) */ public function all(): array { - return $this->all ??= \array_map(static fn(array $result) => new Result($result), $this->data); + return $this->all ??= \array_map(static fn(array $result) => new Result($result), $this->data ?? []); } /** @@ -63,13 +63,13 @@ public function getIterator(): \Traversable public function count(): int { - return \count($this->data); + return \count($this->all()); } /** - * @return Structure[] + * @return Structure[]|null */ - public function jsonSerialize(): array + public function jsonSerialize(): ?array { return $this->data; } diff --git a/tests/Unit/History/Model/ResultsTest.php b/tests/Unit/History/Model/ResultsTest.php new file mode 100644 index 0000000..a8c1a67 --- /dev/null +++ b/tests/Unit/History/Model/ResultsTest.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Unit\History\Model; + +use PHPUnit\Framework\TestCase; +use Zenstruck\Messenger\Monitor\History\Model\Results; + +/** + * @author Kevin Bond + */ +final class ResultsTest extends TestCase +{ + /** + * @test + */ + public function can_create_with_null(): void + { + $results = new Results(null); + + $this->assertCount(0, $results); + $this->assertSame([], $results->all()); + $this->assertSame([], $results->successes()); + $this->assertSame([], $results->failures()); + $this->assertSame(null, $results->jsonSerialize()); + } +} From 919319a9e90f29427c4cec126ef722838af75b24 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sun, 24 Nov 2024 10:52:20 -0500 Subject: [PATCH 4/6] minor(history): split `HistoryListener` --- config/storage_orm.php | 18 ++- .../ZenstruckMessengerMonitorExtension.php | 4 +- src/EventListener/AddMonitorStampListener.php | 28 ++++ .../HandleMonitorStampListener.php} | 73 +-------- .../ReceiveMonitorStampListener.php | 85 ++++++++++ ...ZenstruckMessengerMonitorExtensionTest.php | 16 +- .../AddMonitorStampListenerTest.php | 40 +++++ .../HandleMonitorStampListenerTest.php | 100 ++++++++++++ .../ReceiveMonitorStampListenerTest.php} | 151 ++++-------------- tests/Unit/History/Model/ResultsTest.php | 2 +- 10 files changed, 311 insertions(+), 206 deletions(-) create mode 100644 src/EventListener/AddMonitorStampListener.php rename src/{History/HistoryListener.php => EventListener/HandleMonitorStampListener.php} (54%) create mode 100644 src/EventListener/ReceiveMonitorStampListener.php create mode 100644 tests/Unit/EventListener/AddMonitorStampListenerTest.php create mode 100644 tests/Unit/EventListener/HandleMonitorStampListenerTest.php rename tests/Unit/{History/HistoryListenerTest.php => EventListener/ReceiveMonitorStampListenerTest.php} (56%) diff --git a/config/storage_orm.php b/config/storage_orm.php index 3f3d2e5..ce78c26 100644 --- a/config/storage_orm.php +++ b/config/storage_orm.php @@ -8,7 +8,9 @@ use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; use Zenstruck\Messenger\Monitor\Command\PurgeCommand; use Zenstruck\Messenger\Monitor\Command\SchedulePurgeCommand; -use Zenstruck\Messenger\Monitor\History\HistoryListener; +use Zenstruck\Messenger\Monitor\EventListener\AddMonitorStampListener; +use Zenstruck\Messenger\Monitor\EventListener\HandleMonitorStampListener; +use Zenstruck\Messenger\Monitor\EventListener\ReceiveMonitorStampListener; use Zenstruck\Messenger\Monitor\History\ResultNormalizer; use Zenstruck\Messenger\Monitor\History\Storage; use Zenstruck\Messenger\Monitor\History\Storage\ORMStorage; @@ -29,14 +31,20 @@ ->set('.zenstruck_messenger_monitor.history.result_normalizer', ResultNormalizer::class) ->args([param('kernel.project_dir')]) - ->set('.zenstruck_messenger_monitor.history.listener', HistoryListener::class) + ->set('.zenstruck_messenger_monitor.listener.add_monitor_stamp', AddMonitorStampListener::class) + ->tag('kernel.event_listener', ['method' => '__invoke', 'event' => SendMessageToTransportsEvent::class]) + + ->set('.zenstruck_messenger_monitor.listener.receive_monitor_stamp', ReceiveMonitorStampListener::class) + ->args([ + abstract_arg('exclude_classes') + ]) + ->tag('kernel.event_listener', ['method' => '__invoke', 'event' => WorkerMessageReceivedEvent::class]) + + ->set('.zenstruck_messenger_monitor.listener.handle_monitor_stamp', HandleMonitorStampListener::class) ->args([ service('zenstruck_messenger_monitor.history.storage'), service('.zenstruck_messenger_monitor.history.result_normalizer'), - abstract_arg('exclude_classes'), ]) - ->tag('kernel.event_listener', ['method' => 'addMonitorStamp', 'event' => SendMessageToTransportsEvent::class]) - ->tag('kernel.event_listener', ['method' => 'receiveMessage', 'event' => WorkerMessageReceivedEvent::class]) ->tag('kernel.event_listener', ['method' => 'handleSuccess', 'event' => WorkerMessageHandledEvent::class]) ->tag('kernel.event_listener', ['method' => 'handleFailure', 'event' => WorkerMessageFailedEvent::class]) diff --git a/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php b/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php index 21790e0..c0b966e 100644 --- a/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php +++ b/src/DependencyInjection/ZenstruckMessengerMonitorExtension.php @@ -103,8 +103,8 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container $container->getDefinition('zenstruck_messenger_monitor.history.storage') ->setArgument(1, $entity) ; - $container->getDefinition('.zenstruck_messenger_monitor.history.listener') - ->setArgument(2, $mergedConfig['storage']['exclude']) + $container->getDefinition('.zenstruck_messenger_monitor.listener.receive_monitor_stamp') + ->setArgument(0, $mergedConfig['storage']['exclude']) ; if (!\class_exists(Schedule::class)) { diff --git a/src/EventListener/AddMonitorStampListener.php b/src/EventListener/AddMonitorStampListener.php new file mode 100644 index 0000000..fa0fd48 --- /dev/null +++ b/src/EventListener/AddMonitorStampListener.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\EventListener; + +use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; +use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; + +/** + * @author Kevin Bond + * + * @internal + */ +final class AddMonitorStampListener +{ + public function __invoke(SendMessageToTransportsEvent $event): void + { + $event->setEnvelope($event->getEnvelope()->with(new MonitorStamp())); + } +} diff --git a/src/History/HistoryListener.php b/src/EventListener/HandleMonitorStampListener.php similarity index 54% rename from src/History/HistoryListener.php rename to src/EventListener/HandleMonitorStampListener.php index f06d985..04fb1f2 100644 --- a/src/History/HistoryListener.php +++ b/src/EventListener/HandleMonitorStampListener.php @@ -9,68 +9,31 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Messenger\Monitor\History; +namespace Zenstruck\Messenger\Monitor\EventListener; use Symfony\Component\Messenger\Envelope; -use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; -use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; use Symfony\Component\Messenger\Exception\HandlerFailedException; use Symfony\Component\Messenger\Stamp\HandledStamp; -use Symfony\Component\Scheduler\Messenger\ScheduledStamp; -use Zenstruck\Messenger\Monitor\History\Model\Result; use Zenstruck\Messenger\Monitor\History\Model\Results; -use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; +use Zenstruck\Messenger\Monitor\History\ResultNormalizer; +use Zenstruck\Messenger\Monitor\History\Storage; use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; -use Zenstruck\Messenger\Monitor\Stamp\TagStamp; /** * @author Kevin Bond * * @internal - * - * @phpstan-import-type Structure from Result */ -final class HistoryListener +final class HandleMonitorStampListener { - /** - * @param class-string[] $excludedClasses - */ public function __construct( private Storage $storage, private ResultNormalizer $normalizer, - private array $excludedClasses, ) { } - public function addMonitorStamp(SendMessageToTransportsEvent $event): void - { - $event->setEnvelope($event->getEnvelope()->with(new MonitorStamp())); - } - - public function receiveMessage(WorkerMessageReceivedEvent $event): void - { - $envelope = $event->getEnvelope(); - - if ($this->isMonitoringDisabled($envelope)) { - return; - } - - $stamp = $envelope->last(MonitorStamp::class); - - if (\class_exists(ScheduledStamp::class) && $scheduledStamp = $envelope->last(ScheduledStamp::class)) { - // scheduler transport doesn't trigger SendMessageToTransportsEvent - $stamp = new MonitorStamp($scheduledStamp->messageContext->triggeredAt); - - $event->addStamps(TagStamp::forSchedule($scheduledStamp)); - } - - if ($stamp instanceof MonitorStamp) { - $event->addStamps($stamp->markReceived($event->getReceiverName())); - } - } - public function handleSuccess(WorkerMessageHandledEvent $event): void { if (!$stamp = $event->getEnvelope()->last(MonitorStamp::class)) { @@ -107,29 +70,6 @@ public function handleFailure(WorkerMessageFailedEvent $event): void ); } - private function isMonitoringDisabled(Envelope $envelope): bool - { - $messageClass = $envelope->getMessage()::class; - - foreach ($this->excludedClasses as $excludedClass) { - if (\is_a($messageClass, $excludedClass, true)) { - return true; - } - } - - $stamp = $envelope->last(DisableMonitoringStamp::class) ?? DisableMonitoringStamp::getFor($messageClass); - - if (!$stamp) { - return false; - } - - if ($stamp->onlyWhenNoHandler && !$this->hasNoHandlers($envelope)) { - return false; - } - - return true; - } - private function createResults(Envelope $envelope, ?HandlerFailedException $exception = null): Results { $results = []; @@ -157,9 +97,4 @@ private function createResults(Envelope $envelope, ?HandlerFailedException $exce return new Results($results); } - - private function hasNoHandlers(Envelope $envelope): bool - { - return [] === $envelope->all(HandledStamp::class); - } } diff --git a/src/EventListener/ReceiveMonitorStampListener.php b/src/EventListener/ReceiveMonitorStampListener.php new file mode 100644 index 0000000..ad9e14c --- /dev/null +++ b/src/EventListener/ReceiveMonitorStampListener.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\EventListener; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; +use Symfony\Component\Messenger\Stamp\HandledStamp; +use Symfony\Component\Scheduler\Messenger\ScheduledStamp; +use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; +use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; +use Zenstruck\Messenger\Monitor\Stamp\TagStamp; + +/** + * @author Kevin Bond + * + * @internal + */ +final class ReceiveMonitorStampListener +{ + /** + * @param class-string[] $excludedClasses + */ + public function __construct(private array $excludedClasses) + { + } + + public function __invoke(WorkerMessageReceivedEvent $event): void + { + $envelope = $event->getEnvelope(); + + if ($this->isMonitoringDisabled($envelope)) { + return; + } + + $stamp = $envelope->last(MonitorStamp::class); + + if (\class_exists(ScheduledStamp::class) && $scheduledStamp = $envelope->last(ScheduledStamp::class)) { + // scheduler transport doesn't trigger SendMessageToTransportsEvent + $stamp = new MonitorStamp($scheduledStamp->messageContext->triggeredAt); + + $event->addStamps(TagStamp::forSchedule($scheduledStamp)); + } + + if ($stamp instanceof MonitorStamp) { + $event->addStamps($stamp->markReceived($event->getReceiverName())); + } + } + + private function isMonitoringDisabled(Envelope $envelope): bool + { + $messageClass = $envelope->getMessage()::class; + + foreach ($this->excludedClasses as $excludedClass) { + if (\is_a($messageClass, $excludedClass, true)) { + return true; + } + } + + $stamp = $envelope->last(DisableMonitoringStamp::class) ?? DisableMonitoringStamp::getFor($messageClass); + + if (!$stamp) { + return false; + } + + if ($stamp->onlyWhenNoHandler && !$this->hasNoHandlers($envelope)) { + return false; + } + + return true; + } + + private function hasNoHandlers(Envelope $envelope): bool + { + return [] === $envelope->all(HandledStamp::class); + } +} diff --git a/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php b/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php index 1be43f6..5745100 100644 --- a/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php +++ b/tests/Integration/DependencyInjection/ZenstruckMessengerMonitorExtensionTest.php @@ -17,7 +17,9 @@ use PHPUnit\Framework\Constraint\LogicalNot; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Zenstruck\Messenger\Monitor\DependencyInjection\ZenstruckMessengerMonitorExtension; -use Zenstruck\Messenger\Monitor\History\HistoryListener; +use Zenstruck\Messenger\Monitor\EventListener\AddMonitorStampListener; +use Zenstruck\Messenger\Monitor\EventListener\HandleMonitorStampListener; +use Zenstruck\Messenger\Monitor\EventListener\ReceiveMonitorStampListener; use Zenstruck\Messenger\Monitor\History\Model\ProcessedMessage; use Zenstruck\Messenger\Monitor\History\Storage; use Zenstruck\Messenger\Monitor\History\Storage\ORMStorage; @@ -40,7 +42,9 @@ public function no_config(): void $this->assertContainerBuilderHasAlias(Transports::class, 'zenstruck_messenger_monitor.transports'); $this->assertContainerBuilderHasAlias(Workers::class, 'zenstruck_messenger_monitor.workers'); $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasAliasConstraint(Storage::class))); - $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasServiceDefinitionConstraint('.zenstruck_messenger_monitor.history.listener'))); + $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasServiceDefinitionConstraint('.zenstruck_messenger_monitor.listener.add_monitor_stamp'))); + $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasServiceDefinitionConstraint('.zenstruck_messenger_monitor.listener.receive_monitor_stamp'))); + $this->assertThat($this->container, new LogicalNot(new ContainerBuilderHasServiceDefinitionConstraint('.zenstruck_messenger_monitor.listener.handle_monitor_stamp'))); } /** @@ -55,8 +59,10 @@ public function orm_config(): void $this->assertContainerBuilderHasService('zenstruck_messenger_monitor.history.storage', ORMStorage::class); $this->assertContainerBuilderHasServiceDefinitionWithArgument('zenstruck_messenger_monitor.history.storage', 1, ProcessedMessageImpl::class); $this->assertContainerBuilderHasAlias(Storage::class, 'zenstruck_messenger_monitor.history.storage'); - $this->assertContainerBuilderHasService('.zenstruck_messenger_monitor.history.listener', HistoryListener::class); - $this->assertContainerBuilderHasServiceDefinitionWithArgument('.zenstruck_messenger_monitor.history.listener', 2, []); + $this->assertContainerBuilderHasService('.zenstruck_messenger_monitor.listener.add_monitor_stamp', AddMonitorStampListener::class); + $this->assertContainerBuilderHasService('.zenstruck_messenger_monitor.listener.receive_monitor_stamp', ReceiveMonitorStampListener::class); + $this->assertContainerBuilderHasServiceDefinitionWithArgument('.zenstruck_messenger_monitor.listener.receive_monitor_stamp', 0, []); + $this->assertContainerBuilderHasService('.zenstruck_messenger_monitor.listener.handle_monitor_stamp', HandleMonitorStampListener::class); } /** @@ -69,7 +75,7 @@ public function storage_with_excluded_classes(): void 'exclude' => ['stdClass'], ]]); - $this->assertContainerBuilderHasServiceDefinitionWithArgument('.zenstruck_messenger_monitor.history.listener', 2, [\stdClass::class]); + $this->assertContainerBuilderHasServiceDefinitionWithArgument('.zenstruck_messenger_monitor.listener.receive_monitor_stamp', 0, [\stdClass::class]); } /** diff --git a/tests/Unit/EventListener/AddMonitorStampListenerTest.php b/tests/Unit/EventListener/AddMonitorStampListenerTest.php new file mode 100644 index 0000000..b256bf0 --- /dev/null +++ b/tests/Unit/EventListener/AddMonitorStampListenerTest.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Unit\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; +use Zenstruck\Messenger\Monitor\EventListener\AddMonitorStampListener; +use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; + +/** + * @author Kevin Bond + */ +final class AddMonitorStampListenerTest extends TestCase +{ + /** + * @test + */ + public function adds_monitor_stamp(): void + { + $listener = new AddMonitorStampListener(); + $envelope = new Envelope(new \stdClass()); + $event = new SendMessageToTransportsEvent($envelope, []); + + $this->assertNull($event->getEnvelope()->last(MonitorStamp::class)); + + $listener->__invoke($event); + + $this->assertInstanceOf(MonitorStamp::class, $event->getEnvelope()->last(MonitorStamp::class)); + } +} diff --git a/tests/Unit/EventListener/HandleMonitorStampListenerTest.php b/tests/Unit/EventListener/HandleMonitorStampListenerTest.php new file mode 100644 index 0000000..8ce932e --- /dev/null +++ b/tests/Unit/EventListener/HandleMonitorStampListenerTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Unit\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; +use Symfony\Component\Messenger\Stamp\HandledStamp; +use Zenstruck\Messenger\Monitor\EventListener\HandleMonitorStampListener; +use Zenstruck\Messenger\Monitor\History\Model\Results; +use Zenstruck\Messenger\Monitor\History\ResultNormalizer; +use Zenstruck\Messenger\Monitor\History\Storage; +use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; + +/** + * @author Kevin Bond + */ +final class HandleMonitorStampListenerTest extends TestCase +{ + /** + * @test + */ + public function handles_success(): void + { + $envelope = new Envelope(new \stdClass(), [ + (new MonitorStamp())->markReceived('foo'), + new HandledStamp('handler', 'return'), + ]); + $storage = $this->createMock(Storage::class); + $storage->expects($this->once())->method('save')->with( + $this->isInstanceOf(Envelope::class), + $this->isInstanceOf(Results::class), + ); + + $listener = new HandleMonitorStampListener($storage, new ResultNormalizer(__DIR__)); + + $listener->handleSuccess($event = new WorkerMessageHandledEvent($envelope, 'foo')); + + $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isFinished()); + } + + /** + * @test + */ + public function handles_success_invalid(): void + { + $storage = $this->createMock(Storage::class); + $storage->expects($this->never())->method('save'); + + $listener = new HandleMonitorStampListener($storage, new ResultNormalizer(__DIR__)); + + $listener->handleSuccess(new WorkerMessageHandledEvent(new Envelope(new \stdClass()), 'foo')); + $listener->handleSuccess(new WorkerMessageHandledEvent(new Envelope(new \stdClass(), [new MonitorStamp()]), 'foo')); + } + + /** + * @test + */ + public function handles_failure(): void + { + $envelope = new Envelope(new \stdClass(), [(new MonitorStamp())->markReceived('foo')]); + $exception = new \RuntimeException(); + $storage = $this->createMock(Storage::class); + $storage->expects($this->once())->method('save')->with( + $this->isInstanceOf(Envelope::class), + $this->isInstanceOf(Results::class), + $exception, + ); + + $listener = new HandleMonitorStampListener($storage, new ResultNormalizer(__DIR__)); + + $listener->handleFailure($event = new WorkerMessageFailedEvent($envelope, 'foo', $exception)); + + $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isFinished()); + } + + /** + * @test + */ + public function handles_failure_invalid(): void + { + $storage = $this->createMock(Storage::class); + $storage->expects($this->never())->method('save'); + + $listener = new HandleMonitorStampListener($storage, new ResultNormalizer(__DIR__)); + + $listener->handleFailure(new WorkerMessageFailedEvent(new Envelope(new \stdClass()), 'foo', new \RuntimeException())); + $listener->handleFailure(new WorkerMessageFailedEvent(new Envelope(new \stdClass(), [new MonitorStamp()]), 'foo', new \RuntimeException())); + } +} diff --git a/tests/Unit/History/HistoryListenerTest.php b/tests/Unit/EventListener/ReceiveMonitorStampListenerTest.php similarity index 56% rename from tests/Unit/History/HistoryListenerTest.php rename to tests/Unit/EventListener/ReceiveMonitorStampListenerTest.php index c3af2e3..94ce86b 100644 --- a/tests/Unit/History/HistoryListenerTest.php +++ b/tests/Unit/EventListener/ReceiveMonitorStampListenerTest.php @@ -9,22 +9,16 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Messenger\Monitor\Tests\Unit\History; +namespace Zenstruck\Messenger\Monitor\Tests\Unit\EventListener; use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; -use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent; -use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; -use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; use Symfony\Component\Messenger\Stamp\HandledStamp; use Symfony\Component\Scheduler\Generator\MessageContext; use Symfony\Component\Scheduler\Messenger\ScheduledStamp; use Symfony\Component\Scheduler\Trigger\TriggerInterface; -use Zenstruck\Messenger\Monitor\History\HistoryListener; -use Zenstruck\Messenger\Monitor\History\Model\Results; -use Zenstruck\Messenger\Monitor\History\ResultNormalizer; -use Zenstruck\Messenger\Monitor\History\Storage; +use Zenstruck\Messenger\Monitor\EventListener\ReceiveMonitorStampListener; use Zenstruck\Messenger\Monitor\Stamp\DisableMonitoringStamp; use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; use Zenstruck\Messenger\Monitor\Stamp\TagStamp; @@ -32,36 +26,20 @@ /** * @author Kevin Bond */ -final class HistoryListenerTest extends TestCase +final class ReceiveMonitorStampListenerTest extends TestCase { - /** - * @test - */ - public function adds_monitor_stamp(): void - { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); - $envelope = new Envelope(new \stdClass()); - $event = new SendMessageToTransportsEvent($envelope, []); - - $this->assertNull($event->getEnvelope()->last(MonitorStamp::class)); - - $listener->addMonitorStamp($event); - - $this->assertInstanceOf(MonitorStamp::class, $event->getEnvelope()->last(MonitorStamp::class)); - } - /** * @test */ public function skips_standard_messages_without_monitor_stamp(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new \stdClass()); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); $this->assertEmpty($event->getEnvelope()->all(MonitorStamp::class)); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertEmpty($event->getEnvelope()->all(MonitorStamp::class)); } @@ -71,13 +49,13 @@ public function skips_standard_messages_without_monitor_stamp(): void */ public function marks_standard_message_as_received(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new \stdClass(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); $this->assertSame('foo', $event->getEnvelope()->last(MonitorStamp::class)->transport()); @@ -89,7 +67,7 @@ public function marks_standard_message_as_received(): void */ public function marks_scheduled_message_as_received(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new \stdClass(), [new ScheduledStamp(new MessageContext( 'default', 'id', @@ -100,7 +78,7 @@ public function marks_scheduled_message_as_received(): void $this->assertNull($event->getEnvelope()->last(MonitorStamp::class)); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); $this->assertSame('foo', $event->getEnvelope()->last(MonitorStamp::class)->transport()); @@ -108,87 +86,16 @@ public function marks_scheduled_message_as_received(): void $this->assertSame('schedule:default:id', $event->getEnvelope()->last(TagStamp::class)->value); } - /** - * @test - */ - public function handles_success(): void - { - $envelope = new Envelope(new \stdClass(), [ - (new MonitorStamp())->markReceived('foo'), - new HandledStamp('handler', 'return'), - ]); - $storage = $this->createMock(Storage::class); - $storage->expects($this->once())->method('save')->with( - $this->isInstanceOf(Envelope::class), - $this->isInstanceOf(Results::class), - ); - - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__), []); - - $listener->handleSuccess($event = new WorkerMessageHandledEvent($envelope, 'foo')); - - $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isFinished()); - } - - /** - * @test - */ - public function handles_success_invalid(): void - { - $storage = $this->createMock(Storage::class); - $storage->expects($this->never())->method('save'); - - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__), []); - - $listener->handleSuccess(new WorkerMessageHandledEvent(new Envelope(new \stdClass()), 'foo')); - $listener->handleSuccess(new WorkerMessageHandledEvent(new Envelope(new \stdClass(), [new MonitorStamp()]), 'foo')); - } - - /** - * @test - */ - public function handles_failure(): void - { - $envelope = new Envelope(new \stdClass(), [(new MonitorStamp())->markReceived('foo')]); - $exception = new \RuntimeException(); - $storage = $this->createMock(Storage::class); - $storage->expects($this->once())->method('save')->with( - $this->isInstanceOf(Envelope::class), - $this->isInstanceOf(Results::class), - $exception, - ); - - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__), []); - - $listener->handleFailure($event = new WorkerMessageFailedEvent($envelope, 'foo', $exception)); - - $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isFinished()); - } - - /** - * @test - */ - public function handles_failure_invalid(): void - { - $storage = $this->createMock(Storage::class); - $storage->expects($this->never())->method('save'); - - $listener = new HistoryListener($storage, new ResultNormalizer(__DIR__), []); - - $listener->handleFailure(new WorkerMessageFailedEvent(new Envelope(new \stdClass()), 'foo', new \RuntimeException())); - $listener->handleFailure(new WorkerMessageFailedEvent(new Envelope(new \stdClass(), [new MonitorStamp()]), 'foo', new \RuntimeException())); - } - /** * @test */ public function can_disable_monitoring_with_envelope_stamp(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new \stdClass(), [new MonitorStamp(), new DisableMonitoringStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } @@ -198,11 +105,11 @@ public function can_disable_monitoring_with_envelope_stamp(): void */ public function can_disable_monitoring_message_attribute(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new DisabledMonitoringMessage(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } @@ -212,11 +119,11 @@ public function can_disable_monitoring_message_attribute(): void */ public function can_disable_monitoring_message_interface_attribute(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new DisableMonitoringViaInterface(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } @@ -226,11 +133,11 @@ public function can_disable_monitoring_message_interface_attribute(): void */ public function can_disable_monitoring_message_attribute_without_handler(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new DisabledMonitoringWithoutHandlerMessage(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } @@ -240,14 +147,14 @@ public function can_disable_monitoring_message_attribute_without_handler(): void */ public function can_disable_monitoring_message_without_handler(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope(new \stdClass(), [ new MonitorStamp(), new DisableMonitoringStamp(true), ]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } @@ -257,7 +164,7 @@ public function can_disable_monitoring_message_without_handler(): void */ public function handle_disable_monitoring_message_attribute_with_handler(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope( new EnabledMonitoringWithHandlerMessage(), [ @@ -267,7 +174,7 @@ public function handle_disable_monitoring_message_attribute_with_handler(): void ); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } @@ -277,7 +184,7 @@ public function handle_disable_monitoring_message_attribute_with_handler(): void */ public function handle_disable_monitoring_message_with_handler(): void { - $listener = new HistoryListener($this->createMock(Storage::class), new ResultNormalizer(__DIR__), []); + $listener = new ReceiveMonitorStampListener([]); $envelope = new Envelope( new \stdClass(), [ @@ -288,7 +195,7 @@ public function handle_disable_monitoring_message_with_handler(): void ); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertTrue($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } @@ -298,9 +205,7 @@ public function handle_disable_monitoring_message_with_handler(): void */ public function can_disable_monitoring_message_via_config(): void { - $listener = new HistoryListener( - $this->createMock(Storage::class), - new ResultNormalizer(__DIR__), + $listener = new ReceiveMonitorStampListener( [ MessageToDisableViaConfig::class, ] @@ -308,7 +213,7 @@ public function can_disable_monitoring_message_via_config(): void $envelope = new Envelope(new MessageToDisableViaConfig(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } @@ -318,9 +223,7 @@ public function can_disable_monitoring_message_via_config(): void */ public function can_disable_extended_monitoring_message_via_config(): void { - $listener = new HistoryListener( - $this->createMock(Storage::class), - new ResultNormalizer(__DIR__), + $listener = new ReceiveMonitorStampListener( [ MessageToDisableViaConfig::class, ] @@ -328,7 +231,7 @@ public function can_disable_extended_monitoring_message_via_config(): void $envelope = new Envelope(new ExtendedMessageToDisableViaConfig(), [new MonitorStamp()]); $event = new WorkerMessageReceivedEvent($envelope, 'foo'); - $listener->receiveMessage($event); + $listener->__invoke($event); $this->assertFalse($event->getEnvelope()->last(MonitorStamp::class)->isReceived()); } diff --git a/tests/Unit/History/Model/ResultsTest.php b/tests/Unit/History/Model/ResultsTest.php index a8c1a67..638f86b 100644 --- a/tests/Unit/History/Model/ResultsTest.php +++ b/tests/Unit/History/Model/ResultsTest.php @@ -30,6 +30,6 @@ public function can_create_with_null(): void $this->assertSame([], $results->all()); $this->assertSame([], $results->successes()); $this->assertSame([], $results->failures()); - $this->assertSame(null, $results->jsonSerialize()); + $this->assertNull($results->jsonSerialize()); } } From 005e050737467a068443341a84778a6e9bed18c6 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sun, 24 Nov 2024 19:24:57 -0500 Subject: [PATCH 5/6] minor: add feature tests --- composer.json | 3 +- tests/Feature/BundleServicesTest.php | 34 ++++ tests/Feature/HistoryTest.php | 168 ++++++++++++++++++ .../SerializerTest.php} | 34 +--- tests/Fixture/Message/MessageA.php | 5 + tests/Fixture/Message/MessageAHandler.php | 30 ++++ tests/Fixture/Message/MessageC.php | 25 +++ tests/Fixture/Message/MessageCHandler1.php | 30 ++++ tests/Fixture/Message/MessageCHandler2.php | 26 +++ tests/Fixture/TestKernel.php | 18 +- .../Command/MonitorCommandTest.php | 36 ++++ 11 files changed, 376 insertions(+), 33 deletions(-) create mode 100644 tests/Feature/BundleServicesTest.php create mode 100644 tests/Feature/HistoryTest.php rename tests/{Integration/ZenstruckMessengerMonitorBundleTest.php => Feature/SerializerTest.php} (59%) create mode 100644 tests/Fixture/Message/MessageAHandler.php create mode 100644 tests/Fixture/Message/MessageC.php create mode 100644 tests/Fixture/Message/MessageCHandler1.php create mode 100644 tests/Fixture/Message/MessageCHandler2.php create mode 100644 tests/Integration/Command/MonitorCommandTest.php diff --git a/composer.json b/composer.json index f582be4..9237330 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "symfony/serializer": "^6.4|^7.0", "symfony/var-dumper": "^6.4|^7.0", "zenstruck/console-test": "^1.5", - "zenstruck/foundry": "^2.2" + "zenstruck/foundry": "^2.2", + "zenstruck/messenger-test": "^1.11" }, "suggest": { "knplabs/knp-time-bundle": "For human readable timestamps and durations on your dashboard.", diff --git a/tests/Feature/BundleServicesTest.php b/tests/Feature/BundleServicesTest.php new file mode 100644 index 0000000..c2edd6f --- /dev/null +++ b/tests/Feature/BundleServicesTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Feature; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Messenger\Monitor\Tests\Fixture\TestService; + +/** + * @author Kevin Bond + */ +final class BundleServicesTest extends KernelTestCase +{ + /** + * @test + */ + public function autowires_services(): void + { + /** @var TestService $service */ + $service = self::getContainer()->get(TestService::class); + + $this->assertCount(1, $service->transports); + $this->assertCount(0, $service->workers); + $this->assertCount(0, $service->schedules); + } +} diff --git a/tests/Feature/HistoryTest.php b/tests/Feature/HistoryTest.php new file mode 100644 index 0000000..d8e7b32 --- /dev/null +++ b/tests/Feature/HistoryTest.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Feature; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\Exception\NoHandlerForMessageException; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Factory\ProcessedMessageFactory; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageA; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageAHandler; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageB; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageC; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageCHandler1; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageCHandler2; +use Zenstruck\Messenger\Test\InteractsWithMessenger; + +/** + * @author Kevin Bond + */ +final class HistoryTest extends KernelTestCase +{ + use Factories, InteractsWithMessenger, ResetDatabase; + + /** + * @test + */ + public function flow_for_single_handler(): void + { + ProcessedMessageFactory::assert()->empty(); + + $this->bus()->dispatch(new MessageA(return: 'foo')); + + $this->transport()->processOrFail(1); + + ProcessedMessageFactory::assert()->count(1); + + $message = ProcessedMessageFactory::first(); + $this->assertSame(MessageA::class, $message->type()->class()); + $this->assertSame('async', $message->transport()); + $this->assertFalse($message->isFailure()); + $this->assertCount(1, $message->results()); + $this->assertSame(['data' => 'foo'], $message->results()->all()[0]->data()); + $this->assertFalse($message->results()->all()[0]->isFailure()); + $this->assertSame(MessageAHandler::class, $message->results()->all()[0]->handler()?->class()); + $this->assertNull($message->results()->all()[0]->handler()?->description()); + } + + /** + * @test + */ + public function flow_for_single_handler_failure(): void + { + ProcessedMessageFactory::assert()->empty(); + + $this->bus()->dispatch(new MessageA(return: 'foo', throw: true)); + + $this->transport()->processOrFail(1); + + ProcessedMessageFactory::assert()->count(1); + + $message = ProcessedMessageFactory::first(); + $this->assertSame(MessageA::class, $message->type()->class()); + $this->assertTrue($message->isFailure()); + $this->assertSame(HandlerFailedException::class, $message->failure()?->class()); + $this->assertSame(\sprintf('Handling "%s" failed: error', MessageA::class), $message->failure()->description()); + $this->assertCount(1, $message->results()); + $this->assertTrue($message->results()->all()[0]->isFailure()); + $this->assertSame(\RuntimeException::class, $message->results()->all()[0]->failure()?->class()); + $this->assertSame('error', $message->results()->all()[0]->failure()->description()); + $this->assertSame(MessageAHandler::class, $message->results()->all()[0]->handler()?->class()); + $this->assertSame(['stack_trace'], \array_keys($message->results()->all()[0]->data())); + } + + /** + * @test + */ + public function flow_for_missing_handler(): void + { + ProcessedMessageFactory::assert()->empty(); + + $this->bus()->dispatch(new MessageB()); + + $this->transport()->processOrFail(1); + + ProcessedMessageFactory::assert()->count(1); + + $message = ProcessedMessageFactory::first(); + + $this->assertTrue($message->isFailure()); + $this->assertSame(NoHandlerForMessageException::class, $message->failure()?->class()); + $this->assertSame(\sprintf('No handler for message "%s".', MessageB::class), $message->failure()->description()); + $this->assertEmpty($message->results()->all()); + } + + /** + * @test + */ + public function flow_for_multiple_handlers(): void + { + ProcessedMessageFactory::assert()->empty(); + + $this->bus()->dispatch(new MessageC(return1: 'foo', return2: 'bar')); + + $this->transport()->processOrFail(1); + + ProcessedMessageFactory::assert()->count(1); + + $message = ProcessedMessageFactory::first(); + $this->assertSame(MessageC::class, $message->type()->class()); + $this->assertSame('async', $message->transport()); + $this->assertFalse($message->isFailure()); + $this->assertCount(2, $message->results()); + + $this->assertSame(['data' => 'foo'], $message->results()->all()[0]->data()); + $this->assertFalse($message->results()->all()[0]->isFailure()); + $this->assertSame(MessageCHandler1::class, $message->results()->all()[0]->handler()?->class()); + $this->assertNull($message->results()->all()[0]->handler()?->description()); + + $this->assertSame(['data' => 'bar'], $message->results()->all()[1]->data()); + $this->assertFalse($message->results()->all()[1]->isFailure()); + $this->assertSame(MessageCHandler2::class, $message->results()->all()[1]->handler()?->class()); + $this->assertSame('handle', $message->results()->all()[1]->handler()?->description()); + } + + /** + * @test + */ + public function flow_for_multiple_handlers_one_fails(): void + { + $this->bus()->dispatch(new MessageC(return1: 'foo', return2: 'bar', throw: true)); + + $this->transport()->processOrFail(1); + + ProcessedMessageFactory::assert()->count(1); + + $message = ProcessedMessageFactory::first(); + $this->assertSame(MessageC::class, $message->type()->class()); + $this->assertSame('async', $message->transport()); + $this->assertTrue($message->isFailure()); + + $this->assertSame(HandlerFailedException::class, $message->failure()?->class()); + $this->assertSame(\sprintf('Handling "%s" failed: error', MessageC::class), $message->failure()->description()); + $this->assertCount(2, $message->results()); + + $this->assertSame(['data' => 'bar'], $message->results()->all()[0]->data()); + $this->assertFalse($message->results()->all()[0]->isFailure()); + $this->assertSame(MessageCHandler2::class, $message->results()->all()[0]->handler()?->class()); + $this->assertSame('handle', $message->results()->all()[0]->handler()?->description()); + + $this->assertTrue($message->results()->all()[1]->isFailure()); + $this->assertSame(MessageCHandler1::class, $message->results()->all()[1]->handler()?->class()); + $this->assertNull($message->results()->all()[1]->handler()?->description()); + $this->assertSame(\RuntimeException::class, $message->results()->all()[1]->failure()?->class()); + $this->assertSame('error', $message->results()->all()[1]->failure()->description()); + $this->assertSame(['stack_trace'], \array_keys($message->results()->all()[1]->data())); + } +} diff --git a/tests/Integration/ZenstruckMessengerMonitorBundleTest.php b/tests/Feature/SerializerTest.php similarity index 59% rename from tests/Integration/ZenstruckMessengerMonitorBundleTest.php rename to tests/Feature/SerializerTest.php index c09fc89..aefe0b7 100644 --- a/tests/Integration/ZenstruckMessengerMonitorBundleTest.php +++ b/tests/Feature/SerializerTest.php @@ -9,47 +9,19 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Messenger\Monitor\Tests\Integration; +namespace Zenstruck\Messenger\Monitor\Tests\Feature; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Clock\Test\ClockSensitiveTrait; use Symfony\Component\Serializer\Serializer; -use Zenstruck\Console\Test\InteractsWithConsole; -use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Messenger\Monitor\Stamp\MonitorStamp; -use Zenstruck\Messenger\Monitor\Tests\Fixture\TestService; /** * @author Kevin Bond */ -final class ZenstruckMessengerMonitorBundleTest extends KernelTestCase +final class SerializerTest extends KernelTestCase { - use ClockSensitiveTrait, InteractsWithConsole, ResetDatabase; - - /** - * @test - */ - public function autowires_services(): void - { - /** @var TestService $service */ - $service = self::getContainer()->get(TestService::class); - - $this->assertCount(1, $service->transports); - $this->assertCount(0, $service->workers); - $this->assertCount(0, $service->schedules); - } - - /** - * @test - */ - public function run_messenger_monitor_command(): void - { - $this->executeConsoleCommand('messenger:monitor') - ->assertSuccessful() - ->assertOutputContains('[!] No workers running.') - ->assertOutputContains('async n/a') - ; - } + use ClockSensitiveTrait; /** * @test diff --git a/tests/Fixture/Message/MessageA.php b/tests/Fixture/Message/MessageA.php index 39caa61..8e239b5 100644 --- a/tests/Fixture/Message/MessageA.php +++ b/tests/Fixture/Message/MessageA.php @@ -16,4 +16,9 @@ */ final class MessageA { + public function __construct( + public readonly mixed $return = null, + public readonly bool $throw = false, + ) { + } } diff --git a/tests/Fixture/Message/MessageAHandler.php b/tests/Fixture/Message/MessageAHandler.php new file mode 100644 index 0000000..c6d913a --- /dev/null +++ b/tests/Fixture/Message/MessageAHandler.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Fixture\Message; + +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +/** + * @author Kevin Bond + */ +#[AsMessageHandler] +final class MessageAHandler +{ + public function __invoke(MessageA $message): mixed + { + if ($message->throw) { + throw new \RuntimeException('error'); + } + + return $message->return; + } +} diff --git a/tests/Fixture/Message/MessageC.php b/tests/Fixture/Message/MessageC.php new file mode 100644 index 0000000..bed4830 --- /dev/null +++ b/tests/Fixture/Message/MessageC.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Fixture\Message; + +/** + * @author Kevin Bond + */ +final class MessageC +{ + public function __construct( + public readonly mixed $return1 = null, + public readonly mixed $return2 = null, + public readonly bool $throw = false, + ) { + } +} diff --git a/tests/Fixture/Message/MessageCHandler1.php b/tests/Fixture/Message/MessageCHandler1.php new file mode 100644 index 0000000..93ab4e7 --- /dev/null +++ b/tests/Fixture/Message/MessageCHandler1.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Fixture\Message; + +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +/** + * @author Kevin Bond + */ +#[AsMessageHandler] +final class MessageCHandler1 +{ + public function __invoke(MessageC $message): mixed + { + if ($message->throw) { + throw new \RuntimeException('error'); + } + + return $message->return1; + } +} diff --git a/tests/Fixture/Message/MessageCHandler2.php b/tests/Fixture/Message/MessageCHandler2.php new file mode 100644 index 0000000..f345a89 --- /dev/null +++ b/tests/Fixture/Message/MessageCHandler2.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Fixture\Message; + +use Symfony\Component\Messenger\Attribute\AsMessageHandler; + +/** + * @author Kevin Bond + */ +final class MessageCHandler2 +{ + #[AsMessageHandler] + public function handle(MessageC $message): mixed + { + return $message->return2; + } +} diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index 5293e71..71ba52f 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -20,7 +20,14 @@ use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Zenstruck\Foundry\ZenstruckFoundryBundle; use Zenstruck\Messenger\Monitor\Tests\Fixture\Entity\ProcessedMessage; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageA; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageAHandler; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageB; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageC; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageCHandler1; +use Zenstruck\Messenger\Monitor\Tests\Fixture\Message\MessageCHandler2; use Zenstruck\Messenger\Monitor\ZenstruckMessengerMonitorBundle; +use Zenstruck\Messenger\Test\ZenstruckMessengerTestBundle; /** * @author Kevin Bond @@ -33,6 +40,7 @@ public function registerBundles(): iterable { yield new FrameworkBundle(); yield new DoctrineBundle(); + yield new ZenstruckMessengerTestBundle(); yield new ZenstruckFoundryBundle(); yield new ZenstruckMessengerMonitorBundle(); } @@ -47,7 +55,12 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'serializer' => true, 'messenger' => [ 'transports' => [ - 'async' => 'in-memory://', + 'async' => 'test://', + ], + 'routing' => [ + MessageA::class => 'async', + MessageB::class => 'async', + MessageC::class => 'async', ], ], ]); @@ -78,6 +91,9 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ]); $c->register(TestService::class)->setAutowired(true)->setPublic(true); + $c->register(MessageAHandler::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(MessageCHandler1::class)->setAutowired(true)->setAutoconfigured(true); + $c->register(MessageCHandler2::class)->setAutowired(true)->setAutoconfigured(true); } protected function configureRoutes(RoutingConfigurator $routes): void diff --git a/tests/Integration/Command/MonitorCommandTest.php b/tests/Integration/Command/MonitorCommandTest.php new file mode 100644 index 0000000..2c81945 --- /dev/null +++ b/tests/Integration/Command/MonitorCommandTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Tests\Integration\Command; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Console\Test\InteractsWithConsole; +use Zenstruck\Foundry\Test\ResetDatabase; + +/** + * @author Kevin Bond + */ +final class MonitorCommandTest extends KernelTestCase +{ + use InteractsWithConsole, ResetDatabase; + + /** + * @test + */ + public function run_messenger_monitor_command(): void + { + $this->executeConsoleCommand('messenger:monitor') + ->assertSuccessful() + ->assertOutputContains('[!] No workers running.') + ->assertOutputContains('async 0 0') + ; + } +} From 747bb44bbd3aa64fd3830d7a68663d46a0d03168 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sun, 24 Nov 2024 20:48:10 -0500 Subject: [PATCH 6/6] feat(history): allow `#[TagStamp]` to be added to parent classes/interfaces --- README.md | 4 +- .../ReceiveMonitorStampListener.php | 4 +- src/History/Model/Tags.php | 8 +-- src/Stamp/AttributeStamp.php | 62 +++++++++++++++++++ src/Stamp/DisableMonitoringStamp.php | 30 +-------- src/Stamp/TagStamp.php | 3 +- tests/Unit/History/Model/TagsTest.php | 14 ++++- 7 files changed, 82 insertions(+), 43 deletions(-) create mode 100644 src/Stamp/AttributeStamp.php diff --git a/README.md b/README.md index 2c9418b..83217a6 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ add your own in one of two ways: $bus->dispatch(new MyMessage(), [new TagStamp('tag-1'), new TagStamp('tag-2')]) ``` -2. Add the `TagStamp` as a class attribute to your message: +2. Add the `TagStamp` as a class attribute to your message (and parent class/interface): ```php use Zenstruck\Messenger\Monitor\Stamp\TagStamp; @@ -164,6 +164,8 @@ add your own in one of two ways: { } ``` + > [!TIP] + > You can also add the `TagStamp` attribute to parent classes/interfaces. #### `messenger:monitor:purge` Command diff --git a/src/EventListener/ReceiveMonitorStampListener.php b/src/EventListener/ReceiveMonitorStampListener.php index ad9e14c..36ced8e 100644 --- a/src/EventListener/ReceiveMonitorStampListener.php +++ b/src/EventListener/ReceiveMonitorStampListener.php @@ -65,9 +65,7 @@ private function isMonitoringDisabled(Envelope $envelope): bool } } - $stamp = $envelope->last(DisableMonitoringStamp::class) ?? DisableMonitoringStamp::getFor($messageClass); - - if (!$stamp) { + if (!$stamp = DisableMonitoringStamp::firstFrom($envelope)) { return false; } diff --git a/src/History/Model/Tags.php b/src/History/Model/Tags.php index cd5cbee..34dfa79 100644 --- a/src/History/Model/Tags.php +++ b/src/History/Model/Tags.php @@ -100,12 +100,8 @@ static function(string $tag): array { */ private static function parseFrom(Envelope $envelope): \Traversable { - foreach ((new \ReflectionClass($envelope->getMessage()))->getAttributes(TagStamp::class) as $attribute) { - yield $attribute->newInstance()->value; - } - - foreach ($envelope->all(TagStamp::class) as $tag) { - yield $tag->value; // @phpstan-ignore-line + foreach (TagStamp::from($envelope) as $tag) { + yield $tag->value; } } } diff --git a/src/Stamp/AttributeStamp.php b/src/Stamp/AttributeStamp.php new file mode 100644 index 0000000..c21b316 --- /dev/null +++ b/src/Stamp/AttributeStamp.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\Stamp; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Stamp\StampInterface; + +/** + * @author Kevin Bond + */ +abstract class AttributeStamp implements StampInterface +{ + /** + * @internal + * + * @return static[] + */ + final public static function from(Envelope $envelope): iterable + { + foreach ($envelope->all(static::class) as $stamp) { + yield $stamp; // @phpstan-ignore generator.valueType + } + + $original = $reflection = new \ReflectionClass($envelope->getMessage()); + + while (false !== $reflection) { + + foreach ($reflection->getAttributes(static::class) as $attribute) { + yield $attribute->newInstance(); + } + + $reflection = $reflection->getParentClass(); + } + + foreach ($original->getInterfaces() as $refInterface) { + foreach ($refInterface->getAttributes(static::class) as $attribute) { + yield $attribute->newInstance(); + } + } + } + + /** + * @internal + */ + final public static function firstFrom(Envelope $envelope): ?static + { + foreach (self::from($envelope) as $stamp) { + return $stamp; + } + + return null; + } +} diff --git a/src/Stamp/DisableMonitoringStamp.php b/src/Stamp/DisableMonitoringStamp.php index 548cd59..c7d6645 100644 --- a/src/Stamp/DisableMonitoringStamp.php +++ b/src/Stamp/DisableMonitoringStamp.php @@ -11,41 +11,13 @@ namespace Zenstruck\Messenger\Monitor\Stamp; -use Symfony\Component\Messenger\Stamp\StampInterface; - /** * @author Kevin Bond */ #[\Attribute(\Attribute::TARGET_CLASS)] -final class DisableMonitoringStamp implements StampInterface +final class DisableMonitoringStamp extends AttributeStamp { public function __construct(public readonly bool $onlyWhenNoHandler = false) { } - - /** - * @internal - * - * @param class-string $class - */ - public static function getFor(string $class): ?self - { - $original = $reflection = new \ReflectionClass($class); - - while (false !== $reflection) { - if ($attributes = $reflection->getAttributes(self::class)) { - return $attributes[0]->newInstance(); - } - - $reflection = $reflection->getParentClass(); - } - - foreach ($original->getInterfaces() as $refInterface) { - if ($attributes = $refInterface->getAttributes(self::class)) { - return $attributes[0]->newInstance(); - } - } - - return null; - } } diff --git a/src/Stamp/TagStamp.php b/src/Stamp/TagStamp.php index df5b7b6..522bcb6 100644 --- a/src/Stamp/TagStamp.php +++ b/src/Stamp/TagStamp.php @@ -11,7 +11,6 @@ namespace Zenstruck\Messenger\Monitor\Stamp; -use Symfony\Component\Messenger\Stamp\StampInterface; use Symfony\Component\Scheduler\Messenger\ScheduledStamp; use Zenstruck\Messenger\Monitor\Schedule\TaskInfo; @@ -19,7 +18,7 @@ * @author Kevin Bond */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] -final class TagStamp implements StampInterface, \Stringable +final class TagStamp extends AttributeStamp implements \Stringable { public function __construct( public readonly string $value, diff --git a/tests/Unit/History/Model/TagsTest.php b/tests/Unit/History/Model/TagsTest.php index 28c2779..fe998fe 100644 --- a/tests/Unit/History/Model/TagsTest.php +++ b/tests/Unit/History/Model/TagsTest.php @@ -87,7 +87,7 @@ public function create_from_envelope(): void new TagStamp('qux'), ]); - $this->assertSame(['from', 'attribute', 'bar', 'foo', 'baz', 'qux'], (new Tags($envelope))->all()); + $this->assertSame(['foo', 'bar', 'baz', 'qux', 'from', 'attribute', 'abstract', 'interface'], (new Tags($envelope))->all()); } /** @@ -104,9 +104,19 @@ public function expand(): void } } +#[TagStamp('interface')] +interface InterfaceMessage +{ +} + +#[TagStamp('abstract')] +abstract class AbstractMessage implements InterfaceMessage +{ +} + #[TagStamp('from')] #[TagStamp('attribute')] #[TagStamp('bar')] -class TestMessage +class TestMessage extends AbstractMessage { }