Completed
Pull Request — master (#12)
by
unknown
13:44
created

Interpreter::buildFilterLevel()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 3
Metric Value
c 5
b 0
f 3
dl 0
loc 21
rs 9.0534
cc 4
eloc 14
nc 4
nop 1
1
<?php
2
3
namespace Netdudes\DataSourceryBundle\UQL;
4
5
use Netdudes\DataSourceryBundle\DataSource\Configuration\Field;
6
use Netdudes\DataSourceryBundle\DataSource\Configuration\FieldInterface;
7
use Netdudes\DataSourceryBundle\DataSource\DataSourceInterface;
8
use Netdudes\DataSourceryBundle\Extension\UqlExtensionContainer;
9
use Netdudes\DataSourceryBundle\Query\Filter;
10
use Netdudes\DataSourceryBundle\Query\FilterCondition;
11
use Netdudes\DataSourceryBundle\Query\FilterConditionFactory;
12
use Netdudes\DataSourceryBundle\UQL\AST\ASTArray;
13
use Netdudes\DataSourceryBundle\UQL\AST\ASTAssertion;
14
use Netdudes\DataSourceryBundle\UQL\AST\ASTFunctionCall;
15
use Netdudes\DataSourceryBundle\UQL\AST\ASTGroup;
16
use Netdudes\DataSourceryBundle\UQL\Exception\UQLInterpreterException;
17
18
/**
19
 * Class Interpreter
20
 *
21
 * The Interpreter transforms the generic Abstract Syntax Tree into
22
 * the specific FilterDefinition elements.
23
 */
24
class Interpreter
25
{
26
    /**
27
     * @var UqlExtensionContainer
28
     */
29
    private $extensionContainer;
30
31
    /**
32
     * @var DataSourceInterface
33
     */
34
    private $dataSource;
35
36
    /**
37
     * @var array
38
     */
39
    private $dataSourceElements;
40
41
    /**
42
     * @var bool
43
     */
44
    private $caseSensitive;
45
46
    /**
47
     * @var FilterConditionFactory
48
     */
49
    private $filterConditionFactory;
50
51
    /**
52
     * Constructor needs the columns descriptor to figure out appropriate filtering methods
53
     * and translate identifiers.
54
     *
55
     * @param UqlExtensionContainer  $extensionContainer
56
     * @param DataSourceInterface    $dataSource
57
     * @param bool                   $caseSensitive
58
     * @param FilterConditionFactory $filterConditionFactory
59
     */
60
    public function __construct(
61
        UqlExtensionContainer $extensionContainer,
62
        DataSourceInterface $dataSource,
63
        FilterConditionFactory $filterConditionFactory,
64
        $caseSensitive = true
65
    ) {
66
        $this->extensionContainer = $extensionContainer;
67
        $this->dataSource = $dataSource;
68
        $this->caseSensitive = $caseSensitive;
69
70
        $this->filterConditionFactory = $filterConditionFactory;
71
72
        // Cache an array of data sources (name => object pairs) for reference during the interpretation
73
        $this->dataSourceElements = array_combine(
74
            array_map(
75
                function (FieldInterface $element) use ($caseSensitive) {
76
                    return $caseSensitive ? $element->getUniqueName() : strtolower($element->getUniqueName());
77
                },
78
                $this->dataSource->getFields()
79
            ),
80
            $this->dataSource->getFields()
81
        );
82
    }
83
84
    /**
85
     * Helper method: matches filtering operators to valid UQL operators
86
     * in order to do Filter to UQL transformations
87
     *
88
     * @param $method
89
     *
90
     * @throws Exception\UQLInterpreterException
91
     * @return
92
     */
93
    public static function methodToUQLOperator($method)
94
    {
95
        $translationMap = [
96
            FilterCondition::METHOD_STRING_EQ => "=",
97
            FilterCondition::METHOD_STRING_LIKE => "~",
98
            FilterCondition::METHOD_STRING_NEQ => "!=",
99
            FilterCondition::METHOD_NUMERIC_GT => ">",
100
            FilterCondition::METHOD_NUMERIC_GTE => ">=",
101
            FilterCondition::METHOD_NUMERIC_EQ => "=",
102
            FilterCondition::METHOD_NUMERIC_LTE => "<=",
103
            FilterCondition::METHOD_NUMERIC_LT => "<",
104
            FilterCondition::METHOD_NUMERIC_NEQ => "!=",
105
            FilterCondition::METHOD_IN => "IN",
106
            FilterCondition::METHOD_BOOLEAN => "is",
107
            FilterCondition::METHOD_IS_NULL => "is null",
108
            FilterCondition::METHOD_DATETIME_GT => "after",
109
            FilterCondition::METHOD_DATETIME_GTE => "after or at",
110
            FilterCondition::METHOD_DATETIME_EQ => "at",
111
            FilterCondition::METHOD_DATETIME_LTE => "before or at",
112
            FilterCondition::METHOD_DATETIME_LT => "before",
113
            FilterCondition::METHOD_DATETIME_NEQ => "not at",
114
        ];
115
116
        if (isset($translationMap[$method])) {
117
            return $translationMap[$method];
118
        }
119
120
        throw new UQLInterpreterException("Can't translate filtering method '$method'' into a valid UQL operator");
121
    }
122
123
    /**
124
     * Generate the filter objects corresponding to a UQL string.
125
     *
126
     * @param $UQLStringInput
127
     *
128
     * @return Filter
129
     */
130
    public function generateFilters($UQLStringInput)
131
    {
132
        if (empty(trim($UQLStringInput))) {
133
            return new Filter();
134
        }
135
136
        // Get the Abstract Syntax Tree of the input from the parser
137
        $parser = new Parser();
138
        $AST = $parser->parse($UQLStringInput);
139
140
        // Recursively translate into filters.
141
        $filters = $this->buildFilterLevel($AST);
142
143
        if ($filters instanceof FilterCondition) {
144
            // Single filter. Wrap into dummy filter collection for consistency.
145
            $filterDefinition = new Filter();
146
            $filterDefinition[] = $filters;
147
            $filters = $filterDefinition;
148
        }
149
150
        return $filters;
151
    }
152
153
    /**
154
     * Transforms a subtree of the AST into a concrete filter definition.
155
     * This function recursively builds all sub-trees.
156
     *
157
     * @param $astSubtree
158
     *
159
     * @return Filter|Filter
160
     * @throws \Exception
161
     */
162
    public function buildFilterLevel($astSubtree)
163
    {
164
        if ($astSubtree instanceof ASTGroup) {
165
            $filterDefinition = new Filter();
166
            $condition = $this->translateLogic($astSubtree->getLogic());
167
            $filterDefinition->setConditionType($condition);
168
            foreach ($astSubtree->getElements() as $element) {
169
                $filterDefinition[] = $this->buildFilterLevel($element);
170
            }
171
172
            return $filterDefinition;
173
        } elseif ($astSubtree instanceof ASTAssertion) {
174
            $field = $this->matchDataSourceElement($astSubtree->getIdentifier());
175
            $method = $this->translateOperator($astSubtree->getOperator(), $field);
176
            $value = $this->getValue($astSubtree);
177
178
            return $this->filterConditionFactory->create($value, $method, $field);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->filterCond...alue, $method, $field); (Netdudes\DataSourceryBundle\Query\FilterCondition) is incompatible with the return type documented by Netdudes\DataSourceryBun...reter::buildFilterLevel of type Netdudes\DataSourceryBundle\Query\Filter.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
179
        }
180
181
        throw new UQLInterpreterException('Unexpected Abstract Syntax Tree element');
182
    }
183
184
    /**
185
     * Translate <operator> tokens into Filter Methods.
186
     *
187
     * @param                          $token
188
     * @param FieldInterface $dataSourceElement
189
     *
190
     * @throws Exception\UQLInterpreterException
191
     * @return mixed
192
     */
193
    public function translateOperator($token, FieldInterface $dataSourceElement)
194
    {
195
        $translationTable = [
196
            "T_OP_LT" => [
197
                FilterCondition::METHOD_NUMERIC_LT,
198
                FilterCondition::METHOD_DATETIME_LT,
199
            ],
200
            "T_OP_LTE" => [
201
                FilterCondition::METHOD_NUMERIC_LTE,
202
                FilterCondition::METHOD_DATETIME_LTE,
203
            ],
204
            "T_OP_EQ" => [
205
                FilterCondition::METHOD_NUMERIC_EQ,
206
                FilterCondition::METHOD_STRING_EQ,
207
                FilterCondition::METHOD_DATETIME_EQ,
208
                FilterCondition::METHOD_BOOLEAN,
209
            ],
210
            "T_OP_GTE" => [
211
                FilterCondition::METHOD_NUMERIC_GTE,
212
                FilterCondition::METHOD_DATETIME_GTE,
213
            ],
214
            "T_OP_GT" => [
215
                FilterCondition::METHOD_NUMERIC_GT,
216
                FilterCondition::METHOD_DATETIME_GT,
217
            ],
218
            "T_OP_NEQ" => [
219
                FilterCondition::METHOD_NUMERIC_NEQ,
220
                FilterCondition::METHOD_STRING_NEQ,
221
            ],
222
            "T_OP_LIKE" => [
223
                FilterCondition::METHOD_STRING_LIKE,
224
            ],
225
            "T_OP_IN" => [
226
                FilterCondition::METHOD_IN,
227
            ],
228
        ];
229
230
        if (!isset($translationTable[$token])) {
231
            throw new UQLInterpreterException('Unable to translate token ' . $token . ' to a valid filtering method. Unknown token.');
232
        }
233
        $possibleMethods = $translationTable[$token];
234
235
        // See if any of the methods is the default of the data type
236
        $dataType = $dataSourceElement->getDataType();
237
        foreach ($possibleMethods as $possibleMethod) {
238
            if ($possibleMethod === $dataType->getDefaultFilterMethod()) {
239
                return $possibleMethod;
240
            }
241
        }
242
243
        // Else, just accept the first one in the available methods
244
        foreach ($possibleMethods as $possibleMethod) {
245
            if (in_array($possibleMethod, $dataType->getAvailableFilterMethods())) {
246
                return $possibleMethod;
247
            }
248
        }
249
250
        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() . '"');
251
    }
