diff --git a/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php b/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php index 01da4cf95ed..503ff2c8145 100644 --- a/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php +++ b/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php @@ -13,10 +13,11 @@ namespace ApiPlatform\GraphQl\Serializer\Exception; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface as LegacyConstraintViolationListAwareExceptionInterface; use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; use GraphQL\Error\Error; use GraphQL\Error\FormattedError; -use Symfony\Component\Form\Exception\RuntimeException; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -38,7 +39,7 @@ public function __construct(private readonly array $exceptionToStatus = []) public function normalize(mixed $object, ?string $format = null, array $context = []): array { $validationException = $object->getPrevious(); - if (!$validationException instanceof ConstraintViolationListAwareExceptionInterface) { + if (!($validationException instanceof ConstraintViolationListAwareExceptionInterface || $validationException instanceof LegacyConstraintViolationListAwareExceptionInterface)) { throw new RuntimeException(sprintf('Object is not a "%s".', ConstraintViolationListAwareExceptionInterface::class)); } diff --git a/src/JsonApi/Serializer/ErrorNormalizer.php b/src/JsonApi/Serializer/ErrorNormalizer.php index 55a2f12adbc..19764afa3bf 100644 --- a/src/JsonApi/Serializer/ErrorNormalizer.php +++ b/src/JsonApi/Serializer/ErrorNormalizer.php @@ -15,8 +15,8 @@ use ApiPlatform\Problem\Serializer\ErrorNormalizerTrait; use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use ApiPlatform\State\ApiResource\Error; -use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface; +use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface as LegacyConstraintViolationListAwareExceptionInterface; +use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Serializer; @@ -48,7 +48,7 @@ public function normalize(mixed $object, ?string $format = null, array $context { // TODO: in api platform 4 this will be the default, note that JSON:API is close to Problem so we should use the same normalizer if ($context['rfc_7807_compliant_errors'] ?? false) { - if ($object instanceof ConstraintViolationListAwareExceptionInterface) { + if ($object instanceof LegacyConstraintViolationListAwareExceptionInterface || $object instanceof ConstraintViolationListAwareExceptionInterface) { // TODO: return ['errors' => $this->constraintViolationListNormalizer(...)] return $this->constraintViolationListNormalizer->normalize($object->getConstraintViolationList(), $format, $context); } diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index db186069cee..2e6e5a8907d 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -816,6 +816,7 @@ private function getFormats(array $configFormats): array private function registerValidatorConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void { if (interface_exists(ValidatorInterface::class)) { + $container->setParameter('api_platform.validator.legacy_validation_exception', $config['validator']['legacy_validation_exception'] ?? true); $loader->load('metadata/validator.xml'); $loader->load('validator/validator.xml'); diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index e5bf51e52d1..0056a07e9c1 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -22,6 +22,8 @@ use ApiPlatform\Metadata\Put; use ApiPlatform\ParameterValidator\Exception\ValidationExceptionInterface; use ApiPlatform\Symfony\Controller\MainController; +use ApiPlatform\Symfony\Validator\Exception\ValidationException as LegacyValidationException; +use ApiPlatform\Validator\Exception\ValidationException; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle; use Doctrine\ORM\EntityManagerInterface; @@ -94,6 +96,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->variableNode('serialize_payload_fields')->defaultValue([])->info('Set to null to serialize all payload fields when a validation error is thrown, or set the fields you want to include explicitly.')->end() ->booleanNode('query_parameter_validation')->defaultValue(true)->end() + ->booleanNode('legacy_validation_exception')->defaultValue(true)->info('Uses the legacy "%s" instead of "%s".', LegacyValidationException::class, ValidationException::class)->end() ->end() ->end() ->arrayNode('eager_loading') diff --git a/src/Symfony/Bundle/Resources/config/validator/validator.xml b/src/Symfony/Bundle/Resources/config/validator/validator.xml index c5c66891d1c..8946534fa5d 100644 --- a/src/Symfony/Bundle/Resources/config/validator/validator.xml +++ b/src/Symfony/Bundle/Resources/config/validator/validator.xml @@ -8,6 +8,7 @@ + %api_platform.validator.legacy_validation_exception% diff --git a/src/Symfony/EventListener/ErrorListener.php b/src/Symfony/EventListener/ErrorListener.php index 9a2a53331eb..87b376c0c5b 100644 --- a/src/Symfony/EventListener/ErrorListener.php +++ b/src/Symfony/EventListener/ErrorListener.php @@ -26,7 +26,8 @@ use ApiPlatform\Metadata\Util\ContentNegotiationTrait; use ApiPlatform\State\ApiResource\Error; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; +use ApiPlatform\State\Util\RequestAttributesExtractor; +use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface as LegacyConstraintViolationListAwareExceptionInterface; use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; use Negotiation\Negotiator; use Psr\Log\LoggerInterface; @@ -192,7 +193,7 @@ private function getStatusCode(?HttpOperation $apiOperation, Request $request, ? return 400; } - if ($exception instanceof ConstraintViolationListAwareExceptionInterface) { + if ($exception instanceof ConstraintViolationListAwareExceptionInterface || $exception instanceof LegacyConstraintViolationListAwareExceptionInterface) { return 422; } diff --git a/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php b/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php index 0d3dc6dd7e8..cf137c1ea9b 100644 --- a/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php +++ b/src/Symfony/Validator/Exception/ConstraintViolationListAwareExceptionInterface.php @@ -18,6 +18,8 @@ /** * An exception which has a constraint violation list. + * + * @deprecated use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface */ interface ConstraintViolationListAwareExceptionInterface extends ExceptionInterface { diff --git a/src/Symfony/Validator/Exception/ValidationException.php b/src/Symfony/Validator/Exception/ValidationException.php index 41faa0d1ac3..6100c94b72d 100644 --- a/src/Symfony/Validator/Exception/ValidationException.php +++ b/src/Symfony/Validator/Exception/ValidationException.php @@ -18,7 +18,7 @@ use ApiPlatform\Metadata\ErrorResource; use ApiPlatform\Metadata\Exception\HttpExceptionInterface; use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; -use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface as ApiPlatformConstraintViolationListAwareExceptionInterface; +use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; use ApiPlatform\Validator\Exception\ValidationException as BaseValidationException; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; use Symfony\Component\WebLink\Link; @@ -71,6 +71,6 @@ ], graphQlOperations: [] )] -final class ValidationException extends BaseValidationException implements ConstraintViolationListAwareExceptionInterface, ApiPlatformConstraintViolationListAwareExceptionInterface, \Stringable, ProblemExceptionInterface, HttpExceptionInterface, SymfonyHttpExceptionInterface +final class ValidationException extends BaseValidationException implements ConstraintViolationListAwareExceptionInterface, \Stringable, ProblemExceptionInterface, HttpExceptionInterface, SymfonyHttpExceptionInterface { } diff --git a/src/Symfony/Validator/Validator.php b/src/Symfony/Validator/Validator.php index 6bbd5b4b6ff..9580e008bd2 100644 --- a/src/Symfony/Validator/Validator.php +++ b/src/Symfony/Validator/Validator.php @@ -13,7 +13,8 @@ namespace ApiPlatform\Symfony\Validator; -use ApiPlatform\Symfony\Validator\Exception\ValidationException; +use ApiPlatform\Symfony\Validator\Exception\ValidationException as LegacyValidationException; +use ApiPlatform\Validator\Exception\ValidationException; use ApiPlatform\Validator\ValidatorInterface; use Psr\Container\ContainerInterface; use Symfony\Component\Validator\Constraints\GroupSequence; @@ -28,7 +29,7 @@ */ class Validator implements ValidatorInterface { - public function __construct(private readonly SymfonyValidatorInterface $validator, private readonly ?ContainerInterface $container = null) + public function __construct(private readonly SymfonyValidatorInterface $validator, private readonly ?ContainerInterface $container = null, private readonly ?bool $legacyValidationException = true) { } @@ -57,6 +58,9 @@ public function validate(object $data, array $context = []): void $violations = $this->validator->validate($data, null, $validationGroups); if (0 !== \count($violations)) { + if (true === $this->legacyValidationException) { + throw new LegacyValidationException($violations); + } throw new ValidationException($violations); } } diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index f33e912c31b..c7259d7b7de 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -109,6 +109,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm 'validator' => [ 'serialize_payload_fields' => [], 'query_parameter_validation' => true, + 'legacy_validation_exception' => true, ], 'name_converter' => null, 'enable_swagger' => true, diff --git a/tests/Symfony/Validator/ValidatorTest.php b/tests/Symfony/Validator/ValidatorTest.php index 7d11b46ff6e..14ecce8cf0a 100644 --- a/tests/Symfony/Validator/ValidatorTest.php +++ b/tests/Symfony/Validator/ValidatorTest.php @@ -13,10 +13,11 @@ namespace ApiPlatform\Tests\Symfony\Validator; -use ApiPlatform\Symfony\Validator\Exception\ValidationException; +use ApiPlatform\Symfony\Validator\Exception\ValidationException as LegacyValidationException; use ApiPlatform\Symfony\Validator\ValidationGroupsGeneratorInterface; use ApiPlatform\Symfony\Validator\Validator; use ApiPlatform\Tests\Fixtures\DummyEntity; +use ApiPlatform\Validator\Exception\ValidationException; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Container\ContainerInterface; @@ -43,7 +44,7 @@ public function testValid(): void $symfonyValidatorProphecy->validate($data, null, null)->willReturn($constraintViolationListProphecy->reveal())->shouldBeCalled(); $symfonyValidator = $symfonyValidatorProphecy->reveal(); - $validator = new Validator($symfonyValidator); + $validator = new Validator($symfonyValidator, legacyValidationException: false); $validator->validate(new DummyEntity()); } @@ -58,7 +59,25 @@ public function testInvalid(): void $symfonyValidatorProphecy->validate($data, null, null)->willReturn($constraintViolationList)->shouldBeCalled(); $symfonyValidator = $symfonyValidatorProphecy->reveal(); - $validator = new Validator($symfonyValidator); + $validator = new Validator($symfonyValidator, legacyValidationException: false); + $validator->validate(new DummyEntity()); + } + + /** + * @group legacy + */ + public function testDeprecatedInvalid(): void + { + $this->expectException(LegacyValidationException::class); + + $data = new DummyEntity(); + $constraintViolationList = new ConstraintViolationList([new ConstraintViolation('test', null, [], null, 'test', null), new ConstraintViolation('test', null, [], null, 'test', null)]); + + $symfonyValidatorProphecy = $this->prophesize(SymfonyValidatorInterface::class); + $symfonyValidatorProphecy->validate($data, null, null)->willReturn($constraintViolationList)->shouldBeCalled(); + $symfonyValidator = $symfonyValidatorProphecy->reveal(); + + $validator = new Validator($symfonyValidator, legacyValidationException: true); $validator->validate(new DummyEntity()); } @@ -74,7 +93,7 @@ public function testGetGroupsFromCallable(): void $symfonyValidatorProphecy->validate($data, null, $expectedValidationGroups)->willReturn($constraintViolationListProphecy->reveal())->shouldBeCalled(); $symfonyValidator = $symfonyValidatorProphecy->reveal(); - $validator = new Validator($symfonyValidator); + $validator = new Validator($symfonyValidator, legacyValidationException: false); $validator->validate(new DummyEntity(), ['groups' => fn ($data): array => $data instanceof DummyEntity ? $expectedValidationGroups : []]); } @@ -97,7 +116,7 @@ public function __invoke(object $object): array } }); - $validator = new Validator($symfonyValidatorProphecy->reveal(), $containerProphecy->reveal()); + $validator = new Validator($symfonyValidatorProphecy->reveal(), $containerProphecy->reveal(), legacyValidationException: false); $validator->validate(new DummyEntity(), ['groups' => 'groups_builder']); } @@ -116,7 +135,7 @@ public function testValidatorWithScalarGroup(): void $containerProphecy = $this->prophesize(ContainerInterface::class); $containerProphecy->has('foo')->willReturn(false)->shouldBeCalled(); - $validator = new Validator($symfonyValidator, $containerProphecy->reveal()); + $validator = new Validator($symfonyValidator, $containerProphecy->reveal(), legacyValidationException: false); $validator->validate(new DummyEntity(), ['groups' => 'foo']); } }