Skip to content

Commit 3f6f255

Browse files
committed
feature #51348 [FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs (Yonel Ceruto)
This PR was squashed before being merged into the 6.4 branch. Discussion ---------- [FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | symfony/symfony-docs#18744 Alternative to symfony/symfony#51233 Inspiration: symfony/symfony#51012 Currently, you can determine the sequence of groups to apply dynamically based on the state of your DTO by implementing the `GroupSequenceProviderInterface` in your DTO class. https://symfony.com/doc/current/validation/sequence_provider.html#group-sequence-providers ```php use Symfony\Component\Validator\GroupSequenceProviderInterface; #[Assert\GroupSequenceProvider] class UserDto implements GroupSequenceProviderInterface { // ... public function getGroupSequence(): array|GroupSequence { if ($this->isCompanyType()) { return ['User', 'Company']; } return ['User']; } } ``` It covers most of the common scenarios, but for more advanced ones, it may not be sufficient. Suppose now you need to provide the sequence of groups from an external configuration (or service) which can change its value dynamically: ```php #[Assert\GroupSequenceProvider] class UserDto implements GroupSequenceProviderInterface { // ... public __constructor(private readonly ConfigService $config) { } public function getGroupSequence(): array|GroupSequence { if ($this->config->isEnabled()) { return ['User', $this->config->getGroup()]; } return ['User']; } } ``` This issue cannot be resolved at present without managing the DTO initialization and manually setting its dependencies. On the other hand, since the state of the DTO is not used at all, the implementation of the `GroupSequenceProviderInterface` becomes less fitting to the DTO responsibility. Further, stricter programming may raise a complaint about a violation of SOLID principles here. So, the proposal of this PR is to allow configuring the validation groups provider outside of the DTO, while simultaneously enabling the registration of this provider as a service if necessary. To achieve this, you'll need to implement a new `GroupProviderInterface` in a separate class, and configure it using the new `provider` option within the `GroupSequenceProvider` attribute: ```php #[Assert\GroupSequenceProvider(provider: UserGroupProvider::class)] class UserDto { // ... } class UserGroupProvider implements GroupProviderInterface { public __constructor(private readonly ConfigService $config) { } public function getGroups(object $object): array|GroupSequence { if ($this->config->isEnabled()) { return ['User', $this->config->getGroup()]; } return ['User']; } } ``` That's all you'll need to do if autowiring is enabled under your custom provider. Otherwise, you can manually tag your service with `validator.group_provider` to collect it and utilize it as a provider service during the validation process. In conclusion, no more messing with the DTO structure, just use the new `class` option for more advanced use cases. --- TODO: - [x] Add tests - [x] Create doc PR Commits ------- a3a089a15b8 [FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs
2 parents 6ebe8fc + 2e06c3c commit 3f6f255

File tree

4 files changed

+51
-42
lines changed

4 files changed

+51
-42
lines changed

DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class UnusedTagsPass implements CompilerPassInterface
101101
'twig.runtime',
102102
'validator.auto_mapper',
103103
'validator.constraint_validator',
104+
'validator.group_provider',
104105
'validator.initializer',
105106
'workflow',
106107
];

DependencyInjection/FrameworkExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@
177177
use Symfony\Component\Uid\UuidV4;
178178
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
179179
use Symfony\Component\Validator\ConstraintValidatorInterface;
180+
use Symfony\Component\Validator\GroupProviderInterface;
180181
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
181182
use Symfony\Component\Validator\ObjectInitializerInterface;
182183
use Symfony\Component\Validator\Validation;
@@ -657,6 +658,8 @@ public function load(array $configs, ContainerBuilder $container)
657658
->addTag('serializer.normalizer');
658659
$container->registerForAutoconfiguration(ConstraintValidatorInterface::class)
659660
->addTag('validator.constraint_validator');
661+
$container->registerForAutoconfiguration(GroupProviderInterface::class)
662+
->addTag('validator.group_provider');
660663
$container->registerForAutoconfiguration(ObjectInitializerInterface::class)
661664
->addTag('validator.initializer');
662665
$container->registerForAutoconfiguration(MessageHandlerInterface::class)

Resources/config/validator.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
->call('setConstraintValidatorFactory', [
4343
service('validator.validator_factory'),
4444
])
45+
->call('setGroupProviderLocator', [
46+
tagged_locator('validator.group_provider'),
47+
])
4548
->call('setTranslator', [
4649
service('translator')->ignoreOnInvalid(),
4750
])

Tests/DependencyInjection/FrameworkExtensionTestCase.php

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,16 +1230,18 @@ public function testValidation()
12301230

