OrderByParser::_buildOrderByTree()   D
last analyzed

Complexity

Conditions 26
Paths 109

Size

Total Lines 145
Code Lines 87

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 89
CRAP Score 27.4011

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 87
c 4
b 0
f 0
dl 0
loc 145
ccs 89
cts 102
cp 0.8725
rs 4.0916
cc 26
nc 109
nop 1
crap 27.4011

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\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\Providers\Metadata\ResourceTypeKind;
14
use POData\Common\ODataException;
15
use POData\Common\Messages;
16
use POData\Providers\Metadata\ResourceProperty;
17
use stdClass;
18
19
/**
20
 * Class OrderByParser
21
 *
22
 * Class to parse $orderby query option and perform syntax validation
23
 * and build 'OrderBy Tree' along with next level of validation, the
24
 * created tree is used for building sort functions and 'OrderByInfo' structure.
25
 *
26
 * The syntax of orderby clause is:
27
 *
28
 * OrderByClause         : OrderByPathSegment [, OrderByPathSegment]*
29
 * OrderByPathSegment    : OrderBySubPathSegment[/OrderBySubPathSegment]*[asc|desc]?
30
 * OrderBySubPathSegment : identifier
31
 *
32
 * @package POData\UriProcessor\QueryProcessor\OrderByParser
33
 */
34
class OrderByParser
35
{
36
37
    /**
38
     * The top level sorter function generated from orderby path
39
     * segments.
40
     *
41
     * @var AnonymousFunction
42
     */
43
    private $_topLevelComparisonFunction;
44
45
    /**
46
     * The structure holds information about the navigation properties
47
     * used in the orderby clause (if any) and orderby path if IDSQP
48
     * implementor want to perform sorting.
49
     *
50
     * @var OrderByInfo
51
     */
52
    private $_orderByInfo;
53
54
    /**
55
     * Reference to metadata and query provider wrapper
56
     *
57
     * @var ProvidersWrapper
58
     */
59
    private $_providerWrapper;
60
61
    /**
62
     * This object will be of type of the resource set identified by the
63
     * request uri.
64
     *
65
     * @var mixed
66
     */
67
    private $_dummyObject;
68
69
	private $_rootOrderByNode;
70
    /**
71
     * Creates new instance of OrderByParser
72
     *
73
     * @param ProvidersWrapper $providerWrapper Reference to metadata
74
     *                                                      and query provider
75
     *                                                      wrapper
76
     */
77 80
    private function __construct(ProvidersWrapper $providerWrapper)
78
    {
79 80
        $this->_providerWrapper = $providerWrapper;
80
    }
81
82 80
    private static function _mock($resourceType): object {
83 80
		$instanceType = $resourceType->getInstanceType();
84 80
		$reflection = new \ReflectionClass($instanceType->name);
85 80
		if($reflection->isAbstract()) {
86
			return new stdClass();
87
		}
88 80
		$mock = $reflection->newInstanceWithoutConstructor();
89
90 80
		foreach ($reflection->getProperties() as $property) {
91 80
			$property->setAccessible(true);
92
93 80
			$type = $property->getType();
94 80
			if($resourceType instanceof ResourceProperty) {
95 6
				$resourceType  = $resourceType->getResourceType();
96
			}
97 80
			$resourceProperty = $resourceType->resolveProperty($property->getName());
98 80
			if(is_null($resourceProperty)) continue;
99
100 80
			$resourcePropertyType = $resourceProperty->getResourceType();
101 80
			$resourceKind = $resourcePropertyType->getResourceTypeKind();
102 80
			if ($type && !$type->allowsNull()) {
103
				if ($resourceKind === ResourceTypeKind::PRIMITIVE) {
104
					switch ($type->getName()) {
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

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

104
					switch ($type->/** @scrutinizer ignore-call */ getName()) {
Loading history...
105
						case 'int':
106
							$property->setValue($mock, 0);
107
							break;
108
						case 'string':
109
							$property->setValue($mock, '');
110
							break;
111
						case 'bool':
112
							$property->setValue($mock, false);
113
							break;
114
						case 'array':
115
							// If the property is of type array, set it as an empty array
116
							$property->setValue($mock, []);
117
							break;
118
						default:
119
							break;
120
					}
121
				} else {
122
					continue;
123
				}
124
			} else {
125
				// If the property allows null, set it to null
126 80
				$property->setValue($mock, null);
127
			}
128
		}
129
130 80
		return $mock;
131
	}
132
133
    /**
134
     * This function perform the following tasks with the help of internal helper
135
     * functions
136
     * (1) Read the orderby clause and perform basic syntax errors
137
     * (2) Build 'Order By Tree', creates anonymous sorter function for each leaf
138
     *     node and check for error
139
     * (3) Build 'OrderInfo' structure, holds information about the navigation
140
     *     properties used in the orderby clause (if any) and orderby path if
141
     *     IDSQP implementor want to perform sorting
142
     * (4) Build top level anonymous sorter function
143
     * (4) Release resources hold by the 'Order By Tree'
144
     * (5) Create 'InternalOrderInfo' structure, which wraps 'OrderInfo' and top
145
     *     level sorter function
146
     *
147
     * @param ResourceSetWrapper           $resourceSetWrapper ResourceSetWrapper for the resource targeted by resource path.
148
     * @param ResourceType                 $resourceType       ResourceType for the resource targeted by resource path.
149
     * @param string                       $orderBy            The orderby clause.
150
     * @param ProvidersWrapper $providerWrapper    Reference to the wrapper for IDSQP and IDSMP impl.
151
     *
152
     * @return InternalOrderByInfo
153
     *
154
     * @throws ODataException If any error occur while parsing orderby clause
155
     */
156 80
    public static function parseOrderByClause(
157
        ResourceSetWrapper $resourceSetWrapper,
158
        ResourceType $resourceType,
159
        $orderBy,
160
        ProvidersWrapper $providerWrapper
161
    ) {
162 80
        $orderByParser = new OrderByParser($providerWrapper);
163
        try {
164 80
            $orderByParser->_dummyObject = self::_mock($resourceType);
165
        } catch (\ReflectionException $reflectionException) {
166
            throw ODataException::createInternalServerError(Messages::orderByParserFailedToCreateDummyObject());
167
        }
168 80
        $orderByParser->_rootOrderByNode = new OrderByRootNode($resourceSetWrapper, $resourceType);
169 80
        $orderByPathSegments = $orderByParser->_readOrderBy($orderBy);
170 80
        $orderByParser->_buildOrderByTree($orderByPathSegments);
171 75
        $orderByParser->_createOrderInfo($orderByPathSegments);
172
        //Recursively release the resources
173 74
        $orderByParser->_rootOrderByNode->free();
174
        //creates internal order info wrapper
175 74
        $internalOrderInfo = new InternalOrderByInfo(
176 74
            $orderByParser->_orderByInfo,
177 74
            $orderByParser->_dummyObject
178 74
        );
179 74
        unset($orderByParser->_orderByInfo);
180 74
        unset($orderByParser->_topLevelComparisonFunction);
181 74
        return $internalOrderInfo;
182
    }
183
184
    /**
185
     * Build 'OrderBy Tree' from the given orderby path segments, also build
186
     * comparsion function for each path segment.
187
     *
188
     * @param array(array) &$orderByPathSegments Collection of orderby path segments,
189
     *                                           this is passed by reference
190
     *                                           since we need this function to
191
     *                                           modify this array in two cases:
192
     *                                           1. if asc or desc present, then the
193
     *                                              corresponding sub path segment
194
     *                                              should be removed
195
     *                                           2. remove duplicate orderby path
196
     *                                              segment
197
     *
198
     * @return void
199
     *
200
     * @throws ODataException If any error occurs while processing the orderby path
201
     *                        segments
202
     */
203 80
    private function _buildOrderByTree(&$orderByPathSegments)
204
    {
205 80
        foreach ($orderByPathSegments as $index1 => &$orderBySubPathSegments) {
206 80
            $currentNode = $this->_rootOrderByNode;
207 80
            $currentObject = $this->_dummyObject;
208 80
            $ascending = true;
209 80
            $subPathCount = count($orderBySubPathSegments);
210
            // Check sort order is specified in the path, if so set a
211
            // flag and remove that segment
212 80
            if ($subPathCount > 1) {
213 16
                if ($orderBySubPathSegments[$subPathCount - 1] === '*desc') {
214 3
                    $ascending = false;
215 3
                    unset($orderBySubPathSegments[$subPathCount - 1]);
216 3
                    $subPathCount--;
217 16
                } else if ($orderBySubPathSegments[$subPathCount - 1] === '*asc') {
218 9
                    unset($orderBySubPathSegments[$subPathCount - 1]);
219 9
                    $subPathCount--;
220
                }
221
            }
222
223 80
            $ancestors = array($this->_rootOrderByNode->getResourceSetWrapper()->getName());
224 80
            foreach ($orderBySubPathSegments as $index2 => $orderBySubPathSegment) {
225 80
                $isLastSegment = ($index2 == $subPathCount - 1);
226 80
                $resourceSetWrapper = null;
227 80
                $resourceType = $currentNode->getResourceType();
228 80
                $resourceProperty = $resourceType->resolveProperty($orderBySubPathSegment);
229 80
                if (is_null($resourceProperty)) {
230
                    throw ODataException::createSyntaxError(
231
                        Messages::orderByParserPropertyNotFound(
232
                            $resourceType->getFullName(), $orderBySubPathSegment
233
                        )
234
                    );
235
                }
236
237 80
                if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG)) {
238 1
                    throw ODataException::createBadRequestError(
239 1
                        Messages::orderByParserBagPropertyNotAllowed(
240 1
                            $resourceProperty->getName()
241 1
                        )
242 1
                    );
243 79
                } else if ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) {
244 75
                    if (!$isLastSegment) {
245
                        throw ODataException::createBadRequestError(
246
                            Messages::orderByParserPrimitiveAsIntermediateSegment(
247
                                $resourceProperty->getName()
248
                            )
249
                        );
250
                    }
251
252 75
                    $type = $resourceProperty->getInstanceType();
253 75
                    if ($type instanceof Binary) {
254 75
                        throw ODataException::createBadRequestError(Messages::orderByParserSortByBinaryPropertyNotAllowed($resourceProperty->getName()));
255
                    }
256 8
                } else if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE
257 7
                    || $resourceProperty->getKind() == ResourcePropertyKind::RESOURCE_REFERENCE
258 8
                    || $resourceProperty->getKind() == ResourcePropertyKind::KEY_RESOURCE_REFERENCE
259
                ) {
260 5
                    $this->_assertion($currentNode instanceof OrderByRootNode || $currentNode instanceof OrderByNode);
261 5
                    $resourceSetWrapper = $currentNode->getResourceSetWrapper();
262 5
                    $this->_assertion(!is_null($resourceSetWrapper));
263 5
                    $resourceSetWrapper
264 5
                        = $this->_providerWrapper->getResourceSetWrapperForNavigationProperty(
265 5
                            $resourceSetWrapper, $resourceType, $resourceProperty
266 5
                        );
267 5
                    if (is_null($resourceSetWrapper)) {
268 1
                        throw ODataException::createBadRequestError(
269 1
                            Messages::badRequestInvalidPropertyNameSpecified(
270 1
                                $resourceType->getFullName(), $orderBySubPathSegment
271 1
                            )
272 1
                        );
273
                    }
274
275 4
                    if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE) {
276 1
                        throw ODataException::createBadRequestError(
277 1
                            Messages::orderByParserResourceSetReferenceNotAllowed(
278 1
                                $resourceProperty->getName(), $resourceType->getFullName()
279 1
                            )
280 1
                        );
281
                    }
282
283 3
                    $resourceSetWrapper->checkResourceSetRightsForRead(true);
284
                    /*
285
                    if ($isLastSegment) {
286
						throw ODataException::createBadRequestError(
287
                            Messages::orderByParserSortByNavigationPropertyIsNotAllowed(
288
                                $resourceProperty->getName()
289
                            )
290
                        );
291
                    }
292
                    */
293
294 3
                    $ancestors[] = $orderBySubPathSegment;
295 3
                } else if ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE)) {
296 3
                    if ($isLastSegment) {
297 1
                        throw ODataException::createBadRequestError(
298 1
                            Messages::orderByParserSortByComplexPropertyIsNotAllowed(
299 1
                                $resourceProperty->getName()
300 1
                            )
301 1
                        );
302
                    }
303
304 3
                    $ancestors[] = $orderBySubPathSegment;
305
                } else {
306
                    throw ODataException::createInternalServerError(
307
                        Messages::orderByParserUnexpectedPropertyType()
308
                    );
309
                }
