Passed
Push — master ( ece3a4...72e2c0 )
by Jan
04:26
created

ORMAdapter::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * Symfony DataTables Bundle
5
 * (c) Omines Internetbureau B.V. - https://omines.nl/
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace App\DataTables\Adapter;
14
15
use Doctrine\Common\Persistence\ManagerRegistry;
16
use Doctrine\ORM\EntityManager;
17
use Doctrine\ORM\Query;
18
use Doctrine\ORM\QueryBuilder;
19
use Omines\DataTablesBundle\Adapter\AbstractAdapter;
20
use Omines\DataTablesBundle\Adapter\AdapterQuery;
21
use Omines\DataTablesBundle\Adapter\Doctrine\Event\ORMAdapterQueryEvent;
22
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\AutomaticQueryBuilder;
23
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\QueryBuilderProcessorInterface;
24
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
25
use Omines\DataTablesBundle\Column\AbstractColumn;
26
use Omines\DataTablesBundle\DataTableState;
27
use Omines\DataTablesBundle\Exception\InvalidConfigurationException;
28
use Omines\DataTablesBundle\Exception\MissingDependencyException;
29
use Symfony\Component\OptionsResolver\Options;
30
use Symfony\Component\OptionsResolver\OptionsResolver;
31
32
/**
33
 * ORMAdapter.
34
 *
35
 * @author Niels Keurentjes <[email protected]>
36
 * @author Robbert Beesems <[email protected]>
37
 */
