Объединение @Gedmo\NestedTree и @ORM\UniqueEntity

Я создаю структуру папок, реализованную с помощью поведения NestedTree. Кроме того, я не хочу, чтобы две папки могли иметь одинаковое имя, если они являются братьями и сестрами. Для этого я использую комбинацию @UniqueEntity а также @UniqueConstraint аннотации, но это не работает.

Сначала моя сущность (обрезанная до минимума, поскольку она на 100% идентична значениям по умолчанию для NestedTree):

/**
 * @ORM\Entity
 * @Gedmo\Tree(type="nested")
 * @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
 * @UniqueEntity(fields={"parent", "name"})
 * @ORM\Table(uniqueConstraints={@ORM\UniqueConstraint(name="uniq_url", columns={"parent_id", "name"})})
 */
class Folder
{
    /**
     * @ORM\Column(type="string", nullable=false)
     */
    protected $name;

    /**
     * @Gedmo\TreeParent
     * @ORM\ManyToOne(targetEntity="Folder", inversedBy="children")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="SET NULL")
     */
    protected $parent;
}

Первая попытка (ignoreNull = true)

Когда я создаю две папки с одинаковым именем, у меня нарушается ограничение целостности, что означает, что @UniqueConstraints в базе данных работал, но что @UniqueEntity не:

Integrity constraint violation: 1062 Duplicate entry 'name_of_folder' for key 'uniq_url' 

Вторая попытка (ignoreNull = false)

Я также попытался с ключом ignoreNull, установленным в false (по умолчанию true):

@UniqueEntity(fields={"parent", "name"}, ignoreNull=false)

но тогда я получаю эту ошибку:

Warning: ReflectionProperty::getValue() expects parameter 1 to be object, null given in vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php line 670

Я прибил ошибку до этих строк в Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator:

        $criteria[$fieldName] = $class->reflFields[$fieldName]->getValue($entity);

        if ($constraint->ignoreNull && null === $criteria[$fieldName]) {
            return;
        }

        if ($class->hasAssociation($fieldName)) {
            /* Ensure the Proxy is initialized before using reflection to
             * read its identifiers. This is necessary because the wrapped
             * getter methods in the Proxy are being bypassed.
             */
            $em->initializeObject($criteria[$fieldName]);

            $relatedClass = $em->getClassMetadata($class->getAssociationTargetClass($fieldName));
            //problem
            $relatedId = $relatedClass->getIdentifierValues($criteria[$fieldName]);

            if (count($relatedId) > 1) {
                throw new ConstraintDefinitionException(
                    "Associated entities are not allowed to have more than one identifier field to be " .
                    "part of a unique constraint in: " . $class->getName() . "#" . $fieldName
                );
            }
            $criteria[$fieldName] = array_pop($relatedId);
        }

Проблема появляется на линии, отмеченной //problem, Похоже, что $criteria[$fieldName] === null является причиной ошибки.

Так что вот я, не зная, что делать... Кто-нибудь имеет представление о том, что происходит?

Спасибо.

1 ответ

Решение

Нет простого способа выйти из этой ситуации. Я наконец пошел своим путем и создал валидатор:

сущность

/**
 * @ORM\Entity(repositoryClass="Ibiz\DoctrineExtensionsBundle\Entity\Repository\NestedTreeRepository")
 * @Gedmo\Tree(type="nested")
 * @ORM\Table(uniqueConstraints={@ORM\UniqueConstraint(name="uniq_url", columns={"parent_id", "name"})})
 * @IbizAssert\UniquePath("getName")
 */
class Folder
{
    /**
     * @ORM\Column(type="string", nullable=false)
     */
    protected $name;

    public function getName()
    {
        return $this->name;
    }

}

Validator/ Ограничения / UniquePath.php

namespace Ibiz\DoctrineExtensionsBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class UniquePath extends Constraint
{
    public $em = null;
    public $errorMethod = null;
    public $message = 'The name "%name%" already exists.';
    public $service = 'ibiz.validator.unique_path';

    public function validatedBy()
    {
        return $this->service;
    }

    public function getRequiredOptions()
    {
        return array('errorMethod');
    }

    public function getDefaultOption()
    {
        return 'errorMethod';
    }

    public function getTargets()
    {
        return self::CLASS_CONSTRAINT;
    }
}

Validator/ Ограничения / UniquePathValidator.php

namespace Ibiz\DoctrineExtensionsBundle\Validator\Constraints;

use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;

class UniquePathValidator extends ConstraintValidator
{
    private $registry;

    public function __construct(ManagerRegistry $registry)
    {
        $this->registry = $registry;
    }

    public function validate($entity, Constraint $constraint)
    {
        if ($constraint->errorMethod === null)
        {
            throw new ConstraintDefinitionException('ErrorMethod should be set');
        } else if (!is_string($constraint->errorMethod)) {
            throw new UnexpectedTypeException($constraint->errorMethod, 'string');
        }

        if ($constraint->em) {
            $em = $this->registry->getManager($constraint->em);
        } else {
            $em = $this->registry->getManagerForClass(get_class($entity));
        }

        $className = $this->context->getClassName();
        $repo = $em->getRepository($className);

        $count = $repo->getSameNameSiblingsCount($entity);

        if ($count != 0) {
            $this->context->addViolation($constraint->message, array('%name%' => $entity->{$constraint->errorMethod}()));
        }
    }
}

Entity / Repository / NestedTreeRepository.php

namespace Ibiz\DoctrineExtensionsBundle\Entity\Repository;

use Gedmo\Tree\Entity\Repository\NestedTreeRepository as BaseRepository;

class NestedTreeRepository extends BaseRepository
{
    public function getSameNameSiblingsCountQueryBuilder($node)
    {
        $meta = $this->getClassMetadata();
        if (!$node instanceof $meta->name) {
            throw new InvalidArgumentException("Node is not related to this repository");
        }

        $config = $this->listener->getConfiguration($this->_em, $meta->name);
        $qb = $this->_em->createQueryBuilder();

        $qb->select($qb->expr()->count('n.id'))
            ->from($config['useObjectClass'], 'n');
        if ($node->getParent() === null) {
            $qb->where($qb->expr()->andx(
                   $qb->expr()->eq('n.name', ':name'),
                   $qb->expr()->isNull('n.parent')
               ))
               ->setParameters(array(
                   'name' => $node->getName(),
               ));
        } else {
            $qb->leftJoin('n.parent', 'p')
               ->where($qb->expr()->andx(
                   $qb->expr()->eq('n.name', ':name'),
                   $qb->expr()->eq('p.name', ':parent')
               ))
               ->setParameters(array(
                   'name' => $node->getName(),
                   'parent' => $node->getParent()->getName(),
               ));
        }

        return $qb;
    }

    public function getSameNameSiblingsCountQuery($node)
    {
        return $this->getSameNameSiblingsCountQueryBuilder($node)->getQuery();
    }

    public function getSameNameSiblingsCount($node)
    {
        return $this->getSameNameSiblingsCountQuery($node)->getSingleScalarResult();
    }
}
Другие вопросы по тегам