Passed
Branch master (950424)
by Christopher
11:06
created

ExpressionParser2::calculateResultExpression()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 3
nop 2
dl 0
loc 11
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace POData\UriProcessor\QueryProcessor\ExpressionParser;
4
5
use POData\Common\Messages;
6
use POData\Common\ODataException;
7
use POData\Providers\Expression\IExpressionProvider;
8
use POData\Providers\Expression\PHPExpressionProvider;
9
use POData\Providers\Metadata\ResourceType;
10
use POData\Providers\Metadata\Type\Boolean;
11
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\AbstractExpression;
12
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\ArithmeticExpression;
13
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\ConstantExpression;
14
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\ExpressionType;
15
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\FunctionCallExpression;
16
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\LogicalExpression;
17
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\PropertyAccessExpression;
18
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\RelationalExpression;
19
use POData\UriProcessor\QueryProcessor\ExpressionParser\Expressions\UnaryExpression;
20
use POData\UriProcessor\QueryProcessor\FunctionDescription;
21
22
/**
23
 * Class ExpressionParser2.
24
 *
25
 * Build the basic expression tree for a given expression using base class
26
 * ExpressionParser, modify the expression tree to have null checks
27
 */
28
class ExpressionParser2 extends ExpressionParser
29
{
30
    /**
31
     * @var array
32
     */
33
    private $mapTable;
34
35
    /**
36
     * Collection of navigation properties used in the expression.
37
     *
38
     * @var array<array>
39
     */
40
    private $navigationPropertiesUsedInTheExpression;
41
42
    /**
43
     * Indicates whether the end user has implemented IExpressionProvider or not.
44
     *
45
     * @var bool
46
     */
47
    private $isPHPExpressionProvider;
48
49
    /**
50
     * Create new instance of ExpressionParser2.
51
     *
52
     * @param string       $text                    The text expression to parse
53
     * @param ResourceType $resourceType            The resource type in which
54
     *                                              expression will be applied
55
     * @param bool         $isPHPExpressionProvider True if IExpressionProvider provider is
56
     *                                              implemented by user, False otherwise
57
     */
58
    public function __construct($text, ResourceType $resourceType, $isPHPExpressionProvider)
59
    {
60
        parent::__construct($text, $resourceType, $isPHPExpressionProvider);
61
        $this->navigationPropertiesUsedInTheExpression = [];
62
        $this->isPHPExpressionProvider = $isPHPExpressionProvider;
63
    }
64
65
    /**
66
     * Parse and generate expression from the the given odata expression.
67
     *
68
     *
69
     * @param string              $text               The text expression to parse
70
     * @param ResourceType        $resourceType       The resource type in which
71
     * @param IExpressionProvider $expressionProvider Implementation of IExpressionProvider
72
     *
73
     * @throws ODataException If any error occurs while parsing the odata expression or
74
     *                        building the php/custom expression
75
     *
76
     * @return FilterInfo
77
     */
78
    public static function parseExpression2($text, ResourceType $resourceType, IExpressionProvider $expressionProvider)
79
    {
80
        $expressionParser2 = new self($text, $resourceType, $expressionProvider instanceof PHPExpressionProvider);
81
        $expressionTree = $expressionParser2->parseFilter();
82
83
        $expressionAsString = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $expressionAsString is dead and can be removed.
Loading history...
84
85
        $expressionProvider->setResourceType($resourceType);
86
        $expressionProcessor = new ExpressionProcessor($expressionProvider);
87
88
        try {
89
            $expressionAsString = $expressionProcessor->processExpression($expressionTree);
90
        } catch (\InvalidArgumentException $invalidArgumentException) {
91
            throw ODataException::createInternalServerError($invalidArgumentException->getMessage());
92
        }
93
        $expressionAsString = (null !== $expressionAsString) ? $expressionAsString : '';
94
        return new FilterInfo(
95
            $expressionParser2->navigationPropertiesUsedInTheExpression,
96
            $expressionAsString
97
        );
98
    }
99
100
    /**
101
     * Parse the expression.
102
     *
103
     * @see library/POData/QueryProcessor/ExpressionParser::parseFilter()
104
     *
105
     * @throws ODataException
106
     *
107
     * @return AbstractExpression
108
     */
109
    public function parseFilter()
110
    {
111
        $expression = parent::parseFilter();
112
        if (!$expression->typeIs(new Boolean())) {
113
            throw ODataException::createSyntaxError(
114
                Messages::expressionParser2BooleanRequired()
115
            );
116
        }
117
        if ($this->isPHPExpressionProvider) {
118
            $resultExpression = $this->processNodeForNullability(null, $expression);
119
            if (null != $resultExpression) {
120
                return $resultExpression;
121
            }
122
        }
123
124
        return $expression;
125
    }
126
127
    /**
128
     * Process the expression node for nullability.
129
     *
130
     * @param  AbstractExpression      $parentExpression      The parent expression of expression node to process
131
     * @param  AbstractExpression|null $expression            The expression node to process
132
     * @param  bool                    $checkNullForMostChild Whether to include null check for current property
133
     * @throws ODataException
134
     * @return AbstractExpression|null
135
     */
136
    private function processNodeForNullability(
137
        $parentExpression,
138
        AbstractExpression $expression = null,
139
        $checkNullForMostChild = true
140
    ) {
141
        if ($expression instanceof ArithmeticExpression) {
142
            return $this->processArithmeticNode($expression);
143
        } elseif ($expression instanceof ConstantExpression) {
144
            return null;
145
        } elseif ($expression instanceof FunctionCallExpression) {
146
            return $this->processFunctionCallNode($expression, $parentExpression);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->processFunctionCa...ion, $parentExpression) targeting POData\UriProcessor\Quer...ocessFunctionCallNode() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
147
        } elseif ($expression instanceof LogicalExpression) {
148
            return $this->processLogicalNode($expression, $parentExpression);
149
        } elseif ($expression instanceof PropertyAccessExpression) {
150
            return $this->processPropertyAccessNode(
151
                $expression,
152
                $parentExpression,
153
                $checkNullForMostChild
154
            );
155
        } elseif ($expression instanceof RelationalExpression) {
156
            return $this->processRelationalNode($expression, $parentExpression);
157
        } elseif ($expression instanceof UnaryExpression) {
158
            return $this->processUnaryNode($expression, $parentExpression);
159
        }
160
161
        throw ODataException::createSyntaxError(
162
            Messages::expressionParser2UnexpectedExpression(get_class($expression))
163
        );
164
    }