12311231
$annotations = !class_exists(FullStack::class);
12321232

1233-
$this->assertCount($annotations ? 7 : 6, $calls);
1233+
$this->assertCount($annotations ? 8 : 7, $calls);
12341234
$this->assertSame('setConstraintValidatorFactory', $calls[0][0]);
12351235
$this->assertEquals([new Reference('validator.validator_factory')], $calls[0][1]);
1236-
$this->assertSame('setTranslator', $calls[1][0]);
1237-
$this->assertEquals([new Reference('translator', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE)], $calls[1][1]);
1238-
$this->assertSame('setTranslationDomain', $calls[2][0]);
1239-
$this->assertSame(['%validator.translation_domain%'], $calls[2][1]);
1240-
$this->assertSame('addXmlMappings', $calls[3][0]);
1241-
$this->assertSame([$xmlMappings], $calls[3][1]);
1242-
$i = 3;
1236+
$this->assertSame('setGroupProviderLocator', $calls[1][0]);
1237+
$this->assertInstanceOf(ServiceLocatorArgument::class, $calls[1][1][0]);
1238+
$this->assertSame('setTranslator', $calls[2][0]);
1239+
$this->assertEquals([new Reference('translator', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE)], $calls[2][1]);
1240+
$this->assertSame('setTranslationDomain', $calls[3][0]);
1241+
$this->assertSame(['%validator.translation_domain%'], $calls[3][1]);
1242+
$this->assertSame('addXmlMappings', $calls[4][0]);
1243+
$this->assertSame([$xmlMappings], $calls[4][1]);
1244+
$i = 4;
12431245
if ($annotations) {
12441246
$this->assertSame('enableAttributeMapping', $calls[++$i][0]);
12451247
}
@@ -1288,12 +1290,12 @@ public function testValidationAttributes()
12881290

12891291
$calls = $container->getDefinition('validator.builder')->getMethodCalls();
12901292

1291-
$this->assertCount(7, $calls);
1292-
$this->assertSame('enableAttributeMapping', $calls[4][0]);
1293-
$this->assertSame('addMethodMapping', $calls[5][0]);
1294-
$this->assertSame(['loadValidatorMetadata'], $calls[5][1]);
1295-
$this->assertSame('setMappingCache', $calls[6][0]);
1296-
$this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[6][1]);
1293+
$this->assertCount(8, $calls);
1294+
$this->assertSame('enableAttributeMapping', $calls[5][0]);
1295+
$this->assertSame('addMethodMapping', $calls[6][0]);
1296+
$this->assertSame(['loadValidatorMetadata'], $calls[6][1]);
1297+
$this->assertSame('setMappingCache', $calls[7][0]);
1298+
$this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[7][1]);
12971299
// no cache this time
12981300
}
12991301

@@ -1308,14 +1310,14 @@ public function testValidationLegacyAnnotations()
13081310

13091311
$calls = $container->getDefinition('validator.builder')->getMethodCalls();
13101312

1311-
$this->assertCount(8, $calls);
1312-
$this->assertSame('enableAttributeMapping', $calls[4][0]);
1313+
$this->assertCount(9, $calls);
1314+
$this->assertSame('enableAttributeMapping', $calls[5][0]);
13131315
if (method_exists(ValidatorBuilder::class, 'setDoctrineAnnotationReader')) {
1314-
$this->assertSame('setDoctrineAnnotationReader', $calls[5][0]);
1315-
$this->assertEquals([new Reference('annotation_reader')], $calls[5][1]);
1316-
$i = 6;
1316+
$this->assertSame('setDoctrineAnnotationReader', $calls[6][0]);
1317+
$this->assertEquals([new Reference('annotation_reader')], $calls[6][1]);
1318+
$i = 7;
13171319
} else {
1318-
$i = 5;
1320+
$i = 6;
13191321
}
13201322
$this->assertSame('addMethodMapping', $calls[$i][0]);
13211323
$this->assertSame(['loadValidatorMetadata'], $calls[$i][1]);
@@ -1335,16 +1337,16 @@ public function testValidationPaths()
13351337

13361338
$calls = $container->getDefinition('validator.builder')->getMethodCalls();
13371339

