Passed
Push — master ( 0d88a1...fded30 )
by Andreas
14:17
created

query::parse_constraint_name()   B

Complexity

Conditions 9
Paths 9

Size

Total Lines 44
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 9.0033

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 29
c 1
b 0
f 0
nc 9
nop 1
dl 0
loc 44
ccs 28
cts 29
cp 0.9655
crap 9.0033
rs 8.0555
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
    /**
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 = [];
47
48
    /**
49
     *
50
     * @var array
51
     */
52
    protected $join_tables = [];
53
54 58
    public function __construct(string $class)
55
    {
56 58
        $this->classname = $class;
57 58
        $this->qb = connection::get_em()->createQueryBuilder();
58 58
        $this->qb->from($class, 'c');
59
    }
60
61
    abstract public function execute();
62
63
    public function get_doctrine() : QueryBuilder
64
    {
65
        return $this->qb;
66
    }
67
68 1
    public function add_constraint_with_property(string $name, string $operator, string $property)
69
    {
70
        //TODO: INTREE & IN operator functionality ?
71 1
        $parsed = $this->parse_constraint_name($name);
72 1
        $parsed_property = $this->parse_constraint_name($property);
73 1
        $constraint = $parsed['name'] . ' ' . $operator . ' ' . $parsed_property['name'];
74
75 1
        $this->get_current_group()->add($constraint);
76
    }
77
78 48
    public function add_constraint(string $name, string $operator, $value)
79
    {
80 48
        if ($operator === 'INTREE') {
81 1
            $operator = 'IN';
82 1
            $targetclass = $this->classname;
83 1
            $fieldname = $name;
84
85 1
            if (strpos($name, '.') !== false) {
86 1
                $parsed = $this->parse_constraint_name($name);
87 1
                $fieldname = $parsed['column'];
88 1
                $targetclass = $parsed['targetclass'];
89
            }
90
91 1
            $mapping = connection::get_em()->getClassMetadata($targetclass)->getAssociationMapping($fieldname);
0 ignored issues
show
Bug introduced by
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

91
            $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...
92 1
            $parentfield = $name;
93
94 1
            if ($mapping['targetEntity'] !== get_class($this)) {
95 1
                $cm = connection::get_em()->getClassMetadata($mapping['targetEntity']);
96 1
                $parentfield = $cm->midgard['upfield'];
0 ignored issues
show
Bug introduced by
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...
97
            }
98
99 1
            $value = (array) $value;
100 1
            $value = array_merge($value, $this->get_child_ids($mapping['targetEntity'], $parentfield, $value));
101 47
        } elseif (in_array($operator, ['IN', 'NOT IN'], true)) {
102 1
            $value = array_values($value);
103 46
        } elseif (!in_array($operator, ['=', '>', '<', '<>', '<=', '>=', 'LIKE', 'NOT LIKE'])) {
104 1
            throw new exception('Invalid operator');
105
        }
106 47
        $this->parameters++;
107 47
        $this->get_current_group()->add($this->build_constraint($name, $operator, $value));
108 46
        $this->qb->setParameter($this->parameters, $value);
109
    }
110
111 4
    public function add_order(string $name, string $direction = 'ASC') : bool
112
    {
113 4
        if (!in_array($direction, ['ASC', 'DESC'])) {
114 1
            return false;
115
        }
116
        try {
117 4
            $parsed = $this->parse_constraint_name($name);
118 1
        } catch (exception $e) {
119 1
            return false;
120
        }
121
122 4
        $this->qb->addOrderBy($parsed['name'], $direction);
123 4
        return true;
124
    }
125
126 9
    public function count() : int
127
    {
128 9
        $select = $this->qb->getDQLPart('select');
129 9
        $this->check_groups();
130 9
        $this->qb->select("count(c.id)");
131 9
        $this->pre_execution();
132 9
        $count = (int) $this->qb->getQuery()->getSingleScalarResult();
133
134 9
        $this->post_execution();
135 9
        if (empty($select)) {
136 9
            $this->qb->resetDQLPart('select');
137
        } else {
138
            $this->qb->add('select', $select);
139
        }
140 9
        return $count;
141
    }
142
143 2
    public function set_limit($limit)
144
    {
145 2
        $this->qb->setMaxResults($limit);
146
    }
