[Messenger] Add FlattenException Normalizer

This commit is contained in:
Hugo Monteiro 2020-08-21 14:44:07 +01:00 committed by Fabien Potencier
parent 1f77f32d78
commit 78fbd0ac77
6 changed files with 252 additions and 4 deletions

View File

@ -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

View File

@ -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
-----

View File

@ -0,0 +1,136 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <skalpa@zetareticuli.org>
*/
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,
];
}
}

View File

@ -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(

View File

@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <skalpa@zetareticuli.org>
*/
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);
}
}

View File

@ -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