1
|
|
|
<?php declare(strict_types = 1); |
2
|
|
|
|
3
|
|
|
namespace Artprima\QueryFilterBundle\Query; |
4
|
|
|
|
5
|
|
|
use Artprima\QueryFilterBundle\Exception\InvalidArgumentException; |
6
|
|
|
use Artprima\QueryFilterBundle\Exception\MissingArgumentException; |
7
|
|
|
use Artprima\QueryFilterBundle\Query\Condition; |
8
|
|
|
use Artprima\QueryFilterBundle\Query\Condition\ConditionInterface; |
9
|
|
|
use Artprima\QueryFilterBundle\Query\Mysql\PaginationWalker; |
10
|
|
|
use Doctrine\ORM\QueryBuilder; |
11
|
|
|
use Doctrine\ORM\Query as DoctrineQuery; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Class ProxyQueryBuilder |
15
|
|
|
* |
16
|
|
|
* @author Denis Voytyuk <[email protected]> |
17
|
|
|
*/ |
18
|
|
|
class ProxyQueryBuilder |
19
|
|
|
{ |
20
|
|
|
/** |
21
|
|
|
* @var QueryBuilder |
22
|
|
|
*/ |
23
|
|
|
private $queryBuilder; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var bool |
27
|
|
|
*/ |
28
|
|
|
private $calcRows; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var ConditionInterface[] |
32
|
|
|
*/ |
33
|
|
|
private $conditions = []; |
34
|
|
|
|
35
|
|
|
public function __construct(QueryBuilder $queryBuilder, $calcRows = true) |
36
|
|
|
{ |
37
|
|
|
$this->queryBuilder = $queryBuilder; |
38
|
|
|
$this->calcRows = $calcRows; |
39
|
|
|
|
40
|
|
|
// this way of registering does not seem to be too smart, but for now it can work |
41
|
|
|
$this->registerCondition(new Condition\Between()); |
42
|
|
|
$this->registerCondition(new Condition\Eq()); |
43
|
|
|
$this->registerCondition(new Condition\Gt()); |
44
|
|
|
$this->registerCondition(new Condition\Gte()); |
45
|
|
|
$this->registerCondition(new Condition\In()); |
46
|
|
|
$this->registerCondition(new Condition\IsNotNull()); |
47
|
|
|
$this->registerCondition(new Condition\IsNull()); |
48
|
|
|
$this->registerCondition(new Condition\Like()); |
49
|
|
|
$this->registerCondition(new Condition\Lt()); |
50
|
|
|
$this->registerCondition(new Condition\Lte()); |
51
|
|
|
$this->registerCondition(new Condition\MemberOf()); |
52
|
|
|
$this->registerCondition(new Condition\NotBetween()); |
53
|
|
|
$this->registerCondition(new Condition\NotEq()); |
54
|
|
|
$this->registerCondition(new Condition\NotIn()); |
55
|
|
|
$this->registerCondition(new Condition\NotLike()); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @param int $index parameter id |
60
|
|
|
* @param string $field field name |
61
|
|
|
* @param string $conditionName condition type (eq, like, etc.) |
62
|
|
|
* @param array $val condition parameters information |
63
|
|
|
* @return DoctrineQuery\Expr\Comparison|DoctrineQuery\Expr\Func|string |
64
|
|
|
* @throws InvalidArgumentException |
65
|
|
|
*/ |
66
|
|
|
private function getConditionExpr(int $index, string $field, string $conditionName, array $val) |
67
|
|
|
{ |
68
|
|
|
if (!array_key_exists($conditionName, $this->conditions)) { |
69
|
|
|
throw new InvalidArgumentException(sprintf('Condition "%s" is not registered', $conditionName)); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
$expr = $this->conditions[$conditionName]->getExpr($this->queryBuilder, $field, $index, $val); |
73
|
|
|
|
74
|
|
|
return $expr; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Get neighbor (prev or next) record id for use in navigation |
79
|
|
|
* |
80
|
|
|
* @param int $id record id |
81
|
|
|
* @param boolean $prev if true - get prev id, otherwise - next id |
82
|
|
|
* @param DoctrineQuery\Expr|null $extraAndWhereCondition |
83
|
|
|
* @return int|null neighbor id or null if empty result |
84
|
|
|
* @throws \RuntimeException |
85
|
|
|
* @throws \Doctrine\ORM\NonUniqueResultException |
86
|
|
|
*/ |
87
|
|
|
public function getNeighborRecordId(int $id, bool $prev, ?DoctrineQuery\Expr $extraAndWhereCondition = null): ?int |
88
|
|
|
{ |
89
|
|
|
$sign = $prev ? '<' : '>'; |
90
|
|
|
$order = $prev ? 'DESC' : 'ASC'; |
91
|
|
|
$rootEntities = $this->queryBuilder->getRootEntities(); |
92
|
|
|
|
93
|
|
|
if (count($rootEntities) >= 0) { |
94
|
|
|
throw new \RuntimeException('QueryBuilder must contain exactly one root entity'); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
$rootEntity = reset($rootEntities); |
98
|
|
|
$qb = new QueryBuilder($this->queryBuilder->getEntityManager()); |
99
|
|
|
$qb |
100
|
|
|
->select('c.id') // assuming that the entities index must be always called `id` |
101
|
|
|
->from($rootEntity, 'c') |
102
|
|
|
->where('c.id '.$sign.' :id') |
103
|
|
|
->setParameter(':id', $id) |
104
|
|
|
->orderBy('c.id', $order) |
105
|
|
|
; |
106
|
|
|
|
107
|
|
|
if ($extraAndWhereCondition !== null) { |
108
|
|
|
$qb->andWhere($extraAndWhereCondition); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
$query = $qb->getQuery(); |
112
|
|
|
$query->setMaxResults(1); |
113
|
|
|
$result = $query->getOneOrNullResult(); |
114
|
|
|
|
115
|
|
|
return $result; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* Get prev and next record ids for the given record id |
120
|
|
|
* |
121
|
|
|
* @param int $id record id |
122
|
|
|
* @return array prev and next records id in an array with 'prev' and 'next' keys. One or both items can be null in case of no records. |
123
|
|
|
* @throws \RuntimeException |
124
|
|
|
* @throws \Doctrine\ORM\NonUniqueResultException |
125
|
|
|
*/ |
126
|
|
|
public function getNeighborRecordIds(int $id): array |
127
|
|
|
{ |
128
|
|
|
$prev = $this->getNeighborRecordId($id, true); |
129
|
|
|
$next = $this->getNeighborRecordId($id, false); |
130
|
|
|
|
131
|
|
|
return compact('prev', 'next'); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Get connector expression based on `and`, `or` or `null` |
136
|
|
|
* |
137
|
|
|
* @param $prev |
138
|
|
|
* @param $connector |
139
|
|
|
* @param $condition |
140
|
|
|
* @return DoctrineQuery\Expr\Andx|DoctrineQuery\Expr\Orx |
141
|
|
|
* @throws InvalidArgumentException |
142
|
|
|
*/ |
143
|
|
|
private function getConnectorExpr($prev, $connector, $condition) |
144
|
|
|
{ |
145
|
|
|
$qb = $this->queryBuilder; |
146
|
|
|
|
147
|
|
|
if ($prev === null) { |
148
|
|
|
$expr = $condition; |
149
|
|
|
} elseif ($connector === null || $connector === 'and') { |
150
|
|
|
$expr = $qb->expr()->andX($prev, $condition); |
151
|
|
|
} elseif ($connector === 'or') { |
152
|
|
|
$expr = $qb->expr()->orX($prev, $condition); |
153
|
|
|
} else { |
154
|
|
|
throw new InvalidArgumentException(sprintf('Wrong connector type: %s', $connector)); |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
return $expr; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
public function registerCondition(ConditionInterface $condition) |
161
|
|
|
{ |
162
|
|
|
$this->conditions[$condition->getName()] = $condition; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Add filter and order by conditions to the given QueryBuilder |
167
|
|
|
* |
168
|
|
|
* Example data |
169
|
|
|
* |
170
|
|
|
* array( |
171
|
|
|
* 'searchBy' => array( |
172
|
|
|
* 'e.name' => array( |
173
|
|
|
* 'type' => 'like', |
174
|
|
|
* 'val' => 'a', |
175
|
|
|
* ), |
176
|
|
|
* 'e.city' => array( |
177
|
|
|
* 'type' => 'like', |
178
|
|
|
* 'val' => 'd', |
179
|
|
|
* ), |
180
|
|
|
* 'c.name' => array( |
181
|
|
|
* 'type' => 'like', |
182
|
|
|
* 'val' => 'a', |
183
|
|
|
* ), |
184
|
|
|
* 'concat(concat(concat(concat(p.firstname, ' '), p.middlename), ' '), p.lastname)' => array( |
185
|
|
|
* 'having' => TRUE |
186
|
|
|
* 'type' => 'like' |
187
|
|
|
* 'val' => 'a' |
188
|
|
|
* ) |
189
|
|
|
* 'year' => array( |
190
|
|
|
* 'type' => 'between', |
191
|
|
|
* 'val' => 2015, |
192
|
|
|
* 'x' => 'YEAR(e.startDate)', |
193
|
|
|
* 'y' => 'YEAR(e.endDate)' |
194
|
|
|
* ), |
195
|
|
|
* ), |
196
|
|
|
* 'sortData' => array( |
197
|
|
|
* 'e.name' => 'asc' |
198
|
|
|
* ) |
199
|
|
|
* ) |
200
|
|
|
* |
201
|
|
|
* @param array $by |
202
|
|
|
* @param array $orderBy |
203
|
|
|
* |
204
|
|
|
* @throws MissingArgumentException |
205
|
|
|
* @return DoctrineQuery |
206
|
|
|
* @throws InvalidArgumentException |
207
|
|
|
*/ |
208
|
|
|
public function getSortedAndFilteredQuery(array $by, array $orderBy): DoctrineQuery |
209
|
|
|
{ |
210
|
|
|
$qb = $this->queryBuilder; |
211
|
|
|
|
212
|
|
|
if (!empty($orderBy)) { |
213
|
|
|
foreach ($orderBy as $field => $dir) { |
214
|
|
|
$qb->addOrderBy($field, strtoupper($dir)); |
215
|
|
|
} |
216
|
|
|
} |
217
|
|
|
if (!empty($by)) { |
218
|
|
|
$i = 0; |
219
|
|
|
$where = null; |
220
|
|
|
$having = null; |
221
|
|
|
foreach ($by as $key => $val) { |
222
|
|
|
$i++; |
223
|
|
|
if (is_scalar($val)) { |
224
|
|
|
$where = $this->getConnectorExpr($where, 'and', $qb->expr()->eq($key, '?'.$i)); |
225
|
|
|
$qb->setParameter($i, $val); |
226
|
|
|
// @todo: the following smells bad |
227
|
|
|
//} elseif (is_callable($val)){ |
|
|
|
|
228
|
|
|
// call_user_func_array($val, array(&$where, &$having, &$qb)); |
|
|
|
|
229
|
|
|
} elseif (is_array($val)) { |
230
|
|
|
if (!array_key_exists('x', $val) && !array_key_exists('y', $val)) { |
231
|
|
|
if (!array_key_exists('val', $val)) { |
232
|
|
|
throw new MissingArgumentException('Required "val" argument not given'); |
233
|
|
|
} |
234
|
|
|
if (!is_scalar($val['val'])) { |
235
|
|
|
throw new InvalidArgumentException(sprintf('Unexpected val php type ("%s")', gettype($val['val']))); |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
$condition = $this->getConditionExpr($i, $key, $val['type'], $val); |
240
|
|
|
|
241
|
|
|
if (empty($val['having'])) { |
242
|
|
|
$where = $this->getConnectorExpr($where, $val['connector'] ?? 'and', $condition); |
243
|
|
|
} else { |
244
|
|
|
$having = $this->getConnectorExpr($having, $val['connector'] ?? 'and', $condition); |
245
|
|
|
} |
246
|
|
|
} else { |
247
|
|
|
throw new InvalidArgumentException(sprintf('Unexpected val php type ("%s")', gettype($val))); |
248
|
|
|
} |
249
|
|
|
} |
250
|
|
|
if ($where) { |
251
|
|
|
$qb->add('where', $where); |
252
|
|
|
} |
253
|
|
|
if ($having) { |
254
|
|
|
$qb->add('having', $having); |
255
|
|
|
} |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
$query = $qb->getQuery(); |
259
|
|
|
|
260
|
|
|
if ($this->calcRows) { |
261
|
|
|
$query->setHint(DoctrineQuery::HINT_CUSTOM_OUTPUT_WALKER, PaginationWalker::class); |
262
|
|
|
$query->setHint('mysqlWalker.sqlCalcFoundRows', true); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
return $query; |
266
|
|
|
} |
267
|
|
|
} |
268
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.