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