8000 feature #41994 [Validator] Add support of nested attributes (alexandr… · symfony/symfony@edecf96 · GitHub
[go: up one dir, main page]

Skip to content

Commit edecf96

Browse files
committed
feature #41994 [Validator] Add support of nested attributes (alexandre-daubois)
This PR was merged into the 5.4 branch. Discussion ---------- [Validator] Add support of nested attributes | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | Fix #38503 | License | MIT | Doc PR | symfony/symfony-docs#15541 Although the RFC (https://wiki.php.net/rfc/new_in_initializers) is in the voting phase (until 14 July), it is already well on its way to passing. Based on `@nikic`'s development (php/php-src#7153), this makes the development of support possible. It will obviously take a little while before this pull request is merged. If this pull request is OK for you, I'll get to work on writing the existing documentation for the attribute validation constraints. ![Capture d’écran du 2021-07-05 17-11-23](https://user-images.githubusercontent.com/2144837/124491886-0d2f7d80-ddb4-11eb-8147-493bdc6c48ac.png) Not sure about the Symfony version to target, as `AtLeastOneOf` has been introduced in 5.1. Although, I couldn't find attributes validation documentation for 4.4. Commits ------- 1449450 [Validator] Add support of nested attributes for composite constraints
2 parents ab2ba3a + 1449450 commit edecf96

File tree

12 files changed

+325
-16
lines changed

12 files changed

+325
-16
lines changed

.github/patch-types.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
case false !== strpos($file, '/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php'):
3636
case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Fixtures/'):
3737
case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'):
38+
case false !== strpos($file, '/src/Symfony/Component/Validator/Tests/Fixtures/NestedAttribute/Entity.php'):
3839
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'):
3940
case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/ReflectionIntersectionTypeFixture.php'):
4041
continue 2;

