Подтвердить что ты не робот

Как проверить уникальные объекты в коллекции сущностей в symfony2

У меня есть сущность с отношением OneToMany к другому объекту, когда я сохраняю родительский объект, я хочу, чтобы у детей не было дубликатов.

Здесь классы, которые я использовал, коллекция скидок не должна содержать двух продуктов с тем же именем для данного клиента.

У меня есть объект Client с набором скидок:

/**
 * @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 buy, похоже, обрабатывает только коллекции полей, а не сущностей.

Также существует валидатор All, который позволяет определять ограничения, которые должны применяться для каждого объекта, но не для коллекции в целом.

Мне нужно знать, существуют ли ограничения коллекции объектов в целом, прежде чем они будут сохраняться в базе данных, кроме написания настраиваемого валидатора или написания Callback валидатор каждый раз.

4b9b3361

Ответ 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 измените проверять на:

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

}

Ответ 2

Мне не удается заставить предыдущий ответ работать на symfony 2.6. Из-за следующего кода на l. 852 из RecursiveContextualValidator, он идет только один раз по методу validate, когда 2 элемента равны.

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

}