OrderByParser::buildOrderByTree()   F
last analyzed

Complexity

Conditions 28
Paths 457

Size

Total Lines 176
Code Lines 115

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 28
eloc 115
c 1
b 0
f 0
nc 457
nop 1
dl 0
loc 176
rs 0.6032

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
declare(strict_types=1);
4
5
namespace POData\UriProcessor\QueryProcessor\OrderByParser;
6
7
use POData\Common\InvalidOperationException;
8
use POData\Common\Messages;
9
use POData\Common\ODataException;
10
use POData\Common\ReflectionHandler;
11
use POData\Providers\Metadata\ResourceEntityType;
12
use POData\Providers\Metadata\ResourceProperty;
13
use POData\Providers\Metadata\ResourcePropertyKind;
14
use POData\Providers\Metadata\ResourceSetWrapper;
15
use POData\Providers\Metadata\ResourceType;
16
use POData\Providers\Metadata\Type\Binary;
17
use POData\Providers\ProvidersWrapper;
18
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionLexer;
19
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionTokenId;
20
use ReflectionClass;
21
use ReflectionException;
22
23
/**
24
 * Class OrderByParser.
25
 *
26
 * Class to parse $orderby query option and perform syntax validation
27
 * and build 'OrderBy Tree' along with next level of validation, the
28
 * created tree is used for building sort functions and 'OrderByInfo' structure.
29
 *
30
 * The syntax of orderby clause is:
31
 *
32
 * OrderByClause         : OrderByPathSegment [, OrderByPathSegment]*
33
 * OrderByPathSegment    : OrderBySubPathSegment[/OrderBySubPathSegment]*[asc|desc]?
34
 * OrderBySubPathSegment : identifier
35
 */
36
class OrderByParser
37
{
38
    /**
39
     * Collection of anonymous sorter function corresponding to each orderby path segment.
40
     *
41
     * @var callable[]
42
     */
43
    private $comparisonFunctions = [];
44
45
    /**
46
     * The top level sorter function generated from orderby path segments.
47
     *
48
     * @var callable
49
     */
50
    private $topLevelComparisonFunction;
51
52
    /**
53
     * The structure holds information about the navigation properties
54
     * used in the orderby clause (if any), and orderby path if IDSQP
55
     * implementor want to perform sorting.
56
     *
57
     * @var OrderByInfo
58
     */
59
    private $orderByInfo;
60
61
    /**
62
     * Reference to metadata and query provider wrapper.
63
     *
64
     * @var ProvidersWrapper
65
     */
66
    private $providerWrapper;
67
68
    /**
69
     * This object will be of type of the resource set identified by the request uri.
70
     *
71
     * @var mixed
72
     */
73
    private $dummyObject;
74
75
    /**
76
     * Root node for tree ordering.
77
     *
78
     * @var OrderByNode
79
     */
80
    private $rootOrderByNode;
81
82
    /**
83
     * Creates new instance of OrderByParser.
84
     *
85
     * @param ProvidersWrapper $providerWrapper Reference to metadata
86
     *                                          and query provider
87
     *                                          wrapper
88
     */
89
    private function __construct(ProvidersWrapper $providerWrapper)
90
    {
91
        $this->providerWrapper = $providerWrapper;
92
    }
93
94
    /**
95
     * This function perform the following tasks with the help of internal helper
96
     * functions
97
     * (1) Read the orderby clause and perform basic syntax checks
98
     * (2) Build 'Order By Tree', creates anonymous sorter function for each leaf node and check for error
99
     * (3) Build 'OrderInfo' structure, holds information about the navigation
100
     *     properties used in the orderby clause (if any) and orderby path if
101
     *     IDSQP implementor want to perform sorting
102
     * (4) Build top level anonymous sorter function
103
     * (4) Release resources hold by the 'Order By Tree'
104
     * (5) Create 'InternalOrderInfo' structure, which wraps 'OrderInfo' and top
105
     *     level sorter function.
106
     *
107
     * @param ResourceSetWrapper $resourceSetWrapper ResourceSetWrapper for the resource targeted by resource path
108
     * @param ResourceType       $resourceType       ResourceType for the resource targeted by resource path
109
     * @param string             $orderBy            The orderby clause
110
     * @param ProvidersWrapper   $providerWrapper    Reference to the wrapper for IDSQP and IDSMP impl
111
     *
112
     * @throws InvalidOperationException
113
     * @throws ReflectionException
114
     * @throws ODataException            If any error occur while parsing orderby clause
115
     * @return InternalOrderByInfo
116
     */
117
    public static function parseOrderByClause(
118
        ResourceSetWrapper $resourceSetWrapper,
119
        ResourceType $resourceType,
120
        string $orderBy,
121
        ProvidersWrapper $providerWrapper
122
    ): InternalOrderByInfo {
123
        if (0 == strlen(trim($orderBy))) {
124
            throw new InvalidOperationException('OrderBy clause must not be trimmable to an empty string');
125
        }
126
        $orderByParser = new self($providerWrapper);
127
        try {
128
            $instance = $resourceType->getInstanceType();
129
            assert($instance instanceof ReflectionClass, get_class($instance));
130
            $orderByParser->dummyObject = $instance->newInstanceArgs([]);
131
        } catch (ReflectionException $reflectionException) {
132
            throw ODataException::createInternalServerError(Messages::orderByParserFailedToCreateDummyObject());
133
        }
134
        $orderByParser->rootOrderByNode = new OrderByRootNode($resourceSetWrapper, $resourceType);
135
        $orderByPathSegments            = $orderByParser->readOrderBy($orderBy);
136
137
        $orderByParser->buildOrderByTree($orderByPathSegments);
138
        $orderByParser->createOrderInfo($orderByPathSegments);
139
        $orderByParser->generateTopLevelComparisonFunction();
140
        //Recursively release the resources
141
        $orderByParser->rootOrderByNode->free();
142
        //creates internal order info wrapper
143
        $internalOrderInfo = new InternalOrderByInfo(
144
            $orderByParser->orderByInfo,
145
            $orderByParser->comparisonFunctions,
146
            $orderByParser->topLevelComparisonFunction,
147
            $orderByParser->dummyObject,
148
            $resourceType
149
        );
150
        unset($orderByParser->orderByInfo);
151
        unset($orderByParser->topLevelComparisonFunction);
152
153
        return $internalOrderInfo;
154
    }
155
156
    /**
157
     * Read orderby clause.
158
     *
159
     * @param string $value orderby clause to read
160
     *
161
     * @throws ODataException If any syntax error found while reading the clause
162
     * @return array<array>   An array of 'OrderByPathSegment's, each of which
163
     *                        is array of 'OrderBySubPathSegment's
164
     */
165
    private function readOrderBy(string $value): array
166
    {
167
        $orderByPathSegments = [];
168
        $lexer               = new ExpressionLexer($value);
169
        $i                   = 0;
170
        while ($lexer->getCurrentToken()->getId() != ExpressionTokenId::END()) {
171
            $orderBySubPathSegment = $lexer->readDottedIdentifier();
172
            if (!array_key_exists($i, $orderByPathSegments)) {
173
                $orderByPathSegments[$i] = [];
174
            }
175
176
            $orderByPathSegments[$i][] = $orderBySubPathSegment;
177
            $tokenId                   = $lexer->getCurrentToken()->getId();
178
            if ($tokenId != ExpressionTokenId::END()) {
179
                if ($tokenId != ExpressionTokenId::SLASH()) {
180
                    if ($tokenId != ExpressionTokenId::COMMA()) {
181
                        $lexer->validateToken(ExpressionTokenId::IDENTIFIER());
182
                        $identifier = $lexer->getCurrentToken()->Text;
183
                        if ($identifier !== 'asc' && $identifier !== 'desc') {
184
                            // force lexer to throw syntax error as we found
185
                            // unexpected identifier
186
                            $lexer->validateToken(ExpressionTokenId::DOT());
187
                        }
188
189
                        $orderByPathSegments[$i][] = '*' . $identifier;
190
                        $lexer->nextToken();
191
                        $tokenId = $lexer->getCurrentToken()->getId();
192
                        if ($tokenId != ExpressionTokenId::END()) {
193
                            $lexer->validateToken(ExpressionTokenId::COMMA());
194
                            ++$i;
195
                        }
196
                    } else {
197
                        ++$i;
198
                    }
199
                }
200
201
                $lexer->nextToken();
202
            }
203
        }
204
205
        return $orderByPathSegments;
206
    }
207
208
    /**
209
     * Build 'OrderBy Tree' from the given orderby path segments, also build
210
     * comparison function for each path segment.
211
     *
212
     * @param array(array) &$orderByPathSegments Collection of orderby path segments,
213
     *                                           this is passed by reference
214
     *                                           since we need this function to
215
     *                                           modify this array in two cases:
216
     *                                           1. if asc or desc present, then the
217
     *                                           corresponding sub path segment
218
     *                                           should be removed
219
     *                                           2. remove duplicate orderby path
220
     *                                           segment
221
     *
222
     * @throws ReflectionException
223
     * @throws ODataException      If any error occurs while processing the orderby path segments
224
     * @return mixed
225
     */
226
    private function buildOrderByTree(array &$orderByPathSegments)
227
    {
228
        foreach ($orderByPathSegments as $index1 => &$orderBySubPathSegments) {
229
            /** @var OrderByNode $currentNode */
230
            $currentNode   = $this->rootOrderByNode;
231
            $currentObject = $this->dummyObject;
232
            $ascending     = true;
233
            $subPathCount  = count($orderBySubPathSegments);
234
            // Check sort order is specified in the path, if so set a
235
            // flag and remove that segment
236
            if ($subPathCount > 1) {
237
                if ('*desc' === $orderBySubPathSegments[$subPathCount - 1]) {
238
                    $ascending = false;
239
                    unset($orderBySubPathSegments[$subPathCount - 1]);
240
                    --$subPathCount;
241
                } elseif ('*asc' === $orderBySubPathSegments[$subPathCount - 1]) {
242
                    unset($orderBySubPathSegments[$subPathCount - 1]);
243
                    --$subPathCount;
244
                }
245
            }
246
            $ancestors = [$this->rootOrderByNode->getResourceSetWrapper()->getName()];
247
            foreach ($orderBySubPathSegments as $index2 => $orderBySubPathSegment) {
248
                $isLastSegment      = ($index2 == $subPathCount - 1);
249
                $resourceSetWrapper = null;
250
                /** @var ResourceEntityType $resourceType */
251
                $resourceType = $currentNode->getResourceType();
252
                /** @var ResourceProperty $resourceProperty */
253
                $resourceProperty = $resourceType->resolveProperty($orderBySubPathSegment);
254
                if (null === $resourceProperty) {
255
                    throw ODataException::createSyntaxError(
256
                        Messages::orderByParserPropertyNotFound(
257
                            $resourceType->getFullName(),
258
                            $orderBySubPathSegment
259
                        )
260
                    );
261
                }
262
                /** @var ResourcePropertyKind $rKind */
263
                $rKind   = $resourceProperty->getKind();
264
                $rawKind = ($rKind instanceof ResourcePropertyKind) ? $rKind : $rKind;
265
266
                if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG())) {
267
                    throw ODataException::createBadRequestError(
268
                        Messages::orderByParserBagPropertyNotAllowed(
269
                            $resourceProperty->getName()
270
                        )
271
                    );
272
                } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE())) {
273
                    if (!$isLastSegment) {
274
                        throw ODataException::createBadRequestError(
275
                            Messages::orderByParserPrimitiveAsIntermediateSegment(
276
                                $resourceProperty->getName()
277
                            )
278
                        );
279
                    }
280
281
                    $type = $resourceProperty->getInstanceType();
282
                    if ($type instanceof Binary) {
283
                        throw ODataException::createBadRequestError(
284
                            Messages::orderByParserSortByBinaryPropertyNotAllowed($resourceProperty->getName())
285
                        );
286
                    }
287
                } elseif ($rawKind == ResourcePropertyKind::RESOURCESET_REFERENCE()
288
                    || $rawKind == ResourcePropertyKind::RESOURCE_REFERENCE()
289
                ) {
290
                    $this->assertion($currentNode instanceof OrderByRootNode || $currentNode instanceof OrderByNode);
291
                    $resourceSetWrapper = $currentNode->getResourceSetWrapper();
292
                    $this->assertion(null !== $resourceSetWrapper);
293
                    $resourceSetWrapper
294
                        = $this->providerWrapper->getResourceSetWrapperForNavigationProperty(
295
                            $resourceSetWrapper,
296
                            $resourceType,
297
                            $resourceProperty
298
                        );
299
                    if (null === $resourceSetWrapper) {
300
                        throw ODataException::createBadRequestError(
301
                            Messages::badRequestInvalidPropertyNameSpecified(
302
                                $resourceType->getFullName(),
303
                                $orderBySubPathSegment
304
                            )
305
                        );
306
                    }
307
308
                    if ($rKind == ResourcePropertyKind::RESOURCESET_REFERENCE()) {
309
                        throw ODataException::createBadRequestError(
310
                            Messages::orderByParserResourceSetReferenceNotAllowed(
311
                                $resourceProperty->getName(),
312
                                $resourceType->getFullName()
313
                            )
314
                        );
315
                    }
316
317
                    $resourceSetWrapper->checkResourceSetRightsForRead(true);
318
                    if ($isLastSegment) {
319
                        throw ODataException::createBadRequestError(
320
                            Messages::orderByParserSortByNavigationPropertyIsNotAllowed(
321
                                $resourceProperty->getName()
322
                            )
323
                        );
324
                    }
325
326
                    $ancestors[] = $orderBySubPathSegment;
327
                } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE())) {
328
                    if ($isLastSegment) {
329
                        throw ODataException::createBadRequestError(
330
                            Messages::orderByParserSortByComplexPropertyIsNotAllowed(
331
                                $resourceProperty->getName()
332
                            )
333
                        );
334
                    }
335
336
                    $ancestors[] = $orderBySubPathSegment;
337
                } else {
338
                    throw ODataException::createInternalServerError(
339
                        Messages::orderByParserUnexpectedPropertyType()
340
                    );
341
                }
342
343
                $node = $currentNode->findNode($orderBySubPathSegment);
344
                if (null === $node) {
345
                    if ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE())) {
346
                        $node = new OrderByLeafNode(
347
                            $orderBySubPathSegment,
348
                            $resourceProperty,
349
                            $ascending
350
                        );
351
                        $this->comparisonFunctions[] = $node->buildComparisonFunction($ancestors);
352
                    } elseif ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCE_REFERENCE()) {
353
                        $node = new OrderByNode(
354
                            $orderBySubPathSegment,
355
                            $resourceProperty,
356
                            $resourceSetWrapper
357
                        );
358
                        // Initialize this member variable (identified by
359
                        // $resourceProperty) of parent object.
360
                        try {
361
                            $instance = $resourceProperty->getInstanceType();
362
                            assert($instance instanceof ReflectionClass, get_class($instance));
363
                            $object = $instance->newInstanceArgs();
364
                            $resourceType->setPropertyValue($currentObject, $resourceProperty->getName(), $object);
365
                            $currentObject = $object;
366
                        } catch (ReflectionException $reflectionException) {
367
                            $this->throwBadAccessOrInitException($resourceProperty, $resourceType);
368
                        }
369
                    } elseif ($resourceProperty->getKind() == ResourcePropertyKind::COMPLEX_TYPE()) {
370
                        $node = new OrderByNode($orderBySubPathSegment, $resourceProperty, null);
371
                        // Initialize this member variable
372
                        // (identified by $resourceProperty)of parent object.
373
                        try {
374
                            $instance = $resourceProperty->getInstanceType();
375
                            assert($instance instanceof ReflectionClass, get_class($instance));
376
                            $object = $instance->newInstanceArgs();
377
                            $resourceType->setPropertyValue($currentObject, $resourceProperty->getName(), $object);
378
                            $currentObject = $object;
379
                        } catch (ReflectionException $reflectionException) {
380
                            $this->throwBadAccessOrInitException($resourceProperty, $resourceType);
381
                        }
382
                    }