38
class ORMAdapter extends AbstractAdapter
39
{
40
    /** @var ManagerRegistry */
41
    private $registry;
42
43
    /** @var EntityManager */
44
    protected $manager;
45
46
    /** @var \Doctrine\ORM\Mapping\ClassMetadata */
47
    protected $metadata;
48
49
    /** @var int */
50
    private $hydrationMode;
51
52
    /** @var QueryBuilderProcessorInterface[] */
53
    private $queryBuilderProcessors;
54
55
    /** @var QueryBuilderProcessorInterface[] */
56
    protected $criteriaProcessors;
57
58
    /**
59
     * DoctrineAdapter constructor.
60
     */
61
    public function __construct(ManagerRegistry $registry = null)
62
    {
63
        if (null === $registry) {
64
            throw new MissingDependencyException('Install doctrine/doctrine-bundle to use the ORMAdapter');
65
        }
66
67
        parent::__construct();
68
        $this->registry = $registry;
69
    }
70
71
    /**
72
     * {@inheritdoc}
73
     */
74
    public function configure(array $options)
75
    {
76
        $resolver = new OptionsResolver();
77
        $this->configureOptions($resolver);
78
        $options = $resolver->resolve($options);
79
80
        // Enable automated mode or just get the general default entity manager
81
        if (null === ($this->manager = $this->registry->getManagerForClass($options['entity']))) {
82
            throw new InvalidConfigurationException(sprintf('Doctrine has no manager for entity "%s", is it correctly imported and referenced?', $options['entity']));
83
        }
84
        $this->metadata = $this->manager->getClassMetadata($options['entity']);
85
        if (empty($options['query'])) {
86
            $options['query'] = [new AutomaticQueryBuilder($this->manager, $this->metadata)];
87
        }
88
89
        // Set options
90
        $this->hydrationMode = $options['hydrate'];
91
        $this->queryBuilderProcessors = $options['query'];
92
        $this->criteriaProcessors = $options['criteria'];
93
    }
94
95
    /**
96
     * @param mixed $processor
97
     */
98
    public function addCriteriaProcessor($processor)
99
    {
100
        $this->criteriaProcessors[] = $this->normalizeProcessor($processor);
101
    }
102
103
    protected function prepareQuery(AdapterQuery $query)
104
    {
105
        $state = $query->getState();
106
        $query->set('qb', $builder = $this->createQueryBuilder($state));
107
        $query->set('rootAlias', $rootAlias = $builder->getDQLPart('from')[0]->getAlias());
108
109
        // Provide default field mappings if needed
110
        foreach ($state->getDataTable()->getColumns() as $column) {
111
            if (null === $column->getField() && isset($this->metadata->fieldMappings[$name = $column->getName()])) {
112
                $column->setOption('field', "{$rootAlias}.{$name}");
113
            }
114
        }
115
116
        /** @var Query\Expr\From $fromClause */
117
        $fromClause = $builder->getDQLPart('from')[0];
118
        $identifier = "{$fromClause->getAlias()}.{$this->metadata->getSingleIdentifierFieldName()}";
119
        $query->setTotalRows($this->getCount($builder, $identifier));
120
121
        // Get record count after filtering
122
        $this->buildCriteria($builder, $state);
123
        $query->setFilteredRows($this->getCount($builder, $identifier));
124
125
        // Perform mapping of all referred fields and implied fields
126
        $aliases = $this->getAliases($query);
127
        $query->set('aliases', $aliases);
128
        $query->setIdentifierPropertyPath($this->mapFieldToPropertyPath($identifier, $aliases));
129
    }
130
131
    /**
132
     * @return array
133
     */
134
    protected function getAliases(AdapterQuery $query)
135
    {
136
        /** @var QueryBuilder $builder */
137
        $builder = $query->get('qb');
138
        $aliases = [];
139
140
        /** @var Query\Expr\From $from */
141
        foreach ($builder->getDQLPart('from') as $from) {
142
            $aliases[$from->getAlias()] = [null, $this->manager->getMetadataFactory()->getMetadataFor($from->getFrom())];
143
        }
144
145
        // Alias all joins
146
        foreach ($builder->getDQLPart('join') as $joins) {
147
            /** @var Query\Expr\Join $join */
148
            foreach ($joins as $join) {
149
                if (false === mb_strstr($join->getJoin(), '.')) {
150
                    continue;
151
                }
152
153
                list($origin, $target) = explode('.', $join->getJoin());
154
155
                $mapping = $aliases[$origin][1]->getAssociationMapping($target);
156
                $aliases[$join->getAlias()] = [$join->getJoin(), $this->manager->getMetadataFactory()->getMetadataFor($mapping['targetEntity'])];
157
            }
158
        }
159
160
        return $aliases;
161
    }
162
163
    /**
164
     * {@inheritdoc}
165
     */
166
    protected function mapPropertyPath(AdapterQuery $query, AbstractColumn $column)
167
    {
168
        return $this->mapFieldToPropertyPath($column->getField(), $query->get('aliases'));
0 ignored issues
show
Bug introduced by
It seems like $query->get('aliases') can also be of type null; however, parameter $aliases of App\DataTables\Adapter\O...apFieldToPropertyPath() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

168
        return $this->mapFieldToPropertyPath($column->getField(), /** @scrutinizer ignore-type */ $query->get('aliases'));
Loading history...
169
    }
170
171
    protected function getResults(AdapterQuery $query): \Traversable
172
    {
173
        /** @var QueryBuilder $builder */
174
        $builder = $query->get('qb');
175
        $state = $query->getState();
176
177
        // Apply definitive view state for current 'page' of the table
178
        foreach ($state->getOrderBy() as list($column, $direction)) {
179
            /** @var AbstractColumn $column */
180
            if ($column->isOrderable()) {
181
                $builder->addOrderBy($column->getOrderField(), $direction);
182
            }
183
        }
184
        if ($state->getLength() > 0) {
185
            $builder
186
                ->setFirstResult($state->getStart())
187
                ->setMaxResults($state->getLength())
188
            ;
189
        }
190
191
        $query = $builder->getQuery();
192
        $event = new ORMAdapterQueryEvent($query);
193
        $state->getDataTable()->getEventDispatcher()->dispatch($event, ORMAdapterEvents::PRE_QUERY);
0 ignored issues
show
Bug introduced by
The type App\DataTables\Adapter\ORMAdapterEvents was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with App\DataTables\Adapter\ORMAdapterEvents::PRE_QUERY. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

193
        $state->getDataTable()->getEventDispatcher()->/** @scrutinizer ignore-call */ dispatch($event, ORMAdapterEvents::PRE_QUERY);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
194
195
        foreach ($query->iterate([], $this->hydrationMode) as $result) {
196
            yield $entity = array_values($result)[0];
0 ignored issues
show
Bug introduced by
$result of type null|object is incompatible with the type array expected by parameter $input of array_values(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

196
            yield $entity = array_values(/** @scrutinizer ignore-type */ $result)[0];
Loading history...
197
            if (Query::HYDRATE_OBJECT === $this->hydrationMode) {
198
                $this->manager->detach($entity);
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\ORM\EntityManager::detach() has been deprecated: 2.7 This method is being removed from the ORM and won't have any replacement ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

198
                /** @scrutinizer ignore-deprecated */ $this->manager->detach($entity);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
199
            }
200
        }
201
    }
202
203
    protected function buildCriteria(QueryBuilder $queryBuilder, DataTableState $state)
204
    {
205
        foreach ($this->criteriaProcessors as $provider) {
206
            $provider->process($queryBuilder, $state);
207
        }
208
    }
209
210
    protected function createQueryBuilder(DataTableState $state): QueryBuilder
211
    {
212
        /** @var QueryBuilder $queryBuilder */
213
        $queryBuilder = $this->manager->createQueryBuilder();
214
215
        // Run all query builder processors in order
216
        foreach ($this->queryBuilderProcessors as $processor) {
217
            $processor->process($queryBuilder, $state);
218
        }
219
220
        return $queryBuilder;
221
    }
222
223
    /**
224
     * @param $identifier
225
     * @return int
226
     */
227
    protected function getCount(QueryBuilder $queryBuilder, $identifier)
228
    {
229
        $qb = clone $queryBuilder;
230
231
        $qb->resetDQLPart('orderBy');
232
        $gb = $qb->getDQLPart('groupBy');
233
        if (empty($gb) || !$this->hasGroupByPart($identifier, $gb)) {
234
            $qb->select($qb->expr()->count($identifier));
235
236
            return (int) $qb->getQuery()->getSingleScalarResult();
237
        } else {
238
            $qb->resetDQLPart('groupBy');
239
            $qb->select($qb->expr()->countDistinct($identifier));
240
241
            return (int) $qb->getQuery()->getSingleScalarResult();
242
        }
243
    }
244
245
    /**
246
     * @param $identifier
247
     * @param Query\Expr\GroupBy[] $gbList
248
     * @return bool
249
     */
250
    protected function hasGroupByPart($identifier, array $gbList)
251
    {
252
        foreach ($gbList as $gb) {
253
            if (in_array($identifier, $gb->getParts(), true)) {
254
                return true;
255
            }
256
        }
257
258
        return false;
259
    }
260
261
    /**
262
     * @param string $field
263
     * @return string
264
     */
265
    protected function mapFieldToPropertyPath($field, array $aliases = [])
266
    {
267
        $parts = explode('.', $field);
268
        if (count($parts) < 2) {
269
            throw new InvalidConfigurationException(sprintf("Field name '%s' must consist at least of an alias and a field separated with a period", $field));
270
        }
271
        list($origin, $target) = $parts;
272
273
        $path = [$target];
274
        $current = $aliases[$origin][0];
275
276
        while (null !== $current) {
277
            list($origin, $target) = explode('.', $current);
278
            $path[] = $target;
279
            $current = $aliases[$origin][0];
280
        }
281
282
        if (Query::HYDRATE_ARRAY === $this->hydrationMode) {
283
            return '[' . implode('][', array_reverse($path)) . ']';
284
        } else {
285
            return implode('.', array_reverse($path));
286
        }
287
    }
288
289
    protected function configureOptions(OptionsResolver $resolver)
290
    {
291
        $providerNormalizer = function (Options $options, $value) {
292
            return array_map([$this, 'normalizeProcessor'], (array) $value);
293
        };
294
295
        $resolver
296
            ->setDefaults([
297
                              'hydrate' => Query::HYDRATE_OBJECT,
298
                              'query' => [],
299
                              'criteria' => function (Options $options) {
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

299
                              'criteria' => function (/** @scrutinizer ignore-unused */ Options $options) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
300
                                  return [new SearchCriteriaProvider()];
301
                              },
302
                          ])
303
            ->setRequired('entity')
304
            ->setAllowedTypes('entity', ['string'])
305
            ->setAllowedTypes('hydrate', 'int')
306
            ->setAllowedTypes('query', [QueryBuilderProcessorInterface::class, 'array', 'callable'])
307
            ->setAllowedTypes('criteria', [QueryBuilderProcessorInterface::class, 'array', 'callable', 'null'])
308
            ->setNormalizer('query', $providerNormalizer)
309
            ->setNormalizer('criteria', $providerNormalizer)
310
        ;
311
    }
312
313
    /**
314
     * @param callable|QueryBuilderProcessorInterface $provider
315
     * @return QueryBuilderProcessorInterface
316
     */
317
    private function normalizeProcessor($provider)
318
    {
319
        if ($provider instanceof QueryBuilderProcessorInterface) {
320
            return $provider;
321
        } elseif (is_callable($provider)) {
322
            return new class($provider) implements QueryBuilderProcessorInterface {
323
                private $callable;
324
325
                public function __construct(callable $value)
326
                {
327
                    $this->callable = $value;
328
                }
329
330
                public function process(QueryBuilder $queryBuilder, DataTableState $state)
331
                {
332
                    return call_user_func($this->callable, $queryBuilder, $state);
333
                }
334
            };
335
        }
336
337
        throw new InvalidConfigurationException('Provider must be a callable or implement QueryBuilderProcessorInterface');
338
    }
339
}
340