diff --git a/features/main/validation.feature b/features/main/validation.feature index 0ed47344dcb..3be885c31af 100644 --- a/features/main/validation.feature +++ b/features/main/validation.feature @@ -73,6 +73,39 @@ Feature: Using validations groups """ And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + @createSchema + Scenario: Create a resource with serializedName property + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "dummy_validation_serialized_name" with body: + """ + { + "code": "My Dummy" + } + """ + Then the response status code should be 422 + And the response should be in JSON + And the JSON should be equal to: + """ + { + "@id": "/validation_errors/ad32d13f-c3d4-423b-909a-857b961eb720", + "@type": "ConstraintViolationList", + "status": 422, + "violations": [ + { + "propertyPath": "test", + "message": "This value should not be null.", + "code": "ad32d13f-c3d4-423b-909a-857b961eb720" + } + ], + "hydra:title": "An error occurred", + "hydra:description": "title: This value should not be null.", + "type": "/validation_errors/ad32d13f-c3d4-423b-909a-857b961eb720", + "title": "An error occurred", + "detail": "title: This value should not be null." + } + """ + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" + @!mongodb @createSchema Scenario: Create a resource with collectDenormalizationErrors diff --git a/src/Hydra/Serializer/ConstraintViolationListNormalizer.php b/src/Hydra/Serializer/ConstraintViolationListNormalizer.php index 0d71e1a56bc..26d831f05d1 100644 --- a/src/Hydra/Serializer/ConstraintViolationListNormalizer.php +++ b/src/Hydra/Serializer/ConstraintViolationListNormalizer.php @@ -26,7 +26,7 @@ final class ConstraintViolationListNormalizer extends AbstractConstraintViolatio { public const FORMAT = 'jsonld'; - public function __construct(private readonly UrlGeneratorInterface $urlGenerator, array $serializePayloadFields = null, NameConverterInterface $nameConverter = null) + public function __construct(private readonly ?UrlGeneratorInterface $urlGenerator = null, array $serializePayloadFields = null, NameConverterInterface $nameConverter = null) { parent::__construct($serializePayloadFields, $nameConverter); } @@ -36,14 +36,6 @@ public function __construct(private readonly UrlGeneratorInterface $urlGenerator */ public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - [$messages, $violations] = $this->getMessagesAndViolations($object); - - return [ - '@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'ConstraintViolationList']), - '@type' => 'ConstraintViolationList', - 'hydra:title' => $context['title'] ?? 'An error occurred', - 'hydra:description' => $messages ? implode("\n", $messages) : (string) $object, - 'violations' => $violations, - ]; + return $this->getViolations($object); } } diff --git a/src/Problem/Serializer/ConstraintViolationListNormalizer.php b/src/Problem/Serializer/ConstraintViolationListNormalizer.php index e1f9fd9913f..86c1bd1690e 100644 --- a/src/Problem/Serializer/ConstraintViolationListNormalizer.php +++ b/src/Problem/Serializer/ConstraintViolationListNormalizer.php @@ -20,6 +20,7 @@ * Converts {@see \Symfony\Component\Validator\ConstraintViolationListInterface} the API Problem spec (RFC 7807). * * @see https://tools.ietf.org/html/rfc7807 + * @deprecated this is not used anymore internally and will be removed in 4.0 * * @author Kévin Dunglas */ diff --git a/src/Serializer/AbstractConstraintViolationListNormalizer.php b/src/Serializer/AbstractConstraintViolationListNormalizer.php index 45eca2ef9bd..e725a0b4a01 100644 --- a/src/Serializer/AbstractConstraintViolationListNormalizer.php +++ b/src/Serializer/AbstractConstraintViolationListNormalizer.php @@ -65,8 +65,54 @@ public function hasCacheableSupportsMethod(): bool return true; } + /** + * return string[]. + */ + protected function getViolations(ConstraintViolationListInterface $constraintViolationList): array + { + $violations = []; + + foreach ($constraintViolationList as $violation) { + $class = \is_object($root = $violation->getRoot()) ? $root::class : null; + + if ($this->nameConverter instanceof AdvancedNameConverterInterface) { + $propertyPath = $this->nameConverter->normalize($violation->getPropertyPath(), $class, static::FORMAT); + } elseif ($this->nameConverter instanceof NameConverterInterface) { + $propertyPath = $this->nameConverter->normalize($violation->getPropertyPath()); + } else { + $propertyPath = $violation->getPropertyPath(); + } + + $violationData = [ + 'propertyPath' => $propertyPath, + 'message' => $violation->getMessage(), + 'code' => $violation->getCode(), + ]; + + if ($hint = $violation->getParameters()['hint'] ?? false) { + $violationData['hint'] = $hint; + } + + $constraint = $violation instanceof ConstraintViolation ? $violation->getConstraint() : null; + if ( + [] !== $this->serializePayloadFields + && $constraint + && $constraint->payload + // If some fields are whitelisted, only them are added + && $payloadFields = null === $this->serializePayloadFields ? $constraint->payload : array_intersect_key($constraint->payload, $this->serializePayloadFields) + ) { + $violationData['payload'] = $payloadFields; + } + + $violations[] = $violationData; + } + + return $violations; + } + protected function getMessagesAndViolations(ConstraintViolationListInterface $constraintViolationList): array { + trigger_deprecation('api-platform', '3.2', sprintf('"%s::%s" will be removed in 4.0, use "%1$s::%s', __CLASS__, __METHOD__, 'getViolations')); $violations = $messages = []; foreach ($constraintViolationList as $violation) { diff --git a/src/Symfony/Validator/Exception/ValidationException.php b/src/Symfony/Validator/Exception/ValidationException.php index 47578f6cb6c..9fd094fcd03 100644 --- a/src/Symfony/Validator/Exception/ValidationException.php +++ b/src/Symfony/Validator/Exception/ValidationException.php @@ -59,11 +59,6 @@ public function __construct(private readonly ConstraintViolationListInterface $c parent::__construct($message ?: $this->__toString(), $code, $previous, $errorTitle); } - public function getConstraintViolationList(): ConstraintViolationListInterface - { - return $this->constraintViolationList; - } - public function getId(): string { $ids = []; @@ -148,21 +143,8 @@ public function getInstance(): ?string #[SerializedName('violations')] #[Groups(['json', 'jsonld', 'legacy_jsonld', 'legacy_jsonproblem', 'legacy_json'])] - public function getViolations(): iterable + public function getConstraintViolationList(): ConstraintViolationListInterface { - foreach ($this->getConstraintViolationList() as $violation) { - $propertyPath = $violation->getPropertyPath(); - $violationData = [ - 'propertyPath' => $propertyPath, - 'message' => $violation->getMessage(), - 'code' => $violation->getCode(), - ]; - - if ($hint = $violation->getParameters()['hint'] ?? false) { - $violationData['hint'] = $hint; - } - - yield $violationData; - } + return $this->constraintViolationList; } } diff --git a/tests/Fixtures/TestBundle/Entity/DummyValidationSerializedName.php b/tests/Fixtures/TestBundle/Entity/DummyValidationSerializedName.php new file mode 100644 index 00000000000..d9c6517c2fc --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/DummyValidationSerializedName.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Post; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\Validator\Constraints as Assert; + +#[ApiResource(operations: [ + new GetCollection(), + new Post(uriTemplate: 'dummy_validation_serialized_name'), +] +)] +#[ORM\Entity] +class DummyValidationSerializedName +{ + /** + * @var int|null The id + */ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + /** + * @var string|null The dummy title + */ + #[ORM\Column(nullable: true)] + #[Assert\NotNull()] + #[SerializedName('test')] + private ?string $title = null; + /** + * @var string The dummy code + */ + #[ORM\Column] + private string $code; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(int $id): self + { + $this->id = $id; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): self + { + $this->name = $name; + + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): self + { + $this->title = $title; + + return $this; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } +} diff --git a/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php b/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php index 322f108d3cb..1650634b07a 100644 --- a/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php +++ b/tests/Hydra/Serializer/ConstraintViolationNormalizerTest.php @@ -59,10 +59,7 @@ public function testSupportNormalization(): void */ public function testNormalize(callable $nameConverterFactory, ?array $fields, array $expected): void { - $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); - $urlGeneratorProphecy->generate('api_jsonld_context', ['shortName' => 'ConstraintViolationList'])->willReturn('/context/foo')->shouldBeCalled(); - - $normalizer = new ConstraintViolationListNormalizer($urlGeneratorProphecy->reveal(), $fields, $nameConverterFactory($this)); + $normalizer = new ConstraintViolationListNormalizer(null, $fields, $nameConverterFactory($this)); // Note : we use NotNull constraint and not Constraint class because Constraint is abstract $constraint = new NotNull(); @@ -78,40 +75,28 @@ public function testNormalize(callable $nameConverterFactory, ?array $fields, ar public static function nameConverterAndPayloadFieldsProvider(): iterable { $basicExpectation = [ - '@context' => '/context/foo', - '@type' => 'ConstraintViolationList', - 'hydra:title' => 'An error occurred', - 'hydra:description' => "d: a\n4: 1", - 'violations' => [ - [ - 'propertyPath' => 'd', - 'message' => 'a', - 'code' => 'f24bdbad0becef97a6887238aa58221c', - ], - [ - 'propertyPath' => '4', - 'message' => '1', - 'code' => null, - ], + [ + 'propertyPath' => 'd', + 'message' => 'a', + 'code' => 'f24bdbad0becef97a6887238aa58221c', + ], + [ + 'propertyPath' => '4', + 'message' => '1', + 'code' => null, ], ]; $nameConverterBasedExpectation = [ - '@context' => '/context/foo', - '@type' => 'ConstraintViolationList', - 'hydra:title' => 'An error occurred', - 'hydra:description' => "_d: a\n_4: 1", - 'violations' => [ - [ - 'propertyPath' => '_d', - 'message' => 'a', - 'code' => 'f24bdbad0becef97a6887238aa58221c', - ], - [ - 'propertyPath' => '_4', - 'message' => '1', - 'code' => null, - ], + [ + 'propertyPath' => '_d', + 'message' => 'a', + 'code' => 'f24bdbad0becef97a6887238aa58221c', + ], + [ + 'propertyPath' => '_4', + 'message' => '1', + 'code' => null, ], ]; @@ -132,19 +117,19 @@ public static function nameConverterAndPayloadFieldsProvider(): iterable $nullNameConverterFactory = fn () => null; $expected = $nameConverterBasedExpectation; - $expected['violations'][0]['payload'] = ['severity' => 'warning']; + $expected[0]['payload'] = ['severity' => 'warning']; yield [$advancedNameConverterFactory, ['severity', 'anotherField1'], $expected]; yield [$nameConverterFactory, ['severity', 'anotherField1'], $expected]; $expected = $basicExpectation; - $expected['violations'][0]['payload'] = ['severity' => 'warning']; + $expected[0]['payload'] = ['severity' => 'warning']; yield [$nullNameConverterFactory, ['severity', 'anotherField1'], $expected]; $expected = $nameConverterBasedExpectation; - $expected['violations'][0]['payload'] = ['severity' => 'warning', 'anotherField2' => 'aValue']; + $expected[0]['payload'] = ['severity' => 'warning', 'anotherField2' => 'aValue']; yield [$advancedNameConverterFactory, null, $expected]; yield [$nameConverterFactory, null, $expected]; $expected = $basicExpectation; - $expected['violations'][0]['payload'] = ['severity' => 'warning', 'anotherField2' => 'aValue']; + $expected[0]['payload'] = ['severity' => 'warning', 'anotherField2' => 'aValue']; yield [$nullNameConverterFactory, null, $expected]; yield [$advancedNameConverterFactory, [], $nameConverterBasedExpectation]; diff --git a/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php b/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php index 961c8bc9961..2d3a4eb1c21 100644 --- a/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php +++ b/tests/Problem/Serializer/ConstraintViolationNormalizerTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Serializer; @@ -30,6 +31,7 @@ */ class ConstraintViolationNormalizerTest extends TestCase { + use ExpectDeprecationTrait; use ProphecyTrait; /** @@ -52,10 +54,13 @@ public function testSupportNormalization(): void } /** + * @group legacy + * * @dataProvider nameConverterProvider */ public function testNormalize(callable $nameConverterFactory, array $expected): void { + $this->expectDeprecation('Since api-platform 3.2: "ApiPlatform\Serializer\AbstractConstraintViolationListNormalizer::ApiPlatform\Serializer\AbstractConstraintViolationListNormalizer::getMessagesAndViolations" will be removed in 4.0, use "ApiPlatform\Serializer\AbstractConstraintViolationListNormalizer::getViolations'); $normalizer = new ConstraintViolationListNormalizer(['severity', 'anotherField1'], $nameConverterFactory($this)); // Note : we use NotNull constraint and not Constraint class because Constraint is abstract