ProxyQuery   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 357
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Importance

Changes 0
Metric Value
wmc 45
lcom 2
cbo 7
dl 0
loc 357
rs 8.8
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A __call() 0 4 1
A __get() 0 4 1
A __clone() 0 4 1
A setDistinct() 0 10 2
A isDistinct() 0 4 1
B execute() 0 60 9
A setSortBy() 0 7 1
A getSortBy() 0 4 1
A setSortOrder() 0 13 2
A getSortOrder() 0 4 1
A getSingleScalarResult() 0 6 1
A getQueryBuilder() 0 4 1
A setFirstResult() 0 6 1
A getFirstResult() 0 4 1
A setMaxResults() 0 6 1
A getMaxResults() 0 4 1
A getUniqueParameterId() 0 4 1
B entityJoin() 0 34 6
A setHint() 0 6 1
C getFixedQueryBuilder() 0 67 10

How to fix   Complexity   

Complex Class

Complex classes like ProxyQuery often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ProxyQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\DoctrineORMAdminBundle\Datagrid;
15
16
use Doctrine\Common\Collections\Criteria;
17
use Doctrine\DBAL\Types\Type;
18
use Doctrine\ORM\EntityManager;
19
use Doctrine\ORM\Query;
20
use Doctrine\ORM\QueryBuilder;
21
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
22
23
/**
24
 * This class try to unify the query usage with Doctrine.
25
 *
26
 * @method Query\Expr    expr()
27
 * @method QueryBuilder  setCacheable($cacheable)
28
 * @method bool          isCacheable()
29
 * @method QueryBuilder  setCacheRegion($cacheRegion)
30
 * @method string|null   getCacheRegion()
31
 * @method int           getLifetime()
32
 * @method QueryBuilder  setLifetime($lifetime)
33
 * @method int           getCacheMode()
34
 * @method QueryBuilder  setCacheMode($cacheMode)
35
 * @method int           getType()
36
 * @method EntityManager getEntityManager()
37
 * @method int           getState()
38
 * @method string        getDQL()
39
 * @method Query         getQuery()
40
 * @method string        getRootAlias()
41
 * @method array         getRootAliases()
42
 * @method array         getAllAliases()
43
 * @method array         getRootEntities()
44
 * @method QueryBuilder  setParameter($key, $value, $type = null)
45
 * @method QueryBuilder  setParameters($parameters)
46
 * @method QueryBuilder  getParameters()
47
 * @method QueryBuilder  getParameter($key)
48
 * @method QueryBuilder  add($dqlPartName, $dqlPart, $append = false)
49
 * @method QueryBuilder  select($select = null)
50
 * @method QueryBuilder  distinct($flag = true)
51
 * @method QueryBuilder  addSelect($select = null)
52
 * @method QueryBuilder  delete($delete = null, $alias = null)
53
 * @method QueryBuilder  update($update = null, $alias = null)
54
 * @method QueryBuilder  from($from, $alias, $indexBy = null)
55
 * @method QueryBuilder  indexBy($alias, $indexBy)
56
 * @method QueryBuilder  join($join, $alias, $conditionType = null, $condition = null, $indexBy = null)
57
 * @method QueryBuilder  innerJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null)
58
 * @method QueryBuilder  leftJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null)
59
 * @method QueryBuilder  set($key, $value)
60
 * @method QueryBuilder  where($where)
61
 * @method QueryBuilder  andWhere($where)
62
 * @method QueryBuilder  orWhere($where)
63
 * @method QueryBuilder  groupBy($groupBy)
64
 * @method QueryBuilder  addGroupBy($groupBy)
65
 * @method QueryBuilder  having($having)
66
 * @method QueryBuilder  andHaving($having)
67
 * @method QueryBuilder  orHaving($having)
68
 * @method QueryBuilder  orderBy($sort, $order = null)
69
 * @method QueryBuilder  addOrderBy($sort, $order = null)
70
 * @method QueryBuilder  addCriteria(Criteria $criteria)
71
 * @method mixed         getDQLPart($queryPartName)
72
 * @method array         getDQLParts()
73
 * @method QueryBuilder  resetDQLParts($parts = null)
74
 * @method QueryBuilder  resetDQLPart($part)
75
 */