310
311 76
                $node = $currentNode->findNode($orderBySubPathSegment);
312 76
                if (is_null($node)) {
313 76
                    if ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE) || $resourceProperty->isKindOf(ResourcePropertyKind::KEY_RESOURCE_REFERENCE)) {
314 74
                        $node = new OrderByLeafNode(
315 74
                            $orderBySubPathSegment, $resourceProperty,
316 74
                            $ascending
317 74
                        );
318 6
                    } else if ($resourceProperty->getKind() == ResourcePropertyKind::RESOURCE_REFERENCE || $resourceProperty->getKind() == ResourcePropertyKind::KEY_RESOURCE_REFERENCE) {
319 3
                        $node = new OrderByNode(
320 3
                            $orderBySubPathSegment, $resourceProperty,
321 3
                            $resourceSetWrapper
322 3
                        );
323
                        // Initialize this member variable (identified by
324
                        // $resourceProperty) of parent object.
325 3
                        $object = $this->_mock($resourceProperty);
326 3
                        $currentObject->{$resourceProperty->getName()} = $object;
327 3
                        $currentObject = $object;
328 3
                    } else if ($resourceProperty->getKind() == ResourcePropertyKind::COMPLEX_TYPE) {
329 3
                        $node = new OrderByNode($orderBySubPathSegment, $resourceProperty, null);
330
                        // Initialize this member variable
331
                        // (identified by $resourceProperty)of parent object.
332 3
                        $object = $this->_mock($resourceProperty);
333 3
                        $currentObject->{$resourceProperty->getName()} = $object;
334 3
                        $currentObject = $object;
335
                    }