165
166
    /**
167
     * Process an arithmetic expression node for nullability.
168
     *
169
     * @param ArithmeticExpression $expression The arithmetic expression node
170
     *                                         to process
171
     *
172
     * @return AbstractExpression|null
173
     */
174
    private function processArithmeticNode(ArithmeticExpression $expression)
175
    {
176
        $leftNullableExpTree = $this->processNodeForNullability($expression, $expression->getLeft());
177
        $rightNullableExpTree = $this->processNodeForNullability($expression, $expression->getRight());
178
        $resultExpression = $this->calculateResultExpression($leftNullableExpTree, $rightNullableExpTree);
179
180
        return $resultExpression;
181
    }
182
183
    /**
184
     * Process an arithmetic expression node for nullability.
185
     *
186
     * @param FunctionCallExpression $expression       The function call expression
187
     *                                                 node to process
188
     * @param AbstractExpression     $parentExpression The parent expression of
189
     *                                                 expression node to process
190
     *
191
     * @return null|AbstractExpression
192
     */
193
    private function processFunctionCallNode(
194
        FunctionCallExpression $expression,
195
        $parentExpression
196
    ) {
197
        $paramExpressions = $expression->getParamExpressions();
198
        $checkNullForMostChild = strcmp($expression->getFunctionDescription()->name, 'is_null') === 0;
199
        $resultExpression = null;
200
        foreach ($paramExpressions as $paramExpression) {
201
            $resultExpression1 = $this->processNodeForNullability(
202
                $expression,
203
                $paramExpression,
204
                !$checkNullForMostChild
205
            );
206
            $resultExpression = $this->calculateResultExpression($resultExpression, $resultExpression1);
207
        }
208
209
        if (null == $resultExpression) {
210
            return null;
211
        }
212
213
        if (null == $parentExpression) {
214
            return new LogicalExpression(
215
                $resultExpression,
216
                $expression,
217
                ExpressionType::AND_LOGICAL()
218
            );
219
        }
220
221
        return $resultExpression;
222
    }
223
224
    /**
225
     * Process an logical expression node for nullability.
226
     *
227
     * @param LogicalExpression  $expression       The logical expression node
228
     *                                             to process
229
     * @param AbstractExpression $parentExpression The parent expression of
230
     *                                             expression node to process
231
     *
232
     * @return null|AbstractExpression
233
     */