383
384
                    $currentNode->addNode($node);
385
                } else {
386
                    try {
387
                        $currentObject = ReflectionHandler::getProperty($currentObject, $resourceProperty->getName());
388
                    } catch (ReflectionException $reflectionException) {
389
                        $this->throwBadAccessOrInitException($resourceProperty, $resourceType);
390
                    }
391
392
                    if ($node instanceof OrderByLeafNode) {
393
                        //remove duplicate orderby path
394
                        unset($orderByPathSegments[$index1]);
395
                    }
396
                }
397
398
                $currentNode = $node;
399
            }
400
        }
401
        return null;
402
    }
403
404
    /**
405
     * Assert that the given condition is true, if false throw ODataException for unexpected state.
406
     *
407
     * @param bool $condition The condition to assert
408
     *
409
     * @throws ODataException
410
     */
411
    private function assertion(bool $condition): void
412
    {
413
        if (!$condition) {
414
            throw ODataException::createInternalServerError(Messages::orderByParserUnExpectedState());
415
        }
416
    }
417
418
    /**
419
     * @param  ResourceProperty $resourceProperty
420
     * @param  ResourceType     $resourceType
421
     * @throws ODataException
422
     */
423
    private function throwBadAccessOrInitException(
424
        ResourceProperty $resourceProperty,
425
        ResourceType $resourceType
426
    ): void {
427
        throw ODataException::createInternalServerError(
428
            Messages::orderByParserFailedToAccessOrInitializeProperty(
429
                $resourceProperty->getName(),
430
                $resourceType->getName()
431
            )
432
        );
433
    }
434
435
    /**
436
     * Traverse 'Order By Tree' and create 'OrderInfo' structure.
437
     *
438
     * @param array<array> $orderByPaths The orderby paths
439
     *
440
     * @throws ODataException If parser finds an inconsistent-tree state, throws unexpected state error
441
     * @return void
442
     */
443
    private function createOrderInfo(array $orderByPaths): void
444
    {
445
        $orderByPathSegments           = [];
446
        $navigationPropertiesInThePath = [];
447
        foreach ($orderByPaths as $index => $orderBySubPaths) {
448
            /** @var OrderByNode $currentNode */
449
            $currentNode            = $this->rootOrderByNode;
450
            $orderBySubPathSegments = [];
451
            foreach ($orderBySubPaths as $orderBySubPath) {
452
                /** @var OrderByNode $node */
453
                $node = $currentNode->findNode($orderBySubPath);
454
                $this->assertion(null !== $node);
455
                $resourceProperty = $node->getResourceProperty();
456
                if ($node instanceof OrderByNode && null !== $node->getResourceSetWrapper()) {
457
                    if (!array_key_exists($index, $navigationPropertiesInThePath)) {
458
                        $navigationPropertiesInThePath[$index] = [];
459
                    }
460
461
                    $navigationPropertiesInThePath[$index][] = $resourceProperty;
462
                }
463
464
                $orderBySubPathSegments[] = new OrderBySubPathSegment($resourceProperty);
465
                /** @var OrderByLeafNode $currentNode */
466
                $currentNode = $node;
467
            }
468
469
            $this->assertion($currentNode instanceof OrderByLeafNode);
470
            /** @var OrderByLeafNode $newNode */
471
            $newNode               = $currentNode;
472
            $orderByPathSegments[] = new OrderByPathSegment($orderBySubPathSegments, $newNode->isAscending());
473
            unset($orderBySubPathSegments);
474
        }
475
476
        $this->orderByInfo = new OrderByInfo(
477
            $orderByPathSegments,
478
            empty($navigationPropertiesInThePath) ? null : $navigationPropertiesInThePath
479
        );
480
    }
481
482
    /**
483
     * Generates top level comparison function from sub comparison functions.
484
     * @throws ODataException
485
     */
486
    private function generateTopLevelComparisonFunction(): void
487
    {
488
        $comparisonFunctionCount = count($this->comparisonFunctions);
489
        $this->assertion(0 < $comparisonFunctionCount);
490
        if (1 == $comparisonFunctionCount) {
491
            $this->topLevelComparisonFunction = $this->comparisonFunctions[0];
492
        } else {
493
            $funcList                         = $this->comparisonFunctions;
494
            $this->topLevelComparisonFunction = function ($object1, $object2) use ($funcList) {
495
                $ret = 0;
496
                foreach ($funcList as $f) {
497
                    $ret = $f($object1, $object2);
498
                    if (0 != $ret) {
499
                        return $ret;
500
                    }
501
                }
502
                return $ret;
503
            };
504
        }
505
    }
506
}
507