[Validator][DoctrineBridge][FWBundle] Automatic data validation

This commit is contained in:
Kévin Dunglas 2018-06-26 17:35:39 +02:00
parent af28965c24
commit 2d64e703c2
No known key found for this signature in database
GPG Key ID: 4D04EBEF06AAF3A6
19 changed files with 937 additions and 3 deletions

View File

@ -0,0 +1,58 @@
<?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\Bridge\Doctrine\Tests\Fixtures;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
* @UniqueEntity(fields={"alreadyMappedUnique"})
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DoctrineLoaderEntity
{
/**
* @ORM\Id
* @ORM\Column
*/
public $id;
/**
* @ORM\Column(length=20)
*/
public $maxLength;
/**
* @ORM\Column(length=20)
* @Assert\Length(min=5)
*/
public $mergedMaxLength;
/**
* @ORM\Column(length=20)
* @Assert\Length(min=1, max=10)
*/
public $alreadyMappedMaxLength;
/**
* @ORM\Column(unique=true)
*/
public $unique;
/**
* @ORM\Column(unique=true)
*/
public $alreadyMappedUnique;
}

View File

@ -0,0 +1,94 @@
<?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\Bridge\Doctrine\Tests\Validator;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEntity;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Bridge\Doctrine\Validator\DoctrineLoader;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Tests\Fixtures\Entity;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\ValidatorBuilder;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DoctrineLoaderTest extends TestCase
{
public function testLoadClassMetadata()
{
if (!method_exists(ValidatorBuilder::class, 'addLoader')) {
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
}
$validator = Validation::createValidatorBuilder()
->enableAnnotationMapping()
->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager()))
->getValidator()
;
$classMetadata = $validator->getMetadataFor(new DoctrineLoaderEntity());
$classConstraints = $classMetadata->getConstraints();
$this->assertCount(2, $classConstraints);
$this->assertInstanceOf(UniqueEntity::class, $classConstraints[0]);
$this->assertInstanceOf(UniqueEntity::class, $classConstraints[1]);
$this->assertSame(['alreadyMappedUnique'], $classConstraints[0]->fields);
$this->assertSame('unique', $classConstraints[1]->fields);
$maxLengthMetadata = $classMetadata->getPropertyMetadata('maxLength');
$this->assertCount(1, $maxLengthMetadata);
$maxLengthConstraints = $maxLengthMetadata[0]->getConstraints();
$this->assertCount(1, $maxLengthConstraints);
$this->assertInstanceOf(Length::class, $maxLengthConstraints[0]);
$this->assertSame(20, $maxLengthConstraints[0]->max);
$mergedMaxLengthMetadata = $classMetadata->getPropertyMetadata('mergedMaxLength');
$this->assertCount(1, $mergedMaxLengthMetadata);
$mergedMaxLengthConstraints = $mergedMaxLengthMetadata[0]->getConstraints();
$this->assertCount(1, $mergedMaxLengthConstraints);
$this->assertInstanceOf(Length::class, $mergedMaxLengthConstraints[0]);
$this->assertSame(20, $mergedMaxLengthConstraints[0]->max);
$this->assertSame(5, $mergedMaxLengthConstraints[0]->min);
$alreadyMappedMaxLengthMetadata = $classMetadata->getPropertyMetadata('alreadyMappedMaxLength');
$this->assertCount(1, $alreadyMappedMaxLengthMetadata);
$alreadyMappedMaxLengthConstraints = $alreadyMappedMaxLengthMetadata[0]->getConstraints();
$this->assertCount(1, $alreadyMappedMaxLengthConstraints);
$this->assertInstanceOf(Length::class, $alreadyMappedMaxLengthConstraints[0]);
$this->assertSame(10, $alreadyMappedMaxLengthConstraints[0]->max);
$this->assertSame(1, $alreadyMappedMaxLengthConstraints[0]->min);
}
/**
* @dataProvider regexpProvider
*/
public function testClassValidator(bool $expected, string $classValidatorRegexp = null)
{
$doctrineLoader = new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), $classValidatorRegexp);
$classMetadata = new ClassMetadata(DoctrineLoaderEntity::class);
$this->assertSame($expected, $doctrineLoader->loadClassMetadata($classMetadata));
}
public function regexpProvider()
{
return [
[true, null],
[true, '{^'.preg_quote(DoctrineLoaderEntity::class).'$|^'.preg_quote(Entity::class).'$}'],
[false, '{^'.preg_quote(Entity::class).'$}'],
];
}
}

