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

Interpreter::getValue()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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