From 733e404e2b40c1ac95ad6222da58eef21d87cfce Mon Sep 17 00:00:00 2001 From: Pascal Luna Date: Fri, 20 Sep 2019 13:46:23 +0100 Subject: [PATCH 1/3] Added FlattenExceptionNormalizer --- .../Resources/config/serializer.xml | 5 + .../Normalizer/FlattenExceptionNormalizer.php | 115 +++++++++++++ .../FlattenExceptionNormalizerTest.php | 155 ++++++++++++++++++ .../Component/Serializer/composer.json | 1 + 4 files changed, 276 insertions(+) create mode 100644 src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php create mode 100644 src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 23da8b07bcb0..d6379b71666b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -31,6 +31,11 @@ + + + + + diff --git a/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php new file mode 100644 index 000000000000..61d6e09a07eb --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Normalizer; + +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; + +/** + * Normalizes FlattenException instances. + * + * @author Pascal Luna + */ +class FlattenExceptionNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface +{ + /** + * {@inheritdoc} + * + * @throws InvalidArgumentException + */ + public function normalize($object, $format = null, array $context = []) + { + if (!$object instanceof FlattenException) { + throw new InvalidArgumentException(sprintf('The object must be an instance of %s.', FlattenException::class)); + } + /* @var FlattenException $object */ + + return [ + 'message' => $object->getMessage(), + 'code' => $object->getCode(), + 'status_code' => $object->getStatusCode(), + '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(), + ]; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null) + { + return $data instanceof FlattenException; + } + + /** + * {@inheritdoc} + */ + public function denormalize($data, $type, $format = null, array $context = []) + { + if (!\is_array($data)) { + throw new NotNormalizableValueException(sprintf( + 'The normalized data must be an array, got %s.', + \is_object($data) ? \get_class($data) : \gettype($data) + )); + } + + $object = new FlattenException(); + + $object->setMessage($data['message'] ?? null); + $object->setCode($data['code'] ?? null); + $object->setStatusCode($data['status_code'] ?? null); + $object->setClass($data['class'] ?? null); + $object->setFile($data['file'] ?? null); + $object->setLine($data['line'] ?? null); + + if (isset($data['headers'])) { + $object->setHeaders((array) $data['headers']); + } + if (isset($data['previous'])) { + $object->setPrevious($this->denormalize($data['previous'], $type, $format, $context)); + } + if (isset($data['trace'])) { + $property = new \ReflectionProperty(FlattenException::class, 'trace'); + $property->setAccessible(true); + $property->setValue($object, (array) $data['trace']); + } + if (isset($data['trace_as_string'])) { + $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; + } + + /** + * {@inheritdoc} + */ + public function hasCacheableSupportsMethod(): bool + { + return __CLASS__ === \get_class($this); + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php new file mode 100644 index 000000000000..93c8a7399b29 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Normalizer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Normalizer\FlattenExceptionNormalizer; + +/** + * @author Pascal Luna + */ +class FlattenExceptionNormalizerTest extends TestCase +{ + /** + * @var FlattenExceptionNormalizer + */ + private $normalizer; + + protected function setUp(): void + { + $this->normalizer = new FlattenExceptionNormalizer(); + } + + public function testSupportsNormalization(): void + { + $this->assertTrue($this->normalizer->supportsNormalization(new FlattenException())); + $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + } + + /** + * @dataProvider provideFlattenException + */ + public function testNormalize(FlattenException $exception): void + { + $normalized = $this->normalizer->normalize($exception); + $previous = null === $exception->getPrevious() ? null : $this->normalizer->normalize($exception->getPrevious()); + + $this->assertSame($exception->getMessage(), $normalized['message']); + $this->assertSame($exception->getCode(), $normalized['code']); + $this->assertSame($exception->getStatusCode(), $normalized['status_code']); + $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 constructor' => [new FlattenException()], + '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 testNormalizeBadObjectTypeThrowsException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->normalizer->normalize(new \stdClass()); + } + + public function testSupportsDenormalization(): void + { + $this->assertTrue($this->normalizer->supportsDenormalization(null, FlattenException::class)); + $this->assertFalse($this->normalizer->supportsDenormalization(null, \stdClass::class)); + } + + public function testDenormalizeValidData(): void + { + $normalized = []; + $exception = $this->normalizer->denormalize($normalized, FlattenException::class); + + $this->assertInstanceOf(FlattenException::class, $exception); + $this->assertNull($exception->getMessage()); + $this->assertNull($exception->getCode()); + $this->assertNull($exception->getStatusCode()); + $this->assertNull($exception->getHeaders()); + $this->assertNull($exception->getClass()); + $this->assertNull($exception->getFile()); + $this->assertNull($exception->getLine()); + $this->assertNull($exception->getPrevious()); + $this->assertNull($exception->getTrace()); + $this->assertNull($exception->getTraceAsString()); + + $normalized = [ + 'message' => 'Something went foobar.', + 'code' => 42, + 'status_code' => 404, + 'headers' => ['Content-Type' => 'application/json'], + 'class' => \get_class($this), + 'file' => 'foo.php', + 'line' => 123, + 'previous' => [ + 'message' => 'Previous exception', + 'code' => 0, + ], + '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_code'], $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()); + } + + /** + * @dataProvider provideInvalidNormalizedData + */ + public function testDenormalizeInvalidDataThrowsException($normalized): void + { + $this->expectException(NotNormalizableValueException::class); + $this->normalizer->denormalize($normalized, FlattenException::class); + } + + public function provideInvalidNormalizedData(): array + { + return [ + 'null' => [null], + 'string' => ['foo'], + 'integer' => [42], + 'object' => [new \stdClass()], + ]; + } +} diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 087289fd7ffc..28c2eac9126d 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -25,6 +25,7 @@ "symfony/property-access": "~3.4|~4.0", "symfony/http-foundation": "~3.4|~4.0", "symfony/cache": "~3.4|~4.0", + "symfony/debug": "~3.4|~4.0", "symfony/property-info": "^3.4.13|~4.0", "symfony/validator": "~3.4|~4.0", "doctrine/annotations": "~1.0", From c704b361b0572e2f8f2107a0d9b9cc93d4d8faee Mon Sep 17 00:00:00 2001 From: Pascal Luna Date: Tue, 24 Sep 2019 19:17:06 +0100 Subject: [PATCH 2/3] Renamed normalized properties names according to RFC 7807 --- .../Normalizer/FlattenExceptionNormalizer.php | 14 ++++++++----- .../FlattenExceptionNormalizerTest.php | 20 +++++++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php index 61d6e09a07eb..e29cbca809ee 100644 --- a/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php @@ -34,10 +34,9 @@ public function normalize($object, $format = null, array $context = []) } /* @var FlattenException $object */ - return [ - 'message' => $object->getMessage(), + $normalized = [ + 'detail' => $object->getMessage(), 'code' => $object->getCode(), - 'status_code' => $object->getStatusCode(), 'headers' => $object->getHeaders(), 'class' => $object->getClass(), 'file' => $object->getFile(), @@ -46,6 +45,11 @@ public function normalize($object, $format = null, array $context = []) 'trace' => $object->getTrace(), 'trace_as_string' => $object->getTraceAsString(), ]; + if (null !== $status = $object->getStatusCode()) { + $normalized['status'] = $status; + } + + return $normalized; } /** @@ -70,9 +74,9 @@ public function denormalize($data, $type, $format = null, array $context = []) $object = new FlattenException(); - $object->setMessage($data['message'] ?? null); + $object->setMessage($data['detail'] ?? null); $object->setCode($data['code'] ?? null); - $object->setStatusCode($data['status_code'] ?? null); + $object->setStatusCode($data['status'] ?? null); $object->setClass($data['class'] ?? null); $object->setFile($data['file'] ?? null); $object->setLine($data['line'] ?? null); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php index 93c8a7399b29..a59117504b73 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php @@ -46,9 +46,13 @@ public function testNormalize(FlattenException $exception): void $normalized = $this->normalizer->normalize($exception); $previous = null === $exception->getPrevious() ? null : $this->normalizer->normalize($exception->getPrevious()); - $this->assertSame($exception->getMessage(), $normalized['message']); + $this->assertSame($exception->getMessage(), $normalized['detail']); $this->assertSame($exception->getCode(), $normalized['code']); - $this->assertSame($exception->getStatusCode(), $normalized['status_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']); @@ -98,15 +102,15 @@ public function testDenormalizeValidData(): void $this->assertNull($exception->getTraceAsString()); $normalized = [ - 'message' => 'Something went foobar.', + 'detail' => 'Something went foobar.', 'code' => 42, - 'status_code' => 404, + 'status' => 404, 'headers' => ['Content-Type' => 'application/json'], 'class' => \get_class($this), 'file' => 'foo.php', 'line' => 123, 'previous' => [ - 'message' => 'Previous exception', + 'detail' => 'Previous exception', 'code' => 0, ], 'trace' => [ @@ -119,9 +123,9 @@ public function testDenormalizeValidData(): void $exception = $this->normalizer->denormalize($normalized, FlattenException::class); $this->assertInstanceOf(FlattenException::class, $exception); - $this->assertSame($normalized['message'], $exception->getMessage()); + $this->assertSame($normalized['detail'], $exception->getMessage()); $this->assertSame($normalized['code'], $exception->getCode()); - $this->assertSame($normalized['status_code'], $exception->getStatusCode()); + $this->assertSame($normalized['status'], $exception->getStatusCode()); $this->assertSame($normalized['headers'], $exception->getHeaders()); $this->assertSame($normalized['class'], $exception->getClass()); $this->assertSame($normalized['file'], $exception->getFile()); @@ -130,7 +134,7 @@ public function testDenormalizeValidData(): void $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']['detail'], $previous->getMessage()); $this->assertSame($normalized['previous']['code'], $previous->getCode()); } From bfa2ff84feef9ba5184dac26dce66dc3efae9ddb Mon Sep 17 00:00:00 2001 From: Pascal Luna Date: Fri, 27 Sep 2019 10:19:22 +0100 Subject: [PATCH 3/3] Added experimental tag --- .../Serializer/Normalizer/FlattenExceptionNormalizer.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php index e29cbca809ee..cd80e32a2c2b 100644 --- a/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php @@ -19,6 +19,8 @@ * Normalizes FlattenException instances. * * @author Pascal Luna + * + * @experimental */ class FlattenExceptionNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface {