Completed
Pull Request — master (#22)
by
unknown
05:18 queued 01:16
created

Interpreter::parseValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
rs 9.4285
cc 3
eloc 4
nc 3
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_IS_NULL => "is null",
137
            FilterCondition::METHOD_DATETIME_GT => "after",
138
            FilterCondition::METHOD_DATETIME_GTE => "after or at",
139
            FilterCondition::METHOD_DATETIME_EQ => "at",
140
            FilterCondition::METHOD_DATETIME_LTE => "before or at",
141
            FilterCondition::METHOD_DATETIME_LT => "before",
142
            FilterCondition::METHOD_DATETIME_NEQ => "not at",
143
        ];
144
145
        if (isset($translationMap[$method])) {
146
            return $translationMap[$method];
147
        }
148
149
        throw new UQLInterpreterException("Can't translate filtering method '$method'' into a valid UQL operator");
150
    }
151
152
    /**
153
     * Transforms a subtree of the AST into a concrete filter definition.
154
     * This function recursively builds all sub-trees.
155
     *
156
     * @param ASTGroup|ASTAssertion|mixed $astSubtree
157
     *
158
     * TODO: This looks like it should not be public (it is only used in tests).
159
     * We could move it and it's dependencies to its own class so that it can be tested
160
     *
161
     * @return Filter
162
     * @throws \Exception
163
     */
164
    public function buildFilter($astSubtree)
165
    {
166
        if ($astSubtree instanceof ASTGroup) {
167
            return $this->buildFilterFromASTGroup($astSubtree);
168
        }
169
170
        if ($astSubtree instanceof ASTAssertion) {
171
            $filterCondition = $this->buildFilterConditionFromASTAssertion($astSubtree);
172
            // Single filter. Wrap into dummy filter collection for consistency.
173
            $filter = new Filter();
174
            $filter[] = $filterCondition;
175
176
            return $filter;
177
        }
178
179
        throw new UQLInterpreterException('Unexpected Abstract Syntax Tree element');
180
    }
181
182
    /**
183
     * Translate <operator> tokens into Filter Methods.
184
     *
185
     * @param string         $token
186
     * @param FieldInterface $dataSourceElement
187
     *
188
     * @throws UQLInterpreterException
189
     * @return mixed
190
     */
191
    public function translateOperator($token, FieldInterface $dataSourceElement)
192
    {
193
        $translationTable = [
194
            "T_OP_LT" => [
195
                FilterCondition::METHOD_NUMERIC_LT,
196
                FilterCondition::METHOD_DATETIME_LT,
197
            ],
198
            "T_OP_LTE" => [
199
                FilterCondition::METHOD_NUMERIC_LTE,
200
                FilterCondition::METHOD_DATETIME_LTE,
201
            ],
202
            "T_OP_EQ" => [
203
                FilterCondition::METHOD_NUMERIC_EQ,
204
                FilterCondition::METHOD_STRING_EQ,
205
                FilterCondition::METHOD_DATETIME_EQ,
206
                FilterCondition::METHOD_BOOLEAN,
207
            ],
208
            "T_OP_GTE" => [
209
                FilterCondition::METHOD_NUMERIC_GTE,
210
                FilterCondition::METHOD_DATETIME_GTE,
211
            ],
212
            "T_OP_GT" => [
213
                FilterCondition::METHOD_NUMERIC_GT,
214
                FilterCondition::METHOD_DATETIME_GT,
215
            ],
216
            "T_OP_NEQ" => [
217
                FilterCondition::METHOD_NUMERIC_NEQ,
218
                FilterCondition::METHOD_STRING_NEQ,
219
                FilterCondition::METHOD_DATETIME_NEQ,
220
            ],
221
            "T_OP_LIKE" => [
222
                FilterCondition::METHOD_STRING_LIKE,
223
            ],
224
            "T_OP_IN" => [
225
                FilterCondition::METHOD_IN,
226
            ],
227
            "T_OP_NIN" => [
228
                FilterCondition::METHOD_NIN,
229
            ],
230
        ];
231
232
        if (!isset($translationTable[$token])) {
233
            throw new UQLInterpreterException('Unable to translate token ' . $token . ' to a valid filtering method. Unknown token.');
234
        }
235
        $possibleMethods = $translationTable[$token];
236
237
        // See if any of the methods is the default of the data type
238
        $dataType = $dataSourceElement->getDataType();
239
        foreach ($possibleMethods as $possibleMethod) {
240
            if ($possibleMethod === $dataType->getDefaultFilterMethod()) {
241
                return $possibleMethod;
242
            }
243
        }
244
245
        // Else, just accept the first one in the available methods
246
        foreach ($possibleMethods as $possibleMethod) {
247
            if (in_array($possibleMethod, $dataType->getAvailableFilterMethods())) {
248
                return $possibleMethod;
249
            }
250
        }
251
252
        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...
253
    }
254
255
    /**
256
     * Translate Lexer <logic> tokens into Filter Condition Types.
257
     *
258
     * @param $token
259
     *
260
     * @return string
261
     * @throws \Exception
262
     */
263
    private function translateLogic($token)
