Completed
Push — master ( 55beca...b509a7 )
by Bálint
03:58
created

OrderByParser::_readOrderBy()   B

Complexity

Conditions 9
Paths 15

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 42
rs 7.6924
c 0
b 0
f 0
cc 9
nc 15
nop 1
1
<?php
2
3
namespace POData\UriProcessor\QueryProcessor\OrderByParser;
4
5
use POData\UriProcessor\QueryProcessor\AnonymousFunction;
6
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionLexer;
7
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionTokenId;
8
use POData\Providers\ProvidersWrapper;
9
use POData\Providers\Metadata\Type\Binary;
10
use POData\Providers\Metadata\ResourceSetWrapper;
11
use POData\Providers\Metadata\ResourceType;
12
use POData\Providers\Metadata\ResourcePropertyKind;
13
use POData\Common\ODataException;
14
use POData\Common\Messages;
15
16
/**
17
 * Class OrderByParser
18
 *
19
 * Class to parse $orderby query option and perform syntax validation
20
 * and build 'OrderBy Tree' along with next level of validation, the
21
 * created tree is used for building sort functions and 'OrderByInfo' structure.
22
 *
23
 * The syntax of orderby clause is:
24
 *
25
 * OrderByClause         : OrderByPathSegment [, OrderByPathSegment]*
26
 * OrderByPathSegment    : OrderBySubPathSegment[/OrderBySubPathSegment]*[asc|desc]?
27
 * OrderBySubPathSegment : identifier
28
 *
29
 * @package POData\UriProcessor\QueryProcessor\OrderByParser
30
 */
31
class OrderByParser
32
{
33
34
    /**
35
     * The top level sorter function generated from orderby path
36
     * segments.
37
     *
38
     * @var AnonymousFunction
39
     */
40
    private $_topLevelComparisonFunction;
0 ignored issues
show
Unused Code introduced by
The property $_topLevelComparisonFunction is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
41
42
    /**
43
     * The structure holds information about the navigation properties
44
     * used in the orderby clause (if any) and orderby path if IDSQP
45
     * implementor want to perform sorting.
46
     *
47
     * @var OrderByInfo
48
     */
49
    private $_orderByInfo;
50
51
    /**
52
     * Reference to metadata and query provider wrapper
53
     *
54
     * @var ProvidersWrapper
55
     */
56
    private $_providerWrapper;
57
58
    /**
59
     * This object will be of type of the resource set identified by the
60
     * request uri.
61
     *
62
     * @var mixed
63
     */
64
    private $_dummyObject;
65
66
    /**
67
     * Creates new instance of OrderByParser
68
     *
69
     * @param ProvidersWrapper $providerWrapper Reference to metadata
70
     *                                                      and query provider
71
     *                                                      wrapper
72
     */
73
    private function __construct(ProvidersWrapper $providerWrapper)
74
    {
75
        $this->_providerWrapper = $providerWrapper;
76
    }
77
78
    /**
79
     * This function perform the following tasks with the help of internal helper
80
     * functions
81
     * (1) Read the orderby clause and perform basic syntax errors
82
     * (2) Build 'Order By Tree', creates anonymous sorter function for each leaf
83
     *     node and check for error
84
     * (3) Build 'OrderInfo' structure, holds information about the navigation
85
     *     properties used in the orderby clause (if any) and orderby path if
86
     *     IDSQP implementor want to perform sorting
87
     * (4) Build top level anonymous sorter function
88
     * (4) Release resources hold by the 'Order By Tree'
89
     * (5) Create 'InternalOrderInfo' structure, which wraps 'OrderInfo' and top
90
     *     level sorter function
91
     *
92
     * @param ResourceSetWrapper           $resourceSetWrapper ResourceSetWrapper for the resource targeted by resource path.
93
     * @param ResourceType                 $resourceType       ResourceType for the resource targeted by resource path.
94
     * @param string                       $orderBy            The orderby clause.
95
     * @param ProvidersWrapper $providerWrapper    Reference to the wrapper for IDSQP and IDSMP impl.
96
     *
97
     * @return InternalOrderByInfo
98
     *
99
     * @throws ODataException If any error occur while parsing orderby clause
100
     */
101
    public static function parseOrderByClause(
102
        ResourceSetWrapper $resourceSetWrapper,
103
        ResourceType $resourceType,
104
        $orderBy,
105
        ProvidersWrapper $providerWrapper
106
    ) {
107
        $orderByParser = new OrderByParser($providerWrapper);
108
        try {
109
            $orderByParser->_dummyObject = $resourceType->getInstanceType()->newInstanceWithoutConstructor();
110
        } catch (\ReflectionException $reflectionException) {
111
            throw ODataException::createInternalServerError(Messages::orderByParserFailedToCreateDummyObject());
112
        }
113
        $orderByParser->_rootOrderByNode = new OrderByRootNode($resourceSetWrapper, $resourceType);
0 ignored issues
show
Bug introduced by
The property _rootOrderByNode does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
114
        $orderByPathSegments = $orderByParser->_readOrderBy($orderBy);
115
        $orderByParser->_buildOrderByTree($orderByPathSegments);
116
        $orderByParser->_createOrderInfo($orderByPathSegments);
117
        //Recursively release the resources
118
        $orderByParser->_rootOrderByNode->free();
119
        //creates internal order info wrapper
120
        $internalOrderInfo = new InternalOrderByInfo(
121
            $orderByParser->_orderByInfo,
122
            $orderByParser->_dummyObject
123
        );
124
        unset($orderByParser->_orderByInfo);
125
        unset($orderByParser->_topLevelComparisonFunction);
126
        return $internalOrderInfo;
127
    }
128
129
    /**
130
     * Build 'OrderBy Tree' from the given orderby path segments, also build
131
     * comparsion function for each path segment.
132
     *
133
     * @param array(array) &$orderByPathSegments Collection of orderby path segments,
134
     *                                           this is passed by reference
135
     *                                           since we need this function to
136
     *                                           modify this array in two cases:
137
     *                                           1. if asc or desc present, then the
138
     *                                              corresponding sub path segment
139
     *                                              should be removed
140
     *                                           2. remove duplicate orderby path
141
     *                                              segment
142
     *
143
     * @return void
144
     *
145
     * @throws ODataException If any error occurs while processing the orderby path
146
     *                        segments
147
     */
148
    private function _buildOrderByTree(&$orderByPathSegments)
149
    {
150
        foreach ($orderByPathSegments as $index1 => &$orderBySubPathSegments) {
151
            $currentNode = $this->_rootOrderByNode;
152
            $currentObject = $this->_dummyObject;
153
            $ascending = true;
154
            $subPathCount = count($orderBySubPathSegments);
155
            // Check sort order is specified in the path, if so set a
156
            // flag and remove that segment
157
            if ($subPathCount > 1) {
158
                if ($orderBySubPathSegments[$subPathCount - 1] === '*desc') {
159
                    $ascending = false;
160
                    unset($orderBySubPathSegments[$subPathCount - 1]);
161
                    $subPathCount--;
162
                } else if ($orderBySubPathSegments[$subPathCount - 1] === '*asc') {
163
                    unset($orderBySubPathSegments[$subPathCount - 1]);
164
                    $subPathCount--;
165
                }
166
            }
167
168
            $ancestors = array($this->_rootOrderByNode->getResourceSetWrapper()->getName());
169
            foreach ($orderBySubPathSegments as $index2 => $orderBySubPathSegment) {
170
                $isLastSegment = ($index2 == $subPathCount - 1);
171
                $resourceSetWrapper = null;
172
                $resourceType = $currentNode->getResourceType();
173
                $resourceProperty = $resourceType->resolveProperty($orderBySubPathSegment);
174
                if (is_null($resourceProperty)) {
175
                    throw ODataException::createSyntaxError(
176
                        Messages::orderByParserPropertyNotFound(
177
                            $resourceType->getFullName(), $orderBySubPathSegment
178
                        )
179
                    );
180
                }
181
182
                if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG)) {
183
                    throw ODataException::createBadRequestError(
184
                        Messages::orderByParserBagPropertyNotAllowed(
185
                            $resourceProperty->getName()
186
                        )
187
                    );
188
                } else if ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) {
189
                    if (!$isLastSegment) {
190
                        throw ODataException::createBadRequestError(
191
                            Messages::orderByParserPrimitiveAsIntermediateSegment(
192
                                $resourceProperty->getName()
193
                            )
194
                        );
195
                    }
196
197
                    $type = $resourceProperty->getInstanceType();
198
                    if ($type instanceof Binary) {
199
                        throw ODataException::createBadRequestError(Messages::orderByParserSortByBinaryPropertyNotAllowed($resourceProperty->getName()));
200
                    }
201
                } else if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE
202
                    || $resourceProperty->getKind() == ResourcePropertyKind::RESOURCE_REFERENCE
203
                    || $resourceProperty->getKind() == ResourcePropertyKind::KEY_RESOURCE_REFERENCE
204
                ) {
205
                    $this->_assertion($currentNode instanceof OrderByRootNode || $currentNode instanceof OrderByNode);
206
                    $resourceSetWrapper = $currentNode->getResourceSetWrapper();
207
                    $this->_assertion(!is_null($resourceSetWrapper));
208
                    $resourceSetWrapper
209
                        = $this->_providerWrapper->getResourceSetWrapperForNavigationProperty(
210
                            $resourceSetWrapper, $resourceType, $resourceProperty
211
                        );
212
                    if (is_null($resourceSetWrapper)) {
213
                        throw ODataException::createBadRequestError(
214
                            Messages::badRequestInvalidPropertyNameSpecified(
215
                                $resourceType->getFullName(), $orderBySubPathSegment
216
                            )
217
                        );
218
                    }
219
220
                    if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE) {
221
                        throw ODataException::createBadRequestError(
222
                            Messages::orderByParserResourceSetReferenceNotAllowed(
223
                                $resourceProperty->getName(), $resourceType->getFullName()
224
                            )
225
                        );
226
                    }
