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;
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+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy