浏览代码

Merge remote branch 'beberlei/DoctrineUniqueValidator'

* beberlei/DoctrineUniqueValidator:
  [Doctrine] Fix default value to null for entity manager to make fluent integration with Doctrine Registry work
  [Doctrine] Add fields as default option and allow strings to be passed.
  [Doctrine] Add DoctrineBundle integration (DI Container registration) for the UniqueEntityValidator
  [Doctrine] Implement suggested changes by Stof, added functional test to verify unique validator works.
  [Doctrine] Add Unique Validator
Fabien Potencier 14 年之前
父节点
当前提交
c4232b11fa

+ 56 - 0
src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php

@@ -0,0 +1,56 @@
+<?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\Constraints;
+
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Constraint for the Unique Entity validator
+ * 
+ * @author Benjamin Eberlei <kontakt@beberlei.de>
+ */
+class UniqueEntity extends Constraint
+{
+    public $message = 'This value is already used.';
+    public $em = null;
+    public $fields = array();
+    
+    public function getRequiredOptions()
+    {
+        return array('fields');
+    }
+    
+    /**
+     * The validator must be defined as a service with this name.
+     * 
+     * @return string
+     */
+    public function validatedBy()
+    {
+        return 'doctrine.orm.validator.unique';
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public function getTargets()
+    {
+        return self::CLASS_CONSTRAINT;
+    }
+    
+    public function getDefaultOption()
+    {
+        return 'fields';
+    }
+}

+ 81 - 0
src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php

@@ -0,0 +1,81 @@
+<?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\Constraints;
+
+use Symfony\Bundle\DoctrineBundle\Registry;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
+use Symfony\Component\Validator\ConstraintValidator;
+
+/**
+ * Unique Entity Validator checks if one or a set of fields contain unique values.
+ * 
+ * @author Benjamin Eberlei <kontakt@beberlei.de>
+ */
+class UniqueEntityValidator extends ConstraintValidator
+{
+    /**
+     * @var Registry
+     */
+    private $registry;
+    
+    /**
+     * @param Registry $registry
+     */
+    public function __construct(Registry $registry)
+    {
+        $this->registry = $registry;
+    }
+    
+    /**
+     * @param object $entity
+     * @param Constraint $constraint
+     * @return bool
+     */
+    public function isValid($entity, Constraint $constraint)
+    {
+        if (!is_array($constraint->fields) && !is_string($constraint->fields)) {
+            throw new UnexpectedTypeException($constraint->fields, 'array');
+        }
+        $fields = (array)$constraint->fields;
+        if (count($constraint->fields) == 0) {
+            throw new ConstraintDefinitionException("At least one field has to specified.");
+        }
+        
+        $em = $this->registry->getEntityManager($constraint->em);
+        
+        $className = $this->context->getCurrentClass();
+        $class = $em->getClassMetadata($className);
+        
+        $criteria = array();
+        foreach ($fields as $fieldName) {
+            if (!isset($class->reflFields[$fieldName])) {
+                throw new ConstraintDefinitionException("Only field names mapped by Doctrine can be validated for uniqueness.");
+            }
+            
+            $criteria[$fieldName] = $class->reflFields[$fieldName]->getValue($entity);
+        }
+        
+        $repository = $em->getRepository($className);
+        $result = $repository->findBy($criteria);
+        
+        if (count($result) > 0 && $result[0] !== $entity) {
+            $oldPath = $this->context->getPropertyPath();
+            $this->context->setPropertyPath( empty($oldPath) ? $fields[0] : $oldPath . "." . $fields[0]);
+            $this->context->addViolation($constraint->message, array(), $criteria[$constraint->fields[0]]);
+            $this->context->setPropertyPath($oldPath);
+        }
+        
+        return true; // all true, we added the violation already!
+    }
+}

+ 31 - 0
src/Symfony/Bundle/DoctrineBundle/DependencyInjection/Compiler/AddValidatorNamespaceAliasPass.php

@@ -0,0 +1,31 @@
+<?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\Bundle\DoctrineBundle\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+
+class AddValidatorNamespaceAliasPass implements CompilerPassInterface
+{
+    public function process(ContainerBuilder $container)
+    {
+        if (!$container->hasDefinition('validator.mapping.loader.annotation_loader')) {
+            return;
+        }
+
+        $loader = $container->getDefinition('validator.mapping.loader.annotation_loader');
+        $args = $container->getParameterBag()->resolveValue($loader->getArguments());
+
+        $args[0]['assertORM'] = 'Symfony\\Bridge\\Doctrine\\Validator\\Constraints\\';
+        $loader->replaceArgument(0, $args[0]);
+    }
+}

+ 2 - 0
src/Symfony/Bundle/DoctrineBundle/DoctrineBundle.php

@@ -13,6 +13,7 @@ namespace Symfony\Bundle\DoctrineBundle;
 
 use Symfony\Component\DependencyInjection\Compiler\PassConfig;
 use Symfony\Bundle\DoctrineBundle\DependencyInjection\Compiler\RegisterEventListenersAndSubscribersPass;
+use Symfony\Bundle\DoctrineBundle\DependencyInjection\Compiler\AddValidatorNamespaceAliasPass;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\HttpKernel\Bundle\Bundle;
 
@@ -29,5 +30,6 @@ class DoctrineBundle extends Bundle
         parent::build($container);
 
         $container->addCompilerPass(new RegisterEventListenersAndSubscribersPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION);
+        $container->addCompilerPass(new AddValidatorNamespaceAliasPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION);
     }
 }