76
class ProxyQuery implements ProxyQueryInterface
77
{
78
    /**
79
     * @var QueryBuilder
80
     */
81
    protected $queryBuilder;
82
83
    /**
84
     * @var string
85
     */
86
    protected $sortBy;
87
88
    /**
89
     * @var mixed
90
     */
91
    protected $sortOrder;
92
93
    /**
94
     * @var int
95
     */
96
    protected $uniqueParameterId;
97
98
    /**
99
     * @var string[]
100
     */
101
    protected $entityJoinAliases;
102
103
    /**
104
     * For BC reasons, this property is true by default.
105
     *
106
     * @var bool
107
     */
108
    private $distinct = true;
109
110
    /**
111
     * The map of query hints.
112
     *
113
     * @var array<string,mixed>
114
     */
115
    private $hints = [];
116
117
    /**
118
     * @param QueryBuilder $queryBuilder
119
     */
120
    public function __construct($queryBuilder)
121
    {
122
        $this->queryBuilder = $queryBuilder;
123
        $this->uniqueParameterId = 0;
124
        $this->entityJoinAliases = [];
125
    }
126
127
    public function __call($name, $args)
128
    {
129
        return $this->queryBuilder->$name(...$args);
130
    }
131
132
    public function __get($name)
133
    {
134
        return $this->queryBuilder->$name;
135
    }
136
137
    public function __clone()
138
    {
139
        $this->queryBuilder = clone $this->queryBuilder;
140
    }
141
142
    /**
143
     * Optimize queries with a lot of rows.
144
     * It is not recommended to use "false" with left joins.
145
     *
146
     * @param bool $distinct
147
     *
148
     * @return self
149
     */
150
    final public function setDistinct($distinct)
151
    {
152
        if (!\is_bool($distinct)) {
153
            throw new \InvalidArgumentException('$distinct is not a boolean');
154
        }
155
156
        $this->distinct = $distinct;
157
158
        return $this;
159
    }
160
161
    /**
162
     * @return bool
163
     */
164
    final public function isDistinct()
165
    {
166
        return $this->distinct;
167
    }
168
169
    public function execute(array $params = [], $hydrationMode = null)
170
    {
171
        // always clone the original queryBuilder
172
        $queryBuilder = clone $this->queryBuilder;
173
174
        $rootAlias = current($queryBuilder->getRootAliases());
175
176
        // todo : check how doctrine behave, potential SQL injection here ...
177
        if ($this->getSortBy()) {
178
            $orderByDQLPart = $queryBuilder->getDQLPart('orderBy');
179
            $queryBuilder->resetDQLPart('orderBy');
180
181
            $sortBy = $this->getSortBy();
182
            if (false === strpos($sortBy, '.')) { // add the current alias
183
                $sortBy = $rootAlias.'.'.$sortBy;
184
            }
185
            $queryBuilder->addOrderBy($sortBy, $this->getSortOrder());
186
187
            foreach ($orderByDQLPart as $orderBy) {
188
                $queryBuilder->addOrderBy($orderBy);
189
            }
190
        }
191
192
        /* By default, always add a sort on the identifier fields of the first
193
         * used entity in the query, because RDBMS do not guarantee a
194
         * particular order when no ORDER BY clause is specified, or when
195
         * the field used for sorting is not unique.
196
         */
197
198
        $identifierFields = $queryBuilder
199
            ->getEntityManager()
200
            ->getMetadataFactory()
201
            ->getMetadataFor(current($queryBuilder->getRootEntities()))
202
            ->getIdentifierFieldNames();
203
204
        $existingOrders = [];
205
        /** @var Query\Expr\OrderBy $order */
206
        foreach ($queryBuilder->getDQLPart('orderBy') as $order) {
207
            foreach ($order->getParts() as $part) {
208
                $existingOrders[] = trim(str_replace([Criteria::DESC, Criteria::ASC], '', $part));
209
            }
210
        }
211
212
        foreach ($identifierFields as $identifierField) {
213
            $order = $rootAlias.'.'.$identifierField;
214
            if (!\in_array($order, $existingOrders, true)) {
215
                $queryBuilder->addOrderBy(
216
                    $order,
217
                    $this->getSortOrder() // reusing the sort order is the most natural way to go
218
                );
219
            }
220
        }
221
222
        $query = $this->getFixedQueryBuilder($queryBuilder)->getQuery();
223
        foreach ($this->hints as $name => $value) {
224
            $query->setHint($name, $value);
225
        }
226
227
        return $query->execute($params, $hydrationMode);
228
    }
229
230
    public function setSortBy($parentAssociationMappings, $fieldMapping)
231
    {
232
        $alias = $this->entityJoin($parentAssociationMappings);
233
        $this->sortBy = $alias.'.'.$fieldMapping['fieldName'];
234
235
        return $this;
236
    }
237
238
    public function getSortBy()
239
    {
240
        return $this->sortBy;
241
    }
242
243
    public function setSortOrder($sortOrder)
244
    {
245
        if (!\in_array(strtoupper($sortOrder), $validSortOrders = ['ASC', 'DESC'], true)) {
246
            throw new \InvalidArgumentException(sprintf(
247
                '"%s" is not a valid sort order, valid values are "%s"',
248
                $sortOrder,
249
                implode(', ', $validSortOrders)
250
            ));
251
        }
252
        $this->sortOrder = $sortOrder;
253
254
        return $this;
255
    }
256
257
    public function getSortOrder()
258
    {
259
        return $this->sortOrder;
260
    }
261
262
    public function getSingleScalarResult()
263
    {
264
        $query = $this->queryBuilder->getQuery();
265
266
        return $query->getSingleScalarResult();
267
    }
268
269
    /**
270
     * @return QueryBuilder
271
     */
272
    public function getQueryBuilder()
273
    {
274
        return $this->queryBuilder;
275
    }
276
277
    public function setFirstResult($firstResult)
278
    {
279
        $this->queryBuilder->setFirstResult($firstResult);
280
281
        return $this;
282
    }
283
284
    public function getFirstResult()
285
    {
286
        return $this->queryBuilder->getFirstResult();
287
    }
288
289
    public function setMaxResults($maxResults)
290
    {
291
        $this->queryBuilder->setMaxResults($maxResults);
292
293
        return $this;
294
    }
295
296
    public function getMaxResults()
297
    {
298
        return $this->queryBuilder->getMaxResults();
299
    }
300
301
    public function getUniqueParameterId()
302
    {
303
        return $this->uniqueParameterId++;
304
    }
305
306
    public function entityJoin(array $associationMappings)
307
    {
308
        $alias = current($this->queryBuilder->getRootAliases());
309
310
        $newAlias = 's';
311
312
        $joinedEntities = $this->queryBuilder->getDQLPart('join');
313
314
        foreach ($associationMappings as $associationMapping) {
315
            // Do not add left join to already joined entities with custom query
316
            foreach ($joinedEntities as $joinExprList) {
317
                foreach ($joinExprList as $joinExpr) {
318
                    $newAliasTmp = $joinExpr->getAlias();
319
320
                    if (sprintf('%s.%s', $alias, $associationMapping['fieldName']) === $joinExpr->getJoin()) {
321
                        $this->entityJoinAliases[] = $newAliasTmp;
322
                        $alias = $newAliasTmp;
323
324
                        continue 3;
325
                    }
326
                }
327
            }
328
329
            $newAlias .= '_'.$associationMapping['fieldName'];
330
            if (!\in_array($newAlias, $this->entityJoinAliases, true)) {
331
                $this->entityJoinAliases[] = $newAlias;
332
                $this->queryBuilder->leftJoin(sprintf('%s.%s', $alias, $associationMapping['fieldName']), $newAlias);
333
            }
334
335
            $alias = $newAlias;
336
        }
337
338
        return $alias;
339
    }
340
341
    /**
342
     * Sets a {@see \Doctrine\ORM\Query} hint. If the hint name is not recognized, it is silently ignored.
343
     *
344
     * @param string $name  the name of the hint
345
     * @param mixed  $value the value of the hint
346
     *
347
     * @return ProxyQueryInterface
348
     *
349
     * @see \Doctrine\ORM\Query::setHint
350
     * @see \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER
351
     */
352
    final public function setHint($name, $value)
353
    {
354
        $this->hints[$name] = $value;
355
356
        return $this;
357
    }
358
359
    /**
360
     * This method alters the query to return a clean set of object with a working
361
     * set of Object.
362
     *
363
     * @return QueryBuilder
364
     */
365
    protected function getFixedQueryBuilder(QueryBuilder $queryBuilder)
366
    {
367
        $queryBuilderId = clone $queryBuilder;
368
        $rootAlias = current($queryBuilderId->getRootAliases());
369
370
        // step 1 : retrieve the targeted class
371
        $from = $queryBuilderId->getDQLPart('from');
372
        $class = $from[0]->getFrom();
373
        $metadata = $queryBuilderId->getEntityManager()->getMetadataFactory()->getMetadataFor($class);
374
375
        // step 2 : retrieve identifier columns
376
        $idNames = $metadata->getIdentifierFieldNames();
377
378
        // step 3 : retrieve the different subjects ids
379
        $selects = [];
380
        $idxSelect = '';
381
        foreach ($idNames as $idName) {
382
            $select = sprintf('%s.%s', $rootAlias, $idName);
383
            // Put the ID select on this array to use it on results QB
384
            $selects[$idName] = $select;
385
            // Use IDENTITY if id is a relation too.
386
            // See: http://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html
387
            // Should work only with doctrine/orm: ~2.2
388
            $idSelect = $select;
389
            if ($metadata->hasAssociation($idName)) {
390
                $idSelect = sprintf('IDENTITY(%s) as %s', $idSelect, $idName);
391
            }
392
            $idxSelect .= ('' !== $idxSelect ? ', ' : '').$idSelect;
393
        }
394
        $queryBuilderId->select($idxSelect);
395
        $queryBuilderId->distinct($this->isDistinct());
396
397
        // for SELECT DISTINCT, ORDER BY expressions must appear in idxSelect list
398
        /* Consider
399
            SELECT DISTINCT x FROM tab ORDER BY y;
400
        For any particular x-value in the table there might be many different y
401
        values.  Which one will you use to sort that x-value in the output?
402
        */
403
        $queryId = $queryBuilderId->getQuery();
404
        $queryId->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [OrderByToSelectWalker::class]);
405
        $results = $queryId->execute([], Query::HYDRATE_ARRAY);
406
        $platform = $queryBuilderId->getEntityManager()->getConnection()->getDatabasePlatform();
407
        $idxMatrix = [];
408
        foreach ($results as $id) {
409
            foreach ($idNames as $idName) {
410
                // Convert ids to database value in case of custom type, if provided.
411
                $fieldType = $metadata->getTypeOfField($idName);
412
                $idxMatrix[$idName][] = $fieldType && Type::hasType($fieldType)
413
                    ? Type::getType($fieldType)->convertToDatabaseValue($id[$idName], $platform)
414
                    : $id[$idName];
415
            }
416
        }
417
418
        // step 4 : alter the query to match the targeted ids
419
        foreach ($idxMatrix as $idName => $idx) {
420
            if (\count($idx) > 0) {
421
                $idxParamName = sprintf('%s_idx', $idName);
422
                $idxParamName = preg_replace('/[^\w]+/', '_', $idxParamName);
423
                $queryBuilder->andWhere(sprintf('%s IN (:%s)', $selects[$idName], $idxParamName));
424
                $queryBuilder->setParameter($idxParamName, $idx);
425
                $queryBuilder->setMaxResults(null);
426
                $queryBuilder->setFirstResult(null);
427
            }
428
        }
429
430
        return $queryBuilder;
431
    }
432
}
433