【问题标题】:How to validate unique entities in an entity collection in symfony2如何在 symfony2 中验证实体集合中的唯一实体
【发布时间】:2012-07-31 16:41:33
【问题描述】:

我有一个与另一个实体有 OneToMany 关系的实体,当我坚持父实体时,我想确保子实体不包含重复项。

这是我一直在使用的类,discounts 集合不应包含给定客户的两个同名产品。

我有一个带有折扣集合的客户实体:

/**
 * @ORM\Entity
 */
class Client {

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=128, nullable="true")
     */
    protected $name;

    /**
     * @ORM\OneToMany(targetEntity="Discount", mappedBy="client", cascade={"persist"}, orphanRemoval="true")
     */
    protected $discounts;

}

/**
 * @ORM\Entity
 * @UniqueEntity(fields={"product", "client"}, message="You can't create two discounts for the same product")
 */
    class Discount {
        /**
         * @ORM\Id
         * @ORM\Column(type="string", length=128, nullable="true")
         */
        protected $product;

        /**
         * @ORM\Id
         * @ORM\ManyToOne(targetEntity="Client", inversedBy="discounts")
         * @ORM\JoinColumn(name="client_id", referencedColumnName="id")
         */
        protected $client;

        /**
         * @ORM\Column(type="decimal", scale=2)
         */
        protected $percent;
    }

如您所见,我尝试将UniqueEntity 用于 Discount 类,问题是验证器似乎只检查数据库上加载的内容(为空),所以当实体被持久化时,我得到一个“SQLSTATE [23000]:违反完整性约束”。

我已经检查了Collection 约束购买它似乎只处理字段集合,而不是实体。

还有 All 验证器,它允许您定义要应用于每个实体的约束,但不能应用于整个集合。

除了每次都写custom validatorCallback验证器之外,我需要知道整体是否存在实体集合约束,才能持久化到数据库。

【问题讨论】:

  • 你的解决方案很好,我可以在来自 YAML 翻译文件的消息中添加翻译吗?

标签: validation symfony


【解决方案1】:

我为此创建了一个自定义约束/验证器。

它使用“All”断言验证表单集合,并采用可选参数:属性的属性路径来检查实体是否相等。

(适用于 Symfony 2.1,要使其适应 Symfony 2.0,请检查答案的结尾)

有关创建自定义验证约束的更多信息,请查看The Cookbook

约束:

#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;
}

还有验证器:

#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;
    }
}

在你的情况下,你会像这样使用它:

use Acme\DemoBundle\Validator\Constraints as AcmeAssert;

// ...

/**
 * @ORM\OneToMany(targetEntity="Discount", mappedBy="client", cascade={"persist"}, orphanRemoval="true")
 * @Assert\All(constraints={
 *     @AcmeAssert\UniqueInCollection(propertyPath ="product")
 * })
 */

对于 Symfony 2.0,将 validate 函数更改为:

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

}

【讨论】:

  • 已编辑:$collectionValues 不应该是静态的,以便验证器以相同的形式重复使用
  • 此外,默认情况下,集合类型的 'error_bubbling' 选项为 true。要识别哪些字段重复,您需要将其设置为 false(请参阅stackoverflow.com/questions/8961083/…
  • 非常感谢。我猜它不可能开箱即用,我只是不确定。框架中应包含这种或其他一些进行集合范围断言的方法。
  • 对于 Symfony 2.2 和更高版本,您需要使用 Symfony\Component\PropertyAccess\PropertyAccess 并将 $propertyPath = new PropertyPath($constraint-&gt;propertyPath); $value = $propertyPath-&gt;getValue($value); 替换为 $propertyAccessor = PropertyAccess::getPropertyAccessor(); $value = $propertyAccessor-&gt;getValue($value, $constraint-&gt;propertyPath);
  • 感谢您的回答。我试着让它像你一样工作。不幸的是,当我有两个类似的项目时,它只在validate 方法中使用一次,因为if ($context-&gt;isConstraintValidated($cacheKey, $constraintHash)) { continue; }RecursiveContextualValidator l 上。 852. 你是怎么做到的?
【解决方案2】:

我无法使之前的答案在 symfony 2.6 上有效。由于 l 上的以下代码。 RecursiveContextualValidator 的 852 个,当 2 项相等时,它只在 validate 方法上执行一次。

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

所以,这是我为处理原始问题所做的:

在实体上:

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

代替

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

在验证器上:

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;
    }

}

代替:

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

}

【讨论】:

    【解决方案3】:

    对于 Symfony 4.3(仅测试版),您可以使用我的自定义验证器。 首选的使用方式是作为验证集合的注释:

    use App\Validator\Constraints as App;
    

    ...

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

    Julien 和我的解决方案之间的区别在于,我的约束是在经过验证的 Collection 上定义的,而不是在 Collection 本身的元素上定义的。

    约束:

    #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;
        }
    }
    

    验证器:

    #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;
            }
        }
    }
    

    【讨论】:

    • 很好用!但是,错误消息仅出现在探查器中。如何使其显示为像字段附近的 {{form_error (field)}} 这样的真实错误消息?
    【解决方案4】:

    这是一个使用多个字段的版本,就像 UniqueEntity 一样。如果多个对象具有相同的值,则验证失败。

    用法:

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

    约束类...

    <?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;
        }
    }
    

    验证器本身 ....

    <?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;
            }
    
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多