Skip to content

Commit 78fbd0a

Browse files
monteirofabpot
authored andcommitted
[Messenger] Add FlattenException Normalizer
1 parent 1f77f32 commit 78fbd0a

File tree

6 files changed

+252
-4
lines changed

6 files changed

+252
-4
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Symfony\Component\Messenger\RoutableMessageBus;
3333
use Symfony\Component\Messenger\Transport\InMemoryTransportFactory;
3434
use Symfony\Component\Messenger\Transport\Sender\SendersLocator;
35+
use Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer;
3536
use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
3637
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
3738
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
@@ -64,6 +65,9 @@
6465
abstract_arg('context'),
6566
])
6667

68+
->set('serializer.normalizer.flatten_exception', FlattenExceptionNormalizer::class)
69+
->tag('serializer.normalizer', ['priority' => -880])
70+
6771
->set('messenger.transport.native_php_serializer', PhpSerializer::class)
6872

6973
// Middleware

src/Symfony/Component/Messenger/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.2.0
5+
-----
6+
7+
* 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.
8+
49
5.1.0
510
-----
611

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Messenger\Tests\Transport\Serialization\Normalizer;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\ErrorHandler\Exception\FlattenException;
16+
use Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer;
17+
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
18+
19+
/**
20+
* @author Pascal Luna <skalpa@zetareticuli.org>
21+
*/
22+
class FlattenExceptionNormalizerTest extends TestCase
23+
{
24+
/**
25+
* @var FlattenExceptionNormalizer
26+
*/
27+
private $normalizer;
28+
29+
protected function setUp(): void
30+
{
31+
$this->normalizer = new FlattenExceptionNormalizer();
32+
}
33+
34+
public function testSupportsNormalization()
35+
{
36+
$this->assertTrue($this->normalizer->supportsNormalization(new FlattenException(), null, $this->getMessengerContext()));
37+
$this->assertFalse($this->normalizer->supportsNormalization(new FlattenException()));
38+
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
39+
}
40+
41+
/**
42+
* @dataProvider provideFlattenException
43+
*/
44+
public function testNormalize(FlattenException $exception)
45+
{
46+
$normalized = $this->normalizer->normalize($exception, null, $this->getMessengerContext());
47+
$previous = null === $exception->getPrevious() ? null : $this->normalizer->normalize($exception->getPrevious());
48+
49+
$this->assertSame($exception->getMessage(), $normalized['message']);
50+
$this->assertSame($exception->getCode(), $normalized['code']);
51+
if (null !== $exception->getStatusCode()) {
52+
$this->assertSame($exception->getStatusCode(), $normalized['status']);
53+
} else {
54+
$this->assertArrayNotHasKey('status', $normalized);
55+
}
56+
$this->assertSame($exception->getHeaders(), $normalized['headers']);
57+
$this->assertSame($exception->getClass(), $normalized['class']);
58+
$this->assertSame($exception->getFile(), $normalized['file']);
59+
$this->assertSame($exception->getLine(), $normalized['line']);
60+
$this->assertSame($previous, $normalized['previous']);
61+
$this->assertSame($exception->getTrace(), $normalized['trace']);
62+
$this->assertSame($exception->getTraceAsString(), $normalized['trace_as_string']);
63+
}
64+
65+
public function provideFlattenException(): array
66+
{
67+
return [
68+
'instance from exception' => [FlattenException::createFromThrowable(new \RuntimeException('foo', 42))],
69+
'instance with previous exception' => [FlattenException::createFromThrowable(new \RuntimeException('foo', 42, new \Exception()))],
70+
'instance with headers' => [FlattenException::createFromThrowable(new \RuntimeException('foo', 42), 404, ['Foo' => 'Bar'])],
71+
];
72+
}
73+
74+
public function testSupportsDenormalization()
75+
{
76+
$this->assertFalse($this->normalizer->supportsDenormalization(null, FlattenException::class));
77+
$this->assertTrue($this->normalizer->supportsDenormalization(null, FlattenException::class, null, $this->getMessengerContext()));
78+
$this->assertFalse($this->normalizer->supportsDenormalization(null, \stdClass::class));
79+
}
80+
81+
public function testDenormalizeValidData()
82+
{
83+
$normalized = [
84+
'message' => 'Something went foobar.',
85+
'code' => 42,
86+
'status' => 404,
87+
'headers' => ['Content-Type' => 'application/json'],
88+
'class' => static::class,
89+
'file' => 'foo.php',
90+
'line' => 123,
91+
'previous' => [
92+
'message' => 'Previous exception',
93+
'code' => 0,
94+
'class' => FlattenException::class,
95+
'file' => 'foo.php',
96+
'line' => 123,
97+
'headers' => ['Content-Type' => 'application/json'],
98+
'trace' => [
99+
[
100+
'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => 'foo.php', 'line' => 123, 'args' => [],
101+
],
102+
],
103+
'trace_as_string' => '#0 foo.php(123): foo()'.PHP_EOL.'#1 bar.php(456): bar()',
104+
],
105+
'trace' => [
106+
[
107+
'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => 'foo.php', 'line' => 123, 'args' => [],
108+
],
109+
],
110+
'trace_as_string' => '#0 foo.php(123): foo()'.PHP_EOL.'#1 bar.php(456): bar()',
111+
];
112+
$exception = $this->normalizer->denormalize($normalized, FlattenException::class);
113+
114+
$this->assertInstanceOf(FlattenException::class, $exception);
115+
$this->assertSame($normalized['message'], $exception->getMessage());
116+
$this->assertSame($normalized['code'], $exception->getCode());
117+
$this->assertSame($normalized['status'], $exception->getStatusCode());
118+
$this->assertSame($normalized['headers'], $exception->getHeaders());
119+
$this->assertSame($normalized['class'], $exception->getClass());
120+
$this->assertSame($normalized['file'], $exception->getFile());
121+
$this->assertSame($normalized['line'], $exception->getLine());
122+
$this->assertSame($normalized['trace'], $exception->getTrace());
123+
$this->assertSame($normalized['trace_as_string'], $exception->getTraceAsString());
124+
125+
$this->assertInstanceOf(FlattenException::class, $previous = $exception->getPrevious());
126+
$this->assertSame($normalized['previous']['message'], $previous->getMessage());
127+
$this->assertSame($normalized['previous']['code'], $previous->getCode());
128+
}
129+
130+
private function getMessengerContext(): array
131+
{
132+
return [
133+
Serializer::MESSENGER_SERIALIZATION_CONTEXT => true,
134+
];
135+
}
136+
}

