Passed
Push — master ( 492d83...a726fe )
by Alex
01:10
created

OrderByParser::_buildOrderByTree()   F

Complexity

Conditions 28
Paths 245

Size

Total Lines 194
Code Lines 123

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 194
rs 3.5736
c 0
b 0
f 0
cc 28
eloc 123
nc 245
nop 1

How to fix   Long Method    Complexity   

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 POData\UriProcessor\QueryProcessor\OrderByParser;
4
5
use POData\Common\Messages;
6
use POData\Common\ODataException;
7
use POData\Providers\Metadata\ResourcePropertyKind;
8
use POData\Providers\Metadata\ResourceSetWrapper;
9
use POData\Providers\Metadata\ResourceType;
10
use POData\Providers\Metadata\Type\Binary;
11
use POData\Providers\ProvidersWrapper;
12
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionLexer;
13
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionTokenId;
14
15
/**
16
 * Class OrderByParser.
17
 *
18
 * Class to parse $orderby query option and perform syntax validation
19
 * and build 'OrderBy Tree' along with next level of validation, the
20
 * created tree is used for building sort functions and 'OrderByInfo' structure.
21
 *
22
 * The syntax of orderby clause is:
23
 *
24
 * OrderByClause         : OrderByPathSegment [, OrderByPathSegment]*
25
 * OrderByPathSegment    : OrderBySubPathSegment[/OrderBySubPathSegment]*[asc|desc]?
26
 * OrderBySubPathSegment : identifier
27
 */
