Completed
Pull Request — master (#792)
by Grégoire
01:37
created

ProxyQuery::addOrderedColumns()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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