View File

@ -0,0 +1,121 @@
<?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\Bridge\Doctrine\Validator;
use Doctrine\Common\Persistence\Mapping\MappingException;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
/**
* Guesses and loads the appropriate constraints using Doctrine's metadata.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class DoctrineLoader implements LoaderInterface
{
private $entityManager;
private $classValidatorRegexp;
public function __construct(EntityManagerInterface $entityManager, string $classValidatorRegexp = null)
{
$this->entityManager = $entityManager;
$this->classValidatorRegexp = $classValidatorRegexp;
}
/**
* {@inheritdoc}
*/
public function loadClassMetadata(ClassMetadata $metadata): bool
{
$className = $metadata->getClassName();
if (null !== $this->classValidatorRegexp && !preg_match($this->classValidatorRegexp, $className)) {
return false;
}
try {
$doctrineMetadata = $this->entityManager->getClassMetadata($className);
} catch (MappingException | OrmMappingException $exception) {
return false;
}
if (!$doctrineMetadata instanceof ClassMetadataInfo) {
return false;
}
/* Available keys:
- type
- scale
- length
- unique
- nullable
- precision
*/
$existingUniqueFields = $this->getExistingUniqueFields($metadata);
// Type and nullable aren't handled here, use the PropertyInfo Loader instead.
foreach ($doctrineMetadata->fieldMappings as $mapping) {
if (true === $mapping['unique'] && !isset($existingUniqueFields[$mapping['fieldName']])) {
$metadata->addConstraint(new UniqueEntity(['fields' => $mapping['fieldName']]));
}
if (null === $mapping['length']) {
continue;
}
$constraint = $this->getLengthConstraint($metadata, $mapping['fieldName']);
if (null === $constraint) {
$metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']]));
} elseif (null === $constraint->max) {
// If a Length constraint exists and no max length has been explicitly defined, set it
$constraint->max = $mapping['length'];
}
}
return true;
}
private function getLengthConstraint(ClassMetadata $metadata, string $fieldName): ?Length
{
foreach ($metadata->getPropertyMetadata($fieldName) as $propertyMetadata) {
foreach ($propertyMetadata->getConstraints() as $constraint) {
if ($constraint instanceof Length) {
return $constraint;
}
}
}
return null;
}
private function getExistingUniqueFields(ClassMetadata $metadata): array
{
$fields = [];
foreach ($metadata->getConstraints() as $constraint) {
if (!$constraint instanceof UniqueEntity) {
continue;
}
if (\is_string($constraint->fields)) {
$fields[$constraint->fields] = true;
} elseif (\is_array($constraint->fields) && 1 === \count($constraint->fields)) {
$fields[$constraint->fields[0]] = true;
}
}
return $fields;
}
}

View File

@ -792,6 +792,45 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->end()
->arrayNode('auto_mapping')
->useAttributeAsKey('namespace')
->normalizeKeys(false)
->beforeNormalization()
->ifArray()
->then(function (array $values): array {
foreach ($values as $k => $v) {
if (isset($v['service'])) {
continue;
}
if (isset($v['namespace'])) {
$values[$k]['services'] = [];
continue;
}
if (!\is_array($v)) {
$values[$v]['services'] = [];
unset($values[$k]);
continue;
}
$tmp = $v;
unset($values[$k]);
$values[$k]['services'] = $tmp;
}
return $values;
})
->end()
->arrayPrototype()
->fixXmlConfig('service')
->children()
->arrayNode('services')
->prototype('scalar')->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end()

