Completed
Push — master ( 48c3ae...6eeb89 )
by
unknown
05:00 queued 02:22
created

Interpreter::buildFilterFromASTGroup()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 16
rs 9.2
cc 4
eloc 10
nc 5
nop 1
1
<?php
2
namespace Netdudes\DataSourceryBundle\UQL;
3
4
use Netdudes\DataSourceryBundle\DataSource\Configuration\Field;
5
use Netdudes\DataSourceryBundle\DataSource\Configuration\FieldInterface;
6
use Netdudes\DataSourceryBundle\DataSource\DataSourceInterface;
7
use Netdudes\DataSourceryBundle\Extension\UqlExtensionContainer;
8
use Netdudes\DataSourceryBundle\Query\Filter;
9
use Netdudes\DataSourceryBundle\Query\FilterCondition;
10
use Netdudes\DataSourceryBundle\Query\FilterConditionFactory;
11
use Netdudes\DataSourceryBundle\UQL\AST\ASTArray;
12
use Netdudes\DataSourceryBundle\UQL\AST\ASTAssertion;
13
use Netdudes\DataSourceryBundle\UQL\AST\ASTFunctionCall;
14
use Netdudes\DataSourceryBundle\UQL\AST\ASTGroup;
15
use Netdudes\DataSourceryBundle\UQL\Exception\UQLInterpreterException;
16
17
/**
18
 * Class Interpreter
19
 *
20
 * The Interpreter transforms the generic Abstract Syntax Tree into Filters
21
 */