28
class OrderByParser
29
{
30
    /**
31
     * Collection of anonymous sorter function corresponding to
32
     * each orderby path segment.
33
     *
34
     * @var callable[]
35
     */
36
    private $comparisonFunctions = [];
37
38
    /**
39
     * The top level sorter function generated from orderby path
40
     * segments.
41
     *
42
     * @var callable
43
     */
44
    private $topLevelComparisonFunction;
45
46
    /**
47
     * The structure holds information about the navigation properties
48
     * used in the orderby clause (if any) and orderby path if IDSQP
49
     * implementor want to perform sorting.
50
     *
51
     * @var OrderByInfo
52
     */
53
    private $orderByInfo;
54
55
    /**
56
     * Reference to metadata and query provider wrapper.
57
     *
58
     * @var ProvidersWrapper
59
     */
60
    private $providerWrapper;
61
62
    /**
63
     * This object will be of type of the resource set identified by the
64
     * request uri.
65
     *
66
     * @var mixed
67
     */
68
    private $dummyObject;
69
70
    /*
71
     * Root node for tree ordering
72
     *
73
     * @var mixed
74
     */
75
    private $rootOrderByNode;
76
77
    /**
78
     * Creates new instance of OrderByParser.
79
     *
80
     * @param ProvidersWrapper $providerWrapper Reference to metadata
81
     *                                          and query provider
82
     *                                          wrapper
83
     */
84
    private function __construct(ProvidersWrapper $providerWrapper)
85
    {
86
        $this->providerWrapper = $providerWrapper;
87
    }
88
89
    /**
90
     * This function perform the following tasks with the help of internal helper
91
     * functions
92
     * (1) Read the orderby clause and perform basic syntax errors
93
     * (2) Build 'Order By Tree', creates anonymous sorter function for each leaf
94
     *     node and check for error
95
     * (3) Build 'OrderInfo' structure, holds information about the navigation
96
     *     properties used in the orderby clause (if any) and orderby path if
97
     *     IDSQP implementor want to perform sorting
98
     * (4) Build top level anonymous sorter function
99
     * (4) Release resources hold by the 'Order By Tree'
100
     * (5) Create 'InternalOrderInfo' structure, which wraps 'OrderInfo' and top
101
     *     level sorter function.
102
     *
103
     * @param ResourceSetWrapper $resourceSetWrapper ResourceSetWrapper for the resource targeted by resource path
104
     * @param ResourceType       $resourceType       ResourceType for the resource targeted by resource path
105
     * @param string             $orderBy            The orderby clause
106
     * @param ProvidersWrapper   $providerWrapper    Reference to the wrapper for IDSQP and IDSMP impl
107
     *
108
     * @throws ODataException If any error occur while parsing orderby clause
109
     *
110
     * @return InternalOrderByInfo
111
     */
112
    public static function parseOrderByClause(
113
        ResourceSetWrapper $resourceSetWrapper,
114
        ResourceType $resourceType,
115
        $orderBy,
116
        ProvidersWrapper $providerWrapper
117
    ) {
118
        assert(is_string($orderBy), "OrderBy clause must be a string");
119
        $orderByParser = new self($providerWrapper);
120
        try {
121
            $orderByParser->dummyObject = $resourceType->getInstanceType()->newInstance();
0 ignored issues
show
Bug introduced by
The method newInstance does only exist in ReflectionClass, but not in POData\Providers\Metadata\Type\IType.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
122
        } catch (\ReflectionException $reflectionException) {
123
            throw ODataException::createInternalServerError(Messages::orderByParserFailedToCreateDummyObject());
124
        }
125
        $orderByParser->rootOrderByNode = new OrderByRootNode($resourceSetWrapper, $resourceType);
126
        $orderByPathSegments = $orderByParser->readOrderBy($orderBy);
127
128
        $orderByParser->buildOrderByTree($orderByPathSegments);
129
        $orderByParser->createOrderInfo($orderByPathSegments);
130
        $orderByParser->generateTopLevelComparisonFunction();
131
        //Recursively release the resources
132
        $orderByParser->rootOrderByNode->free();
133
        //creates internal order info wrapper
134
        $internalOrderInfo = new InternalOrderByInfo(
135
            $orderByParser->orderByInfo,
136
            $orderByParser->comparisonFunctions,
137
            $orderByParser->topLevelComparisonFunction,
138
            $orderByParser->dummyObject,
139
            $resourceType
140
        );
141
        unset($orderByParser->orderByInfo);
142
        unset($orderByParser->topLevelComparisonFunction);
143
144
        return $internalOrderInfo;
145
    }
146
147
    /**
148
     * Build 'OrderBy Tree' from the given orderby path segments, also build
149
     * comparsion function for each path segment.
150
     *
151
     * @param array(array) &$orderByPathSegments Collection of orderby path segments,
152
     *                                           this is passed by reference
153
     *                                           since we need this function to
154
     *                                           modify this array in two cases:
155
     *                                           1. if asc or desc present, then the
156
     *                                           corresponding sub path segment
157
     *                                           should be removed
158
     *                                           2. remove duplicate orderby path
159
     *                                           segment
160
     *
161
     * @throws ODataException If any error occurs while processing the orderby path
162
     *                        segments
163
     */
164
    private function buildOrderByTree(&$orderByPathSegments)
165
    {
166
        foreach ($orderByPathSegments as $index1 => &$orderBySubPathSegments) {
167
            $currentNode = $this->rootOrderByNode;
168
            $currentObject = $this->dummyObject;
169
            $ascending = true;
170
            $subPathCount = count($orderBySubPathSegments);
171
            // Check sort order is specified in the path, if so set a
172
            // flag and remove that segment
173
            if ($subPathCount > 1) {
174
                if ('*desc' === $orderBySubPathSegments[$subPathCount - 1]) {
175
                    $ascending = false;
176
                    unset($orderBySubPathSegments[$subPathCount - 1]);
177
                    --$subPathCount;
178
                } elseif ('*asc' === $orderBySubPathSegments[$subPathCount - 1]) {
179
                    unset($orderBySubPathSegments[$subPathCount - 1]);
180
                    --$subPathCount;
181
                }
182
            }
183
184
            $ancestors = [$this->rootOrderByNode->getResourceSetWrapper()->getName()];
185
            foreach ($orderBySubPathSegments as $index2 => $orderBySubPathSegment) {
186
                $isLastSegment = ($index2 == $subPathCount - 1);
187
                $resourceSetWrapper = null;
188
                $resourceType = $currentNode->getResourceType();
189
                $resourceProperty = $resourceType->resolveProperty($orderBySubPathSegment);
190
                if (is_null($resourceProperty)) {
191
                    throw ODataException::createSyntaxError(
192
                        Messages::orderByParserPropertyNotFound(
193
                            $resourceType->getFullName(),
194
                            $orderBySubPathSegment
195
                        )
196
                    );
197
                }
198
199
                if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG)) {
200
                    throw ODataException::createBadRequestError(
201
                        Messages::orderByParserBagPropertyNotAllowed(
202
                            $resourceProperty->getName()
203
                        )
204
                    );
205
                } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) {
206
                    if (!$isLastSegment) {
207
                        throw ODataException::createBadRequestError(
208
                            Messages::orderByParserPrimitiveAsIntermediateSegment(
209
                                $resourceProperty->getName()
210
                            )
211
                        );
212
                    }
213
214
                    $type = $resourceProperty->getInstanceType();
215
                    if ($type instanceof Binary) {
216
                        throw ODataException::createBadRequestError(
217
                            Messages::orderByParserSortByBinaryPropertyNotAllowed($resourceProperty->getName())
218
                        );
219
                    }
220
                } elseif ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE
221
                    || $resourceProperty->getKind() == ResourcePropertyKind::RESOURCE_REFERENCE
222
                ) {
223
                    $this->assertion($currentNode instanceof OrderByRootNode || $currentNode instanceof OrderByNode);
224
                    $resourceSetWrapper = $currentNode->getResourceSetWrapper();
225
                    $this->assertion(!is_null($resourceSetWrapper));
226
                    $resourceSetWrapper
227
                        = $this->providerWrapper->getResourceSetWrapperForNavigationProperty(
228
                            $resourceSetWrapper,
229
                            $resourceType,
230
                            $resourceProperty
231
                        );
232
                    if (is_null($resourceSetWrapper)) {
233
                        throw ODataException::createBadRequestError(
234
                            Messages::badRequestInvalidPropertyNameSpecified(
235
                                $resourceType->getFullName(),
236
                                $orderBySubPathSegment
237
                            )
238
                        );
239
                    }
240
241
                    if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE) {
242
                        throw ODataException::createBadRequestError(
243
                            Messages::orderByParserResourceSetReferenceNotAllowed(
244
                                $resourceProperty->getName(),
245
                                $resourceType->getFullName()
246
                            )
247
                        );
248
                    }
249
250
                    $resourceSetWrapper->checkResourceSetRightsForRead(true);
251
                    if ($isLastSegment) {
252
                        throw ODataException::createBadRequestError(
253
                            Messages::orderByParserSortByNavigationPropertyIsNotAllowed(
254
                                $resourceProperty->getName()
255
                            )
256
                        );
257
                    }
258
259
                    $ancestors[] = $orderBySubPathSegment;
260
                } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE)) {
261
                    if ($isLastSegment) {
262
                        throw ODataException::createBadRequestError(
263
                            Messages::orderByParserSortByComplexPropertyIsNotAllowed(
264
                                $resourceProperty->getName()
265
                            )
266
                        );
267
                    }
268
269
                    $ancestors[] = $orderBySubPathSegment;
270
                } else {
271
                    throw ODataException::createInternalServerError(
272
                        Messages::orderByParserUnexpectedPropertyType()
273
                    );
274
                }
275
276
                $node = $currentNode->findNode($orderBySubPathSegment);
277
                if (is_null($node)) {
278
                    if ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) {
279
                        $node = new OrderByLeafNode(
280
                            $orderBySubPathSegment,
281
                            $resourceProperty,
282
                            $ascending
283
                        );
284
                        $this->comparisonFunctions[] = $node->buildComparisonFunction($ancestors);
285
                    } elseif ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCE_REFERENCE) {
286
                        $node = new OrderByNode(
287
                            $orderBySubPathSegment,
288
                            $resourceProperty,
289
                            $resourceSetWrapper
0 ignored issues
show
Bug introduced by
It seems like $resourceSetWrapper can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
290
                        );
291
                        // Initialize this member variable (identified by
292
                        // $resourceProperty) of parent object.
293
                        try {
294
                            $object = $resourceProperty->getInstanceType()->newInstance();
295
                            $resourceType->setPropertyValue($currentObject, $resourceProperty->getName(), $object);
296
                            $currentObject = $object;
297
                        } catch (\ReflectionException $reflectionException) {
298
                            throw ODataException::createInternalServerError(
299
                                Messages::orderByParserFailedToAccessOrInitializeProperty(
300
                                    $resourceProperty->getName(),
301
                                    $resourceType->getName()
302
                                )
303
                            );
304
                        }
305
                    } elseif ($resourceProperty->getKind() == ResourcePropertyKind::COMPLEX_TYPE) {
306
                        $node = new OrderByNode($orderBySubPathSegment, $resourceProperty, null);
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<POData\Providers\...ata\ResourceSetWrapper>.

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...
307
                        // Initialize this member variable
308
                        // (identified by $resourceProperty)of parent object.
309
                        try {
310
                            $object = $resourceProperty->getInstanceType()->newInstance();
311
                            $resourceType->setPropertyValue($currentObject, $resourceProperty->getName(), $object);
312
                            $currentObject = $object;
313
                        } catch (\ReflectionException $reflectionException) {
314
                            throw ODataException::createInternalServerError(
315
                                Messages::orderByParserFailedToAccessOrInitializeProperty(
316
                                    $resourceProperty->getName(),
317
                                    $resourceType->getName()
318
                                )
319
                            );
320
                        }
321
                    }
