diff --git a/CHANGELOG.md b/CHANGELOG.md index a33cf391ffc..a8a71712b93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v3.2.10 + +### Bug fixes + +* [6f3c6a663](https://github.com/api-platform/core/commit/6f3c6a663cc55730580b82d146b8d62cac4f1bc5) fix(symfony): attribute filter names (#6062) +* [dc77c7949](https://github.com/api-platform/core/commit/dc77c7949a6e8c48d57708d8f43027e00124388c) fix(symfony): disable symfony error handling by default +* [f75649d49](https://github.com/api-platform/core/commit/f75649d49139e332bb739aece56a315943162770) fix(symfony): use Type constraint violation code instead of exception code + +## v3.2.9 + +* [ecffcde](https://github.com/api-platform/core/pull/6063/commits/ecffcdeb0a27e49256c56502f6f6e327d9e03d5b) chore: remove comparator conflict wrongly introduced + ## v3.2.8 ### Bug fixes @@ -1977,4 +1989,4 @@ Please read #2825 if you have issues with the behavior of Readable/Writable Link ## 1.0.0 beta 2 * Preserve indexes when normalizing and denormalizing associative arrays -* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance +* Allow setting default order for property when registering a `Doctrine\Orm\Filter\OrderFilter` instance \ No newline at end of file diff --git a/docs/src/DependencyInjection/Compiler/AttributeFilterPass.php b/docs/src/DependencyInjection/Compiler/AttributeFilterPass.php index a6bf1432e3d..a38b1efcd55 100644 --- a/docs/src/DependencyInjection/Compiler/AttributeFilterPass.php +++ b/docs/src/DependencyInjection/Compiler/AttributeFilterPass.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Playground\DependencyInjection\Compiler; -use ApiPlatform\Util\AttributeFilterExtractorTrait; +use ApiPlatform\Metadata\Util\AttributeFilterExtractorTrait; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; diff --git a/features/main/validation.feature b/features/main/validation.feature index 170c4c51a27..b8ab1feb8a6 100644 --- a/features/main/validation.feature +++ b/features/main/validation.feature @@ -119,40 +119,40 @@ Feature: Using validations groups { "propertyPath": "", "message": "This value should be of type unknown.", - "code": "0", + "code": "ba785a8c-82cb-4283-967c-3cf342181b40", "hint": "Failed to create object because the class misses the \"baz\" property." }, { "propertyPath": "qux", "message": "This value should be of type string.", - "code": "0" + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" }, { "propertyPath": "foo", "message": "This value should be of type bool.", - "code": "0" + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" }, { "propertyPath": "bar", "message": "This value should be of type int.", - "code": "0" + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" }, { "propertyPath": "uuid", "message": "This value should be of type uuid.", - "code": "0", + "code": "ba785a8c-82cb-4283-967c-3cf342181b40", "hint": "Invalid UUID string: y" }, { "propertyPath": "relatedDummy", "message": "This value should be of type array|string.", - "code": "0", + "code": "ba785a8c-82cb-4283-967c-3cf342181b40", "hint": "The type of the \"relatedDummy\" attribute must be \"array\" (nested document) or \"string\" (IRI), \"integer\" given." }, { "propertyPath": "relatedDummies", "message": "This value should be of type array.", - "code": "0" + "code": "ba785a8c-82cb-4283-967c-3cf342181b40" } ] } diff --git a/src/Doctrine/Odm/State/LinksHandlerInterface.php b/src/Doctrine/Odm/State/LinksHandlerInterface.php index 9d2835b0e2e..a60421fe1f9 100644 --- a/src/Doctrine/Odm/State/LinksHandlerInterface.php +++ b/src/Doctrine/Odm/State/LinksHandlerInterface.php @@ -24,7 +24,7 @@ interface LinksHandlerInterface /** * Handle Doctrine ORM links. * - * @see ApiPlatform\Doctrine\Odm\State\LinksHandlerTrait + * @see LinksHandlerTrait * * @param array $uriVariables * @param array{entityClass: string, operation: Operation}&array $context diff --git a/src/Doctrine/Odm/State/Options.php b/src/Doctrine/Odm/State/Options.php index edc56dceb4b..919c10f8045 100644 --- a/src/Doctrine/Odm/State/Options.php +++ b/src/Doctrine/Odm/State/Options.php @@ -20,7 +20,7 @@ class Options implements OptionsInterface /** * @param mixed $handleLinks experimental callable, typed mixed as we may want a service name in the future * - * @see \ApiPlatform\Doctrine\Odm\State\LinksHandlerInterface + * @see LinksHandlerInterface */ public function __construct( protected ?string $documentClass = null, diff --git a/src/Doctrine/Orm/State/LinksHandlerInterface.php b/src/Doctrine/Orm/State/LinksHandlerInterface.php index be78bc06bd7..98b75c078ef 100644 --- a/src/Doctrine/Orm/State/LinksHandlerInterface.php +++ b/src/Doctrine/Orm/State/LinksHandlerInterface.php @@ -25,7 +25,7 @@ interface LinksHandlerInterface /** * Handle Doctrine ORM links. * - * @see ApiPlatform\Doctrine\Orm\State\LinksHandlerTrait + * @see LinksHandlerTrait * * @param array $uriVariables * @param array{entityClass: string, operation: Operation}&array $context diff --git a/src/Doctrine/Orm/State/Options.php b/src/Doctrine/Orm/State/Options.php index e5704a253c0..50d13f1b7a4 100644 --- a/src/Doctrine/Orm/State/Options.php +++ b/src/Doctrine/Orm/State/Options.php @@ -20,7 +20,7 @@ class Options implements OptionsInterface /** * @param string|callable $handleLinks experimental callable, typed mixed as we may want a service name in the future * - * @see \ApiPlatform\Doctrine\Orm\State\LinksHandlerInterface + * @see LinksHandlerInterface */ public function __construct( protected ?string $entityClass = null, diff --git a/src/Elasticsearch/Tests/State/ItemProviderTest.php b/src/Elasticsearch/Tests/State/ItemProviderTest.php index 64fe245d8bd..0a260acef4b 100644 --- a/src/Elasticsearch/Tests/State/ItemProviderTest.php +++ b/src/Elasticsearch/Tests/State/ItemProviderTest.php @@ -91,7 +91,7 @@ public function testGetInexistantItem(): void $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $clientClass = class_exists(\Elasticsearch\Client::class) ? \Elasticsearch\Client::class : \Elastic\Elasticsearch\ClientInterface::class; + $clientClass = class_exists(\Elasticsearch\Client::class) ? \Elasticsearch\Client::class : ClientInterface::class; $clientProphecy = $this->prophesize($clientClass); $clientProphecy->get(['index' => 'foo', 'id' => '404'])->willReturn([ diff --git a/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php b/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php index 148119dd57c..90c87a881cf 100644 --- a/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php +++ b/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php @@ -22,7 +22,7 @@ use Symfony\Component\Validator\ConstraintViolationListInterface; /** - * Converts {@see \Symfony\Component\Validator\ConstraintViolationListInterface} to a JSON API error representation. + * Converts {@see ConstraintViolationListInterface} to a JSON API error representation. * * @author Héctor Hurtarte */ diff --git a/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php index 1857d4f952a..2bd4401ceb2 100644 --- a/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php +++ b/src/Metadata/Resource/Factory/NotExposedOperationResourceMetadataCollectionFactory.php @@ -60,7 +60,7 @@ public function create(string $resourceClass): ResourceMetadataCollection foreach ($resourceMetadataCollection as $resource) { $operations = $resource->getOperations(); - /** @var \ApiPlatform\Metadata\HttpOperation $operation */ + /** @var HttpOperation $operation */ foreach ($operations as $operation) { // An item operation has been found, nothing to do anymore in this factory if (('GET' === $operation->getMethod() && !$operation instanceof CollectionOperationInterface) || ($operation->getExtraProperties()['is_legacy_resource_metadata'] ?? false)) { diff --git a/src/Metadata/Util/ContentNegotiationTrait.php b/src/Metadata/Util/ContentNegotiationTrait.php index 4a257d81fcb..c86b2cc3c28 100644 --- a/src/Metadata/Util/ContentNegotiationTrait.php +++ b/src/Metadata/Util/ContentNegotiationTrait.php @@ -28,7 +28,7 @@ trait ContentNegotiationTrait /** * Gets the format associated with the mime type. * - * Adapted from {@see \Symfony\Component\HttpFoundation\Request::getFormat}. + * Adapted from {@see Request::getFormat}. * * @param array $formats */ diff --git a/src/State/Provider/DeserializeProvider.php b/src/State/Provider/DeserializeProvider.php index 0bebb6b234e..b2217275051 100644 --- a/src/State/Provider/DeserializeProvider.php +++ b/src/State/Provider/DeserializeProvider.php @@ -97,7 +97,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c if ($exception->canUseMessageForUser()) { $parameters['hint'] = $exception->getMessage(); } - $violations->add(new ConstraintViolation($this->translator->trans($message, ['{{ type }}' => implode('|', $exception->getExpectedTypes() ?? [])], 'validators'), $message, $parameters, null, $exception->getPath(), null, null, (string) $exception->getCode())); + $violations->add(new ConstraintViolation($this->translator->trans($message, ['{{ type }}' => implode('|', $exception->getExpectedTypes() ?? [])], 'validators'), $message, $parameters, null, $exception->getPath(), null, null, (string) Type::INVALID_TYPE_ERROR)); } if (0 !== \count($violations)) { throw new ValidationException($violations); diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 4c4b3658d32..134fe401aba 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -232,6 +232,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.collection.pagination.items_per_page_parameter_name', $config['defaults']['pagination_items_per_page_parameter_name'] ?? $config['collection']['pagination']['items_per_page_parameter_name']); $container->setParameter('api_platform.collection.pagination.partial_parameter_name', $config['defaults']['pagination_partial_parameter_name'] ?? $config['collection']['pagination']['partial_parameter_name']); $container->setParameter('api_platform.collection.pagination', $this->getPaginationDefaults($config['defaults'] ?? [], $config['collection']['pagination'])); + $container->setParameter('api_platform.handle_symfony_errors', $config['handle_symfony_errors'] ?? false); $container->setParameter('api_platform.http_cache.etag', $config['defaults']['cache_headers']['etag'] ?? true); $container->setParameter('api_platform.http_cache.max_age', $config['defaults']['cache_headers']['max_age'] ?? null); $container->setParameter('api_platform.http_cache.shared_max_age', $config['defaults']['cache_headers']['shared_max_age'] ?? null); diff --git a/src/Symfony/Bundle/DependencyInjection/Compiler/AttributeFilterPass.php b/src/Symfony/Bundle/DependencyInjection/Compiler/AttributeFilterPass.php index 14af328e000..ed9b98c4c45 100644 --- a/src/Symfony/Bundle/DependencyInjection/Compiler/AttributeFilterPass.php +++ b/src/Symfony/Bundle/DependencyInjection/Compiler/AttributeFilterPass.php @@ -14,8 +14,8 @@ namespace ApiPlatform\Symfony\Bundle\DependencyInjection\Compiler; use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\Util\AttributeFilterExtractorTrait; use ApiPlatform\Metadata\Util\ReflectionClassRecursiveIterator; -use ApiPlatform\Util\AttributeFilterExtractorTrait; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index aa40e22253d..948cf49f156 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -103,6 +103,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->booleanNode('force_eager')->defaultTrue()->info('Force join on every relation. If disabled, it will only join relations having the EAGER fetch mode.')->end() ->end() ->end() + ->booleanNode('handle_symfony_errors')->defaultFalse()->info('Allows to handle symfony exceptions.')->end() ->booleanNode('enable_swagger')->defaultTrue()->info('Enable the Swagger documentation and export.')->end() ->booleanNode('enable_swagger_ui')->defaultValue(class_exists(TwigBundle::class))->info('Enable Swagger UI')->end() ->booleanNode('enable_re_doc')->defaultValue(class_exists(TwigBundle::class))->info('Enable ReDoc')->end() diff --git a/src/Symfony/Bundle/Resources/config/symfony/events.xml b/src/Symfony/Bundle/Resources/config/symfony/events.xml index 41b8d29ac02..f16daef8574 100644 --- a/src/Symfony/Bundle/Resources/config/symfony/events.xml +++ b/src/Symfony/Bundle/Resources/config/symfony/events.xml @@ -6,6 +6,7 @@ + %api_platform.handle_symfony_errors% diff --git a/src/Symfony/EventListener/DeserializeListener.php b/src/Symfony/EventListener/DeserializeListener.php index e4349f899d0..a246ad9fef3 100644 --- a/src/Symfony/EventListener/DeserializeListener.php +++ b/src/Symfony/EventListener/DeserializeListener.php @@ -115,7 +115,7 @@ public function onKernelRequest(RequestEvent $event): void if ($exception->canUseMessageForUser()) { $parameters['hint'] = $exception->getMessage(); } - $violations->add(new ConstraintViolation($this->translator->trans($message, ['{{ type }}' => implode('|', $exception->getExpectedTypes() ?? [])], 'validators'), $message, $parameters, null, $exception->getPath(), null, null, (string) $exception->getCode())); + $violations->add(new ConstraintViolation($this->translator->trans($message, ['{{ type }}' => implode('|', $exception->getExpectedTypes() ?? [])], 'validators'), $message, $parameters, null, $exception->getPath(), null, null, Type::INVALID_TYPE_ERROR)); } if (0 !== \count($violations)) { throw new ValidationException($violations); diff --git a/src/Symfony/EventListener/ExceptionListener.php b/src/Symfony/EventListener/ExceptionListener.php index bb59aa04621..7aa50a5a995 100644 --- a/src/Symfony/EventListener/ExceptionListener.php +++ b/src/Symfony/EventListener/ExceptionListener.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Symfony\EventListener; use ApiPlatform\Metadata\Error; +use ApiPlatform\Util\RequestAttributesExtractor; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\EventListener\ErrorListener; @@ -25,13 +26,22 @@ */ final class ExceptionListener { - public function __construct(private readonly ErrorListener $errorListener) + public function __construct(private readonly ErrorListener $errorListener, public bool $handleSymfonyErrors = false) { } public function onKernelException(ExceptionEvent $event): void { $request = $event->getRequest(); + + // Normalize exceptions only for routes managed by API Platform + if ( + false === $this->handleSymfonyErrors + && !((RequestAttributesExtractor::extractAttributes($request)['respond'] ?? $request->attributes->getBoolean('_api_respond', false)) || $request->attributes->getBoolean('_graphql', false)) + ) { + return; + } + // Don't loop on errors leave it to Symfony as we could not handle this properly if (($operation = $request->attributes->get('_api_operation')) && $operation instanceof Error) { return; diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index 3548f63f4ff..ee87d2f05c2 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -2233,7 +2233,7 @@ public function thereIsADummyWithSubEntity(string $strId, string $name): void public function thereIsAGroupWithUuidAndNUsers(string $uuid, int $nbUsers): void { $group = new Group(); - $group->setUuid(\Symfony\Component\Uid\Uuid::fromString($uuid)); + $group->setUuid(SymfonyUuid::fromString($uuid)); $this->manager->persist($group); diff --git a/tests/Fixtures/app/config/config_common.yml b/tests/Fixtures/app/config/config_common.yml index 0de51bbbf60..4981e21ff3f 100644 --- a/tests/Fixtures/app/config/config_common.yml +++ b/tests/Fixtures/app/config/config_common.yml @@ -73,6 +73,7 @@ api_platform: Symfony\Component\Serializer\Exception\ExceptionInterface: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST ApiPlatform\Exception\InvalidArgumentException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST ApiPlatform\Exception\FilterValidationException: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST + handle_symfony_errors: true http_cache: invalidation: enabled: true diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index 2cadbb0d603..33eda6cf678 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -226,6 +226,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm ], 'keep_legacy_inflector' => true, 'event_listeners_backward_compatibility_layer' => true, + 'handle_symfony_errors' => false, ], $config); } diff --git a/tests/Symfony/EventListener/DeserializeListenerTest.php b/tests/Symfony/EventListener/DeserializeListenerTest.php index 197dfa1c22d..8290d47b801 100644 --- a/tests/Symfony/EventListener/DeserializeListenerTest.php +++ b/tests/Symfony/EventListener/DeserializeListenerTest.php @@ -366,7 +366,7 @@ public function testTurnPartialDenormalizationExceptionIntoValidationException() $this->assertSame($violation->getPropertyPath(), 'foo'); $this->assertNull($violation->getInvalidValue()); $this->assertNull($violation->getPlural()); - $this->assertSame($violation->getCode(), '0'); + $this->assertSame($violation->getCode(), 'ba785a8c-82cb-4283-967c-3cf342181b40'); } } }