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

Filterer::buildFilterGroup()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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