252
253
    /**
254
     * Translate Lexer <logic> tokens into Filter Condition Types.
255
     *
256
     * @param $token
257
     *
258
     * @return mixed
259
     * @throws \Exception
260
     */
261
    protected function translateLogic($token)
262
    {
263
        $translationTable = [
264
            "T_LOGIC_AND" => Filter::CONDITION_TYPE_AND,
265
            "T_LOGIC_OR" => Filter::CONDITION_TYPE_OR,
266
            "T_LOGIC_XOR" => Filter::CONDITION_TYPE_XOR,
267
        ];
268
269
        if (isset($translationTable[$token])) {
270
            return $translationTable[$token];
271
        }
272
273
        throw new \Exception('Unable to translate token ' . $token . ' to a valid filter condition type.');
274
    }
275
276
    /**
277
     * Trim and clean up the value to be set in the filter.
278
     *
279
     * @param $value
280
     *
281
     * @return mixed
282
     */
283
    protected function parseValue($value)
284
    {
285
        if (is_bool($value)) {
286
            return $value ? "1" : "0";
287
        }
288
289
        return trim($value, "\"'");
290
    }
291
292
    /**
293
     * @param ASTFunctionCall $functionCall
294
     *
295
     * @return mixed
296
     * @throws Exception\UQLInterpreterException
297
     */
