|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* @author CONTENT CONTROL http://www.contentcontrol-berlin.de/ |
|
4
|
|
|
* @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/ |
|
5
|
|
|
* @license http://www.gnu.org/licenses/gpl.html GNU General Public License |
|
6
|
|
|
*/ |
|
7
|
|
|
|
|
8
|
|
|
namespace midgard\portable; |
|
9
|
|
|
|
|
10
|
|
|
use midgard\portable\storage\connection; |
|
11
|
|
|
use midgard\portable\api\error\exception; |
|
12
|
|
|
use Doctrine\ORM\QueryBuilder; |
|
13
|
|
|
use Doctrine\ORM\Query\Expr\Join; |
|
14
|
|
|
use Doctrine\ORM\Query\Expr; |
|
15
|
|
|
|
|
16
|
|
|
abstract class query |
|
17
|
|
|
{ |
|
18
|
|
|
/** |
|
19
|
|
|
* |
|
20
|
|
|
* @var \Doctrine\ORM\QueryBuilder |
|
21
|
|
|
*/ |
|
22
|
|
|
protected $qb; |
|
23
|
|
|
|
|
24
|
|
|
/** |
|
25
|
|
|
* |
|
26
|
|
|
* @var boolean |
|
27
|
|
|
*/ |
|
28
|
|
|
protected $include_deleted = false; |
|
29
|
|
|
|
|
30
|
|
|
/** |
|
31
|
|
|
* |
|
32
|
|
|
* @var int |
|
33
|
|
|
*/ |
|
34
|
|
|
protected $parameters = 0; |
|
35
|
|
|
|
|
36
|
|
|
/** |
|
37
|
|
|
* |
|
38
|
|
|
* @var string |
|
39
|
|
|
*/ |
|
40
|
|
|
protected $classname = null; |
|
41
|
|
|
|
|
42
|
|
|
/** |
|
43
|
|
|
* |
|
44
|
|
|
* @var array |
|
45
|
|
|
*/ |
|
46
|
|
|
protected $groupstack = array(); |
|
47
|
|
|
|
|
48
|
|
|
/** |
|
49
|
|
|
* |
|
50
|
|
|
* @var array |
|
51
|
|
|
*/ |
|
52
|
|
|
protected $join_tables = array(); |
|
53
|
|
|
|
|
54
|
57 |
|
public function __construct($class) |
|
55
|
|
|
{ |
|
56
|
57 |
|
$this->classname = $class; |
|
57
|
57 |
|
$this->qb = connection::get_em()->createQueryBuilder(); |
|
58
|
57 |
|
$this->qb->from($class, 'c'); |
|
59
|
57 |
|
} |
|
60
|
|
|
|
|
61
|
|
|
abstract public function execute(); |
|
62
|
|
|
|
|
63
|
|
|
/** |
|
64
|
|
|
* @return \Doctrine\ORM\QueryBuilder |
|
65
|
|
|
*/ |
|
66
|
|
|
public function get_doctrine() |
|
67
|
|
|
{ |
|
68
|
|
|
return $this->qb; |
|
69
|
|
|
} |
|
70
|
|
|
|
|
71
|
1 |
|
public function add_constraint_with_property($name, $operator, $property) |
|
72
|
1 |
|
{ |
|
73
|
|
|
//TODO: INTREE & IN operator functionality ? |
|
74
|
1 |
|
$parsed = $this->parse_constraint_name($name); |
|
75
|
1 |
|
$parsed_property = $this->parse_constraint_name($property); |
|
76
|
1 |
|
$constraint = $parsed['name'] . ' ' . $operator . ' ' . $parsed_property['name']; |
|
77
|
|
|
|
|
78
|
1 |
|
$this->get_current_group()->add($constraint); |
|
79
|
|
|
|
|
80
|
1 |
|
return true; |
|
81
|
|
|
} |
|
82
|
|
|
|
|
83
|
48 |
|
public function add_constraint($name, $operator, $value) |
|
84
|
|
|
{ |
|
85
|
48 |
|
if ($operator === 'INTREE') { |
|
86
|
1 |
|
$operator = 'IN'; |
|
87
|
1 |
|
$targetclass = $this->classname; |
|
88
|
1 |
|
$fieldname = $name; |
|
89
|
|
|
|
|
90
|
1 |
|
if (strpos($name, '.') !== false) { |
|
91
|
1 |
|
$parsed = $this->parse_constraint_name($name); |
|
92
|
1 |
|
$fieldname = $parsed['column']; |
|
93
|
1 |
|
$targetclass = $parsed['targetclass']; |
|
94
|
1 |
|
} |
|
95
|
|
|
|
|
96
|
1 |
|
$mapping = connection::get_em()->getClassMetadata($targetclass)->getAssociationMapping($fieldname); |
|
97
|
1 |
|
$parentfield = $name; |
|
98
|
|
|
|
|
99
|
1 |
|
if ($mapping['targetEntity'] !== get_class($this)) { |
|
100
|
1 |
|
$cm = connection::get_em()->getClassMetadata($mapping['targetEntity']); |
|
101
|
1 |
|
$parentfield = $cm->midgard['upfield']; |
|
|
|
|
|
|
102
|
1 |
|
} |
|
103
|
|
|
|
|
104
|
1 |
|
$value = (array) $value; |
|
105
|
1 |
|
$value = array_merge($value, $this->get_child_ids($mapping['targetEntity'], $parentfield, $value)); |
|
106
|
1 |
|
} elseif ( $operator === 'IN' |
|
107
|
47 |
|
|| $operator === 'NOT IN') { |
|
108
|
1 |
|
$value = array_values($value); |
|
109
|
47 |
|
} elseif (!in_array($operator, array('=', '>', '<', '<>', '<=', '>=', 'LIKE', 'NOT LIKE'))) { |
|
110
|
4 |
|
return false; |
|
111
|
|
|
} |
|
112
|
47 |
|
$this->parameters++; |
|
113
|
47 |
|
$this->get_current_group()->add($this->build_constraint($name, $operator, $value)); |
|
114
|
46 |
|
$this->qb->setParameter($this->parameters, $value); |
|
115
|
|
|
|
|
116
|
46 |
|
return true; |
|
117
|
|
|
} |
|
118
|
|
|
|
|
119
|
3 |
|
public function add_order($name, $direction = 'ASC') |
|
120
|
|
|
{ |
|
121
|
3 |
|
if (!in_array($direction, array('ASC', 'DESC'))) { |
|
122
|
1 |
|
return false; |
|
123
|
|
|
} |
|
124
|
|
|
try { |
|
125
|
3 |
|
$parsed = $this->parse_constraint_name($name); |
|
126
|
3 |
|
} catch (exception $e) { |
|
127
|
1 |
|
return false; |
|
128
|
|
|
} |
|
129
|
|
|
|
|
130
|
3 |
|
$this->qb->addOrderBy($parsed['name'], $direction); |
|
131
|
3 |
|
return true; |
|
132
|
|
|
} |
|
133
|
|
|
|
|
134
|
9 |
|
public function count() |
|
135
|
|
|
{ |
|
136
|
9 |
|
$select = $this->qb->getDQLPart('select'); |
|
137
|
9 |
|
$this->check_groups(); |
|
138
|
9 |
|
$this->qb->select("count(c.id)"); |
|
139
|
9 |
|
$this->pre_execution(); |
|
140
|
9 |
|
$count = intval($this->qb->getQuery()->getSingleScalarResult()); |
|
141
|
|
|
|
|
142
|
9 |
|
$this->post_execution(); |
|
143
|
9 |
|
if (empty($select)) { |
|
144
|
9 |
|
$this->qb->resetDQLPart('select'); |
|
145
|
9 |
|
} else { |
|
146
|
|
|
$this->qb->add('select', $select); |
|
147
|
|
|
} |
|
148
|
9 |
|
return $count; |
|
149
|
|
|
} |
|
150
|
|
|
|
|
151
|
2 |
|
public function set_limit($limit) |
|
152
|
|
|
{ |
|
153
|
2 |
|
$this->qb->setMaxResults($limit); |
|
154
|
2 |
|
} |
|
155
|
|
|
|
|
156
|
1 |
|
public function set_offset($offset) |
|
157
|
|
|
{ |
|
158
|
1 |
|
$this->qb->setFirstResult($offset); |
|
159
|
1 |
|
} |
|
160
|
|
|
|
|
161
|
12 |
|
public function include_deleted() |
|
162
|
|
|
{ |
|
163
|
12 |
|
$this->include_deleted = true; |
|
164
|
12 |
|
} |
|
165
|
|
|
|
|
166
|
50 |
|
public function begin_group($operator = 'OR') |
|
167
|
|
|
{ |
|
168
|
50 |
|
if ($operator === 'OR') { |
|
169
|
2 |
|
$this->groupstack[] = $this->qb->expr()->orX(); |
|
170
|
50 |
|
} elseif ($operator === 'AND') { |
|
171
|
48 |
|
$this->groupstack[] = $this->qb->expr()->andX(); |
|
172
|
48 |
|
} else { |
|
173
|
1 |
|
return false; |
|
174
|
|
|
} |
|
175
|
|
|
|
|
176
|
49 |
|
return true; |
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
46 |
|
public function end_group() |
|
180
|
|
|
{ |
|
181
|
46 |
|
if (empty($this->groupstack)) { |
|
182
|
1 |
|
return false; |
|
183
|
|
|
} |
|
184
|
45 |
|
$group = array_pop($this->groupstack); |
|
185
|
45 |
|
if ($group->count() > 0) { |
|
186
|
44 |
|
if (!empty($this->groupstack)) { |
|
187
|
1 |
|
$this->get_current_group()->add($group); |
|
188
|
1 |
|
} else { |
|
189
|
44 |
|
$this->qb->andWhere($group); |
|
190
|
|
|
} |
|
191
|
44 |
|
} |
|
192
|
45 |
|
return true; |
|
193
|
|
|
} |
|
194
|
|
|
|
|
195
|
|
|
/** |
|
196
|
|
|
* @return Doctrine\ORM\Query\Expr: |
|
|
|
|
|
|
197
|
|
|
*/ |
|
198
|
48 |
|
public function get_current_group() |
|
199
|
|
|
{ |
|
200
|
48 |
|
if (empty($this->groupstack)) { |
|
201
|
48 |
|
$this->begin_group('AND'); |
|
202
|
48 |
|
} |
|
203
|
|
|
|
|
204
|
48 |
|
return $this->groupstack[(count($this->groupstack) - 1)]; |
|
205
|
|
|
} |
|
206
|
|
|
|
|
207
|
49 |
|
protected function pre_execution() |
|
208
|
|
|
{ |
|
209
|
49 |
|
if ($this->include_deleted) { |
|
210
|
12 |
|
connection::get_em()->getFilters()->disable('softdelete'); |
|
211
|
12 |
|
} |
|
212
|
49 |
|
} |
|
213
|
|
|
|
|
214
|
49 |
|
protected function post_execution() |
|
215
|
|
|
{ |
|
216
|
49 |
|
if ($this->include_deleted) { |
|
217
|
12 |
|
connection::get_em()->getFilters()->enable('softdelete'); |
|
218
|
12 |
|
} |
|
219
|
49 |
|
} |
|
220
|
|
|
|
|
221
|
1 |
|
protected function add_collection_join($current_table, $targetclass) |
|
222
|
|
|
{ |
|
223
|
1 |
|
if (!array_key_exists($targetclass, $this->join_tables)) { |
|
224
|
1 |
|
$this->join_tables[$targetclass] = 'j' . count($this->join_tables); |
|
225
|
1 |
|
$c = $this->join_tables[$targetclass] . ".parentguid = " . $current_table . ".guid"; |
|
226
|
1 |
|
$this->qb->innerJoin("midgard:" . $targetclass, $this->join_tables[$targetclass], Join::WITH, $c); |
|
227
|
1 |
|
} |
|
228
|
1 |
|
return $this->join_tables[$targetclass]; |
|
229
|
|
|
} |
|
230
|
|
|
|
|
231
|
6 |
|
protected function add_join($current_table, $mrp, $property) |
|
232
|
|
|
{ |
|
233
|
6 |
|
$targetclass = $mrp->get_link_name($property); |
|
234
|
6 |
|
if (!array_key_exists($targetclass, $this->join_tables)) { |
|
235
|
6 |
|
$this->join_tables[$targetclass] = 'j' . count($this->join_tables); |
|
236
|
|
|
|
|
237
|
|
|
// custom join |
|
238
|
6 |
|
if ($mrp->is_special_link($property)) { |
|
239
|
1 |
|
$c = $this->join_tables[$targetclass] . "." . $mrp->get_link_target($property) . " = " . $current_table . "." . $property; |
|
240
|
1 |
|
$this->qb->innerJoin("midgard:" . $targetclass, $this->join_tables[$targetclass], Join::WITH, $c); |
|
241
|
1 |
|
} else { |
|
242
|
5 |
|
$this->qb->join($current_table . '.' . $property, $this->join_tables[$targetclass]); |
|
243
|
|
|
} |
|
244
|
6 |
|
} |
|
245
|
6 |
|
return $this->join_tables[$targetclass]; |
|
246
|
|
|
} |
|
247
|
|
|
|
|
248
|
50 |
|
protected function parse_constraint_name($name) |
|
249
|
|
|
{ |
|
250
|
50 |
|
$current_table = 'c'; |
|
251
|
50 |
|
$targetclass = $this->classname; |
|
252
|
|
|
|
|
253
|
|
|
// metadata |
|
254
|
50 |
|
$name = str_replace('metadata.', 'metadata_', $name); |
|
255
|
50 |
|
$column = $name; |
|
256
|
50 |
|
if (strpos($name, ".") !== false) { |
|
257
|
7 |
|
$parts = explode('.', $name); |
|
258
|
7 |
|
$column = array_pop($parts); |
|
259
|
7 |
|
foreach ($parts as $part) { |
|
260
|
|
|
if ( $part === 'parameter' |
|
261
|
7 |
|
|| $part === 'attachment') { |
|
262
|
1 |
|
$targetclass = 'midgard_' . $part; |
|
263
|
1 |
|
$current_table = $this->add_collection_join($current_table, $targetclass); |
|
264
|
1 |
|
} else { |
|
265
|
6 |
|
$mrp = new \midgard_reflection_property($targetclass); |
|
266
|
|
|
|
|
267
|
6 |
|
if ( !$mrp->is_link($part) |
|
268
|
6 |
|
&& !$mrp->is_special_link($part)) { |
|
269
|
|
|
throw exception::ok(); |
|
270
|
|
|
} |
|
271
|
6 |
|
$targetclass = $mrp->get_link_name($part); |
|
272
|
6 |
|
$current_table = $this->add_join($current_table, $mrp, $part); |
|
273
|
|
|
} |
|
274
|
7 |
|
} |
|
275
|
|
|
// mrp only gives us non-namespaced classnames, so we make it an alias |
|
276
|
7 |
|
$targetclass = 'midgard:' . $targetclass; |
|
277
|
7 |
|
} |
|
278
|
|
|
|
|
279
|
50 |
|
$cm = connection::get_em()->getClassMetadata($targetclass); |
|
280
|
50 |
|
if (array_key_exists($column, $cm->midgard['field_aliases'])) { |
|
281
|
2 |
|
$column = $cm->midgard['field_aliases'][$column]; |
|
|
|
|
|
|
282
|
2 |
|
} |
|
283
|
|
|
|
|
284
|
50 |
|
if ( !$cm->hasField($column) |
|
285
|
50 |
|
&& !$cm->hasAssociation($column)) { |
|
286
|
3 |
|
throw exception::ok(); |
|
287
|
|
|
} |
|
288
|
|
|
|
|
289
|
|
|
return array( |
|
290
|
49 |
|
'name' => $current_table . '.' . $column, |
|
291
|
49 |
|
'column' => $column, |
|
292
|
|
|
'targetclass' => $targetclass |
|
293
|
49 |
|
); |
|
294
|
|
|
} |
|
295
|
|
|
|
|
296
|
47 |
|
protected function build_constraint($name, $operator, $value) |
|
297
|
|
|
{ |
|
298
|
47 |
|
$parsed = $this->parse_constraint_name($name); |
|
299
|
46 |
|
$expression = $operator . ' ?' . $this->parameters; |
|
300
|
|
|
|
|
301
|
|
|
if ( $operator === 'IN' |
|
302
|
46 |
|
|| $operator === 'NOT IN') { |
|
303
|
2 |
|
$expression = $operator . '( ?' . $this->parameters . ')'; |
|
304
|
2 |
|
} |
|
305
|
|
|
|
|
306
|
|
|
if ( $value === 0 |
|
307
|
46 |
|
|| $value === null |
|
308
|
46 |
|
|| is_array($value)) { |
|
309
|
7 |
|
$cm = connection::get_em()->getClassMetadata($parsed['targetclass']); |
|
310
|
7 |
|
if ($cm->hasAssociation($parsed['column'])) { |
|
311
|
7 |
|
$group = false; |
|
312
|
|
|
// TODO: there seems to be no way to make Doctrine accept default values for association fields, |
|
313
|
|
|
// so we need a silly workaorund for existing DBs |
|
314
|
7 |
|
if ($operator === '<>' || $operator === '>') { |
|
315
|
3 |
|
$group = $this->qb->expr()->andX(); |
|
316
|
3 |
|
$group->add($parsed['name'] . ' IS NOT NULL'); |
|
317
|
7 |
|
} elseif ($operator === 'IN') { |
|
318
|
2 |
|
if (array_search(0, $value) !== false) { |
|
319
|
1 |
|
$group = $this->qb->expr()->orX(); |
|
320
|
1 |
|
$group->add($parsed['name'] . ' IS NULL'); |
|
321
|
1 |
|
} |
|
322
|
6 |
|
} elseif ($operator === 'NOT IN') { |
|
323
|
1 |
|
if (array_search(0, $value) === false) { |
|
324
|
1 |
|
$group = $this->qb->expr()->orX(); |
|
325
|
1 |
|
$group->add($parsed['name'] . ' IS NULL'); |
|
326
|
1 |
|
} |
|
327
|
1 |
|
} else { |
|
328
|
4 |
|
$group = $this->qb->expr()->orX(); |
|
329
|
4 |
|
$group->add($parsed['name'] . ' IS NULL'); |
|
330
|
|
|
} |
|
331
|
7 |
|
if ($group) { |
|
332
|
6 |
|
$group->add($parsed['name'] . ' ' . $expression); |
|
333
|
6 |
|
return $group; |
|
334
|
1 |
|
} |
|
335
|
1 |
|
} |
|
336
|
2 |
|
} |
|
337
|
|
|
|
|
338
|
42 |
|
return $parsed['name'] . ' ' . $expression; |
|
339
|
|
|
} |
|
340
|
|
|
|
|
341
|
49 |
|
protected function check_groups() |
|
342
|
|
|
{ |
|
343
|
49 |
|
while (!empty($this->groupstack)) { |
|
344
|
44 |
|
$this->end_group(); |
|
345
|
44 |
|
} |
|
346
|
49 |
|
} |
|
347
|
|
|
|
|
348
|
1 |
|
private function get_child_ids($targetclass, $fieldname, array $parent_values) |
|
349
|
|
|
{ |
|
350
|
1 |
|
$qb = connection::get_em()->createQueryBuilder(); |
|
351
|
1 |
|
$qb->from($targetclass, 'c') |
|
352
|
1 |
|
->where('c.' . $fieldname . ' IN (?0)') |
|
353
|
1 |
|
->setParameter(0, $parent_values) |
|
354
|
1 |
|
->select("c.id"); |
|
355
|
|
|
|
|
356
|
1 |
|
$this->pre_execution(); |
|
357
|
1 |
|
$results = $qb->getQuery()->getScalarResult(); |
|
358
|
1 |
|
$this->post_execution(); |
|
359
|
|
|
|
|
360
|
1 |
|
$ids = array_map('current', $results); |
|
361
|
1 |
|
if (!empty($ids)) { |
|
362
|
1 |
|
$ids = array_merge($ids, $this->get_child_ids($targetclass, $fieldname, $ids)); |
|
363
|
1 |
|
} |
|
364
|
|
|
|
|
365
|
1 |
|
return $ids; |
|
366
|
|
|
} |
|
367
|
|
|
} |
|
368
|
|
|
|
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.