Completed
Push — 3.x ( fc4ac9...0816de )
by Marko
02:48 queued 25s
created

ProxyQuery::isDistinct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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