Passed
Push — master ( 622e79...efafc0 )
by Andreas
03:31
created

query::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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