147
148 1
    public function set_offset($offset)
149
    {
150 1
        $this->qb->setFirstResult($offset);
151
    }
152
153 12
    public function include_deleted()
154
    {
155 12
        $this->include_deleted = true;
156
    }
157
158 50
    public function begin_group(string $operator = 'OR') : bool
159
    {
160 50
        if ($operator === 'OR') {
161 2
            $this->groupstack[] = $this->qb->expr()->orX();
162 49
        } elseif ($operator === 'AND') {
163 48
            $this->groupstack[] = $this->qb->expr()->andX();
164
        } else {
165 1
            return false;
166
        }
167
168 49
        return true;
169
    }
170
171 46
    public function end_group() : bool
172
    {
173 46
        if (empty($this->groupstack)) {
174 1
            return false;
175
        }
176 45
        $group = array_pop($this->groupstack);
177 45
        if ($group->count() > 0) {
178 44
            if (empty($this->groupstack)) {
179 44
                $this->qb->andWhere($group);
180
            } else {
181 1
                $this->get_current_group()->add($group);
182
            }
183
        }
184 45
        return true;
185
    }
186
187 48
    public function get_current_group() : Composite
188
    {
189 48
        if (empty($this->groupstack)) {
190 48
            $this->begin_group('AND');
191
        }
192
193 48
        return $this->groupstack[(count($this->groupstack) - 1)];
194
    }
195
196 50
    protected function pre_execution()
197
    {
198 50
        if ($this->include_deleted) {
199 12
            connection::get_em()->getFilters()->disable('softdelete');
200
        }
201
    }
202
203 50
    protected function post_execution()
204
    {
205 50
        if ($this->include_deleted) {
206 12
            connection::get_em()->getFilters()->enable('softdelete');
207
        }
208
    }
209
210 1
    protected function add_collection_join(string $current_table, string $targetclass) : string
211
    {
212 1
        if (!isset($this->join_tables[$targetclass])) {
213 1
            $this->join_tables[$targetclass] = 'j' . count($this->join_tables);
214 1
            $c = $this->join_tables[$targetclass] . ".parentguid = " . $current_table . ".guid";
215 1
            $this->qb->innerJoin(connection::get_fqcn($targetclass), $this->join_tables[$targetclass], Join::WITH, $c);
216
        }
217 1
        return $this->join_tables[$targetclass];
218
    }
219
220 6
    protected function add_join(string $current_table, \midgard_reflection_property $mrp, string $property) : string
221
    {
222 6
        $targetclass = $mrp->get_link_name($property);
223 6
        if (!isset($this->join_tables[$targetclass])) {
224 6
            $this->join_tables[$targetclass] = 'j' . count($this->join_tables);
225
226
            // custom join
227 6
            if ($mrp->is_special_link($property)) {
228 1
                $c = $this->join_tables[$targetclass] . "." . $mrp->get_link_target($property) . " = " . $current_table . "." . $property;
229 1
                $this->qb->innerJoin(connection::get_fqcn($targetclass), $this->join_tables[$targetclass], Join::WITH, $c);
0 ignored issues
show
Bug introduced by
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

229
                $this->qb->innerJoin(connection::get_fqcn(/** @scrutinizer ignore-type */ $targetclass), $this->join_tables[$targetclass], Join::WITH, $c);
Loading history...
230
            } else {
231 5
                $this->qb->leftJoin($current_table . '.' . $property, $this->join_tables[$targetclass]);
232
            }
233
        }
234 6
        return $this->join_tables[$targetclass];
235
    }
236
237 51
    protected function parse_constraint_name(string $name) : array
