Completed
Push — master-dev-kit ( 1e64d8...90e190 )
by Sullivan
03:20 queued 28s
created

ProxyQuery::getQueryBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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