View File

@ -107,6 +107,7 @@ use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Translation\Command\XliffLintCommand as BaseXliffLintCommand;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
use Symfony\Component\Validator\ObjectInitializerInterface;
use Symfony\Component\WebLink\HttpHeaderSerializer;
use Symfony\Component\Workflow;
@ -280,7 +281,8 @@ class FrameworkExtension extends Extension
$container->removeDefinition('console.command.messenger_debug');
}
$this->registerValidationConfiguration($config['validation'], $container, $loader);
$propertyInfoEnabled = $this->isConfigEnabled($container, $config['property_info']);
$this->registerValidationConfiguration($config['validation'], $container, $loader, $propertyInfoEnabled);
$this->registerEsiConfiguration($config['esi'], $container, $loader);
$this->registerSsiConfiguration($config['ssi'], $container, $loader);
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
@ -301,7 +303,7 @@ class FrameworkExtension extends Extension
$this->registerSerializerConfiguration($config['serializer'], $container, $loader);
}
if ($this->isConfigEnabled($container, $config['property_info'])) {
if ($propertyInfoEnabled) {
$this->registerPropertyInfoConfiguration($container, $loader);
}
@ -1152,7 +1154,7 @@ class FrameworkExtension extends Extension
}
}
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, bool $propertyInfoEnabled)
{
if (!$this->validatorConfigEnabled = $this->isConfigEnabled($container, $config)) {
return;
@ -1203,6 +1205,11 @@ class FrameworkExtension extends Extension
if (!$container->getParameter('kernel.debug')) {
$validatorBuilder->addMethodCall('setMetadataCache', [new Reference('validator.mapping.cache.symfony')]);
}
$container->setParameter('validator.auto_mapping', $config['auto_mapping']);
if (!$propertyInfoEnabled || !$config['auto_mapping'] || !class_exists(PropertyInfoLoader::class)) {
$container->removeDefinition('validator.property_info_loader');
}
}
private function registerValidatorMapping(ContainerBuilder $container, array $config, array &$files)

View File

@ -53,6 +53,7 @@ use Symfony\Component\Translation\DependencyInjection\TranslationDumperPass;
use Symfony\Component\Translation\DependencyInjection\TranslationExtractorPass;
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
use Symfony\Component\Translation\DependencyInjection\TranslatorPathsPass;
use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass;
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass;
@ -124,6 +125,7 @@ class FrameworkBundle extends Bundle
$container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING);
$this->addCompilerPassIfExists($container, AddMimeTypeGuesserPass::class);
$this->addCompilerPassIfExists($container, MessengerPass::class);
$this->addCompilerPassIfExists($container, AddAutoMappingConfigurationPass::class);
$container->addCompilerPass(new RegisterReverseContainerPass(true));
$container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING);

View File

@ -190,6 +190,7 @@
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="static-method" type="xsd:string" />
<xsd:element name="mapping" type="file_mapping" />
<xsd:element name="auto-mapping" type="auto_mapping" />
</xsd:choice>
<xsd:attribute name="enabled" type="xsd:boolean" />
@ -207,6 +208,13 @@
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="auto_mapping">
<xsd:sequence>
<xsd:element name="service" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="namespace" type="xsd:string" use="required" />
</xsd:complexType>
<xsd:simpleType name="email-validation-mode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="html5" />

View File

@ -60,5 +60,12 @@
<argument></argument>
<tag name="validator.constraint_validator" alias="Symfony\Component\Validator\Constraints\EmailValidator" />
</service>
<service id="validator.property_info_loader" class="Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader">
<argument type="service" id="property_info" />
<argument type="service" id="property_info" />
<tag name="validator.auto_mapper" />
</service>
</services>
</container>

View File

