Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory;
use Symfony\Component\Validator\Mapping\Loader\AttributeLoader;
use Symfony\Component\Validator\Mapping\Loader\LoaderChain;
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader;
use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader;
use Symfony\Component\Validator\ValidatorBuilder;

/**
* Warms up XML and YAML validator metadata.
* Warms up validator metadata.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
*
Expand Down Expand Up @@ -77,14 +78,14 @@ protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array
/**
* @param LoaderInterface[] $loaders
*
* @return XmlFileLoader[]|YamlFileLoader[]
* @return list<XmlFileLoader|YamlFileLoader|AttributeLoader>
*/
private function extractSupportedLoaders(array $loaders): array
{
$supportedLoaders = [];

foreach ($loaders as $loader) {
if ($loader instanceof XmlFileLoader || $loader instanceof YamlFileLoader) {
if (method_exists($loader, 'getMappedClasses')) {
$supportedLoaders[] = $loader;
} elseif ($loader instanceof LoaderChain) {
$supportedLoaders = array_merge($supportedLoaders, $this->extractSupportedLoaders($loader->getLoaders()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class UnusedTagsPass implements CompilerPassInterface
'twig.extension',
'twig.loader',
'twig.runtime',
'validator.attribute_metadata',
'validator.auto_mapper',
'validator.constraint_validator',
'validator.group_provider',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,12 @@
use Symfony\Component\TypeInfo\TypeResolver\TypeResolverInterface;
use Symfony\Component\Uid\Factory\UuidFactory;
use Symfony\Component\Uid\UuidV4;
use Symfony\Component\Validator\Attribute\ExtendsValidationFor;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\DependencyInjection\AttributeMetadataPass as ValidatorAttributeMetadataPass;
use Symfony\Component\Validator\GroupProviderInterface;
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
use Symfony\Component\Validator\ObjectInitializerInterface;
Expand Down Expand Up @@ -1796,22 +1799,35 @@ private function registerValidationConfiguration(array $config, ContainerBuilder
$files = ['xml' => [], 'yml' => []];
$this->registerValidatorMapping($container, $config, $files);

if (!empty($files['xml'])) {
if ($files['xml']) {
$validatorBuilder->addMethodCall('addXmlMappings', [$files['xml']]);
}

if (!empty($files['yml'])) {
if ($files['yml']) {
$validatorBuilder->addMethodCall('addYamlMappings', [$files['yml']]);
}

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

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

$container->registerAttributeForAutoconfiguration(ExtendsValidationFor::class, function (ChildDefinition $definition, ExtendsValidationFor $attribute) {
$definition->addTag('validator.attribute_metadata', ['for' => $attribute->class]);
});

if ($config['enable_attributes'] ?? false) {
$validatorBuilder->addMethodCall('enableAttributeMapping');
}

if (\array_key_exists('static_method', $config) && $config['static_method']) {
if ($config['static_method'] ?? false) {
foreach ($config['static_method'] as $methodName) {
$validatorBuilder->addMethodCall('addMethodMapping', [$methodName]);
}
Expand Down Expand Up @@ -1850,9 +1866,11 @@ private function registerValidatorMapping(ContainerBuilder $container, array $co
$files['yaml' === $extension ? 'yml' : $extension][] = $path;
};

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

foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) {
Expand Down Expand Up @@ -2055,7 +2073,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
}

$serializerLoaders = [];
if (isset($config['enable_attributes']) && $config['enable_attributes']) {
if ($config['enable_attributes'] ?? false) {
$attributeLoader = new Definition(AttributeLoader::class);

$serializerLoaders[] = $attributeLoader;
Expand Down Expand Up @@ -2095,7 +2113,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
$chainLoader->replaceArgument(0, $serializerLoaders);
$container->getDefinition('serializer.mapping.cache_warmer')->replaceArgument(0, $serializerLoaders);

if (isset($config['name_converter']) && $config['name_converter']) {
if ($config['name_converter'] ?? false) {
$container->setParameter('.serializer.name_converter', $config['name_converter']);
$container->getDefinition('serializer.name_converter.metadata_aware')->setArgument(1, new Reference($config['name_converter']));
}
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass;
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass;
use Symfony\Component\Validator\DependencyInjection\AttributeMetadataPass;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\Registry;
use Symfony\Component\Workflow\DependencyInjection\WorkflowDebugPass;
Expand Down Expand Up @@ -155,6 +156,7 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass($registerListenersPass, PassConfig::TYPE_BEFORE_REMOVING);
$this->addCompilerPassIfExists($container, AddConstraintValidatorsPass::class);
$this->addCompilerPassIfExists($container, AddValidatorInitializersPass::class);
$this->addCompilerPassIfExists($container, AttributeMetadataPass::class);
$this->addCompilerPassIfExists($container, AddConsoleCommandPass::class, PassConfig::TYPE_BEFORE_REMOVING);
// must be registered before the AddConsoleCommandPass
$container->addCompilerPass(new TranslationLintCommandPass(), PassConfig::TYPE_BEFORE_REMOVING, 10);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Symfony\Bundle\FrameworkBundle\CacheWarmer\ValidatorCacheWarmer;
use Symfony\Component\Cache\Adapter\PhpArrayAdapter;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\Form\Form;
use Symfony\Component\Validator\Constraints\EmailValidator;
use Symfony\Component\Validator\Constraints\ExpressionLanguageProvider;
use Symfony\Component\Validator\Constraints\ExpressionValidator;
Expand Down Expand Up @@ -127,5 +128,9 @@
service('property_info'),
])
->tag('validator.auto_mapper')

->set('validator.form.attribute_metadata', Form::class)
->tag('container.excluded')
->tag('validator.attribute_metadata')
;
};
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
use Symfony\Component\Translation\LocaleSwitcher;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Component\Validator\Constraints\Traverse;
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\Validator\ValidatorInterface;
Expand Down Expand Up @@ -1312,16 +1313,17 @@ public function testValidation()
$projectDir = $container->getParameter('kernel.project_dir');

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

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

$annotations = !class_exists(FullStack::class);
$attributes = !class_exists(FullStack::class);

$this->assertCount($annotations ? 8 : 7, $calls);
$this->assertCount($attributes ? 8 : 7, $calls);
$this->assertSame('setConstraintValidatorFactory', $calls[0][0]);
$this->assertEquals([new Reference('validator.validator_factory')], $calls[0][1]);
$this->assertSame('setGroupProviderLocator', $calls[1][0]);
Expand All @@ -1333,7 +1335,7 @@ public function testValidation()
$this->assertSame('addXmlMappings', $calls[4][0]);
$this->assertSame([$xmlMappings], $calls[4][1]);
$i = 4;
if ($annotations) {
if ($attributes) {
$this->assertSame('enableAttributeMapping', $calls[++$i][0]);
}
$this->assertSame('addMethodMapping', $calls[++$i][0]);
Expand Down Expand Up @@ -1408,15 +1410,19 @@ public function testValidationPaths()
$this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[8][1]);

$xmlMappings = $calls[4][1][0];
$this->assertCount(3, $xmlMappings);
try {
// Testing symfony/symfony
$this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]);
} catch (\Exception $e) {
// Testing symfony/framework-bundle with deps=high
$this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]);

if (!(new \ReflectionClass(Form::class))->getAttributes(Traverse::class)) {
try {
// Testing symfony/symfony
$this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]);
} catch (\Exception $e) {
// Testing symfony/framework-bundle with deps=high
$this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]);
}
array_shift($xmlMappings);
}
$this->assertStringEndsWith('TestBundle/Resources/config/validation.xml', $xmlMappings[1]);
$this->assertCount(2, $xmlMappings);
$this->assertStringEndsWith('TestBundle/Resources/config/validation.xml', $xmlMappings[0]);

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

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

try {
// Testing symfony/symfony
$this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]);
} catch (\Exception $e) {
// Testing symfony/framework-bundle with deps=high
$this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]);