336
337 76
                    $currentNode->addNode($node);
338
                } else {
339 2
                    $currentObject = $currentObject->{$resourceProperty->getName()};
340
341 2
                    if ($node instanceof OrderByLeafNode) {
342
                        //remove duplicate orderby path
343 2
                        unset($orderByPathSegments[$index1]);
344
                    }
345
                }
346
347 76
                $currentNode = $node;
348
            }
349
        }
350
    }
351
352
    /**
353
     * Traverse 'Order By Tree' and create 'OrderInfo' structure
354
     *
355
     * @param array(array) $orderByPaths The orderby paths.
356
     *
357
     * @return OrderByInfo
358
     *
359
     * @throws ODataException In case parser found any tree inconsisitent
360
     *                        state, throws unexpected state error
361
     */
362 75
    private function _createOrderInfo($orderByPaths)
363
    {
364 75
        $orderByPathSegments = array();
365 75
        $navigationPropertiesInThePath = array();
366 75
        foreach ($orderByPaths as $index => $orderBySubPaths) {
367 75
            $currentNode = $this->_rootOrderByNode;
368 75
            $orderBySubPathSegments = array();
369 75
            foreach ($orderBySubPaths as $orderBySubPath) {
370 75
                $node = $currentNode->findNode($orderBySubPath);
371 75
                $this->_assertion(!is_null($node));
372 75
                $resourceProperty = $node->getResourceProperty();
373 75
                if ($node instanceof OrderByNode && !is_null($node->getResourceSetWrapper())) {
374 3
                    if (!array_key_exists($index, $navigationPropertiesInThePath)) {
375 3
                        $navigationPropertiesInThePath[$index] = array();
376
                    }
377
378 3
                    $navigationPropertiesInThePath[$index][] = $resourceProperty;
379
                }
380
381 75
                $orderBySubPathSegments[] = new OrderBySubPathSegment($resourceProperty);
382 75
                $currentNode = $node;
383
            }
384
385 75
            $this->_assertion($currentNode instanceof OrderByLeafNode);
386 74
            $orderByPathSegments[] = new OrderByPathSegment($orderBySubPathSegments, $currentNode->isAscending());
387 74
            unset($orderBySubPathSegments);
388
        }
389
390 74
        $this->_orderByInfo = new OrderByInfo($orderByPathSegments, empty($navigationPropertiesInThePath) ? null : $navigationPropertiesInThePath);
391
    }
