Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d42f00c

Browse files
authored
fix(validation): normalize constraint violation list (#5866)
1 parent 2080936 commit d42f00c

11 files changed

Lines changed: 328 additions & 68 deletions

File tree

features/main/validation.feature

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,39 @@ Feature: Using validations groups
7373
"""
7474
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
7575

76+
@createSchema
77+
Scenario: Create a resource with serializedName property
78+
When I add "Content-Type" header equal to "application/ld+json"
79+
And I send a "POST" request to "dummy_validation_serialized_name" with body:
80+
"""
81+
{
82+
"code": "My Dummy"
83+
}
84+
"""
85+
Then the response status code should be 422
86+
And the response should be in JSON
87+
And the JSON should be equal to:
88+
"""
89+
{
90+
"@id": "/validation_errors/ad32d13f-c3d4-423b-909a-857b961eb720",
91+
"@type": "ConstraintViolationList",
92+
"status": 422,
93+
"violations": [
94+
{
95+
"propertyPath": "test",
96+
"message": "This value should not be null.",
97+
"code": "ad32d13f-c3d4-423b-909a-857b961eb720"
98+
}
99+
],
100+
"hydra:title": "An error occurred",
101+
"hydra:description": "title: This value should not be null.",
102+
"type": "/validation_errors/ad32d13f-c3d4-423b-909a-857b961eb720",
103+
"title": "An error occurred",
104+
"detail": "title: This value should not be null."
105+
}
106+
"""
107+
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
108+
76109
@!mongodb
77110
@createSchema
78111
Scenario: Create a resource with collectDenormalizationErrors
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Hal\Serializer;
15+
16+
use ApiPlatform\Serializer\AbstractConstraintViolationListNormalizer;
17+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
18+
19+
/**
20+
* Converts {@see \Symfony\Component\Validator\ConstraintViolationListInterface} to a Hal error representation.
21+
*/
22+
final class ConstraintViolationListNormalizer extends AbstractConstraintViolationListNormalizer
23+
{
24+
public const FORMAT = 'json';
25+
26+
public function __construct(array $serializePayloadFields = null, NameConverterInterface $nameConverter = null)
27+
{
28+
parent::__construct($serializePayloadFields, $nameConverter);
29+
}
30+
31+
/**
32+
* {@inheritdoc}
33+
*/
34+
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
35+
{
36+
return $this->getViolations($object);
37+
}
38+
}

src/Hydra/Serializer/ConstraintViolationListNormalizer.php

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ final class ConstraintViolationListNormalizer extends AbstractConstraintViolatio
2626
{
2727
public const FORMAT = 'jsonld';
2828

29-
public function __construct(private readonly UrlGeneratorInterface $urlGenerator, array $serializePayloadFields = null, NameConverterInterface $nameConverter = null)
29+
// @phpstan-ignore-next-line prevent BC break (can't remove this useless argument)
30+
public function __construct(private readonly ?UrlGeneratorInterface $urlGenerator = null, array $serializePayloadFields = null, NameConverterInterface $nameConverter = null)
3031
{
3132
parent::__construct($serializePayloadFields, $nameConverter);
3233
}
@@ -36,14 +37,6 @@ public function __construct(private readonly UrlGeneratorInterface $urlGenerator
3637
*/
3738
public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
3839
{
39-
[$messages, $violations] = $this->getMessagesAndViolations($object);
40-
41-
return [
42-
'@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'ConstraintViolationList']),
43-
'@type' => 'ConstraintViolationList',
44-
'hydra:title' => $context['title'] ?? 'An error occurred',
45-
'hydra:description' => $messages ? implode("\n", $messages) : (string) $object,
46-
'violations' => $violations,
47-
];
40+
return $this->getViolations($object);
4841
}
4942
}

src/Problem/Serializer/ConstraintViolationListNormalizer.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* Converts {@see \Symfony\Component\Validator\ConstraintViolationListInterface} the API Problem spec (RFC 7807).
2121
*
2222
* @see https://tools.ietf.org/html/rfc7807
23+
* @deprecated this is not used anymore internally and will be removed in 4.0
2324
*
2425
* @author Kévin Dunglas <[email protected]>
2526
*/

src/Serializer/AbstractConstraintViolationListNormalizer.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,54 @@ public function hasCacheableSupportsMethod(): bool
6565
return true;
6666
}
6767

68+
/**
69+
* return string[].
70+
*/
71+
protected function getViolations(ConstraintViolationListInterface $constraintViolationList): array
72+
{
73+
$violations = [];
74+
75+
foreach ($constraintViolationList as $violation) {
76+
$class = \is_object($root = $violation->getRoot()) ? $root::class : null;
77+
78+
if ($this->nameConverter instanceof AdvancedNameConverterInterface) {
79+
$propertyPath = $this->nameConverter->normalize($violation->getPropertyPath(), $class, static::FORMAT);
80+
} elseif ($this->nameConverter instanceof NameConverterInterface) {
81+
$propertyPath = $this->nameConverter->normalize($violation->getPropertyPath());
82+
} else {
83+
$propertyPath = $violation->getPropertyPath();
84+
}
85+
86+
$violationData = [
87+
'propertyPath' => $propertyPath,
88+
'message' => $violation->getMessage(),
89+
'code' => $violation->getCode(),
90+
];
91+
92+
if ($hint = $violation->getParameters()['hint'] ?? false) {
93+
$violationData['hint'] = $hint;
94+
}
95+
96+
$constraint = $violation instanceof ConstraintViolation ? $violation->getConstraint() : null;
97+
if (
98+
[] !== $this->serializePayloadFields
99+
&& $constraint
100+
&& $constraint->payload
101+
// If some fields are whitelisted, only them are added
102+
&& $payloadFields = null === $this->serializePayloadFields ? $constraint->payload : array_intersect_key($constraint->payload, $this->serializePayloadFields)
103+
) {
104+
$violationData['payload'] = $payloadFields;
105+
}
106+
107+
$violations[] = $violationData;
108+
}
109+
110+
return $violations;
111+
}
112+
68113
protected function getMessagesAndViolations(ConstraintViolationListInterface $constraintViolationList): array
69114
{
115+
trigger_deprecation('api-platform', '3.2', sprintf('"%s::%s" will be removed in 4.0, use "%1$s::%s', __CLASS__, __METHOD__, 'getViolations'));
70116
$violations = $messages = [];
71117

72118
foreach ($constraintViolationList as $violation) {

src/Symfony/Bundle/Resources/config/hal.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@
5757
<!-- Run after serializer.denormalizer.array but before serializer.normalizer.object -->
5858
<tag name="serializer.normalizer" priority="-995" />
5959
</service>
60+
61+
<service id="api_platform.hal.normalizer.constraint_violation_list" class="ApiPlatform\Hal\Serializer\ConstraintViolationListNormalizer" public="false">
62+
<argument>%api_platform.validator.serialize_payload_fields%</argument>
63+
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
64+
65+
<tag name="serializer.normalizer" priority="-780" />
66+
</service>
6067
</services>
6168

6269
</container>

src/Symfony/Validator/Exception/ValidationException.php

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,6 @@ public function __construct(private readonly ConstraintViolationListInterface $c
5959
parent::__construct($message ?: $this->__toString(), $code, $previous, $errorTitle);
6060
}
6161

62-
public function getConstraintViolationList(): ConstraintViolationListInterface
63-
{
64-
return $this->constraintViolationList;
65-
}
66-
6762
public function getId(): string
6863
{
6964
$ids = [];
@@ -148,21 +143,8 @@ public function getInstance(): ?string
148143

149144
#[SerializedName('violations')]
150145
#[Groups(['json', 'jsonld', 'legacy_jsonld', 'legacy_jsonproblem', 'legacy_json'])]
151-
public function getViolations(): iterable
146+
public function getConstraintViolationList(): ConstraintViolationListInterface
152147
{
153-
foreach ($this->getConstraintViolationList() as $violation) {
154-
$propertyPath = $violation->getPropertyPath();
155-
$violationData = [
156-
'propertyPath' => $propertyPath,
157-
'message' => $violation->getMessage(),
158-
'code' => $violation->getCode(),
159-
];
160-
161-
if ($hint = $violation->getParameters()['hint'] ?? false) {
162-
$violationData['hint'] = $hint;
163-
}
164-
165-
yield $violationData;
166-
}
148+
return $this->constraintViolationList;
167149
}
168150
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Fixtures\TestBundle\Document;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\GetCollection;
18+
use ApiPlatform\Metadata\Post;
19+
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
20+
use Symfony\Component\Serializer\Annotation\SerializedName;
21+
use Symfony\Component\Validator\Constraints as Assert;
22+
23+
#[ApiResource(operations: [
24+
new GetCollection(),
25+
new Post(uriTemplate: 'dummy_validation_serialized_name'),
26+
]
27+
)]
28+
#[ODM\Document]
29+
class DummyValidationSerializedName
30+
{
31+
/**
32+
* @var int|null The id
33+
*/
34+
#[ODM\Id(strategy: 'INCREMENT', type: 'int')]
35+
private ?int $id = null;
36+
/**
37+
* @var string|null The dummy title
38+
*/
39+
#[ODM\Field(type: 'string', nullable: true)]
40+
#[Assert\NotNull()]
41+
#[SerializedName('test')]
42+
private ?string $title = null;
43+
/**
44+
* @var string The dummy code
45+
*/
46+
#[ODM\Field(type: 'string')]
47+
private string $code;
48+
49+
public function getId(): ?int
50+
{
51+
return $this->id;
52+
}
53+
54+
public function setId(int $id): self
55+
{
56+
$this->id = $id;
57+
58+
return $this;
59+
}
60+
61+
public function getTitle(): ?string
62+
{
63+
return $this->title;
64+
}
65+
66+
public function setTitle(?string $title): self
67+
{
68+
$this->title = $title;
69+
70+
return $this;
71+
}
72+
73+
public function getCode(): ?string
74+
{
75+
return $this->code;
76+
}
77+
78+
public function setCode(string $code): self
79+
{
80+
$this->code = $code;
81+
82+
return $this;
83+
}
84+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\GetCollection;
18+
use ApiPlatform\Metadata\Post;
19+
use Doctrine\ORM\Mapping as ORM;
20+
use Symfony\Component\Serializer\Annotation\SerializedName;
21+
use Symfony\Component\Validator\Constraints as Assert;
22+
23+
#[ApiResource(operations: [
24+
new GetCollection(),
25+
new Post(uriTemplate: 'dummy_validation_serialized_name'),
26+
]
27+
)]
28+
#[ORM\Entity]
29+
class DummyValidationSerializedName
30+
{
31+
/**
32+
* @var int|null The id
33+
*/
34+
#[ORM\Column(type: 'integer')]
35+
#[ORM\Id]
36+
#[ORM\GeneratedValue(strategy: 'AUTO')]
37+
private ?int $id = null;
38+
/**
39+
* @var string|null The dummy title
40+
*/
41+
#[ORM\Column(nullable: true)]
42+
#[Assert\NotNull()]
43+
#[SerializedName('test')]
44+
private ?string $title = null;
45+
/**
46+
* @var string The dummy code
47+
*/
48+
#[ORM\Column]
49+
private string $code;
50+
51+
public function getId(): ?int
52+
{
53+
return $this->id;
54+
}
55+
56+
public function setId(int $id): self
57+
{
58+
$this->id = $id;
59+
60+
return $this;
61+
}
62+
63+
public function getTitle(): ?string
64+
{
65+
return $this->title;
66+
}
67+
68+
public function setTitle(?string $title): self
69+
{
70+
$this->title = $title;
71+
72+
return $this;
73+
}
74+
75+
public function getCode(): ?string
76+
{
77+
return $this->code;
78+
}
79+
80+
public function setCode(string $code): self
81+
{
82+
$this->code = $code;
83+
84+
return $this;
85+
}
86+
}

0 commit comments

Comments
 (0)