Completed
Pull Request — master (#22)
by
unknown
04:09
created

Filterer::buildSetInclusionCondition()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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