Completed
Pull Request — master (#12)
by
unknown
14:02
created

Interpreter::getValue()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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