Completed
Pull Request — 3.x (#703)
by
unknown
01:53
created

ProxyQuery::entityJoin()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 34
rs 8.439
c 0
b 0
f 0
cc 6
eloc 18
nc 8
nop 1
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
        // todo : check how doctrine behave, potential SQL injection here ...
91
        if ($this->getSortBy()) {
92
            $sortBy = $this->getSortBy();
93
            if (strpos($sortBy, '.') === false) { // add the current alias
94
                $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...
95
            }
96
            $queryBuilder->addOrderBy($sortBy, $this->getSortOrder());
97
        } else {
98
            $queryBuilder->resetDQLPart('orderBy');
99
        }
100
101
        return $this->getFixedQueryBuilder($queryBuilder)->getQuery()->execute($params, $hydrationMode);
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function setSortBy($parentAssociationMappings, $fieldMapping)
108
    {
109
        $alias = $this->entityJoin($parentAssociationMappings);
110
        $this->sortBy = $alias.'.'.$fieldMapping['fieldName'];
111
112
        return $this;
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    public function getSortBy()
119
    {
120
        return $this->sortBy;
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function setSortOrder($sortOrder)
127
    {
128
        $this->sortOrder = $sortOrder;
129
130
        return $this;
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function getSortOrder()
137
    {
138
        return $this->sortOrder;
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function getSingleScalarResult()
145
    {
146
        $query = $this->queryBuilder->getQuery();
147
148
        return $query->getSingleScalarResult();
149
    }
150
151
    /**
152
     * @return mixed
153
     */
154
    public function getQueryBuilder()
155
    {
156
        return $this->queryBuilder;
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function setFirstResult($firstResult)
163
    {
164
        $this->queryBuilder->setFirstResult($firstResult);
165
166
        return $this;
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function getFirstResult()
173
    {
174
        return $this->queryBuilder->getFirstResult();
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function setMaxResults($maxResults)
181
    {
182
        $this->queryBuilder->setMaxResults($maxResults);
183
184
        return $this;
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190
    public function getMaxResults()
191
    {
192
        return $this->queryBuilder->getMaxResults();
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function getUniqueParameterId()
199
    {
200
        return $this->uniqueParameterId++;
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    public function entityJoin(array $associationMappings)
207
    {
208
        $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...
209
210
        $newAlias = 's';
211
212
        $joinedEntities = $this->queryBuilder->getDQLPart('join');
213
214
        foreach ($associationMappings as $associationMapping) {
215
            // Do not add left join to already joined entities with custom query
216
             foreach ($joinedEntities as $joinExprList) {
217
                 foreach ($joinExprList as $joinExpr) {
218
                     $newAliasTmp = $joinExpr->getAlias();
219
220
                     if (sprintf('%s.%s', $alias, $associationMapping['fieldName']) === $joinExpr->getJoin()) {
221
                         $this->entityJoinAliases[] = $newAliasTmp;
222
                         $alias = $newAliasTmp;
223
224
                         continue 3;
225
                     }
226
                 }
227
             }
228
229
            $newAlias .= '_'.$associationMapping['fieldName'];
230
            if (!in_array($newAlias, $this->entityJoinAliases)) {
231
                $this->entityJoinAliases[] = $newAlias;
232
                $this->queryBuilder->leftJoin(sprintf('%s.%s', $alias, $associationMapping['fieldName']), $newAlias);
233
            }
234
235
            $alias = $newAlias;
236
        }
237
238
        return $alias;
239
    }
240
241
    /**
242
     * This method alters the query to return a clean set of object with a working
243
     * set of Object.
244
     *
245
     * @param QueryBuilder $queryBuilder
246
     *
247
     * @return QueryBuilder
248
     */
249
    protected function getFixedQueryBuilder(QueryBuilder $queryBuilder)
250
    {
251
        $queryBuilderId = clone $queryBuilder;
252
253
        // step 1 : retrieve the targeted class
254
        $from = $queryBuilderId->getDQLPart('from');
255
        $class = $from[0]->getFrom();
256
        $metadata = $queryBuilderId->getEntityManager()->getMetadataFactory()->getMetadataFor($class);
257
258
        // step 2 : retrieve identifier columns
259
        $idNames = $metadata->getIdentifierFieldNames();
260
261
        // step 3 : retrieve the different subjects ids
262
        $selects = array();
263
        $idxSelect = '';
264
        foreach ($idNames as $idName) {
265
            $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...
266
            // Put the ID select on this array to use it on results QB
267
            $selects[$idName] = $select;
268
            // Use IDENTITY if id is a relation too. See: http://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html
269
            // Should work only with doctrine/orm: ~2.2
270
            $idSelect = $select;
271
            if ($metadata->hasAssociation($idName)) {
272
                $idSelect = sprintf('IDENTITY(%s) as %s', $idSelect, $idName);
273
            }
274
            $idxSelect .= ($idxSelect !== '' ? ', ' : '').$idSelect;
275
        }
276
        $queryBuilderId->resetDQLPart('select');
277
        $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...
278
279
        // for SELECT DISTINCT, ORDER BY expressions must appear in idxSelect list
280
        /* Consider
281
            SELECT DISTINCT x FROM tab ORDER BY y;
282
        For any particular x-value in the table there might be many different y
283
        values.  Which one will you use to sort that x-value in the output?
284
        */
285
        // todo : check how doctrine behave, potential SQL injection here ...
286
        if ($this->getSortBy()) {
287
            $sortBy = $this->getSortBy();
288
            if (strpos($sortBy, '.') === false) { // add the current alias
289
                $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...
290
            }
291
            $sortBy .= ' AS __order_by';
292
            $queryBuilderId->addSelect($sortBy);
293
        }
294
295
        /* For each ORDER BY clause defined directly in the DQL parts of the query,
296
           we add an entry in the SELECT clause. */
297
        $dqlParts = $queryBuilderId->getDqlParts();
298
        if ($dqlParts['orderBy'] && count($dqlParts['orderBy'])) {
299
            foreach ($dqlParts['orderBy'] as $part) {
300
                foreach ($part->getParts() as $orderBy) {
301
                    $queryBuilderId->addSelect(preg_replace("/\s+(ASC|DESC)$/i", '', $orderBy));
302
                }
303
            }
304
        }
305
306
        $results = $queryBuilderId->getQuery()->execute(array(), Query::HYDRATE_ARRAY);
307
        $idxMatrix = array();
308
        foreach ($results as $id) {
309
            foreach ($idNames as $idName) {
310
                $idxMatrix[$idName][] = $id[$idName];
311
            }
312
        }
313
314
        // step 4 : alter the query to match the targeted ids
315
        foreach ($idxMatrix as $idName => $idx) {
316
            if (count($idx) > 0) {
317
                $idxParamName = sprintf('%s_idx', $idName);
318
                $idxParamName = preg_replace('/[^\w]+/', '_', $idxParamName);
319
                $queryBuilder->andWhere(sprintf('%s IN (:%s)', $selects[$idName], $idxParamName));
320
                $queryBuilder->setParameter($idxParamName, $idx);
321
                $queryBuilder->setMaxResults(null);
322
                $queryBuilder->setFirstResult(null);
323
            }
324
        }
325
326
        return $queryBuilder;
327
    }
328
}
329