Как проверить уникальные объекты в коллекции сущностей в 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 валидатор каждый раз.
Ответы
Ответ 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
}
Ответ 3
Начиная с Symfony 4.3 и выше, существует уникальное ограничение на сборку. См. Https://symfony.com/doc/current/reference/constraints/Unique.html.