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\Query\Expr\Join; |
||||
13 | use Doctrine\ORM\QueryBuilder; |
||||
14 | use Doctrine\ORM\Query\Expr\Composite; |
||||
15 | |||||
16 | abstract class query |
||||
17 | { |
||||
18 | protected QueryBuilder $qb; |
||||
19 | |||||
20 | protected bool $include_deleted = false; |
||||
21 | |||||
22 | protected int $parameters = 0; |
||||
23 | |||||
24 | protected string $classname; |
||||
25 | |||||
26 | protected array $groupstack = []; |
||||
27 | |||||
28 | protected array $join_tables = []; |
||||
29 | |||||
30 | 59 | public function __construct(string $class) |
|||
31 | { |
||||
32 | 59 | $this->classname = $class; |
|||
33 | 59 | $this->qb = connection::get_em()->createQueryBuilder(); |
|||
34 | 59 | $this->qb->from($class, 'c'); |
|||
35 | } |
||||
36 | |||||
37 | abstract public function execute(); |
||||
38 | |||||
39 | public function get_doctrine() : QueryBuilder |
||||
40 | { |
||||
41 | return $this->qb; |
||||
42 | } |
||||
43 | |||||
44 | 1 | public function add_constraint_with_property(string $name, string $operator, string $property) |
|||
45 | { |
||||
46 | //TODO: INTREE & IN operator functionality ? |
||||
47 | 1 | $parsed = $this->parse_constraint_name($name); |
|||
48 | 1 | $parsed_property = $this->parse_constraint_name($property); |
|||
49 | 1 | $constraint = $parsed['name'] . ' ' . $operator . ' ' . $parsed_property['name']; |
|||
50 | |||||
51 | 1 | $this->get_current_group()->add($constraint); |
|||
52 | } |
||||
53 | |||||
54 | 49 | public function add_constraint(string $name, string $operator, $value) |
|||
55 | { |
||||
56 | 49 | if ($operator === 'INTREE') { |
|||
57 | 1 | $operator = 'IN'; |
|||
58 | 1 | $targetclass = $this->classname; |
|||
59 | 1 | $fieldname = $name; |
|||
60 | |||||
61 | 1 | if (str_contains($name, '.')) { |
|||
62 | 1 | $parsed = $this->parse_constraint_name($name); |
|||
63 | 1 | $fieldname = $parsed['column']; |
|||
64 | 1 | $targetclass = $parsed['targetclass']; |
|||
65 | } |
||||
66 | |||||
67 | 1 | $mapping = connection::get_em()->getClassMetadata($targetclass)->getAssociationMapping($fieldname); |
|||
0 ignored issues
–
show
|
|||||
68 | 1 | $parentfield = $name; |
|||
69 | |||||
70 | 1 | if ($mapping['targetEntity'] !== get_class($this)) { |
|||
71 | 1 | $cm = connection::get_em()->getClassMetadata($mapping['targetEntity']); |
|||
72 | 1 | $parentfield = $cm->midgard['upfield']; |
|||
0 ignored issues
–
show
|
|||||
73 | } |
||||
74 | |||||
75 | 1 | $value = (array) $value; |
|||
76 | 1 | $value = array_merge($value, $this->get_child_ids($mapping['targetEntity'], $parentfield, $value)); |
|||
77 | 48 | } elseif (in_array($operator, ['IN', 'NOT IN'], true)) { |
|||
78 | 1 | $value = array_values($value); |
|||
79 | 47 | } elseif (!in_array($operator, ['=', '>', '<', '<>', '<=', '>=', 'LIKE', 'NOT LIKE'])) { |
|||
80 | 1 | throw new exception('Invalid operator'); |
|||
81 | } |
||||
82 | 48 | $this->parameters++; |
|||
83 | 48 | $this->get_current_group()->add($this->build_constraint($name, $operator, $value)); |
|||
84 | 47 | $this->qb->setParameter($this->parameters, $value); |
|||
85 | } |
||||
86 | |||||
87 | 4 | public function add_order(string $name, string $direction = 'ASC') : bool |
|||
88 | { |
||||
89 | 4 | if (!in_array($direction, ['ASC', 'DESC'])) { |
|||
90 | 1 | return false; |
|||
91 | } |
||||
92 | try { |
||||
93 | 4 | $parsed = $this->parse_constraint_name($name); |
|||
94 | 1 | } catch (exception) { |
|||
95 | 1 | return false; |
|||
96 | } |
||||
97 | |||||
98 | 4 | $this->qb->addOrderBy($parsed['name'], $direction); |
|||
99 | 4 | return true; |
|||
100 | } |
||||
101 | |||||
102 | 9 | public function count() : int |
|||
103 | { |
||||
104 | 9 | $select = $this->qb->getDQLPart('select'); |
|||
105 | 9 | $this->check_groups(); |
|||
106 | 9 | $this->qb->select("count(c.id)"); |
|||
107 | 9 | $this->pre_execution(); |
|||
108 | 9 | $count = (int) $this->qb->getQuery()->getSingleScalarResult(); |
|||
109 | |||||
110 | 9 | $this->post_execution(); |
|||
111 | 9 | if (empty($select)) { |
|||
112 | 9 | $this->qb->resetDQLPart('select'); |
|||
113 | } else { |
||||
114 | $this->qb->add('select', $select); |
||||
115 | } |
||||
116 | 9 | return $count; |
|||
117 | } |
||||
118 | |||||
119 | 2 | public function set_limit($limit) |
|||
120 | { |
||||
121 | 2 | $this->qb->setMaxResults($limit); |
|||
122 | } |
||||
123 | |||||
124 | 1 | public function set_offset($offset) |
|||
125 | { |
||||
126 | 1 | $this->qb->setFirstResult($offset); |
|||
127 | } |
||||
128 | |||||
129 | 12 | public function include_deleted() |
|||
130 | { |
||||
131 | 12 | $this->include_deleted = true; |
|||
132 | } |
||||
133 | |||||
134 | 51 | public function begin_group(string $operator = 'OR') : bool |
|||
135 | { |
||||
136 | 51 | if ($operator === 'OR') { |
|||
137 | 2 | $this->groupstack[] = $this->qb->expr()->orX(); |
|||
138 | 50 | } elseif ($operator === 'AND') { |
|||
139 | 49 | $this->groupstack[] = $this->qb->expr()->andX(); |
|||
140 | } else { |
||||
141 | 1 | return false; |
|||
142 | } |
||||
143 | |||||
144 | 50 | return true; |
|||
145 | } |
||||
146 | |||||
147 | 47 | public function end_group() : bool |
|||
148 | { |
||||
149 | 47 | if (empty($this->groupstack)) { |
|||
150 | 1 | return false; |
|||
151 | } |
||||
152 | 46 | $group = array_pop($this->groupstack); |
|||
153 | 46 | if ($group->count() > 0) { |
|||
154 | 45 | if (empty($this->groupstack)) { |
|||
155 | 45 | $this->qb->andWhere($group); |
|||
156 | } else { |
||||
157 | 1 | $this->get_current_group()->add($group); |
|||
158 | } |
||||
159 | } |
||||
160 | 46 | return true; |
|||
161 | } |
||||
162 | |||||
163 | 49 | public function get_current_group() : Composite |
|||
164 | { |
||||
165 | 49 | if (empty($this->groupstack)) { |
|||
166 | 49 | $this->begin_group('AND'); |
|||
167 | } |
||||
168 | |||||
169 | 49 | return $this->groupstack[(count($this->groupstack) - 1)]; |
|||
170 | } |
||||
171 | |||||
172 | 51 | protected function pre_execution() |
|||
173 | { |
||||
174 | 51 | if ($this->include_deleted) { |
|||
175 | 12 | connection::get_em()->getFilters()->disable('softdelete'); |
|||
176 | } |
||||
177 | } |
||||
178 | |||||
179 | 51 | protected function post_execution() |
|||
180 | { |
||||
181 | 51 | if ($this->include_deleted) { |
|||
182 | 12 | connection::get_em()->getFilters()->enable('softdelete'); |
|||
183 | } |
||||
184 | } |
||||
185 | |||||
186 | 1 | protected function add_collection_join(string $current_table, string $targetclass) : string |
|||
187 | { |
||||
188 | 1 | if (!isset($this->join_tables[$targetclass])) { |
|||
189 | 1 | $this->join_tables[$targetclass] = 'j' . count($this->join_tables); |
|||
190 | 1 | $c = $this->join_tables[$targetclass] . ".parentguid = " . $current_table . ".guid"; |
|||
191 | 1 | $this->qb->innerJoin(connection::get_fqcn($targetclass), $this->join_tables[$targetclass], Join::WITH, $c); |
|||
192 | } |
||||
193 | 1 | return $this->join_tables[$targetclass]; |
|||
194 | } |
||||
195 | |||||
196 | 6 | protected function add_join(string $current_table, \midgard_reflection_property $mrp, string $property) : string |
|||
197 | { |
||||
198 | 6 | $targetclass = $mrp->get_link_name($property); |
|||
199 | 6 | if (!isset($this->join_tables[$targetclass])) { |
|||
200 | 6 | $this->join_tables[$targetclass] = 'j' . count($this->join_tables); |
|||
201 | |||||
202 | // custom join |
||||
203 | 6 | if ($mrp->is_special_link($property)) { |
|||
204 | 1 | $c = $this->join_tables[$targetclass] . "." . $mrp->get_link_target($property) . " = " . $current_table . "." . $property; |
|||
205 | 1 | $this->qb->innerJoin(connection::get_fqcn($targetclass), $this->join_tables[$targetclass], Join::WITH, $c); |
|||
0 ignored issues
–
show
It seems like
$targetclass can also be of type null ; however, parameter $classname of midgard\portable\storage\connection::get_fqcn() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
206 | } else { |
||||
207 | 5 | $this->qb->leftJoin($current_table . '.' . $property, $this->join_tables[$targetclass]); |
|||
208 | } |
||||
209 | } |
||||
210 | 6 | return $this->join_tables[$targetclass]; |
|||
211 | } |
||||
212 | |||||
213 | 52 | protected function parse_constraint_name(string $name) : array |
|||
214 | { |
||||
215 | 52 | $current_table = 'c'; |
|||
216 | 52 | $targetclass = $this->classname; |
|||
217 | |||||
218 | // metadata |
||||
219 | 52 | $name = str_replace('metadata.', 'metadata_', $name); |
|||
220 | 52 | $column = $name; |
|||
221 | 52 | if (str_contains($name, ".")) { |
|||
222 | 7 | $parts = explode('.', $name); |
|||
223 | 7 | $column = array_pop($parts); |
|||
224 | 7 | foreach ($parts as $part) { |
|||
225 | 7 | if (in_array($part, ['parameter', 'attachment'], true)) { |
|||
226 | 1 | $targetclass = 'midgard_' . $part; |
|||
227 | 1 | $current_table = $this->add_collection_join($current_table, $targetclass); |
|||
228 | } else { |
||||
229 | 6 | $mrp = new \midgard_reflection_property($targetclass); |
|||
230 | |||||
231 | 6 | if ( !$mrp->is_link($part) |
|||
232 | 6 | && !$mrp->is_special_link($part)) { |
|||
233 | throw exception::invalid_property($part); |
||||
234 | } |
||||
235 | 6 | $targetclass = $mrp->get_link_name($part); |
|||
236 | 6 | $current_table = $this->add_join($current_table, $mrp, $part); |
|||
237 | } |
||||
238 | } |
||||
239 | // mrp only gives us non-namespaced classnames, so we make it an alias |
||||
240 | 7 | $targetclass = connection::get_fqcn($targetclass); |
|||
0 ignored issues
–
show
It seems like
$targetclass can also be of type null ; however, parameter $classname of midgard\portable\storage\connection::get_fqcn() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
241 | } |
||||
242 | |||||
243 | 52 | $cm = connection::get_em()->getClassMetadata($targetclass); |
|||
244 | 52 | if (isset($cm->midgard['field_aliases'][$column])) { |
|||
0 ignored issues
–
show
|
|||||
245 | 2 | $column = $cm->midgard['field_aliases'][$column]; |
|||
246 | } |
||||
247 | |||||
248 | 52 | if ( !$cm->hasField($column) |
|||
249 | 52 | && !$cm->hasAssociation($column)) { |
|||
250 | 3 | throw exception::invalid_property($column); |
|||
251 | } |
||||
252 | |||||
253 | 51 | return [ |
|||
254 | 51 | 'name' => $current_table . '.' . $column, |
|||
255 | 51 | 'column' => $column, |
|||
256 | 51 | 'targetclass' => $targetclass |
|||
257 | 51 | ]; |
|||
258 | } |
||||
259 | |||||
260 | 48 | protected function build_constraint(string $name, string $operator, $value) |
|||
261 | { |
||||
262 | 48 | $parsed = $this->parse_constraint_name($name); |
|||
263 | 47 | $expression = $operator . ' ?' . $this->parameters; |
|||
264 | |||||
265 | 47 | if (in_array($operator, ['IN', 'NOT IN'], true)) { |
|||
266 | 2 | $expression = $operator . '( ?' . $this->parameters . ')'; |
|||
267 | } |
||||
268 | |||||
269 | 47 | if ( $value === 0 |
|||
270 | 43 | || $value === null |
|||
271 | 47 | || is_array($value)) { |
|||
272 | 7 | $cm = connection::get_em()->getClassMetadata($parsed['targetclass']); |
|||
273 | 7 | if ($cm->hasAssociation($parsed['column'])) { |
|||
274 | 7 | $group = false; |
|||
275 | // TODO: there seems to be no way to make Doctrine accept default values for association fields, |
||||
276 | // so we need a silly workaround for existing DBs |
||||
277 | 7 | if (in_array($operator, ['<>', '>'], true)) { |
|||
278 | 3 | $group = $this->qb->expr()->andX(); |
|||
279 | 3 | $group->add($parsed['name'] . ' IS NOT NULL'); |
|||
280 | 6 | } elseif ($operator === 'IN') { |
|||
281 | 2 | if (array_search(0, $value) !== false) { |
|||
282 | 1 | $group = $this->qb->expr()->orX(); |
|||
283 | 2 | $group->add($parsed['name'] . ' IS NULL'); |
|||
284 | } |
||||
285 | 5 | } elseif ($operator === 'NOT IN') { |
|||
286 | 1 | if (array_search(0, $value) === false) { |
|||
287 | 1 | $group = $this->qb->expr()->orX(); |
|||
288 | 1 | $group->add($parsed['name'] . ' IS NULL'); |
|||
289 | } |
||||
290 | } else { |
||||
291 | 4 | $group = $this->qb->expr()->orX(); |
|||
292 | 4 | $group->add($parsed['name'] . ' IS NULL'); |
|||
293 | } |
||||
294 | 7 | if ($group) { |
|||
295 | 6 | $group->add($parsed['name'] . ' ' . $expression); |
|||
296 | 6 | return $group; |
|||
297 | } |
||||
298 | } |
||||
299 | } |
||||
300 | |||||
301 | 43 | return $parsed['name'] . ' ' . $expression; |
|||
302 | } |
||||
303 | |||||
304 | 51 | protected function check_groups() |
|||
305 | { |
||||
306 | 51 | while (!empty($this->groupstack)) { |
|||
307 | 45 | $this->end_group(); |
|||
308 | } |
||||
309 | } |
||||
310 | |||||
311 | 1 | private function get_child_ids(string $targetclass, string $fieldname, array $parent_values) : array |
|||
312 | { |
||||
313 | 1 | $qb = connection::get_em()->createQueryBuilder(); |
|||
314 | 1 | $qb->from($targetclass, 'c') |
|||
315 | 1 | ->where('c.' . $fieldname . ' IN (?0)') |
|||
316 | 1 | ->setParameter(0, $parent_values) |
|||
317 | 1 | ->select("c.id"); |
|||
318 | |||||
319 | 1 | $this->pre_execution(); |
|||
320 | 1 | $results = $qb->getQuery()->getScalarResult(); |
|||
321 | 1 | $this->post_execution(); |
|||
322 | |||||
323 | 1 | $ids = array_map('current', $results); |
|||
324 | 1 | if (!empty($ids)) { |
|||
325 | 1 | $ids = array_merge($ids, $this->get_child_ids($targetclass, $fieldname, $ids)); |
|||
326 | } |
||||
327 | |||||
328 | 1 | return $ids; |
|||
329 | } |
||||
330 | } |
||||
331 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.