Completed
Push — master ( 88cf70...c428e5 )
by Andreas
03:12
created

query::add_join()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 3
dl 0
loc 16
ccs 11
cts 11
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
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 2
    public function set_limit($limit)
138
    {
139 2
        $this->qb->setMaxResults($limit);
140 2
    }
141
142 1
    public function set_offset($offset)
143
    {
144 1
        $this->qb->setFirstResult($offset);
145 1
    }
146
147 12
    public function include_deleted()
148
    {
149 12
        $this->include_deleted = true;
150 12
    }
151
152 50
    public function begin_group($operator = 'OR')
153
    {
154 50
        if ($operator === 'OR') {
155 2
            $this->groupstack[] = $this->qb->expr()->orX();
156 50
        } elseif ($operator === 'AND') {
157 48
            $this->groupstack[] = $this->qb->expr()->andX();
158 48
        } else {
159 1
            return false;
160
        }
161
162 49
        return true;
163
    }
164
165 46
    public function end_group()
166
    {
167 46
        if (empty($this->groupstack)) {
168 1
            return false;
169
        }
170 45
        $group = array_pop($this->groupstack);
171 45
        if ($group->count() > 0) {
172 44
            if (!empty($this->groupstack)) {
173 1
                $this->get_current_group()->add($group);
174 1
            } else {
175 44
                $this->qb->andWhere($group);
176
            }
177 44
        }
178 45
        return true;
179
    }
180
181
    /**
182
     *
183
     * @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...
184
     */
185 48
    protected function get_current_group()
186
    {
187 48
        if (empty($this->groupstack)) {
188 48
            $this->begin_group('AND');
189 48
        }
190
191 48
        return $this->groupstack[(count($this->groupstack) - 1)];
192
    }
193
194 49
    protected function pre_execution()
195
    {
196 49
        if ($this->include_deleted) {
197 12
            connection::get_em()->getFilters()->disable('softdelete');
198 12
        }
199 49
    }
200
201 49
    protected function post_execution()
202
    {
203 49
        if ($this->include_deleted) {
204 12
            connection::get_em()->getFilters()->enable('softdelete');
205 12
        }
206 49
    }
207
208 1
    protected function add_collection_join($current_table, $targetclass)
209
    {
210 1
        if (!array_key_exists($targetclass, $this->join_tables)) {
211 1
            $this->join_tables[$targetclass] = 'j' . count($this->join_tables);
212 1
            $c = $this->join_tables[$targetclass] . ".parentguid = " . $current_table . ".guid";
213 1
            $this->qb->innerJoin("midgard:" . $targetclass, $this->join_tables[$targetclass], Join::WITH, $c);
214 1
        }
215 1
        return $this->join_tables[$targetclass];
216
    }
217
218 6
    protected function add_join($current_table, $mrp, $property)
219
    {
220 6
        $targetclass = $mrp->get_link_name($property);
221 6
        if (!array_key_exists($targetclass, $this->join_tables)) {
222 6
            $this->join_tables[$targetclass] = 'j' . count($this->join_tables);
223
224
            // custom join
225 6
            if ($mrp->is_special_link($property)) {
226 1
                $c = $this->join_tables[$targetclass] . "." . $mrp->get_link_target($property) . " = " . $current_table . "." . $property;
227 1
                $this->qb->innerJoin("midgard:" . $targetclass, $this->join_tables[$targetclass], Join::WITH, $c);
228 1
            } else {
229 5
                $this->qb->join($current_table . '.' . $property, $this->join_tables[$targetclass]);
230
            }
231 6
        }
232 6
        return $this->join_tables[$targetclass];
233
    }
234
235 50
    protected function parse_constraint_name($name)
236
    {
237 50
        $current_table = 'c';
238 50
        $targetclass = $this->classname;
239
240
        // metadata
241 50
        $name = str_replace('metadata.', 'metadata_', $name);
242 50
        $column = $name;
243 50
        if (strpos($name, ".") !== false) {
244 7
            $parts = explode('.', $name);
245 7
            $column = array_pop($parts);
246 7
            foreach ($parts as $part) {
247
                if (   $part === 'parameter'
248 7
                    || $part === 'attachment') {
249 1
                    $targetclass = 'midgard_' . $part;
250 1
                    $current_table = $this->add_collection_join($current_table, $targetclass);
251 1
                } else {
252 6
                    $mrp = new \midgard_reflection_property($targetclass);
253
254 6
                    if (   !$mrp->is_link($part)
255 6
                        && !$mrp->is_special_link($part)) {
256
                        throw exception::ok();
257
                    }
258 6
                    $targetclass = $mrp->get_link_name($part);
259 6
                    $current_table = $this->add_join($current_table, $mrp, $part);
260
                }
261 7
            }
262
            // mrp only gives us non-namespaced classnames, so we make it an alias
263 7
            $targetclass = 'midgard:' . $targetclass;
264 7
        }
265
266 50
        $cm = connection::get_em()->getClassMetadata($targetclass);
267 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...
268 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...
269 2
        }
270
271 50
        if (   !$cm->hasField($column)
272 50
            && !$cm->hasAssociation($column)) {
273 3
            throw exception::ok();
274
        }
275
276
        return array(
277 49
            'name' => $current_table . '.' . $column,
278 49
            'column' => $column,
279
            'targetclass' => $targetclass
280 49
        );
281
    }
282
283 47
    protected function build_constraint($name, $operator, $value)
284
    {
285 47
        $parsed = $this->parse_constraint_name($name);
286 46
        $expression = $operator . ' ?' . $this->parameters;
287
288
        if (   $operator === 'IN'
289 46
            || $operator === 'NOT IN') {
290 2
            $expression = $operator . '( ?' . $this->parameters . ')';
291 2
        }
292
293
        if (   $value === 0
294 46
            || $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 workaorund for existing DBs
301 7
                if ($operator === '<>' || $operator === '>') {
302 3
                    $group = $this->qb->expr()->andX();
303 3
                    $group->add($parsed['name'] . ' IS NOT NULL');
304 7
                } elseif ($operator === 'IN') {
305 2
                    if (array_search(0, $value) !== false) {
306 1
                        $group = $this->qb->expr()->orX();
307 1
                        $group->add($parsed['name'] . ' IS NULL');
308 1
                    }
309 6
                } 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 1
                    }
314 1
                } 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 1
            }
323 2
        }
324
325 42
        return $parsed['name'] . ' ' . $expression;
326
    }
327
328 49
    protected function check_groups()
329
    {
330 49
        while (!empty($this->groupstack)) {
331 44
            $this->end_group();
332 44
        }
333 49
    }
334
335 1
    private function get_child_ids($targetclass, $fieldname, array $parent_values)
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 1
        }
351
352 1
        return $ids;
353
    }
354
}
355