322
323
                    $currentNode->addNode($node);
324
                } else {
325
                    try {
326
                        // If a magic method for properties exists (eg Eloquent), dive into it directly and return value
327
                        if (method_exists($currentObject, '__get')) {
328
                            $targProperty = $resourceProperty->getName();
329
330
                            return $currentObject->$targProperty;
331
                        }
332
                        $reflectionClass = new \ReflectionClass(get_class($currentObject));
333
                        $reflectionProperty = $reflectionClass->getProperty($resourceProperty->getName());
334
                        $reflectionProperty->setAccessible(true);
335
                        $currentObject = $reflectionProperty->getValue($currentObject);
336
                    } catch (\ReflectionException $reflectionException) {
337
                        throw ODataException::createInternalServerError(
338
                            Messages::orderByParserFailedToAccessOrInitializeProperty(
339
                                $resourceProperty->getName(),
340
                                $resourceType->getName()
341
                            )
342
                        );
343
                    }
344
345
                    if ($node instanceof OrderByLeafNode) {
346
                        //remove duplicate orderby path
347
                        unset($orderByPathSegments[$index1]);
348
                    }
349
                }
350
351
                $currentNode = $node;
352
            }
353
        }
354
        return null;
355
    }
356
357
    /**
358
     * Traverse 'Order By Tree' and create 'OrderInfo' structure.
359
     *
360
     * @param array(array) $orderByPaths The orderby paths
0 ignored issues
show
Documentation introduced by
The doc-type array(array) could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
361
     *
362
     * @throws ODataException In case parser found any tree inconsisitent
363
     *                        state, throws unexpected state error
364
     *
365
     * @return OrderByInfo
366
     */
367
    private function createOrderInfo($orderByPaths)
368
    {
369
        $orderByPathSegments = [];
370
        $navigationPropertiesInThePath = [];
371
        foreach ($orderByPaths as $index => $orderBySubPaths) {
372
            $currentNode = $this->rootOrderByNode;
373
            $orderBySubPathSegments = [];
374
            foreach ($orderBySubPaths as $orderBySubPath) {
375
                $node = $currentNode->findNode($orderBySubPath);
376
                $this->assertion(!is_null($node));
377
                $resourceProperty = $node->getResourceProperty();
378
                if ($node instanceof OrderByNode && !is_null($node->getResourceSetWrapper())) {
379
                    if (!array_key_exists($index, $navigationPropertiesInThePath)) {
380
                        $navigationPropertiesInThePath[$index] = [];
381
                    }
382
383
                    $navigationPropertiesInThePath[$index][] = $resourceProperty;
384
                }
385
386
                $orderBySubPathSegments[] = new OrderBySubPathSegment($resourceProperty);
387
                $currentNode = $node;
388
            }
389
390
            $this->assertion($currentNode instanceof OrderByLeafNode);
391
            $orderByPathSegments[] = new OrderByPathSegment($orderBySubPathSegments, $currentNode->isAscending());
392
            unset($orderBySubPathSegments);
393
        }
394
395
        $this->orderByInfo = new OrderByInfo(
396
            $orderByPathSegments,
397
            empty($navigationPropertiesInThePath) ? null : $navigationPropertiesInThePath
398
        );
399
    }