src/Symfony/Component/Validator/Constraints/All.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@
1717
*
1818
* @author Bernhard Schussek <bschussek@gmail.com>
1919
*/
20+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2021
class All extends Composite
2122
{
2223
public $constraints = [];
2324

25+
public function __construct($constraints = null, array $groups = null, $payload = null)
26+
{
27+
parent::__construct($constraints ?? [], $groups, $payload);
28+
}
29+
2430
public function getDefaultOption()
2531
{
2632
return 'constraints';

src/Symfony/Component/Validator/Constraints/AtLeastOneOf.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*
1818
* @author Przemysław Bogusz <przemyslaw.bogusz@tubotax.pl>
1919
*/
20+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2021
class AtLeastOneOf extends Composite
2122
{
2223
public const AT_LEAST_ONE_OF_ERROR = 'f27e6d6c-261a-4056-b391-6673a623531c';
@@ -30,6 +31,15 @@ class AtLeastOneOf extends Composite
3031
public $messageCollection = 'Each element of this collection should satisfy its own set of constraints.';
3132
public $includeInternalMessages = true;
3233

34+
public function __construct($constraints = null, array $groups = null, $payload = null, string $message = null, string $messageCollection = null, bool $includeInternalMessages = null)
35+
{
36+
parent::__construct($constraints ?? [], $groups, $payload);
37+
38+
$this->message = $message ?? $this->message;
39+
$this->messageCollection = $messageCollection ?? $this->messageCollection;
40+
$this->includeInternalMessages = $includeInternalMessages ?? $this->includeInternalMessages;
41+
}
42+
3343
public function getDefaultOption()
3444
{
3545
return 'constraints';

src/Symfony/Component/Validator/Constraints/Collection.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*
2020
* @author Bernhard Schussek <bschussek@gmail.com>
2121
*/
22+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2223
class Collection extends Composite
2324
{
2425
public const MISSING_FIELD_ERROR = '2fa2158c-2a7f-484b-98aa-975522539ff8';
@@ -38,15 +39,20 @@ class Collection extends Composite
3839
/**
3940
* {@inheritdoc}
4041
*/
41-
public function __construct($options = null)
42+
public function __construct($fields = null, array $groups = null, $payload = null, bool $allowExtraFields = null, bool $allowMissingFields = null, string $extraFieldsMessage = null, string $missingFieldsMessage = null)
4243
{
43-
// no known options set? $options is the fields array
44-
if (\is_array($options)
45-
&& !array_intersect(array_keys($options), ['groups', 'fields', 'allowExtraFields', 'allowMissingFields', 'extraFieldsMessage', 'missingFieldsMessage'])) {
46-
$options = ['fields' => $options];
44+
// no known options set? $fields is the fields array
45+
if (\is_array($fields)
46+
&& !array_intersect(array_keys($fields), ['groups', 'fields', 'allowExtraFields', 'allowMissingFields', 'extraFieldsMessage', 'missingFieldsMessage'])) {
47+
$fields = ['fields' => $fields];
4748
}
4849

49-
parent::__construct($options);
50+
parent::__construct($fields, $groups, $payload);
51+
52+
$this->allowExtraFields = $allowExtraFields ?? $this->allowExtraFields;
53+
$this->allowMissingFields = $allowMissingFields ?? $this->allowMissingFields;
54+
$this->extraFieldsMessage = $extraFieldsMessage ?? $this->extraFieldsMessage;
55+
$this->missingFieldsMessage = $missingFieldsMessage ?? $this->missingFieldsMessage;
5056
}
5157

5258
/**

src/Symfony/Component/Validator/Constraints/Composite.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ abstract class Composite extends Constraint
5151
* cached. When constraints are loaded from the cache, no more group
5252
* checks need to be done.
5353
*/
54-
public function __construct($options = null)
54+
public function __construct($options = null, array $groups = null, $payload = null)
5555
{
56-
parent::__construct($options);
56+
parent::__construct($options, $groups, $payload);
5757

5858
$this->initializeNestedConstraints();
5959

src/Symfony/Component/Validator/Constraints/Sequentially.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@
2020
*
2121
* @author Maxime Steinhausser <maxime.steinhausser@gmail.com>
2222
*/
23+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
2324
class Sequentially extends Composite
2425
{
2526
public $constraints = [];
2627

28+
public function __construct($constraints = null, array $groups = null, $payload = null)
29+
{
30+
parent::__construct($constraints ?? [], $groups, $payload);
31+
}
32+
2733
public function getDefaultOption()
2834
{
2935
return 'constraints';

src/Symfony/Component/Validator/Tests/Fixtures/Annotation/Entity.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ class Entity extends EntityParent implements EntityInterfaceB
2929
* @Assert\All(constraints={@Assert\NotNull, @Assert\Range(min=3)})
3030
* @Assert\Collection(fields={
3131
* "foo" = {@Assert\NotNull, @Assert\Range(min=3)},
32-
* "bar" = @Assert\Range(min=5)
33-
* })
32+
* "bar" = @Assert\Range(min=5),
33+
* "baz" = @Assert\Required({@Assert\Email()}),
34+
* "qux" = @Assert\Optional({@Assert\NotBlank()})
35+
* }, allowExtraFields=true)
3436
* @Assert\Choice(choices={"A", "B"}, message="Must be one of %choices%")
37+
* @Assert\AtLeastOneOf({@Assert\NotNull, @Assert\Range(min=3)}, message="foo", includeInternalMessages=false)
38+
* @Assert\Sequentially({@Assert\NotBlank, @Assert\Range(min=5)})
3539
*/
3640
public $firstName;
3741
/**

src/Symfony/Component/Validator/Tests/Fixtures/Attribute/Entity.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ class Entity extends EntityParent implements EntityInterfaceB
2929
* @Assert\All(constraints={@Assert\NotNull, @Assert\Range(min=3)})
3030
* @Assert\Collection(fields={
3131
* "foo" = {@Assert\NotNull, @Assert\Range(min=3)},
32-
* "bar" = @Assert\Range(min=5)
33-
* })
32+
* "bar" = @Assert\Range(min=5),
33+
* "baz" = @Assert\Required({@Assert\Email()}),
34+
* "qux" = @Assert\Optional({@Assert\NotBlank()})
35+
* }, allowExtraFields=true)
3436
* @Assert\Choice(choices={"A", "B"}, message="Must be one of %choices%")
37+
* @Assert\AtLeastOneOf({@Assert\NotNull, @Assert\Range(min=3)}, message="foo", includeInternalMessages=false)
38+
* @Assert\Sequentially({@Assert\NotBlank, @Assert\Range(min=5)})
3539
*/
3640
#[
3741
Assert\NotNull,
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
namespace Symfony\Component\Validator\Tests\Fixtures\NestedAttribute;
13+
14+
use Symfony\Component\Validator\Constraints as Assert;
15+
use Symfony\Component\Validator\Context\ExecutionContextInterface;
16+
use Symfony\Component\Validator\Tests\Fixtures\Attribute\EntityParent;
17+
use Symfony\Component\Validator\Tests\Fixtures\EntityInterfaceB;
18+
use Symfony\Component\Validator\Tests\Fixtures\CallbackClass;
19+
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA 10000 ;
20+
21+
#[
22+
ConstraintA,
23+
Assert\GroupSequence(['Foo', 'Entity']),
24+
Assert\Callback([CallbackClass::class, 'callback']),
25+
]
26+
class Entity extends EntityParent implements EntityInterfaceB
27+
{
28+
#[
29+
Assert\NotNull,
30+
Assert\Range(min: 3),
31+
Assert\All([
32+
new Assert\NotNull(),
33+
new Assert\Range(min: 3),
34+
]),
35+
Assert\All(
36+
constraints: [
37+
new Assert\NotNull(),
38+
new Assert\Range(min: 3),
39+
],
40+
),
41+
Assert\Collection(
42+
fields: [
43+
'foo' => [
44+
new Assert\NotNull(),
45+
new Assert\Range(min: 3),
46+
],
47+
'bar' => new Assert\Range(min: 5),
48+
'baz' => new Assert\Required([new Assert\Email()]),
49+
'qux' => new Assert\Optional([new Assert\NotBlank()]),
50+
],
51+
allowExtraFields: true
52+
),
53+
Assert\Choice(choices: ['A', 'B'], message: 'Must be one of %choices%'),
54+
Assert\AtLeastOneOf(
55+
constraints: [
56+
new Assert\NotNull(),
57+
new Assert\Range(min: 3),
58+
],
59+
message: 'foo',
60+
includeInternalMessages: false,
61+
),
62+
Assert\Sequentially([
63+
new Assert\NotBlank(),
64+
new Assert\Range(min: 5),
65+
]),
66+
]
67+
public $firstName;
68+
#[Assert\Valid]
69+
public $childA;
70+
#[Assert\Valid]
71+
public $childB;
72+
protected $lastName;
73+
public $reference;
74+
public $reference2;
75+
private $internal;
76+
public $data = 'Overridden data';
77+
public $initialized = false;
78+
79+
public function __construct($internal = null)
80+
{
81+
$this->internal = $internal;
82+
}
83+
84+
public function getFirstName()
85+
{
86+
return $this->firstName;
87+
}
88+
89+
public function getInternal()
90+
{
91+
return $this->internal.' from getter';
92+
}
93+
94+
public function setLastName($lastName)
95+
{
96+
$this->lastName = $lastName;
97+
}
98+
99+
#[Assert\NotNull]
100+
public function getLastName()
101+
{
102+
return $this->lastName;
103+
}
104+
105+
public function getValid()
106+
{
107+
}
108+
109+
#[Assert\IsTrue]
110+
public function isValid()
111+
{
112+
return 'valid';
113+
}
114+
115+
#[Assert\IsTrue]
116+
public function hasPermissions()
117+
{
118+
return 'permissions';
119+
}
120+
121+
public function getData()
122+
{
123+
return 'Overridden data';
124+
}
125+
126+
#[Assert\Callback(payload: 'foo')]
127+
public function validateMe(ExecutionContextInterface $context)
128+
{
129+
}
130+
131+
#[Assert\Callback]
132+
public static function validateMeStatic($object, ExecutionContextInterface $context)
133+
{
134+
}
135+
136+
/**
137+
* @return mixed
138+
*/
139+
public function getChildA()
140+
{
141+
return $this->childA;
142+
}
143+
144+
/**
145+
* @param mixed $childA
146+
*/
147+
public function setChildA($childA)
148+
{
149+
$this->childA = $childA;
150+
}
151+
152+
/**
153+
* @return mixed
154+
*/
155+
public function getChildB()
156+
{
157+
return $this->childB;
158+
}
159+
160+
/**
161+
* @param mixed $childB
162+
*/
163+
public function setChildB($childB)
164+
{
165+
$this->childB = $childB;
166+
}
167+
168+
public function getReference()
169+
{
170+
return $this->reference;
171+
}
172+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
namespace Symfony\Component\Validator\Tests\Fixtures\NestedAttribute;
13+
14+
use Symfony\Component\Validator\Constraints\NotNull;
15+
use Symfony\Component\Validator\Tests\Fixtures\EntityInterfaceA;
16+
17+
class EntityParent implements EntityInterfaceA
18+
{
19+
protected $firstName;
20+
private $internal;
21+
private $data = 'Data';
22+
private $child;
23+
24+
#[NotNull]
25+
protected $other;
26+
27+
public function getData()
28+
{
29+
return 'Data';
30+
}
31+
32+
public function getChild()
33+
{
34+
return $this->child;
35+
}
36+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
namespace Symfony\Component\Validator\Tests\Fixtures\NestedAttribute;
13+
14+
use Symfony\Component\Validator\Constraints as Assert;
15+
use Symfony\Component\Validator\GroupSequenceProviderInterface;
16+
17+
#[Assert\GroupSequenceProvider]
18+
class GroupSequenceProviderEntity implements GroupSequenceProviderInterface
19+
{
20+
public $firstName;
21+
public $lastName;
22+
23+
protected $sequence = [];
24+
25+
public function __construct($sequence)
26+
{
27+
$this->sequence = $sequence;
28+
}
29+
30+
public function getGroupSequence()
31+
{
32+
return $this->sequence;
33+
}
34+
}

0 commit comments

Comments
 (0)
0