From a3a089a15b867e297aeb4ebabe6e8fafc5bc1a79 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Thu, 10 Aug 2023 15:53:58 -0400 Subject: [PATCH] [FrameworkBundle][Validator] Allow implementing validation groups provider outside DTOs --- .../Compiler/UnusedTagsPass.php | 1 + .../FrameworkExtension.php | 3 + .../Resources/config/validator.php | 3 + .../FrameworkExtensionTestCase.php | 86 ++++++++++--------- src/Symfony/Component/Validator/CHANGELOG.md | 1 + .../Constraints/GroupSequenceProvider.php | 9 ++ .../Validator/GroupProviderInterface.php | 30 +++++++ .../Validator/Mapping/ClassMetadata.php | 21 ++++- .../Mapping/ClassMetadataInterface.php | 2 + .../Mapping/Loader/AnnotationLoader.php | 1 + .../Mapping/Loader/XmlFileLoader.php | 1 + .../Mapping/Loader/YamlFileLoader.php | 3 + .../constraint-mapping-1.0.xsd | 41 +++++---- .../Constraints/GroupSequenceProviderTest.php | 26 ++++++ .../Tests/Dummy/DummyGroupProvider.php | 23 +++++ .../Fixtures/Attribute/GroupProviderDto.php | 22 +++++ .../Mapping/Loader/XmlFileLoaderTest.php | 16 ++++ .../Mapping/Loader/YamlFileLoaderTest.php | 16 ++++ .../Mapping/Loader/constraint-mapping.xml | 6 ++ .../Mapping/Loader/constraint-mapping.yml | 3 + .../Validator/RecursiveValidatorTest.php | 28 +++++- .../RecursiveContextualValidator.php | 21 +++-- .../Validator/RecursiveValidator.php | 11 ++- .../Component/Validator/ValidatorBuilder.php | 14 ++- 24 files changed, 316 insertions(+), 72 deletions(-) create mode 100644 src/Symfony/Component/Validator/GroupProviderInterface.php create mode 100644 src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceProviderTest.php create mode 100644 src/Symfony/Component/Validator/Tests/Dummy/DummyGroupProvider.php create mode 100644 src/Symfony/Component/Validator/Tests/Fixtures/Attribute/GroupProviderDto.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 047d30265fc74..5f975f8681495 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -101,6 +101,7 @@ class UnusedTagsPass implements CompilerPassInterface 'twig.runtime', 'validator.auto_mapper', 'validator.constraint_validator', + 'validator.group_provider', 'validator.initializer', 'workflow', ]; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 7fbef54888b2d..69f3c3bd3cdc4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -177,6 +177,7 @@ use Symfony\Component\Uid\UuidV4; use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider; use Symfony\Component\Validator\ConstraintValidatorInterface; +use Symfony\Component\Validator\GroupProviderInterface; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; use Symfony\Component\Validator\ObjectInitializerInterface; use Symfony\Component\Validator\Validation; @@ -657,6 +658,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('serializer.normalizer'); $container->registerForAutoconfiguration(ConstraintValidatorInterface::class) ->addTag('validator.constraint_validator'); + $container->registerForAutoconfiguration(GroupProviderInterface::class) + ->addTag('validator.group_provider'); $container->registerForAutoconfiguration(ObjectInitializerInterface::class) ->addTag('validator.initializer'); $container->registerForAutoconfiguration(MessageHandlerInterface::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php index 54a5d09cfc161..adde2de238e05 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.php @@ -42,6 +42,9 @@ ->call('setConstraintValidatorFactory', [ service('validator.validator_factory'), ]) + ->call('setGroupProviderLocator', [ + tagged_locator('validator.group_provider'), + ]) ->call('setTranslator', [ service('translator')->ignoreOnInvalid(), ]) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index 8a26d72ea5b70..11d441bed5438 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -1230,16 +1230,18 @@ public function testValidation() $annotations = !class_exists(FullStack::class); - $this->assertCount($annotations ? 7 : 6, $calls); + $this->assertCount($annotations ? 8 : 7, $calls); $this->assertSame('setConstraintValidatorFactory', $calls[0][0]); $this->assertEquals([new Reference('validator.validator_factory')], $calls[0][1]); - $this->assertSame('setTranslator', $calls[1][0]); - $this->assertEquals([new Reference('translator', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE)], $calls[1][1]); - $this->assertSame('setTranslationDomain', $calls[2][0]); - $this->assertSame(['%validator.translation_domain%'], $calls[2][1]); - $this->assertSame('addXmlMappings', $calls[3][0]); - $this->assertSame([$xmlMappings], $calls[3][1]); - $i = 3; + $this->assertSame('setGroupProviderLocator', $calls[1][0]); + $this->assertInstanceOf(ServiceLocatorArgument::class, $calls[1][1][0]); + $this->assertSame('setTranslator', $calls[2][0]); + $this->assertEquals([new Reference('translator', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE)], $calls[2][1]); + $this->assertSame('setTranslationDomain', $calls[3][0]); + $this->assertSame(['%validator.translation_domain%'], $calls[3][1]); + $this->assertSame('addXmlMappings', $calls[4][0]); + $this->assertSame([$xmlMappings], $calls[4][1]); + $i = 4; if ($annotations) { $this->assertSame('enableAttributeMapping', $calls[++$i][0]); } @@ -1288,12 +1290,12 @@ public function testValidationAttributes() $calls = $container->getDefinition('validator.builder')->getMethodCalls(); - $this->assertCount(7, $calls); - $this->assertSame('enableAttributeMapping', $calls[4][0]); - $this->assertSame('addMethodMapping', $calls[5][0]); - $this->assertSame(['loadValidatorMetadata'], $calls[5][1]); - $this->assertSame('setMappingCache', $calls[6][0]); - $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[6][1]); + $this->assertCount(8, $calls); + $this->assertSame('enableAttributeMapping', $calls[5][0]); + $this->assertSame('addMethodMapping', $calls[6][0]); + $this->assertSame(['loadValidatorMetadata'], $calls[6][1]); + $this->assertSame('setMappingCache', $calls[7][0]); + $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[7][1]); // no cache this time } @@ -1308,14 +1310,14 @@ public function testValidationLegacyAnnotations() $calls = $container->getDefinition('validator.builder')->getMethodCalls(); - $this->assertCount(8, $calls); - $this->assertSame('enableAttributeMapping', $calls[4][0]); + $this->assertCount(9, $calls); + $this->assertSame('enableAttributeMapping', $calls[5][0]); if (method_exists(ValidatorBuilder::class, 'setDoctrineAnnotationReader')) { - $this->assertSame('setDoctrineAnnotationReader', $calls[5][0]); - $this->assertEquals([new Reference('annotation_reader')], $calls[5][1]); - $i = 6; + $this->assertSame('setDoctrineAnnotationReader', $calls[6][0]); + $this->assertEquals([new Reference('annotation_reader')], $calls[6][1]); + $i = 7; } else { - $i = 5; + $i = 6; } $this->assertSame('addMethodMapping', $calls[$i][0]); $this->assertSame(['loadValidatorMetadata'], $calls[$i][1]); @@ -1335,16 +1337,16 @@ public function testValidationPaths() $calls = $container->getDefinition('validator.builder')->getMethodCalls(); - $this->assertCount(8, $calls); - $this->assertSame('addXmlMappings', $calls[3][0]); - $this->assertSame('addYamlMappings', $calls[4][0]); - $this->assertSame('enableAttributeMapping', $calls[5][0]); - $this->assertSame('addMethodMapping', $calls[6][0]); - $this->assertSame(['loadValidatorMetadata'], $calls[6][1]); - $this->assertSame('setMappingCache', $calls[7][0]); - $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[7][1]); + $this->assertCount(9, $calls); + $this->assertSame('addXmlMappings', $calls[4][0]); + $this->assertSame('addYamlMappings', $calls[5][0]); + $this->assertSame('enableAttributeMapping', $calls[6][0]); + $this->assertSame('addMethodMapping', $calls[7][0]); + $this->assertSame(['loadValidatorMetadata'], $calls[7][1]); + $this->assertSame('setMappingCache', $calls[8][0]); + $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[8][1]); - $xmlMappings = $calls[3][1][0]; + $xmlMappings = $calls[4][1][0]; $this->assertCount(3, $xmlMappings); try { // Testing symfony/symfony @@ -1355,7 +1357,7 @@ public function testValidationPaths() } $this->assertStringEndsWith('TestBundle/Resources/config/validation.xml', $xmlMappings[1]); - $yamlMappings = $calls[4][1][0]; + $yamlMappings = $calls[5][1][0]; $this->assertCount(1, $yamlMappings); $this->assertStringEndsWith('TestBundle/Resources/config/validation.yml', $yamlMappings[0]); } @@ -1370,7 +1372,7 @@ public function testValidationPathsUsingCustomBundlePath() ]); $calls = $container->getDefinition('validator.builder')->getMethodCalls(); - $xmlMappings = $calls[3][1][0]; + $xmlMappings = $calls[4][1][0]; $this->assertCount(3, $xmlMappings); try { @@ -1382,7 +1384,7 @@ public function testValidationPathsUsingCustomBundlePath() } $this->assertStringEndsWith('CustomPathBundle/Resources/config/validation.xml', $xmlMappings[1]); - $yamlMappings = $calls[4][1][0]; + $yamlMappings = $calls[5][1][0]; $this->assertCount(1, $yamlMappings); $this->assertStringEndsWith('CustomPathBundle/Resources/config/validation.yml', $yamlMappings[0]); } @@ -1395,9 +1397,9 @@ public function testValidationNoStaticMethod() $annotations = !class_exists(FullStack::class); - $this->assertCount($annotations ? 6 : 5, $calls); - $this->assertSame('addXmlMappings', $calls[3][0]); - $i = 3; + $this->assertCount($annotations ? 7 : 6, $calls); + $this->assertSame('addXmlMappings', $calls[4][0]); + $i = 4; if ($annotations) { $this->assertSame('enableAttributeMapping', $calls[++$i][0]); } @@ -1426,14 +1428,14 @@ public function testValidationMapping() $calls = $container->getDefinition('validator.builder')->getMethodCalls(); - $this->assertSame('addXmlMappings', $calls[3][0]); - $this->assertCount(3, $calls[3][1][0]); - - $this->assertSame('addYamlMappings', $calls[4][0]); + $this->assertSame('addXmlMappings', $calls[4][0]); $this->assertCount(3, $calls[4][1][0]); - $this->assertStringContainsString('foo.yml', $calls[4][1][0][0]); - $this->assertStringContainsString('validation.yml', $calls[4][1][0][1]); - $this->assertStringContainsString('validation.yaml', $calls[4][1][0][2]); + + $this->assertSame('addYamlMappings', $calls[5][0]); + $this->assertCount(3, $calls[5][1][0]); + $this->assertStringContainsString('foo.yml', $calls[5][1][0][0]); + $this->assertStringContainsString('validation.yml', $calls[5][1][0][1]); + $this->assertStringContainsString('validation.yaml', $calls[5][1][0][2]); } public function testValidationAutoMapping() diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index addca159de6a3..5660c2f4e4dfb 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -15,6 +15,7 @@ CHANGELOG * Deprecate `ValidatorBuilder::enableAnnotationMapping()`, use `ValidatorBuilder::enableAttributeMapping()` instead * Deprecate `ValidatorBuilder::disableAnnotationMapping()`, use `ValidatorBuilder::disableAttributeMapping()` instead * Deprecate `AnnotationLoader`, use `AttributeLoader` instead + * Add `GroupProviderInterface` to implement validation group providers outside the underlying class 6.3 --- diff --git a/src/Symfony/Component/Validator/Constraints/GroupSequenceProvider.php b/src/Symfony/Component/Validator/Constraints/GroupSequenceProvider.php index 9b125470fa47a..24da9acfd21f1 100644 --- a/src/Symfony/Component/Validator/Constraints/GroupSequenceProvider.php +++ b/src/Symfony/Component/Validator/Constraints/GroupSequenceProvider.php @@ -11,11 +11,16 @@ namespace Symfony\Component\Validator\Constraints; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; +use Symfony\Component\Validator\Attribute\HasNamedArguments; + /** * Attribute to define a group sequence provider. * * @Annotation * + * @NamedArgumentConstructor + * * @Target({"CLASS", "ANNOTATION"}) * * @author Bernhard Schussek @@ -23,4 +28,8 @@ #[\Attribute(\Attribute::TARGET_CLASS)] class GroupSequenceProvider { + #[HasNamedArguments] + public function __construct(public ?string $provider = null) + { + } } diff --git a/src/Symfony/Component/Validator/GroupProviderInterface.php b/src/Symfony/Component/Validator/GroupProviderInterface.php new file mode 100644 index 0000000000000..7651ca2233e3b --- /dev/null +++ b/src/Symfony/Component/Validator/GroupProviderInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator; + +use Symfony\Component\Validator\Constraints\GroupSequence; + +/** + * Defines the interface for a validation group provider. + * + * @author Yonel Ceruto + */ +interface GroupProviderInterface +{ + /** + * Returns which validation groups should be used for a certain state + * of the object. + * + * @return string[]|string[][]|GroupSequence + */ + public function getGroups(object $object): array|GroupSequence; +} diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php index 7450137b20abb..3a6c7181b9e1c 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadata.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadata.php @@ -86,6 +86,13 @@ class ClassMetadata extends GenericMetadata implements ClassMetadataInterface */ public bool $groupSequenceProvider = false; + /** + * @internal This property is public in order to reduce the size of the + * class' serialized representation. Do not access it. Use + * {@link getGroupProvider()} instead. + */ + public ?string $groupProvider = null; + /** * The strategy for traversing traversable objects. * @@ -123,6 +130,7 @@ public function __sleep(): array 'getters', 'groupSequence', 'groupSequenceProvider', + 'groupProvider', 'members', 'name', 'properties', @@ -319,6 +327,7 @@ public function addGetterMethodConstraints(string $property, string $method, arr public function mergeConstraints(self $source) { if ($source->isGroupSequenceProvider()) { + $this->setGroupProvider($source->getGroupProvider()); $this->setGroupSequenceProvider(true); } @@ -432,7 +441,7 @@ public function setGroupSequenceProvider(bool $active) throw new GroupDefinitionException('Defining a group sequence provider is not allowed with a static group sequence.'); } - if (!$this->getReflectionClass()->implementsInterface(GroupSequenceProviderInterface::class)) { + if (null === $this->groupProvider && !$this->getReflectionClass()->implementsInterface(GroupSequenceProviderInterface::class)) { throw new GroupDefinitionException(sprintf('Class "%s" must implement GroupSequenceProviderInterface.', $this->name)); } @@ -444,6 +453,16 @@ public function isGroupSequenceProvider(): bool return $this->groupSequenceProvider; } + public function setGroupProvider(?string $provider): void + { + $this->groupProvider = $provider; + } + + public function getGroupProvider(): ?string + { + return $this->groupProvider; + } + public function getCascadingStrategy(): int { return $this->cascadingStrategy; diff --git a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php index 32ccca4d384a0..6625e37e6056a 100644 --- a/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php +++ b/src/Symfony/Component/Validator/Mapping/ClassMetadataInterface.php @@ -30,6 +30,8 @@ * @see GroupSequence * @see GroupSequenceProviderInterface * @see TraversalStrategy + * + * @method string|null getGroupProvider() */ interface ClassMetadataInterface extends MetadataInterface { diff --git a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php index 910ff6ab39443..ebc1795f2cb58 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/AnnotationLoader.php @@ -51,6 +51,7 @@ public function loadClassMetadata(ClassMetadata $metadata): bool if ($constraint instanceof GroupSequence) { $metadata->setGroupSequence($constraint->groups); } elseif ($constraint instanceof GroupSequenceProvider) { + $metadata->setGroupProvider($constraint->provider); $metadata->setGroupSequenceProvider(true); } elseif ($constraint instanceof Constraint) { $metadata->addConstraint($constraint); diff --git a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php index bf36b15f3712f..94d3f071e5a77 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/XmlFileLoader.php @@ -201,6 +201,7 @@ private function loadClassesFromXml(): void private function loadClassMetadataFromXml(ClassMetadata $metadata, \SimpleXMLElement $classDescription): void { if (\count($classDescription->{'group-sequence-provider'}) > 0) { + $metadata->setGroupProvider($classDescription->{'group-sequence-provider'}[0]->value ?: null); $metadata->setGroupSequenceProvider(true); } diff --git a/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php index aa30b9542b8d6..e610b45427313 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/Validator/Mapping/Loader/YamlFileLoader.php @@ -150,6 +150,9 @@ private function loadClassesFromYaml(): void private function loadClassMetadataFromYaml(ClassMetadata $metadata, array $classDescription): void { if (isset($classDescription['group_sequence_provider'])) { + if (\is_string($classDescription['group_sequence_provider'])) { + $metadata->setGroupProvider($classDescription['group_sequence_provider']); + } $metadata->setGroupSequenceProvider( (bool) $classDescription['group_sequence_provider'] ); diff --git a/src/Symfony/Component/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd b/src/Symfony/Component/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd index 1ca840b05db2e..a744e50b94add 100644 --- a/src/Symfony/Component/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd +++ b/src/Symfony/Component/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd @@ -14,9 +14,9 @@ validation constraints. ]]> - + - + - + - + @@ -72,15 +72,18 @@ - + + + + - + - + - + @@ -122,15 +125,15 @@ - + - + @@ -139,14 +142,14 @@ - + - + @@ -155,6 +158,6 @@ - + diff --git a/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceProviderTest.php b/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceProviderTest.php new file mode 100644 index 0000000000000..3b52419dd3737 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Constraints/GroupSequenceProviderTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Constraints; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Constraints\GroupSequenceProvider; +use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider; + +class GroupSequenceProviderTest extends TestCase +{ + public function testCreateAttributeStyle() + { + $sequence = new GroupSequenceProvider(provider: DummyGroupProvider::class); + + $this->assertSame(DummyGroupProvider::class, $sequence->provider); + } +} diff --git a/src/Symfony/Component/Validator/Tests/Dummy/DummyGroupProvider.php b/src/Symfony/Component/Validator/Tests/Dummy/DummyGroupProvider.php new file mode 100644 index 0000000000000..ec76bf5ebc7f9 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Dummy/DummyGroupProvider.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Dummy; + +use Symfony\Component\Validator\Constraints\GroupSequence; +use Symfony\Component\Validator\GroupProviderInterface; + +class DummyGroupProvider implements GroupProviderInterface +{ + public function getGroups(object $object): array|GroupSequence + { + return ['foo', 'bar']; + } +} diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/Attribute/GroupProviderDto.php b/src/Symfony/Component/Validator/Tests/Fixtures/Attribute/GroupProviderDto.php new file mode 100644 index 0000000000000..fdb60c8d76f56 --- /dev/null +++ b/src/Symfony/Component/Validator/Tests/Fixtures/Attribute/GroupProviderDto.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Tests\Fixtures\Attribute; + +use Symfony\Component\Validator\Constraints as Assert; +use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider; + +#[Assert\GroupSequenceProvider(provider: DummyGroupProvider::class)] +class GroupProviderDto +{ + public string $firstName = ''; + public string $lastName = ''; +} diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php index 48533e6e4311d..5ba519ab195c5 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -24,6 +24,8 @@ use Symfony\Component\Validator\Exception\MappingException; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader; +use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider; +use Symfony\Component\Validator\Tests\Fixtures\Attribute\GroupProviderDto; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithRequiredArgument; @@ -126,6 +128,20 @@ public function testLoadGroupSequenceProvider() $this->assertEquals($expected, $metadata); } + public function testLoadGroupProvider() + { + $loader = new XmlFileLoader(__DIR__.'/constraint-mapping.xml'); + $metadata = new ClassMetadata(GroupProviderDto::class); + + $loader->loadClassMetadata($metadata); + + $expected = new ClassMetadata(GroupProviderDto::class); + $expected->setGroupProvider(DummyGroupProvider::class); + $expected->setGroupSequenceProvider(true); + + $this->assertEquals($expected, $metadata); + } + public function testThrowExceptionIfDocTypeIsSet() { $loader = new XmlFileLoader(__DIR__.'/withdoctype.xml'); diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php index 960fb244516d8..a5c983939bcb2 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -21,6 +21,8 @@ use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; +use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider; +use Symfony\Component\Validator\Tests\Fixtures\Attribute\GroupProviderDto; use Symfony\Component\Validator\Tests\Fixtures\ConstraintA; use Symfony\Component\Validator\Tests\Fixtures\ConstraintB; use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithRequiredArgument; @@ -165,4 +167,18 @@ public function testLoadGroupSequenceProvider() $this->assertEquals($expected, $metadata); } + + public function testLoadGroupProvider() + { + $loader = new YamlFileLoader(__DIR__.'/constraint-mapping.yml'); + $metadata = new ClassMetadata(GroupProviderDto::class); + + $loader->loadClassMetadata($metadata); + + $expected = new ClassMetadata(GroupProviderDto::class); + $expected->setGroupProvider(DummyGroupProvider::class); + $expected->setGroupSequenceProvider(true); + + $this->assertEquals($expected, $metadata); + } } diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml index 0b949554025df..6183b074a65cb 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.xml @@ -121,4 +121,10 @@ + + + + Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider + + diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml index 4d2a694c3de86..0cf87cffe0a69 100644 --- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml +++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/constraint-mapping.yml @@ -60,3 +60,6 @@ Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\Entity: Symfony\Component\Validator\Tests\Fixtures\NestedAttribute\GroupSequenceProviderEntity: group_sequence_provider: true + +Symfony\Component\Validator\Tests\Fixtures\Attribute\GroupProviderDto: + group_sequence_provider: Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider diff --git a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php index 4ade8b44da860..cffbaa5fbeca5 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/RecursiveValidatorTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Tests\Validator; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; use Symfony\Component\Translation\IdentityTranslator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\All; @@ -44,6 +45,8 @@ use Symfony\Component\Validator\ObjectInitializerInterface; use Symfony\Component\Validator\Tests\Constraints\Fixtures\ChildA; use Symfony\Component\Validator\Tests\Constraints\Fixtures\ChildB; +use Symfony\Component\Validator\Tests\Dummy\DummyGroupProvider; +use Symfony\Component\Validator\Tests\Fixtures\Attribute\GroupProviderDto; use Symfony\Component\Validator\Tests\Fixtures\CascadedChild; use Symfony\Component\Validator\Tests\Fixtures\CascadingEntity; use Symfony\Component\Validator\Tests\Fixtures\EntityWithGroupedConstraintOnMethods; @@ -57,6 +60,7 @@ use Symfony\Component\Validator\Validator\LazyProperty; use Symfony\Component\Validator\Validator\RecursiveValidator; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Service\ServiceLocatorTrait; class RecursiveValidatorTest extends TestCase { @@ -1262,6 +1266,22 @@ public function testReplaceDefaultGroup($sequence, array $assertViolations) } } + public function testGroupProvider() + { + $dto = new GroupProviderDto(); + + $metadata = new ClassMetadata($dto::class); + $metadata->addPropertyConstraint('firstName', new NotBlank(groups: ['foo'])); + $metadata->addPropertyConstraint('lastName', new NotBlank(groups: ['foo'])); + $metadata->setGroupProvider(DummyGroupProvider::class); + $metadata->setGroupSequenceProvider(true); + $this->metadataFactory->addMetadata($metadata); + + $violations = $this->validate($dto, null, 'Default'); + + $this->assertCount(2, $violations); + } + public static function getConstraintMethods() { return [ @@ -2040,8 +2060,14 @@ protected function createValidator(MetadataFactoryInterface $metadataFactory, ar $contextFactory = new ExecutionContextFactory($translator); $validatorFactory = new ConstraintValidatorFactory(); + $factories = [ + DummyGroupProvider::class => static fn () => new DummyGroupProvider(), + ]; + $groupProviderLocator = new class($factories) implements ContainerInterface { + use ServiceLocatorTrait; + }; - return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory, $objectInitializers); + return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory, $objectInitializers, $groupProviderLocator); } public function testEmptyGroupsArrayDoesNotTriggerDeprecation() diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 156140f1910ba..30f9f6e92f6bf 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Validator; +use Psr\Container\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\Composite; use Symfony\Component\Validator\Constraints\Existence; @@ -50,13 +51,14 @@ class RecursiveContextualValidator implements ContextualValidatorInterface private MetadataFactoryInterface $metadataFactory; private ConstraintValidatorFactoryInterface $validatorFactory; private array $objectInitializers; + private ?ContainerInterface $groupProviderLocator; /** * Creates a validator for the given context. * * @param ObjectInitializerInterface[] $objectInitializers The object initializers */ - public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = []) + public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = [], ContainerInterface $groupProviderLocator = null) { $this->context = $context; $this->defaultPropertyPath = $context->getPropertyPath(); @@ -64,6 +66,7 @@ public function __construct(ExecutionContextInterface $context, MetadataFactoryI $this->metadataFactory = $metadataFactory; $this->validatorFactory = $validatorFactory; $this->objectInitializers = $objectInitializers; + $this->groupProviderLocator = $groupProviderLocator; } public function atPath(string $path): static @@ -436,10 +439,18 @@ private function validateClassNode(object $object, ?string $cacheKey, ClassMetad $group = $metadata->getGroupSequence(); $defaultOverridden = true; } elseif ($metadata->isGroupSequenceProvider()) { - // The group sequence is dynamically obtained from the validated - // object - /* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */ - $group = $object->getGroupSequence(); + if (null !== $provider = $metadata->getGroupProvider()) { + if (null === $this->groupProviderLocator) { + throw new \LogicException('A group provider locator is required when using group provider.'); + } + + $group = $this->groupProviderLocator->get($provider)->getGroups($object); + } else { + // The group sequence is dynamically obtained from the validated + // object + /* @var \Symfony\Component\Validator\GroupSequenceProviderInterface $object */ + $group = $object->getGroupSequence(); + } $defaultOverridden = true; if (!$group instanceof GroupSequence) { diff --git a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php index a6897c85a735d..8736e2b2ae6d9 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveValidator.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Validator; +use Psr\Container\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraints\GroupSequence; use Symfony\Component\Validator\ConstraintValidatorFactoryInterface; @@ -32,18 +33,20 @@ class RecursiveValidator implements ValidatorInterface protected $metadataFactory; protected $validatorFactory; protected $objectInitializers; + protected ?ContainerInterface $groupProviderLocator; /** * Creates a new validator. * * @param ObjectInitializerInterface[] $objectInitializers The object initializers */ - public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = []) + public function __construct(ExecutionContextFactoryInterface $contextFactory, MetadataFactoryInterface $metadataFactory, ConstraintValidatorFactoryInterface $validatorFactory, array $objectInitializers = [], ContainerInterface $groupProviderLocator = null) { $this->contextFactory = $contextFactory; $this->metadataFactory = $metadataFactory; $this->validatorFactory = $validatorFactory; $this->objectInitializers = $objectInitializers; + $this->groupProviderLocator = $groupProviderLocator; } public function startContext(mixed $root = null): ContextualValidatorInterface @@ -52,7 +55,8 @@ public function startContext(mixed $root = null): ContextualValidatorInterface $this->contextFactory->createContext($this, $root), $this->metadataFactory, $this->validatorFactory, - $this->objectInitializers + $this->objectInitializers, + $this->groupProviderLocator, ); } @@ -62,7 +66,8 @@ public function inContext(ExecutionContextInterface $context): ContextualValidat $context, $this->metadataFactory, $this->validatorFactory, - $this->objectInitializers + $this->objectInitializers, + $this->groupProviderLocator, ); } diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 0123056aeb608..f24aa285c7c39 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -15,6 +15,7 @@ use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use Psr\Cache\CacheItemPoolInterface; +use Psr\Container\ContainerInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\Exception\LogicException; @@ -53,6 +54,7 @@ class ValidatorBuilder private bool $enableAttributeMapping = false; private ?MetadataFactoryInterface $metadataFactory = null; private ConstraintValidatorFactoryInterface $validatorFactory; + private ?ContainerInterface $groupProviderLocator = null; private ?CacheItemPoolInterface $mappingCache = null; private ?TranslatorInterface $translator = null; private ?string $translationDomain = null; @@ -312,6 +314,16 @@ public function setConstraintValidatorFactory(ConstraintValidatorFactoryInterfac return $this; } + /** + * @return $this + */ + public function setGroupProviderLocator(ContainerInterface $groupProviderLocator): static + { + $this->groupProviderLocator = $groupProviderLocator; + + return $this; + } + /** * Sets the translator used for translating violation messages. * @@ -414,7 +426,7 @@ public function getValidator(): ValidatorInterface $contextFactory = new ExecutionContextFactory($translator, $this->translationDomain); - return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory, $this->initializers); + return new RecursiveValidator($contextFactory, $metadataFactory, $validatorFactory, $this->initializers, $this->groupProviderLocator); } private function createAnnotationReader(): Reader 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