Completed
Pull Request — master (#829)
by Grégoire
01:57
created

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