vendor/sonata-project/doctrine-orm-admin-bundle/src/Model/ModelManager.php line 44

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of the Sonata Project package.
  5.  *
  6.  * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Sonata\DoctrineORMAdminBundle\Model;
  12. use Doctrine\DBAL\Exception;
  13. use Doctrine\DBAL\LockMode;
  14. use Doctrine\DBAL\Platforms\AbstractPlatform;
  15. use Doctrine\DBAL\Types\Type;
  16. use Doctrine\ORM\AbstractQuery;
  17. use Doctrine\ORM\EntityManagerInterface;
  18. use Doctrine\ORM\Mapping\ClassMetadata;
  19. use Doctrine\ORM\OptimisticLockException;
  20. use Doctrine\ORM\QueryBuilder;
  21. use Doctrine\ORM\Tools\Pagination\Paginator;
  22. use Doctrine\ORM\UnitOfWork;
  23. use Doctrine\Persistence\ManagerRegistry;
  24. use Doctrine\Persistence\Mapping\MappingException;
  25. use Sonata\AdminBundle\Datagrid\ProxyQueryInterface as BaseProxyQueryInterface;
  26. use Sonata\AdminBundle\Exception\LockException;
  27. use Sonata\AdminBundle\Exception\ModelManagerException;
  28. use Sonata\AdminBundle\Model\LockInterface;
  29. use Sonata\AdminBundle\Model\ModelManagerInterface;
  30. use Sonata\AdminBundle\Model\ProxyResolverInterface;
  31. use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery;
  32. use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQueryInterface;
  33. use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
  34. /**
  35.  * @phpstan-template T of object
  36.  * @phpstan-implements ModelManagerInterface<T>
  37.  * @phpstan-implements LockInterface<T>
  38.  */
  39. final class ModelManager implements ModelManagerInterfaceLockInterfaceProxyResolverInterface
  40. {
  41.     public const ID_SEPARATOR '~';
  42.     private const BATCH_SIZE 20;
  43.     /**
  44.      * @var EntityManagerInterface[]
  45.      */
  46.     private array $cache = [];
  47.     public function __construct(
  48.         private ManagerRegistry $registry,
  49.         private PropertyAccessorInterface $propertyAccessor
  50.     ) {
  51.     }
  52.     public function getRealClass(object $object): string
  53.     {
  54.         $class $object::class;
  55.         $em $this->registry->getManagerForClass($class);
  56.         if (null === $em) {
  57.             return $class;
  58.         }
  59.         try {
  60.             return $em->getClassMetadata($class)->getName();
  61.         } catch (MappingException) {
  62.             return $class;
  63.         }
  64.     }
  65.     public function create(object $object): void
  66.     {
  67.         try {
  68.             $entityManager $this->getEntityManager($object);
  69.             $entityManager->persist($object);
  70.             $entityManager->flush();
  71.         } catch (\PDOException|Exception $exception) {
  72.             throw new ModelManagerException(
  73.                 sprintf('Failed to create object: %s'$this->getRealClass($object)),
  74.                 (int) $exception->getCode(),
  75.                 $exception
  76.             );
  77.         }
  78.     }
  79.     public function update(object $object): void
  80.     {
  81.         try {
  82.             $entityManager $this->getEntityManager($object);
  83.             $entityManager->persist($object);
  84.             $entityManager->flush();
  85.         } catch (\PDOException|Exception $exception) {
  86.             throw new ModelManagerException(
  87.                 sprintf('Failed to update object: %s'$this->getRealClass($object)),
  88.                 (int) $exception->getCode(),
  89.                 $exception
  90.             );
  91.         }
  92.     }
  93.     public function delete(object $object): void
  94.     {
  95.         try {
  96.             $entityManager $this->getEntityManager($object);
  97.             $entityManager->remove($object);
  98.             $entityManager->flush();
  99.         } catch (\PDOException|Exception $exception) {
  100.             throw new ModelManagerException(
  101.                 sprintf('Failed to delete object: %s'$this->getRealClass($object)),
  102.                 (int) $exception->getCode(),
  103.                 $exception
  104.             );
  105.         }
  106.     }
  107.     public function getLockVersion(object $object)
  108.     {
  109.         $metadata $this->getMetadata($object::class);
  110.         if (!$metadata->isVersioned || !isset($metadata->reflFields[$metadata->versionField])) {
  111.             return null;
  112.         }
  113.         return $metadata->reflFields[$metadata->versionField]->getValue($object);
  114.     }
  115.     public function lock(object $object, ?int $expectedVersion): void
  116.     {
  117.         $metadata $this->getMetadata($object::class);
  118.         if (!$metadata->isVersioned) {
  119.             return;
  120.         }
  121.         try {
  122.             $entityManager $this->getEntityManager($object);
  123.             $entityManager->lock($objectLockMode::OPTIMISTIC$expectedVersion);
  124.         } catch (OptimisticLockException $exception) {
  125.             throw new LockException(
  126.                 $exception->getMessage(),
  127.                 $exception->getCode(),
  128.                 $exception
  129.             );
  130.         }
  131.     }
  132.     /**
  133.      * @param int|string $id
  134.      *
  135.      * @phpstan-param class-string<T> $class
  136.      * @phpstan-return T|null
  137.      */
  138.     public function find(string $class$id): ?object
  139.     {
  140.         $values array_combine($this->getIdentifierFieldNames($class), explode(self::ID_SEPARATOR, (string) $id));
  141.         return $this->getEntityManager($class)->getRepository($class)->find($values);
  142.     }
  143.     /**
  144.      * @phpstan-param class-string<T> $class
  145.      * @phpstan-return array<T>
  146.      */
  147.     public function findBy(string $class, array $criteria = []): array
  148.     {
  149.         return $this->getEntityManager($class)->getRepository($class)->findBy($criteria);
  150.     }
  151.     /**
  152.      * @phpstan-param class-string<T> $class
  153.      * @phpstan-return T|null
  154.      */
  155.     public function findOneBy(string $class, array $criteria = []): ?object
  156.     {
  157.         return $this->getEntityManager($class)->getRepository($class)->findOneBy($criteria);
  158.     }
  159.     /**
  160.      * NEXT_MAJOR: Change the visibility to private.
  161.      *
  162.      * @param string|object $class
  163.      *
  164.      * @phpstan-param class-string|object $class
  165.      */
  166.     public function getEntityManager($class): EntityManagerInterface
  167.     {
  168.         if (\is_object($class)) {
  169.             $class $class::class;
  170.         }
  171.         if (!isset($this->cache[$class])) {
  172.             $em $this->registry->getManagerForClass($class);
  173.             if (!$em instanceof EntityManagerInterface) {
  174.                 throw new \RuntimeException(sprintf('No entity manager defined for class %s'$class));
  175.             }
  176.             $this->cache[$class] = $em;
  177.         }
  178.         return $this->cache[$class];
  179.     }
  180.     public function createQuery(string $classstring $alias 'o'): BaseProxyQueryInterface
  181.     {
  182.         $repository $this->getEntityManager($class)->getRepository($class);
  183.         /** @phpstan-var ProxyQuery<T> $proxyQuery */
  184.         $proxyQuery = new ProxyQuery($repository->createQueryBuilder($alias));
  185.         return $proxyQuery;
  186.     }
  187.     public function supportsQuery(object $query): bool
  188.     {
  189.         return $query instanceof ProxyQuery || $query instanceof AbstractQuery || $query instanceof QueryBuilder;
  190.     }
  191.     public function executeQuery(object $query)
  192.     {
  193.         if ($query instanceof QueryBuilder) {
  194.             return $query->getQuery()->execute();
  195.         }
  196.         if ($query instanceof AbstractQuery) {
  197.             return $query->execute();
  198.         }
  199.         if ($query instanceof ProxyQuery) {
  200.             /** @phpstan-var Paginator<T> $results */
  201.             $results $query->execute();
  202.             return $results;
  203.         }
  204.         throw new \InvalidArgumentException(sprintf(
  205.             'Argument 1 passed to %s() must be an instance of %s, %s, or %s',
  206.             __METHOD__,
  207.             QueryBuilder::class,
  208.             AbstractQuery::class,
  209.             ProxyQuery::class
  210.         ));
  211.     }
  212.     public function getIdentifierValues(object $model): array
  213.     {
  214.         $metadata $this->getMetadata($model::class);
  215.         $platform $this->getEntityManager($model::class)->getConnection()->getDatabasePlatform();
  216.         $identifiers = [];
  217.         foreach ($metadata->getIdentifierValues($model) as $name => $value) {
  218.             if (!\is_object($value)) {
  219.                 $identifiers[] = $value;
  220.                 continue;
  221.             }
  222.             $fieldType $metadata->getTypeOfField($name);
  223.             if (null !== $fieldType && Type::hasType($fieldType)) {
  224.                 $identifiers[] = $this->getValueFromType($valueType::getType($fieldType), $fieldType$platform);
  225.                 continue;
  226.             }
  227.             $identifierMetadata $this->getMetadata($value::class);
  228.             foreach ($identifierMetadata->getIdentifierValues($value) as $identifierValue) {
  229.                 $identifiers[] = $identifierValue;
  230.             }
  231.         }
  232.         return $identifiers;
  233.     }
  234.     public function getIdentifierFieldNames(string $class): array
  235.     {
  236.         return $this->getMetadata($class)->getIdentifierFieldNames();
  237.     }
  238.     public function getNormalizedIdentifier(object $model): ?string
  239.     {
  240.         if (\in_array($this->getEntityManager($model)->getUnitOfWork()->getEntityState($model), [
  241.             UnitOfWork::STATE_NEW,
  242.             UnitOfWork::STATE_REMOVED,
  243.         ], true)) {
  244.             return null;
  245.         }
  246.         $values $this->getIdentifierValues($model);
  247.         if (=== \count($values)) {
  248.             return null;
  249.         }
  250.         return implode(self::ID_SEPARATOR$values);
  251.     }
  252.     /**
  253.      * The ORM implementation does nothing special but you still should use
  254.      * this method when using the id in a URL to allow for future improvements.
  255.      */
  256.     public function getUrlSafeIdentifier(object $model): ?string
  257.     {
  258.         return $this->getNormalizedIdentifier($model);
  259.     }
  260.     /**
  261.      * @throws \InvalidArgumentException if value passed as argument 3 is an empty array
  262.      */
  263.     public function addIdentifiersToQuery(string $classBaseProxyQueryInterface $query, array $idx): void
  264.     {
  265.         if (!$query instanceof ProxyQueryInterface) {
  266.             throw new \TypeError(sprintf('The query MUST implement %s.'ProxyQueryInterface::class));
  267.         }
  268.         if ([] === $idx) {
  269.             throw new \InvalidArgumentException(sprintf(
  270.                 'Array passed as argument 3 to "%s()" must not be empty.',
  271.                 __METHOD__
  272.             ));
  273.         }
  274.         $fieldNames $this->getIdentifierFieldNames($class);
  275.         $qb $query->getQueryBuilder();
  276.         $rootAlias current($qb->getRootAliases());
  277.         $metadata $this->getMetadata($class);
  278.         $prefix uniqid();
  279.         $sqls = [];
  280.         foreach ($idx as $pos => $id) {
  281.             $ids explode(self::ID_SEPARATOR, (string) $id);
  282.             $ands = [];
  283.             foreach ($fieldNames as $index => $name) {
  284.                 $parameterName sprintf('field_%s_%s_%d'$prefix$name$pos);
  285.                 $ands[] = sprintf('%s.%s = :%s'$rootAlias$name$parameterName);
  286.                 $qb->setParameter(
  287.                     $parameterName,
  288.                     $ids[$index],
  289.                     $metadata->getTypeOfField($name)
  290.                 );
  291.             }
  292.             $sqls[] = implode(' AND '$ands);
  293.         }
  294.         $qb->andWhere(sprintf('( %s )'implode(' OR '$sqls)));
  295.     }
  296.     public function batchDelete(string $classBaseProxyQueryInterface $queryint $batchSize self::BATCH_SIZE): void
  297.     {
  298.         if (!$query instanceof ProxyQueryInterface) {
  299.             throw new \TypeError(sprintf('The query MUST implement %s.'ProxyQueryInterface::class));
  300.         }
  301.         if ([] !== $query->getQueryBuilder()->getDQLPart('join')) {
  302.             $rootAlias current($query->getQueryBuilder()->getRootAliases());
  303.             // Distinct is needed to iterate, even if group by is used
  304.             // @see https://github.com/doctrine/orm/issues/5868
  305.             $query->getQueryBuilder()->distinct();
  306.             $query->getQueryBuilder()->select($rootAlias);
  307.         }
  308.         $entityManager $this->getEntityManager($class);
  309.         $i 0;
  310.         $confirmedDeletionsCount 0;
  311.         try {
  312.             foreach ($query->getDoctrineQuery()->toIterable() as $object) {
  313.                 $entityManager->remove($object);
  314.                 if (=== (++$i $batchSize)) {
  315.                     $entityManager->flush();
  316.                     $confirmedDeletionsCount $i;
  317.                     $entityManager->clear();
  318.                 }
  319.             }
  320.             $entityManager->flush();
  321.             $entityManager->clear();
  322.         } catch (\PDOException|Exception $exception) {
  323.             $id null;
  324.             if (isset($object)) {
  325.                 $id $this->getNormalizedIdentifier($object);
  326.             }
  327.             if (null === $id) {
  328.                 throw new ModelManagerException(
  329.                     sprintf('Failed to perform batch deletion for "%s" objects'$class),
  330.                     (int) $exception->getCode(),
  331.                     $exception
  332.                 );
  333.             }
  334.             $msg 'Failed to delete object "%s" (id: %s) while performing batch deletion';
  335.             if ($i $batchSize) {
  336.                 $msg .= sprintf(' (%u objects were successfully deleted before this error)'$confirmedDeletionsCount);
  337.             }
  338.             throw new ModelManagerException(
  339.                 sprintf(
  340.                     $msg,
  341.                     $class,
  342.                     $id
  343.                 ),
  344.                 (int) $exception->getCode(),
  345.                 $exception
  346.             );
  347.         }
  348.     }
  349.     public function getExportFields(string $class): array
  350.     {
  351.         return $this->getMetadata($class)->getFieldNames();
  352.     }
  353.     public function reverseTransform(object $object, array $array = []): void
  354.     {
  355.         $metadata $this->getMetadata($object::class);
  356.         foreach ($array as $name => $value) {
  357.             $property $this->getFieldName($metadata$name);
  358.             $this->propertyAccessor->setValue($object$property$value);
  359.         }
  360.     }
  361.     /**
  362.      * @phpstan-template TObject of object
  363.      * @phpstan-param class-string<TObject> $class
  364.      * @phpstan-return ClassMetadata<TObject>
  365.      */
  366.     private function getMetadata(string $class): ClassMetadata
  367.     {
  368.         return $this->getEntityManager($class)->getClassMetadata($class);
  369.     }
  370.     /**
  371.      * @param ClassMetadata<object> $metadata
  372.      */
  373.     private function getFieldName(ClassMetadata $metadatastring $name): string
  374.     {
  375.         if (\array_key_exists($name$metadata->fieldMappings)) {
  376.             return $metadata->fieldMappings[$name]['fieldName'];
  377.         }
  378.         if (\array_key_exists($name$metadata->associationMappings)) {
  379.             return $metadata->associationMappings[$name]['fieldName'];
  380.         }
  381.         return $name;
  382.     }
  383.     private function getValueFromType(object $valueType $typestring $fieldTypeAbstractPlatform $platform): string
  384.     {
  385.         if ($platform->hasDoctrineTypeMappingFor($fieldType)
  386.             && 'binary' === $platform->getDoctrineTypeMapping($fieldType)
  387.         ) {
  388.             return (string) $type->convertToPHPValue($value$platform);
  389.         }
  390.         // some libraries may have `toString()` implementation
  391.         if (\is_callable([$value'toString'])) {
  392.             return $value->toString();
  393.         }
  394.         // final fallback to magic `__toString()` which may throw an exception in 7.4
  395.         if (method_exists($value'__toString')) {
  396.             return $value->__toString();
  397.         }
  398.         return (string) $type->convertToDatabaseValue($value$platform);
  399.     }
  400. }