Completed
Push — master ( d5c275...27397e )
by
unknown
15s
created

Filterer::buildExpression()   D

Complexity

Conditions 20
Paths 20

Size

Total Lines 40
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 4.8819
c 0
b 0
f 0
cc 20
eloc 34
nc 20
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Netdudes\DataSourceryBundle\DataSource\Driver\Doctrine\QueryBuilder;
4
5
use Doctrine\ORM\Query\Expr;
6
use Doctrine\ORM\Query\Expr\Andx;
7
use Doctrine\ORM\Query\Expr\Composite;
8
use Doctrine\ORM\Query\Expr\Orx;
9
use Doctrine\ORM\QueryBuilder;
10
use Netdudes\DataSourceryBundle\Query\Filter;
11
use Netdudes\DataSourceryBundle\Query\FilterCondition;
12
13
class Filterer
14
{
15
    /**
16
     * @var array
17
     */
18
    private $uniqueNameToQueryFieldMap;
19
20
    /**
21
     * Apply the filters to the query builders
22
     *
23
     * @param QueryBuilder $queryBuilder
24
     * @param Filter       $filterDefinition
25
     * @param array        $uniqueNameToQueryFieldMap
26
     *
27
     * @return QueryBuilder
28
     * @throws \Exception
29
     */
30
    public function filter(QueryBuilder $queryBuilder, Filter $filterDefinition, $uniqueNameToQueryFieldMap)
31
    {
32
        $this->uniqueNameToQueryFieldMap = $uniqueNameToQueryFieldMap;
33
        $expressions = $this->buildFilterGroup($queryBuilder, $filterDefinition);
34
        if ($expressions->count() > 0) {
35
            $queryBuilder->andWhere($expressions);
36
        }
37
38
        return $queryBuilder;
39
    }
40
41
    /**
42
     * Builds a filter group and, recursively, constructs a tree of conditions for the filters
43
     *
44
     * @param QueryBuilder $queryBuilder
45
     * @param Filter       $filterDefinition
46
     *
47
     * @throws \Exception
48
     * @return Andx|Orx
49
     */
50
    private function buildFilterGroup(QueryBuilder $queryBuilder, Filter $filterDefinition)
51
    {
52
        // Container for the expressions to add to the $queryBuilder
53
        $filterConditionType = $filterDefinition->getConditionType();
54
        if ($filterConditionType == Filter::CONDITION_TYPE_AND) {
55
            $expressions = $queryBuilder->expr()->andX();
56
        } elseif ($filterConditionType == Filter::CONDITION_TYPE_OR) {
57
            $expressions = $queryBuilder->expr()->orX();
58
        } else {
59
            throw new \Exception("Unknown condition type $filterConditionType on the filter.");
60
        }
61
62
        // Loop through all the filters in the collection
63
        $this->addExpressionsForFilter($expressions, $filterDefinition, $queryBuilder);
64
65
        return $expressions;
66
    }
67
68
    /**
69
     * @param Composite    $expressions
70
     * @param Filter       $filterDefinition
71
     * @param QueryBuilder $queryBuilder
72
     *
73
     * @throws \Exception
74
     */
75
    private function addExpressionsForFilter(Composite $expressions, Filter $filterDefinition, QueryBuilder $queryBuilder)
76
    {
77
        foreach ($filterDefinition as $subFilterDefinition) {
78
            if ($subFilterDefinition instanceof Filter) {
79
                // If the element is itself a Collection, recursively build it
80
                $expressions->add($this->buildFilterGroup($queryBuilder, $subFilterDefinition));
81
            } elseif ($subFilterDefinition instanceof FilterCondition) {
82
                $this->addExpressionsForFilterCondition($expressions, $subFilterDefinition, $queryBuilder);
83
            }
84
        }
85
    }
86
87
    /**
88
     * @param Composite       $expressions
89
     * @param FilterCondition $filterCondition
90
     * @param QueryBuilder    $queryBuilder
91
     *
92
     * @throws \Exception
93
     */
94
    private function addExpressionsForFilterCondition(Composite $expressions, FilterCondition $filterCondition, QueryBuilder $queryBuilder)
95
    {
96
        $token = $this->buildUniqueToken($filterCondition, $queryBuilder);
97
98
        $expression = $this->buildExpression($filterCondition, $token, $queryBuilder);
99
        $expressions->add($expression);
100
101
        $this->setExpressionParameters($filterCondition, $token, $queryBuilder);
102
    }
103
104
    /**
105
     * @param FilterCondition $filterCondition
106
     * @param QueryBuilder    $queryBuilder
107
     *
108
     * @return string
109
     */
110
    private function buildUniqueToken(FilterCondition $filterCondition, QueryBuilder $queryBuilder)
111
    {
112
        return ':token_'
113
            . strtolower(str_replace(['.', '-'], '_', $filterCondition->getFieldName()))
114
            . '_' . $queryBuilder->getParameters()->count();
115
    }
116
117
    /**
118
     * @param FilterCondition $filterCondition
119
     * @param string          $token
120
     * @param QueryBuilder    $queryBuilder
121
     *
122
     * @throws \Exception
123
     *
124
     * @return Expr|string
125
     */
126
    private function buildExpression(FilterCondition $filterCondition, $token, QueryBuilder $queryBuilder)
127
    {
128
        $identifier = $this->uniqueNameToQueryFieldMap[$filterCondition->getFieldName()];
129
130
        if (null === $filterCondition->getValue()) {
131
            return $this->buildExpressionForNullValue($filterCondition, $identifier, $queryBuilder);
132
        }
133
134
        $filterMethod = $filterCondition->getMethod();
135
        switch ($filterMethod) {
136
            case FilterCondition::METHOD_STRING_LIKE:
137
                return $queryBuilder->expr()->like($identifier, $token);
138
            case FilterCondition::METHOD_STRING_EQ:
139
            case FilterCondition::METHOD_NUMERIC_EQ:
140
            case FilterCondition::METHOD_BOOLEAN:
141
            case FilterCondition::METHOD_DATETIME_EQ:
142
                return $queryBuilder->expr()->eq($identifier, $token);
143
            case FilterCondition::METHOD_NUMERIC_GT:
144
            case FilterCondition::METHOD_DATETIME_GT:
145
                return $queryBuilder->expr()->gt($identifier, $token);
146
            case FilterCondition::METHOD_NUMERIC_GTE:
147
            case FilterCondition::METHOD_DATETIME_GTE:
148
                return $queryBuilder->expr()->gte($identifier, $token);
149
            case FilterCondition::METHOD_NUMERIC_LTE:
150
            case FilterCondition::METHOD_DATETIME_LTE:
151
                return $queryBuilder->expr()->lte($identifier, $token);
152
            case FilterCondition::METHOD_NUMERIC_LT:
153
            case FilterCondition::METHOD_DATETIME_LT:
154
                return $queryBuilder->expr()->lt($identifier, $token);
155
            case FilterCondition::METHOD_STRING_NEQ:
156
            case FilterCondition::METHOD_NUMERIC_NEQ:
157
            case FilterCondition::METHOD_DATETIME_NEQ:
158
                return $queryBuilder->expr()->neq($identifier, $token);
159
            case FilterCondition::METHOD_IN:
160
            case FilterCondition::METHOD_NIN:
161
                return $this->buildExpressionForCollection($filterCondition, $identifier, $token, $queryBuilder);
162
            default:
163
                throw new \Exception("Unknown filtering method \"$filterMethod\" for column \"" . $filterCondition->getFieldName() . '"');
164
        }
165
    }
166
167
    /**
168
     * @param FilterCondition $filterCondition
169
     * @param string          $identifier
170
     * @param QueryBuilder    $queryBuilder
171
     *
172
     * @return Andx|Orx
173
     *
174
     * @throws \Exception
175
     */
176
    private function buildExpressionForNullValue(FilterCondition $filterCondition, $identifier, QueryBuilder $queryBuilder)
177
    {
178
        $method = $filterCondition->getMethod();
179
180
        $isEmptyFiltering = in_array($method, [
181
            FilterCondition::METHOD_STRING_EQ,
182
            FilterCondition::METHOD_NUMERIC_EQ,
183
            FilterCondition::METHOD_DATETIME_EQ,
184
        ]);
185
        if ($isEmptyFiltering) {
186
            return $queryBuilder->expr()->orX(
187
                $queryBuilder->expr()->eq($identifier, $queryBuilder->expr()->literal('')),
188
                $queryBuilder->expr()->isNull($identifier)
189
            );
190
        }
191
192
        $isNotEmptyFiltering = in_array($method, [
193
            FilterCondition::METHOD_STRING_NEQ,
194
            FilterCondition::METHOD_NUMERIC_NEQ,
195
            FilterCondition::METHOD_DATETIME_NEQ,
196
        ]);
197
        if ($isNotEmptyFiltering) {
198
            return $queryBuilder->expr()->andX(
199
                $queryBuilder->expr()->neq($identifier, $queryBuilder->expr()->literal('')),
200
                $queryBuilder->expr()->isNotNull($identifier)
201
            );
202
        }
203
204
        throw new \Exception("The $method operator cannot be used to compare against null value");
205
    }
206
207
    /**
208
     * @param FilterCondition $filterCondition
209
     * @param string          $identifier
210
     * @param string          $token
211
     * @param QueryBuilder    $queryBuilder
212
     *
213
     * @return Andx|Orx|string
214
     *
215
     * @throws \Exception
216
     */
217
    private function buildExpressionForCollection(FilterCondition $filterCondition, $identifier, $token, QueryBuilder $queryBuilder)
218
    {
219
        $method = $filterCondition->getMethod();
220
        $value = $filterCondition->getValue();
221
222
        if (!is_array($value)) {
223
            throw new \Exception('Only arrays can be arguments of a METHOD_IN or METHOD_NIN filter');
224
        }
225
226
        if (count($value) <= 0) {
227
            // The array is empty, therefore this will always be "false". We use an always-false expression
228
            // to emulate this without actually using an invalid empty array in the IN statement.
229
            return '1=2';
230
        }
231
232
        if ($method === FilterCondition::METHOD_IN) {
233
            return $queryBuilder->expr()->in($identifier, $token);
234
        } else {
235
            return $queryBuilder->expr()->notIn($identifier, $token);
236
        }
237
    }
238
239
    /**
240
     * @param FilterCondition $filterCondition
241
     * @param string          $token
242
     * @param QueryBuilder    $queryBuilder
243
     */
244
    private function setExpressionParameters(FilterCondition $filterCondition, $token, QueryBuilder $queryBuilder)
245
    {
246
        $filterMethod = $filterCondition->getMethod();
247
248
        $filteringUsingInOperator = in_array($filterMethod, [FilterCondition::METHOD_IN, FilterCondition::METHOD_NIN]);
249
        if ($filteringUsingInOperator && count($filterCondition->getValue()) <= 0) {
250
            return;
251
        }
252
253
        $comparingAgainstNull = null === $filterCondition->getValue();
254
        if ($comparingAgainstNull) {
255
            return;
256
        }
257
258
        $valueInDatabase = $filterCondition->getValueInDatabase();
259
        if ($filterMethod == FilterCondition::METHOD_STRING_LIKE) {
260
            $valueInDatabase = str_replace('*', '%', $valueInDatabase);
261
        }
262
263
        $queryBuilder->setParameter($token, $valueInDatabase);
264
    }
265
}
266