Completed
Pull Request — master (#15)
by
unknown
02:18
created

Interpreter   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 387
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Importance

Changes 14
Bugs 2 Features 3
Metric Value
wmc 37
c 14
b 2
f 3
lcom 1
cbo 14
dl 0
loc 387
rs 8.6

13 Methods

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