Issues (134)

src/query.php (5 issues)

Labels
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
The method getAssociationMapping() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. Did you maybe mean getAssociationNames()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

67
            $mapping = connection::get_em()->getClassMetadata($targetclass)->/** @scrutinizer ignore-call */ getAssociationMapping($fieldname);

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.

Loading history...
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
Accessing midgard on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
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 ignore-type  annotation

205
                $this->qb->innerJoin(connection::get_fqcn(/** @scrutinizer ignore-type */ $targetclass), $this->join_tables[$targetclass], Join::WITH, $c);
Loading history...
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 ignore-type  annotation

240
            $targetclass = connection::get_fqcn(/** @scrutinizer ignore-type */ $targetclass);
Loading history...
241
        }
242
243 52
        $cm = connection::get_em()->getClassMetadata($targetclass);
244 52
        if (isset($cm->midgard['field_aliases'][$column])) {
0 ignored issues
show
Accessing midgard on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
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