@ -232,6 +232,7 @@ class ConfigurationTest extends TestCase
'mapping' => [
'paths' => [],
],
'auto_mapping' => [],
],
'annotations' => [
'cache' => 'php_array',

View File

@ -0,0 +1,12 @@
<?php
$container->loadFromExtension('framework', [
'property_info' => ['enabled' => true],
'validation' => [
'auto_mapping' => [
'App\\' => ['foo', 'bar'],
'Symfony\\' => ['a', 'b'],
'Foo\\',
],
],
]);

View File

@ -0,0 +1,20 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:framework="http://symfony.com/schema/dic/symfony">
<framework:config>
<framework:property-info enabled="true" />
<framework:validation>
<framework:auto-mapping namespace="App\">
<framework:service>foo</framework:service>
<framework:service>bar</framework:service>
</framework:auto-mapping>
<framework:auto-mapping namespace="Symfony\">
<framework:service>a</framework:service>
<framework:service>b</framework:service>
</framework:auto-mapping>
<framework:auto-mapping namespace="Foo\" />
</framework:validation>
</framework:config>
</container>

View File

@ -0,0 +1,7 @@
framework:
property_info: { enabled: true }
validation:
auto_mapping:
'App\': ['foo', 'bar']
'Symfony\': ['a', 'b']
'Foo\': []

View File

@ -50,6 +50,8 @@ use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\DependencyInjection\TranslatorPass;
use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass;
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Workflow;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -1033,6 +1035,23 @@ abstract class FrameworkExtensionTest extends TestCase
$this->assertContains('validation.yaml', $calls[4][1][0][2]);
}
public function testValidationAutoMapping()
{
if (!class_exists(PropertyInfoLoader::class)) {
$this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+');
}
$container = $this->createContainerFromFile('validation_auto_mapping');
$parameter = [
'App\\' => ['services' => ['foo', 'bar']],
'Symfony\\' => ['services' => ['a', 'b']],
'Foo\\' => ['services' => []],
];
$this->assertSame($parameter, $container->getParameter('validator.auto_mapping'));
$this->assertTrue($container->hasDefinition('validator.property_info_loader'));
}
public function testFormsCanBeEnabledWithoutCsrfProtection()
{
$container = $this->createContainerFromFile('form_no_csrf');

View File

@ -0,0 +1,93 @@
<?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\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Injects the automapping configuration as last argument of loaders tagged with the "validator.auto_mapper" tag.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AddAutoMappingConfigurationPass implements CompilerPassInterface
{
private $validatorBuilderService;
private $tag;
public function __construct(string $validatorBuilderService = 'validator.builder', string $tag = 'validator.auto_mapper')
{
$this->validatorBuilderService = $validatorBuilderService;
$this->tag = $tag;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasParameter('validator.auto_mapping') || !$container->hasDefinition($this->validatorBuilderService)) {
return;
}
$config = $container->getParameter('validator.auto_mapping');
$globalNamespaces = [];
$servicesToNamespaces = [];
foreach ($config as $namespace => $value) {
if ([] === $value['services']) {
$globalNamespaces[] = $namespace;
continue;
}
foreach ($value['services'] as $service) {
$servicesToNamespaces[$service][] = $namespace;
}
}
$validatorBuilder = $container->getDefinition($this->validatorBuilderService);
foreach ($container->findTaggedServiceIds($this->tag) as $id => $tags) {
$regexp = $this->getRegexp(array_merge($globalNamespaces, $servicesToNamespaces[$id] ?? []));
$container->getDefinition($id)->setArgument('$classValidatorRegexp', $regexp);
$validatorBuilder->addMethodCall('addLoader', [new Reference($id)]);
}
$container->getParameterBag()->remove('validator.auto_mapping');
}
/**
* Builds a regexp to check if a class is auto-mapped.
*/
private function getRegexp(array $patterns): string
{
$regexps = [];
foreach ($patterns as $pattern) {
// Escape namespace
$regex = preg_quote(ltrim($pattern, '\\'));
// Wildcards * and **
$regex = strtr($regex, ['\\*\\*' => '.*?', '\\*' => '[^\\\\]*?']);
// If this class does not end by a slash, anchor the end
if ('\\' !== substr($regex, -1)) {
$regex .= '$';
}
$regexps[] = '^'.$regex;
}
return sprintf('{%s}', implode('|', $regexps));
}
}

