Completed
Pull Request — master (#12)
by
unknown
10:27 queued 08:52
created

Interpreter::getValue()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 16
rs 9.2
cc 4
eloc 8
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\DataType\PercentDataType;
9
use Netdudes\DataSourceryBundle\Extension\UqlExtensionContainer;
10
use Netdudes\DataSourceryBundle\Query\Filter;
11
use Netdudes\DataSourceryBundle\Query\FilterCondition;
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
     * Constructor needs the columns descriptor to figure out appropriate filtering methods
48
     * and translate identifiers.
49
     *
50
     * @param UqlExtensionContainer $extensionContainer
51
     * @param DataSourceInterface   $dataSource
52
     * @param bool                  $caseSensitive
53
     */
54
    public function __construct(UqlExtensionContainer $extensionContainer, DataSourceInterface $dataSource, $caseSensitive = true)
55
    {
56
        $this->extensionContainer = $extensionContainer;
57
        $this->dataSource = $dataSource;
58
        $this->caseSensitive = $caseSensitive;
59
60
        // Cache an array of data sources (name => object pairs) for reference during the interpretation
61
        $this->dataSourceElements = array_combine(
62
            array_map(
63
                function (FieldInterface $element) use ($caseSensitive) {
64
                    return $caseSensitive ? $element->getUniqueName() : strtolower($element->getUniqueName());
65
                },
66
                $this->dataSource->getFields()
67
            ),
68
            $this->dataSource->getFields()
69
        );
70
    }
71
72
    /**
73
     * Helper method: matches filtering operators to valid UQL operators
74
     * in order to do Filter to UQL transformations
75
     *
76
     * @param $method
77
     *
78
     * @throws Exception\UQLInterpreterException
79
     * @return
80
     */
81
    public static function methodToUQLOperator($method)
82
    {
83
        $translationMap = [
84
            FilterCondition::METHOD_STRING_EQ => "=",
85
            FilterCondition::METHOD_STRING_LIKE => "~",
86
            FilterCondition::METHOD_STRING_NEQ => "!=",
87
            FilterCondition::METHOD_NUMERIC_GT => ">",
88
            FilterCondition::METHOD_NUMERIC_GTE => ">=",
89
            FilterCondition::METHOD_NUMERIC_EQ => "=",
90
            FilterCondition::METHOD_NUMERIC_LTE => "<=",
91
            FilterCondition::METHOD_NUMERIC_LT => "<",
92
            FilterCondition::METHOD_NUMERIC_NEQ => "!=",
93
            FilterCondition::METHOD_IN => "IN",
94
            FilterCondition::METHOD_BOOLEAN => "is",
95
            FilterCondition::METHOD_IS_NULL => "is null",
96
            FilterCondition::METHOD_DATETIME_GT => "after",
97
            FilterCondition::METHOD_DATETIME_GTE => "after or at",
98
            FilterCondition::METHOD_DATETIME_EQ => "at",
99
            FilterCondition::METHOD_DATETIME_LTE => "before or at",
100
            FilterCondition::METHOD_DATETIME_LT => "before",
101
            FilterCondition::METHOD_DATETIME_NEQ => "not at",
102
        ];
103
104
        if (isset($translationMap[$method])) {
105
            return $translationMap[$method];
106
        }
107
108
        throw new UQLInterpreterException("Can't translate filtering method '$method'' into a valid UQL operator");
109
    }
110
111
    /**
112
     * Generate the filter objects corresponding to a UQL string.
113
     *
114
     * @param $UQLStringInput
115
     *
116
     * @return Filter
117
     */
118
    public function generateFilters($UQLStringInput)
119
    {
120
        if (empty(trim($UQLStringInput))) {
121
            return new Filter();
122
        }
123
124
        // Get the Abstract Syntax Tree of the input from the parser
125
        $parser = new Parser();
126
        $AST = $parser->parse($UQLStringInput);
127
128
        // Recursively translate into filters.
129
        $filters = $this->buildFilterLevel($AST);
130
131
        if ($filters instanceof FilterCondition) {
132
            // Single filter. Wrap into dummy filter collection for consistency.
133
            $filterDefinition = new Filter();
134
            $filterDefinition[] = $filters;
135
            $filters = $filterDefinition;
136
        }
137
138
        return $filters;
139
    }
140
141
    /**
142
     * Transforms a subtree of the AST into a concrete filter definition.
143
     * This function recursively builds all sub-trees.
144
     *
145
     * @param $astSubtree
146
     *
147
     * @return Filter|Filter
148
     * @throws \Exception
149
     */
150
    public function buildFilterLevel($astSubtree)
151
    {
152
        if ($astSubtree instanceof ASTGroup) {
153
            $filterDefinition = new Filter();
154
            $condition = $this->translateLogic($astSubtree->getLogic());
155
            $filterDefinition->setConditionType($condition);
156
            foreach ($astSubtree->getElements() as $element) {
157
                $filterDefinition[] = $this->buildFilterLevel($element);
158
            }
159
160
            return $filterDefinition;
161
        } elseif ($astSubtree instanceof ASTAssertion) {
162
            return $this->getFilterCondition($astSubtree);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getFilterCondition($astSubtree); (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...
163
        }
164
165
        throw new UQLInterpreterException('Unexpected Abstract Syntax Tree element');
166
    }
167
168
    /**
169
     * Translate <operator> tokens into Filter Methods.
170
     *
171
     * @param                          $token
172
     * @param FieldInterface $dataSourceElement
173
     *
174
     * @throws Exception\UQLInterpreterException
175
     * @return mixed
176
     */
177
    public function translateOperator($token, FieldInterface $dataSourceElement)
178
    {
179
        $translationTable = [
180
            "T_OP_LT" => [
181
                FilterCondition::METHOD_NUMERIC_LT,
182
                FilterCondition::METHOD_DATETIME_LT,
183
            ],
184
            "T_OP_LTE" => [
185
                FilterCondition::METHOD_NUMERIC_LTE,
186
                FilterCondition::METHOD_DATETIME_LTE,
187
            ],
188
            "T_OP_EQ" => [
189
                FilterCondition::METHOD_NUMERIC_EQ,
190
                FilterCondition::METHOD_STRING_EQ,
191
                FilterCondition::METHOD_DATETIME_EQ,
192
                FilterCondition::METHOD_BOOLEAN,
193
            ],
194
            "T_OP_GTE" => [
195
                FilterCondition::METHOD_NUMERIC_GTE,
196
                FilterCondition::METHOD_DATETIME_GTE,
197
            ],
198
            "T_OP_GT" => [
199
                FilterCondition::METHOD_NUMERIC_GT,
200
                FilterCondition::METHOD_DATETIME_GT,
201
            ],
202
            "T_OP_NEQ" => [
203
                FilterCondition::METHOD_NUMERIC_NEQ,
204
                FilterCondition::METHOD_STRING_NEQ,
205
            ],
206
            "T_OP_LIKE" => [
207
                FilterCondition::METHOD_STRING_LIKE,
208
            ],
209
            "T_OP_IN" => [
210
                FilterCondition::METHOD_IN,
211
            ],
212
        ];
213
214
        if (!isset($translationTable[$token])) {
215
            throw new UQLInterpreterException('Unable to translate token ' . $token . ' to a valid filtering method. Unknown token.');
216
        }
217
        $possibleMethods = $translationTable[$token];
218
219
        // See if any of the methods is the default of the data type
220
        $dataType = $dataSourceElement->getDataType();
221
        foreach ($possibleMethods as $possibleMethod) {
222
            if ($possibleMethod === $dataType->getDefaultFilterMethod()) {
223
                return $possibleMethod;
224
            }
225
        }
226
227
        // Else, just accept the first one in the available methods
228
        foreach ($possibleMethods as $possibleMethod) {
229
            if (in_array($possibleMethod, $dataType->getAvailableFilterMethods())) {
230
                return $possibleMethod;
231
            }
232
        }
233
234
        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() . '"');
235
    }
236
237
    /**
238
     * Translate Lexer <logic> tokens into Filter Condition Types.
239
     *
240
     * @param $token
241
     *
242
     * @return mixed
243
     * @throws \Exception
244
     */
245
    protected function translateLogic($token)
246
    {
247
        $translationTable = [
248
            "T_LOGIC_AND" => Filter::CONDITION_TYPE_AND,
249
            "T_LOGIC_OR" => Filter::CONDITION_TYPE_OR,
250
            "T_LOGIC_XOR" => Filter::CONDITION_TYPE_XOR,
251
        ];
252
253
        if (isset($translationTable[$token])) {
254
            return $translationTable[$token];
255
        }
256
257
        throw new \Exception('Unable to translate token ' . $token . ' to a valid filter condition type.');
258
    }
259
260
    /**
261
     * Trim and clean up the value to be set in the filter.
262
     *
263
     * @param $value
264
     *
265
     * @return mixed
266
     */
267
    protected function parseValue($value)
268
    {
269
        if (is_bool($value)) {
270
            return $value ? "1" : "0";
271
        }
272
273
        return trim($value, "\"'");
274
    }
275
276
    /**
277
     * @param ASTFunctionCall $functionCall
278
     *
279
     * @return mixed
280
     * @throws Exception\UQLInterpreterException
281
     */
282
    private function callFunction(ASTFunctionCall $functionCall)
283
    {
284
        try {
285
            return $this->extensionContainer->callFunction($functionCall->getFunctionName(), $functionCall->getArguments());
286
        } catch (\Exception $e) {
287
            throw new UQLInterpreterException("The execution of function '" . $functionCall->getFunctionName() . "' failed. Please check the arguments are valid. (" . $e->getMessage() . ")");
288
        }
289
    }
290
291
    private function parseArray($elements)
292
    {
293
        $array = [];
294
        foreach ($elements as $element) {
295
            $array[] = $this->parseValue($element);
296
        }
297
298
        return $array;
299
    }
300
301
    /**
302
     * @param $identifier
303
     *
304
     * @return Field
305
     * @throws UQLInterpreterException
306
     */
307
    private function matchDataSourceElement($identifier)
308
    {
309
        if (!$this->caseSensitive) {
310
            $identifier = strtolower($identifier);
311
        }
312
313
        if (!isset($this->dataSourceElements[$identifier])) {
314
            throw new UQLInterpreterException('Unknown filtering element "' . $identifier . '"');
315
        }
316
317
        return $this->dataSourceElements[$identifier];
318
    }
319
320
    /**
321
     * @param ASTAssertion $astSubtree
322
     *
323
     * @return array|mixed
324
     *
325
     * @throws UQLInterpreterException
326
     */
327
    private function getValue(ASTAssertion $astSubtree)
328
    {
329
        if ($astSubtree->getValue() instanceof ASTFunctionCall) {
330
            return $this->callFunction($astSubtree->getValue());
331
        }
332
333
        if ($astSubtree->getOperator() == 'T_OP_IN') {
334
            if (!($astSubtree->getValue() instanceof ASTArray)) {
335
                throw new UQLInterpreterException('Only arrays are valid arguments for IN statements');
336
            }
337
338
            return $this->parseArray($astSubtree->getValue()->getElements());
339
        }
340
341
        return $this->parseValue($astSubtree->getValue());
342
    }
343
344
    /**
345
     * @param ASTAssertion $astSubtree
346
     *
347
     * @return FilterCondition
348
     *
349
     * @throws UQLInterpreterException
350
     */
351
    private function getFilterCondition(ASTAssertion $astSubtree)
352
    {
353
        $dataSourceElement = $this->matchDataSourceElement($astSubtree->getIdentifier());
354
        $method = $this->translateOperator($astSubtree->getOperator(), $dataSourceElement);
355
        $value = $this->getValue($astSubtree);
356
357
        $valueInDatabase = $this->workoutDatabaseValue($value, $dataSourceElement);
358
359
        return new FilterCondition($dataSourceElement->getUniqueName(), $method, $value, $valueInDatabase);
360
    }
361
362
    /**
363
     * @param string $value
364
     * @param Field  $dataSourceElement
365
     *
366
     * @return string
367
     */
368
    private function workoutDatabaseValue($value, Field $dataSourceElement)
369
    {
370
        $valueInDatabase = $value;
371
372
        $dataType = $dataSourceElement->getDataType();
373
        if ($dataType instanceof PercentDataType) {
374
            $valueInDatabase = $this->identifyPercentDataBaseValue($value);
375
        }
376
377
        return $valueInDatabase;
378
    }
379
380
    /**
381
     * @param string $value
382
     *
383
     * @return string
384
     */
385
    private function identifyPercentDataBaseValue($value)
386
    {
387
        $dividedByHundredScale = 2;
388
        $positionOfSeparator = strpos($value, '.') + 1;
389
        $digitsAfterSeparator = substr($value, -(strlen($value) - ($positionOfSeparator)));
390
        $scale = strlen($digitsAfterSeparator) + $dividedByHundredScale;
391
392
        return bcdiv($value, 100, $scale);
393
    }
394
}
395