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\Persistence\ManagerRegistry;
  13. use Doctrine\Common\Util\ClassUtils;
  14. use Doctrine\DBAL\DBALException;
  15. use Doctrine\DBAL\LockMode;
  16. use Doctrine\DBAL\Platforms\AbstractPlatform;
  17. use Doctrine\DBAL\Types\Type;
  18. use Doctrine\ORM\EntityManager;
  19. use Doctrine\ORM\Mapping\ClassMetadata;
  20. use Doctrine\ORM\OptimisticLockException;
  21. use Doctrine\ORM\Query;
  22. use Doctrine\ORM\QueryBuilder;
  23. use Doctrine\ORM\UnitOfWork;
  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;
  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;
  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.     public function getModelIdentifier($class)
  265.     {
  266.         return $this->getMetadata($class)->identifier;
  267.     }
  268.     public function getIdentifierValues($entity)
  269.     {
  270.         // Fix code has an impact on performance, so disable it ...
  271.         //$entityManager = $this->getEntityManager($entity);
  272.         //if (!$entityManager->getUnitOfWork()->isInIdentityMap($entity)) {
  273.         //    throw new \RuntimeException('Entities passed to the choice field must be managed');
  274.         //}
  275.         $class ClassUtils::getClass($entity);
  276.         $metadata $this->getMetadata($class);
  277.         $platform $this->getEntityManager($class)->getConnection()->getDatabasePlatform();
  278.         $identifiers = [];
  279.         foreach ($metadata->getIdentifierValues($entity) as $name => $value) {
  280.             if (!\is_object($value)) {
  281.                 $identifiers[] = $value;
  282.                 continue;
  283.             }
  284.             $fieldType $metadata->getTypeOfField($name);
  285.             $type $fieldType && Type::hasType($fieldType) ? Type::getType($fieldType) : null;
  286.             if ($type) {
  287.                 $identifiers[] = $this->getValueFromType($value$type$fieldType$platform);
  288.                 continue;
  289.             }
  290.             $metadata $this->getMetadata(ClassUtils::getClass($value));
  291.             foreach ($metadata->getIdentifierValues($value) as $value) {
  292.                 $identifiers[] = $value;
  293.             }
  294.         }
  295.         return $identifiers;
  296.     }
  297.     public function getIdentifierFieldNames($class)
  298.     {
  299.         return $this->getMetadata($class)->getIdentifierFieldNames();
  300.     }
  301.     public function getNormalizedIdentifier($entity)
  302.     {
  303.         if (is_scalar($entity)) {
  304.             throw new \RuntimeException('Invalid argument, object or null required');
  305.         }
  306.         if (!$entity) {
  307.             return;
  308.         }
  309.         if (\in_array($this->getEntityManager($entity)->getUnitOfWork()->getEntityState($entity), [
  310.             UnitOfWork::STATE_NEW,
  311.             UnitOfWork::STATE_REMOVED,
  312.         ], true)) {
  313.             return;
  314.         }
  315.         $values $this->getIdentifierValues($entity);
  316.         if (=== \count($values)) {
  317.             return;
  318.         }
  319.         return implode(self::ID_SEPARATOR$values);
  320.     }
  321.     /**
  322.      * {@inheritdoc}
  323.      *
  324.      * The ORM implementation does nothing special but you still should use
  325.      * this method when using the id in a URL to allow for future improvements.
  326.      */
  327.     public function getUrlsafeIdentifier($entity)
  328.     {
  329.         return $this->getNormalizedIdentifier($entity);
  330.     }
  331.     public function addIdentifiersToQuery($classProxyQueryInterface $queryProxy, array $idx)
  332.     {
  333.         $fieldNames $this->getIdentifierFieldNames($class);
  334.         $qb $queryProxy->getQueryBuilder();
  335.         $prefix uniqid();
  336.         $sqls = [];
  337.         foreach ($idx as $pos => $id) {
  338.             $ids explode(self::ID_SEPARATOR$id);
  339.             $ands = [];
  340.             foreach ($fieldNames as $posName => $name) {
  341.                 $parameterName sprintf('field_%s_%s_%d'$prefix$name$pos);
  342.                 $ands[] = sprintf('%s.%s = :%s'current($qb->getRootAliases()), $name$parameterName);
  343.                 $qb->setParameter($parameterName$ids[$posName]);
  344.             }
  345.             $sqls[] = implode(' AND '$ands);
  346.         }
  347.         $qb->andWhere(sprintf('( %s )'implode(' OR '$sqls)));
  348.     }
  349.     public function batchDelete($classProxyQueryInterface $queryProxy)
  350.     {
  351.         $queryProxy->select('DISTINCT '.current($queryProxy->getRootAliases()));
  352.         try {
  353.             $entityManager $this->getEntityManager($class);
  354.             $i 0;
  355.             foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
  356.                 $entityManager->remove($object[0]);
  357.                 if (=== (++$i 20)) {
  358.                     $entityManager->flush();
  359.                     $entityManager->clear();
  360.                 }
  361.             }
  362.             $entityManager->flush();
  363.             $entityManager->clear();
  364.         } catch (\PDOException $e) {
  365.             throw new ModelManagerException(''0$e);
  366.         } catch (DBALException $e) {
  367.             throw new ModelManagerException(''0$e);
  368.         }
  369.     }
  370.     public function getDataSourceIterator(DatagridInterface $datagrid, array $fields$firstResult null$maxResult null)
  371.     {
  372.         $datagrid->buildPager();
  373.         $query $datagrid->getQuery();
  374.         $query->select('DISTINCT '.current($query->getRootAliases()));
  375.         $query->setFirstResult($firstResult);
  376.         $query->setMaxResults($maxResult);
  377.         if ($query instanceof ProxyQueryInterface) {
  378.             $sortBy $query->getSortBy();
  379.             if (!empty($sortBy)) {
  380.                 $query->addOrderBy($sortBy$query->getSortOrder());
  381.                 $query $query->getQuery();
  382.                 $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [OrderByToSelectWalker::class]);
  383.             } else {
  384.                 $query $query->getQuery();
  385.             }
  386.         }
  387.         return new DoctrineORMQuerySourceIterator($query$fields);
  388.     }
  389.     public function getExportFields($class)
  390.     {
  391.         $metadata $this->getEntityManager($class)->getClassMetadata($class);
  392.         return $metadata->getFieldNames();
  393.     }
  394.     public function getModelInstance($class)
  395.     {
  396.         $r = new \ReflectionClass($class);
  397.         if ($r->isAbstract()) {
  398.             throw new \RuntimeException(sprintf('Cannot initialize abstract class: %s'$class));
  399.         }
  400.         $constructor $r->getConstructor();
  401.         if (null !== $constructor && (!$constructor->isPublic() || $constructor->getNumberOfRequiredParameters() > 0)) {
  402.             return $r->newInstanceWithoutConstructor();
  403.         }
  404.         return new $class();
  405.     }
  406.     public function getSortParameters(FieldDescriptionInterface $fieldDescriptionDatagridInterface $datagrid)
  407.     {
  408.         $values $datagrid->getValues();
  409.         if ($fieldDescription->getName() === $values['_sort_by']->getName() || $values['_sort_by']->getName() === $fieldDescription->getOption('sortable')) {
  410.             if ('ASC' === $values['_sort_order']) {
  411.                 $values['_sort_order'] = 'DESC';
  412.             } else {
  413.                 $values['_sort_order'] = 'ASC';
  414.             }
  415.         } else {
  416.             $values['_sort_order'] = 'ASC';
  417.         }
  418.         $values['_sort_by'] = \is_string($fieldDescription->getOption('sortable')) ? $fieldDescription->getOption('sortable') : $fieldDescription->getName();
  419.         return ['filter' => $values];
  420.     }
  421.     public function getPaginationParameters(DatagridInterface $datagrid$page)
  422.     {
  423.         $values $datagrid->getValues();
  424.         $values['_sort_by'] = $values['_sort_by']->getName();
  425.         $values['_page'] = $page;
  426.         return ['filter' => $values];
  427.     }
  428.     public function getDefaultSortValues($class)
  429.     {
  430.         return [
  431.             '_sort_order' => 'ASC',
  432.             '_sort_by' => implode(','$this->getModelIdentifier($class)),
  433.             '_page' => 1,
  434.             '_per_page' => 25,
  435.         ];
  436.     }
  437.     public function modelTransform($class$instance)
  438.     {
  439.         return $instance;
  440.     }
  441.     public function modelReverseTransform($class, array $array = [])
  442.     {
  443.         $instance $this->getModelInstance($class);
  444.         $metadata $this->getMetadata($class);
  445.         $reflClass $metadata->reflClass;
  446.         foreach ($array as $name => $value) {
  447.             $reflection_property false;
  448.             // property or association ?
  449.             if (\array_key_exists($name$metadata->fieldMappings)) {
  450.                 $property $metadata->fieldMappings[$name]['fieldName'];
  451.                 $reflection_property $metadata->reflFields[$name];
  452.             } elseif (\array_key_exists($name$metadata->associationMappings)) {
  453.                 $property $metadata->associationMappings[$name]['fieldName'];
  454.             } else {
  455.                 $property $name;
  456.             }
  457.             $setter 'set'.$this->camelize($name);
  458.             if ($reflClass->hasMethod($setter)) {
  459.                 if (!$reflClass->getMethod($setter)->isPublic()) {
  460.                     throw new PropertyAccessDeniedException(sprintf(
  461.                         'Method "%s()" is not public in class "%s"',
  462.                         $setter,
  463.                         $reflClass->getName()
  464.                     ));
  465.                 }
  466.                 $instance->$setter($value);
  467.             } elseif ($reflClass->hasMethod('__set')) {
  468.                 // needed to support magic method __set
  469.                 $instance->$property $value;
  470.             } elseif ($reflClass->hasProperty($property)) {
  471.                 if (!$reflClass->getProperty($property)->isPublic()) {
  472.                     throw new PropertyAccessDeniedException(sprintf(
  473.                         'Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?',
  474.                             $property,
  475.                             $reflClass->getName(),
  476.                             ucfirst($property)
  477.                     ));
  478.                 }
  479.                 $instance->$property $value;
  480.             } elseif ($reflection_property) {
  481.                 $reflection_property->setValue($instance$value);
  482.             }
  483.         }
  484.         return $instance;
  485.     }
  486.     public function getModelCollectionInstance($class)
  487.     {
  488.         return new \Doctrine\Common\Collections\ArrayCollection();
  489.     }
  490.     public function collectionClear(&$collection)
  491.     {
  492.         return $collection->clear();
  493.     }
  494.     public function collectionHasElement(&$collection, &$element)
  495.     {
  496.         return $collection->contains($element);
  497.     }
  498.     public function collectionAddElement(&$collection, &$element)
  499.     {
  500.         return $collection->add($element);
  501.     }
  502.     public function collectionRemoveElement(&$collection, &$element)
  503.     {
  504.         return $collection->removeElement($element);
  505.     }
  506.     /**
  507.      * method taken from Symfony\Component\PropertyAccess\PropertyAccessor.
  508.      *
  509.      * @param string $property
  510.      *
  511.      * @return mixed
  512.      */
  513.     protected function camelize($property)
  514.     {
  515.         return str_replace(' '''ucwords(str_replace('_'' '$property)));
  516.     }
  517.     /**
  518.      * @param mixed $value
  519.      */
  520.     private function getValueFromType($valueType $typestring $fieldTypeAbstractPlatform $platform): string
  521.     {
  522.         if ($platform->hasDoctrineTypeMappingFor($fieldType) &&
  523.             'binary' === $platform->getDoctrineTypeMapping($fieldType)
  524.         ) {
  525.             return (string) $type->convertToPHPValue($value$platform);
  526.         }
  527.         return (string) $type->convertToDatabaseValue($value$platform);
  528.     }
  529. }