Completed
Pull Request — 3.x (#827)
by
unknown
02:26
created

ProxyQuery   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 348
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Importance

Changes 0
Metric Value
wmc 43
c 0
b 0
f 0
lcom 2
cbo 7
dl 0
loc 348
rs 8.3157

21 Methods

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