vendor/omines/datatables-bundle/src/Adapter/Doctrine/ORMAdapter.php line 63

Open in your IDE?
  1. <?php
  2. /*
  3.  * Symfony DataTables Bundle
  4.  * (c) Omines Internetbureau B.V. - https://omines.nl/
  5.  *
  6.  * For the full copyright and license information, please view the LICENSE
  7.  * file that was distributed with this source code.
  8.  */
  9. declare(strict_types=1);
  10. namespace Omines\DataTablesBundle\Adapter\Doctrine;
  11. use Doctrine\ORM\AbstractQuery;
  12. use Doctrine\ORM\EntityManager;
  13. use Doctrine\ORM\Mapping\ClassMetadata;
  14. use Doctrine\ORM\Query;
  15. use Doctrine\ORM\QueryBuilder;
  16. use Doctrine\Persistence\ManagerRegistry;
  17. use Omines\DataTablesBundle\Adapter\AbstractAdapter;
  18. use Omines\DataTablesBundle\Adapter\AdapterQuery;
  19. use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent;
  20. use Omines\DataTablesBundle\Adapter\Doctrine\ORM\AutomaticQueryBuilder;
  21. use Omines\DataTablesBundle\Adapter\Doctrine\ORM\QueryBuilderProcessorInterface;
  22. use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
  23. use Omines\DataTablesBundle\Column\AbstractColumn;
  24. use Omines\DataTablesBundle\DataTableState;
  25. use Omines\DataTablesBundle\Exception\InvalidConfigurationException;
  26. use Omines\DataTablesBundle\Exception\MissingDependencyException;
  27. use Symfony\Component\OptionsResolver\Options;
  28. use Symfony\Component\OptionsResolver\OptionsResolver;
  29. /**
  30.  * ORMAdapter.
  31.  *
  32.  * @author Niels Keurentjes <niels.keurentjes@omines.com>
  33.  * @author Robbert Beesems <robbert.beesems@omines.com>
  34.  */
  35. class ORMAdapter extends AbstractAdapter
  36. {
  37.     /** @var ManagerRegistry */
  38.     private $registry;
  39.     /** @var EntityManager */
  40.     protected $manager;
  41.     /** @var ClassMetadata */
  42.     protected $metadata;
  43.     /** @var AbstractQuery::HYDRATE_*|string|null */
  44.     private $hydrationMode;
  45.     /** @var QueryBuilderProcessorInterface[] */
  46.     private $queryBuilderProcessors;
  47.     /** @var QueryBuilderProcessorInterface[] */
  48.     protected $criteriaProcessors;
  49.     /**
  50.      * DoctrineAdapter constructor.
  51.      */
  52.     public function __construct(ManagerRegistry $registry null)
  53.     {
  54.         if (null === $registry) {
  55.             throw new MissingDependencyException('Install doctrine/doctrine-bundle to use the ORMAdapter');
  56.         }
  57.         parent::__construct();
  58.         $this->registry $registry;
  59.     }
  60.     /**
  61.      * {@inheritdoc}
  62.      */
  63.     public function configure(array $options)
  64.     {
  65.         $resolver = new OptionsResolver();
  66.         $this->configureOptions($resolver);
  67.         $options $resolver->resolve($options);
  68.         $this->afterConfiguration($options);
  69.     }
  70.     /**
  71.      * @param mixed $processor
  72.      */
  73.     public function addCriteriaProcessor($processor)
  74.     {
  75.         $this->criteriaProcessors[] = $this->normalizeProcessor($processor);
  76.     }
  77.     protected function prepareQuery(AdapterQuery $query)
  78.     {
  79.         $state $query->getState();
  80.         $query->set('qb'$builder $this->createQueryBuilder($state));
  81.         $query->set('rootAlias'$rootAlias $builder->getDQLPart('from')[0]->getAlias());
  82.         // Provide default field mappings if needed
  83.         foreach ($state->getDataTable()->getColumns() as $column) {
  84.             if (null === $column->getField() && isset($this->metadata->fieldMappings[$name $column->getName()])) {
  85.                 $column->setOption('field'"{$rootAlias}.{$name}");
  86.             }
  87.         }
  88.         /** @var Query\Expr\From $fromClause */
  89.         $fromClause $builder->getDQLPart('from')[0];
  90.         $identifier "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}";
  91.         $query->setTotalRows($this->getCount($builder$identifier));
  92.         // Get record count after filtering
  93.         $this->buildCriteria($builder$state);
  94.         $query->setFilteredRows($this->getCount($builder$identifier));
  95.         // Perform mapping of all referred fields and implied fields
  96.         $aliases $this->getAliases($query);
  97.         $query->set('aliases'$aliases);
  98.         $query->setIdentifierPropertyPath($this->mapFieldToPropertyPath($identifier$aliases));
  99.     }
  100.     /**
  101.      * @return array
  102.      */
  103.     protected function getAliases(AdapterQuery $query)
  104.     {
  105.         /** @var QueryBuilder $builder */
  106.         $builder $query->get('qb');
  107.         $aliases = [];
  108.         /** @var Query\Expr\From $from */
  109.         foreach ($builder->getDQLPart('from') as $from) {
  110.             $aliases[$from->getAlias()] = [null$this->manager->getMetadataFactory()->getMetadataFor($from->getFrom())];
  111.         }
  112.         // Alias all joins
  113.         foreach ($builder->getDQLPart('join') as $joins) {
  114.             /** @var Query\Expr\Join $join */
  115.             foreach ($joins as $join) {
  116.                 if (false === mb_strstr($join->getJoin(), '.')) {
  117.                     continue;
  118.                 }
  119.                 list($origin$target) = explode('.'$join->getJoin());
  120.                 $mapping $aliases[$origin][1]->getAssociationMapping($target);
  121.                 $aliases[$join->getAlias()] = [$join->getJoin(), $this->manager->getMetadataFactory()->getMetadataFor($mapping['targetEntity'])];
  122.             }
  123.         }
  124.         return $aliases;
  125.     }
  126.     /**
  127.      * {@inheritdoc}
  128.      */
  129.     protected function mapPropertyPath(AdapterQuery $queryAbstractColumn $column)
  130.     {
  131.         return $this->mapFieldToPropertyPath($column->getField(), $query->get('aliases'));
  132.     }
  133.     protected function getResults(AdapterQuery $query): \Traversable
  134.     {
  135.         /** @var QueryBuilder $builder */
  136.         $builder $query->get('qb');
  137.         $state $query->getState();
  138.         // Apply definitive view state for current 'page' of the table
  139.         foreach ($state->getOrderBy() as list($column$direction)) {
  140.             /** @var AbstractColumn $column */
  141.             if ($column->isOrderable()) {
  142.                 $builder->addOrderBy($column->getOrderField(), $direction);
  143.             }
  144.         }
  145.         if (null !== $state->getLength()) {
  146.             $builder
  147.                 ->setFirstResult($state->getStart())
  148.                 ->setMaxResults($state->getLength())
  149.             ;
  150.         }
  151.         $query $builder->getQuery();
  152.         $event = new ORMAdapterQueryEvent($query);
  153.         $state->getDataTable()->getEventDispatcher()->dispatch($eventORMAdapterEvents::PRE_QUERY);
  154.         foreach ($query->iterate([], $this->hydrationMode) as $result) {
  155.             yield $entity array_values($result)[0];
  156.             if (Query::HYDRATE_OBJECT === $this->hydrationMode) {
  157.                 $this->manager->detach($entity);
  158.             }
  159.         }
  160.     }
  161.     protected function buildCriteria(QueryBuilder $queryBuilderDataTableState $state)
  162.     {
  163.         foreach ($this->criteriaProcessors as $provider) {
  164.             $provider->process($queryBuilder$state);
  165.         }
  166.     }
  167.     protected function createQueryBuilder(DataTableState $state): QueryBuilder
  168.     {
  169.         /** @var QueryBuilder $queryBuilder */
  170.         $queryBuilder $this->manager->createQueryBuilder();
  171.         // Run all query builder processors in order
  172.         foreach ($this->queryBuilderProcessors as $processor) {
  173.             $processor->process($queryBuilder$state);
  174.         }
  175.         return $queryBuilder;
  176.     }
  177.     /**
  178.      * @param $identifier
  179.      *
  180.      * @return int
  181.      */
  182.     protected function getCount(QueryBuilder $queryBuilder$identifier)
  183.     {
  184.         $qb = clone $queryBuilder;
  185.         $qb->resetDQLPart('orderBy');
  186.         $gb $qb->getDQLPart('groupBy');
  187.         if (empty($gb) || !$this->hasGroupByPart($identifier$gb)) {
  188.             $qb->select($qb->expr()->count($identifier));
  189.             return (int) $qb->getQuery()->getSingleScalarResult();
  190.         } else {
  191.             $qb->resetDQLPart('groupBy');
  192.             $qb->select($qb->expr()->countDistinct($identifier));
  193.             return (int) $qb->getQuery()->getSingleScalarResult();
  194.         }
  195.     }
  196.     /**
  197.      * @param $identifier
  198.      * @param Query\Expr\GroupBy[] $gbList
  199.      *
  200.      * @return bool
  201.      */
  202.     protected function hasGroupByPart($identifier, array $gbList)
  203.     {
  204.         foreach ($gbList as $gb) {
  205.             if (in_array($identifier$gb->getParts(), true)) {
  206.                 return true;
  207.             }
  208.         }
  209.         return false;
  210.     }
  211.     /**
  212.      * @param string $field
  213.      *
  214.      * @return string
  215.      */
  216.     protected function mapFieldToPropertyPath($field, array $aliases = [])
  217.     {
  218.         $parts explode('.'$field);
  219.         if (count($parts) < 2) {
  220.             throw new InvalidConfigurationException(sprintf("Field name '%s' must consist at least of an alias and a field separated with a period"$field));
  221.         }
  222.         $origin $parts[0];
  223.         array_shift($parts);
  224.         $target array_reverse($parts);
  225.         $path $target;
  226.         $current = isset($aliases[$origin]) ? $aliases[$origin][0] : null;
  227.         while (null !== $current) {
  228.             list($origin$target) = explode('.'$current);
  229.             $path[] = $target;
  230.             $current $aliases[$origin][0];
  231.         }
  232.         if (Query::HYDRATE_ARRAY === $this->hydrationMode) {
  233.             return '[' implode(']['array_reverse($path)) . ']';
  234.         } else {
  235.             return implode('.'array_reverse($path));
  236.         }
  237.     }
  238.     protected function configureOptions(OptionsResolver $resolver)
  239.     {
  240.         $providerNormalizer = function (Options $options$value) {
  241.             return array_map([$this'normalizeProcessor'], (array) $value);
  242.         };
  243.         $resolver
  244.             ->setDefaults([
  245.                 'hydrate' => Query::HYDRATE_OBJECT,
  246.                 'query' => [],
  247.                 'criteria' => function (Options $options) {
  248.                     return [new SearchCriteriaProvider()];
  249.                 },
  250.             ])
  251.             ->setRequired('entity')
  252.             ->setAllowedTypes('entity', ['string'])
  253.             ->setAllowedTypes('hydrate''int')
  254.             ->setAllowedTypes('query', [QueryBuilderProcessorInterface::class, 'array''callable'])
  255.             ->setAllowedTypes('criteria', [QueryBuilderProcessorInterface::class, 'array''callable''null'])
  256.             ->setNormalizer('query'$providerNormalizer)
  257.             ->setNormalizer('criteria'$providerNormalizer)
  258.         ;
  259.     }
  260.     protected function afterConfiguration(array $options): void
  261.     {
  262.         // Enable automated mode or just get the general default entity manager
  263.         $manager $this->registry->getManagerForClass($options['entity']);
  264.         if (!$manager instanceof EntityManager) {
  265.             throw new InvalidConfigurationException(sprintf('Doctrine has no valid entity manager for entity "%s", is it correctly imported and referenced?'$options['entity']));
  266.         }
  267.         $this->manager $manager;
  268.         $this->metadata $this->manager->getClassMetadata($options['entity']);
  269.         if (empty($options['query'])) {
  270.             $options['query'] = [new AutomaticQueryBuilder($this->manager$this->metadata)];
  271.         }
  272.         // Set options
  273.         $this->hydrationMode $options['hydrate'];
  274.         $this->queryBuilderProcessors $options['query'];
  275.         $this->criteriaProcessors $options['criteria'];
  276.     }
  277.     /**
  278.      * @param callable|QueryBuilderProcessorInterface $provider
  279.      *
  280.      * @return QueryBuilderProcessorInterface
  281.      */
  282.     private function normalizeProcessor($provider)
  283.     {
  284.         if ($provider instanceof QueryBuilderProcessorInterface) {
  285.             return $provider;
  286.         } elseif (is_callable($provider)) {
  287.             return new class($provider) implements QueryBuilderProcessorInterface {
  288.                 private $callable;
  289.                 public function __construct(callable $value)
  290.                 {
  291.                     $this->callable $value;
  292.                 }
  293.                 public function process(QueryBuilder $queryBuilderDataTableState $state)
  294.                 {
  295.                     return call_user_func($this->callable$queryBuilder$state);
  296.                 }
  297.             };
  298.         }
  299.         // @phpstan-ignore-next-line
  300.         throw new InvalidConfigurationException('Provider must be a callable or implement QueryBuilderProcessorInterface');
  301.     }
  302. }