diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 23da8b07bcb04..d6379b71666b7 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 0000000000000..cd80e32a2c2b9 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/FlattenExceptionNormalizer.php @@ -0,0 +1,121 @@ + + * + * 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 + * + * @experimental + */ +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 */ + + $normalized = [ + 'detail' => $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) + { + 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['detail'] ?? null); + $object->setCode($data['code'] ?? null); + $object->setStatusCode($data['status'] ?? 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 0000000000000..a59117504b736 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/FlattenExceptionNormalizerTest.php @@ -0,0 +1,159 @@ + + * + * 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['detail']); + $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 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 = [ + 'detail' => 'Something went foobar.', + 'code' => 42, + 'status' => 404, + 'headers' => ['Content-Type' => 'application/json'], + 'class' => \get_class($this), + 'file' => 'foo.php', + 'line' => 123, + 'previous' => [ + 'detail' => '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['detail'], $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']['detail'], $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 087289fd7ffc1..28c2eac9126db 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",