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); |
|
|
|
|
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
|
|
|
|
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:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.