How to validate unique entities in an entity collection in symfony2

后端 未结 4 591
心在旅途
心在旅途 2020-12-29 09:46

I have an entity with a OneToMany relation to another entity, when I persist the parent entity I want to ensure the children contain no duplicates.

Here\'s the class

相关标签:
4条回答
  • I've created a custom constraint/validator for this.

    It validates a form collection using the "All" assertion, and takes an optional parameter : the property path of the property to check the entity equality.

    (it's for Symfony 2.1, to adapt it to Symfony 2.0 check the end of the answer) :

    For more information on creating custom validation constraints, check The Cookbook

    The constraint :

    #src/Acme/DemoBundle/Validator/constraint/UniqueInCollection.php
    <?php
    
    namespace Acme\DemoBundle\Validator\Constraint;
    
    use Symfony\Component\Validator\Constraint;
    
    /**
    * @Annotation
    */
    class UniqueInCollection extends Constraint
    {
        public $message = 'The error message (with %parameters%)';
        // The property path used to check wether objects are equal
        // If none is specified, it will check that objects are equal
        public $propertyPath = null;
    }
    

    And the validator :

    #src/Acme/DemoBundle/Validator/constraint/UniqueInCollectionValidator.php
    <?php
    
    namespace Acme\DemoBundle\Validator\Constraint;
    
    use Symfony\Component\Validator\Constraint;
    use Symfony\Component\Validator\ConstraintValidator;
    use Symfony\Component\Form\Util\PropertyPath;
    
    class UniqueInCollectionValidator extends ConstraintValidator
    {
    
        // We keep an array with the previously checked values of the collection
        private $collectionValues = array();
    
        // validate is new in Symfony 2.1, in Symfony 2.0 use "isValid" (see below)
        public function validate($value, Constraint $constraint)
        {
            // Apply the property path if specified
            if($constraint->propertyPath){
                $propertyPath = new PropertyPath($constraint->propertyPath);
                $value = $propertyPath->getValue($value);
            }
    
            // Check that the value is not in the array
            if(in_array($value, $this->collectionValues))
                $this->context->addViolation($constraint->message, array());
    
            // Add the value in the array for next items validation
            $this->collectionValues[] = $value;
        }
    }
    

    In your case, you would use it like this :

    use Acme\DemoBundle\Validator\Constraints as AcmeAssert;
    
    // ...
    
    /**
     * @ORM\OneToMany(targetEntity="Discount", mappedBy="client", cascade={"persist"}, orphanRemoval="true")
     * @Assert\All(constraints={
     *     @AcmeAssert\UniqueInCollection(propertyPath ="product")
     * })
     */
    

    For Symfony 2.0, change the validate function by :

    public function isValid($value, Constraint $constraint)
    {
            $valid = true;
    
            if($constraint->propertyPath){
                $propertyPath = new PropertyPath($constraint->propertyPath);
                $value = $propertyPath->getValue($value);
            }
    
            if(in_array($value, $this->collectionValues)){
                $valid = false;
                $this->setMessage($constraint->message, array('%string%' => $value));
            }
    
            $this->collectionValues[] = $value;
    
            return $valid
    
    }
    
    0 讨论(0)
  • 2020-12-29 10:26

    I can't manage to make the previous answer works on symfony 2.6. Because of the following code on l. 852 of RecursiveContextualValidator, it only goes once on the validate method when 2 items are equals.

    if ($context->isConstraintValidated($cacheKey, $constraintHash)) {
        continue; 
    } 
    

    So, here is what I've done to deals with the original issue :

    On the Entity :

    * @AcmeAssert\UniqueInCollection(propertyPath ="product")
    

    Instead of

    * @Assert\All(constraints={
    *     @AcmeAssert\UniqueInCollection(propertyPath ="product")
    * })
    

    On the validator :

    public function validate($collection, Constraint $constraint){
    
        $propertyAccessor = PropertyAccess::getPropertyAccessor(); 
    
        $previousValues = array();
        foreach($collection as $collectionItem){
            $value = $propertyAccessor->getValue($collectionItem, $constraint->propertyPath);
            $previousSimilarValuesNumber = count(array_keys($previousValues,$value));
            if($previousSimilarValuesNumber == 1){
                $this->context->addViolation($constraint->message, array('%email%' => $value));
            }
            $previousValues[] = $value;
        }
    
    }
    

    Instead of :

    public function isValid($value, Constraint $constraint)
    {
        $valid = true;
    
        if($constraint->propertyPath){
            $propertyAccessor = PropertyAccess::getPropertyAccessor(); 
            $value = $propertyAccessor->getValue($value, $constraint->propertyPath);
        }
    
        if(in_array($value, $this->collectionValues)){
            $valid = false;
            $this->setMessage($constraint->message, array('%string%' => $value));
        }
    
        $this->collectionValues[] = $value;
    
        return $valid
    
    }
    
    0 讨论(0)
  • 2020-12-29 10:30

    Here is a version working with multiple fields just like UniqueEntity does. Validation fails if multiple objects have same values.

    Usage:

    /**
    * ....
    * @App\UniqueInCollection(fields={"name", "email"})
    */
    private $contacts;
    //Validation fails if multiple contacts have same name AND email
    

    The constraint class ...

    <?php
    namespace App\Validator\Constraints;
    
    use Symfony\Component\Validator\Constraint;
    
    /**
     * @Annotation
     */
    class UniqueInCollection extends Constraint
    {
        public $message = 'Entry is duplicated.';
        public $fields;
    
        public function validatedBy()
        {
            return UniqueInCollectionValidator::class;
        }
    }
    

    The validator itself ....

    <?php
    
    namespace App\Validator\Constraints;
    
    use Symfony\Component\PropertyAccess\PropertyAccess;
    use Symfony\Component\Validator\Constraint;
    use Symfony\Component\Validator\ConstraintValidator;
    use Symfony\Component\Validator\Exception\UnexpectedTypeException;
    use Symfony\Component\Validator\Exception\UnexpectedValueException;
    
    class UniqueInCollectionValidator extends ConstraintValidator
    {
        /**
         * @var \Symfony\Component\PropertyAccess\PropertyAccessor
         */
        private $propertyAccessor;
    
        public function __construct()
        {
            $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
        }
    
        /**
         * @param mixed $collection
         * @param Constraint $constraint
         * @throws \Exception
         */
        public function validate($collection, Constraint $constraint)
        {
            if (!$constraint instanceof UniqueInCollection) {
                throw new UnexpectedTypeException($constraint, UniqueInCollection::class);
            }
    
            if (null === $collection) {
                return;
            }
    
            if (!\is_array($collection) && !$collection instanceof \IteratorAggregate) {
                throw new UnexpectedValueException($collection, 'array|IteratorAggregate');
            }
    
            if ($constraint->fields === null) {
                throw new \Exception('Option propertyPath can not be null');
            }
    
            if(is_array($constraint->fields)) $fields = $constraint->fields;
            else $fields = [$constraint->fields];
    
    
            $propertyValues = [];
            foreach ($collection as $key => $element) {
                $propertyValue = [];
                foreach ($fields as $field) {
                    $propertyValue[] = $this->propertyAccessor->getValue($element, $field);
                }
    
    
                if (in_array($propertyValue, $propertyValues, true)) {
    
                    $this->context->buildViolation($constraint->message)
                        ->atPath(sprintf('[%s]', $key))
                        ->addViolation();
                }
    
                $propertyValues[] = $propertyValue;
            }
    
        }
    }
    
    0 讨论(0)
  • 2020-12-29 10:35

    For Symfony 4.3(only tested version) you can use my custom validator. Prefered way of usage is as annotaion on validated collection:

    use App\Validator\Constraints as App;
    

    ...

    /**
     * @ORM\OneToMany
     *
     * @App\UniqueProperty(
     *     propertyPath="entityProperty"
     * )
     */
    private $entities;
    

    Difference between Julien and my solution is, that my Constraint is defined on validated Collection instead on element of Collection itself.

    Constraint:

    #src/Validator/Constraints/UniqueProperty.php
    <?php
    
    
    namespace App\Validator\Constraints;
    
    
    use Symfony\Component\Validator\Constraint;
    
    /**
     * @Annotation
     */
    class UniqueProperty extends Constraint
    {
        public $message = 'This collection should contain only elements with uniqe value.';
        public $propertyPath;
    
        public function validatedBy()
        {
            return UniquePropertyValidator::class;
        }
    }
    

    Validator:

    #src/Validator/Constraints/UniquePropertyValidator.php
    <?php
    
    namespace App\Validator\Constraints;
    
    use Symfony\Component\PropertyAccess\PropertyAccess;
    use Symfony\Component\Validator\Constraint;
    use Symfony\Component\Validator\ConstraintValidator;
    use Symfony\Component\Validator\Exception\UnexpectedTypeException;
    use Symfony\Component\Validator\Exception\UnexpectedValueException;
    
    class UniquePropertyValidator extends ConstraintValidator
    {
        /**
         * @var \Symfony\Component\PropertyAccess\PropertyAccessor
         */
        private $propertyAccessor;
    
        public function __construct()
        {
            $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
        }
    
        /**
         * @param mixed $value
         * @param Constraint $constraint
         * @throws \Exception
         */
        public function validate($value, Constraint $constraint)
        {
            if (!$constraint instanceof UniqueProperty) {
                throw new UnexpectedTypeException($constraint, UniqueProperty::class);
            }
    
            if (null === $value) {
                return;
            }
    
            if (!\is_array($value) && !$value instanceof \IteratorAggregate) {
                throw new UnexpectedValueException($value, 'array|IteratorAggregate');
            }
    
            if ($constraint->propertyPath === null) {
                throw new \Exception('Option propertyPath can not be null');
            }
    
            $propertyValues = [];
            foreach ($value as $key => $element) {
                $propertyValue = $this->propertyAccessor->getValue($element, $constraint->propertyPath);
                if (in_array($propertyValue, $propertyValues, true)) {
                    $this->context->buildViolation($constraint->message)
                        ->atPath(sprintf('[%s]', $key))
                        ->addViolation();
                }
    
                $propertyValues[] = $propertyValue;
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题