View File

@ -0,0 +1,151 @@
<?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\Mapping\Loader;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type as PropertyInfoType;
use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Validator\Mapping\ClassMetadata;
/**
* Guesses and loads the appropriate constraints using PropertyInfo.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class PropertyInfoLoader implements LoaderInterface
{
private $listExtractor;
private $typeExtractor;
private $classValidatorRegexp;
public function __construct(PropertyListExtractorInterface $listExtractor, PropertyTypeExtractorInterface $typeExtractor, string $classValidatorRegexp = null)
{
$this->listExtractor = $listExtractor;
$this->typeExtractor = $typeExtractor;
$this->classValidatorRegexp = $classValidatorRegexp;
}
/**
* {@inheritdoc}
*/
public function loadClassMetadata(ClassMetadata $metadata)
{
$className = $metadata->getClassName();
if (null !== $this->classValidatorRegexp && !preg_match($this->classValidatorRegexp, $className)) {
return false;
}
if (!$properties = $this->listExtractor->getProperties($className)) {
return false;
}
foreach ($properties as $property) {
$types = $this->typeExtractor->getTypes($className, $property);
if (null === $types) {
continue;
}
$hasTypeConstraint = false;
$hasNotNullConstraint = false;
$hasNotBlankConstraint = false;
$allConstraint = null;
foreach ($metadata->getPropertyMetadata($property) as $propertyMetadata) {
foreach ($propertyMetadata->getConstraints() as $constraint) {
if ($constraint instanceof Type) {
$hasTypeConstraint = true;
} elseif ($constraint instanceof NotNull) {
$hasNotNullConstraint = true;
} elseif ($constraint instanceof NotBlank) {
$hasNotBlankConstraint = true;
} elseif ($constraint instanceof All) {
$allConstraint = $constraint;
}
}
}
$builtinTypes = [];
$nullable = false;
$scalar = true;
foreach ($types as $type) {
$builtinTypes[] = $type->getBuiltinType();
if ($scalar && !\in_array($type->getBuiltinType(), [PropertyInfoType::BUILTIN_TYPE_INT, PropertyInfoType::BUILTIN_TYPE_FLOAT, PropertyInfoType::BUILTIN_TYPE_STRING, PropertyInfoType::BUILTIN_TYPE_BOOL], true)) {
$scalar = false;
}
if (!$nullable && $type->isNullable()) {
$nullable = true;
}
}
if (!$hasTypeConstraint) {
if (1 === \count($builtinTypes)) {
if ($types[0]->isCollection() && (null !== $collectionValueType = $types[0]->getCollectionValueType())) {
$this->handleAllConstraint($property, $allConstraint, $collectionValueType, $metadata);
}
$metadata->addPropertyConstraint($property, $this->getTypeConstraint($builtinTypes[0], $types[0]));
} elseif ($scalar) {
$metadata->addPropertyConstraint($property, new Type(['type' => 'scalar']));
}
}
if (!$nullable && !$hasNotBlankConstraint && !$hasNotNullConstraint) {
$metadata->addPropertyConstraint($property, new NotNull());
}
}
return true;
}
private function getTypeConstraint(string $builtinType, PropertyInfoType $type): Type
{
if (PropertyInfoType::BUILTIN_TYPE_OBJECT === $builtinType && null !== $className = $type->getClassName()) {
return new Type(['type' => $className]);
}
return new Type(['type' => $builtinType]);
}
private function handleAllConstraint(string $property, ?All $allConstraint, PropertyInfoType $propertyInfoType, ClassMetadata $metadata)
{
$containsTypeConstraint = false;
$containsNotNullConstraint = false;
if (null !== $allConstraint) {
foreach ($allConstraint->constraints as $constraint) {
if ($constraint instanceof Type) {
$containsTypeConstraint = true;
} elseif ($constraint instanceof NotNull) {
$containsNotNullConstraint = true;
}
}
}
$constraints = [];
if (!$containsNotNullConstraint && !$propertyInfoType->isNullable()) {
$constraints[] = new NotNull();
}
if (!$containsTypeConstraint) {
$constraints[] = $this->getTypeConstraint($propertyInfoType->getBuiltinType(), $propertyInfoType);
}
if (null === $allConstraint) {
$metadata->addPropertyConstraint($property, new All(['constraints' => $constraints]));
} else {
$allConstraint->constraints = array_merge($allConstraint->constraints, $constraints);
}
}
}