1338-
$this->assertCount(8, $calls);
1339-
$this->assertSame('addXmlMappings', $calls[3][0]);
1340-
$this->assertSame('addYamlMappings', $calls[4][0]);
1341-
$this->assertSame('enableAttributeMapping', $calls[5][0]);
1342-
$this->assertSame('addMethodMapping', $calls[6][0]);
1343-
$this->assertSame(['loadValidatorMetadata'], $calls[6][1]);
1344-
$this->assertSame('setMappingCache', $calls[7][0]);
1345-
$this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[7][1]);
1340+
$this->assertCount(9, $calls);
1341+
$this->assertSame('addXmlMappings', $calls[4][0]);
1342+
$this->assertSame('addYamlMappings', $calls[5][0]);
1343+
$this->assertSame('enableAttributeMapping', $calls[6][0]);
1344+
$this->assertSame('addMethodMapping', $calls[7][0]);
1345+
$this->assertSame(['loadValidatorMetadata'], $calls[7][1]);
1346+
$this->assertSame('setMappingCache', $calls[8][0]);
1347+
$this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[8][1]);
13461348

1347-
$xmlMappings = $calls[3][1][0];
1349+
$xmlMappings = $calls[4][1][0];
13481350
$this->assertCount(3, $xmlMappings);
13491351
try {
13501352
// Testing symfony/symfony
@@ -1355,7 +1357,7 @@ public function testValidationPaths()
13551357
}
13561358
$this->assertStringEndsWith('TestBundle/Resources/config/validation.xml', $xmlMappings[1]);
13571359

1358-
$yamlMappings = $calls[4][1][0];
1360+
$yamlMappings = $calls[5][1][0];
13591361
$this->assertCount(1, $yamlMappings);
13601362
$this->assertStringEndsWith('TestBundle/Resources/config/validation.yml', $yamlMappings[0]);
13611363
}
@@ -1370,7 +1372,7 @@ public function testValidationPathsUsingCustomBundlePath()
13701372
]);
13711373

13721374
$calls = $container->getDefinition('validator.builder')->getMethodCalls();
1373-
$xmlMappings = $calls[3][1][0];
1375+
$xmlMappings = $calls[4][1][0];
13741376
$this->assertCount(3, $xmlMappings);
13751377

13761378
try {
@@ -1382,7 +1384,7 @@ public function testValidationPathsUsingCustomBundlePath()
13821384
}
13831385
$this->assertStringEndsWith('CustomPathBundle/Resources/config/validation.xml', $xmlMappings[1]);
13841386

1385-
$yamlMappings = $calls[4][1][0];
1387+
$yamlMappings = $calls[5][1][0];
13861388
$this->assertCount(1, $yamlMappings);
13871389
$this->assertStringEndsWith('CustomPathBundle/Resources/config/validation.yml', $yamlMappings[0]);
13881390
}
@@ -1395,9 +1397,9 @@ public function testValidationNoStaticMethod()
13951397

13961398
$annotations = !class_exists(FullStack::class);
13971399

1398-
$this->assertCount($annotations ? 6 : 5, $calls);
1399-
$this->assertSame('addXmlMappings', $calls[3][0]);
1400-
$i = 3;
1400+
$this->assertCount($annotations ? 7 : 6, $calls);
1401+
$this->assertSame('addXmlMappings', $calls[4][0]);
1402+
$i = 4;
14011403
if ($annotations) {
14021404
$this->assertSame('enableAttributeMapping', $calls[++$i][0]);
14031405
}
@@ -1426,14 +1428,14 @@ public function testValidationMapping()
14261428

14271429
$calls = $container->getDefinition('validator.builder')->getMethodCalls();
14281430

1429-
$this->assertSame('addXmlMappings', $calls[3][0]);
1430-
$this->assertCount(3, $calls[3][1][0]);
1431-
1432-
$this->assertSame('addYamlMappings', $calls[4][0]);
1431+
$this->assertSame('addXmlMappings', $calls[4][0]);
14331432
$this->assertCount(3, $calls[4][1][0]);
1434-
$this->assertStringContainsString('foo.yml', $calls[4][1][0][0]);
1435-
$this->assertStringContainsString('validation.yml', $calls[4][1][0][1]);
1436-
$this->assertStringContainsString('validation.yaml', $calls[4][1][0][2]);
1433+
1434+
$this->assertSame('addYamlMappings', $calls[5][0]);
1435+
$this->assertCount(3, $calls[5][1][0]);
1436+
$this->assertStringContainsString('foo.yml', $calls[5][1][0][0]);
1437+
$this->assertStringContainsString('validation.yml', $calls[5][1][0][1]);
1438+
$this->assertStringContainsString('validation.yaml', $calls[5][1][0][2]);
14371439
}
14381440

14391441
public function testValidationAutoMapping()

0 commit comments

Comments
 (0)