+ 9 - 0
src/Symfony/Bundle/DoctrineBundle/Resources/config/orm.xml

@@ -31,6 +31,9 @@
 
         <!-- form field factory guesser -->
         <parameter key="form.type_guesser.doctrine.class">Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser</parameter>
+        
+        <!-- validator -->
+        <parameter key="doctrine.orm.validator.unique.class">Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator</parameter>
     </parameters>
 
     <services>
@@ -60,5 +63,11 @@
         <service id="doctrine.orm.configuration" class="%doctrine.orm.configuration.class%" abstract="true" public="false" />
 
         <service id="doctrine.orm.entity_manager.abstract" class="%doctrine.orm.entity_manager.class%" factory-class="%doctrine.orm.entity_manager.class%" factory-method="create" abstract="true" />
+        
+        <!-- validator -->
+        <service id="doctrine.orm.validator.unique" class="%doctrine.orm.validator.unique.class%">
+            <tag name="validator.constraint_validator" alias="doctrine.orm.validator.unique" />
+            <argument type="service" id="doctrine" />
+        </service>
     </services>
 </container>

+ 2 - 0
src/Symfony/Bundle/DoctrineBundle/Tests/ContainerTest.php

@@ -37,6 +37,8 @@ class ContainerTest extends TestCase
         $this->assertInstanceOf('Doctrine\Common\EventManager', $container->get('doctrine.dbal.event_manager'));
         $this->assertInstanceOf('Doctrine\DBAL\Event\Listeners\MysqlSessionInit', $container->get('doctrine.dbal.default_connection.events.mysqlsessioninit'));
         $this->assertInstanceOf('Symfony\Bundle\DoctrineBundle\CacheWarmer\ProxyCacheWarmer', $container->get('doctrine.orm.proxy_cache_warmer'));
+        $this->assertInstanceOf('Symfony\Bundle\DoctrineBundle\Registry', $container->get('doctrine'));
+        $this->assertInstanceOf('Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator', $container->get('doctrine.orm.validator.unique'));
 
         $this->assertSame($container->get('my.platform'), $container->get('doctrine.dbal.default_connection')->getDatabasePlatform());
 

+ 9 - 0
src/Symfony/Component/Validator/ConstraintValidator.php

