vendor/symfony/doctrine-bridge/Validator/Constraints/UniqueEntityValidator.php line 43

  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Bridge\Doctrine\Validator\Constraints;
  11. use Doctrine\Persistence\ManagerRegistry;
  12. use Doctrine\Persistence\Mapping\ClassMetadata;
  13. use Doctrine\Persistence\ObjectManager;
  14. use Symfony\Component\Validator\Constraint;
  15. use Symfony\Component\Validator\ConstraintValidator;
  16. use Symfony\Component\Validator\Exception\ConstraintDefinitionException;
  17. use Symfony\Component\Validator\Exception\UnexpectedTypeException;
  18. use Symfony\Component\Validator\Exception\UnexpectedValueException;
  19. /**
  20.  * Unique Entity Validator checks if one or a set of fields contain unique values.
  21.  *
  22.  * @author Benjamin Eberlei <kontakt@beberlei.de>
  23.  */
  24. class UniqueEntityValidator extends ConstraintValidator
  25. {
  26.     private ManagerRegistry $registry;
  27.     public function __construct(ManagerRegistry $registry)
  28.     {
  29.         $this->registry $registry;
  30.     }
  31.     /**
  32.      * @param object $entity
  33.      *
  34.      * @throws UnexpectedTypeException
  35.      * @throws ConstraintDefinitionException
  36.      */
  37.     public function validate(mixed $entityConstraint $constraint)
  38.     {
  39.         if (!$constraint instanceof UniqueEntity) {
  40.             throw new UnexpectedTypeException($constraintUniqueEntity::class);
  41.         }
  42.         if (!\is_array($constraint->fields) && !\is_string($constraint->fields)) {
  43.             throw new UnexpectedTypeException($constraint->fields'array');
  44.         }
  45.         if (null !== $constraint->errorPath && !\is_string($constraint->errorPath)) {
  46.             throw new UnexpectedTypeException($constraint->errorPath'string or null');
  47.         }
  48.         $fields = (array) $constraint->fields;
  49.         if (=== \count($fields)) {
  50.             throw new ConstraintDefinitionException('At least one field has to be specified.');
  51.         }
  52.         if (null === $entity) {
  53.             return;
  54.         }
  55.         if (!\is_object($entity)) {
  56.             throw new UnexpectedValueException($entity'object');
  57.         }
  58.         if ($constraint->em) {
  59.             $em $this->registry->getManager($constraint->em);
  60.             if (!$em) {
  61.                 throw new ConstraintDefinitionException(sprintf('Object manager "%s" does not exist.'$constraint->em));
  62.             }
  63.         } else {
  64.             $em $this->registry->getManagerForClass($entity::class);
  65.             if (!$em) {
  66.                 throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".'get_debug_type($entity)));
  67.             }
  68.         }
  69.         $class $em->getClassMetadata($entity::class);
  70.         $criteria = [];
  71.         $hasNullValue false;
  72.         foreach ($fields as $fieldName) {
  73.             if (!$class->hasField($fieldName) && !$class->hasAssociation($fieldName)) {
  74.                 throw new ConstraintDefinitionException(sprintf('The field "%s" is not mapped by Doctrine, so it cannot be validated for uniqueness.'$fieldName));
  75.             }
  76.             $fieldValue $class->reflFields[$fieldName]->getValue($entity);
  77.             if (null === $fieldValue) {
  78.                 $hasNullValue true;
  79.             }
  80.             if ($constraint->ignoreNull && null === $fieldValue) {
  81.                 continue;
  82.             }
  83.             $criteria[$fieldName] = $fieldValue;
  84.             if (null !== $criteria[$fieldName] && $class->hasAssociation($fieldName)) {
  85.                 /* Ensure the Proxy is initialized before using reflection to
  86.                  * read its identifiers. This is necessary because the wrapped
  87.                  * getter methods in the Proxy are being bypassed.
  88.                  */
  89.                 $em->initializeObject($criteria[$fieldName]);
  90.             }
  91.         }
  92.         // validation doesn't fail if one of the fields is null and if null values should be ignored
  93.         if ($hasNullValue && $constraint->ignoreNull) {
  94.             return;
  95.         }
  96.         // skip validation if there are no criteria (this can happen when the
  97.         // "ignoreNull" option is enabled and fields to be checked are null
  98.         if (empty($criteria)) {
  99.             return;
  100.         }
  101.         if (null !== $constraint->entityClass) {
  102.             /* Retrieve repository from given entity name.
  103.              * We ensure the retrieved repository can handle the entity
  104.              * by checking the entity is the same, or subclass of the supported entity.
  105.              */
  106.             $repository $em->getRepository($constraint->entityClass);
  107.             $supportedClass $repository->getClassName();
  108.             if (!$entity instanceof $supportedClass) {
  109.                 throw new ConstraintDefinitionException(sprintf('The "%s" entity repository does not support the "%s" entity. The entity should be an instance of or extend "%s".'$constraint->entityClass$class->getName(), $supportedClass));
  110.             }
  111.         } else {
  112.             $repository $em->getRepository($entity::class);
  113.         }
  114.         $arguments = [$criteria];
  115.         /* If the default repository method is used, it is always enough to retrieve at most two entities because:
  116.          * - No entity returned, the current entity is definitely unique.
  117.          * - More than one entity returned, the current entity cannot be unique.
  118.          * - One entity returned the uniqueness depends on the current entity.
  119.          */
  120.         if ('findBy' === $constraint->repositoryMethod) {
  121.             $arguments = [$criterianull2];
  122.         }
  123.         $result $repository->{$constraint->repositoryMethod}(...$arguments);
  124.         if ($result instanceof \IteratorAggregate) {
  125.             $result $result->getIterator();
  126.         }
  127.         /* If the result is a MongoCursor, it must be advanced to the first
  128.          * element. Rewinding should have no ill effect if $result is another
  129.          * iterator implementation.
  130.          */
  131.         if ($result instanceof \Iterator) {
  132.             $result->rewind();
  133.             if ($result instanceof \Countable && \count($result)) {
  134.                 $result = [$result->current(), $result->current()];
  135.             } else {
  136.                 $result $result->valid() && null !== $result->current() ? [$result->current()] : [];
  137.             }
  138.         } elseif (\is_array($result)) {
  139.             reset($result);
  140.         } else {
  141.             $result null === $result ? [] : [$result];
  142.         }
  143.         /* If no entity matched the query criteria or a single entity matched,
  144.          * which is the same as the entity being validated, the criteria is
  145.          * unique.
  146.          */
  147.         if (!$result || (=== \count($result) && current($result) === $entity)) {
  148.             return;
  149.         }
  150.         $errorPath $constraint->errorPath ?? $fields[0];
  151.         $invalidValue $criteria[$errorPath] ?? $criteria[$fields[0]];
  152.         $this->context->buildViolation($constraint->message)
  153.             ->atPath($errorPath)
  154.             ->setParameter('{{ value }}'$this->formatWithIdentifiers($em$class$invalidValue))
  155.             ->setInvalidValue($invalidValue)
  156.             ->setCode(UniqueEntity::NOT_UNIQUE_ERROR)
  157.             ->setCause($result)
  158.             ->addViolation();
  159.     }
  160.     private function formatWithIdentifiers(ObjectManager $emClassMetadata $classmixed $value)
  161.     {
  162.         if (!\is_object($value) || $value instanceof \DateTimeInterface) {
  163.             return $this->formatValue($valueself::PRETTY_DATE);
  164.         }
  165.         if ($value instanceof \Stringable) {
  166.             return (string) $value;
  167.         }
  168.         if ($class->getName() !== $idClass $value::class) {
  169.             // non unique value might be a composite PK that consists of other entity objects
  170.             if ($em->getMetadataFactory()->hasMetadataFor($idClass)) {
  171.                 $identifiers $em->getClassMetadata($idClass)->getIdentifierValues($value);
  172.             } else {
  173.                 // this case might happen if the non unique column has a custom doctrine type and its value is an object
  174.                 // in which case we cannot get any identifiers for it
  175.                 $identifiers = [];
  176.             }
  177.         } else {
  178.             $identifiers $class->getIdentifierValues($value);
  179.         }
  180.         if (!$identifiers) {
  181.             return sprintf('object("%s")'$idClass);
  182.         }
  183.         array_walk($identifiers, function (&$id$field) {
  184.             if (!\is_object($id) || $id instanceof \DateTimeInterface) {
  185.                 $idAsString $this->formatValue($idself::PRETTY_DATE);
  186.             } else {
  187.                 $idAsString sprintf('object("%s")'$id::class);
  188.             }
  189.             $id sprintf('%s => %s'$field$idAsString);
  190.         });
  191.         return sprintf('object("%s") identified by (%s)'$idClassimplode(', '$identifiers));
  192.     }
  193. }