src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ public function testUsesTheCustomFormatAndContext()
6464
$message = new DummyMessage('Foo');
6565

6666
$serializer = $this->getMockBuilder(SerializerComponent\SerializerInterface::class)->getMock();
67-
$serializer->expects($this->once())->method('serialize')->with($message, 'csv', ['foo' => 'bar'])->willReturn('Yay');
68-
$serializer->expects($this->once())->method('deserialize')->with('Yay', DummyMessage::class, 'csv', ['foo' => 'bar'])->willReturn($message);
67+
$serializer->expects($this->once())->method('serialize')->with($message, 'csv', ['foo' => 'bar', Serializer::MESSENGER_SERIALIZATION_CONTEXT => true])->willReturn('Yay');
68+
$serializer->expects($this->once())->method('deserialize')->with('Yay', DummyMessage::class, 'csv', ['foo' => 'bar', Serializer::MESSENGER_SERIALIZATION_CONTEXT => true])->willReturn($message);
6969

7070
$encoder = new Serializer($serializer, 'csv', ['foo' => 'bar']);
7171

@@ -94,6 +94,7 @@ public function testEncodedWithSymfonySerializerForStamps()
9494
[$this->anything()],
9595
[$message, 'json', [
9696
ObjectNormalizer::GROUPS => ['foo'],
97+
Serializer::MESSENGER_SERIALIZATION_CONTEXT => true,
9798
]]
9899
)
99100
;
@@ -117,9 +118,10 @@ public function testDecodeWithSymfonySerializerStamp()
117118
->expects($this->exactly(2))
118119
->method('deserialize')
119120
->withConsecutive(
120-
['[{"context":{"groups":["foo"]}}]', SerializerStamp::class.'[]', 'json', []],
121+
['[{"context":{"groups":["foo"]}}]', SerializerStamp::class.'[]', 'json', [Serializer::MESSENGER_SERIALIZATION_CONTEXT => true]],
121122
['{}', DummyMessage::class, 'json', [
122123
ObjectNormalizer::GROUPS => ['foo'],
124+
Serializer::MESSENGER_SERIALIZATION_CONTEXT => true,
123125
]]
124126
)
125127
->willReturnOnConsecutiveCalls(
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Messenger\Transport\Serialization\Normalizer;
13+
14+
use Symfony\Component\ErrorHandler\Exception\FlattenException;
15+
use Symfony\Component\Messenger\Transport\Serialization\Serializer;
16+
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
17+
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
18+
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
19+
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
20+
21+
/**
22+
* This normalizer is only used in Debug/Dev/Messenger contexts.
23+
*
24+
* @author Pascal Luna <skalpa@zetareticuli.org>
25+
*/
26+
final class FlattenExceptionNormalizer implements DenormalizerInterface, ContextAwareNormalizerInterface
27+
{
28+
use NormalizerAwareTrait;
29+
30+
/**
31+
* {@inheritdoc}
32+
*
33+
* @throws InvalidArgumentException
34+
*/
35+
public function normalize($object, $format = null, array $context = [])
36+
{
37+
$normalized = [
38+
'message' => $object->getMessage(),
39+
'code' => $object->getCode(),
40+
'headers' => $object->getHeaders(),
41+
'class' => $object->getClass(),
42+
'file' => $object->getFile(),
43+
'line' => $object->getLine(),
44+
'previous' => null === $object->getPrevious() ? null : $this->normalize($object->getPrevious(), $format, $context),
45+
'trace' => $object->getTrace(),
46+
'trace_as_string' => $object->getTraceAsString(),
47+
];
48+
if (null !== $status = $object->getStatusCode()) {
49+
$normalized['status'] = $status;
50+
}
51+
52+
return $normalized;
53+
}
54+
55+
/**
56+
* {@inheritdoc}
57+
*/
58+
public function supportsNormalization($data, $format = null, array $context = [])
59+
{
60+
return $data instanceof FlattenException && ($context[Serializer::MESSENGER_SERIALIZATION_CONTEXT] ?? false);
61+
}
62+
63+
/**
64+
* {@inheritdoc}
65+
*/
66+
public function denormalize($data, $type, $format = null, array $context = [])
67+
{
68+
$object = new FlattenException();
69+
70+
$object->setMessage($data['message']);
71+
$object->setCode($data['code']);
72+
$object->setStatusCode($data['status'] ?? null);
73+
$object->setClass($data['class']);
74+
$object->setFile($data['file']);
75+
$object->setLine($data['line']);
76+
$object->setHeaders((array) $data['headers']);
77+
78+
if (isset($data['previous'])) {
79+
$object->setPrevious($this->denormalize($data['previous'], $type, $format, $context));
80+
}
81+
82+
$property = new \ReflectionProperty(FlattenException::class, 'trace');
83+
$property->setAccessible(true);
84+
$property->setValue($object, (array) $data['trace']);
85+
86+
$property = new \ReflectionProperty(FlattenException::class, 'traceAsString');
87+
$property->setAccessible(true);
88+
$property->setValue($object, $data['trace_as_string']);
89+
90+
return $object;
91+
}
92+
93+
/**
94+
* {@inheritdoc}
95+
*/
96+
public function supportsDenormalization($data, $type, $format = null, array $context = [])
97+
{
98+
return FlattenException::class === $type && ($context[Serializer::MESSENGER_SERIALIZATION_CONTEXT] ?? false);
99+
}
100+
}

src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
*/
3131
class Serializer implements SerializerInterface
3232
{
33+
public const MESSENGER_SERIALIZATION_CONTEXT = 'messenger_serialization';
3334
private const STAMP_HEADER_PREFIX = 'X-Message-Stamp-';
3435

3536
private $serializer;
@@ -40,7 +41,7 @@ public function __construct(SymfonySerializerInterface $serializer = null, strin
4041
{
4142
$this->serializer = $serializer ?? self::create()->serializer;
4243
$this->format = $format;
43-
$this->context = $context;
44+
$this->context = $context + [self::MESSENGER_SERIALIZATION_CONTEXT => true];
4445
}
4546

4647
public static function create(): self

0 commit comments

Comments
 (0)