238
    {
239 51
        $current_table = 'c';
240 51
        $targetclass = $this->classname;
241
242
        // metadata
243 51
        $name = str_replace('metadata.', 'metadata_', $name);
244 51
        $column = $name;
245 51
        if (strpos($name, ".") !== false) {
246 7
            $parts = explode('.', $name);
247 7
            $column = array_pop($parts);
248 7
            foreach ($parts as $part) {
249 7
                if (in_array($part, ['parameter', 'attachment'], true)) {
250 1
                    $targetclass = 'midgard_' . $part;
251 1
                    $current_table = $this->add_collection_join($current_table, $targetclass);
252
                } else {
253 6
                    $mrp = new \midgard_reflection_property($targetclass);
254
255 6
                    if (   !$mrp->is_link($part)
256 6
                        && !$mrp->is_special_link($part)) {
257
                        throw exception::ok();
258
                    }
259 6
                    $targetclass = $mrp->get_link_name($part);
260 6
                    $current_table = $this->add_join($current_table, $mrp, $part);
261
                }
262
            }
263
            // mrp only gives us non-namespaced classnames, so we make it an alias
264 7
            $targetclass = connection::get_fqcn($targetclass);
0 ignored issues
show
Bug introduced by
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

264
            $targetclass = connection::get_fqcn(/** @scrutinizer ignore-type */ $targetclass);
Loading history...
265
        }
266
267 51
        $cm = connection::get_em()->getClassMetadata($targetclass);
268 51
        if (isset($cm->midgard['field_aliases'][$column])) {
0 ignored issues
show
Bug introduced by
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...
269 2
            $column = $cm->midgard['field_aliases'][$column];
270
        }
271
272 51
        if (   !$cm->hasField($column)
273 51
            && !$cm->hasAssociation($column)) {
274 3
            throw exception::ok();
275
        }
276
277 50
        return [
278 50
            'name' => $current_table . '.' . $column,
279 50
            'column' => $column,
280 50
            'targetclass' => $targetclass
281 50
        ];
282
    }
283
284 47
    protected function build_constraint(string $name, string $operator, $value)
285
    {
286 47
        $parsed = $this->parse_constraint_name($name);
287 46
        $expression = $operator . ' ?' . $this->parameters;
288
289 46
        if (in_array($operator, ['IN', 'NOT IN'], true)) {
290 2
            $expression = $operator . '( ?' . $this->parameters . ')';
291
        }
292
293 46
        if (   $value === 0
294 42
            || $value === null
295 46
            || is_array($value)) {
296 7
            $cm = connection::get_em()->getClassMetadata($parsed['targetclass']);
297 7
            if ($cm->hasAssociation($parsed['column'])) {
298 7
                $group = false;
299
                // TODO: there seems to be no way to make Doctrine accept default values for association fields,
300
                // so we need a silly workaround for existing DBs
301 7
                if (in_array($operator, ['<>', '>'], true)) {
302 3
                    $group = $this->qb->expr()->andX();
303 3
                    $group->add($parsed['name'] . ' IS NOT NULL');
304 6
                } elseif ($operator === 'IN') {
305 2
                    if (array_search(0, $value) !== false) {
306 1
                        $group = $this->qb->expr()->orX();
307 2
                        $group->add($parsed['name'] . ' IS NULL');
308
                    }
309 5
                } elseif ($operator === 'NOT IN') {
310 1
                    if (array_search(0, $value) === false) {
311 1
                        $group = $this->qb->expr()->orX();
312 1
                        $group->add($parsed['name'] . ' IS NULL');
313
                    }
314
                } else {
315 4
                    $group = $this->qb->expr()->orX();
316 4
                    $group->add($parsed['name'] . ' IS NULL');
317
                }
318 7
                if ($group) {
319 6
                    $group->add($parsed['name'] . ' ' . $expression);
320 6
                    return $group;
321
                }
322
            }
323
        }
324
325 42
        return $parsed['name'] . ' ' . $expression;
326
    }
327
328 50
    protected function check_groups()
329
    {
330 50
        while (!empty($this->groupstack)) {
331 44
            $this->end_group();
332
        }
333
    }
334
335 1
    private function get_child_ids(string $targetclass, string $fieldname, array $parent_values) : array
336
    {
337 1
        $qb = connection::get_em()->createQueryBuilder();
338 1
        $qb->from($targetclass, 'c')
339 1
            ->where('c.' . $fieldname . ' IN (?0)')
340 1
            ->setParameter(0, $parent_values)
341 1
            ->select("c.id");
342
343 1
        $this->pre_execution();
344 1
        $results = $qb->getQuery()->getScalarResult();
345 1
        $this->post_execution();
346
347 1
        $ids = array_map('current', $results);
348 1
        if (!empty($ids)) {
349 1
            $ids = array_merge($ids, $this->get_child_ids($targetclass, $fieldname, $ids));
350
        }
351
352 1
        return $ids;
353
    }
354
}
355