View File

@ -0,0 +1,73 @@
<?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\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass;
use Symfony\Component\Validator\Tests\Fixtures\PropertyInfoLoaderEntity;
use Symfony\Component\Validator\ValidatorBuilder;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class AddAutoMappingConfigurationPassTest extends TestCase
{
public function testNoConfigParameter()
{
$container = new ContainerBuilder();
(new AddAutoMappingConfigurationPass())->process($container);
$this->assertCount(1, $container->getDefinitions());
}
public function testNoValidatorBuilder()
{
$container = new ContainerBuilder();
(new AddAutoMappingConfigurationPass())->process($container);
$this->assertCount(1, $container->getDefinitions());
}
/**
* @dataProvider mappingProvider
*/
public function testProcess(string $namespace, array $services, string $expectedRegexp)
{
$container = new ContainerBuilder();
$container->setParameter('validator.auto_mapping', [
'App\\' => ['services' => []],
$namespace => ['services' => $services],
]);
$container->register('validator.builder', ValidatorBuilder::class);
foreach ($services as $service) {
$container->register($service)->addTag('validator.auto_mapper');
}
(new AddAutoMappingConfigurationPass())->process($container);
foreach ($services as $service) {
$this->assertSame($expectedRegexp, $container->getDefinition($service)->getArgument('$classValidatorRegexp'));
}
$this->assertCount(\count($services), $container->getDefinition('validator.builder')->getMethodCalls());
}
public function mappingProvider(): array
{
return [
['Foo\\', ['foo', 'baz'], '{^App\\\\|^Foo\\\\}'],
[PropertyInfoLoaderEntity::class, ['class'], '{^App\\\\|^Symfony\\\\Component\\\\Validator\\\\Tests\\\\Fixtures\\\\PropertyInfoLoaderEntity$}'],
['Symfony\Component\Validator\Tests\Fixtures\\', ['trailing_antislash'], '{^App\\\\|^Symfony\\\\Component\\\\Validator\\\\Tests\\\\Fixtures\\\\}'],
['Symfony\Component\Validator\Tests\Fixtures\\*', ['trailing_star'], '{^App\\\\|^Symfony\\\\Component\\\\Validator\\\\Tests\\\\Fixtures\\\\[^\\\\]*?$}'],
['Symfony\Component\Validator\Tests\Fixtures\\**', ['trailing_double_star'], '{^App\\\\|^Symfony\\\\Component\\\\Validator\\\\Tests\\\\Fixtures\\\\.*?$}'],
];
}
}

View File

@ -0,0 +1,49 @@
<?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\Tests\Fixtures;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PropertyInfoLoaderEntity
{
public $nullableString;
public $string;
public $scalar;
public $object;
public $collection;
/**
* @Assert\Type(type="int")
*/
public $alreadyMappedType;
/**
* @Assert\NotNull
*/
public $alreadyMappedNotNull;
/**
* @Assert\NotBlank
*/
public $alreadyMappedNotBlank;
/**
* @Assert\All({
* @Assert\Type(type="string"),
* @Assert\Iban
* })
*/
public $alreadyPartiallyMappedCollection;
}

View File