400
401
    /**
402
     * Generates top level comparison function from sub comparison functions.
403
     */
404
    private function generateTopLevelComparisonFunction()
405
    {
406
        $comparisonFunctionCount = count($this->comparisonFunctions);
407
        $this->assertion(0 < $comparisonFunctionCount);
408
        if (1 == $comparisonFunctionCount) {
409
            $this->topLevelComparisonFunction = $this->comparisonFunctions[0];
410
        } else {
411
            $funcList = $this->comparisonFunctions;
412
            $this->topLevelComparisonFunction = function ($object1, $object2) use ($funcList) {
413
                $ret = 0;
414
                foreach ($funcList as $f) {
415
                    $ret = $f($object1, $object2);
416
                    if (0 != $ret) {
417
                        return $ret;
418
                    }
419
                }
420
                return $ret;
421
            };
422
        }
423
    }
424
425
    /**
426
     * Read orderby clause.
427
     *
428
     * @param string $value orderby clause to read
429
     *
430
     * @throws ODataException If any syntax error found while reading the clause
431
     *
432
     * @return array(array) An array of 'OrderByPathSegment's, each of which
0 ignored issues
show
Documentation introduced by
The doc-type array(array) could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
433
     *                      is array of 'OrderBySubPathSegment's
434
     */
435
    private function readOrderBy($value)
436
    {
437
        $orderByPathSegments = [];
438
        $lexer = new ExpressionLexer($value);
439
        $i = 0;
440
        while ($lexer->getCurrentToken()->Id != ExpressionTokenId::END) {
441
            $orderBySubPathSegment = $lexer->readDottedIdentifier();
442
            if (!array_key_exists($i, $orderByPathSegments)) {
443
                $orderByPathSegments[$i] = [];
444
            }
445
446
            $orderByPathSegments[$i][] = $orderBySubPathSegment;
447
            $tokenId = $lexer->getCurrentToken()->Id;
448
            if ($tokenId != ExpressionTokenId::END) {
449
                if ($tokenId != ExpressionTokenId::SLASH) {
450
                    if ($tokenId != ExpressionTokenId::COMMA) {
451
                        $lexer->validateToken(ExpressionTokenId::IDENTIFIER);
452
                        $identifier = $lexer->getCurrentToken()->Text;
453
                        if ($identifier !== 'asc' && $identifier !== 'desc') {
454
                            // force lexer to throw syntax error as we found
455
                            // unexpected identifier
456
                            $lexer->validateToken(ExpressionTokenId::DOT);
457
                        }
458
459
                        $orderByPathSegments[$i][] = '*' . $identifier;
460
                        $lexer->nextToken();
461
                        $tokenId = $lexer->getCurrentToken()->Id;
462
                        if ($tokenId != ExpressionTokenId::END) {
463
                            $lexer->validateToken(ExpressionTokenId::COMMA);
464
                            ++$i;
465
                        }
466
                    } else {
467
                        ++$i;
468
                    }
469
                }
470
471
                $lexer->nextToken();
472
            }
473
        }
474
475
        return $orderByPathSegments;
476
    }
477
478
    /**
479
     * Assert that the given condition is true, if false throw
480
     * ODataException for unexpected state.
481
     *
482
     * @param bool $condition The condition to assert
483
     *
484
     * @throws ODataException
485
     */
486
    private function assertion($condition)
487
    {
488
        if (!$condition) {
489
            throw ODataException::createInternalServerError(Messages::orderByParserUnExpectedState());
490
        }
491
    }
492
}
493