Passed
Push — master ( 599585...a7cf01 )
by Béla
03:50
created

OrderByParser::_mock()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 11.1032

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 17
c 2
b 1
f 0
dl 0
loc 28
ccs 9
cts 16
cp 0.5625
rs 8.8333
cc 7
nc 6
nop 1
crap 11.1032
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;
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
	private $_rootOrderByNode;
67
    /**
68
     * Creates new instance of OrderByParser
69
     *
70
     * @param ProvidersWrapper $providerWrapper Reference to metadata
71
     *                                                      and query provider
72
     *                                                      wrapper
73
     */
74 80
    private function __construct(ProvidersWrapper $providerWrapper)
75
    {
76 80
        $this->_providerWrapper = $providerWrapper;
77
    }
78
79 80
    private static function _mock($instanceType)
80
    {
81 80
		$reflection = new \ReflectionClass($instanceType->name);
82 80
		$mock = $reflection->newInstanceWithoutConstructor();
83
84 80
		foreach ($reflection->getProperties() as $property) {
85 80
			$property->setAccessible(true);
86
87
			// Check if the property has a non-nullable type and set a default value
88 80
			$type = $property->getType();
89 80
			if ($type && !$type->allowsNull()) {
90
				// Determine a default value based on the type
91
				if ($type->getName() === 'int') {
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

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

396
                        $lexer->validateToken(/** @scrutinizer ignore-type */ ExpressionTokenId::IDENTIFIER);
Loading history...
397 9
                        $identifier = $lexer->getCurrentToken()->Text;
398 9
                        if ($identifier !== 'asc' && $identifier !== 'desc') {
399
                            // force lexer to throw syntax error as we found
400
                            // unexpected identifier
401
                            $lexer->validateToken(ExpressionTokenId::DOT);
402
                        }
403
404 9
                        $orderByPathSegments[$i][] = '*' . $identifier;
405 9
                        $lexer->nextToken();
406 9
                        $tokenId = $lexer->getCurrentToken()->Id;
407 9
                        if ($tokenId != ExpressionTokenId::END) {
408 9
                            $lexer->validateToken(ExpressionTokenId::COMMA);
409 9
                            $i++;
410
                        }
411
                    } else {
412 19
                        $i++;
413
                    }
414
                }
415
416 26
                $lexer->nextToken();
417
            }
418
        }
419
420 80
        return $orderByPathSegments;
421
    }
422
423
    /**
424
     * Assert that the given condition is true, if false throw
425
     * ODataException for unexpected state
426
     *
427
     * @param boolean $condition The condition to assert
428
     *
429
     * @return void
430
     *
431
     * @throws ODataException
432
     */
433 77
    private function _assertion($condition)
434
    {
435 77
        if (!$condition) {
436 1
            throw ODataException::createInternalServerError(Messages::orderByParserUnExpectedState());
437
        }
438
    }
439
}
440