diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php index 05939162d8..3d99912972 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php @@ -32,6 +32,7 @@ use Symfony\Component\Messenger\Retry\MultiplierRetryStrategy; use Symfony\Component\Messenger\RoutableMessageBus; use Symfony\Component\Messenger\Transport\InMemoryTransportFactory; use Symfony\Component\Messenger\Transport\Sender\SendersLocator; +use Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer; use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer; use Symfony\Component\Messenger\Transport\Serialization\Serializer; use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; @@ -64,6 +65,9 @@ return static function (ContainerConfigurator $container) { abstract_arg('context'), ]) + ->set('serializer.normalizer.flatten_exception', FlattenExceptionNormalizer::class) + ->tag('serializer.normalizer', ['priority' => -880]) + ->set('messenger.transport.native_php_serializer', PhpSerializer::class) // Middleware diff --git a/src/Symfony/Component/Messenger/CHANGELOG.md b/src/Symfony/Component/Messenger/CHANGELOG.md index d2f6b00000..bdc428446a 100644 --- a/src/Symfony/Component/Messenger/CHANGELOG.md +++ b/src/Symfony/Component/Messenger/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +5.2.0 +----- + +* Added `FlattenExceptionNormalizer` to give more information about the exception on Messenger background processes. The `FlattenExceptionNormalizer` has a higher priority than `ProblemNormalizer` and it is only used when the Messenger serialization context is set. + 5.1.0 ----- diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Serialization/Normalizer/FlattenExceptionNormalizerTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Serialization/Normalizer/FlattenExceptionNormalizerTest.php new file mode 100644 index 0000000000..516905a960 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Transport/Serialization/Normalizer/FlattenExceptionNormalizerTest.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Messenger\Tests\Transport\Serialization\Normalizer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer; +use Symfony\Component\Messenger\Transport\Serialization\Serializer; + +/** + * @author Pascal Luna + */ +class FlattenExceptionNormalizerTest extends TestCase +{ + /** + * @var FlattenExceptionNormalizer + */ + private $normalizer; + + protected function setUp(): void + { + $this->normalizer = new FlattenExceptionNormalizer(); + } + + public function testSupportsNormalization() + { + $this->assertTrue($this->normalizer->supportsNormalization(new FlattenException(), null, $this->getMessengerContext())); + $this->assertFalse($this->normalizer->supportsNormalization(new FlattenException())); + $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + } + + /** + * @dataProvider provideFlattenException + */ + public function testNormalize(FlattenException $exception) + { + $normalized = $this->normalizer->normalize($exception, null, $this->getMessengerContext()); + $previous = null === $exception->getPrevious() ? null : $this->normalizer->normalize($exception->getPrevious()); + + $this->assertSame($exception->getMessage(), $normalized['message']); + $this->assertSame($exception->getCode(), $normalized['code']); + if (null !== $exception->getStatusCode()) { + $this->assertSame($exception->getStatusCode(), $normalized['status']); + } else { + $this->assertArrayNotHasKey('status', $normalized); + } + $this->assertSame($exception->getHeaders(), $normalized['headers']); + $this->assertSame($exception->getClass(), $normalized['class']); + $this->assertSame($exception->getFile(), $normalized['file']); + $this->assertSame($exception->getLine(), $normalized['line']); + $this->assertSame($previous, $normalized['previous']); + $this->assertSame($exception->getTrace(), $normalized['trace']); + $this->assertSame($exception->getTraceAsString(), $normalized['trace_as_string']); + } + + public function provideFlattenException(): array + { + return [ + 'instance from exception' => [FlattenException::createFromThrowable(new \RuntimeException('foo', 42))], + 'instance with previous exception' => [FlattenException::createFromThrowable(new \RuntimeException('foo', 42, new \Exception()))], + 'instance with headers' => [FlattenException::createFromThrowable(new \RuntimeException('foo', 42), 404, ['Foo' => 'Bar'])], + ]; + } + + public function testSupportsDenormalization() + { + $this->assertFalse($this->normalizer->supportsDenormalization(null, FlattenException::class)); + $this->assertTrue($this->normalizer->supportsDenormalization(null, FlattenException::class, null, $this->getMessengerContext())); + $this->assertFalse($this->normalizer->supportsDenormalization(null, \stdClass::class)); + } + + public function testDenormalizeValidData() + { + $normalized = [ + 'message' => 'Something went foobar.', + 'code' => 42, + 'status' => 404, + 'headers' => ['Content-Type' => 'application/json'], + 'class' => static::class, + 'file' => 'foo.php', + 'line' => 123, + 'previous' => [ + 'message' => 'Previous exception', + 'code' => 0, + 'class' => FlattenException::class, + 'file' => 'foo.php', + 'line' => 123, + 'headers' => ['Content-Type' => 'application/json'], + 'trace' => [ + [ + 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => 'foo.php', 'line' => 123, 'args' => [], + ], + ], + 'trace_as_string' => '#0 foo.php(123): foo()'.PHP_EOL.'#1 bar.php(456): bar()', + ], + 'trace' => [ + [ + 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => 'foo.php', 'line' => 123, 'args' => [], + ], + ], + 'trace_as_string' => '#0 foo.php(123): foo()'.PHP_EOL.'#1 bar.php(456): bar()', + ]; + $exception = $this->normalizer->denormalize($normalized, FlattenException::class); + + $this->assertInstanceOf(FlattenException::class, $exception); + $this->assertSame($normalized['message'], $exception->getMessage()); + $this->assertSame($normalized['code'], $exception->getCode()); + $this->assertSame($normalized['status'], $exception->getStatusCode()); + $this->assertSame($normalized['headers'], $exception->getHeaders()); + $this->assertSame($normalized['class'], $exception->getClass()); + $this->assertSame($normalized['file'], $exception->getFile()); + $this->assertSame($normalized['line'], $exception->getLine()); + $this->assertSame($normalized['trace'], $exception->getTrace()); + $this->assertSame($normalized['trace_as_string'], $exception->getTraceAsString()); + + $this->assertInstanceOf(FlattenException::class, $previous = $exception->getPrevious()); + $this->assertSame($normalized['previous']['message'], $previous->getMessage()); + $this->assertSame($normalized['previous']['code'], $previous->getCode()); + } + + private function getMessengerContext(): array + { + return [ + Serializer::MESSENGER_SERIALIZATION_CONTEXT => true, + ]; + } +} diff --git a/src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php b/src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php index adff357dc9..b6714d3d40 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php @@ -64,8 +64,8 @@ class SerializerTest extends TestCase $message = new DummyMessage('Foo'); $serializer = $this->getMockBuilder(SerializerComponent\SerializerInterface::class)->getMock(); - $serializer->expects($this->once())->method('serialize')->with($message, 'csv', ['foo' => 'bar'])->willReturn('Yay'); - $serializer->expects($this->once())->method('deserialize')->with('Yay', DummyMessage::class, 'csv', ['foo' => 'bar'])->willReturn($message); + $serializer->expects($this->once())->method('serialize')->with($message, 'csv', ['foo' => 'bar', Serializer::MESSENGER_SERIALIZATION_CONTEXT => true])->willReturn('Yay'); + $serializer->expects($this->once())->method('deserialize')->with('Yay', DummyMessage::class, 'csv', ['foo' => 'bar', Serializer::MESSENGER_SERIALIZATION_CONTEXT => true])->willReturn($message); $encoder = new Serializer($serializer, 'csv', ['foo' => 'bar']); @@ -94,6 +94,7 @@ class SerializerTest extends TestCase [$this->anything()], [$message, 'json', [ ObjectNormalizer::GROUPS => ['foo'], + Serializer::MESSENGER_SERIALIZATION_CONTEXT => true, ]] ) ; @@ -117,9 +118,10 @@ class SerializerTest extends TestCase ->expects($this->exactly(2)) ->method('deserialize') ->withConsecutive( - ['[{"context":{"groups":["foo"]}}]', SerializerStamp::class.'[]', 'json', []], + ['[{"context":{"groups":["foo"]}}]', SerializerStamp::class.'[]', 'json', [Serializer::MESSENGER_SERIALIZATION_CONTEXT => true]], ['{}', DummyMessage::class, 'json', [ ObjectNormalizer::GROUPS => ['foo'], + Serializer::MESSENGER_SERIALIZATION_CONTEXT => true, ]] ) ->willReturnOnConsecutiveCalls( diff --git a/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php b/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php new file mode 100644 index 0000000000..6ee46c05a6 --- /dev/null +++ b/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.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 Symfony\Component\Messenger\Transport\Serialization\Normalizer; + +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\Messenger\Transport\Serialization\Serializer; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; + +/** + * This normalizer is only used in Debug/Dev/Messenger contexts. + * + * @author Pascal Luna + */ +final class FlattenExceptionNormalizer implements DenormalizerInterface, ContextAwareNormalizerInterface +{ + use NormalizerAwareTrait; + + /** + * {@inheritdoc} + * + * @throws InvalidArgumentException + */ + public function normalize($object, $format = null, array $context = []) + { + $normalized = [ + 'message' => $object->getMessage(), + 'code' => $object->getCode(), + 'headers' => $object->getHeaders(), + 'class' => $object->getClass(), + 'file' => $object->getFile(), + 'line' => $object->getLine(), + 'previous' => null === $object->getPrevious() ? null : $this->normalize($object->getPrevious(), $format, $context), + 'trace' => $object->getTrace(), + 'trace_as_string' => $object->getTraceAsString(), + ]; + if (null !== $status = $object->getStatusCode()) { + $normalized['status'] = $status; + } + + return $normalized; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null, array $context = []) + { + return $data instanceof FlattenException && ($context[Serializer::MESSENGER_SERIALIZATION_CONTEXT] ?? false); + } + + /** + * {@inheritdoc} + */ + public function denormalize($data, $type, $format = null, array $context = []) + { + $object = new FlattenException(); + + $object->setMessage($data['message']); + $object->setCode($data['code']); + $object->setStatusCode($data['status'] ?? null); + $object->setClass($data['class']); + $object->setFile($data['file']); + $object->setLine($data['line']); + $object->setHeaders((array) $data['headers']); + + if (isset($data['previous'])) { + $object->setPrevious($this->denormalize($data['previous'], $type, $format, $context)); + } + + $property = new \ReflectionProperty(FlattenException::class, 'trace'); + $property->setAccessible(true); + $property->setValue($object, (array) $data['trace']); + + $property = new \ReflectionProperty(FlattenException::class, 'traceAsString'); + $property->setAccessible(true); + $property->setValue($object, $data['trace_as_string']); + + return $object; + } + + /** + * {@inheritdoc} + */ + public function supportsDenormalization($data, $type, $format = null, array $context = []) + { + return FlattenException::class === $type && ($context[Serializer::MESSENGER_SERIALIZATION_CONTEXT] ?? false); + } +} diff --git a/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php b/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php index 22d48f7e23..378cdd4821 100644 --- a/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php +++ b/src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php @@ -30,6 +30,7 @@ use Symfony\Component\Serializer\SerializerInterface as SymfonySerializerInterfa */ class Serializer implements SerializerInterface { + public const MESSENGER_SERIALIZATION_CONTEXT = 'messenger_serialization'; private const STAMP_HEADER_PREFIX = 'X-Message-Stamp-'; private $serializer; @@ -40,7 +41,7 @@ class Serializer implements SerializerInterface { $this->serializer = $serializer ?? self::create()->serializer; $this->format = $format; - $this->context = $context; + $this->context = $context + [self::MESSENGER_SERIALIZATION_CONTEXT => true]; } public static function create(): self