Completed
Pull Request — 3.x (#827)
by
unknown
02:34 queued 48s
created

ProxyQuery::setDistinct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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