264
    {
265
        $translationTable = [
266
            "T_LOGIC_AND" => Filter::CONDITION_TYPE_AND,
267
            "T_LOGIC_OR" => Filter::CONDITION_TYPE_OR,
268
            "T_LOGIC_XOR" => Filter::CONDITION_TYPE_XOR,
269
        ];
270
271
        if (isset($translationTable[$token])) {
272
            return $translationTable[$token];
273
        }
274
275
        throw new \Exception('Unable to translate token ' . $token . ' to a valid filter condition type.');
276
    }
277
278
    /**
279
     * Trim and clean up the value to be set in the filter.
280
     *
281
     * @param mixed $value
282
     *
283
     * @return mixed
284
     */
285
    private function parseValue($value)
286
    {
287
        if (is_bool($value)) {
288
            return $value ? "1" : "0";
289
        }
290
291
        return trim($value, "\"'");
292
    }
293
294
    /**
295
     * @param ASTFunctionCall $functionCall
296
     *
297
     * @return mixed
298
     * @throws FunctionNotFoundException
299
     * @throws UQLInterpreterException
300
     */
301
    private function callFunction(ASTFunctionCall $functionCall)
302
    {
303
        $functionName = $functionCall->getFunctionName();
304
        $function = $this->extensionContainer->getFunction($functionName);
305
        $arguments = $this->getFunctionArguments($functionCall, $function);
306
307
        try {
308
            return $function->call($arguments);
309
        } catch (\Exception $e) {
310
            throw new UQLInterpreterException("The execution of function '$functionName' failed. Please check the arguments are valid. (" . $e->getMessage() . ")");
311
        }
312
    }
313
314
    /**
315
     * @param array $elements
316
     *
317
     * @return array
318
     */
319
    private function parseArray($elements)
320
    {
321
        $array = [];
322
        foreach ($elements as $element) {
323
            $array[] = $this->parseValue($element);
324
        }
325
326
        return $array;
327
    }
328
329
    /**
330
     * @param string $identifier
331
     *
332
     * @return Field
333
     * @throws UQLInterpreterException
334
     */
335
    private function matchDataSourceElement($identifier)
336
    {
337
        if (!$this->caseSensitive) {
338
            $identifier = strtolower($identifier);
339
        }
340
341
        if (!isset($this->dataSourceElements[$identifier])) {
342
            throw new UQLInterpreterException('Unknown filtering element "' . $identifier . '"');
343
        }
344
345
        return $this->dataSourceElements[$identifier];
346
    }
347
348
    /**
349
     * @param ASTAssertion $astSubtree
350
     *
351
     * @throws UQLInterpreterException
352
     *
353
     * @return array|mixed
354
     */
355
    private function getValue(ASTAssertion $astSubtree)
356
    {
357
        $value = $astSubtree->getValue();
358
        $operator = $astSubtree->getOperator();
359
360
        if ($value instanceof ASTFunctionCall) {
361
            return $this->callFunction($value);
362
        }
363
364
        if (in_array($operator, ['T_OP_IN', 'T_OP_NIN'])) {
365
            if (!($value instanceof ASTArray)) {
366
                throw new UQLInterpreterException('Only arrays are valid arguments for IN / NOT IN statements');
367
            }
368
369
            return $this->parseArray($value->getElements());
370
        }
371
372
        if (null === $value) {
373
            if (!in_array($operator, ['T_OP_EQ', 'T_OP_NEQ'])) {
374
                throw new UQLInterpreterException('Only IS / IS NOT operator can be used to compare against null value');
375
            }
376
377
            return null;
378
        }
379
380
        return $this->parseValue($value);
381
    }
382
383
    /**
384
     * @param ASTAssertion $astSubtree
385
     *
386
     * @throws UQLInterpreterException
387
     *
388
     * @return FilterCondition
389
     */
390
    private function buildFilterConditionFromASTAssertion(ASTAssertion $astSubtree)
391
    {
392
        $field = $this->matchDataSourceElement($astSubtree->getIdentifier());
393
        $method = $this->translateOperator($astSubtree->getOperator(), $field);
394
        $value = $this->getValue($astSubtree);
395
396
        return $this->filterConditionFactory->create($field, $method, $value);
397
    }
398
399
    /**
400
     * @param ASTGroup $astSubtree
401
     *
402
     * @throws UQLInterpreterException
403
     * @throws \Exception
404
     *
405
     * @return Filter
406
     */
407
    private function buildFilterFromASTGroup(ASTGroup $astSubtree)
408
    {
409
        $filter = new Filter();
410
        $condition = $this->translateLogic($astSubtree->getLogic());
411
        $filter->setConditionType($condition);
412
        foreach ($astSubtree->getElements() as $element) {
413
            if ($element instanceof ASTGroup) {
414
                $filter[] = $this->buildFilterFromASTGroup($element);
415
            }
416
            if ($element instanceof ASTAssertion) {
417
                $filter[] = $this->buildFilterConditionFromASTAssertion($element);
418
            }
419
        }
420
421
        return $filter;
422
    }
423
424
    /**
425
     * @param ASTFunctionCall      $functionCall
426
     * @param UqlFunctionInterface $function
427
     *
428
     * @return array
429
     */
430
    private function getFunctionArguments(ASTFunctionCall $functionCall, $function)
431
    {
432
        $arguments = $functionCall->getArguments();
433
434
        if ($function instanceof ContextAwareUqlFunction) {
435
            $context = $this->contextFactory->create($this->dataSource->getEntityClass());
436
            array_unshift($arguments, $context);
437
        }
438
439
        return $arguments;
440
    }
441
}
442