Completed
Pull Request — master (#22)
by
unknown
02:11
created

Filterer::filter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
c 0
b 0
f 0
rs 9.4285
cc 2
eloc 6
nc 2
nop 3
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
        // Build an unique token name for parameter substitution
100
        $token = $this->buildUniqueToken($filterCondition, $queryBuilder);
101
        $filterMethod = $filterCondition->getMethod();
102
103
        // Add the filtering statement
104
        $valueInDatabase = $filterCondition->getValueInDatabase();
105
106
        // Flag to not insert the parameter if the logic requires it
107
        $ignoreParameter = false;
108
109
        // Depending on the filter type, create a condition
110
        $condition = $this->buildCondition($filterCondition, $token, $queryBuilder);
111
        $expressions->add($condition);
112
113
        // Ignore value if needed
114
        if (
115
            (in_array($filterMethod, [FilterCondition::METHOD_IN, FilterCondition::METHOD_NIN]) && count($valueInDatabase) <= 0) ||
116
            ($filterMethod == FilterCondition::METHOD_IS_NULL)
117
            || null === $filterCondition->getValue()
118
        ) {
119
            $ignoreParameter = true;
120
        }
121
122
        // Modify the value if needed
123
        if ($filterMethod == FilterCondition::METHOD_STRING_LIKE) {
124
            $valueInDatabase = str_replace('*', '%', $valueInDatabase);
125
        }
126
127
        // Insert the value substituting the token
128
        if (!$ignoreParameter) {
129
            $queryBuilder->setParameter($token, $valueInDatabase);
130
        }
131
    }
132
133
    /**
134
     * @param FilterCondition $filterCondition
135
     * @param string          $token
136
     * @param QueryBuilder    $queryBuilder
137
     *
138
     * @throws ColumnNotFoundException
139
     * @throws \Exception
140
     *
141
     * @return Expr|string
142
     */
143
    protected function buildCondition(FilterCondition $filterCondition, $token, QueryBuilder $queryBuilder)
144
    {
145
        $filterMethod = $filterCondition->getMethod();
146
        $identifier = $this->uniqueNameToQueryFieldMap[$filterCondition->getFieldName()];
147
        $value = $filterCondition->getValue();
148
149
        if (null === $value) {
150
            if ($this->isNullFiltering($filterMethod)) {
151
                return $queryBuilder->expr()->isNull($identifier);
152
            } elseif ($this->isNotNullFiltering($filterMethod)) {
153
                return $queryBuilder->expr()->isNotNull($identifier);
154
            } else {
155
                throw new \Exception("The $filterMethod operator cannot be used to compare against null value");
156
            }
157
        }
158
159
        switch ($filterMethod) {
160
            case FilterCondition::METHOD_STRING_LIKE:
161
                return $queryBuilder->expr()->like($identifier, $token);
162
            case FilterCondition::METHOD_STRING_EQ:
163
            case FilterCondition::METHOD_NUMERIC_EQ:
164
            case FilterCondition::METHOD_BOOLEAN:
165
            case FilterCondition::METHOD_DATETIME_EQ:
166
                return $queryBuilder->expr()->eq($identifier, $token);
167
            case FilterCondition::METHOD_NUMERIC_GT:
168
            case FilterCondition::METHOD_DATETIME_GT:
169
                return $queryBuilder->expr()->gt($identifier, $token);
170
            case FilterCondition::METHOD_NUMERIC_GTE:
171
            case FilterCondition::METHOD_DATETIME_GTE:
172
                return $queryBuilder->expr()->gte($identifier, $token);
173
            case FilterCondition::METHOD_NUMERIC_LTE:
174
            case FilterCondition::METHOD_DATETIME_LTE:
175
                return $queryBuilder->expr()->lte($identifier, $token);
176
            case FilterCondition::METHOD_NUMERIC_LT:
177
            case FilterCondition::METHOD_DATETIME_LT:
178
                return $queryBuilder->expr()->lt($identifier, $token);
179
            case FilterCondition::METHOD_STRING_NEQ:
180
            case FilterCondition::METHOD_NUMERIC_NEQ:
181
            case FilterCondition::METHOD_DATETIME_NEQ:
182
                return $queryBuilder->expr()->neq($identifier, $token);
183
            case FilterCondition::METHOD_IN:
184
            case FilterCondition::METHOD_NIN:
185
                if (!is_array($value)) {
186
                    throw new \Exception('Only arrays can be arguments of a METHOD_IN or METHOD_NIN filter');
187
                }
188
189
                if (count($filterCondition->getValue()) <= 0) {
190
                    // The array is empty, therefore this will always be "false". We use an always-false expression
191
                    // to emulate this without actually using an invalid empty array in the IN statement.
192
                    return '1=2';
193
                }
194
195
                if ($filterMethod === FilterCondition::METHOD_IN) {
196
                    return $queryBuilder->expr()->in($identifier, $token);
197
                } else {
198
                    return $queryBuilder->expr()->notIn($identifier, $token);
199
                }
200
            case FilterCondition::METHOD_IS_NULL:
201
                if ($filterCondition->getValue()) {
202
                    return $queryBuilder->expr()->isNull($identifier);
203
                }
204
205
                return $queryBuilder->expr()->isNotNull($identifier);
206
            default:
207
                throw new \Exception("Unknown filtering method \"$filterMethod\" for column \"" . $filterCondition->getFieldName() . '"');
208
        }
209
    }
