Passed
Branch master (056094)
by Andreas
04:47
created

query   F

Complexity

Total Complexity 64

Size/Duplication

Total Lines 350
Duplicated Lines 0 %

Test Coverage

Coverage 98.06%

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 350
ccs 202
cts 206
cp 0.9806
rs 3.4883
wmc 64

20 Methods

Rating   Name   Duplication   Size   Complexity  
A get_current_group() 0 7 2
A pre_execution() 0 4 2
A begin_group() 0 11 3
A add_join() 0 15 3
A set_limit() 0 3 1
C add_constraint() 0 34 7
A add_collection_join() 0 8 2
A get_child_ids() 0 18 2
A end_group() 0 14 4
A post_execution() 0 4 2
A count() 0 15 2
A include_deleted() 0 3 1
A add_order() 0 13 3
A set_offset() 0 3 1
A get_doctrine() 0 3 1
D parse_constraint_name() 0 45 10
A add_constraint_with_property() 0 10 1
C build_constraint() 0 43 14
A __construct() 0 5 1
A check_groups() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use query, and based on these observations, apply Extract Interface, too.

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