392
393
    /**
394
     * Read orderby clause.
395
     *
396
     * @param string $value orderby clause to read.
397
     *
398
     * @return array(array) An array of 'OrderByPathSegment's, each of which
399
     *                      is array of 'OrderBySubPathSegment's
400
     *
401
     * @throws ODataException If any syntax error found while reading the clause
402
     */
403 80
    private function _readOrderBy($value)
404
    {
405 80
        $orderByPathSegments = array();
406 80
        $lexer = new ExpressionLexer($value);
407 80
        $i = 0;
408 80
        while ($lexer->getCurrentToken()->Id != ExpressionTokenId::END) {
409 80
            $orderBySubPathSegment = $lexer->readDottedIdentifier();
410 80
            if (!array_key_exists($i, $orderByPathSegments)) {
411 80
                $orderByPathSegments[$i] = array();
412
            }
413
414 80
            $orderByPathSegments[$i][] = $orderBySubPathSegment;
415 80
            $tokenId = $lexer->getCurrentToken()->Id;
416 80
            if ($tokenId != ExpressionTokenId::END) {
417 26
                if ($tokenId != ExpressionTokenId::SLASH) {
418 22
                    if ($tokenId != ExpressionTokenId::COMMA) {
419 9
                        $lexer->validateToken(ExpressionTokenId::IDENTIFIER);
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...sionTokenId::IDENTIFIER of type integer is incompatible with the type POData\UriProcessor\Quer...arser\ExpressionTokenId expected by parameter $tokenId of POData\UriProcessor\Quer...nLexer::validateToken(). ( Ignorable by Annotation )

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

419
                        $lexer->validateToken(/** @scrutinizer ignore-type */ ExpressionTokenId::IDENTIFIER);
Loading history...
420 9
                        $identifier = $lexer->getCurrentToken()->Text;
421 9
                        if ($identifier !== 'asc' && $identifier !== 'desc') {
422
                            // force lexer to throw syntax error as we found
423
                            // unexpected identifier
424
                            $lexer->validateToken(ExpressionTokenId::DOT);
425
                        }
426
427 9
                        $orderByPathSegments[$i][] = '*' . $identifier;
428 9
                        $lexer->nextToken();
429 9
                        $tokenId = $lexer->getCurrentToken()->Id;
430 9
                        if ($tokenId != ExpressionTokenId::END) {
431 9
                            $lexer->validateToken(ExpressionTokenId::COMMA);
432 9
                            $i++;
433
                        }
434
                    } else {
435 19
                        $i++;
436
                    }
437
                }
438
439 26
                $lexer->nextToken();
440
            }
441
        }
442
443 80
        return $orderByPathSegments;
444
    }
445
446
    /**
447
     * Assert that the given condition is true, if false throw
448
     * ODataException for unexpected state
449
     *
450
     * @param boolean $condition The condition to assert
451
     *
452
     * @return void
453
     *
454
     * @throws ODataException
455
     */
456 77
    private function _assertion($condition)
457
    {
458 77
        if (!$condition) {
459 1
            throw ODataException::createInternalServerError(Messages::orderByParserUnExpectedState());
460
        }
461
    }
462
}
463