227
228
                    $resourceSetWrapper->checkResourceSetRightsForRead(true);
229
                    /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
230
                    if ($isLastSegment) {
231
						throw ODataException::createBadRequestError(
232
                            Messages::orderByParserSortByNavigationPropertyIsNotAllowed(
233
                                $resourceProperty->getName()
234
                            )
235
                        );
236
                    }
237
                    */
238
239
                    $ancestors[] = $orderBySubPathSegment;
240
                } else if ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE)) {
241
                    if ($isLastSegment) {
242
                        throw ODataException::createBadRequestError(
243
                            Messages::orderByParserSortByComplexPropertyIsNotAllowed(
244
                                $resourceProperty->getName()
245
                            )
246
                        );
247
                    }
248
249
                    $ancestors[] = $orderBySubPathSegment;
250
                } else {
251
                    throw ODataException::createInternalServerError(
252
                        Messages::orderByParserUnexpectedPropertyType()
253
                    );
254
                }
255
256
                $node = $currentNode->findNode($orderBySubPathSegment);
257
                if (is_null($node)) {
258
                    if ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE) || $resourceProperty->isKindOf(ResourcePropertyKind::KEY_RESOURCE_REFERENCE)) {
259
                        $node = new OrderByLeafNode(
260
                            $orderBySubPathSegment, $resourceProperty,
261
                            $ascending
262
                        );
263
                    } else if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCE_REFERENCE || $resourceProperty->getKind() == ResourcePropertyKind::KEY_RESOURCE_REFERENCE) {
264
                        $node = new OrderByNode(
265
                            $orderBySubPathSegment, $resourceProperty,
266
                            $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...
267
                        );
268
                        // Initialize this member variable (identified by
269
                        // $resourceProperty) of parent object.
270
                        try {
271
                            $dummyProperty
272
                                = new \ReflectionProperty(
273
                                    $currentObject, $resourceProperty->getName()
274
                                );
275
                            $object = $resourceProperty->getInstanceType()->newInstanceWithoutConstructor();
276
                            $dummyProperty->setAccessible(true);
277
                            $dummyProperty->setValue($currentObject, $object);
278
                            $currentObject = $object;
279
                        } catch (\ReflectionException $reflectionException) {
280
                            throw ODataException::createInternalServerError(
281
                                Messages::orderByParserFailedToAccessOrInitializeProperty(
282
                                    $resourceProperty->getName(), $resourceType->getName()
283
                                )
284
                            );
285
                        }
286
                    } else if ($resourceProperty->getKind() == ResourcePropertyKind::COMPLEX_TYPE) {
287
                        $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...
288
                        // Initialize this member variable
289
                        // (identified by $resourceProperty)of parent object.
290
                        try {
291
                            $dummyProperty
292
                                = new \ReflectionProperty(
293
                                    $currentObject, $resourceProperty->getName()
294
                                );
295
                            $object = $resourceProperty->getInstanceType()->newInstanceWithoutConstructor();
296
                            $dummyProperty->setValue($currentObject, $object);
297
                            $currentObject = $object;
298
                        } catch (\ReflectionException $reflectionException) {
299
                            throw ODataException::createInternalServerError(
300
                                Messages::orderByParserFailedToAccessOrInitializeProperty(
301
                                    $resourceProperty->getName(), $resourceType->getName()
302
                                )
303
                            );
304
                        }
305
                    }
306
307
                    $currentNode->addNode($node);
308
                } else {
309
                    try {
310
                        $reflectionClass = new \ReflectionClass(get_class($currentObject));
311
                        $reflectionProperty = $reflectionClass->getProperty($resourceProperty->getName());
312
                        $reflectionProperty->setAccessible(true);
313
                        $currentObject = $reflectionProperty->getValue($currentObject);
314
315
                        //$dummyProperty = new \ReflectionProperty(
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
316
                        //    $currentObject, $resourceProperty->getName()
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
317
                        //);
318
                        //$currentObject = $dummyProperty->getValue($currentObject);
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
319
                    } catch (\ReflectionException $reflectionException) {
320
                            throw ODataException::createInternalServerError(
321
                                Messages::orderByParserFailedToAccessOrInitializeProperty(
322
                                    $resourceProperty->getName(),
323
                                    $resourceType->getName()
324
                                )
325
                            );
326
                    }
327
328
                    if ($node instanceof OrderByLeafNode) {
329
                        //remove duplicate orderby path
330
                        unset($orderByPathSegments[$index1]);
331
                    }
332
                }
