FilterQuery   F
last analyzed

Complexity

Total Complexity 81

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 175
dl 0
loc 372
ccs 0
cts 173
cp 0
rs 2
c 3
b 0
f 0
wmc 81

23 Methods

Rating   Name   Duplication   Size   Complexity  
A hasParameter() 0 7 3
A __construct() 0 11 2
A getAlias() 0 3 1
A setHydrate() 0 3 1
A getParameter() 0 10 4
A createJoins() 0 8 2
A getValue() 0 17 4
C removeWhere() 0 22 14
A getWhere() 0 3 1
A getQueryBuilder() 0 3 1
A or() 0 4 1
A getOrderBy() 0 3 1
A getParameterName() 0 3 1
B removeOrderBy() 0 16 8
A setPaginated() 0 3 1
C getOperator() 0 31 13
A createJoinAliases() 0 7 2
A getHydrate() 0 3 1
A isPaginated() 0 3 1
D build() 0 55 12
A addWhere() 0 15 3
A addCallback() 0 5 1
A addOrderBy() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like FilterQuery 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 FilterQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * FilterQuery.php
4
 *
5
 * @since 19/01/17
6
 * @author gseidel
7
 */
8
9
namespace Enhavo\Bundle\AppBundle\Filter;
10
11
12
use Doctrine\ORM\EntityManagerInterface;
13
use Doctrine\ORM\QueryBuilder;
14
use Enhavo\Bundle\AppBundle\Exception\FilterException;
15
16
class FilterQuery
17
{
18
    const OPERATOR_EQUALS = '=';
19
    const OPERATOR_GREATER = '>';
20
    const OPERATOR_LESS = '<';
21
    const OPERATOR_GREATER_EQUAL = '>=';
22
    const OPERATOR_LESS_EQUAL = '<=';
23
    const OPERATOR_NOT = '!=';
24
    const OPERATOR_LIKE = 'like';
25
    const OPERATOR_START_LIKE = 'start_like';
26
    const OPERATOR_END_LIKE = 'end_like';
27
    const OPERATOR_IN = 'in';
28
29
    const ORDER_ASC = 'asc';
30
    const ORDER_DESC = 'desc';
31
32
    const HYDRATE_OBJECT = 'object';
33
    const HYDRATE_ID = 'id';
34
35
    const UNIQUE_PARAMETER_PREFIX = 'qohbmrxo'; // This is a random sequence of 8 characters generated by a password generator.
36
                                                // It's added to join aliases and parameter names to avoid easily guessable names
37
                                                // that might accidentally be duplicated by someone in the callback function.
38
39
    /**
40
     * @var array
41
     */
42
    private $where = [];
43
44
    /**
45
     * @var FilterQueryOr[]
46
     */
47
    private $orBlocks = [];
48
49
    /**
50
     * @var array
51
     */
52
    private $orderBy = [];
53
54
    /**
55
     * @var array
56
     */
57
    private $callbacks = [];
58
59
    /**
60
     * @var QueryBuilder
61
     */
62
    private $queryBuilder;
63
64
    /**
65
     * @var string
66
     */
67
    private $alias;
68
69
    /**
70
     * @var string
71
     */
72
    private $hydrate;
73
74
    /**
75
     * @var bool
76
     */
77
    private $paginated = true;
78
79
    public function __construct(EntityManagerInterface $em, $class, $alias = 'a')
80
    {
81
        if(strlen($alias) != 1) {
82
            throw new \InvalidArgumentException('Alias should be a single letter');
83
        }
84
85
        $this->alias = $alias;
86
        $this->queryBuilder = new QueryBuilder($em);
87
        $this->queryBuilder->select($this->getAlias());
88
        $this->queryBuilder->from($class, $this->getAlias());
89
        $this->queryBuilder->addGroupBy($this->alias . '.id');
90
    }
91
92
    public function addOrderBy($property, $order, $joinProperty = null)
93
    {
94
        if ($joinProperty === null) {
95
            $joinProperty = [];
96
        } elseif (!is_array($joinProperty)) {
97
            $joinProperty = [ $joinProperty ];
98
        }
99
        $this->orderBy[] = [
100
            'property' => $property,
101
            'order' => $order,
102
            'joinProperty' => $joinProperty
103
        ];
104
105
        return $this;
106
    }
107
108
    public function removeOrderBy($property, $order)
109
    {
110
        if(!$property && !$order){
111
            return $this;
112
        }
113
        foreach ($this->orderBy as $index => $orderBy){
114
            if($property && $orderBy['property'] !== $property){
115
                continue;
116
            }
117
            if($order && $orderBy['operator'] !== $order){
118
                continue;
119
            }
120
            unset($this->orderBy[$index]);
121
        }
122
123
        return $this;
124
    }
125
126
    public function addWhere($property, $operator, $value, $joinProperty = null)
127
    {
128
        if ($joinProperty === null) {
129
            $joinProperty = [];
130
        } elseif (!is_array($joinProperty)) {
131
            $joinProperty = [ $joinProperty ];
132
        }
133
        $this->where[] = [
134
            'property' => $property,
135
            'operator' => $operator,
136
            'value' => $value,
137
            'joinProperty' => $joinProperty
138
        ];
139
140
        return $this;
141
    }
142
143
    public function removeWhere($property, $operator, $value, $joinProperty = null)
144
    {
145
        if(!$property && !$operator && !$value && !$joinProperty){
146
            return $this;
147
        }
148
        foreach ($this->where as $index => $where){
149
            if($property && $where['property'] !== $property){
150
                continue;
151
            }
152
            if($operator && $where['operator'] !== $operator){
153
                continue;
154
            }
155
            if($value && $where['value'] !== $value){
156
                continue;
157
            }
158
            if($joinProperty && $where['joinProperty'] !== $joinProperty){
159
                continue;
160
            }
161
            unset($this->where[$index]);
162
        }
163
164
        return $this;
165
    }
166
167
    public function or(): FilterQueryOr
168
    {
169
        $this->orBlocks []= new FilterQueryOr();
170
        return $this->orBlocks[count($this->orBlocks) - 1];
171
    }
172
173
    public function addCallback(callable $callback, $additionalParameters = [])
174
    {
175
        $this->callbacks []= [
176
            'function' => $callback,
177
            'parameters' => $additionalParameters
178
        ];
179
    }
180
181
    public function getWhere()
182
    {
183
        return $this->where;
184
    }
185
186
    public function getOrderBy()
187
    {
188
        return $this->orderBy;
189
    }
190
191
    /**
192
     * @return QueryBuilder
193
     */
194
    public function getQueryBuilder()
195
    {
196
        return $this->queryBuilder;
197
    }
198
199
    public function build()
200
    {
201
        /** @var QueryBuilder $query */
202
        $query = $this->queryBuilder;
203
        $globalIndex = 0;
204
        foreach($this->getWhere() as $where) {
205
            $globalIndex++;
206
            if(count($where['joinProperty'])) {
207
                $joinPrefix = $this->createJoins($query, $where['joinProperty'], $globalIndex);
208
                $query->andWhere(sprintf('%s.%s %s %s', $joinPrefix, $where['property'], $this->getOperator($where), $this->getParameter($where, $globalIndex)));
209
            } else {
210
                $query->andWhere(sprintf('%s.%s %s %s', $this->getAlias(), $where['property'], $this->getOperator($where), $this->getParameter($where, $globalIndex)));
211
            }
212
            if($this->hasParameter($where)) {
213
                $query->setParameter($this->getParameterName($globalIndex), $this->getValue($where));
214
            }
215
        }
216
217
        foreach($this->orBlocks as $orBlock) {
218
            $orQueryStrings = [];
219
            foreach($orBlock->getWhere() as $where) {
220
                $globalIndex++;
221
                if(count($where['joinProperty'])) {
222
                    $joinPrefix = $this->createJoins($query, $where['joinProperty'], $globalIndex);
223
                    $orQueryStrings []= sprintf('%s.%s %s %s', $joinPrefix, $where['property'], $this->getOperator($where), $this->getParameter($where, $globalIndex));
224
                } else {
225
                    $orQueryStrings []= sprintf('%s.%s %s %s', $this->getAlias(), $where['property'], $this->getOperator($where), $this->getParameter($where, $globalIndex));
226
                }
227
                if($this->hasParameter($where)) {
228
                    $query->setParameter($this->getParameterName($globalIndex), $this->getValue($where));
229
                }
230
            }
231
            $query->andWhere(sprintf('(%s)', implode(' OR ', $orQueryStrings)));
232
        }
233
234
        foreach($this->getOrderBy() as $order) {
235
            $globalIndex++;
236
            if(count($order['joinProperty'])) {
237
                $joinPrefix = $this->createJoins($query, $order['joinProperty'], $globalIndex);
238
                $query->addOrderBy(sprintf('%s.%s', $joinPrefix, $order['property']), $order['order']);
239
            } else {
240
                $query->addOrderBy(sprintf('%s.%s', $this->getAlias(), $order['property']), $order['order']);
241
            }
242
        }
243
244
        if ($this->getHydrate() === self::HYDRATE_ID) {
245
            $query->select($this->getAlias() . '.id');
246
        }
247
248
        foreach($this->callbacks as $callbackInfo) {
249
            $parameters = array_merge([$query, $this->getAlias()], $callbackInfo['parameters']);
250
            call_user_func_array($callbackInfo['function'], $parameters);
251
        }
252
253
        return $this;
254
    }
255
256
    private function createJoins(QueryBuilder $query, $joins, $index)
257
    {
258
        $joinPrefixes = $this->createJoinAliases($index, count($joins));
259
        foreach($joins as $joinProperty) {
260
            $joinPrefix = array_shift($joinPrefixes);
261
            $query->leftJoin(sprintf('%s.%s', $joinPrefix, $joinProperty), $joinPrefixes[0]);
262
        }
263
        return $joinPrefixes[0];
264
    }
265
266
    public function createJoinAliases($index, $length)
267
    {
268
        $result = [$this->getAlias()];
269
        for($i = 0; $i < $length; $i++) {
270
            $result []= 'j' . self::UNIQUE_PARAMETER_PREFIX . $index . '_' . $i;
271
        }
272
        return $result;
273
    }
274
275
    private function getValue($where)
276
    {
277
        $value = $where['value'];
278
279
        if($where['operator'] == FilterQuery::OPERATOR_LIKE) {
280
            return '%'.$value.'%';
281
        }
282
283
        if($where['operator'] == FilterQuery::OPERATOR_START_LIKE) {
284
            return $value.'%';
285
        }
286
287
        if($where['operator'] == FilterQuery::OPERATOR_END_LIKE) {
288
            return '%'.$value;
289
        }
290
291
        return $value;
292
    }
293
294
    private function getOperator($where)
295
    {
296
        $value = $where['value'];
297
298
        switch($where['operator']) {
299
            case(FilterQuery::OPERATOR_EQUALS):
300
                if($value === null) {
301
                    return 'is';
302
                }
303
                return '=';
304
            case(FilterQuery::OPERATOR_GREATER):
305
                return '>';
306
            case(FilterQuery::OPERATOR_GREATER_EQUAL):
307
                return '>=';
308
            case(FilterQuery::OPERATOR_LESS):
309
                return '<';
310
            case(FilterQuery::OPERATOR_LESS_EQUAL):
311
                return '<=';
312
            case(FilterQuery::OPERATOR_NOT):
313
                if($value === null) {
314
                    return 'is not';
315
                }
316
                return '!=';
317
            case(FilterQuery::OPERATOR_LIKE):
318
            case(FilterQuery::OPERATOR_START_LIKE):
319
            case(FilterQuery::OPERATOR_END_LIKE):
320
                return 'like';
321
            case(FilterQuery::OPERATOR_IN):
322
                return 'in';
323
        }
324
        throw new FilterException('Operator not supported in Repository');
325
    }
326
327
    private function getParameter($where, $number)
328
    {
329
        $value = $where['value'];
330
        if($where['operator'] === FilterQuery::OPERATOR_EQUALS && $value === null) {
331
            return 'null';
332
        }
333
        if($where['operator'] === FilterQuery::OPERATOR_IN) {
334
            return '(:' . $this->getParameterName($number) . ')';
335
        }
336
        return ':' . $this->getParameterName($number);
337
    }
338
339
    private function hasParameter($where)
340
    {
341
        $value = $where['value'];
342
        if(FilterQuery::OPERATOR_EQUALS && $value === null) {
343
            return false;
344
        }
345
        return true;
346
    }
347
348
    private function getParameterName($globalIndex)
349
    {
350
        return 'p' . self::UNIQUE_PARAMETER_PREFIX . $globalIndex;
351
    }
352
353
    public function getAlias()
354
    {
355
        return $this->alias;
356
    }
357
358
    /**
359
     * @return string
360
     */
361
    public function getHydrate(): string
362
    {
363
        return $this->hydrate;
364
    }
365
366
    /**
367
     * @param string $hydrate
368
     */
369
    public function setHydrate(string $hydrate): void
370
    {
371
        $this->hydrate = $hydrate;
372
    }
373
374
    /**
375
     * @return bool
376
     */
377
    public function isPaginated(): bool
378
    {
379
        return $this->paginated;
380
    }
381
382
    /**
383
     * @param bool $paginated
384
     */
385
    public function setPaginated(bool $paginated): void
386
    {
387
        $this->paginated = $paginated;
388
    }
389
}
390