Skip to content

Commit 2a2b7ce

Browse files
[Validator] Allow using attributes to declare compile-time constraint metadata
1 parent 1c3432f commit 2a2b7ce

File tree

6 files changed

+67
-37
lines changed

6 files changed

+67
-37
lines changed

CacheWarmer/ValidatorCacheWarmer.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
use Symfony\Component\Cache\Adapter\ArrayAdapter;
1515
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
1616
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
17+
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
1718
use Symfony\Component\Validator\Mapping\Loader\LoaderChain;
1819
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
1920
use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader;
2021
use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader;
2122
use Symfony\Component\Validator\ValidatorBuilder;
2223

2324
/**
24-
* Warms up XML and YAML validator metadata.
25+
* Warms up validator metadata.
2526
*
2627
* @author Titouan Galopin <[email protected]>
2728
*
@@ -77,14 +78,14 @@ protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array
7778
/**
7879
* @param LoaderInterface[] $loaders
7980
*
80-
* @return XmlFileLoader[]|YamlFileLoader[]
81+
* @return list<XmlFileLoader|YamlFileLoader|AttributeLoader>
8182
*/
8283
private function extractSupportedLoaders(array $loaders): array
8384
{
8485
$supportedLoaders = [];
8586

8687
foreach ($loaders as $loader) {
87-
if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader) {
88+
if (method_exists($loader, 'getMappedClasses')) {
8889
$supportedLoaders[] = $loader;
8990
} elseif ($loader instanceof LoaderChain) {
9091
$supportedLoaders = array_merge($supportedLoaders, $this->extractSupportedLoaders($loader->getLoaders()));

DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class UnusedTagsPass implements CompilerPassInterface
102102
'twig.extension',
103103
'twig.loader',
104104
'twig.runtime',
105+
'validator.attribute_metadata',
105106
'validator.auto_mapper',
106107
'validator.constraint_validator',
107108
'validator.group_provider',

DependencyInjection/FrameworkExtension.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,9 @@
216216
use Symfony\Component\Uid\UuidV4;
217217
use Symfony\Component\Validator\Constraint;
218218
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
219+
use Symfony\Component\Validator\Constraints\Traverse;
219220
use Symfony\Component\Validator\ConstraintValidatorInterface;
221+
use Symfony\Component\Validator\DependencyInjection\AttributeMetadataPass as ValidatorAttributeMetadataPass;
220222
use Symfony\Component\Validator\GroupProviderInterface;
221223
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
222224
use Symfony\Component\Validator\ObjectInitializerInterface;
@@ -1801,22 +1803,31 @@ private function registerValidationConfiguration(array $config, ContainerBuilder
18011803
$files = ['xml' => [], 'yml' => []];
18021804
$this->registerValidatorMapping($container, $config, $files);
18031805

1804-
if (!empty($files['xml'])) {
1806+
if ($files['xml']) {
18051807
$validatorBuilder->addMethodCall('addXmlMappings', [$files['xml']]);
18061808
}
18071809

1808-
if (!empty($files['yml'])) {
1810+
if ($files['yml']) {
18091811
$validatorBuilder->addMethodCall('addYamlMappings', [$files['yml']]);
18101812
}
18111813

18121814
$definition = $container->findDefinition('validator.email');
18131815
$definition->replaceArgument(0, $config['email_validation_mode']);
18141816

1815-
if (\array_key_exists('enable_attributes', $config) && $config['enable_attributes']) {
1817+
// When attributes are disabled, it means from runtime-discovery only; autoconfiguration should still happen.
1818+
// And when runtime-discovery of attributes is enabled, we can skip compile-time autoconfiguration in debug mode.
1819+
if (class_exists(ValidatorAttributeMetadataPass::class) && (!($config['enable_attributes'] ?? false) || !$container->getParameter('kernel.debug'))) {
1820+
// The $reflector argument hints at where the attribute could be used
1821+
$container->registerAttributeForAutoconfiguration(Constraint::class, function (ChildDefinition $definition, Constraint $attribute, \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector) {
1822+
$definition->addTag('validator.attribute_metadata');
1823+
});
1824+
}
1825+
1826+
if ($config['enable_attributes'] ?? false) {
18161827
$validatorBuilder->addMethodCall('enableAttributeMapping');
18171828
}
18181829

1819-
if (\array_key_exists('static_method', $config) && $config['static_method']) {
1830+
if ($config['static_method'] ?? false) {
18201831
foreach ($config['static_method'] as $methodName) {
18211832
$validatorBuilder->addMethodCall('addMethodMapping', [$methodName]);
18221833
}
@@ -1855,9 +1866,11 @@ private function registerValidatorMapping(ContainerBuilder $container, array $co
18551866
$files['yaml' === $extension ? 'yml' : $extension][] = $path;
18561867
};
18571868

1858-
if (ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/validator'])) {
1859-
$reflClass = new \ReflectionClass(Form::class);
1860-
$fileRecorder('xml', \dirname($reflClass->getFileName()).'/Resources/config/validation.xml');
1869+
if (!ContainerBuilder::willBeAvailable('symfony/form', Form::class, ['symfony/framework-bundle', 'symfony/validator'])) {
1870+
$container->removeDefinition('validator.form.attribute_metadata');
1871+
} elseif (!($r = new \ReflectionClass(Form::class))->getAttributes(Traverse::class) || !class_exists(ValidatorAttributeMetadataPass::class)) {
1872+
// BC with symfony/form & symfony/validator < 7.4
1873+
$fileRecorder('xml', \dirname($r->getFileName()).'/Resources/config/validation.xml');
18611874
}
18621875

18631876
foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) {
@@ -2060,7 +2073,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
20602073
}
20612074

20622075
$serializerLoaders = [];
2063-
if (isset($config['enable_attributes']) && $config['enable_attributes']) {
2076+
if ($config['enable_attributes'] ?? false) {
20642077
$attributeLoader = new Definition(AttributeLoader::class);
20652078

20662079
$serializerLoaders[] = $attributeLoader;
@@ -2100,7 +2113,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
21002113
$chainLoader->replaceArgument(0, $serializerLoaders);
21012114
$container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $serializerLoaders);
21022115

2103-
if (isset($config['name_converter']) && $config['name_converter']) {
2116+
if ($config['name_converter'] ?? false) {
21042117
$container->setParameter('.serializer.name_converter', $config['name_converter']);
21052118
$container->getDefinition('serializer.name_converter.metadata_aware')->setArgument(1, new Reference($config['name_converter']));
21062119
}

FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass;
7676
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
7777
use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass;
78+
use Symfony\Component\Validator\DependencyInjection\AttributeMetadataPass;
7879
use Symfony\Component\VarExporter\Internal\Hydrator;
7980
use Symfony\Component\VarExporter\Internal\Registry;
8081
use Symfony\Component\Workflow\DependencyInjection\WorkflowDebugPass;
@@ -155,6 +156,7 @@ public function build(ContainerBuilder $container): void
155156
$container->addCompilerPass($registerListenersPass, PassConfig::TYPE_BEFORE_REMOVING);
156157
$this->addCompilerPassIfExists($container, AddConstraintValidatorsPass::class);
157158
$this->addCompilerPassIfExists($container, AddValidatorInitializersPass::class);
159+
$this->addCompilerPassIfExists($container, AttributeMetadataPass::class);
158160
$this->addCompilerPassIfExists($container, AddConsoleCommandPass::class, PassConfig::TYPE_BEFORE_REMOVING);
159161
// must be registered before the AddConsoleCommandPass
160162
$container->addCompilerPass(new TranslationLintCommandPass(), PassConfig::TYPE_BEFORE_REMOVING, 10);

Resources/config/validator.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bundle\FrameworkBundle\CacheWarmer\ValidatorCacheWarmer;
1515
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
1616
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
17+
use Symfony\Component\Form\Form;
1718
use Symfony\Component\Validator\Constraints\EmailValidator;
1819
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
1920
use Symfony\Component\Validator\Constraints\ExpressionValidator;
@@ -127,5 +128,9 @@
127128
service('property_info'),
128129
])
129130
->tag('validator.auto_mapper')
131+
132+
->set('validator.form.attribute_metadata', Form::class)
133+
->tag('container.excluded')
134+
->tag('validator.attribute_metadata')
130135
;
131136
};

Tests/DependencyInjection/FrameworkExtensionTestCase.php

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
9494
use Symfony\Component\Translation\LocaleSwitcher;
9595
use Symfony\Component\Translation\TranslatableMessage;
96+
use Symfony\Component\Validator\Constraints\Traverse;
9697
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
9798
use Symfony\Component\Validator\Validation;
9899
use Symfony\Component\Validator\Validator\ValidatorInterface;
@@ -1312,16 +1313,17 @@ public function testValidation()
13121313
$projectDir = $container->getParameter('kernel.project_dir');
13131314

13141315
$ref = new \ReflectionClass(Form::class);
1315-
$xmlMappings = [
1316-
\dirname($ref->getFileName()).'/Resources/config/validation.xml',
1317-
strtr($projectDir.'/config/validator/foo.xml', '/', \DIRECTORY_SEPARATOR),
1318-
];
1316+
$xmlMappings = [];
1317+
if (!$ref->getAttributes(Traverse::class)) {
1318+
$xmlMappings[] = \dirname($ref->getFileName()).'/Resources/config/validation.xml';
1319+
}
1320+
$xmlMappings[] = strtr($projectDir.'/config/validator/foo.xml', '/', \DIRECTORY_SEPARATOR);
13191321

13201322
$calls = $container->getDefinition('validator.builder')->getMethodCalls();
13211323

1322-
$annotations = !class_exists(FullStack::class);
1324+
$attributes = !class_exists(FullStack::class);
13231325

1324-
$this->assertCount($annotations ? 8 : 7, $calls);
1326+
$this->assertCount($attributes ? 8 : 7, $calls);
13251327
$this->assertSame('setConstraintValidatorFactory', $calls[0][0]);
13261328
$this->assertEquals([new Reference('validator.validator_factory')], $calls[0][1]);
13271329
$this->assertSame('setGroupProviderLocator', $calls[1][0]);
@@ -1333,7 +1335,7 @@ public function testValidation()
13331335
$this->assertSame('addXmlMappings', $calls[4][0]);
13341336
$this->assertSame([$xmlMappings], $calls[4][1]);
13351337
$i = 4;
1336-
if ($annotations) {
1338+
if ($attributes) {
13371339
$this->assertSame('enableAttributeMapping', $calls[++$i][0]);
13381340
}
13391341
$this->assertSame('addMethodMapping', $calls[++$i][0]);
@@ -1408,15 +1410,19 @@ public function testValidationPaths()
14081410
$this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[8][1]);
14091411

14101412
$xmlMappings = $calls[4][1][0];
1411-
$this->assertCount(3, $xmlMappings);
1412-
try {
1413-
// Testing symfony/symfony
1414-
$this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]);
1415-
} catch (\Exception $e) {
1416-
// Testing symfony/framework-bundle with deps=high
1417-
$this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]);
1413+
1414+
if (!(new \ReflectionClass(Form::class))->getAttributes(Traverse::class)) {
1415+
try {
1416+
// Testing symfony/symfony
1417+
$this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]);
1418+
} catch (\Exception $e) {
1419+
// Testing symfony/framework-bundle with deps=high
1420+
$this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]);
1421+
}
1422+
array_shift($xmlMappings);
14181423
}
1419-
$this->assertStringEndsWith('TestBundle/Resources/config/validation.xml', $xmlMappings[1]);
1424+
$this->assertCount(2, $xmlMappings);
1425+
$this->assertStringEndsWith('TestBundle/Resources/config/validation.xml', $xmlMappings[0]);
14201426

14211427
$yamlMappings = $calls[5][1][0];
14221428
$this->assertCount(1, $yamlMappings);
@@ -1434,16 +1440,19 @@ public function testValidationPathsUsingCustomBundlePath()
14341440

14351441
$calls = $container->getDefinition('validator.builder')->getMethodCalls();
14361442
$xmlMappings = $calls[4][1][0];
1437-
$this->assertCount(3, $xmlMappings);
1438-
1439-
try {
1440-
// Testing symfony/symfony
1441-
$this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]);
1442-
} catch (\Exception $e) {
1443-
// Testing symfony/framework-bundle with deps=high
1444-
$this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]);
1443+
1444+
if (!(new \ReflectionClass(Form::class))->getAttributes(Traverse::class)) {
1445+
try {
1446+
// Testing symfony/symfony
1447+
$this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]);
1448+
} catch (\Exception $e) {
1449+
// Testing symfony/framework-bundle with deps=high
1450+
$this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]);
1451+
}
1452+
array_shift($xmlMappings);
14451453
}
1446-
$this->assertStringEndsWith('CustomPathBundle/Resources/config/validation.xml', $xmlMappings[1]);
1454+
$this->assertCount(2, $xmlMappings);
1455+
$this->assertStringEndsWith('CustomPathBundle/Resources/config/validation.xml', $xmlMappings[0]);
14471456

14481457
$yamlMappings = $calls[5][1][0];
14491458
$this->assertCount(1, $yamlMappings);
@@ -1490,7 +1499,6 @@ public function testValidationMapping()
14901499
$calls = $container->getDefinition('validator.builder')->getMethodCalls();
14911500

14921501
$this->assertSame('addXmlMappings', $calls[4][0]);
1493-
$this->assertCount(3, $calls[4][1][0]);
14941502

14951503
$this->assertSame('addYamlMappings', $calls[5][0]);
14961504
$this->assertCount(3, $calls[5][1][0]);

0 commit comments

Comments
 (0)