Completed
Pull Request — master (#511)
by Grégoire
02:26
created

ProxyQuery::getQueryBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
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 symfony package.
5
 * (c) Fabien Potencier <[email protected]>
6
 * (c) Jonathan H. Wage <[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\ORM\Query;
15
use Doctrine\ORM\QueryBuilder;
16
use Sonata\AdminBundle\Datagrid\ProxyQueryInterface;
17
18
/**
19
 * This class try to unify the query usage with Doctrine.
20
 */
21
class ProxyQuery implements ProxyQueryInterface
22
{
23
    /**
24
     * @var QueryBuilder
25
     */
26
    protected $queryBuilder;
27
28
    /**
29
     * @var string
30
     */
31
    protected $sortBy;
32
33
    /**
34
     * @var mixed
35
     */
36
    protected $sortOrder;
37
38
    /**
39
     * @var int
40
     */
41
    protected $uniqueParameterId;
42
43
    /**
44
     * @var string[]
45
     */
46
    protected $entityJoinAliases;
47
48
    /**
49
     * @param QueryBuilder $queryBuilder
50
     */
51
    public function __construct($queryBuilder)
52
    {
53
        $this->queryBuilder      = $queryBuilder;
54
        $this->uniqueParameterId = 0;
55
        $this->entityJoinAliases = array();
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function execute(array $params = array(), $hydrationMode = null)
62
    {
63
        // always clone the original queryBuilder
64
        $queryBuilder = clone $this->queryBuilder;
65
66
        // todo : check how doctrine behave, potential SQL injection here ...
67
        if ($this->getSortBy()) {
68
            $sortBy = $this->getSortBy();
69
            if (strpos($sortBy, '.') === false) { // add the current alias
70
                $sortBy = $queryBuilder->getRootAlias().'.'.$sortBy;
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...
71
            }
72
            $queryBuilder->addOrderBy($sortBy, $this->getSortOrder());
73
        } else {
74
            $queryBuilder->resetDQLPart('orderBy');
75
        }
76
77
        /* By default, always add a sort on the identifier fields of the first
78
            * used entity in the query, because RDBMS do not guarantee a
79
            * particular order when no ORDER BY clause is specified, or when
80
            * the field used for sorting is not unique. */
81
82
        $identifierFields = $queryBuilder
83
            ->getEntityManager()
84
            ->getMetadataFactory()
85
            ->getMetadataFor(current($queryBuilder->getRootEntities()))
86
            ->getIdentifierFieldNames();
87
88
        foreach ($identifierFields as $identifierField) {
89
            $queryBuilder->addOrderBy(
90
                $queryBuilder->getRootAlias().'.'.$identifierField,
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...
91
                $this->getSortOrder() // reusing the sort order is the more natural way to go
92
            );
93
        }
94
95
        return $this->getFixedQueryBuilder($queryBuilder)->getQuery()->execute($params, $hydrationMode);
96
    }
97
98
    /**
99
     * This method alters the query to return a clean set of object with a working
100
     * set of Object.
101
     *
102
     * @param QueryBuilder $queryBuilder
103
     *
104
     * @return QueryBuilder
105
     */
106
    private function getFixedQueryBuilder(QueryBuilder $queryBuilder)
107
    {
108
        $queryBuilderId = clone $queryBuilder;
109
110
        // step 1 : retrieve the targeted class
111
        $from  = $queryBuilderId->getDQLPart('from');
112
        $class = $from[0]->getFrom();
113
        $metadata = $queryBuilderId->getEntityManager()->getMetadataFactory()->getMetadataFor($class);
114
115
        // step 2 : retrieve identifier columns
116
        $idNames = $metadata->getIdentifierFieldNames();
117
118
        // step 3 : retrieve the different subjects ids
119
        $selects = array();
120
        $idxSelect = '';
121
        foreach ($idNames as $idName) {
122
            $select = sprintf('%s.%s', $queryBuilderId->getRootAlias(), $idName);
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...
123
            // Put the ID select on this array to use it on results QB
124
            $selects[$idName] = $select;
125
            // Use IDENTITY if id is a relation too. See: http://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html
126
            // Should work only with doctrine/orm: ~2.2
127
            $idSelect = $select;
128
            if ($metadata->hasAssociation($idName)) {
129
                $idSelect = sprintf('IDENTITY(%s) as %s', $idSelect, $idName);
130
            }
131
            $idxSelect .= ($idxSelect !== '' ? ', ' : '').$idSelect;
132
        }
133
        $queryBuilderId->resetDQLPart('select');
134
        $queryBuilderId->add('select', 'DISTINCT '.$idxSelect);
0 ignored issues
show
Documentation introduced by
'DISTINCT ' . $idxSelect is of type string, but the function expects a object<Doctrine\ORM\Query\Expr\Base>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
135
136
        // for SELECT DISTINCT, ORDER BY expressions must appear in idxSelect list
137
        /* Consider
138
            SELECT DISTINCT x FROM tab ORDER BY y;
139
        For any particular x-value in the table there might be many different y
140
        values.  Which one will you use to sort that x-value in the output?
141
        */
142
        // todo : check how doctrine behave, potential SQL injection here ...
143
        if ($this->getSortBy()) {
144
            $sortBy = $this->getSortBy();
145
            if (strpos($sortBy, '.') === false) { // add the current alias
146
                $sortBy = $queryBuilderId->getRootAlias().'.'.$sortBy;
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...
147
            }
148
            $sortBy .= ' AS __order_by';
149
            $queryBuilderId->addSelect($sortBy);
150
        }
151
152
        $results    = $queryBuilderId->getQuery()->execute(array(), Query::HYDRATE_ARRAY);
153
        $idxMatrix  = array();
154
        foreach ($results as $id) {
155
            foreach ($idNames as $idName) {
156
                $idxMatrix[$idName][] = $id[$idName];
157
            }
158
        }
159
160
        // step 4 : alter the query to match the targeted ids
161
        foreach ($idxMatrix as $idName => $idx) {
162
            if (count($idx) > 0) {
163
                $idxParamName = sprintf('%s_idx', $idName);
164
                $queryBuilder->andWhere(sprintf('%s IN (:%s)', $selects[$idName], $idxParamName));
165
                $queryBuilder->setParameter($idxParamName, $idx);
166
                $queryBuilder->setMaxResults(null);
167
                $queryBuilder->setFirstResult(null);
168
            }
169
        }
170
171
        return $queryBuilder;
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    public function __call($name, $args)
178
    {
179
        return call_user_func_array(array($this->queryBuilder, $name), $args);
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    public function __get($name)
186
    {
187
        return $this->queryBuilder->$name;
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193
    public function setSortBy($parentAssociationMappings, $fieldMapping)
194
    {
195
        $alias        = $this->entityJoin($parentAssociationMappings);
196
        $this->sortBy = $alias.'.'.$fieldMapping['fieldName'];
197
198
        return $this;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204
    public function getSortBy()
205
    {
206
        return $this->sortBy;
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    public function setSortOrder($sortOrder)
213
    {
214
        $this->sortOrder = $sortOrder;
215
216
        return $this;
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222
    public function getSortOrder()
223
    {
224
        return $this->sortOrder;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230
    public function getSingleScalarResult()
231
    {
232
        $query = $this->queryBuilder->getQuery();
233
234
        return $query->getSingleScalarResult();
235
    }
236
237
    /**
238
     * {@inheritdoc}
239
     */
240
    public function __clone()
241
    {
242
        $this->queryBuilder = clone $this->queryBuilder;
243
    }
244
245
    /**
246
     * @return mixed
247
     */
248
    public function getQueryBuilder()
249
    {
250
        return $this->queryBuilder;
251
    }
252
253
    /**
254
     * {@inheritdoc}
255
     */
256
    public function setFirstResult($firstResult)
257
    {
258
        $this->queryBuilder->setFirstResult($firstResult);
259
260
        return $this;
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266
    public function getFirstResult()
267
    {
268
        return $this->queryBuilder->getFirstResult();
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function setMaxResults($maxResults)
275
    {
276
        $this->queryBuilder->setMaxResults($maxResults);
277
278
        return $this;
279
    }
280
281
    /**
282
     * {@inheritdoc}
283
     */
284
    public function getMaxResults()
285
    {
286
        return $this->queryBuilder->getMaxResults();
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     */
292
    public function getUniqueParameterId()
293
    {
294
        return $this->uniqueParameterId++;
295
    }
296
297
    /**
298
     * {@inheritdoc}
299
     */
300
    public function entityJoin(array $associationMappings)
301
    {
302
        $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...
303
304
        $newAlias = 's';
305
306
        $joinedEntities = $this->queryBuilder->getDQLPart('join');
307
308
        foreach ($associationMappings as $associationMapping) {
309
310
             // Do not add left join to already joined entities with custom query
311
             foreach ($joinedEntities as $joinExprList) {
312
                 foreach ($joinExprList as $joinExpr) {
313
                     $newAliasTmp = $joinExpr->getAlias();
314
315
                     if (sprintf('%s.%s', $alias, $associationMapping['fieldName']) === $joinExpr->getJoin()) {
316
                         $this->entityJoinAliases[] = $newAliasTmp;
317
                         $alias = $newAliasTmp;
318
319
                         continue 3;
320
                     }
321
                 }
322
             }
323
324
            $newAlias .= '_'.$associationMapping['fieldName'];
325
            if (!in_array($newAlias, $this->entityJoinAliases)) {
326
                $this->entityJoinAliases[] = $newAlias;
327
                $this->queryBuilder->leftJoin(sprintf('%s.%s', $alias, $associationMapping['fieldName']), $newAlias);
328
            }
329
330
            $alias = $newAlias;
331
        }
332
333
        return $alias;
334
    }
335
}
336