Skip to content

Commit bc996cc

Browse files
[Validator] Add support of nested attributes
1 parent d46125a commit bc996cc

File tree

10 files changed

+278
-0
lines changed

10 files changed

+278
-0
lines changed

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)
26+
{
27+
parent::__construct($constraints ?? []);
28+
}
29+
2430
public function getDefaultOption()
2531
{
2632
return 'constraints';

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

Lines changed: 6 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,11 @@ 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)
35+
{
36+
parent::__construct($constraints ?? []);
37+
}
38+
3339
public function getDefaultOption()
3440
{
3541
return 'constraints';

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

Lines changed: 1 addition & 0 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';

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
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 = [];

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class Entity extends EntityParent implements EntityInterfaceB
3232
* "bar" = @Assert\Range(min=5)
3333
* })
3434
* @Assert\Choice(choices={"A", "B"}, message="Must be one of %choices%")
35+
* @Assert\AtLeastOneOf({@Assert\NotNull, @Assert\Range(min=3)})
36+
* @Assert\Sequentially({@Assert\NotBlank, @Assert\Range(min=5)})
3537
*/
3638
public $firstName;
3739
/**

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class Entity extends EntityParent implements EntityInterfaceB
3232
* "bar" = @Assert\Range(min=5)
3333
* })
3434
* @Assert\Choice(choices={"A", "B"}, message="Must be one of %choices%")
35+
* @Assert\AtLeastOneOf({@Assert\NotNull, @Assert\Range(min=3)})
36+
* @Assert\Sequentially({@Assert\NotBlank, @Assert\Range(min=5)})
3537
*/
3638
#[
3739
Assert\NotNull,
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
]
49+
]),
50+
Assert\Choice(choices: ['A', 'B'], message: 'Must be one of %choices%'),
51+
Assert\AtLeastOneOf(
52+
constraints: [
53+
new Assert\NotNull(),
54+
new Assert\Range(min: 3),
55+
]
56+
),
57+
Assert\Sequentially([
58+
new Assert\NotBlank(),
59+
new Assert\Range(min: 5),
60+
]),
61+
]
62+
public $firstName;
63+
#[Assert\Valid]
64+
public $childA;
65+
#[Assert\Valid]
66+
public $childB;
67+
protected $lastName;
68+
public $reference;
69+
public $reference2;
70+
private $internal;
71+
public $data = 'Overridden data';
72+
public $initialized = false;
73+
74+
public function __construct($internal = null)
75+
{
76+
$this->internal = $internal;
77+
}
78+
79+
public function getFirstName()
80+
{
81+
return $this->firstName;
82+
}
83+
84+
public function getInternal()
85+
{
86+
return $this->internal.' from getter';
87+
}
88+
89+
public function setLastName($lastName)
90+
{
91+
$this->lastName = $lastName;
92+
}
93+
94+
#[Assert\NotNull]
95+
public function getLastName()
96+
{
97+
return $this->lastName;
98+
}
99+
100+
public function getValid()
101+
{
102+
}
103+
104+
#[Assert\IsTrue]
105+
public function isValid()
106+
{
107+
return 'valid';
108+
}
109+
110+
#[Assert\IsTrue]
111+
public function hasPermissions()
112+
{
113+
return 'permissions';
114+
}
115+
116+
public function getData()
117+
{
118+
return 'Overridden data';
119+
}
120+
121+
#[Assert\Callback(payload: 'foo')]
122+
public function validateMe(ExecutionContextInterface $context)
123+
{
124+
}
125+
126+
#[Assert\Callback]
127+
public static function validateMeStatic($object, ExecutionContextInterface $context)
128+
{
129+
}
130+
131+
/**
132+
* @return mixed
133+
*/
134+
public function getChildA()
135+
{
136+
return $this->childA;
137+
}
138+
139+
/**
140+
* @param mixed $childA
141+
*/
142+
public function setChildA($childA)
143+
{
144+
$this->childA = $childA;
145+
}
146+
147+
/**
148+
* @return mixed
149+
*/
150+
public function getChildB()
151+
{
152+
return $this->childB;
153+
}
154+
155+
/**
156+
* @param mixed $childB
157+
*/
158+
public function setChildB($childB)
159+
{
160+
$this->childB = $childB;
161+
}
162+
163+
public function getReference()
164+
{
165+
return $this->reference;
166+
}
167+
}
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+
}

src/Symfony/Component/Validator/Tests/Mapping/Loader/AnnotationLoaderTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
use Doctrine\Common\Annotations\AnnotationReader;
1515
use PHPUnit\Framework\TestCase;
1616
use Symfony\Component\Validator\Constraints\All;
17+
use Symfony\Component\Validator\Constraints\AtLeastOneOf;
1718
use Symfony\Component\Validator\Constraints\Callback;
1819
use Symfony\Component\Validator\Constraints\Choice;
1920
use Symfony\Component\Validator\Constraints\Collection;
2021
use Symfony\Component\Validator\Constraints\IsTrue;
22+
use Symfony\Component\Validator\Constraints\NotBlank;
2123
use Symfony\Component\Validator\Constraints\NotNull;
2224
use Symfony\Component\Validator\Constraints\Range;
25+
use Symfony\Component\Validator\Constraints\Sequentially;
2326
use Symfony\Component\Validator\Constraints\Valid;
2427
use Symfony\Component\Validator\Mapping\ClassMetadata;
2528
use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader;
@@ -73,6 +76,14 @@ public function testLoadClassMetadata(string $namespace)
7376
'message' => 'Must be one of %choices%',
7477
'choices' => ['A', 'B'],
7578
]));
79+
$expected->addPropertyConstraint('firstName', new AtLeastOneOf([
80+
new NotNull(),
81+
new Range(['min' => 3]),
82+
]));
83+
$expected->addPropertyConstraint('firstName', new Sequentially([
84+
new NotBlank(),
85+
new Range(['min' => 5]),
86+
]));
7687
$expected->addPropertyConstraint('childA', new Valid());
7788
$expected->addPropertyConstraint('childB', new Valid());
7889
$expected->addGetterConstraint('lastName', new NotNull());
@@ -149,6 +160,14 @@ public function testLoadClassMetadataAndMerge(string $namespace)
149160
'message' => 'Must be one of %choices%',
150161
'choices' => ['A', 'B'],
151162
]));
163+
$expected->addPropertyConstraint('firstName', new AtLeastOneOf([
164+
new NotNull(),
165+
new Range(['min' => 3]),
166+
]));
167+
$expected->addPropertyConstraint('firstName', new Sequentially([
168+
new NotBlank(),
169+
new Range(['min' => 5]),
170+
]));
152171
$expected->addPropertyConstraint('childA', new Valid());
153172
$expected->addPropertyConstraint('childB', new Valid());
154173
$expected->addGetterConstraint('lastName', new NotNull());
@@ -185,5 +204,9 @@ public function provideNamespaces(): iterable
185204
if (\PHP_VERSION_ID >= 80000) {
186205
yield 'attributes' => ['Symfony\Component\Validator\Tests\Fixtures\Attribute'];
187206
}
207+
208+
if (\PHP_VERSION_ID >= 80100) {
209+
yield 'nested_attributes' => ['Symfony\Component\Validator\Tests\Fixtures\NestedAttribute'];
210+
}
188211
}
189212
}

0 commit comments

Comments
 (0)