From a0a6b785356dcd0212d64e6a9b44fba837ccc7a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 16 Jul 2016 11:30:37 +0200 Subject: [PATCH 1/3] Refactor exception support --- src/Action/ExceptionAction.php | 92 +++++++++++++++++++ .../ApiPlatformExtension.php | 29 ++++-- .../DependencyInjection/Configuration.php | 68 +++++++++----- .../Symfony/Bundle/Resources/config/api.xml | 13 +++ .../Symfony/Bundle/Resources/config/hydra.xml | 27 ++---- .../EventListener/ExceptionListener.php | 7 +- src/Hydra/Action/ExceptionAction.php | 52 ----------- src/Hydra/Serializer/ErrorNormalizer.php | 6 +- .../ApiPlatformExtensionTest.php | 5 +- .../DependencyInjection/ConfigurationTest.php | 1 + tests/Hydra/ApiDocumentationBuilderTest.php | 3 +- 11 files changed, 188 insertions(+), 115 deletions(-) create mode 100644 src/Action/ExceptionAction.php rename src/{Hydra => }/EventListener/ExceptionListener.php (74%) delete mode 100644 src/Hydra/Action/ExceptionAction.php diff --git a/src/Action/ExceptionAction.php b/src/Action/ExceptionAction.php new file mode 100644 index 00000000000..aa18c5eec7d --- /dev/null +++ b/src/Action/ExceptionAction.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Action; + +use ApiPlatform\Core\Exception\InvalidArgumentException; +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\Exception\ExceptionInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * Renders a normalized exception for a given {@see \Symfony\Component\Debug\Exception\FlattenException}. + * + * @author Baptiste Meyer + * @author Kévin Dunglas + */ +final class ExceptionAction +{ + const DEFAULT_EXCEPTION_TO_STATUS = [ + ExceptionInterface::class => Response::HTTP_BAD_REQUEST, + InvalidArgumentException::class => Response::HTTP_BAD_REQUEST, + ]; + + private $serializer; + private $exceptionFormats; + private $exceptionToStatus; + + public function __construct(SerializerInterface $serializer, array $exceptionFormats, $exceptionToStatus = []) + { + $this->serializer = $serializer; + $this->exceptionFormats = $exceptionFormats; + $this->exceptionToStatus = array_merge(self::DEFAULT_EXCEPTION_TO_STATUS, $exceptionToStatus); + } + + /** + * Converts a an exception to a JSON response. + * + * @param FlattenException $exception + * @param Request $request + * + * @return Response + */ + public function __invoke(FlattenException $exception, Request $request) : Response + { + $exceptionClass = $exception->getClass(); + foreach ($this->exceptionToStatus as $class => $status) { + if (is_a($exceptionClass, $class, true)) { + $exception->setStatusCode($status); + + break; + } + } + + $headers = $exception->getHeaders(); + + $format = $this->getErrorFormat($request); + $headers['Content-Type'] = $format['value'][0]; + + return new Response($this->serializer->serialize($exception, $format['key']), $exception->getStatusCode(), $headers); + } + + /** + * Get the error format and its associated MIME type. + * + * @param Request $request + * + * @return array + */ + private function getErrorFormat(Request $request) + { + $requestFormat = $request->getRequestFormat(null); + if (null === $requestFormat || !isset($this->exceptionFormats[$requestFormat])) { + return ['key' => $requestFormat, 'value' => $this->exceptionFormats[$requestFormat]]; + } + + reset($this->exceptionFormats); + + return each($this->exceptionFormats); + } +} diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 6b290714fac..9d6852e8b31 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -53,23 +53,19 @@ public function load(array $configs, ContainerBuilder $container) $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $formats = []; - foreach ($config['formats'] as $format => $value) { - foreach ($value['mime_types'] as $mimeType) { - $formats[$format][] = $mimeType; - } - } - $container->setAlias('api_platform.naming.resource_path_naming_strategy', $config['naming']['resource_path_naming_strategy']); if ($config['name_converter']) { $container->setAlias('api_platform.name_converter', $config['name_converter']); } + $formats = $this->getFormats($config['formats']); + $container->setParameter('api_platform.title', $config['title']); $container->setParameter('api_platform.description', $config['description']); $container->setParameter('api_platform.version', $config['version']); $container->setParameter('api_platform.formats', $formats); + $container->setParameter('api_platform.error_formats', $this->getFormats($config['error_formats'])); $container->setParameter('api_platform.collection.order', $config['collection']['order']); $container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']); $container->setParameter('api_platform.collection.pagination.enabled', $config['collection']['pagination']['enabled']); @@ -179,4 +175,23 @@ private function registerFileLoaders(ContainerBuilder $container) $container->getDefinition('api_platform.metadata.resource.name_collection_factory.xml')->replaceArgument(0, $xmlResources); $container->getDefinition('api_platform.metadata.resource.metadata_factory.xml')->replaceArgument(0, $xmlResources); } + + /** + * Normalizes the format from config to the one accepted by Symfony HttpFoundation. + * + * @param array $configFormats + * + * @return array + */ + private function getFormats(array $configFormats) : array + { + $formats = []; + foreach ($configFormats as $format => $value) { + foreach ($value['mime_types'] as $mimeType) { + $formats[$format][] = $mimeType; + } + } + + return $formats; + } } diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index 877356be1ed..ccadeed1024 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -11,6 +11,7 @@ namespace ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -34,31 +35,6 @@ public function getConfigTreeBuilder() ->scalarNode('title')->defaultValue('')->info('The title of the API.')->end() ->scalarNode('description')->defaultValue('')->info('The description of the API.')->end() ->scalarNode('version')->defaultValue('0.0.0')->info('The version of the API.')->end() - ->arrayNode('formats') - ->defaultValue(['jsonld' => ['mime_types' => ['application/ld+json']]]) - ->info('The list of enabled formats. The first one will be the default.') - ->normalizeKeys(false) - ->useAttributeAsKey('format') - ->beforeNormalization() - ->ifArray() - ->then(function ($v) { - foreach ($v as $format => $value) { - if (isset($value['mime_types'])) { - continue; - } - - $v[$format] = ['mime_types' => $value]; - } - - return $v; - }) - ->end() - ->prototype('array') - ->children() - ->arrayNode('mime_types')->prototype('scalar')->end()->end() - ->end() - ->end() - ->end() ->arrayNode('naming') ->addDefaultsIfNotSet() ->children() @@ -92,6 +68,48 @@ public function getConfigTreeBuilder() ->end() ->end(); + $this->addFormatSection($rootNode, 'formats', ['jsonld' => ['mime_types' => ['application/ld+json']]]); + $this->addFormatSection($rootNode, 'error_formats', ['jsonld' => ['mime_types' => ['application/ld+json']]]); + return $treeBuilder; } + + /** + * Adds a format section. + * + * @param ArrayNodeDefinition $rootNode + * @param string $key + * @param array $defaultValue + */ + private function addFormatSection(ArrayNodeDefinition $rootNode, string $key, array $defaultValue) + { + $rootNode + ->children() + ->arrayNode($key) + ->defaultValue($defaultValue) + ->info('The list of enabled formats. The first one will be the default.') + ->normalizeKeys(false) + ->useAttributeAsKey('format') + ->beforeNormalization() + ->ifArray() + ->then(function ($v) { + foreach ($v as $format => $value) { + if (isset($value['mime_types'])) { + continue; + } + + $v[$format] = ['mime_types' => $value]; + } + + return $v; + }) + ->end() + ->prototype('array') + ->children() + ->arrayNode('mime_types')->prototype('scalar')->end()->end() + ->end() + ->end() + ->end() + ->end(); + } } diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 4aa2ebfbc93..573ae0aee0e 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -108,6 +108,14 @@ + + api_platform.action.exception + + + + + + @@ -120,6 +128,11 @@ + + + + %api_platform.error_formats% + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml b/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml index c7137fae4f5..b8c67defaf2 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml @@ -31,14 +31,6 @@ - - api_platform.hydra.action.exception - - - - - - @@ -49,6 +41,13 @@ + + + %kernel.debug% + + + + @@ -63,13 +62,6 @@ - - - %kernel.debug% - - - - %api_platform.collection.pagination.page_parameter_name% @@ -88,11 +80,6 @@ - - - - - diff --git a/src/Hydra/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php similarity index 74% rename from src/Hydra/EventListener/ExceptionListener.php rename to src/EventListener/ExceptionListener.php index b3498183b63..c65c2b03262 100644 --- a/src/Hydra/EventListener/ExceptionListener.php +++ b/src/EventListener/ExceptionListener.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace ApiPlatform\Core\Hydra\EventListener; +namespace ApiPlatform\Core\EventListener; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\EventListener\ExceptionListener as BaseExceptionListener; @@ -24,8 +24,9 @@ final class ExceptionListener extends BaseExceptionListener { public function onKernelException(GetResponseForExceptionEvent $event) { - // Normalize exceptions with hydra errors only for resources - if (!$event->getRequest()->attributes->has('_api_resource_class')) { + $request = $event->getRequest(); + // Normalize exceptions only for routes managed by API Platform + if (!$request->attributes->has('_api_resource_class') && !$request->attributes->has('_api_respond')) { return; } diff --git a/src/Hydra/Action/ExceptionAction.php b/src/Hydra/Action/ExceptionAction.php deleted file mode 100644 index f19d642726a..00000000000 --- a/src/Hydra/Action/ExceptionAction.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace ApiPlatform\Core\Hydra\Action; - -use ApiPlatform\Core\Exception\InvalidArgumentException; -use Symfony\Component\Debug\Exception\FlattenException; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\Serializer\Exception\ExceptionInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Renders a normalized exception for a given {@see \Symfony\Component\Debug\Exception\FlattenException}. - * - * @author Baptiste Meyer - */ -final class ExceptionAction -{ - private $normalizer; - - public function __construct(NormalizerInterface $normalizer) - { - $this->normalizer = $normalizer; - } - - /** - * Converts a an exception to a JSON response. - */ - public function __invoke(FlattenException $exception) : JsonResponse - { - $exceptionClass = $exception->getClass(); - if ( - is_a($exceptionClass, ExceptionInterface::class, true) || - is_a($exceptionClass, InvalidArgumentException::class, true) - ) { - $exception->setStatusCode(JsonResponse::HTTP_BAD_REQUEST); - } - - $headers = $exception->getHeaders(); - $headers['Content-Type'] = 'application/ld+json'; - - return new JsonResponse($this->normalizer->normalize($exception, 'hydra-error'), $exception->getStatusCode(), $headers); - } -} diff --git a/src/Hydra/Serializer/ErrorNormalizer.php b/src/Hydra/Serializer/ErrorNormalizer.php index f8afa89cad0..eb87a12a3c9 100644 --- a/src/Hydra/Serializer/ErrorNormalizer.php +++ b/src/Hydra/Serializer/ErrorNormalizer.php @@ -16,15 +16,14 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface; /** - * Converts {@see \Exception} or {@see \Symfony\Component\Debug\Exception\FlattenException} - * to a Hydra error representation. + * Converts {@see \Exception} or {@see \Symfony\Component\Debug\Exception\FlattenException} to a Hydra error representation. * * @author Kévin Dunglas * @author Samuel ROZE */ final class ErrorNormalizer implements NormalizerInterface { - const FORMAT = 'hydra-error'; + const FORMAT = 'jsonld'; private $urlGenerator; private $debug; @@ -41,7 +40,6 @@ public function __construct(UrlGeneratorInterface $urlGenerator, bool $debug) public function normalize($object, $format = null, array $context = []) { $message = $object->getMessage(); - if ($this->debug) { $trace = $object->getTrace(); } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 55123417217..4dd12626b4d 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -177,6 +177,7 @@ private function getContainerBuilderProphecy() 'api_platform.description' => 'description', 'api_platform.version' => 'version', 'api_platform.formats' => ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']], + 'api_platform.error_formats' => ['jsonld' => ['application/ld+json']], 'api_platform.collection.order' => null, 'api_platform.collection.order_parameter_name' => 'order', 'api_platform.collection.pagination.enabled' => true, @@ -222,6 +223,7 @@ private function getContainerBuilderProphecy() $definitions = [ 'api_platform.action.placeholder', 'api_platform.action.entrypoint', + 'api_platform.action.exception', 'api_platform.item_data_provider', 'api_platform.collection_data_provider', 'api_platform.filters', @@ -259,6 +261,7 @@ private function getContainerBuilderProphecy() 'api_platform.listener.view.serialize', 'api_platform.listener.view.validate', 'api_platform.listener.view.respond', + 'api_platform.listener.exception', 'api_platform.serializer.normalizer.item', 'api_platform.serializer.context_builder', 'api_platform.doctrine.metadata_factory', @@ -293,11 +296,9 @@ private function getContainerBuilderProphecy() 'api_platform.hal.normalizer.item', 'api_platform.hal.normalizer.collection', 'api_platform.hydra.action.documentation', - 'api_platform.hydra.action.exception', 'api_platform.hydra.documentation_builder', 'api_platform.hydra.listener.response.add_link_header', 'api_platform.hydra.listener.exception.validation', - 'api_platform.hydra.listener.exception', 'api_platform.hydra.normalizer.resource_name_collection', 'api_platform.hydra.normalizer.collection', 'api_platform.hydra.normalizer.partial_collection_view', diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 93f08cef38d..041356646ea 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -35,6 +35,7 @@ public function testDefaultConfig() 'description' => 'description', 'version' => '1.0.0', 'formats' => ['jsonld' => ['mime_types' => ['application/ld+json']]], + 'error_formats' => ['jsonld' => ['mime_types' => ['application/ld+json']]], 'naming' => [ 'resource_path_naming_strategy' => 'api_platform.naming.resource_path_naming_strategy.underscore', ], diff --git a/tests/Hydra/ApiDocumentationBuilderTest.php b/tests/Hydra/ApiDocumentationBuilderTest.php index 345ebc143bc..4032c6cfa2f 100644 --- a/tests/Hydra/ApiDocumentationBuilderTest.php +++ b/tests/Hydra/ApiDocumentationBuilderTest.php @@ -35,7 +35,6 @@ public function testGetApiDocumention() { $title = 'Test Api'; $desc = 'test ApiGerard'; - $formats = ['jsonld' => ['application/ld+json']]; $resourceNameCollectionFactoryProphecy = $this->prophesize(ResourceNameCollectionFactoryInterface::class); $resourceNameCollectionFactoryProphecy->create()->willReturn(new ResourceNameCollection(['dummy' => 'dummy']))->shouldBeCalled(); @@ -60,8 +59,8 @@ public function testGetApiDocumention() $operationMethodResolverProphecy->getCollectionOperationMethod('dummy', 'post')->shouldBeCalled()->willReturn('POST'); $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('api_entrypoint')->willReturn('/')->shouldBeCalled(1); $urlGenerator->generate('api_hydra_doc')->willReturn('/doc')->shouldBeCalled(1); - $urlGenerator->generate('api_hydra_entrypoint')->willReturn('/')->shouldBeCalled(1); $urlGenerator->generate('api_hydra_doc', [], UrlGeneratorInterface::ABS_URL)->willReturn('/doc')->shouldBeCalled(1); From 86a57d617a8c767e497257faf74f4331b190c2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 16 Jul 2016 12:23:50 +0200 Subject: [PATCH 2/3] Refactor the validation error system --- src/Action/ExceptionAction.php | 37 +++--------- .../Symfony/Bundle/Resources/config/api.xml | 7 +++ .../Symfony/Bundle/Resources/config/hydra.xml | 16 ++---- .../ValidationExceptionListener.php | 56 +++++++++++++++++++ .../ValidationExceptionListener.php | 50 ----------------- .../ConstraintViolationListNormalizer.php | 4 +- src/Util/ErrorFormatGuesser.php | 42 ++++++++++++++ .../ApiPlatformExtensionTest.php | 2 +- tests/Hydra/Serializer/ItemNormalizerTest.php | 1 - 9 files changed, 121 insertions(+), 94 deletions(-) create mode 100644 src/Bridge/Symfony/Validator/EventListener/ValidationExceptionListener.php delete mode 100644 src/Bridge/Symfony/Validator/Hydra/EventListener/ValidationExceptionListener.php rename src/{Bridge/Symfony/Validator => }/Hydra/Serializer/ConstraintViolationListNormalizer.php (95%) create mode 100644 src/Util/ErrorFormatGuesser.php diff --git a/src/Action/ExceptionAction.php b/src/Action/ExceptionAction.php index aa18c5eec7d..b0a8a297155 100644 --- a/src/Action/ExceptionAction.php +++ b/src/Action/ExceptionAction.php @@ -12,12 +12,11 @@ namespace ApiPlatform\Core\Action; use ApiPlatform\Core\Exception\InvalidArgumentException; +use ApiPlatform\Core\Util\ErrorFormatGuesser; use Symfony\Component\Debug\Exception\FlattenException; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Serializer\Exception\ExceptionInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\SerializerInterface; /** @@ -34,25 +33,25 @@ final class ExceptionAction ]; private $serializer; - private $exceptionFormats; + private $errorFormats; private $exceptionToStatus; - public function __construct(SerializerInterface $serializer, array $exceptionFormats, $exceptionToStatus = []) + public function __construct(SerializerInterface $serializer, array $errorFormats, $exceptionToStatus = []) { $this->serializer = $serializer; - $this->exceptionFormats = $exceptionFormats; + $this->errorFormats = $errorFormats; $this->exceptionToStatus = array_merge(self::DEFAULT_EXCEPTION_TO_STATUS, $exceptionToStatus); } /** * Converts a an exception to a JSON response. * - * @param FlattenException $exception - * @param Request $request + * @param \Exception|FlattenException $exception + * @param Request $request * * @return Response */ - public function __invoke(FlattenException $exception, Request $request) : Response + public function __invoke($exception, Request $request) : Response { $exceptionClass = $exception->getClass(); foreach ($this->exceptionToStatus as $class => $status) { @@ -64,29 +63,9 @@ public function __invoke(FlattenException $exception, Request $request) : Respon } $headers = $exception->getHeaders(); - - $format = $this->getErrorFormat($request); + $format = ErrorFormatGuesser::guessErrorFormat($request, $this->errorFormats); $headers['Content-Type'] = $format['value'][0]; return new Response($this->serializer->serialize($exception, $format['key']), $exception->getStatusCode(), $headers); } - - /** - * Get the error format and its associated MIME type. - * - * @param Request $request - * - * @return array - */ - private function getErrorFormat(Request $request) - { - $requestFormat = $request->getRequestFormat(null); - if (null === $requestFormat || !isset($this->exceptionFormats[$requestFormat])) { - return ['key' => $requestFormat, 'value' => $this->exceptionFormats[$requestFormat]]; - } - - reset($this->exceptionFormats); - - return each($this->exceptionFormats); - } } diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 573ae0aee0e..c3f04186e56 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -108,6 +108,13 @@ + + + %api_platform.error_formats% + + + + api_platform.action.exception diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml b/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml index b8c67defaf2..6f0ef68300c 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/hydra.xml @@ -25,13 +25,13 @@ - - + - - + + - + + @@ -56,12 +56,6 @@ - - - - - - %api_platform.collection.pagination.page_parameter_name% diff --git a/src/Bridge/Symfony/Validator/EventListener/ValidationExceptionListener.php b/src/Bridge/Symfony/Validator/EventListener/ValidationExceptionListener.php new file mode 100644 index 00000000000..dc4cd497ccc --- /dev/null +++ b/src/Bridge/Symfony/Validator/EventListener/ValidationExceptionListener.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Bridge\Symfony\Validator\EventListener; + +use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException; +use ApiPlatform\Core\Util\ErrorFormatGuesser; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * Handles validation errors. + * + * @author Kévin Dunglas + */ +final class ValidationExceptionListener +{ + private $serializer; + private $errorFormats; + + public function __construct(SerializerInterface $serializer, array $errorFormats) + { + $this->serializer = $serializer; + $this->errorFormats = $errorFormats; + } + + /** + * Returns a list of violations normalized in the Hydra format. + * + * @param GetResponseForExceptionEvent $event + */ + public function onKernelException(GetResponseForExceptionEvent $event) + { + $exception = $event->getException(); + if (!$exception instanceof ValidationException) { + return; + } + + $format = ErrorFormatGuesser::guessErrorFormat($event->getRequest(), $this->errorFormats); + + $event->setResponse(new Response( + $this->serializer->serialize($exception->getConstraintViolationList(), $format['key']), + Response::HTTP_BAD_REQUEST, + ['Content-Type' => $format['value'][0]] + )); + } +} diff --git a/src/Bridge/Symfony/Validator/Hydra/EventListener/ValidationExceptionListener.php b/src/Bridge/Symfony/Validator/Hydra/EventListener/ValidationExceptionListener.php deleted file mode 100644 index 9a35ebabb21..00000000000 --- a/src/Bridge/Symfony/Validator/Hydra/EventListener/ValidationExceptionListener.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace ApiPlatform\Core\Bridge\Symfony\Validator\Hydra\EventListener; - -use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Handles validation errors. - * - * @author Kévin Dunglas - */ -final class ValidationExceptionListener -{ - private $normalizer; - - public function __construct(NormalizerInterface $normalizer) - { - $this->normalizer = $normalizer; - } - - /** - * Returns a list of violations normalized in the Hydra format. - * - * @param GetResponseForExceptionEvent $event - */ - public function onKernelException(GetResponseForExceptionEvent $event) - { - $exception = $event->getException(); - - if ($exception instanceof ValidationException) { - $event->setResponse(new JsonResponse( - $this->normalizer->normalize($exception->getConstraintViolationList(), 'hydra-error'), - JsonResponse::HTTP_BAD_REQUEST, - ['Content-Type' => 'application/ld+json'] - )); - } - } -} diff --git a/src/Bridge/Symfony/Validator/Hydra/Serializer/ConstraintViolationListNormalizer.php b/src/Hydra/Serializer/ConstraintViolationListNormalizer.php similarity index 95% rename from src/Bridge/Symfony/Validator/Hydra/Serializer/ConstraintViolationListNormalizer.php rename to src/Hydra/Serializer/ConstraintViolationListNormalizer.php index afef7b35793..ecd5c70481b 100644 --- a/src/Bridge/Symfony/Validator/Hydra/Serializer/ConstraintViolationListNormalizer.php +++ b/src/Hydra/Serializer/ConstraintViolationListNormalizer.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace ApiPlatform\Core\Bridge\Symfony\Validator\Hydra\Serializer; +namespace ApiPlatform\Core\Hydra\Serializer; use ApiPlatform\Core\Api\UrlGeneratorInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -22,7 +22,7 @@ */ final class ConstraintViolationListNormalizer implements NormalizerInterface { - const FORMAT = 'hydra-error'; + const FORMAT = 'jsonld'; /** * @var UrlGeneratorInterface diff --git a/src/Util/ErrorFormatGuesser.php b/src/Util/ErrorFormatGuesser.php new file mode 100644 index 00000000000..2c6839d553e --- /dev/null +++ b/src/Util/ErrorFormatGuesser.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Util; + +use Symfony\Component\HttpFoundation\Request; + +/** + * Guesses the error format to use. + * + * @author Kévin Dunglas + */ +abstract class ErrorFormatGuesser +{ + /** + * Get the error format and its associated MIME type. + * + * @param Request $request + * @param array $errorFormats + * + * @return array + */ + public static function guessErrorFormat(Request $request, array $errorFormats) : array + { + $requestFormat = $request->getRequestFormat(null); + if (null === $requestFormat || !isset($errorFormats[$requestFormat])) { + return ['key' => $requestFormat, 'value' => $errorFormats[$requestFormat]]; + } + + reset($errorFormats); + + return each($errorFormats); + } +} diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 4dd12626b4d..f6e87978ca6 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -262,6 +262,7 @@ private function getContainerBuilderProphecy() 'api_platform.listener.view.validate', 'api_platform.listener.view.respond', 'api_platform.listener.exception', + 'api_platform.listener.exception.validation', 'api_platform.serializer.normalizer.item', 'api_platform.serializer.context_builder', 'api_platform.doctrine.metadata_factory', @@ -298,7 +299,6 @@ private function getContainerBuilderProphecy() 'api_platform.hydra.action.documentation', 'api_platform.hydra.documentation_builder', 'api_platform.hydra.listener.response.add_link_header', - 'api_platform.hydra.listener.exception.validation', 'api_platform.hydra.normalizer.resource_name_collection', 'api_platform.hydra.normalizer.collection', 'api_platform.hydra.normalizer.partial_collection_view', diff --git a/tests/Hydra/Serializer/ItemNormalizerTest.php b/tests/Hydra/Serializer/ItemNormalizerTest.php index 1653e177248..9574e878ed3 100644 --- a/tests/Hydra/Serializer/ItemNormalizerTest.php +++ b/tests/Hydra/Serializer/ItemNormalizerTest.php @@ -123,7 +123,6 @@ public function testNormalize() '@type' => 'Dummy', 'name' => 'hello', - ]; $this->assertEquals($expected, $normalizer->normalize($dummy)); } From 095c4eef021b9c28b35225289cea28248d73b9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 16 Jul 2016 15:36:45 +0200 Subject: [PATCH 3/3] Add suport for the API Problem format --- features/problem.feature | 47 +++++++++++++ .../ApiPlatformExtension.php | 7 +- .../DependencyInjection/Configuration.php | 5 +- .../Bundle/Resources/config/problem.xml | 26 ++++++++ src/Hydra/Serializer/ErrorNormalizer.php | 2 +- .../ConstraintViolationListNormalizer.php | 63 ++++++++++++++++++ src/Problem/Serializer/ErrorNormalizer.php | 65 ++++++++++++++++++ src/Util/ErrorFormatGuesser.php | 2 +- .../ApiPlatformExtensionTest.php | 6 +- .../DependencyInjection/ConfigurationTest.php | 5 +- .../ConstraintViolationNormalizerTest.php | 66 +++++++++++++++++++ .../Hydra/Serializer/ErrorNormalizerTest.php | 64 ++++++++++++++++++ .../ConstraintViolationNormalizerTest.php | 59 +++++++++++++++++ .../Serializer/ErrorNormalizerTest.php | 56 ++++++++++++++++ tests/Util/ErrorFormatGuesserTest.php | 48 ++++++++++++++ 15 files changed, 514 insertions(+), 7 deletions(-) create mode 100644 features/problem.feature create mode 100644 src/Bridge/Symfony/Bundle/Resources/config/problem.xml create mode 100644 src/Problem/Serializer/ConstraintViolationListNormalizer.php create mode 100644 src/Problem/Serializer/ErrorNormalizer.php create mode 100644 tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php create mode 100644 tests/Hydra/Serializer/ErrorNormalizerTest.php create mode 100644 tests/Problem/Serializer/ConstraintViolationNormalizerTest.php create mode 100644 tests/Problem/Serializer/ErrorNormalizerTest.php create mode 100644 tests/Util/ErrorFormatGuesserTest.php diff --git a/features/problem.feature b/features/problem.feature new file mode 100644 index 00000000000..46f1dd45923 --- /dev/null +++ b/features/problem.feature @@ -0,0 +1,47 @@ +Feature: Error handling valid according to RFC 7807 (application/problem+json) + In order to be able to handle error client side + As a client software developer + I need to retrieve an RFC 7807 compliant serialization of errors + + Scenario: Get an error + When I add "Accept" header equal to "application/json" + And I send a "POST" request to "/dummies" with body: + """ + {} + """ + Then the response status code should be 400 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/problem+json" + And the JSON should be equal to: + """ + { + "type": "https://tools.ietf.org/html/rfc2616#section-10", + "title": "An error occurred", + "detail": "name: This value should not be blank.", + "violations": [ + { + "propertyPath": "name", + "message": "This value should not be blank." + } + ] + } + """ + + Scenario: Get an error during deserialization of simple relation + When I add "Accept" header equal to "application/json" + And I send a "POST" request to "/dummies" with body: + """ + { + "name": "Foo", + "relatedDummy": { + "name": "bar" + } + } + """ + Then the response status code should be 400 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/problem+json" + And the JSON node "type" should be equal to "https://tools.ietf.org/html/rfc2616#section-10" + And the JSON node "title" should be equal to "An error occurred" + And the JSON node "detail" should be equal to 'Nested objects for attribute "relatedDummy" of "ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy" are not enabled. Use serialization groups to change that behavior.' + And the JSON node "trace" should exist diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 9d6852e8b31..86ea217e5d7 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -60,12 +60,13 @@ public function load(array $configs, ContainerBuilder $container) } $formats = $this->getFormats($config['formats']); + $errorFormats = $this->getFormats($config['error_formats']); $container->setParameter('api_platform.title', $config['title']); $container->setParameter('api_platform.description', $config['description']); $container->setParameter('api_platform.version', $config['version']); $container->setParameter('api_platform.formats', $formats); - $container->setParameter('api_platform.error_formats', $this->getFormats($config['error_formats'])); + $container->setParameter('api_platform.error_formats', $errorFormats); $container->setParameter('api_platform.collection.order', $config['collection']['order']); $container->setParameter('api_platform.collection.order_parameter_name', $config['collection']['order_parameter_name']); $container->setParameter('api_platform.collection.pagination.enabled', $config['collection']['pagination']['enabled']); @@ -95,6 +96,10 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('hal.xml'); } + if (isset($errorFormats['jsonproblem'])) { + $loader->load('problem.xml'); + } + $this->registerAnnotationLoaders($container); $this->registerFileLoaders($container); diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index ccadeed1024..0cce2b43974 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -69,7 +69,10 @@ public function getConfigTreeBuilder() ->end(); $this->addFormatSection($rootNode, 'formats', ['jsonld' => ['mime_types' => ['application/ld+json']]]); - $this->addFormatSection($rootNode, 'error_formats', ['jsonld' => ['mime_types' => ['application/ld+json']]]); + $this->addFormatSection($rootNode, 'error_formats', [ + 'jsonproblem' => ['mime_types' => ['application/problem+json']], + 'jsonld' => ['mime_types' => ['application/ld+json']], + ]); return $treeBuilder; } diff --git a/src/Bridge/Symfony/Bundle/Resources/config/problem.xml b/src/Bridge/Symfony/Bundle/Resources/config/problem.xml new file mode 100644 index 00000000000..9ad6e44ce41 --- /dev/null +++ b/src/Bridge/Symfony/Bundle/Resources/config/problem.xml @@ -0,0 +1,26 @@ + + + + + + + + jsonproblem + + + + + + + + + + %kernel.debug% + + + + + + diff --git a/src/Hydra/Serializer/ErrorNormalizer.php b/src/Hydra/Serializer/ErrorNormalizer.php index eb87a12a3c9..b85f86e9816 100644 --- a/src/Hydra/Serializer/ErrorNormalizer.php +++ b/src/Hydra/Serializer/ErrorNormalizer.php @@ -28,7 +28,7 @@ final class ErrorNormalizer implements NormalizerInterface private $urlGenerator; private $debug; - public function __construct(UrlGeneratorInterface $urlGenerator, bool $debug) + public function __construct(UrlGeneratorInterface $urlGenerator, bool $debug = false) { $this->urlGenerator = $urlGenerator; $this->debug = $debug; diff --git a/src/Problem/Serializer/ConstraintViolationListNormalizer.php b/src/Problem/Serializer/ConstraintViolationListNormalizer.php new file mode 100644 index 00000000000..76056fd7ac8 --- /dev/null +++ b/src/Problem/Serializer/ConstraintViolationListNormalizer.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Problem\Serializer; + +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Validator\ConstraintViolationListInterface; + +/** + * Converts {@see \Symfony\Component\Validator\ConstraintViolationListInterface} the API Problem spec (RFC 7807). + * + * @see https://tools.ietf.org/html/rfc7807 + + * @author Kévin Dunglas + */ +final class ConstraintViolationListNormalizer implements NormalizerInterface +{ + const FORMAT = 'jsonproblem'; + + /** + * {@inheritdoc} + */ + public function normalize($object, $format = null, array $context = []) + { + $violations = []; + $messages = []; + + foreach ($object as $violation) { + $violations[] = [ + 'propertyPath' => $violation->getPropertyPath(), + 'message' => $violation->getMessage(), + ]; + + $propertyPath = $violation->getPropertyPath(); + $prefix = $propertyPath ? sprintf('%s: ', $propertyPath) : ''; + + $messages [] = $prefix.$violation->getMessage(); + } + + return [ + 'type' => $context['type'] ?? 'https://tools.ietf.org/html/rfc2616#section-10', + 'title' => $context['title'] ?? 'An error occurred', + 'detail' => $messages ? implode("\n", $messages) : (string) $object, + 'violations' => $violations, + ]; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null) + { + return self::FORMAT === $format && $data instanceof ConstraintViolationListInterface; + } +} diff --git a/src/Problem/Serializer/ErrorNormalizer.php b/src/Problem/Serializer/ErrorNormalizer.php new file mode 100644 index 00000000000..b89cd97e6a1 --- /dev/null +++ b/src/Problem/Serializer/ErrorNormalizer.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Problem\Serializer; + +use Symfony\Component\Debug\Exception\FlattenException; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * Normalizes errors according to the API Problem spec (RFC 7807). + * + * @see https://tools.ietf.org/html/rfc7807 + * + * @author Kévin Dunglas + */ +final class ErrorNormalizer implements NormalizerInterface +{ + const FORMAT = 'jsonproblem'; + + private $debug; + + public function __construct(bool $debug = false) + { + $this->debug = $debug; + } + + /** + * {@inheritdoc} + */ + public function normalize($object, $format = null, array $context = []) + { + $message = $object->getMessage(); + if ($this->debug) { + $trace = $object->getTrace(); + } + + $data = [ + 'type' => $context['type'] ?? 'https://tools.ietf.org/html/rfc2616#section-10', + 'title' => $context['title'] ?? 'An error occurred', + 'detail' => $message ?? (string) $object, + ]; + + if (isset($trace)) { + $data['trace'] = $trace; + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null) + { + return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException); + } +} diff --git a/src/Util/ErrorFormatGuesser.php b/src/Util/ErrorFormatGuesser.php index 2c6839d553e..34ea1818898 100644 --- a/src/Util/ErrorFormatGuesser.php +++ b/src/Util/ErrorFormatGuesser.php @@ -31,7 +31,7 @@ abstract class ErrorFormatGuesser public static function guessErrorFormat(Request $request, array $errorFormats) : array { $requestFormat = $request->getRequestFormat(null); - if (null === $requestFormat || !isset($errorFormats[$requestFormat])) { + if (null !== $requestFormat && isset($errorFormats[$requestFormat])) { return ['key' => $requestFormat, 'value' => $errorFormats[$requestFormat]]; } diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index f6e87978ca6..df0178d0cdf 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -177,7 +177,7 @@ private function getContainerBuilderProphecy() 'api_platform.description' => 'description', 'api_platform.version' => 'version', 'api_platform.formats' => ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']], - 'api_platform.error_formats' => ['jsonld' => ['application/ld+json']], + 'api_platform.error_formats' => ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']], 'api_platform.collection.order' => null, 'api_platform.collection.order_parameter_name' => 'order', 'api_platform.collection.pagination.enabled' => true, @@ -305,7 +305,9 @@ private function getContainerBuilderProphecy() 'api_platform.hydra.normalizer.collection_filters', 'api_platform.hydra.normalizer.constraint_violation_list', 'api_platform.hydra.normalizer.error', - + 'api_platform.problem.encoder', + 'api_platform.problem.normalizer.constraint_violation_list', + 'api_platform.problem.normalizer.error', ]; foreach ($definitions as $definition) { diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 041356646ea..67774365e13 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -35,7 +35,10 @@ public function testDefaultConfig() 'description' => 'description', 'version' => '1.0.0', 'formats' => ['jsonld' => ['mime_types' => ['application/ld+json']]], - 'error_formats' => ['jsonld' => ['mime_types' => ['application/ld+json']]], + 'error_formats' => [ + 'jsonproblem' => ['mime_types' => ['application/problem+json']], + 'jsonld' => ['mime_types' => ['application/ld+json']], + ], 'naming' => [ 'resource_path_naming_strategy' => 'api_platform.naming.resource_path_naming_strategy.underscore', ], diff --git a/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php b/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php new file mode 100644 index 00000000000..936216c00da --- /dev/null +++ b/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Tests\Hydra\Serializer; + +use ApiPlatform\Core\Api\UrlGeneratorInterface; +use ApiPlatform\Core\Hydra\Serializer\ConstraintViolationListNormalizer; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; + +/** + * @author Kévin Dunglas + */ +class ConstraintViolationNormalizerTest extends \PHPUnit_Framework_TestCase +{ + public function testSupportNormalization() + { + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + + $normalizer = new ConstraintViolationListNormalizer($urlGeneratorProphecy->reveal()); + + $this->assertTrue($normalizer->supportsNormalization(new ConstraintViolationList(), ConstraintViolationListNormalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization(new ConstraintViolationList(), 'xml')); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ConstraintViolationListNormalizer::FORMAT)); + } + + public function testNormalize() + { + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + $urlGeneratorProphecy->generate('api_jsonld_context', ['shortName' => 'ConstraintViolationList'])->willReturn('/context/foo')->shouldBeCalled(); + + $normalizer = new ConstraintViolationListNormalizer($urlGeneratorProphecy->reveal()); + + $list = new ConstraintViolationList([ + new ConstraintViolation('a', 'b', [], 'c', 'd', 'e'), + new ConstraintViolation('1', '2', [], '3', '4', '5'), + ]); + + $expected = [ + '@context' => '/context/foo', + '@type' => 'ConstraintViolationList', + 'hydra:title' => 'An error occurred', + 'hydra:description' => 'd: a +4: 1', + 'violations' => [ + [ + 'propertyPath' => 'd', + 'message' => 'a', + ], + [ + 'propertyPath' => '4', + 'message' => '1', + ], + ], + ]; + $this->assertEquals($expected, $normalizer->normalize($list)); + } +} diff --git a/tests/Hydra/Serializer/ErrorNormalizerTest.php b/tests/Hydra/Serializer/ErrorNormalizerTest.php new file mode 100644 index 00000000000..144a87b3456 --- /dev/null +++ b/tests/Hydra/Serializer/ErrorNormalizerTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Tests\Hydra\Serializer; + +use ApiPlatform\Core\Api\UrlGeneratorInterface; +use ApiPlatform\Core\Hydra\Serializer\ErrorNormalizer; +use Symfony\Component\Debug\Exception\FlattenException; + +/** + * @author Kévin Dunglas + */ +class ErrorNormalizerTest extends \PHPUnit_Framework_TestCase +{ + public function testSupportNormalization() + { + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + + $normalizer = new ErrorNormalizer($urlGeneratorProphecy->reveal()); + + $this->assertTrue($normalizer->supportsNormalization(new \Exception(), ErrorNormalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization(new \Exception(), 'xml')); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ErrorNormalizer::FORMAT)); + + $this->assertTrue($normalizer->supportsNormalization(new FlattenException(), ErrorNormalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization(new FlattenException(), 'xml')); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ErrorNormalizer::FORMAT)); + } + + public function testNormalize() + { + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + $urlGeneratorProphecy->generate('api_jsonld_context', ['shortName' => 'Error'])->willReturn('/context/foo')->shouldBeCalled(); + + $normalizer = new ErrorNormalizer($urlGeneratorProphecy->reveal()); + + $this->assertEquals( + [ + '@context' => '/context/foo', + '@type' => 'Error', + 'hydra:title' => 'An error occurred', + 'hydra:description' => 'Hello', + ], + $normalizer->normalize(new \Exception('Hello')) + ); + $this->assertEquals( + [ + '@context' => '/context/foo', + '@type' => 'Error', + 'hydra:title' => 'Hi', + 'hydra:description' => 'Hello', + ], + $normalizer->normalize(new \Exception('Hello'), null, ['title' => 'Hi']) + ); + } +} diff --git a/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php b/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php new file mode 100644 index 00000000000..87cdbd3ddaf --- /dev/null +++ b/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Tests\Problem\Serializer; + +use ApiPlatform\Core\Problem\Serializer\ConstraintViolationListNormalizer; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; + +/** + * @author Kévin Dunglas + */ +class ConstraintViolationNormalizerTest extends \PHPUnit_Framework_TestCase +{ + public function testSupportNormalization() + { + $normalizer = new ConstraintViolationListNormalizer(); + + $this->assertTrue($normalizer->supportsNormalization(new ConstraintViolationList(), ConstraintViolationListNormalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization(new ConstraintViolationList(), 'xml')); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ConstraintViolationListNormalizer::FORMAT)); + } + + public function testNormalize() + { + $normalizer = new ConstraintViolationListNormalizer(); + + $list = new ConstraintViolationList([ + new ConstraintViolation('a', 'b', [], 'c', 'd', 'e'), + new ConstraintViolation('1', '2', [], '3', '4', '5'), + ]); + + $expected = [ + 'type' => 'https://tools.ietf.org/html/rfc2616#section-10', + 'title' => 'An error occurred', + 'detail' => 'd: a +4: 1', + 'violations' => [ + [ + 'propertyPath' => 'd', + 'message' => 'a', + ], + [ + 'propertyPath' => '4', + 'message' => '1', + ], + ], + ]; + $this->assertEquals($expected, $normalizer->normalize($list)); + } +} diff --git a/tests/Problem/Serializer/ErrorNormalizerTest.php b/tests/Problem/Serializer/ErrorNormalizerTest.php new file mode 100644 index 00000000000..b3296e30fcc --- /dev/null +++ b/tests/Problem/Serializer/ErrorNormalizerTest.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Tests\Problem\Serializer; + +use ApiPlatform\Core\Problem\Serializer\ErrorNormalizer; +use Symfony\Component\Debug\Exception\FlattenException; + +/** + * @author Kévin Dunglas + */ +class ErrorNormalizerTest extends \PHPUnit_Framework_TestCase +{ + public function testSupportNormalization() + { + $normalizer = new ErrorNormalizer(); + + $this->assertTrue($normalizer->supportsNormalization(new \Exception(), ErrorNormalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization(new \Exception(), 'xml')); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ErrorNormalizer::FORMAT)); + + $this->assertTrue($normalizer->supportsNormalization(new FlattenException(), ErrorNormalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization(new FlattenException(), 'xml')); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ErrorNormalizer::FORMAT)); + } + + public function testNormalize() + { + $normalizer = new ErrorNormalizer(); + + $this->assertEquals( + [ + 'type' => 'https://tools.ietf.org/html/rfc2616#section-10', + 'title' => 'An error occurred', + 'detail' => 'Hello', + ], + $normalizer->normalize(new \Exception('Hello')) + ); + $this->assertEquals( + [ + 'type' => 'https://dunglas.fr', + 'title' => 'Hi', + 'detail' => 'Hello', + ], + $normalizer->normalize(new \Exception('Hello'), null, ['type' => 'https://dunglas.fr', 'title' => 'Hi']) + ); + } +} diff --git a/tests/Util/ErrorFormatGuesserTest.php b/tests/Util/ErrorFormatGuesserTest.php new file mode 100644 index 00000000000..11f364207a7 --- /dev/null +++ b/tests/Util/ErrorFormatGuesserTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace ApiPlatform\Core\Tests\Util; + +use ApiPlatform\Core\Util\ErrorFormatGuesser; +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Kévin Dunglas + */ +class ErrorFormatGuesserTest extends \PHPUnit_Framework_TestCase +{ + public function testGuessErrorFormat() + { + $request = new Request(); + $request->setRequestFormat('jsonld'); + + $format = ErrorFormatGuesser::guessErrorFormat($request, ['xml' => ['text/xml'], 'jsonld' => ['application/ld+json', 'application/json']]); + $this->assertEquals('jsonld', $format['key']); + $this->assertEquals('application/ld+json', $format['value'][0]); + } + + public function testFallback() + { + $format = ErrorFormatGuesser::guessErrorFormat(new Request(), ['xml' => ['text/xml'], 'jsonld' => ['application/ld+json', 'application/json']]); + $this->assertEquals('xml', $format['key']); + $this->assertEquals('text/xml', $format['value'][0]); + } + + public function testFallbackWhenNotSupported() + { + $request = new Request(); + $request->setRequestFormat('html'); + + $format = ErrorFormatGuesser::guessErrorFormat($request, ['xml' => ['text/xml'], 'jsonld' => ['application/ld+json', 'application/json']]); + $this->assertEquals('xml', $format['key']); + $this->assertEquals('text/xml', $format['value'][0]); + } +}