333
334
                $currentNode = $node;
335
            }
336
        }
337
    }
338
339
    /**
340
     * Traverse 'Order By Tree' and create 'OrderInfo' structure
341
     *
342
     * @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...
343
     *
344
     * @return OrderByInfo
345
     *
346
     * @throws ODataException In case parser found any tree inconsisitent
347
     *                        state, throws unexpected state error
348
     */
349
    private function _createOrderInfo($orderByPaths)
350
    {
351
        $orderByPathSegments = array();
352
        $navigationPropertiesInThePath = array();
353
        foreach ($orderByPaths as $index => $orderBySubPaths) {
354
            $currentNode = $this->_rootOrderByNode;
355
            $orderBySubPathSegments = array();
356
            foreach ($orderBySubPaths as $orderBySubPath) {
357
                $node = $currentNode->findNode($orderBySubPath);
358
                $this->_assertion(!is_null($node));
359
                $resourceProperty = $node->getResourceProperty();
360
                if ($node instanceof OrderByNode && !is_null($node->getResourceSetWrapper())) {
361
                    if (!array_key_exists($index, $navigationPropertiesInThePath)) {
362
                        $navigationPropertiesInThePath[$index] = array();
363
                    }
364
365
                    $navigationPropertiesInThePath[$index][] = $resourceProperty;
366
                }
367
368
                $orderBySubPathSegments[] = new OrderBySubPathSegment($resourceProperty);
369
                $currentNode = $node;
370
            }
371
372
            $this->_assertion($currentNode instanceof OrderByLeafNode);
373
            $orderByPathSegments[] = new OrderByPathSegment($orderBySubPathSegments, $currentNode->isAscending());
374
            unset($orderBySubPathSegments);
375
        }
376
377
        $this->_orderByInfo = new OrderByInfo($orderByPathSegments, empty($navigationPropertiesInThePath) ? null : $navigationPropertiesInThePath);
378
    }
379
380
    /**
381
     * Read orderby clause.
382
     *
383
     * @param string $value orderby clause to read.
384
     *
385
     * @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...
386
     *                      is array of 'OrderBySubPathSegment's
387
     *
388
     * @throws ODataException If any syntax error found while reading the clause
389
     */
390
    private function _readOrderBy($value)
391
    {
392
        $orderByPathSegments = array();
393
        $lexer = new ExpressionLexer($value);
394
        $i = 0;
395
        while ($lexer->getCurrentToken()->Id != ExpressionTokenId::END) {
396
            $orderBySubPathSegment = $lexer->readDottedIdentifier();
397
            if (!array_key_exists($i, $orderByPathSegments)) {
398
                $orderByPathSegments[$i] = array();
399
            }
400
401
            $orderByPathSegments[$i][] = $orderBySubPathSegment;
402
            $tokenId = $lexer->getCurrentToken()->Id;
403
            if ($tokenId != ExpressionTokenId::END) {
404
                if ($tokenId != ExpressionTokenId::SLASH) {
405
                    if ($tokenId != ExpressionTokenId::COMMA) {
406
                        $lexer->validateToken(ExpressionTokenId::IDENTIFIER);
407
                        $identifier = $lexer->getCurrentToken()->Text;
408
                        if ($identifier !== 'asc' && $identifier !== 'desc') {
409
                            // force lexer to throw syntax error as we found
410
                            // unexpected identifier
411
                            $lexer->validateToken(ExpressionTokenId::DOT);
412
                        }
413
414
                        $orderByPathSegments[$i][] = '*' . $identifier;
415
                        $lexer->nextToken();
416
                        $tokenId = $lexer->getCurrentToken()->Id;
417
                        if ($tokenId != ExpressionTokenId::END) {
418
                            $lexer->validateToken(ExpressionTokenId::COMMA);
419
                            $i++;
420
                        }
421
                    } else {
422
                        $i++;
423
                    }
424
                }
425
426
                $lexer->nextToken();
427
            }
428
        }
429
430
        return $orderByPathSegments;
431
    }
432
433
    /**
434
     * Assert that the given condition is true, if false throw
435
     * ODataException for unexpected state
436
     *
437
     * @param boolean $condition The condition to assert
438
     *
439
     * @return void
440
     *
441
     * @throws ODataException
442
     */
443
    private function _assertion($condition)
444
    {
445
        if (!$condition) {
446
            throw ODataException::createInternalServerError(Messages::orderByParserUnExpectedState());
447
        }
448
    }
449
}
450