Completed
Push — master ( 1af4f5...caaac4 )
by Andreas
03:43
created

query   C

Complexity

Total Complexity 64

Size/Duplication

Total Lines 349
Duplicated Lines 2.87 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 97.52%

Importance

Changes 0
Metric Value
dl 10
loc 349
ccs 197
cts 202
cp 0.9752
rs 5.8364
c 0
b 0
f 0
wmc 64
lcom 1
cbo 11

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
execute() 0 1 ?
A add_constraint_with_property() 0 11 1
C add_constraint() 0 35 7
A add_order() 0 14 3
A count() 10 10 1
A set_lang() 0 4 1
A toggle_read_only() 0 4 1
A set_limit() 0 4 1
A set_offset() 0 4 1
A include_deleted() 0 4 1
A begin_group() 0 12 3
A end_group() 0 15 4
A get_current_group() 0 8 2
A pre_execution() 0 6 2
A post_execution() 0 6 2
A add_collection_join() 0 9 2
A add_join() 0 16 3
C parse_constraint_name() 0 47 10
C build_constraint() 0 44 14
A check_groups() 0 6 2
A get_child_ids() 0 19 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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\QueryBuilder;
13
use Doctrine\ORM\Query\Expr\Join;
14
use Doctrine\ORM\Query\Expr;
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 = array();
47
48
    /**
49
     *
50
     * @var array
51
     */
52
    protected $join_tables = array();
53
54 57
    public function __construct($class)
55
    {
56 57
        $this->classname = $class;
57 57
        $this->qb = connection::get_em()->createQueryBuilder();
58 57
        $this->qb->from($class, 'c');
59 57
    }
60
61
    abstract public function execute();
62
63 2
    public function add_constraint_with_property($name, $operator, $property)
64
    {
65
        //TODO: INTREE & IN operator functionality ?
66 1
        $parsed = $this->parse_constraint_name($name);
67 1
        $parsed_property = $this->parse_constraint_name($property);
68 1
        $constraint = $parsed['name'] . ' ' . $operator . ' ' . $parsed_property['name'];
69
70 1
        $this->get_current_group()->add($constraint);
71
72 2
        return true;
73
    }
74
75 48
    public function add_constraint($name, $operator, $value)
76
    {
77 48
        if ($operator === 'INTREE') {
78 1
            $operator = 'IN';
79 1
            $targetclass = $this->classname;
80 1
            $fieldname = $name;
81
82 1
            if (strpos($name, '.') !== false) {
83 1
                $parsed = $this->parse_constraint_name($name);
84 1
                $fieldname = $parsed['column'];
85 1
                $targetclass = $parsed['targetclass'];
86 1
            }
87
88 1
            $mapping = connection::get_em()->getClassMetadata($targetclass)->getAssociationMapping($fieldname);
89 1
            $parentfield = $name;
90
91 1
            if ($mapping['targetEntity'] !== get_class($this)) {
92 1
                $cm = connection::get_em()->getClassMetadata($mapping['targetEntity']);
93 1
                $parentfield = $cm->midgard['upfield'];
0 ignored issues
show
Bug introduced by
Accessing midgard on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
94 1
            }
95
96 1
            $value = (array) $value;
97 1
            $value = array_merge($value, $this->get_child_ids($mapping['targetEntity'], $parentfield, $value));
98 1
        } elseif (   $operator === 'IN'
99 47
                 || $operator === 'NOT IN') {
100 1
            $value = array_values($value);
101 47
        } elseif (!in_array($operator, array('=', '>', '<', '<>', '<=', '>=', 'LIKE', 'NOT LIKE'))) {
102 4
            return false;
103
        }
104 47
        $this->parameters++;
105 47
        $this->get_current_group()->add($this->build_constraint($name, $operator, $value));
106 46
        $this->qb->setParameter($this->parameters, $value);
107
108 46
        return true;
109
    }
110
111 3
    public function add_order($name, $direction = 'ASC')
112
    {
113 3
        if (!in_array($direction, array('ASC', 'DESC'))) {
114 1
            return false;
115
        }
116
        try {
117 3
            $parsed = $this->parse_constraint_name($name);
118 3
        } catch (exception $e) {
119 1
            return false;
120
        }
121
122 3
        $this->qb->addOrderBy($parsed['name'], $direction);
123 3
        return true;
124
    }
125
126 9 View Code Duplication
    public function count()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
127
    {
128 9
        $this->check_groups();
129 9
        $this->qb->select("count(c.id)");
130 9
        $this->pre_execution();
131 9
        $count = intval($this->qb->getQuery()->getSingleScalarResult());
132
133 9
        $this->post_execution();
134 9
        return $count;
135
    }
