Skip to content

Commit 0fb9e1f

Browse files
ycerutofabpot
authored andcommitted
[FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs
1 parent ffa004c commit 0fb9e1f

20 files changed

+265
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ CHANGELOG
1515
* Deprecate `ValidatorBuilder::enableAnnotationMapping()`, use `ValidatorBuilder::enableAttributeMapping()` instead
1616
* Deprecate `ValidatorBuilder::disableAnnotationMapping()`, use `ValidatorBuilder::disableAttributeMapping()` instead
1717
* Deprecate `AnnotationLoader`, use `AttributeLoader` instead
18+
* Add `GroupProviderInterface` to implement validation group providers outside the underlying class
1819

1920
6.3
2021
---

Constraints/GroupSequenceProvider.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,25 @@
1111

1212
namespace Symfony\Component\Validator\Constraints;
1313

14+
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
15+
use Symfony\Component\Validator\Attribute\HasNamedArguments;
16+
1417
/**
1518
* Attribute to define a group sequence provider.
1619
*
1720
* @Annotation
1821
*
22+
* @NamedArgumentConstructor
23+
*
1924
* @Target({"CLASS", "ANNOTATION"})
2025
*
2126
* @author Bernhard Schussek <[email protected]>
2227
*/
2328
#[\Attribute(\Attribute::TARGET_CLASS)]
2429
class GroupSequenceProvider
2530
{
31+
#[HasNamedArguments]
32+
public function __construct(public ?string $provider = null)
33+
{
34+
}
2635
}

GroupProviderInterface.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Component\Validator;
13+
14+
use Symfony\Component\Validator\Constraints\GroupSequence;
15+
16+
/**
17+
* Defines the interface for a validation group provider.
18+
*
19+
* @author Yonel Ceruto <[email protected]>
20+
*/
21+
interface GroupProviderInterface
22+
{
23+
/**
24+
* Returns which validation groups should be used for a certain state
25+
* of the object.
26+
*
27+
* @return string[]|string[][]|GroupSequence
28+
*/
29+
public function getGroups(object $object): array|GroupSequence;
30+
}

Mapping/ClassMetadata.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface
8686
*/
8787
public bool $groupSequenceProvider = false;
8888

89+
/**
90+
* @internal This property is public in order to reduce the size of the
91+
* class' serialized representation. Do not access it. Use
92+
* {@link getGroupProvider()} instead.
93+
*/
94+
public ?string $groupProvider = null;
95+
8996
/**
9097
* The strategy for traversing traversable objects.
9198
*
@@ -123,6 +130,7 @@ public function __sleep(): array
123130
'getters',
124131
'groupSequence',
125132
'groupSequenceProvider',
133+
'groupProvider',
126134
'members',
127135
'name',
128136
'properties',
@@ -319,6 +327,7 @@ public function addGetterMethodConstraints(string $property, string $method, arr
319327
public function mergeConstraints(self $source)
320328
{
321329
if ($source->isGroupSequenceProvider()) {
330+
$this->setGroupProvider($source->getGroupProvider());
322331
$this->setGroupSequenceProvider(true);
323332
}
324333

@@ -432,7 +441,7 @@ public function setGroupSequenceProvider(bool $active)
432441
throw new GroupDefinitionException('Defining a group sequence provider is not allowed with a static group sequence.');
433442
}
434443

435-
if (!$this->getReflectionClass()->implementsInterface(GroupSequenceProviderInterface::class)) {
444+
if (null === $this->groupProvider && !$this->getReflectionClass()->implementsInterface(GroupSequenceProviderInterface::class)) {
436445
throw new GroupDefinitionException(sprintf('Class "%s" must implement GroupSequenceProviderInterface.', $this->name));
437446
}
438447

@@ -444,6 +453,16 @@ public function isGroupSequenceProvider(): bool
444453
return $this->groupSequenceProvider;
445454
}
446455

456+
public function setGroupProvider(?string $provider): void
457+
{
458+
$this->groupProvider = $provider;
459+
}
460+
461+
public function getGroupProvider(): ?string
462+
{
463+
return $this->groupProvider;
464+
}
465+
447466
public function getCascadingStrategy(): int
448467
{
449468
return $this->cascadingStrategy;

Mapping/ClassMetadataInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
* @see GroupSequence
3131
* @see GroupSequenceProviderInterface
3232
* @see TraversalStrategy
33+
*
34+
* @method string|null getGroupProvider()
3335
*/
3436
interface ClassMetadataInterface extends MetadataInterface
3537
{

Mapping/Loader/AnnotationLoader.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public function loadClassMetadata(ClassMetadata $metadata): bool
5151
if ($constraint instanceof GroupSequence) {
5252
$metadata->setGroupSequence($constraint->groups);
5353
} elseif ($constraint instanceof GroupSequenceProvider) {
54+
$metadata->setGroupProvider($constraint->provider);
5455
$metadata->setGroupSequenceProvider(true);
5556
} elseif ($constraint instanceof Constraint) {
5657
$metadata->addConstraint($constraint);

Mapping/Loader/XmlFileLoader.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ private function loadClassesFromXml(): void
201201
private function loadClassMetadataFromXml(ClassMetadata $metadata, \SimpleXMLElement $classDescription): void
202202
{
203203
if (\count($classDescription->{'group-sequence-provider'}) > 0) {
204+
$metadata->setGroupProvider($classDescription->{'group-sequence-provider'}[0]->value ?: null);
204205
$metadata->setGroupSequenceProvider(true);
205206
}
206207

Mapping/Loader/YamlFileLoader.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ private function loadClassesFromYaml(): void
150150
private function loadClassMetadataFromYaml(ClassMetadata $metadata, array $classDescription): void
151151
{
152152
if (isset($classDescription['group_sequence_provider'])) {
153+
if (\is_string($classDescription['group_sequence_provider'])) {
154+
$metadata->setGroupProvider($classDescription['group_sequence_provider']);
155+
}
153156
$metadata->setGroupSequenceProvider(
154157
(bool) $classDescription['group_sequence_provider']
155158
);

Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
validation constraints.
1515
]]></xsd:documentation>
1616
</xsd:annotation>
17-
17+
1818
<xsd:element name="constraint-mapping" type="constraint-mapping" />
19-
19+
2020
<xsd:complexType name="constraint-mapping">
2121
<xsd:annotation>
2222
<xsd:documentation><![CDATA[
@@ -28,7 +28,7 @@
2828
<xsd:element name="class" type="class" maxOccurs="unbounded" />
2929
</xsd:sequence>
3030
</xsd:complexType>
31-
31+
3232
<xsd:complexType name="namespace">
3333
<xsd:annotation>
3434
<xsd:documentation><![CDATA[
@@ -41,13 +41,13 @@
4141
</xsd:extension>
4242
</xsd:simpleContent>
4343
</xsd:complexType>
44-
44+
4545
<xsd:complexType name="class">
4646
<xsd:annotation>
4747
<xsd:documentation><![CDATA[
4848
Contains constraints for a single class.
49-
50-
Nested elements may be class constraints, property and/or getter
49+
50+
Nested elements may be class constraints, property and/or getter
5151
definitions.
5252
]]></xsd:documentation>
5353
</xsd:annotation>
@@ -72,15 +72,18 @@
7272
<xsd:element name="value" type="value" minOccurs="1" maxOccurs="unbounded" />
7373
</xsd:sequence>
7474
</xsd:complexType>
75-
75+
7676
<xsd:complexType name="group-sequence-provider">
7777
<xsd:annotation>
7878
<xsd:documentation><![CDATA[
7979
Defines the name of the group sequence provider for a class.
8080
]]></xsd:documentation>
8181
</xsd:annotation>
82+
<xsd:sequence>
83+
<xsd:element name="value" type="value" minOccurs="0" maxOccurs="unbounded" />
84+
</xsd:sequence>
8285
</xsd:complexType>
83-
86+
8487
<xsd:complexType name="property">
8588
<xsd:annotation>
8689
<xsd:documentation><![CDATA[
@@ -93,7 +96,7 @@
9396
</xsd:sequence>
9497
<xsd:attribute name="name" type="xsd:string" use="required" />
9598
</xsd:complexType>
96-
99+
97100
<xsd:complexType name="getter">
98101
<xsd:annotation>
99102
<xsd:documentation><![CDATA[
@@ -106,14 +109,14 @@
106109
</xsd:sequence>
107110
<xsd:attribute name="property" type="xsd:string" use="required" />
108111
</xsd:complexType>
109-
112+
110113
<xsd:complexType name="constraint" mixed="true">
111114
<xsd:annotation>
112115
<xsd:documentation><![CDATA[
113116
Contains a constraint definition. The name of the constraint should be
114117
given in the "name" option.
115-
116-
May contain a single value, multiple "constraint" elements,
118+
119+
May contain a single value, multiple "constraint" elements,
117120
multiple "value" elements or multiple "option" elements.
118121
]]></xsd:documentation>
119122
</xsd:annotation>
@@ -122,15 +125,15 @@
122125
<xsd:element name="option" type="option" minOccurs="1" maxOccurs="unbounded" />
123126
<xsd:element name="value" type="value" minOccurs="1" maxOccurs="unbounded" />
124127
</xsd:choice>
125-
<xsd:attribute name="name" type="xsd:string" use="required" />
128+
<xsd:attribute name="name" type="xsd:string" use="required" />
126129
</xsd:complexType>
127-
130+
128131
<xsd:complexType name="option" mixed="true">
129132
<xsd:annotation>
130133
<xsd:documentation><![CDATA[
131134
Contains a constraint option definition. The name of the option
132135
should be given in the "name" option.
133-
136+
134137
May contain a single value, multiple "value" elements or multiple
135138
"constraint" elements.
136139
]]></xsd:documentation>
@@ -139,14 +142,14 @@
139142
<xsd:element name="constraint" type="constraint" minOccurs="1" maxOccurs="unbounded" />
140143
<xsd:element name="value" type="value" minOccurs="1" maxOccurs="unbounded" />
141144
</xsd:choice>
142-
<xsd:attribute name="name" type="xsd:string" use="required" />
145+
<xsd:attribute name="name" type="xsd:string" use="required" />
143146
</xsd:complexType>
144-
147+
145148
<xsd:complexType name="value" mixed="true">
146149
<xsd:annotation>
147150
<xsd:documentation><![CDATA[
148151
A value of an element.
149-
152+
150153
May contain a single value, multiple "value" elements or multiple
151154
"constraint" elements.
152155
]]></xsd:documentation>
@@ -155,6 +158,6 @@
155158
<xsd:element name="constraint" type="constraint" minOccurs="1" maxOccurs="unbounded" />
156159
<xsd:element name="value" type="value" minOccurs="1" maxOccurs="unbounded" />
157160
</xsd:choice>
158-
<xsd:attribute name="key" type="xsd:string" use="optional" />
161+
<xsd:attribute name="key" type="xsd:string" use="optional" />
159162
</xsd:complexType>
160163
</xsd:schema>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Component\Validator\Tests\Constraints;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Validator\Constraints\GroupSequenceProvider;
16+
use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider;
17+
18+
class GroupSequenceProviderTest extends TestCase
19+
{
20+
public function testCreateAttributeStyle()
21+
{
22+
$sequence = new GroupSequenceProvider(provider: DummyGroupProvider::class);
23+
24+
$this->assertSame(DummyGroupProvider::class, $sequence->provider);
25+
}
26+
}

Tests/Dummy/DummyGroupProvider.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Component\Validator\Tests\Dummy;
13+
14+
use Symfony\Component\Validator\Constraints\GroupSequence;
15+
use Symfony\Component\Validator\GroupProviderInterface;
16+
17+
class DummyGroupProvider implements GroupProviderInterface
18+
{
19+
public function getGroups(object $object): array|GroupSequence
20+
{
21+
return ['foo', 'bar'];
22+
}
23+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Component\Validator\Tests\Fixtures\Attribute;
13+
14+
use Symfony\Component\Validator\Constraints as Assert;
15+
use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider;
16+
17+
#[Assert\GroupSequenceProvider(provider: DummyGroupProvider::class)]
18+
class GroupProviderDto
19+
{
20+
public string $firstName = '';
21+
public string $lastName = '';
22+
}

Tests/Mapping/Loader/XmlFileLoaderTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
use Symfony\Component\Validator\Exception\MappingException;
2525
use Symfony\Component\Validator\Mapping\ClassMetadata;
2626
use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader;
27+
use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider;
28+
use Symfony\Component\Validator\Tests\Fixtures\Attribute\GroupProviderDto;
2729
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA;
2830
use Symfony\Component\Validator\Tests\Fixtures\ConstraintB;
2931
use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithRequiredArgument;
@@ -126,6 +128,20 @@ public function testLoadGroupSequenceProvider()
126128
$this->assertEquals($expected, $metadata);
127129
}
128130

131+
public function testLoadGroupProvider()
132+
{
133+
$loader = new XmlFileLoader(__DIR__.'/constraint-mapping.xml');
134+
$metadata = new ClassMetadata(GroupProviderDto::class);
135+
136+
$loader->loadClassMetadata($metadata);
137+
138+
$expected = new ClassMetadata(GroupProviderDto::class);
139+
$expected->setGroupProvider(DummyGroupProvider::class);
140+
$expected->setGroupSequenceProvider(true);
141+
142+
$this->assertEquals($expected, $metadata);
143+
}
144+
129145
public function testThrowExceptionIfDocTypeIsSet()
130146
{
131147
$loader = new XmlFileLoader(__DIR__.'/withdoctype.xml');

0 commit comments

Comments
 (0)