210
211
    /**
212
     * @param FilterCondition $filterCondition
213
     * @param QueryBuilder    $queryBuilder
214
     *
215
     * @return string
216
     * @throws ColumnNotFoundException
217
     */
218
    protected function buildUniqueToken(FilterCondition $filterCondition, QueryBuilder $queryBuilder)
219
    {
220
        return ':token_'
221
            . strtolower(str_replace(['.', '-'], '_', $filterCondition->getFieldName()))
222
            . '_' . $queryBuilder->getParameters()->count();
223
    }
224
225
    /**
226
     * Helper method: transforms a column identifier to a database field for use
227
     * in filtering and sorting
228
     *
229
     * @param array|Field[] $fields
230
     * @param string        $dataSourceFieldUniqueName
231
     *
232
     * @throws ColumnNotFoundException
233
     * @return mixed
234
     */
235
    protected function getDatabaseFilterQueryFieldByDataSourceFieldUniqueName(array $fields, $dataSourceFieldUniqueName)
236
    {
237
        $dataSourceField = null;
238
        foreach ($fields as $field) {
239
            if ($field->getUniqueName() == $dataSourceFieldUniqueName) {
240
                $dataSourceField = $field;
241
                break;
242
            }
243
        }
244
245
        if (is_null($dataSourceField)) {
246
            throw new ColumnNotFoundException("Could not find column \"$dataSourceFieldUniqueName\" in the data source");
247
        }
248
249
        return $dataSourceField->getDatabaseFilterQueryField();
250
    }
251
252
    /**
253
     * @param string $method
254
     *
255
     * @return bool
256
     */
257
    private function isNullFiltering($method)
258
    {
259
        return in_array($method, [
260
            FilterCondition::METHOD_STRING_EQ,
261
            FilterCondition::METHOD_NUMERIC_EQ,
262
            FilterCondition::METHOD_DATETIME_EQ,
263
        ]);
264
    }
265
266
    /**
267
     * @param string $method
268
     *
269
     * @return bool
270
     */
271
    private function isNotNullFiltering($method)
272
    {
273
        return in_array($method, [
274
            FilterCondition::METHOD_STRING_NEQ,
275
            FilterCondition::METHOD_NUMERIC_NEQ,
276
            FilterCondition::METHOD_DATETIME_NEQ,
277
        ]);
278
    }
279
}
280