298
    private function callFunction(ASTFunctionCall $functionCall)
299
    {
300
        try {
301
            return $this->extensionContainer->callFunction($functionCall->getFunctionName(), $functionCall->getArguments());
302
        } catch (\Exception $e) {
303
            throw new UQLInterpreterException("The execution of function '" . $functionCall->getFunctionName() . "' failed. Please check the arguments are valid. (" . $e->getMessage() . ")");
304
        }
305
    }
306
307
    private function parseArray($elements)
308
    {
309
        $array = [];
310
        foreach ($elements as $element) {
311
            $array[] = $this->parseValue($element);
312
        }
313
314
        return $array;
315
    }
316
317
    /**
318
     * @param $identifier
319
     *
320
     * @return Field
321
     * @throws UQLInterpreterException
322
     */
323
    private function matchDataSourceElement($identifier)
324
    {
325
        if (!$this->caseSensitive) {
326
            $identifier = strtolower($identifier);
327
        }
328
329
        if (!isset($this->dataSourceElements[$identifier])) {
330
            throw new UQLInterpreterException('Unknown filtering element "' . $identifier . '"');
331
        }
332
333
        return $this->dataSourceElements[$identifier];
334
    }
335
336
    /**
337
     * @param ASTAssertion $astSubtree
338
     *
339
     * @return array|mixed
340
     *
341
     * @throws UQLInterpreterException
342
     */
343
    private function getValue(ASTAssertion $astSubtree)
344
    {
345
        if ($astSubtree->getValue() instanceof ASTFunctionCall) {
346
            return $this->callFunction($astSubtree->getValue());
347
        }
348
349
        if ($astSubtree->getOperator() == 'T_OP_IN') {
350
            if (!($astSubtree->getValue() instanceof ASTArray)) {
351
                throw new UQLInterpreterException('Only arrays are valid arguments for IN statements');
352
            }
353
354
            return $this->parseArray($astSubtree->getValue()->getElements());
355
        }
356
357
        return $this->parseValue($astSubtree->getValue());
358
    }
359
}
360