22
class Interpreter
23
{
24
    /**
25
     * @var UqlExtensionContainer
26
     */
27
    private $extensionContainer;
28
29
    /**
30
     * @var DataSourceInterface
31
     */
32
    private $dataSource;
33
34
    /**
35
     * @var array
36
     */
37
    private $dataSourceElements;
38
39
    /**
40
     * @var bool
41
     */
42
    private $caseSensitive;
43
44
    /**
45
     * @var FilterConditionFactory
46
     */
47
    private $filterConditionFactory;
48
49
    /**
50
     * Constructor needs the columns descriptor to figure out appropriate filtering methods
51
     * and translate identifiers.
52
     *
53
     * @param UqlExtensionContainer  $extensionContainer
54
     * @param DataSourceInterface    $dataSource
55
     * @param FilterConditionFactory $filterConditionFactory
56
     * @param bool                   $caseSensitive
57
     */
58
    public function __construct(
59
        UqlExtensionContainer $extensionContainer,
60
        DataSourceInterface $dataSource,
61
        FilterConditionFactory $filterConditionFactory,
62
        $caseSensitive = true
63
    ) {
64
        $this->extensionContainer = $extensionContainer;
65
        $this->dataSource = $dataSource;
66
        $this->filterConditionFactory = $filterConditionFactory;
67
        $this->caseSensitive = $caseSensitive;
68
69
        // Cache an array of data sources (name => object pairs) for reference during the interpretation
70
        $this->dataSourceElements = array_combine(
71
            array_map(
72
                function (FieldInterface $element) use ($caseSensitive) {
73
                    return $caseSensitive ? $element->getUniqueName() : strtolower($element->getUniqueName());
74
                },
75
                $this->dataSource->getFields()
76
            ),
77
            $this->dataSource->getFields()
78
        );
79
    }
80
81
    /**
82
     * Helper method: matches filtering operators to valid UQL operators
83
     * in order to do Filter to UQL transformations
84
     *
85
     * @param $method
86
     *
87
     * @throws Exception\UQLInterpreterException
88
     * @return
89
     */
90
    public static function methodToUQLOperator($method)
91
    {
92
        $translationMap = [
93
            FilterCondition::METHOD_STRING_EQ => "=",
94
            FilterCondition::METHOD_STRING_LIKE => "~",
95
            FilterCondition::METHOD_STRING_NEQ => "!=",
96
            FilterCondition::METHOD_NUMERIC_GT => ">",
97
            FilterCondition::METHOD_NUMERIC_GTE => ">=",
98
            FilterCondition::METHOD_NUMERIC_EQ => "=",
99
            FilterCondition::METHOD_NUMERIC_LTE => "<=",
100
            FilterCondition::METHOD_NUMERIC_LT => "<",
101
            FilterCondition::METHOD_NUMERIC_NEQ => "!=",
102
            FilterCondition::METHOD_IN => "IN",
103
            FilterCondition::METHOD_BOOLEAN => "is",
104
            FilterCondition::METHOD_IS_NULL => "is null",
105
            FilterCondition::METHOD_DATETIME_GT => "after",
106
            FilterCondition::METHOD_DATETIME_GTE => "after or at",
107
            FilterCondition::METHOD_DATETIME_EQ => "at",
108
            FilterCondition::METHOD_DATETIME_LTE => "before or at",
109
            FilterCondition::METHOD_DATETIME_LT => "before",
110
            FilterCondition::METHOD_DATETIME_NEQ => "not at",
111
        ];
112
113
        if (isset($translationMap[$method])) {
114
            return $translationMap[$method];
115
        }
116
117
        throw new UQLInterpreterException("Can't translate filtering method '$method'' into a valid UQL operator");
118
    }
119
120
    /**
121
     * Generate the filter objects corresponding to a UQL string.
122
     *
123
     * @param string $uql
124
     *
125
     * @return Filter
126
     */
127
    public function interpret($uql)
128
    {
129
        if (empty(trim($uql))) {
130
            return new Filter();
131
        }
132
133
        $parser = new Parser();
134
        $AST = $parser->parse($uql);
135
136
        return $this->buildFilter($AST);
137
    }
138
139
    /**
140
     * Transforms a subtree of the AST into a concrete filter definition.
141
     * This function recursively builds all sub-trees.
142
     *
143
     * @param ASTGroup|ASTAssertion|mixed $astSubtree
144
     *
145
     * TODO: make private
146
     *
147
     * @return Filter|FilterCondition
148
     * @throws \Exception
149
     */
150
    public function buildFilter($astSubtree)
151
    {
152
        if ($astSubtree instanceof ASTGroup) {
153
            return $this->buildFilterFromASTGroup($astSubtree);
154
        }
155
156
        if ($astSubtree instanceof ASTAssertion) {
157
            $filterCondition = $this->buildFilterConditionFromASTAssertion($astSubtree);
158
            // Single filter. Wrap into dummy filter collection for consistency.
159
            $filter = new Filter();
160
            $filter[] = $filterCondition;
161
            return $filter;
162
        }
163
164
        throw new UQLInterpreterException('Unexpected Abstract Syntax Tree element');
165
    }
166
167
    /**
168
     * Translate <operator> tokens into Filter Methods.
169
     *
170
     * @param string         $token
171
     * @param FieldInterface $dataSourceElement
172
     *
173
     * @throws Exception\UQLInterpreterException
174
     * @return mixed
175
     */
176
    public function translateOperator($token, FieldInterface $dataSourceElement)
177
    {
178
        $translationTable = [
179
            "T_OP_LT" => [
180
                FilterCondition::METHOD_NUMERIC_LT,
181
                FilterCondition::METHOD_DATETIME_LT,
182
            ],
183
            "T_OP_LTE" => [
184
                FilterCondition::METHOD_NUMERIC_LTE,
185
                FilterCondition::METHOD_DATETIME_LTE,
186
            ],
187
            "T_OP_EQ" => [
188
                FilterCondition::METHOD_NUMERIC_EQ,
189
                FilterCondition::METHOD_STRING_EQ,
190
                FilterCondition::METHOD_DATETIME_EQ,
191
                FilterCondition::METHOD_BOOLEAN,
192
            ],
193
            "T_OP_GTE" => [
194
                FilterCondition::METHOD_NUMERIC_GTE,
195
                FilterCondition::METHOD_DATETIME_GTE,
196
            ],
197
            "T_OP_GT" => [
198
                FilterCondition::METHOD_NUMERIC_GT,
199
                FilterCondition::METHOD_DATETIME_GT,
200
            ],
201
            "T_OP_NEQ" => [
202
                FilterCondition::METHOD_NUMERIC_NEQ,
203
                FilterCondition::METHOD_STRING_NEQ,
204
            ],
205
            "T_OP_LIKE" => [
206
                FilterCondition::METHOD_STRING_LIKE,
207
            ],
208
            "T_OP_IN" => [
209
                FilterCondition::METHOD_IN,
210
            ],
211
        ];
212
213
        if (!isset($translationTable[$token])) {
214
            throw new UQLInterpreterException('Unable to translate token ' . $token . ' to a valid filtering method. Unknown token.');
215
        }
216
        $possibleMethods = $translationTable[$token];
217
218
        // See if any of the methods is the default of the data type
219
        $dataType = $dataSourceElement->getDataType();
220
        foreach ($possibleMethods as $possibleMethod) {
221
            if ($possibleMethod === $dataType->getDefaultFilterMethod()) {
222
                return $possibleMethod;
223
            }
224
        }
225
226
        // Else, just accept the first one in the available methods
227
        foreach ($possibleMethods as $possibleMethod) {
228
            if (in_array($possibleMethod, $dataType->getAvailableFilterMethods())) {
229
                return $possibleMethod;
230
            }
231
        }
232
233
        throw new UQLInterpreterException('Unable to translate token ' . $token . ' to a valid filtering method. No methods are valid for the data type "' . $dataType->getName() . '" for data element "' . $dataSourceElement->getUniqueName() . '"');
234
    }
235
236
    /**
237
     * Translate Lexer <logic> tokens into Filter Condition Types.
238
     *
239
     * @param $token
240
     *
241
     * @return string
242
     * @throws \Exception
243
     */
244
    private function translateLogic($token)
245
    {
246
        $translationTable = [
247
            "T_LOGIC_AND" => Filter::CONDITION_TYPE_AND,
248
            "T_LOGIC_OR" => Filter::CONDITION_TYPE_OR,
249
            "T_LOGIC_XOR" => Filter::CONDITION_TYPE_XOR,
250
        ];
251
252
        if (isset($translationTable[$token])) {
253
            return $translationTable[$token];
254
        }
255
256
        throw new \Exception('Unable to translate token ' . $token . ' to a valid filter condition type.');
257
    }
258
259
    /**
260
     * Trim and clean up the value to be set in the filter.
261
     *
262
     * @param $value
263
     *
264
     * @return mixed
265
     */
266
    private function parseValue($value)
267
    {
268
        if (is_bool($value)) {
269
            return $value ? "1" : "0";
270
        }
271
272
        return trim($value, "\"'");
273
    }
274
275
    /**
276
     * @param ASTFunctionCall $functionCall
277
     *
278
     * @return mixed
279
     * @throws Exception\UQLInterpreterException
280
     */
281
    private function callFunction(ASTFunctionCall $functionCall)
282
    {
283
        try {
284
            return $this->extensionContainer->callFunction($functionCall->getFunctionName(), $functionCall->getArguments());
285
        } catch (\Exception $e) {
286
            throw new UQLInterpreterException("The execution of function '" . $functionCall->getFunctionName() . "' failed. Please check the arguments are valid. (" . $e->getMessage() . ")");
287
        }
288
    }
289
290
    /**
291
     * @param array $elements
292
     *
293
     * @return array
294
     */
295
    private function parseArray($elements)
296
    {
297
        $array = [];
298
        foreach ($elements as $element) {
299
            $array[] = $this->parseValue($element);
300
        }
301
302
        return $array;
303
    }
304
305
    /**
306
     * @param string $identifier
307
     *
308
     * @return Field
309
     * @throws UQLInterpreterException
310
     */
311
    private function matchDataSourceElement($identifier)
312
    {
313
        if (!$this->caseSensitive) {
314
            $identifier = strtolower($identifier);
315
        }
316
317
        if (!isset($this->dataSourceElements[$identifier])) {
318
            throw new UQLInterpreterException('Unknown filtering element "' . $identifier . '"');
319
        }
320
321
        return $this->dataSourceElements[$identifier];
322
    }
323
324
    /**
325
     * @param ASTAssertion $astSubtree
326
     *
327
     * @throws UQLInterpreterException
328
     *
329
     * @return array|mixed
330
     */
331
    private function getValue(ASTAssertion $astSubtree)
332
    {
333
        if ($astSubtree->getValue() instanceof ASTFunctionCall) {
334
            return $this->callFunction($astSubtree->getValue());
335
        }
336
337
        if ($astSubtree->getOperator() == 'T_OP_IN') {
338
            if (!($astSubtree->getValue() instanceof ASTArray)) {
339
                throw new UQLInterpreterException('Only arrays are valid arguments for IN statements');
340
            }
341
342
            return $this->parseArray($astSubtree->getValue()->getElements());
343
        }
344
345
        return $this->parseValue($astSubtree->getValue());
346
    }
347
348
    /**
349
     * @param ASTAssertion $astSubtree
350
     *
351
     * @throws UQLInterpreterException
352
     *
353
     * @return FilterCondition
354
     */
355
    private function buildFilterConditionFromASTAssertion(ASTAssertion $astSubtree)
356
    {
357
        $field = $this->matchDataSourceElement($astSubtree->getIdentifier());
358
        $method = $this->translateOperator($astSubtree->getOperator(), $field);
359
        $value = $this->getValue($astSubtree);
360
361
        return $this->filterConditionFactory->create($field, $method, $value);
362
    }
363
364
    /**
365
     * @param ASTGroup $astSubtree
366
     *
367
     * @throws UQLInterpreterException
368
     * @throws \Exception
369
     *
370
     * @return Filter
371
     */
372
    private function buildFilterFromASTGroup(ASTGroup $astSubtree)
373
    {
374
        $filter = new Filter();
375
        $condition = $this->translateLogic($astSubtree->getLogic());
376
        $filter->setConditionType($condition);
377
        foreach ($astSubtree->getElements() as $element) {
378
            if ($element instanceof ASTGroup) {
379
                $filter[] = $this->buildFilterFromASTGroup($element);
380
            }
381
            if ($element instanceof ASTAssertion) {
382
                $filter[] = $this->buildFilterConditionFromASTAssertion($element);
383
            }
384
        }
385
386
        return $filter;
387
    }
388
}
389