Completed
Pull Request — master (#12)
by
unknown
11:33
created

Interpreter::translateOperator()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 59
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 59
rs 8.7117
cc 6
eloc 37
nc 8
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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