234
    private function processLogicalNode(
235
        LogicalExpression $expression,
236
        $parentExpression
237
    ) {
238
        $leftNullableExpTree = $this->processNodeForNullability($expression, $expression->getLeft());
239
        $rightNullableExpTree = $this->processNodeForNullability($expression, $expression->getRight());
240
        if ($expression->getNodeType() == ExpressionType::OR_LOGICAL()) {
0 ignored issues
show
Bug Best Practice introduced by
The method POData\UriProcessor\Quer...ssionType::OR_LOGICAL() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

240
        if ($expression->getNodeType() == ExpressionType::/** @scrutinizer ignore-call */ OR_LOGICAL()) {
Loading history...
241
            if (null !== $leftNullableExpTree) {
242
                $resultExpression = new LogicalExpression(
243
                    $leftNullableExpTree,
244
                    $expression->getLeft(),
245
                    ExpressionType::AND_LOGICAL()
0 ignored issues
show
Bug Best Practice introduced by
The method POData\UriProcessor\Quer...sionType::AND_LOGICAL() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

245
                    ExpressionType::/** @scrutinizer ignore-call */ 
246
                                    AND_LOGICAL()
Loading history...
246
                );
247
                $expression->setLeft($resultExpression);
248
            }
249
250
            if (null !== $rightNullableExpTree) {
251
                $resultExpression = new LogicalExpression(
252
                    $rightNullableExpTree,
253
                    $expression->getRight(),
254
                    ExpressionType::AND_LOGICAL()
255
                );
256
                $expression->setRight($resultExpression);
257
            }
258
259
            return null;
260
        }
261
262
        $resultExpression = $this->calculateResultExpression($leftNullableExpTree, $rightNullableExpTree);
263
264
        if (null == $resultExpression) {
265
            return null;
266
        }
267
268
        if (null == $parentExpression) {
269
            return new LogicalExpression(
270
                $resultExpression,
271
                $expression,
272
                ExpressionType::AND_LOGICAL()
273
            );
274
        }
275
276
        return $resultExpression;
277
    }
278
279
    /**
280
     * Process an property access expression node for nullability.
281
     *
282
     * @param PropertyAccessExpression $expression            The property access
283
     *                                                        expression node to process
284
     * @param AbstractExpression       $parentExpression      The parent expression of
285
     *                                                        expression node to process
286
     * @param bool                     $checkNullForMostChild Whether to check null for
287
     *                                                        most child node or not
288
     *
289
     * @return LogicalExpression|UnaryExpression|null
290
     */
291
    private function processPropertyAccessNode(
292
        PropertyAccessExpression $expression,
293
        $parentExpression,
294
        $checkNullForMostChild
295
    ) {
296
        $navigationsUsed = $expression->getNavigationPropertiesInThePath();
297
        if (!empty($navigationsUsed)) {
298
            $this->navigationPropertiesUsedInTheExpression[] = $navigationsUsed;
299
        }
300
301
        $nullableExpTree = $expression->createNullableExpressionTree($checkNullForMostChild);
302
303
        if (null == $parentExpression) {
304
            return new LogicalExpression(
305
                $nullableExpTree,
306
                $expression,
307
                ExpressionType::AND_LOGICAL()
308
            );
309
        }
310
311
        return $nullableExpTree;
312
    }
313
314
    /**
315
     * Process a relational expression node for nullability.
316
     *
317
     * @param RelationalExpression $expression       The relational expression node
318
     *                                               to process
319
     * @param AbstractExpression   $parentExpression The parent expression of
320
     *                                               expression node to process
321
     *
322
     * @return null|AbstractExpression
323
     */
324
    private function processRelationalNode(
325
        RelationalExpression $expression,
326
        $parentExpression
327
    ) {
328
        $leftNullableExpTree = $this->processNodeForNullability($expression, $expression->getLeft());
329
        $rightNullableExpTree = $this->processNodeForNullability($expression, $expression->getRight());
330
331
        $resultExpression = $this->calculateResultExpression($leftNullableExpTree, $rightNullableExpTree);
332
333
        if (null == $resultExpression) {
334
            return null;
335
        }
336
337
        if (null == $parentExpression) {
338
            return new LogicalExpression(
339
                $resultExpression,
340
                $expression,
341
                ExpressionType::AND_LOGICAL()
342
            );
343
        }
344
345
        return $resultExpression;
346
    }
347
348
    /**
349
     * Process an unary expression node for nullability.
350
     *
351
     * @param UnaryExpression    $expression       The unary expression node
352
     *                                             to process
353
     * @param AbstractExpression $parentExpression The parent expression of
354
     *                                             expression node to process
355
     *
356
     * @throws ODataException
357
     * @return AbstractExpression|null
358
     */
359
    private function processUnaryNode(
360
        UnaryExpression $expression,
361
        $parentExpression
362
    ) {
363
        if (ExpressionType::NEGATE() == $expression->getNodeType()) {
0 ignored issues
show
Bug Best Practice introduced by
The method POData\UriProcessor\Quer...xpressionType::NEGATE() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

363
        if (ExpressionType::/** @scrutinizer ignore-call */ NEGATE() == $expression->getNodeType()) {
Loading history...
364
            return $this->processNodeForNullability($expression, $expression->getChild());
365
        }
366
367
        if (ExpressionType::NOT_LOGICAL() == $expression->getNodeType()) {
0 ignored issues
show
Bug Best Practice introduced by
The method POData\UriProcessor\Quer...sionType::NOT_LOGICAL() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

367
        if (ExpressionType::/** @scrutinizer ignore-call */ NOT_LOGICAL() == $expression->getNodeType()) {
Loading history...
368
            $resultExpression = $this->processNodeForNullability($expression, $expression->getChild());
369
            if (null == $resultExpression) {
370
                return null;
371
            }
372
373
            if (null == $parentExpression) {
374
                return new LogicalExpression(
375
                    $resultExpression,
376
                    $expression,
377
                    ExpressionType::AND_LOGICAL()
378
                );
379
            }
380
381
            return $resultExpression;
382
        }
383
384
        throw ODataException::createSyntaxError(
385
            Messages::expressionParser2UnexpectedExpression(get_class($expression))
386
        );
387
    }
388
389
    /**
390
     * Merge two null check expression trees by removing duplicate nodes.
391
     *
392
     * @param AbstractExpression $nullCheckExpTree1 First expression
393
     * @param AbstractExpression $nullCheckExpTree2 Second expression
394
     *
395
     * @return LogicalExpression|UnaryExpression|null
396
     */
397
    private function mergeNullableExpressionTrees(
398
        $nullCheckExpTree1,
399
        $nullCheckExpTree2
400
    ) {
401
        $this->mapTable = [];
402
        $this->map($nullCheckExpTree1);
403
        $this->map($nullCheckExpTree2);
404
        $expression = null;
405
        foreach ($this->mapTable as $node) {
406
            if (null == $expression) {
407
                $expression = new UnaryExpression(
408
                    new FunctionCallExpression(
409
                        FunctionDescription::isNullCheckFunction($node->getType()),
410
                        [$node]
411
                    ),
412
                    ExpressionType::NOT_LOGICAL(),
0 ignored issues
show
Bug Best Practice introduced by
The method POData\UriProcessor\Quer...sionType::NOT_LOGICAL() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

412
                    ExpressionType::/** @scrutinizer ignore-call */ 
413
                                    NOT_LOGICAL(),
Loading history...
413
                    new Boolean()
414
                );
415
            } else {
416
                $expression = new LogicalExpression(
417
                    $expression,
418
                    new UnaryExpression(
419
                        new FunctionCallExpression(
420
                            FunctionDescription::isNullCheckFunction(
421
                                $node->getType()
422
                            ),
423
                            [$node]
424
                        ),
425
                        ExpressionType::NOT_LOGICAL(),
426
                        new Boolean()
427
                    ),
428
                    ExpressionType::AND_LOGICAL()
429
                );
430
            }
431
        }
432
433
        return $expression;
434
    }
435
436
    /**
437
     *  Populate map table.
438
     *
439
     * @param AbstractExpression $nullCheckExpTree The expression to verify
440
     *
441
     * @throws ODataException
442
     */
443
    private function map($nullCheckExpTree)
444
    {
445
        if ($nullCheckExpTree instanceof LogicalExpression) {
446
            $this->map($nullCheckExpTree->getLeft());
447
            $this->map($nullCheckExpTree->getRight());
448
        } elseif ($nullCheckExpTree instanceof UnaryExpression) {
449
            $this->map($nullCheckExpTree->getChild());
450
        } elseif ($nullCheckExpTree instanceof FunctionCallExpression) {
451
            $param = $nullCheckExpTree->getParamExpressions();
452
            $this->map($param[0]);
453
        } elseif ($nullCheckExpTree instanceof PropertyAccessExpression) {
454
            $parent = $nullCheckExpTree;
455
            $key = null;
456
            do {
457
                $key = $parent->getResourceProperty()->getName() . '_' . $key;
458
                $parent = $parent->getParent();
459
            } while (null != $parent);
460
461
            $this->mapTable[$key] = $nullCheckExpTree;
462
        } else {
463
            throw ODataException::createSyntaxError(
464
                Messages::expressionParser2UnexpectedExpression(get_class($nullCheckExpTree))
465
            );
466
        }
467
    }
468
469
    /**
470
     * @param  AbstractExpression|null $leftNullableExpTree
471
     * @param  AbstractExpression|null $rightNullableExpTree
472
     * @return null|AbstractExpression
473
     */
474
    private function calculateResultExpression($leftNullableExpTree, $rightNullableExpTree)
475
    {
476
        if (null != $leftNullableExpTree && null != $rightNullableExpTree) {
477
            $resultExpression = $this->mergeNullableExpressionTrees(
478
                $leftNullableExpTree,
479
                $rightNullableExpTree
480
            );
481
        } else {
482
            $resultExpression = null != $leftNullableExpTree ? $leftNullableExpTree : $rightNullableExpTree;
483
        }
484
        return $resultExpression;
485
    }
486
}
487