Completed
Push — master ( f7f008...2d0f6c )
by
unknown
02:21
created

Interpreter::getFunctionArguments()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
rs 9.4285
cc 2
eloc 6
nc 2
nop 2
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_BOOLEAN => "is",
135
            FilterCondition::METHOD_IS_NULL => "is null",
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
        ];
227
228
        if (!isset($translationTable[$token])) {
229
            throw new UQLInterpreterException('Unable to translate token ' . $token . ' to a valid filtering method. Unknown token.');
230
        }
231
        $possibleMethods = $translationTable[$token];
232
233
        // See if any of the methods is the default of the data type
234
        $dataType = $dataSourceElement->getDataType();
235
        foreach ($possibleMethods as $possibleMethod) {
236
            if ($possibleMethod === $dataType->getDefaultFilterMethod()) {
237
                return $possibleMethod;
238
            }
239
        }
240
241
        // Else, just accept the first one in the available methods
242
        foreach ($possibleMethods as $possibleMethod) {
243
            if (in_array($possibleMethod, $dataType->getAvailableFilterMethods())) {
244
                return $possibleMethod;
245
            }
246
        }
247
248
        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() . '"');
249
    }
250
251
    /**
252
     * Translate Lexer <logic> tokens into Filter Condition Types.
253
     *
254
     * @param $token
255
     *
256
     * @return string
257
     * @throws \Exception
258
     */
259
    private function translateLogic($token)
260
    {
261
        $translationTable = [
262
            "T_LOGIC_AND" => Filter::CONDITION_TYPE_AND,
263
            "T_LOGIC_OR" => Filter::CONDITION_TYPE_OR,
264
            "T_LOGIC_XOR" => Filter::CONDITION_TYPE_XOR,
265
        ];
266
267
        if (isset($translationTable[$token])) {
268
            return $translationTable[$token];
269
        }
270
271
        throw new \Exception('Unable to translate token ' . $token . ' to a valid filter condition type.');
272
    }
273
274
    /**
275
     * Trim and clean up the value to be set in the filter.
276
     *
277
     * @param mixed $value
278
     *
279
     * @return mixed
280
     */
281
    private function parseValue($value)
282
    {
283
        if (is_bool($value)) {
284
            return $value ? "1" : "0";
285
        }
286
287
        return trim($value, "\"'");
288
    }
289
290
    /**
291
     * @param ASTFunctionCall $functionCall
292
     *
293
     * @return mixed
294
     * @throws FunctionNotFoundException
295
     * @throws UQLInterpreterException
296
     */
297
    private function callFunction(ASTFunctionCall $functionCall)
298
    {
299
        $functionName = $functionCall->getFunctionName();
300
        $function = $this->extensionContainer->getFunction($functionName);
301
        $arguments = $this->getFunctionArguments($functionCall, $function);
302
303
        try {
304
            return $function->call($arguments);
305
        } catch (\Exception $e) {
306
            throw new UQLInterpreterException("The execution of function '$functionName' failed. Please check the arguments are valid. (" . $e->getMessage() . ")");
307
        }
308
    }
309
310
    /**
311
     * @param array $elements
312
     *
313
     * @return array
314
     */
315
    private function parseArray($elements)
316
    {
317
        $array = [];
318
        foreach ($elements as $element) {
319
            $array[] = $this->parseValue($element);
320
        }
321
322
        return $array;
323
    }
324
325
    /**
326
     * @param string $identifier
327
     *
328
     * @return Field
329
     * @throws UQLInterpreterException
330
     */
331
    private function matchDataSourceElement($identifier)
332
    {
333
        if (!$this->caseSensitive) {
334
            $identifier = strtolower($identifier);
335
        }
336
337
        if (!isset($this->dataSourceElements[$identifier])) {
338
            throw new UQLInterpreterException('Unknown filtering element "' . $identifier . '"');
339
        }
340
341
        return $this->dataSourceElements[$identifier];
342
    }
343
344
    /**
345
     * @param ASTAssertion $astSubtree
346
     *
347
     * @throws UQLInterpreterException
348
     *
349
     * @return array|mixed
350
     */
351
    private function getValue(ASTAssertion $astSubtree)
352
    {
353
        if ($astSubtree->getValue() instanceof ASTFunctionCall) {
354
            return $this->callFunction($astSubtree->getValue());
355
        }
356
357
        if ($astSubtree->getOperator() == 'T_OP_IN') {
358
            if (!($astSubtree->getValue() instanceof ASTArray)) {
359
                throw new UQLInterpreterException('Only arrays are valid arguments for IN statements');
360
            }
361
362
            return $this->parseArray($astSubtree->getValue()->getElements());
363
        }
364
365
        return $this->parseValue($astSubtree->getValue());
366
    }
367
368
    /**
369
     * @param ASTAssertion $astSubtree
370
     *
371
     * @throws UQLInterpreterException
372
     *
373
     * @return FilterCondition
374
     */
375
    private function buildFilterConditionFromASTAssertion(ASTAssertion $astSubtree)
376
    {
377
        $field = $this->matchDataSourceElement($astSubtree->getIdentifier());
378
        $method = $this->translateOperator($astSubtree->getOperator(), $field);
379
        $value = $this->getValue($astSubtree);
380
381
        return $this->filterConditionFactory->create($field, $method, $value);
382
    }
383
384
    /**
385
     * @param ASTGroup $astSubtree
386
     *
387
     * @throws UQLInterpreterException
388
     * @throws \Exception
389
     *
390
     * @return Filter
391
     */
392
    private function buildFilterFromASTGroup(ASTGroup $astSubtree)
393
    {
394
        $filter = new Filter();
395
        $condition = $this->translateLogic($astSubtree->getLogic());
396
        $filter->setConditionType($condition);
397
        foreach ($astSubtree->getElements() as $element) {
398
            if ($element instanceof ASTGroup) {
399
                $filter[] = $this->buildFilterFromASTGroup($element);
400
            }
401
            if ($element instanceof ASTAssertion) {
402
                $filter[] = $this->buildFilterConditionFromASTAssertion($element);
403
            }
404
        }
405
406
        return $filter;
407
    }
408
409
    /**
410
     * @param ASTFunctionCall      $functionCall
411
     * @param UqlFunctionInterface $function
412
     *
413
     * @return array
414
     */
415
    private function getFunctionArguments(ASTFunctionCall $functionCall, $function)
416
    {
417
        $arguments = $functionCall->getArguments();
418
419
        if ($function instanceof ContextAwareUqlFunction) {
420
            $context = $this->contextFactory->create($this->dataSource->getEntityClass());
421
            array_unshift($arguments, $context);
422
        }
423
424
        return $arguments;
425
    }
426
}
427