From da5c90733b510fba17bc50640c75099a1123545f Mon Sep 17 00:00:00 2001 From: MatTheCat Date: Fri, 8 Dec 2023 20:11:12 +0100 Subject: [PATCH] [Messenger] Deprecate `HandleTrait` in favor of a new `SingleHandlingTrait` --- UPGRADE-7.1.md | 1 + src/Symfony/Component/Messenger/CHANGELOG.md | 1 + .../Component/Messenger/HandleTrait.php | 4 + .../Messenger/SingleHandlingTrait.php | 69 ++++++++++ ...estTracesWithSingleHandlingTraitAction.php | 33 +++++ .../Messenger/Tests/HandleTraitTest.php | 3 + .../Tests/SingleHandlingTraitTest.php | 127 ++++++++++++++++++ .../Tests/TraceableMessageBusTest.php | 36 +++++ .../Messenger/TraceableMessageBus.php | 3 +- src/Symfony/Component/Messenger/composer.json | 3 +- 10 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/Messenger/SingleHandlingTrait.php create mode 100644 src/Symfony/Component/Messenger/Tests/Fixtures/TestTracesWithSingleHandlingTraitAction.php create mode 100644 src/Symfony/Component/Messenger/Tests/SingleHandlingTraitTest.php diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md index cefaa966b352f..25c269a5761f2 100644 --- a/UPGRADE-7.1.md +++ b/UPGRADE-7.1.md @@ -10,6 +10,7 @@ Messenger --------- * Make `#[AsMessageHandler]` final + * Deprecate `HandleTrait`, use `SingleHandlingTrait` instead Workflow -------- diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index e741320def16e..bc4888daad038 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add option `redis_sentinel` as an alias for `sentinel_master` * Add `--all` option to the `messenger:consume` command * Make `#[AsMessageHandler]` final + * Deprecate `HandleTrait`, use `SingleHandlingTrait` instead 7.0 --- diff --git a/src/Symfony/Component/Messenger/HandleTrait.php b/src/Symfony/Component/Messenger/HandleTrait.php index ac86302af4095..a09af219a1ca3 100644 --- a/src/Symfony/Component/Messenger/HandleTrait.php +++ b/src/Symfony/Component/Messenger/HandleTrait.php @@ -14,10 +14,14 @@ use Symfony\Component\Messenger\Exception\LogicException; use Symfony\Component\Messenger\Stamp\HandledStamp; +trigger_deprecation('symfony/messenger', '7.1', 'The "%s" class is deprecated, use "%s" instead.', HandleTrait::class, SingleHandlingTrait::class); + /** * Leverages a message bus to expect a single, synchronous message handling and return its result. * * @author Maxime Steinhausser + * + * @deprecated since Symfony 7.1, use SingleHandlingTrait instead. */ trait HandleTrait { diff --git a/src/Symfony/Component/Messenger/SingleHandlingTrait.php b/src/Symfony/Component/Messenger/SingleHandlingTrait.php new file mode 100644 index 0000000000000..922edfe3303db --- /dev/null +++ b/src/Symfony/Component/Messenger/SingleHandlingTrait.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger; + +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\Stamp\HandledStamp; +use Symfony\Component\Messenger\Stamp\StampInterface; + +trait SingleHandlingTrait +{ + private readonly MessageBusInterface $messageBus; + + /** + * Dispatches the given message, expecting to be handled by a single handler + * and returns the result from the handler returned value. + * This behavior is useful for both synchronous command & query buses, + * the last one usually returning the handler result. + * + * @param object|Envelope $message The message or the message pre-wrapped in an envelope + * @param StampInterface[] $stamps + */ + private function handle(object $message, array $stamps = []): mixed + { + if (!isset($this->messageBus)) { + throw new LogicException(sprintf('You must provide a "%s" instance in the "%s::$messageBus" property, but that property has not been initialized yet.', MessageBusInterface::class, static::class)); + } + + $exceptions = []; + + try { + $envelope = $this->messageBus->dispatch($message, $stamps); + } catch (HandlerFailedException $exception) { + $envelope = $exception->getEnvelope(); + $exceptions = $exception->getWrappedExceptions(); + } + + /** @var HandledStamp[] $handledStamps */ + $handledStamps = $envelope->all(HandledStamp::class); + + $handlers = array_merge( + array_map(static fn (HandledStamp $stamp) => $stamp->getHandlerName(), $handledStamps), + array_keys($exceptions), + ); + + if (!$handlers) { + throw new LogicException(sprintf('Message of type "%s" was handled zero times. Exactly one handler is expected when using "%s::%s()".', $envelope->getMessage()::class, static::class, __FUNCTION__)); + } + + if (\count($handlers) > 1) { + throw new LogicException(sprintf('Message of type "%s" was handled multiple times. Only one handler is expected when using "%s::%s()", got %d: "%s".', $envelope->getMessage()::class, static::class, __FUNCTION__, \count($handlers), implode('", "', $handlers))); + } + + if ($exceptions) { + throw reset($exceptions); + } + + return $handledStamps[0]->getResult(); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Fixtures/TestTracesWithSingleHandlingTraitAction.php b/src/Symfony/Component/Messenger/Tests/Fixtures/TestTracesWithSingleHandlingTraitAction.php new file mode 100644 index 0000000000000..f80b908553e88 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Fixtures/TestTracesWithSingleHandlingTraitAction.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Fixtures; + +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\SingleHandlingTrait; + +/** + * @see \Symfony\Component\Messenger\Tests\TraceableMessageBusTest::testItTracesDispatchWhenSingleHandlingTraitIsUsed + */ +class TestTracesWithSingleHandlingTraitAction +{ + use SingleHandlingTrait; + + public function __construct(MessageBusInterface $messageBus) + { + $this->messageBus = $messageBus; + } + + public function __invoke($message) + { + $this->handle($message); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/HandleTraitTest.php b/src/Symfony/Component/Messenger/Tests/HandleTraitTest.php index 6a016b4165832..0bf33eb9c31b3 100644 --- a/src/Symfony/Component/Messenger/Tests/HandleTraitTest.php +++ b/src/Symfony/Component/Messenger/Tests/HandleTraitTest.php @@ -20,6 +20,9 @@ use Symfony\Component\Messenger\Stamp\HandledStamp; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; +/** + * @group legacy + */ class HandleTraitTest extends TestCase { public function testItThrowsOnNoMessageBusInstance() diff --git a/src/Symfony/Component/Messenger/Tests/SingleHandlingTraitTest.php b/src/Symfony/Component/Messenger/Tests/SingleHandlingTraitTest.php new file mode 100644 index 0000000000000..a5202fbb53be6 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/SingleHandlingTraitTest.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\Exception\LogicException; +use Symfony\Component\Messenger\MessageBus; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\SingleHandlingTrait; +use Symfony\Component\Messenger\Stamp\HandledStamp; +use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; + +class SingleHandlingTraitTest extends TestCase +{ + public function testItThrowsOnNoMessageBusInstance() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('You must provide a "Symfony\Component\Messenger\MessageBusInterface" instance in the "Symfony\Component\Messenger\Tests\SingleHandlerBus::$messageBus" property, but that property has not been initialized yet.'); + $singleHandlerBus = new SingleHandlerBus(null); + $message = new DummyMessage('Hello'); + + $singleHandlerBus->dispatch($message); + } + + public function testHandleReturnsHandledStampResult() + { + $bus = $this->createMock(MessageBus::class); + $singleHandlerBus = new SingleHandlerBus($bus); + + $message = new DummyMessage('Hello'); + $bus->expects($this->once())->method('dispatch')->willReturn( + new Envelope($message, [new HandledStamp('result', 'DummyHandler::__invoke')]) + ); + + $this->assertSame('result', $singleHandlerBus->dispatch($message)); + } + + public function testHandleAcceptsEnvelopes() + { + $bus = $this->createMock(MessageBus::class); + $singleHandlerBus = new SingleHandlerBus($bus); + + $envelope = new Envelope(new DummyMessage('Hello'), [new HandledStamp('result', 'DummyHandler::__invoke')]); + $bus->expects($this->once())->method('dispatch')->willReturn($envelope); + + $this->assertSame('result', $singleHandlerBus->dispatch($envelope)); + } + + public function testHandleThrowsOnNoHandledStamp() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Message of type "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage" was handled zero times. Exactly one handler is expected when using "Symfony\Component\Messenger\Tests\SingleHandlerBus::handle()".'); + $bus = $this->createMock(MessageBus::class); + $singleHandlerBus = new SingleHandlerBus($bus); + + $message = new DummyMessage('Hello'); + $bus->expects($this->once())->method('dispatch')->willReturn(new Envelope($message)); + + $singleHandlerBus->dispatch($message); + } + + public function testHandleThrowsOnMultipleHandledStamps() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Message of type "Symfony\Component\Messenger\Tests\Fixtures\DummyMessage" was handled multiple times. Only one handler is expected when using "Symfony\Component\Messenger\Tests\SingleHandlerBus::handle()", got 2: "FirstDummyHandler::__invoke", "SecondDummyHandler::__invoke".'); + $bus = $this->createMock(MessageBus::class); + $singleHandlerBus = new SingleHandlerBus($bus); + + $message = new DummyMessage('Hello'); + $bus->expects($this->once())->method('dispatch')->willThrowException( + new HandlerFailedException( + new Envelope($message, [new HandledStamp('first_result', 'FirstDummyHandler::__invoke')]), + ['SecondDummyHandler::__invoke' => new \RuntimeException('SecondDummyHandler failed.')] + ) + ); + + $singleHandlerBus->dispatch($message); + } + + public function testHandleThrowsWrappedException() + { + $bus = $this->createMock(MessageBus::class); + $singleHandlerBus = new SingleHandlerBus($bus); + + $message = new DummyMessage('Hello'); + $wrappedException = new \RuntimeException('Handler failed.'); + $bus->expects($this->once())->method('dispatch')->willThrowException( + new HandlerFailedException( + new Envelope($message), + ['DummyHandler::__invoke' => new \RuntimeException('Handler failed.')] + ) + ); + + $this->expectException($wrappedException::class); + $this->expectExceptionMessage($wrappedException->getMessage()); + + $singleHandlerBus->dispatch($message); + } +} + +class SingleHandlerBus +{ + use SingleHandlingTrait; + + public function __construct(?MessageBusInterface $messageBus) + { + if ($messageBus) { + $this->messageBus = $messageBus; + } + } + + public function dispatch($message): string + { + return $this->handle($message); + } +} diff --git a/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php b/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php index 12ccf943079bd..8a68d266efe2f 100644 --- a/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php +++ b/src/Symfony/Component/Messenger/Tests/TraceableMessageBusTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Stamp\DelayStamp; @@ -19,12 +20,20 @@ use Symfony\Component\Messenger\Tests\Fixtures\AnEnvelopeStamp; use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Tests\Fixtures\TestTracesWithHandleTraitAction; +use Symfony\Component\Messenger\Tests\Fixtures\TestTracesWithSingleHandlingTraitAction; use Symfony\Component\Messenger\TraceableMessageBus; class TraceableMessageBusTest extends TestCase { + use ExpectDeprecationTrait; + + /** + * @group legacy + */ public function testItTracesDispatch() { + $this->expectDeprecation('Since symfony/messenger 7.1: The "Symfony\Component\Messenger\HandleTrait" class is deprecated, use "Symfony\Component\Messenger\SingleHandlingTrait" instead.'); + $message = new DummyMessage('Hello'); $stamp = new DelayStamp(5); @@ -49,6 +58,9 @@ public function testItTracesDispatch() ], $actualTracedMessage); } + /** + * @group legacy + */ public function testItTracesDispatchWhenHandleTraitIsUsed() { $message = new DummyMessage('Hello'); @@ -73,6 +85,30 @@ public function testItTracesDispatchWhenHandleTraitIsUsed() ], $actualTracedMessage); } + public function testItTracesDispatchWhenSingleHandlingTraitIsUsed() + { + $message = new DummyMessage('Hello'); + + $bus = $this->createMock(MessageBusInterface::class); + $bus->expects($this->once())->method('dispatch')->with($message)->willReturn((new Envelope($message))->with($stamp = new HandledStamp('result', 'handlerName'))); + + $traceableBus = new TraceableMessageBus($bus); + (new TestTracesWithSingleHandlingTraitAction($traceableBus))($message); + $this->assertCount(1, $tracedMessages = $traceableBus->getDispatchedMessages()); + $actualTracedMessage = $tracedMessages[0]; + unset($actualTracedMessage['callTime']); // don't check, too variable + $this->assertEquals([ + 'message' => $message, + 'stamps' => [], + 'stamps_after_dispatch' => [$stamp], + 'caller' => [ + 'name' => 'TestTracesWithSingleHandlingTraitAction.php', + 'file' => (new \ReflectionClass(TestTracesWithSingleHandlingTraitAction::class))->getFileName(), + 'line' => (new \ReflectionMethod(TestTracesWithSingleHandlingTraitAction::class, '__invoke'))->getStartLine() + 2, + ], + ], $actualTracedMessage); + } + public function testItTracesDispatchWithEnvelope() { $message = new DummyMessage('Hello'); diff --git a/src/Symfony/Component/Messenger/TraceableMessageBus.php b/src/Symfony/Component/Messenger/TraceableMessageBus.php index de8dd7aa261c4..a9d7340c77045 100644 --- a/src/Symfony/Component/Messenger/TraceableMessageBus.php +++ b/src/Symfony/Component/Messenger/TraceableMessageBus.php @@ -63,9 +63,10 @@ private function getCaller(): array $line = $trace[1]['line'] ?? null; $handleTraitFile = (new \ReflectionClass(HandleTrait::class))->getFileName(); + $singleHandlingTraitFile = (new \ReflectionClass(SingleHandlingTrait::class))->getFileName(); $found = false; for ($i = 1; $i < 8; ++$i) { - if (isset($trace[$i]['file'], $trace[$i + 1]['file'], $trace[$i + 1]['line']) && $trace[$i]['file'] === $handleTraitFile) { + if (isset($trace[$i]['file'], $trace[$i + 1]['file'], $trace[$i + 1]['line']) && ($trace[$i]['file'] === $singleHandlingTraitFile || $trace[$i]['file'] === $handleTraitFile)) { $file = $trace[$i + 1]['file']; $line = $trace[$i + 1]['line']; $found = true; diff --git a/src/Symfony/Component/Messenger/composer.json b/src/Symfony/Component/Messenger/composer.json index c51fdbfb58161..3fdfe4a55ee26 100644 --- a/src/Symfony/Component/Messenger/composer.json +++ b/src/Symfony/Component/Messenger/composer.json @@ -18,7 +18,8 @@ "require": { "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/clock": "^6.4|^7.0" + "symfony/clock": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0",