From 03bbdb699d9e737a8dfae25456d5df905beeefa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Fri, 4 Sep 2020 10:36:44 +0200 Subject: [PATCH] Use FetchJoinORMAdapter from DatatablesBundle instead of custom one. --- .../Adapter/FetchJoinORMAdapter.php | 166 --------- src/DataTables/Adapter/ORMAdapter.php | 347 ------------------ src/DataTables/PartsDataTable.php | 2 +- 3 files changed, 1 insertion(+), 514 deletions(-) delete mode 100644 src/DataTables/Adapter/FetchJoinORMAdapter.php delete mode 100644 src/DataTables/Adapter/ORMAdapter.php diff --git a/src/DataTables/Adapter/FetchJoinORMAdapter.php b/src/DataTables/Adapter/FetchJoinORMAdapter.php deleted file mode 100644 index c3041834..00000000 --- a/src/DataTables/Adapter/FetchJoinORMAdapter.php +++ /dev/null @@ -1,166 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). - * - * Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics) - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - */ - -namespace App\DataTables\Adapter; - -use Doctrine\ORM\Query; -use Doctrine\ORM\QueryBuilder; -use Doctrine\ORM\Tools\Pagination\Paginator; -use Omines\DataTablesBundle\Adapter\AdapterQuery; -use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent; -use Omines\DataTablesBundle\Column\AbstractColumn; -use Symfony\Component\OptionsResolver\OptionsResolver; - -/** - * Similar to ORMAdapter this class allows to access objects from the doctrine ORM. - * Unlike the default ORMAdapter supports Fetch Joins (additional entites are fetched from DB via joins) using - * the Doctrine Paginator. - * - * @author Jan Böhmer - */ -class FetchJoinORMAdapter extends ORMAdapter -{ - protected $use_simple_total; - - public function configure(array $options): void - { - parent::configure($options); - $this->use_simple_total = $options['simple_total_query']; - } - - public function getResults(AdapterQuery $query): \Traversable - { - $builder = $query->get('qb'); - $state = $query->getState(); - - // Apply definitive view state for current 'page' of the table - foreach ($state->getOrderBy() as [$column, $direction]) { - /** @var AbstractColumn $column */ - if ($column->isOrderable()) { - $builder->addOrderBy($column->getOrderField(), $direction); - } - } - if ($state->getLength() > 0) { - $builder - ->setFirstResult($state->getStart()) - ->setMaxResults($state->getLength()); - } - - $query = $builder->getQuery(); - $event = new ORMAdapterQueryEvent($query); - $state->getDataTable()->getEventDispatcher()->dispatch($event); - - //Use Doctrine paginator for result iteration - $paginator = new Paginator($query); - - foreach ($paginator->getIterator() as $result) { - yield $result; - $this->manager->detach($result); - } - } - - public function getCount(QueryBuilder $queryBuilder, string $identifier) - { - $paginator = new Paginator($queryBuilder); - - return $paginator->count(); - } - - protected function configureOptions(OptionsResolver $resolver): void - { - parent::configureOptions($resolver); - - //Enforce object hydration mode (fetch join only works for objects) - $resolver->addAllowedValues('hydrate', Query::HYDRATE_OBJECT); - - /* - * Add the possibility to replace the query for total entity count through a very simple one, to improve performance. - * You can only use this option, if you did not apply any criteria to your total count. - */ - $resolver->setDefault('simple_total_query', false); - } - - protected function prepareQuery(AdapterQuery $query): void - { - $state = $query->getState(); - $query->set('qb', $builder = $this->createQueryBuilder($state)); - $query->set('rootAlias', $rootAlias = $builder->getDQLPart('from')[0]->getAlias()); - - // Provide default field mappings if needed - foreach ($state->getDataTable()->getColumns() as $column) { - if (null === $column->getField() && isset($this->metadata->fieldMappings[$name = $column->getName()])) { - $column->setOption('field', "{$rootAlias}.{$name}"); - } - } - - /** @var Query\Expr\From $fromClause */ - $fromClause = $builder->getDQLPart('from')[0]; - $identifier = "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}"; - - //Use simpler (faster) total count query if the user wanted so... - if ($this->use_simple_total) { - $query->setTotalRows($this->getSimpleTotalCount($builder)); - } else { - $query->setTotalRows($this->getCount($builder, $identifier)); - } - - // Get record count after filtering - $this->buildCriteria($builder, $state); - $query->setFilteredRows($this->getCount($builder, $identifier)); - - // Perform mapping of all referred fields and implied fields - $aliases = $this->getAliases($query); - $query->set('aliases', $aliases); - $query->setIdentifierPropertyPath($this->mapFieldToPropertyPath($identifier, $aliases)); - } - - protected function getSimpleTotalCount(QueryBuilder $queryBuilder): int - { - /** The paginator count queries can be rather slow, so when query for total count (100ms or longer), - * just return the entity count. - */ - /** @var Query\Expr\From $from_expr */ - $from_expr = $queryBuilder->getDQLPart('from')[0]; - - return $this->manager->getRepository($from_expr->getFrom())->count([]); - } -} diff --git a/src/DataTables/Adapter/ORMAdapter.php b/src/DataTables/Adapter/ORMAdapter.php deleted file mode 100644 index 141c202e..00000000 --- a/src/DataTables/Adapter/ORMAdapter.php +++ /dev/null @@ -1,347 +0,0 @@ - - * @author Robbert Beesems - */ -class ORMAdapter extends AbstractAdapter -{ - /** - * @var EntityManager - */ - protected $manager; - - /** - * @var \Doctrine\ORM\Mapping\ClassMetadata - */ - protected $metadata; - - /** - * @var QueryBuilderProcessorInterface[] - */ - protected $criteriaProcessors; - /** - * @var ManagerRegistry - */ - private $registry; - - /** - * @var int - */ - private $hydrationMode; - - /** - * @var QueryBuilderProcessorInterface[] - */ - private $queryBuilderProcessors; - - /** - * DoctrineAdapter constructor. - */ - public function __construct(?ManagerRegistry $registry = null) - { - if (null === $registry) { - throw new MissingDependencyException('Install doctrine/doctrine-bundle to use the ORMAdapter'); - } - - parent::__construct(); - $this->registry = $registry; - } - - public function configure(array $options): void - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - // Enable automated mode or just get the general default entity manager - if (null === ($this->manager = $this->registry->getManagerForClass($options['entity']))) { - throw new InvalidConfigurationException(sprintf('Doctrine has no manager for entity "%s", is it correctly imported and referenced?', $options['entity'])); - } - $this->metadata = $this->manager->getClassMetadata($options['entity']); - if (empty($options['query'])) { - $options['query'] = [new AutomaticQueryBuilder($this->manager, $this->metadata)]; - } - - // Set options - $this->hydrationMode = $options['hydrate']; - $this->queryBuilderProcessors = $options['query']; - $this->criteriaProcessors = $options['criteria']; - } - - public function addCriteriaProcessor($processor): void - { - $this->criteriaProcessors[] = $this->normalizeProcessor($processor); - } - - protected function prepareQuery(AdapterQuery $query): void - { - $state = $query->getState(); - $query->set('qb', $builder = $this->createQueryBuilder($state)); - $query->set('rootAlias', $rootAlias = $builder->getDQLPart('from')[0]->getAlias()); - - // Provide default field mappings if needed - foreach ($state->getDataTable()->getColumns() as $column) { - if (null === $column->getField() && isset($this->metadata->fieldMappings[$name = $column->getName()])) { - $column->setOption('field', "{$rootAlias}.{$name}"); - } - } - - /** @var Query\Expr\From $fromClause */ - $fromClause = $builder->getDQLPart('from')[0]; - $identifier = "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}"; - $query->setTotalRows($this->getCount($builder, $identifier)); - - // Get record count after filtering - $this->buildCriteria($builder, $state); - $query->setFilteredRows($this->getCount($builder, $identifier)); - - // Perform mapping of all referred fields and implied fields - $aliases = $this->getAliases($query); - $query->set('aliases', $aliases); - $query->setIdentifierPropertyPath($this->mapFieldToPropertyPath($identifier, $aliases)); - } - - /** - * @return array - */ - protected function getAliases(AdapterQuery $query) - { - /** @var QueryBuilder $builder */ - $builder = $query->get('qb'); - $aliases = []; - - /** @var Query\Expr\From $from */ - foreach ($builder->getDQLPart('from') as $from) { - $aliases[$from->getAlias()] = [null, $this->manager->getMetadataFactory()->getMetadataFor($from->getFrom())]; - } - - // Alias all joins - foreach ($builder->getDQLPart('join') as $joins) { - /** @var Query\Expr\Join $join */ - foreach ($joins as $join) { - if (false === mb_strstr($join->getJoin(), '.')) { - continue; - } - - [$origin, $target] = explode('.', $join->getJoin()); - - $mapping = $aliases[$origin][1]->getAssociationMapping($target); - $aliases[$join->getAlias()] = [$join->getJoin(), $this->manager->getMetadataFactory()->getMetadataFor($mapping['targetEntity'])]; - } - } - - return $aliases; - } - - protected function mapPropertyPath(AdapterQuery $query, AbstractColumn $column) - { - return $this->mapFieldToPropertyPath($column->getField(), $query->get('aliases')); - } - - protected function getResults(AdapterQuery $query): \Traversable - { - /** @var QueryBuilder $builder */ - $builder = $query->get('qb'); - $state = $query->getState(); - - // Apply definitive view state for current 'page' of the table - foreach ($state->getOrderBy() as [$column, $direction]) { - /** @var AbstractColumn $column */ - if ($column->isOrderable()) { - $builder->addOrderBy($column->getOrderField(), $direction); - } - } - if ($state->getLength() > 0) { - $builder - ->setFirstResult($state->getStart()) - ->setMaxResults($state->getLength()) - ; - } - - $q = $builder->getQuery(); - $event = new ORMAdapterQueryEvent($q); - $state->getDataTable()->getEventDispatcher()->dispatch($event, ORMAdapterEvents::PRE_QUERY); - - foreach ($query->iterate([], $this->hydrationMode) as $result) { - yield $entity = array_values($result)[0]; - if (Query::HYDRATE_OBJECT === $this->hydrationMode) { - $this->manager->detach($entity); - } - } - } - - protected function buildCriteria(QueryBuilder $queryBuilder, DataTableState $state): void - { - foreach ($this->criteriaProcessors as $provider) { - $provider->process($queryBuilder, $state); - } - } - - protected function createQueryBuilder(DataTableState $state): QueryBuilder - { - /** @var QueryBuilder $queryBuilder */ - $queryBuilder = $this->manager->createQueryBuilder(); - - // Run all query builder processors in order - foreach ($this->queryBuilderProcessors as $processor) { - $processor->process($queryBuilder, $state); - } - - return $queryBuilder; - } - - /** - * @param $identifier - * - * @return int - */ - protected function getCount(QueryBuilder $queryBuilder, string $identifier) - { - $qb = clone $queryBuilder; - - $qb->resetDQLPart('orderBy'); - $gb = $qb->getDQLPart('groupBy'); - if (empty($gb) || !$this->hasGroupByPart($identifier, $gb)) { - $qb->select($qb->expr()->count($identifier)); - - return (int) $qb->getQuery()->getSingleScalarResult(); - } - $qb->resetDQLPart('groupBy'); - $qb->select($qb->expr()->countDistinct($identifier)); - - return (int) $qb->getQuery()->getSingleScalarResult(); - } - - /** - * @param $identifier - * @param Query\Expr\GroupBy[] $gbList - * - * @return bool - */ - protected function hasGroupByPart($identifier, array $gbList) - { - foreach ($gbList as $gb) { - if (in_array($identifier, $gb->getParts(), true)) { - return true; - } - } - - return false; - } - - /** - * @param string $field - * - * @return string - */ - protected function mapFieldToPropertyPath($field, array $aliases = []) - { - $parts = explode('.', $field); - if (count($parts) < 2) { - throw new InvalidConfigurationException(sprintf("Field name '%s' must consist at least of an alias and a field separated with a period", $field)); - } - [$origin, $target] = $parts; - - $path = [$target]; - $current = $aliases[$origin][0]; - - while (null !== $current) { - [$origin, $target] = explode('.', $current); - $path[] = $target; - $current = $aliases[$origin][0]; - } - - if (Query::HYDRATE_ARRAY === $this->hydrationMode) { - return '['.implode('][', array_reverse($path)).']'; - } - - return implode('.', array_reverse($path)); - } - - protected function configureOptions(OptionsResolver $resolver): void - { - $providerNormalizer = function (Options $options, $value) { - return array_map([$this, 'normalizeProcessor'], (array) $value); - }; - - $resolver - ->setDefaults([ - 'hydrate' => Query::HYDRATE_OBJECT, - 'query' => [], - 'criteria' => static function (Options $options) { - return [new SearchCriteriaProvider()]; - }, - ]) - ->setRequired('entity') - ->setAllowedTypes('entity', ['string']) - ->setAllowedTypes('hydrate', 'int') - ->setAllowedTypes('query', [QueryBuilderProcessorInterface::class, 'array', 'callable']) - ->setAllowedTypes('criteria', [QueryBuilderProcessorInterface::class, 'array', 'callable', 'null']) - ->setNormalizer('query', $providerNormalizer) - ->setNormalizer('criteria', $providerNormalizer) - ; - } - - /** - * @param callable|QueryBuilderProcessorInterface $provider - * - * @return QueryBuilderProcessorInterface - */ - private function normalizeProcessor($provider) - { - if ($provider instanceof QueryBuilderProcessorInterface) { - return $provider; - } - - if (is_callable($provider)) { - return new class($provider) implements QueryBuilderProcessorInterface { - private $callable; - - public function __construct(callable $value) - { - $this->callable = $value; - } - - public function process(QueryBuilder $queryBuilder, DataTableState $state) - { - return call_user_func($this->callable, $queryBuilder, $state); - } - }; - } - - throw new InvalidConfigurationException('Provider must be a callable or implement QueryBuilderProcessorInterface'); - } -} diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index 0290b29e..51f68686 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -42,7 +42,6 @@ declare(strict_types=1); namespace App\DataTables; -use App\DataTables\Adapter\FetchJoinORMAdapter; use App\DataTables\Column\EntityColumn; use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\LocaleDateTimeColumn; @@ -61,6 +60,7 @@ use App\Services\Attachments\PartPreviewGenerator; use App\Services\EntityURLGenerator; use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\FetchJoinORMAdapter; use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; use Omines\DataTablesBundle\Column\BoolColumn; use Omines\DataTablesBundle\Column\MapColumn;