@ -0,0 +1,171 @@
<?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\Tests\Mapping\Loader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Validator\Constraints\Iban;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Type as TypeConstraint;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader;
use Symfony\Component\Validator\Tests\Fixtures\Entity;
use Symfony\Component\Validator\Tests\Fixtures\PropertyInfoLoaderEntity;
use Symfony\Component\Validator\Validation;
/**
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class PropertyInfoLoaderTest extends TestCase
{
public function testLoadClassMetadata()
{
$propertyInfoStub = $this->createMock(PropertyInfoExtractorInterface::class);
$propertyInfoStub
->method('getProperties')
->willReturn([
'nullableString',
'string',
'scalar',
'object',
'collection',
'alreadyMappedType',
'alreadyMappedNotNull',
'alreadyMappedNotBlank',
'alreadyPartiallyMappedCollection',
])
;
$propertyInfoStub
->method('getTypes')
->will($this->onConsecutiveCalls(
[new Type(Type::BUILTIN_TYPE_STRING, true)],
[new Type(Type::BUILTIN_TYPE_STRING)],
[new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_BOOL)],
[new Type(Type::BUILTIN_TYPE_OBJECT, true, Entity::class)],
[new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, Entity::class))],
[new Type(Type::BUILTIN_TYPE_FLOAT, true)], // The existing constraint is float
[new Type(Type::BUILTIN_TYPE_STRING, true)],
[new Type(Type::BUILTIN_TYPE_STRING, true)],
[new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, null, new Type(Type::BUILTIN_TYPE_FLOAT))]
))
;
$propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub);
$validator = Validation::createValidatorBuilder()
->enableAnnotationMapping()
->addLoader($propertyInfoLoader)
->getValidator()
;
$classMetadata = $validator->getMetadataFor(new PropertyInfoLoaderEntity());
$nullableStringMetadata = $classMetadata->getPropertyMetadata('nullableString');
$this->assertCount(1, $nullableStringMetadata);
$nullableStringConstraints = $nullableStringMetadata[0]->getConstraints();
$this->assertCount(1, $nullableStringConstraints);
$this->assertInstanceOf(TypeConstraint::class, $nullableStringConstraints[0]);
$this->assertSame('string', $nullableStringConstraints[0]->type);
$stringMetadata = $classMetadata->getPropertyMetadata('string');
$this->assertCount(1, $stringMetadata);
$stringConstraints = $stringMetadata[0]->getConstraints();
$this->assertCount(2, $stringConstraints);
$this->assertInstanceOf(TypeConstraint::class, $stringConstraints[0]);
$this->assertSame('string', $stringConstraints[0]->type);
$this->assertInstanceOf(NotNull::class, $stringConstraints[1]);
$scalarMetadata = $classMetadata->getPropertyMetadata('scalar');
$this->assertCount(1, $scalarMetadata);
$scalarConstraints = $scalarMetadata[0]->getConstraints();
$this->assertCount(1, $scalarConstraints);
$this->assertInstanceOf(TypeConstraint::class, $scalarConstraints[0]);
$this->assertSame('scalar', $scalarConstraints[0]->type);
$objectMetadata = $classMetadata->getPropertyMetadata('object');
$this->assertCount(1, $objectMetadata);
$objectConstraints = $objectMetadata[0]->getConstraints();
$this->assertCount(1, $objectConstraints);
$this->assertInstanceOf(TypeConstraint::class, $objectConstraints[0]);
$this->assertSame(Entity::class, $objectConstraints[0]->type);
$collectionMetadata = $classMetadata->getPropertyMetadata('collection');
$this->assertCount(1, $collectionMetadata);
$collectionConstraints = $collectionMetadata[0]->getConstraints();
$this->assertCount(2, $collectionConstraints);
$this->assertInstanceOf(All::class, $collectionConstraints[0]);
$this->assertInstanceOf(NotNull::class, $collectionConstraints[0]->constraints[0]);
$this->assertInstanceOf(TypeConstraint::class, $collectionConstraints[0]->constraints[1]);
$this->assertSame(Entity::class, $collectionConstraints[0]->constraints[1]->type);
$alreadyMappedTypeMetadata = $classMetadata->getPropertyMetadata('alreadyMappedType');
$this->assertCount(1, $alreadyMappedTypeMetadata);
$alreadyMappedTypeConstraints = $alreadyMappedTypeMetadata[0]->getConstraints();
$this->assertCount(1, $alreadyMappedTypeMetadata);
$this->assertInstanceOf(TypeConstraint::class, $alreadyMappedTypeConstraints[0]);
$alreadyMappedNotNullMetadata = $classMetadata->getPropertyMetadata('alreadyMappedNotNull');
$this->assertCount(1, $alreadyMappedNotNullMetadata);
$alreadyMappedNotNullConstraints = $alreadyMappedNotNullMetadata[0]->getConstraints();
$this->assertCount(1, $alreadyMappedNotNullMetadata);
$this->assertInstanceOf(NotNull::class, $alreadyMappedNotNullConstraints[0]);
$alreadyMappedNotBlankMetadata = $classMetadata->getPropertyMetadata('alreadyMappedNotBlank');
$this->assertCount(1, $alreadyMappedNotBlankMetadata);
$alreadyMappedNotBlankConstraints = $alreadyMappedNotBlankMetadata[0]->getConstraints();
$this->assertCount(1, $alreadyMappedNotBlankMetadata);
$this->assertInstanceOf(NotBlank::class, $alreadyMappedNotBlankConstraints[0]);
$alreadyPartiallyMappedCollectionMetadata = $classMetadata->getPropertyMetadata('alreadyPartiallyMappedCollection');
$this->assertCount(1, $alreadyPartiallyMappedCollectionMetadata);
$alreadyPartiallyMappedCollectionConstraints = $alreadyPartiallyMappedCollectionMetadata[0]->getConstraints();
$this->assertCount(2, $alreadyPartiallyMappedCollectionConstraints);
$this->assertInstanceOf(All::class, $alreadyPartiallyMappedCollectionConstraints[0]);
$this->assertInstanceOf(TypeConstraint::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[0]);
$this->assertSame('string', $alreadyPartiallyMappedCollectionConstraints[0]->constraints[0]->type);
$this->assertInstanceOf(Iban::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[1]);
$this->assertInstanceOf(NotNull::class, $alreadyPartiallyMappedCollectionConstraints[0]->constraints[2]);
}
/**
* @dataProvider regexpProvider
*/
public function testClassValidator(bool $expected, string $classValidatorRegexp = null)
{
$propertyInfoStub = $this->createMock(PropertyInfoExtractorInterface::class);
$propertyInfoStub
->method('getProperties')
->willReturn(['string'])
;
$propertyInfoStub
->method('getTypes')
->willReturn([new Type(Type::BUILTIN_TYPE_STRING)])
;
$propertyInfoLoader = new PropertyInfoLoader($propertyInfoStub, $propertyInfoStub, $classValidatorRegexp);
$classMetadata = new ClassMetadata(PropertyInfoLoaderEntity::class);
$this->assertSame($expected, $propertyInfoLoader->loadClassMetadata($classMetadata));
}
public function regexpProvider()
{
return [
[true, null],
[true, '{^'.preg_quote(PropertyInfoLoaderEntity::class).'$|^'.preg_quote(Entity::class).'$}'],
[false, '{^'.preg_quote(Entity::class).'$}'],
];
}
}

View File

@ -32,6 +32,7 @@
"symfony/expression-language": "~3.4|~4.0",
"symfony/cache": "~3.4|~4.0",
"symfony/property-access": "~3.4|~4.0",
"symfony/property-info": "~3.4|~4.0",
"symfony/translation": "~4.2",
"doctrine/annotations": "~1.0",
"doctrine/cache": "~1.0",
@ -56,6 +57,7 @@
"symfony/config": "",
"egulias/email-validator": "Strict (RFC compliant) email validation",
"symfony/property-access": "For accessing properties within comparison constraints",
"symfony/property-info": "To automatically add NotNull and Type constraints",
"symfony/expression-language": "For using the Expression validator"
},
"autoload": {