Passed
Push — master ( a7cf01...71d572 )
by Béla
03:43
created

OrderByParser   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 423
Duplicated Lines 0 %

Test Coverage

Coverage 84.16%

Importance

Changes 5
Bugs 1 Features 0
Metric Value
wmc 58
eloc 191
c 5
b 1
f 0
dl 0
loc 423
ccs 170
cts 202
cp 0.8416
rs 4.5599

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A _assertion() 0 4 2
B _readOrderBy() 0 41 9
D _buildOrderByTree() 0 145 26
A parseOrderByClause() 0 26 2
B _mock() 0 46 11
B _createOrderInfo() 0 29 7

How to fix   Complexity   

Complex Class

Complex classes like OrderByParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OrderByParser, and based on these observations, apply Extract Interface, too.

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

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

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