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

OrderByParser::_mock()   C

Complexity

Conditions 12
Paths 18

Size

Total Lines 49
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 30

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 36
c 4
b 1
f 0
dl 0
loc 49
ccs 17
cts 34
cp 0.5
rs 6.9666
cc 12
nc 18
nop 1
crap 30

How to fix   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
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
		if($reflection->isAbstract()) {
85
			return new stdClass();
0 ignored issues
show
Bug introduced by
The type POData\UriProcessor\Quer...\OrderByParser\stdClass was not found. Did you mean stdClass? If so, make sure to prefix the type with \.
Loading history...
86
		}
87 80
		$mock = $reflection->newInstanceWithoutConstructor();
88
89 80
		foreach ($reflection->getProperties() as $property) {
90 80
			$property->setAccessible(true);
91
92 80
			$type = $property->getType();
93 80
			if($resourceType instanceof ResourceProperty) {
94 6
				$resourceType  = $resourceType->getResourceType();
95
			}
96 80
			$resourceProperty = $resourceType->resolveProperty($property->getName());
97 80
			if(is_null($resourceProperty)) continue;
98
99 80
			$resourcePropertyType = $resourceProperty->getResourceType();
100 80
			$resourceKind = $resourcePropertyType->getResourceTypeKind();
101 80
			if ($type && !$type->allowsNull()) {
102
				if ($resourceKind === ResourceTypeKind::PRIMITIVE) {
103
					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

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

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