Completed
Pull Request — 3.x-dev-kit (#736)
by
unknown
12:39 queued 10:58
created

ProxyQuery   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Importance

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

20 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
B execute() 0 55 8
A setSortBy() 0 7 1
A getSortBy() 0 4 1
A setSortOrder() 0 6 1
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
A addOrderedColumns() 0 10 3

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 Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
19
20
/**
21
 * This class try to unify the query usage with Doctrine.
22
 */
23
class ProxyQuery implements ProxyQueryInterface
24
{
25
    /**
26
     * @var QueryBuilder
27
     */
28
    protected $queryBuilder;
29
30
    /**
31
     * @var string
32
     */
33
    protected $sortBy;
34
35
    /**
36
     * @var mixed
37
     */
38
    protected $sortOrder;
39
40
    /**
41
     * @var int
42
     */
43
    protected $uniqueParameterId;
44
45
    /**
46
     * @var string[]
47
     */
48
    protected $entityJoinAliases;
49
50
    /**
51
     * The map of query hints.
52
     *
53
     * @var array<string,mixed>
54
     */
55
    private $hints = [];
56
57
    /**
58
     * @param QueryBuilder $queryBuilder
59
     */
60
    public function __construct($queryBuilder)
61
    {
62
        $this->queryBuilder = $queryBuilder;
63
        $this->uniqueParameterId = 0;
64
        $this->entityJoinAliases = [];
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    public function __call($name, $args)
71
    {
72
        return call_user_func_array([$this->queryBuilder, $name], $args);
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    public function __get($name)
79
    {
80
        return $this->queryBuilder->$name;
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function __clone()
87
    {
88
        $this->queryBuilder = clone $this->queryBuilder;
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function execute(array $params = [], $hydrationMode = null)
95
    {
96
        // always clone the original queryBuilder
97
        $queryBuilder = clone $this->queryBuilder;
98
99
        $rootAlias = current($queryBuilder->getRootAliases());
100
101
        // todo : check how doctrine behave, potential SQL injection here ...
102
        if ($this->getSortBy()) {
103
            $sortBy = $this->getSortBy();
104
            if (strpos($sortBy, '.') === false) { // add the current alias
105
                $sortBy = $rootAlias.'.'.$sortBy;
106
            }
107
            $queryBuilder->addOrderBy($sortBy, $this->getSortOrder());
108
        } else {
109
            $queryBuilder->resetDQLPart('orderBy');
110
        }
111
112
        /* By default, always add a sort on the identifier fields of the first
113
         * used entity in the query, because RDBMS do not guarantee a
114
         * particular order when no ORDER BY clause is specified, or when
115
         * the field used for sorting is not unique.
116
         */
117
118
        $identifierFields = $queryBuilder
119
            ->getEntityManager()
120
            ->getMetadataFactory()
121
            ->getMetadataFor(current($queryBuilder->getRootEntities()))
122
            ->getIdentifierFieldNames();
123
124
        $existingOrders = [];
125
        /** @var Query\Expr\OrderBy $order */
126
        foreach ($queryBuilder->getDQLPart('orderBy') as $order) {
127
            foreach ($order->getParts() as $part) {
128
                $existingOrders[] = trim(str_replace([Criteria::DESC, Criteria::ASC], '', $part));
129
            }
130
        }
131
132
        foreach ($identifierFields as $identifierField) {
133
            $order = $rootAlias.'.'.$identifierField;
134
            if (!in_array($order, $existingOrders)) {
135
                $queryBuilder->addOrderBy(
136
                    $order,
137
                    $this->getSortOrder() // reusing the sort order is the most natural way to go
138
                );
139
            }
140
        }
141
142
        $query = $this->getFixedQueryBuilder($queryBuilder)->getQuery();
143
        foreach ($this->hints as $name => $value) {
144
            $query->setHint($name, $value);
145
        }
146
147
        return $query->execute($params, $hydrationMode);
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153
    public function setSortBy($parentAssociationMappings, $fieldMapping)
154
    {
155
        $alias = $this->entityJoin($parentAssociationMappings);
156
        $this->sortBy = $alias.'.'.$fieldMapping['fieldName'];
157
158
        return $this;
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function getSortBy()
165
    {
166
        return $this->sortBy;
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function setSortOrder($sortOrder)
173
    {
174
        $this->sortOrder = $sortOrder;
175
176
        return $this;
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182
    public function getSortOrder()
183
    {
184
        return $this->sortOrder;
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190
    public function getSingleScalarResult()
191
    {
192
        $query = $this->queryBuilder->getQuery();
193
194
        return $query->getSingleScalarResult();
195
    }
196
197
    /**
198
     * @return mixed
199
     */
200
    public function getQueryBuilder()
201
    {
202
        return $this->queryBuilder;
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208
    public function setFirstResult($firstResult)
209
    {
210
        $this->queryBuilder->setFirstResult($firstResult);
211
212
        return $this;
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218
    public function getFirstResult()
219
    {
220
        return $this->queryBuilder->getFirstResult();
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     */
226
    public function setMaxResults($maxResults)
227
    {
228
        $this->queryBuilder->setMaxResults($maxResults);
229
230
        return $this;
231
    }
232
233
    /**
234
     * {@inheritdoc}
235
     */
236
    public function getMaxResults()
237
    {
238
        return $this->queryBuilder->getMaxResults();
239
    }
240
241
    /**
242
     * {@inheritdoc}
243
     */
244
    public function getUniqueParameterId()
245
    {
246
        return $this->uniqueParameterId++;
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252
    public function entityJoin(array $associationMappings)
253
    {
254
        $alias = $this->queryBuilder->getRootAlias();
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\ORM\QueryBuilder::getRootAlias() has been deprecated with message: Please use $qb->getRootAliases() instead.

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

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

Loading history...
255
256
        $newAlias = 's';
257
258
        $joinedEntities = $this->queryBuilder->getDQLPart('join');
259
260
        foreach ($associationMappings as $associationMapping) {
261
            // Do not add left join to already joined entities with custom query
262
            foreach ($joinedEntities as $joinExprList) {
263
                foreach ($joinExprList as $joinExpr) {
264
                    $newAliasTmp = $joinExpr->getAlias();
265
266
                    if (sprintf('%s.%s', $alias, $associationMapping['fieldName']) === $joinExpr->getJoin()) {
267
                        $this->entityJoinAliases[] = $newAliasTmp;
268
                        $alias = $newAliasTmp;
269
270
                        continue 3;
271
                    }
272
                }
273
            }
274
275
            $newAlias .= '_'.$associationMapping['fieldName'];
276
            if (!in_array($newAlias, $this->entityJoinAliases)) {
277
                $this->entityJoinAliases[] = $newAlias;
278
                $this->queryBuilder->leftJoin(sprintf('%s.%s', $alias, $associationMapping['fieldName']), $newAlias);
279
            }
280
281
            $alias = $newAlias;
282
        }
283
284
        return $alias;
285
    }
286
287
    /**
288
     * Sets a {@see \Doctrine\ORM\Query} hint. If the hint name is not recognized, it is silently ignored.
289
     *
290
     * @param string $name  the name of the hint
291
     * @param mixed  $value the value of the hint
292
     *
293
     * @return ProxyQueryInterface
294
     *
295
     * @see \Doctrine\ORM\Query::setHint
296
     * @see \Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER
297
     */
298
    final public function setHint($name, $value)
299
    {
300
        $this->hints[$name] = $value;
301
302
        return $this;
303
    }
304
305
    /**
306
     * This method alters the query to return a clean set of object with a working
307
     * set of Object.
308
     *
309
     * @param QueryBuilder $queryBuilder
310
     *
311
     * @return QueryBuilder
312
     */
313
    protected function getFixedQueryBuilder(QueryBuilder $queryBuilder)
314
    {
315
        $queryBuilderId = clone $queryBuilder;
316
        $rootAlias = current($queryBuilderId->getRootAliases());
317
318
        // step 1 : retrieve the targeted class
319
        $from = $queryBuilderId->getDQLPart('from');
320
        $class = $from[0]->getFrom();
321
        $metadata = $queryBuilderId->getEntityManager()->getMetadataFactory()->getMetadataFor($class);
322
323
        // step 2 : retrieve identifier columns
324
        $idNames = $metadata->getIdentifierFieldNames();
325
326
        // step 3 : retrieve the different subjects ids
327
        $selects = [];
328
        $idxSelect = '';
329
        foreach ($idNames as $idName) {
330
            $select = sprintf('%s.%s', $rootAlias, $idName);
331
            // Put the ID select on this array to use it on results QB
332
            $selects[$idName] = $select;
333
            // Use IDENTITY if id is a relation too.
334
            // See: http://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html
335
            // Should work only with doctrine/orm: ~2.2
336
            $idSelect = $select;
337
            if ($metadata->hasAssociation($idName)) {
338
                $idSelect = sprintf('IDENTITY(%s) as %s', $idSelect, $idName);
339
            }
340
            $idxSelect .= ($idxSelect !== '' ? ', ' : '').$idSelect;
341
        }
342
        $queryBuilderId->select($idxSelect);
343
        $queryBuilderId->distinct();
344
345
        // for SELECT DISTINCT, ORDER BY expressions must appear in idxSelect list
346
        /* Consider
347
            SELECT DISTINCT x FROM tab ORDER BY y;
348
        For any particular x-value in the table there might be many different y
349
        values.  Which one will you use to sort that x-value in the output?
350
        */
351
        $this->addOrderedColumns($queryBuilderId);
352
353
        $results = $queryBuilderId->getQuery()->execute([], Query::HYDRATE_ARRAY);
354
        $platform = $queryBuilderId->getEntityManager()->getConnection()->getDatabasePlatform();
355
        $idxMatrix = [];
356
        foreach ($results as $id) {
357
            foreach ($idNames as $idName) {
358
                // Convert ids to database value in case of custom type, if provided.
359
                $fieldType = $metadata->getTypeOfField($idName);
360
                $idxMatrix[$idName][] = $fieldType && Type::hasType($fieldType)
361
                    ? Type::getType($fieldType)->convertToDatabaseValue($id[$idName], $platform)
362
                    : $id[$idName];
363
            }
364
        }
365
366
        // step 4 : alter the query to match the targeted ids
367
        foreach ($idxMatrix as $idName => $idx) {
368
            if (count($idx) > 0) {
369
                $idxParamName = sprintf('%s_idx', $idName);
370
                $idxParamName = preg_replace('/[^\w]+/', '_', $idxParamName);
371
                $queryBuilder->andWhere(sprintf('%s IN (:%s)', $selects[$idName], $idxParamName));
372
                $queryBuilder->setParameter($idxParamName, $idx);
373
                $queryBuilder->setMaxResults(null);
374
                $queryBuilder->setFirstResult(null);
375
            }
376
        }
377
378
        return $queryBuilder;
379
    }
380
381
    private function addOrderedColumns(QueryBuilder $queryBuilder)
382
    {
383
        /* For each ORDER BY clause defined directly in the DQL parts of the query,
384
           we add an entry in the SELECT clause. */
385
        foreach ((array) $queryBuilder->getDqlPart('orderBy') as $part) {
386
            foreach ($part->getParts() as $orderBy) {
387
                $queryBuilder->addSelect(preg_replace("/\s+(ASC|DESC)$/i", '', $orderBy));
388
            }
389
        }
390
    }
391
}
392