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

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\DBALException;
  14. use Doctrine\DBAL\LockMode;
  15. use Doctrine\DBAL\Platforms\AbstractPlatform;
  16. use Doctrine\DBAL\Types\Type;
  17. use Doctrine\ORM\EntityManager;
  18. use Doctrine\ORM\Mapping\ClassMetadata;
  19. use Doctrine\ORM\OptimisticLockException;
  20. use Doctrine\ORM\Query;
  21. use Doctrine\ORM\QueryBuilder;
  22. use Doctrine\ORM\UnitOfWork;
  23. use Doctrine\Persistence\ManagerRegistry;
  24. use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
  25. use Sonata\AdminBundle\Datagrid\DatagridInterface;
  26. use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
  27. use Sonata\AdminBundle\Exception\LockException;
  28. use Sonata\AdminBundle\Exception\ModelManagerException;
  29. use Sonata\AdminBundle\Model\LockInterface;
  30. use Sonata\AdminBundle\Model\ModelManagerInterface;
  31. use Sonata\DoctrineORMAdminBundle\Admin\FieldDescription;
  32. use Sonata\DoctrineORMAdminBundle\Datagrid\OrderByToSelectWalker;
  33. use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery;
  34. use Sonata\Exporter\Source\DoctrineORMQuerySourceIterator;
  35. use Symfony\Component\Form\Exception\PropertyAccessDeniedException;
  36. class ModelManager implements ModelManagerInterfaceLockInterface
  37. {
  38.     public const ID_SEPARATOR '~';
  39.     /**
  40.      * @var ManagerRegistry
  41.      */
  42.     protected $registry;
  43.     /**
  44.      * @var EntityManager[]
  45.      */
  46.     protected $cache = [];
  47.     public function __construct(ManagerRegistry $registry)
  48.     {
  49.         $this->registry $registry;
  50.     }
  51.     /**
  52.      * @param string $class
  53.      *
  54.      * @return ClassMetadata
  55.      */
  56.     public function getMetadata($class)
  57.     {
  58.         return $this->getEntityManager($class)->getMetadataFactory()->getMetadataFor($class);
  59.     }
  60.     /**
  61.      * Returns the model's metadata holding the fully qualified property, and the last
  62.      * property name.
  63.      *
  64.      * @param string $baseClass        The base class of the model holding the fully qualified property
  65.      * @param string $propertyFullName The name of the fully qualified property (dot ('.') separated
  66.      *                                 property string)
  67.      *
  68.      * @return array(
  69.      *                \Doctrine\ORM\Mapping\ClassMetadata $parentMetadata,
  70.      *                string $lastPropertyName,
  71.      *                array $parentAssociationMappings
  72.      *                )
  73.      */
  74.     public function getParentMetadataForProperty($baseClass$propertyFullName)
  75.     {
  76.         $nameElements explode('.'$propertyFullName);
  77.         $lastPropertyName array_pop($nameElements);
  78.         $class $baseClass;
  79.         $parentAssociationMappings = [];
  80.         foreach ($nameElements as $nameElement) {
  81.             $metadata $this->getMetadata($class);
  82.             if (isset($metadata->associationMappings[$nameElement])) {
  83.                 $parentAssociationMappings[] = $metadata->associationMappings[$nameElement];
  84.                 $class $metadata->getAssociationTargetClass($nameElement);
  85.                 continue;
  86.             }
  87.             break;
  88.         }
  89.         $properties = \array_slice($nameElements, \count($parentAssociationMappings));
  90.         $properties[] = $lastPropertyName;
  91.         return [$this->getMetadata($class), implode('.'$properties), $parentAssociationMappings];
  92.     }
  93.     /**
  94.      * @param string $class
  95.      *
  96.      * @return bool
  97.      */
  98.     public function hasMetadata($class)
  99.     {
  100.         return $this->getEntityManager($class)->getMetadataFactory()->hasMetadataFor($class);
  101.     }
  102.     public function getNewFieldDescriptionInstance($class$name, array $options = [])
  103.     {
  104.         if (!\is_string($name)) {
  105.             throw new \RuntimeException('The name argument must be a string');
  106.         }
  107.         if (!isset($options['route']['name'])) {
  108.             $options['route']['name'] = 'edit';
  109.         }
  110.         if (!isset($options['route']['parameters'])) {
  111.             $options['route']['parameters'] = [];
  112.         }
  113.         list($metadata$propertyName$parentAssociationMappings) = $this->getParentMetadataForProperty($class$name);
  114.         $fieldDescription = new FieldDescription();
  115.         $fieldDescription->setName($name);
  116.         $fieldDescription->setOptions($options);
  117.         $fieldDescription->setParentAssociationMappings($parentAssociationMappings);
  118.         if (isset($metadata->associationMappings[$propertyName])) {
  119.             $fieldDescription->setAssociationMapping($metadata->associationMappings[$propertyName]);
  120.         }
  121.         if (isset($metadata->fieldMappings[$propertyName])) {
  122.             $fieldDescription->setFieldMapping($metadata->fieldMappings[$propertyName]);
  123.         }
  124.         return $fieldDescription;
  125.     }
  126.     public function create($object)
  127.     {
  128.         try {
  129.             $entityManager $this->getEntityManager($object);
  130.             $entityManager->persist($object);
  131.             $entityManager->flush();
  132.         } catch (\PDOException $e) {
  133.             throw new ModelManagerException(
  134.                 sprintf('Failed to create object: %s'ClassUtils::getClass($object)),
  135.                 $e->getCode(),
  136.                 $e
  137.             );
  138.         } catch (DBALException $e) {
  139.             throw new ModelManagerException(
  140.                 sprintf('Failed to create object: %s'ClassUtils::getClass($object)),
  141.                 $e->getCode(),
  142.                 $e
  143.             );
  144.         }
  145.     }
  146.     public function update($object)
  147.     {
  148.         try {
  149.             $entityManager $this->getEntityManager($object);
  150.             $entityManager->persist($object);
  151.             $entityManager->flush();
  152.         } catch (\PDOException $e) {
  153.             throw new ModelManagerException(
  154.                 sprintf('Failed to update object: %s'ClassUtils::getClass($object)),
  155.                 $e->getCode(),
  156.                 $e
  157.             );
  158.         } catch (DBALException $e) {
  159.             throw new ModelManagerException(
  160.                 sprintf('Failed to update object: %s'ClassUtils::getClass($object)),
  161.                 $e->getCode(),
  162.                 $e
  163.             );
  164.         }
  165.     }
  166.     public function delete($object)
  167.     {
  168.         try {
  169.             $entityManager $this->getEntityManager($object);
  170.             $entityManager->remove($object);
  171.             $entityManager->flush();
  172.         } catch (\PDOException $e) {
  173.             throw new ModelManagerException(
  174.                 sprintf('Failed to delete object: %s'ClassUtils::getClass($object)),
  175.                 $e->getCode(),
  176.                 $e
  177.             );
  178.         } catch (DBALException $e) {
  179.             throw new ModelManagerException(
  180.                 sprintf('Failed to delete object: %s'ClassUtils::getClass($object)),
  181.                 $e->getCode(),
  182.                 $e
  183.             );
  184.         }
  185.     }
  186.     public function getLockVersion($object)
  187.     {
  188.         $metadata $this->getMetadata(ClassUtils::getClass($object));
  189.         if (!$metadata->isVersioned) {
  190.             return null;
  191.         }
  192.         return $metadata->reflFields[$metadata->versionField]->getValue($object);
  193.     }
  194.     public function lock($object$expectedVersion)
  195.     {
  196.         $metadata $this->getMetadata(ClassUtils::getClass($object));
  197.         if (!$metadata->isVersioned) {
  198.             return;
  199.         }
  200.         try {
  201.             $entityManager $this->getEntityManager($object);
  202.             $entityManager->lock($objectLockMode::OPTIMISTIC$expectedVersion);
  203.         } catch (OptimisticLockException $e) {
  204.             throw new LockException($e->getMessage(), $e->getCode(), $e);
  205.         }
  206.     }
  207.     public function find($class$id)
  208.     {
  209.         if (!isset($id)) {
  210.             return null;
  211.         }
  212.         $values array_combine($this->getIdentifierFieldNames($class), explode(self::ID_SEPARATOR, (string) $id));
  213.         return $this->getEntityManager($class)->getRepository($class)->find($values);
  214.     }
  215.     public function findBy($class, array $criteria = [])
  216.     {
  217.         return $this->getEntityManager($class)->getRepository($class)->findBy($criteria);
  218.     }
  219.     public function findOneBy($class, array $criteria = [])
  220.     {
  221.         return $this->getEntityManager($class)->getRepository($class)->findOneBy($criteria);
  222.     }
  223.     /**
  224.      * @param string $class
  225.      *
  226.      * @return EntityManager
  227.      */
  228.     public function getEntityManager($class)
  229.     {
  230.         if (\is_object($class)) {
  231.             $class = \get_class($class);
  232.         }
  233.         if (!isset($this->cache[$class])) {
  234.             $em $this->registry->getManagerForClass($class);
  235.             if (!$em) {
  236.                 throw new \RuntimeException(sprintf('No entity manager defined for class %s'$class));
  237.             }
  238.             $this->cache[$class] = $em;
  239.         }
  240.         return $this->cache[$class];
  241.     }
  242.     public function getParentFieldDescription($parentAssociationMapping$class)
  243.     {
  244.         $fieldName $parentAssociationMapping['fieldName'];
  245.         $metadata $this->getMetadata($class);
  246.         $associatingMapping $metadata->associationMappings[$parentAssociationMapping];
  247.         $fieldDescription $this->getNewFieldDescriptionInstance($class$fieldName);
  248.         $fieldDescription->setName($parentAssociationMapping);
  249.         $fieldDescription->setAssociationMapping($associatingMapping);
  250.         return $fieldDescription;
  251.     }
  252.     public function createQuery($class$alias 'o')
  253.     {
  254.         $repository $this->getEntityManager($class)->getRepository($class);
  255.         return new ProxyQuery($repository->createQueryBuilder($alias));
  256.     }
  257.     public function executeQuery($query)
  258.     {
  259.         if ($query instanceof QueryBuilder) {
  260.             return $query->getQuery()->execute();
  261.         }
  262.         return $query->execute();
  263.     }
  264.     /**
  265.      * NEXT_MAJOR: Remove this function.
  266.      *
  267.      * @deprecated since sonata-project/doctrine-orm-admin-bundle 3.x. To be removed in 4.0.
  268.      */
  269.     public function getModelIdentifier($class)
  270.     {
  271.         return $this->getMetadata($class)->identifier;
  272.     }
  273.     public function getIdentifierValues($entity)
  274.     {
  275.         // Fix code has an impact on performance, so disable it ...
  276.         //$entityManager = $this->getEntityManager($entity);
  277.         //if (!$entityManager->getUnitOfWork()->isInIdentityMap($entity)) {
  278.         //    throw new \RuntimeException('Entities passed to the choice field must be managed');
  279.         //}
  280.         $class ClassUtils::getClass($entity);
  281.         $metadata $this->getMetadata($class);
  282.         $platform $this->getEntityManager($class)->getConnection()->getDatabasePlatform();
  283.         $identifiers = [];
  284.         foreach ($metadata->getIdentifierValues($entity) as $name => $value) {
  285.             if (!\is_object($value)) {
  286.                 $identifiers[] = $value;
  287.                 continue;
  288.             }
  289.             $fieldType $metadata->getTypeOfField($name);
  290.             $type $fieldType && Type::hasType($fieldType) ? Type::getType($fieldType) : null;
  291.             if ($type) {
  292.                 $identifiers[] = $this->getValueFromType($value$type$fieldType$platform);
  293.                 continue;
  294.             }
  295.             $identifierMetadata $this->getMetadata(ClassUtils::getClass($value));
  296.             foreach ($identifierMetadata->getIdentifierValues($value) as $value) {
  297.                 $identifiers[] = $value;
  298.             }
  299.         }
  300.         return $identifiers;
  301.     }
  302.     public function getIdentifierFieldNames($class)
  303.     {
  304.         return $this->getMetadata($class)->getIdentifierFieldNames();
  305.     }
  306.     public function getNormalizedIdentifier($entity)
  307.     {
  308.         if (null === $entity) {
  309.             return null;
  310.         }
  311.         if (!\is_object($entity)) {
  312.             throw new \RuntimeException('Invalid argument, object or null required');
  313.         }
  314.         if (\in_array($this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity), [
  315.             UnitOfWork::STATE_NEW,
  316.             UnitOfWork::STATE_REMOVED,
  317.         ], true)) {
  318.             return null;
  319.         }
  320.         $values $this->getIdentifierValues($entity);
  321.         if (=== \count($values)) {
  322.             return null;
  323.         }
  324.         return implode(self::ID_SEPARATOR$values);
  325.     }
  326.     /**
  327.      * {@inheritdoc}
  328.      *
  329.      * The ORM implementation does nothing special but you still should use
  330.      * this method when using the id in a URL to allow for future improvements.
  331.      */
  332.     public function getUrlSafeIdentifier($entity)
  333.     {
  334.         return $this->getNormalizedIdentifier($entity);
  335.     }
  336.     public function addIdentifiersToQuery($classProxyQueryInterface $queryProxy, array $idx)
  337.     {
  338.         $fieldNames $this->getIdentifierFieldNames($class);
  339.         $qb $queryProxy->getQueryBuilder();
  340.         $prefix uniqid();
  341.         $sqls = [];
  342.         foreach ($idx as $pos => $id) {
  343.             $ids explode(self::ID_SEPARATOR$id);
  344.             $ands = [];
  345.             foreach ($fieldNames as $posName => $name) {
  346.                 $parameterName sprintf('field_%s_%s_%d'$prefix$name$pos);
  347.                 $ands[] = sprintf('%s.%s = :%s'current($qb->getRootAliases()), $name$parameterName);
  348.                 $qb->setParameter($parameterName$ids[$posName]);
  349.             }
  350.             $sqls[] = implode(' AND '$ands);
  351.         }
  352.         $qb->andWhere(sprintf('( %s )'implode(' OR '$sqls)));
  353.     }
  354.     public function batchDelete($classProxyQueryInterface $queryProxy)
  355.     {
  356.         $queryProxy->select('DISTINCT '.current($queryProxy->getRootAliases()));
  357.         try {
  358.             $entityManager $this->getEntityManager($class);
  359.             $i 0;
  360.             foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
  361.                 $entityManager->remove($object[0]);
  362.                 if (=== (++$i 20)) {
  363.                     $entityManager->flush();
  364.                     $entityManager->clear();
  365.                 }
  366.             }
  367.             $entityManager->flush();
  368.             $entityManager->clear();
  369.         } catch (\PDOException $e) {
  370.             throw new ModelManagerException(''0$e);
  371.         } catch (DBALException $e) {
  372.             throw new ModelManagerException(''0$e);
  373.         }
  374.     }
  375.     public function getDataSourceIterator(DatagridInterface $datagrid, array $fields$firstResult null$maxResult null)
  376.     {
  377.         $datagrid->buildPager();
  378.         $query $datagrid->getQuery();
  379.         $query->select('DISTINCT '.current($query->getRootAliases()));
  380.         $query->setFirstResult($firstResult);
  381.         $query->setMaxResults($maxResult);
  382.         if ($query instanceof ProxyQueryInterface) {
  383.             $sortBy $query->getSortBy();
  384.             if (!empty($sortBy)) {
  385.                 $query->addOrderBy($sortBy$query->getSortOrder());
  386.                 $query $query->getQuery();
  387.                 $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [OrderByToSelectWalker::class]);
  388.             } else {
  389.                 $query $query->getQuery();
  390.             }
  391.         }
  392.         return new DoctrineORMQuerySourceIterator($query$fields);
  393.     }
  394.     public function getExportFields($class)
  395.     {
  396.         $metadata $this->getEntityManager($class)->getClassMetadata($class);
  397.         return $metadata->getFieldNames();
  398.     }
  399.     public function getModelInstance($class)
  400.     {
  401.         $r = new \ReflectionClass($class);
  402.         if ($r->isAbstract()) {
  403.             throw new \RuntimeException(sprintf('Cannot initialize abstract class: %s'$class));
  404.         }
  405.         $constructor $r->getConstructor();
  406.         if (null !== $constructor && (!$constructor->isPublic() || $constructor->getNumberOfRequiredParameters() > 0)) {
  407.             return $r->newInstanceWithoutConstructor();
  408.         }
  409.         return new $class();
  410.     }
  411.     public function getSortParameters(FieldDescriptionInterface $fieldDescriptionDatagridInterface $datagrid)
  412.     {
  413.         $values $datagrid->getValues();
  414.         if ($this->isFieldAlreadySorted($fieldDescription$datagrid)) {
  415.             if ('ASC' === $values['_sort_order']) {
  416.                 $values['_sort_order'] = 'DESC';
  417.             } else {
  418.                 $values['_sort_order'] = 'ASC';
  419.             }
  420.         } else {
  421.             $values['_sort_order'] = 'ASC';
  422.         }
  423.         $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
  424.         return ['filter' => $values];
  425.     }
  426.     public function getPaginationParameters(DatagridInterface $datagrid$page)
  427.     {
  428.         $values $datagrid->getValues();
  429.         if (isset($values['_sort_by']) && $values['_sort_by'] instanceof FieldDescriptionInterface) {
  430.             $values['_sort_by'] = $values['_sort_by']->getName();
  431.         }
  432.         $values['_page'] = $page;
  433.         return ['filter' => $values];
  434.     }
  435.     public function getDefaultSortValues($class)
  436.     {
  437.         return [
  438.             '_sort_order' => 'ASC',
  439.             '_sort_by' => implode(','$this->getModelIdentifier($class)),
  440.             '_page' => 1,
  441.             '_per_page' => 25,
  442.         ];
  443.     }
  444.     public function modelTransform($class$instance)
  445.     {
  446.         return $instance;
  447.     }
  448.     public function modelReverseTransform($class, array $array = [])
  449.     {
  450.         $instance $this->getModelInstance($class);
  451.         $metadata $this->getMetadata($class);
  452.         $reflClass $metadata->reflClass;
  453.         foreach ($array as $name => $value) {
  454.             $reflection_property false;
  455.             // property or association ?
  456.             if (\array_key_exists($name$metadata->fieldMappings)) {
  457.                 $property $metadata->fieldMappings[$name]['fieldName'];
  458.                 $reflection_property $metadata->reflFields[$name];
  459.             } elseif (\array_key_exists($name$metadata->associationMappings)) {
  460.                 $property $metadata->associationMappings[$name]['fieldName'];
  461.             } else {
  462.                 $property $name;
  463.             }
  464.             $setter 'set'.$this->camelize($name);
  465.             if ($reflClass->hasMethod($setter)) {
  466.                 if (!$reflClass->getMethod($setter)->isPublic()) {
  467.                     throw new PropertyAccessDeniedException(sprintf(
  468.                         'Method "%s()" is not public in class "%s"',
  469.                         $setter,
  470.                         $reflClass->getName()
  471.                     ));
  472.                 }
  473.                 $instance->$setter($value);
  474.             } elseif ($reflClass->hasMethod('__set')) {
  475.                 // needed to support magic method __set
  476.                 $instance->$property $value;
  477.             } elseif ($reflClass->hasProperty($property)) {
  478.                 if (!$reflClass->getProperty($property)->isPublic()) {
  479.                     throw new PropertyAccessDeniedException(sprintf(
  480.                         'Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?',
  481.                         $property,
  482.                         $reflClass->getName(),
  483.                         ucfirst($property)
  484.                     ));
  485.                 }
  486.                 $instance->$property $value;
  487.             } elseif ($reflection_property) {
  488.                 $reflection_property->setValue($instance$value);
  489.             }
  490.         }
  491.         return $instance;
  492.     }
  493.     public function getModelCollectionInstance($class)
  494.     {
  495.         return new \Doctrine\Common\Collections\ArrayCollection();
  496.     }
  497.     public function collectionClear(&$collection)
  498.     {
  499.         return $collection->clear();
  500.     }
  501.     public function collectionHasElement(&$collection, &$element)
  502.     {
  503.         return $collection->contains($element);
  504.     }
  505.     public function collectionAddElement(&$collection, &$element)
  506.     {
  507.         return $collection->add($element);
  508.     }
  509.     public function collectionRemoveElement(&$collection, &$element)
  510.     {
  511.         return $collection->removeElement($element);
  512.     }
  513.     /**
  514.      * method taken from Symfony\Component\PropertyAccess\PropertyAccessor.
  515.      *
  516.      * @param string $property
  517.      *
  518.      * @return mixed
  519.      */
  520.     protected function camelize($property)
  521.     {
  522.         return str_replace(' '''ucwords(str_replace('_'' '$property)));
  523.     }
  524.     private function isFieldAlreadySorted(FieldDescriptionInterface $fieldDescriptionDatagridInterface $datagrid): bool
  525.     {
  526.         $values $datagrid->getValues();
  527.         if (!isset($values['_sort_by']) || !$values['_sort_by'] instanceof FieldDescriptionInterface) {
  528.             return false;
  529.         }
  530.         return $values['_sort_by']->getName() === $fieldDescription->getName()
  531.             || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable');
  532.     }
  533.     /**
  534.      * @param mixed $value
  535.      */
  536.     private function getValueFromType($valueType $typestring $fieldTypeAbstractPlatform $platform): string
  537.     {
  538.         if ($platform->hasDoctrineTypeMappingFor($fieldType) &&
  539.             'binary' === $platform->getDoctrineTypeMapping($fieldType)
  540.         ) {
  541.             return (string) $type->convertToPHPValue($value$platform);
  542.         }
  543.         // some libraries may have `toString()` implementation
  544.         if (\is_callable([$value'toString'])) {
  545.             return $value->toString();
  546.         }
  547.         // final fallback to magic `__toString()` which may throw an exception in 7.4
  548.         if (method_exists($value'__toString')) {
  549.             return $value->__toString();
  550.         }
  551.         return (string) $type->convertToDatabaseValue($value$platform);
  552.     }
  553. }