136
137
    public function set_lang($language)
0 ignored issues
show
Unused Code introduced by
The parameter $language is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
138
    {
139
        throw new \Exception("Not implemented");
140
    }
141
142
    public function toggle_read_only($toggle = false)
0 ignored issues
show
Unused Code introduced by
The parameter $toggle is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
143
    {
144
        throw new \Exception("Not implemented");
145
    }
146
147 2
    public function set_limit($limit)
148
    {
149 2
        $this->qb->setMaxResults($limit);
150 2
    }
151
152 1
    public function set_offset($offset)
153
    {
154 1
        $this->qb->setFirstResult($offset);
155 1
    }
156
157 12
    public function include_deleted()
158
    {
159 12
        $this->include_deleted = true;
160 12
    }
161
162 50
    public function begin_group($operator = 'OR')
163
    {
164 50
        if ($operator === 'OR') {
165 2
            $this->groupstack[] = $this->qb->expr()->orX();
166 50
        } elseif ($operator === 'AND') {
167 48
            $this->groupstack[] = $this->qb->expr()->andX();
168 48
        } else {
169 1
            return false;
170
        }
171
172 49
        return true;
173
    }
174
175 46
    public function end_group()
176
    {
177 46
        if (empty($this->groupstack)) {
178 1
            return false;
179
        }
180 45
        $group = array_pop($this->groupstack);
181 45
        if ($group->count() > 0) {
182 44
            if (!empty($this->groupstack)) {
183 1
                $this->get_current_group()->add($group);
184 1
            } else {
185 44
                $this->qb->andWhere($group);
186
            }
187 44
        }
188 45
        return true;
189
    }
190
191
    /**
192
     *
193
     * @return Doctrine\ORM\Query\Expr:
0 ignored issues
show
Documentation introduced by
The doc-type Doctrine\ORM\Query\Expr: could not be parsed: Unknown type name "Doctrine\ORM\Query\Expr:" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
194
     */
195 48
    protected function get_current_group()
196
    {
197 48
        if (empty($this->groupstack)) {
198 48
            $this->begin_group('AND');
199 48
        }
200
201 48
        return $this->groupstack[(count($this->groupstack) - 1)];
202
    }
203
204 49
    protected function pre_execution()
205
    {
206 49
        if ($this->include_deleted) {
207 12
            connection::get_em()->getFilters()->disable('softdelete');
208 12
        }
209 49
    }
210
211 49
    protected function post_execution()
212
    {
213 49
        if ($this->include_deleted) {
214 12
            connection::get_em()->getFilters()->enable('softdelete');
215 12
        }
216 49
    }
217
218 1
    protected function add_collection_join($current_table, $targetclass)
219
    {
220 1
        if (!array_key_exists($targetclass, $this->join_tables)) {
221 1
            $this->join_tables[$targetclass] = 'j' . count($this->join_tables);
222 1
            $c = $this->join_tables[$targetclass] . ".parentguid = " . $current_table . ".guid";
223 1
            $this->qb->innerJoin("midgard:" . $targetclass, $this->join_tables[$targetclass], Join::WITH, $c);
224 1
        }
225 1
        return $this->join_tables[$targetclass];
226
    }
227
228 6
    protected function add_join($current_table, $mrp, $property)
229
    {
230 6
        $targetclass = $mrp->get_link_name($property);
231 6
        if (!array_key_exists($targetclass, $this->join_tables)) {
232 6
            $this->join_tables[$targetclass] = 'j' . count($this->join_tables);
233
234
            // custom join
235 6
            if ($mrp->is_special_link($property)) {
236 1
                $c = $this->join_tables[$targetclass] . "." . $mrp->get_link_target($property) . " = " . $current_table . "." . $property;
237 1
                $this->qb->innerJoin("midgard:" . $targetclass, $this->join_tables[$targetclass], Join::WITH, $c);
238 1
            } else {
239 5
                $this->qb->join($current_table . '.' . $property, $this->join_tables[$targetclass]);
240
            }
241 6
        }
242 6
        return $this->join_tables[$targetclass];
243
    }
244
245 50
    protected function parse_constraint_name($name)