if (!(new \ReflectionClass(Form::class))->getAttributes(Traverse::class)) {
try {
// Testing symfony/symfony
$this->assertStringEndsWith('Component'.\DIRECTORY_SEPARATOR.'Form/Resources/config/validation.xml', $xmlMappings[0]);
} catch (\Exception $e) {
// Testing symfony/framework-bundle with deps=high
$this->assertStringEndsWith('symfony'.\DIRECTORY_SEPARATOR.'form/Resources/config/validation.xml', $xmlMappings[0]);
}
array_shift($xmlMappings);
}
$this->assertStringEndsWith('CustomPathBundle/Resources/config/validation.xml', $xmlMappings[1]);
$this->assertCount(2, $xmlMappings);
$this->assertStringEndsWith('CustomPathBundle/Resources/config/validation.xml', $xmlMappings[0]);

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

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

$this->assertSame('addYamlMappings', $calls[5][0]);
$this->assertCount(3, $calls[5][1][0]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@

namespace Symfony\Component\Form\Extension\Validator\Constraints;

use Symfony\Component\Validator\Attribute\HasNamedArguments;
use Symfony\Component\Validator\Constraint;

/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class Form extends Constraint
{
public const NOT_SYNCHRONIZED_ERROR = '1dafa156-89e1-4736-b832-419c2e501fca';
Expand All @@ -26,6 +28,12 @@ class Form extends Constraint
self::NO_SUCH_FIELD_ERROR => 'NO_SUCH_FIELD_ERROR',
];

#[HasNamedArguments]
public function __construct(mixed $options = null, ?array $groups = null, mixed $payload = null)
{
parent::__construct($options, $groups, $payload);
}

public function getTargets(): string|array
{
return self::CLASS_CONSTRAINT;
Expand Down
4 changes: 4 additions & 0 deletions src/Symfony/Component/Form/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Validator\Constraints\Form as AssertForm;
use Symfony\Component\Form\Util\FormUtil;
use Symfony\Component\Form\Util\InheritDataAwareIterator;
use Symfony\Component\Form\Util\OrderedHashMap;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\PropertyAccess\PropertyPathInterface;
use Symfony\Component\Validator\Constraints\Traverse;

/**
* Form represents a form.
Expand Down Expand Up @@ -68,6 +70,8 @@
*
* @implements \IteratorAggregate<string, FormInterface>
*/
#[AssertForm]
#[Traverse(false)]
class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterface
{
private ?FormInterface $parent = null;
Expand Down
32 changes: 32 additions & 0 deletions src/Symfony/Component/Validator/Attribute/ExtendsValidationFor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Validator\Attribute;

/**
* Declares that constraints listed on the current class should be added to the given class.
*
* Classes that use this attribute should contain only properties and methods that
* exist on the target class (not necessarily all of them).
*
* @author Nicolas Grekas <p@tchwork.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
final class ExtendsValidationFor
{
/**
* @param class-string $class
*/
public function __construct(
public string $class,
) {
}
}
2 changes: 2 additions & 0 deletions src/Symfony/Component/Validator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ CHANGELOG
7.4
---

* Add `#[ExtendsValidationFor]` to declare new constraints for a class
* Add `ValidatorBuilder::addAttributeMappings()` and `AttributeMetadataPass` to declare compile-time constraint metadata using attributes
* Deprecate implementing `__sleep/wakeup()` on `GenericMetadata` implementations; use `__(un)serialize()` instead
* Deprecate passing a list of choices to the first argument of the `Choice` constraint. Use the `choices` option instead
* Add the `min` and `max` parameter to the `Length` constraint violation
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Validator/Constraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
abstract class Constraint
{
/**
Expand Down
Loading
Loading