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