246
    {
247 50
        $current_table = 'c';
248 50
        $targetclass = $this->classname;
249
250
        // metadata
251 50
        $name = str_replace('metadata.', 'metadata_', $name);
252 50
        $column = $name;
253 50
        if (strpos($name, ".") !== false) {
254 7
            $parts = explode('.', $name);
255 7
            $column = array_pop($parts);
256 7
            foreach ($parts as $part) {
257
                if (   $part === 'parameter'
258 7
                    || $part === 'attachment') {
259 1
                    $targetclass = 'midgard_' . $part;
260 1
                    $current_table = $this->add_collection_join($current_table, $targetclass);
261 1
                } else {
262 6
                    $mrp = new \midgard_reflection_property($targetclass);
263
264 6
                    if (   !$mrp->is_link($part)
265 6
                        && !$mrp->is_special_link($part)) {
266
                        throw exception::ok();
267
                    }
268 6
                    $targetclass = $mrp->get_link_name($part);
269 6
                    $current_table = $this->add_join($current_table, $mrp, $part);
270
                }
271 7
            }
272
            // mrp only gives us non-namespaced classnames, so we make it an alias
273 7
            $targetclass = 'midgard:' . $targetclass;
274 7
        }
275
276 50
        $cm = connection::get_em()->getClassMetadata($targetclass);
277 50
        if (array_key_exists($column, $cm->midgard['field_aliases'])) {
0 ignored issues
show
Bug introduced by
Accessing midgard on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
278 2
            $column = $cm->midgard['field_aliases'][$column];
0 ignored issues
show
Bug introduced by
Accessing midgard on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
279 2
        }
280
281 50
        if (   !$cm->hasField($column)
282 50
            && !$cm->hasAssociation($column)) {
283 3
            throw exception::ok();
284
        }
285
286
        return array(
287 49
            'name' => $current_table . '.' . $column,
288 49
            'column' => $column,
289
            'targetclass' => $targetclass
290 49
        );
291
    }
292
293 47
    protected function build_constraint($name, $operator, $value)
294
    {
295 47
        $parsed = $this->parse_constraint_name($name);
296 46
        $expression = $operator . ' ?' . $this->parameters;
297
298
        if (   $operator === 'IN'
299 46
            || $operator === 'NOT IN') {
300 2
            $expression = $operator . '( ?' . $this->parameters . ')';
301 2
        }
302
303
        if (   $value === 0
304 46
            || $value === null
305 46
            || is_array($value)) {
306 7
            $cm = connection::get_em()->getClassMetadata($parsed['targetclass']);
307 7
            if ($cm->hasAssociation($parsed['column'])) {
308 7
                $group = false;
309
                // TODO: there seems to be no way to make Doctrine accept default values for association fields,
310
                // so we need a silly workaorund for existing DBs
311 7
                if ($operator === '<>' || $operator === '>') {
312 3
                    $group = $this->qb->expr()->andX();
313 3
                    $group->add($parsed['name'] . ' IS NOT NULL');
314 7
                } elseif ($operator === 'IN') {
315 2
                    if (array_search(0, $value) !== false) {
316 1
                        $group = $this->qb->expr()->orX();
317 1
                        $group->add($parsed['name'] . ' IS NULL');
318 1
                    }
319 6
                } elseif ($operator === 'NOT IN') {
320 1
                    if (array_search(0, $value) === false) {
321 1
                        $group = $this->qb->expr()->orX();
322 1
                        $group->add($parsed['name'] . ' IS NULL');
323 1
                    }
324 1
                } else {
325 4
                    $group = $this->qb->expr()->orX();
326 4
                    $group->add($parsed['name'] . ' IS NULL');
327
                }
328 7
                if ($group) {
329 6
                    $group->add($parsed['name'] . ' ' . $expression);
330 6
                    return $group;
331
                }
332 1
            }
333 2
        }
334
335 42
        return $parsed['name'] . ' ' . $expression;
336
    }
337
338 49
    protected function check_groups()
339
    {
340 49
        while (!empty($this->groupstack)) {
341 44
            $this->end_group();
342 44
        }
343 49
    }
344
345 1
    private function get_child_ids($targetclass, $fieldname, array $parent_values)
346
    {
347 1
        $qb = connection::get_em()->createQueryBuilder();
348 1
        $qb->from($targetclass, 'c')
349 1
            ->where('c.' . $fieldname . ' IN (?0)')
350 1
            ->setParameter(0, $parent_values)
351 1
            ->select("c.id");
352
353 1
        $this->pre_execution();
354 1
        $results = $qb->getQuery()->getScalarResult();
355 1
        $this->post_execution();
356
357 1
        $ids = array_map('current', $results);
358 1
        if (!empty($ids)) {
359 1
            $ids = array_merge($ids, $this->get_child_ids($targetclass, $fieldname, $ids));
360 1
        }
361
362 1
        return $ids;
363
    }
364
}
365