@@ -13,8 +13,17 @@ namespace Symfony\Component\Validator;
 
 abstract class ConstraintValidator implements ConstraintValidatorInterface
 {
+    /**
+     * @var ExecutionContext
+     */
     protected $context;
+    /**
+     * @var string
+     */
     private $messageTemplate;
+    /**
+     * @var array
+     */
     private $messageParameters;
 
     /**

+ 103 - 0
tests/Symfony/Tests/Bridge/Doctrine/Validator/Constraints/UniqueValidatorTest.php

@@ -0,0 +1,103 @@
+<?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\Tests\Bridge\Doctrine\Validator\Constraints;
+
+require_once __DIR__.'/../../Form/DoctrineOrmTestCase.php';
+require_once __DIR__.'/../../Fixtures/SingleIdentEntity.php';
+
+use Symfony\Tests\Bridge\Doctrine\Form\DoctrineOrmTestCase;
+use Symfony\Tests\Bridge\Doctrine\Form\Fixtures\SingleIdentEntity;
+use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
+use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
+use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator;
+use Symfony\Component\Validator\Mapping\ClassMetadata;
+use Symfony\Component\Validator\Validator;
+use Doctrine\ORM\Tools\SchemaTool;
+
+class UniqueValidatorTest extends DoctrineOrmTestCase
+{
+    protected function createRegistryMock($entityManagerName, $em)
+    {
+        $registry = $this->getMock('Symfony\Bundle\DoctrineBundle\Registry', array(), array(), '', false);
+        $registry->expects($this->any())
+                 ->method('getEntityManager')
+                 ->with($this->equalTo($entityManagerName))
+                 ->will($this->returnValue($em));
+        return $registry;
+    }
+    
+    protected function createMetadataFactoryMock($metadata)
+    {
+        $metadataFactory = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface');
+        $metadataFactory->expects($this->any())
+                        ->method('getClassMetadata')
+                        ->with($this->equalTo($metadata->name))
+                        ->will($this->returnValue($metadata));
+        return $metadataFactory;
+    }
+    
+    protected function createValidatorFactory($uniqueValidator)
+    {
+        $validatorFactory = $this->getMock('Symfony\Component\Validator\ConstraintValidatorFactoryInterface');
+        $validatorFactory->expects($this->any())
+                         ->method('getInstance')
+                         ->with($this->isInstanceOf('Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity'))
+                         ->will($this->returnValue($uniqueValidator));
+        return $validatorFactory;
+    }
+    
+    /**
+     * This is a functinoal test as there is a large integration necessary to get the validator working.
+     */
+    public function testValidateUniqueness()
+    {
+        $entityManagerName = "foo";
+        $em = $this->createTestEntityManager();
+        $schemaTool = new SchemaTool($em);
+        $schemaTool->createSchema(array(
+            $em->getClassMetadata('Symfony\Tests\Bridge\Doctrine\Form\Fixtures\SingleIdentEntity')
+        ));
+        
+        $entity1 = new SingleIdentEntity(1, 'Foo');
+        
+        $registry = $this->createRegistryMock($entityManagerName, $em);
+        
+        $uniqueValidator = new UniqueEntityValidator($registry);
+        
+        $metadata = new ClassMetadata('Symfony\Tests\Bridge\Doctrine\Form\Fixtures\SingleIdentEntity');
+        $metadata->addConstraint(new UniqueEntity(array('fields' => array('name'), 'em' => $entityManagerName)));
+        
+        $metadataFactory = $this->createMetadataFactoryMock($metadata);
+        $validatorFactory = $this->createValidatorFactory($uniqueValidator);
+        
+        $validator = new Validator($metadataFactory, $validatorFactory);
+        
+        $violationsList = $validator->validate($entity1);
+        $this->assertEquals(0, $violationsList->count(), "No violations found on entity before it is saved to the database.");
+        
+        $em->persist($entity1);
+        $em->flush();
+        
+        $violationsList = $validator->validate($entity1);
+        $this->assertEquals(0, $violationsList->count(), "No violations found on entity after it was saved to the database.");
+        
+        $entity2 = new SingleIdentEntity(2, 'Foo');
+        
+        $violationsList = $validator->validate($entity2);
+        $this->assertEquals(1, $violationsList->count(), "No violations found on entity after it was saved to the database.");
+        
+        $violation = $violationsList[0];
+        $this->assertEquals('This value is already used.', $violation->getMessage());
+        $this->assertEquals('name', $violation->getPropertyPath());
+        $this->assertEquals('Foo', $violation->getInvalidValue());
+    }
+}