UriProcessor::_getCurrentExpandedProjectionNode()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 10.0268

Importance

Changes 5
Bugs 3 Features 0
Metric Value
eloc 15
c 5
b 3
f 0
dl 0
loc 23
ccs 5
cts 18
cp 0.2778
rs 9.7666
cc 4
nc 3
nop 1
crap 10.0268
1
<?php
2
3
namespace POData\UriProcessor;
4
5
use POData\Providers\ProvidersWrapper;
6
use POData\Providers\Metadata\ResourcePropertyKind;
7
use POData\Providers\Metadata\ResourceTypeKind;
8
use POData\Providers\Metadata\ResourceSetWrapper;
9
use POData\Providers\Metadata\ResourceProperty;
10
use POData\Providers\Query\QueryType;
11
use POData\UriProcessor\QueryProcessor\QueryProcessor;
12
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
13
use POData\UriProcessor\ResourcePathProcessor\ResourcePathProcessor;
14
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\SegmentDescriptor;
15
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
16
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetSource;
17
use POData\IService;
18
use POData\Common\Messages;
19
use POData\Common\ODataException;
20
use POData\Common\InvalidOperationException;
21
use POData\Common\ODataConstants;
22
use POData\Providers\Query\QueryResult;
23
use POData\OperationContext\HTTPRequestMethod;
24
25
/**
26
 * Class UriProcessor
27
 *
28
 * A type to process client's requets URI
29
 * The syntax of request URI is:
30
 *  Scheme Host Port ServiceRoot ResourcePath ? QueryOption
31
 * For more details refer:
32
 * http://www.odata.org/developers/protocols/uri-conventions#UriComponents
33
 *
34
 * @package POData\UriProcessor
35
 */
36
class UriProcessor
37
{
38
    /**
39
     * Description of the OData request that a client has submitted.
40
     *
41
     * @var RequestDescription
42
     */
43
    private $request;
44
45
    /**
46
     * Holds reference to the data service instance.
47
     *
48
     * @var IService
49
     */
50
    private $service;
51
52
    /**
53
     * Holds reference to the wrapper over IDSMP and IDSQP implementation.
54
     *
55
     * @var ProvidersWrapper
56
     */
57
    private $providers;
58
59
    /**
60
     * Collection of segment names.
61
     *
62
     * @var string[]
63
     */
64
    private $_segmentNames;
65
66
    /**
67
     * Collection of segment ResourceSetWrapper instances.
68
     *
69
     * @var ResourceSetWrapper[]
70
     */
71
    private $_segmentResourceSetWrappers;
72
73
    /**
74
     * Constructs a new instance of UriProcessor
75
     *
76
     * @param IService $service Reference to the data service instance.
77
     */
78 94
    private function __construct(IService $service)
79
    {
80 94
        $this->service = $service;
81 94
        $this->providers = $service->getProvidersWrapper();
82 94
        $this->_segmentNames = array();
83 94
        $this->_segmentResourceSetWrappers = array();
84
    }
85
86
    /**
87
     * Process the resource path and query options of client's request uri.
88
     *
89
     * @param IService $service Reference to the data service instance.
90
     *
91
     * @return URIProcessor
92
     *
93
     * @throws ODataException
94
     */
95 94
    public static function process(IService $service)
96
    {
97 94
        $absoluteRequestUri = $service->getHost()->getAbsoluteRequestUri();
98 94
        $absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri();
99
100 94
        if (!$absoluteServiceUri->isBaseOf($absoluteRequestUri)) {
101
            throw ODataException::createInternalServerError(
102
                Messages::uriProcessorRequestUriDoesNotHaveTheRightBaseUri(
103
                    $absoluteRequestUri->getUrlAsString(),
104
                    $absoluteServiceUri->getUrlAsString()
105
                )
106
            );
107
        }
108
109 94
        $uriProcessor = new UriProcessor($service);
110
        //Parse the resource path part of the request Uri.
111 94
        $uriProcessor->request = ResourcePathProcessor::process($service);
112
113 88
        $uriProcessor->request->setUriProcessor($uriProcessor);
114
115
        //Parse the query string options of the request Uri.
116 88
        QueryProcessor::process($uriProcessor->request, $service);
117
118 66
        return $uriProcessor;
119
    }
120
121
    /**
122
     * Process the resource path and query options of client's request uri.
123
     *
124
     * @param IService $service Reference to the data service instance.
125
     *
126
     * @return URIProcessor
127
     *
128
     * @throws ODataException
129
     */
130
    public static function processPart($service, $request)
131
    {
132
        $uriProcessor = new UriProcessor($service);
133
        //Parse the resource path part of the request Uri.
134
        $uriProcessor->request = $request;
135
136
        $request->setUriProcessor($uriProcessor);
137
138
139
        //Parse the query string options of the request Uri.
140
        QueryProcessor::process($uriProcessor->request, $service);
141
142
        return $uriProcessor;
143
    }
144
145
    /**
146
     * Gets reference to the request submitted by client.
147
     *
148
     * @return RequestDescription
149
     */
150 66
    public function getRequest()
151
    {
152 66
        return $this->request;
153
    }
154
155
    /**
156
     * Execute the client submitted request against the data source.
157
     */
158 6
    public function execute()
159
    {
160 6
        $operationContext = $this->service->getOperationContext();
161 6
        if (!$operationContext) {
0 ignored issues
show
introduced by
$operationContext is of type POData\OperationContext\IOperationContext, thus it always evaluated to true.
Loading history...
162 6
            $this->executeBase($this->request);
163 6
            return;
164
        }
165
166
        $requestMethod = $operationContext->incomingRequest()->getMethod();
167
        if ($requestMethod == HTTPRequestMethod::GET) {
168
            $this->executeGet($this->request);
169
        } elseif ($requestMethod == HTTPRequestMethod::PUT) {
170
            $this->executePut($this->request);
171
        } elseif ($requestMethod == HTTPRequestMethod::POST) {
172
            if ($this->request->getLastSegment()->getTargetKind() == TargetKind::BATCH) {
173
                $this->executeBatch($this->request);
174
            } else {
175
                $this->executePost($this->request);
176
            }
177
        } elseif ($requestMethod == HTTPRequestMethod::DELETE) {
178
            $this->executeDelete($this->request);
179
        } else {
180
            throw ODataException::createNotImplementedError(Messages::unsupportedMethod($requestMethod));
181
        }
182
    }
183
184
    /**
185
     * Execute the client submitted request against the data source (GET)
186
     */
187
    protected function executeGet(RequestDescription $request)
188
    {
189
        return $this->executeBase($request);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->executeBase($request) targeting POData\UriProcessor\UriProcessor::executeBase() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
190
    }
191
192
    /**
193
     * Execute the client submitted request against the data source (PUT)
194
     */
195
    protected function executePut(RequestDescription $request)
196
    {
197
        $callback = function($uriProcessor, $segment) use ($request) {
198
            $requestMethod = $request->getRequestMethod();
199
            $resourceSet = $segment->getTargetResourceSetWrapper();
200
            $keyDescriptor = $segment->getKeyDescriptor();
201
            $data = $uriProcessor->request->getData();
202
203
            if (!$resourceSet || !$keyDescriptor) {
204
                $url = $request->getRequestUrl()->getUrlAsString();
205
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
206
            }
207
208
            if (!$data) {
209
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
210
            }
211
212
            $expand = $this->_getExpandedProjectionNodes($request);
213
            $filter = $request->getFilterInfo();
214
            $result = $uriProcessor->providers->putResource($resourceSet, $request, $keyDescriptor, $data, $filter, $expand);
215
216
            $segment->setSingleResult(true);
217
            $segment->setResult($result);
218
219
            return $result;
220
        };
221
222
        $segments = $request->getSegments();
223
224
        foreach ($segments as $segment) {
225
            if (is_null($segment->getNext()) || $segment->getNext()->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
226
                $this->applyQueryOptions($request, $segment, $callback);
227
            }
228
        }
229
            //?? TODO : TEST
230
            // Apply $select and $expand options to result set, this function will be always applied
231
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
232
            // not delegate $expand/$select operation to IDSQP2 implementation
233
        $this->handleExpansion($request);
234
    }
235
236
    /**
237
     * Execute the client submitted request against the data source (POST)
238
     */
239
    protected function executePost(RequestDescription $request)
240
    {
241
        $callback = function($uriProcessor, $segment) use ($request) {
242
            $requestMethod = $request->getRequestMethod();
243
            $resourceSet = $segment->getTargetResourceSetWrapper();
244
            $data = $request->getData();
245
246
            if (!$resourceSet) {
247
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
248
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
249
            }
250
251
            if (!$data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
252
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
253
            }
254
255
            $result = $uriProcessor->providers->postResource($resourceSet, $request, $data);
256
257
            $segment->setSingleResult(true);
258
            $segment->setResult($result);
259
260
            return $result;
261
        };
262
263
        $segments = $request->getSegments();
264
265
        foreach ($segments as $segment) {
266
            if (is_null($segment->getNext()) || $segment->getNext()->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
267
                $this->applyQueryOptions($request, $segment, $callback);
268
            }
269
        }
270
            //?? TODO : TEST
271
            // Apply $select and $expand options to result set, this function will be always applied
272
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
273
            // not delegate $expand/$select operation to IDSQP2 implementation
274
        $this->handleExpansion($request);
275
    }
276
277
    /**
278
     * Execute the client submitted request against the data source (DELETE)
279
     */
280
    protected function executeDelete(RequestDescription $request)
281
    {
282
        return $this->executeBase($request, function($uriProcessor, $segment) use ($request) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->executeBase($requ...ion(...) { /* ... */ }) targeting POData\UriProcessor\UriProcessor::executeBase() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
283
            $requestMethod = $request->getRequestMethod();
284
            $resourceSet = $segment->getTargetResourceSetWrapper();
285
            $keyDescriptor = $segment->getKeyDescriptor();
286
287
            if (!$resourceSet || !$keyDescriptor) {
288
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
289
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
290
            }
291
292
            $result = $uriProcessor->providers->deleteResource($resourceSet, $request, $keyDescriptor);
293
            $segment->setResult($result);
294
            return $result;
295
        });
296
    }
297
298
    /**
299
     * Execute the client submitted batch request against the data source (POST)
300
     */
301
    protected function executeBatch(RequestDescription $request)
302
    {
303
        $callback = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $callback is dead and can be removed.
Loading history...
304
        $post_callback = function($uriProcessor, $segment) {
0 ignored issues
show
Unused Code introduced by
The assignment to $post_callback is dead and can be removed.
Loading history...
305
            $requestMethod = $uriProcessor->service->getOperationContext()->incomingRequest()->getMethod();
306
            $resourceSet = $segment->getTargetResourceSetWrapper();
307
            $data = $uriProcessor->request->getData();
308
309
            if (!$resourceSet) {
310
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
311
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
312
            }
313
314
            if (!$data) {
315
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
316
            }
317
318
            $entity = $uriProcessor->providers->postResource($resourceSet, $data);
319
320
            $segment->setSingleResult(true);
321
            $segment->setResult($entity);
322
323
            return $entity;
324
        };
325
326
        foreach ($request->getParts() as $request) {
327
            $this->providers->getExpressionProvider($request)->clear();
328
329
            switch ($request->getRequestMethod()) {
330
                case HTTPRequestMethod::GET:
331
                    $this->executeGet($request);
332
                    $request->setExecuted();
333
                    break;
334
                case HTTPRequestMethod::PUT:
335
                    $this->executePut($request);
336
                    $request->setExecuted();
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
337
                case HTTPRequestMethod::POST:
338
                    $this->executePost($request);
339
                    $request->setExecuted();
340
                    break;
341
                case HTTPRequestMethod::DELETE:
342
                    $this->executeDelete($request);
343
                    $request->setExecuted();
344
                    break;
345
            }
346
        }
347
348
        return;
349
        return $this->executeBase($request, function($uriProcessor, $segment) {
0 ignored issues
show
Unused Code introduced by
return $this->executeBas...ion(...) { /* ... */ }) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
350
            $requestMethod = $uriProcessor->service->getOperationContext()->incomingRequest()->getMethod();
351
            $resourceSet = $segment->getTargetResourceSetWrapper();
352
            $data = $uriProcessor->request->getData();
353
354
            if (!$resourceSet) {
355
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
356
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
357
            }
358
359
            if (!$data) {
360
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
361
            }
362
363
            $entity = $uriProcessor->providers->postResource($resourceSet, $data);
364
365
            $segment->setSingleResult(true);
366
            $segment->setResult($entity);
367
368
            return $entity;
369
        });
370
    }
371
372
    /**
373
     * Execute the client submitted request against the data source
374
     *
375
     * @param callable $callback Function, what must be called
376
     */
377 6
    protected function executeBase(RequestDescription $request, $callback = null)
378
    {
379 6
        $segments = $request->getSegments();
380
381 6
        foreach ($segments as $segment) {
382
383 6
            $requestTargetKind = $segment->getTargetKind();
384
385 6
            if ($segment->getTargetSource() == TargetSource::ENTITY_SET) {
386 6
                $this->handleSegmentTargetsToResourceSet($segment, $request);
387 2
            } else if ($requestTargetKind == TargetKind::RESOURCE) {
388
                if (is_null($segment->getPrevious()->getResult())) {
389
                    throw ODataException::createResourceNotFoundError(
390
                        $segment->getPrevious()->getIdentifier()
391
                    );
392
                }
393
                $this->_handleSegmentTargetsToRelatedResource($request, $segment);
394 2
            } else if ($requestTargetKind == TargetKind::LINK) {
395
                $segment->setResult($segment->getPrevious()->getResult());
396 2
            } else if ($segment->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
397
                // we are done, $count will the last segment and
398
                // taken care by _applyQueryOptions method
399 2
                $segment->setResult($request->getCountValue());
400 2
                break;
401
            } else {
402
                if ($requestTargetKind == TargetKind::MEDIA_RESOURCE) {
403
                    if (is_null($segment->getPrevious()->getResult())) {
404
                        throw ODataException::createResourceNotFoundError(
405
                            $segment->getPrevious()->getIdentifier()
406
                        );
407
                    }
408
                    // For MLE and Named Stream the result of last segment
409
                    // should be that of previous segment, this is required
410
                    // while retrieving content type or stream from IDSSP
411
                    $segment->setResult($segment->getPrevious()->getResult());
412
                    // we are done, as named stream property or $value on
413
                    // media resource will be the last segment
414
                    break;
415
                }
416
417
                $value = $segment->getPrevious()->getResult();
418
                while (!is_null($segment)) {
419
                    //TODO: what exactly is this doing here?  Once a null's found it seems everything will be null
420
                    if (!is_null($value)) {
421
                        $value = null;
422
                    } else {
423
                        try {
424
                            //see #88
425
                            $property = new \ReflectionProperty($value, $segment->getIdentifier());
426
                            $value = $property->getValue($value);
427
                        } catch (\ReflectionException $reflectionException) {
428
                            //throw ODataException::createInternalServerError(Messages::orderByParserFailedToAccessOrInitializeProperty($resourceProperty->getName(), $resourceType->getName()));
429
                        }
430
                    }
431
432
                    $segment->setResult($value);
433
                    $segment = $segment->getNext();
434
                    if (!is_null($segment) && $segment->getIdentifier() == ODataConstants::URI_VALUE_SEGMENT) {
435
                        $segment->setResult($value);
436
                        $segment = $segment->getNext();
437
                    }
438
                }
439
440
                break;
441
442
            }
443
444 6
            if (is_null($segment->getNext()) || $segment->getNext()->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
445 6
                $this->applyQueryOptions($request, $segment, $callback);
446
            }
447
        }
448
449
            // Apply $select and $expand options to result set, this function will be always applied
450
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
451
            // not delegate $expand/$select operation to IDSQP2 implementation
452 6
        $this->handleExpansion($request);
453
    }
454
455
    /**
456
     * Query for a resource set pointed by the given segment descriptor and update the descriptor with the result.
457
     *
458
     * @param SegmentDescriptor $segment Describes the resource set to query
459
     * @return void
460
     *
461
     */
462 6
    private function handleSegmentTargetsToResourceSet(SegmentDescriptor $segment, RequestDescription $request) {
463 6
        if ($segment->isSingleResult()) {
464
            $entityInstance = $this->providers->getResourceFromResourceSet(
465
                $segment->getTargetResourceSetWrapper(),
466
                $request,
467
                $segment->getKeyDescriptor(),
468
                $this->_getExpandedProjectionNodes($request)
469
            );
470
471
            $segment->setResult($entityInstance);
472
473
        } else {
474
475 6
            $internalskiptokentinfo = $request->getInternalSkipTokenInfo();
476
477 6
            $queryResult = $this->providers->getResourceSet(
478 6
                $request->queryType,
479 6
                $segment->getTargetResourceSetWrapper(),
480 6
                $request,
481 6
                $request->getFilterInfo(),
482 6
                $request->getInternalOrderByInfo(),
483 6
                $request->getTopOptionCount() ?? $request->getTopCount(),
484 6
                $request->getSkipCount(),
485 6
                $internalskiptokentinfo ? $internalskiptokentinfo->getSkipTokenInfo() : null,
486 6
                $this->_getExpandedProjectionNodes($request)
487 6
            );
488 6
            $segment->setResult($queryResult);
489
        }
490
    }
491
492
    /**
493
     * Query for a related resource set or resource set reference pointed by the
494
     * given segment descriptor and update the descriptor with the result.
495
     *
496
     * @param SegmentDescriptor &$segment Describes the related resource
497
     *                                              to query.
498
     *
499
     * @return void
500
     */
501
    private function _handleSegmentTargetsToRelatedResource(RequestDescription $request, SegmentDescriptor $segment) {
502
        $projectedProperty = $segment->getProjectedProperty();
503
        $projectedPropertyKind = $projectedProperty->getKind();
504
505
        if ($projectedPropertyKind == ResourcePropertyKind::RESOURCESET_REFERENCE) {
506
            if ($segment->isSingleResult()) {
507
                $entityInstance = $this->providers->getResourceFromRelatedResourceSet(
508
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
509
                    $segment->getPrevious()->getResult(),
510
                    $segment->getTargetResourceSetWrapper(),
511
                    $projectedProperty,
512
                    $segment->getKeyDescriptor()
513
                );
514
515
                $segment->setResult($entityInstance);
516
            } else {
517
                $queryResult = $this->providers->getRelatedResourceSet(
518
                    $request->queryType,
519
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
520
                    $segment->getPrevious()->getResult(),
521
                    $segment->getTargetResourceSetWrapper(),
522
                    $segment->getProjectedProperty(),
523
                    $request->getFilterInfo(),
524
                    //TODO: why are these null?  see #98
525
                    null, // $orderby
526
                    null, // $top
527
                    null  // $skip
528
                );
529
530
                $segment->setResult($queryResult);
531
            }
532
        } else if ($projectedPropertyKind == ResourcePropertyKind::RESOURCE_REFERENCE) {
533
            $entityInstance = $this->providers->getRelatedResourceReference(
534
                $segment->getPrevious()->getTargetResourceSetWrapper(),
535
                $segment->getPrevious()->getResult(),
536
                $segment->getTargetResourceSetWrapper(),
537
                $segment->getProjectedProperty()
538
            );
539
540
            $segment->setResult($entityInstance);
541
        } else {
542
            //Unexpected state
543
        }
544
    }
545
546
    /**
547
     * Applies the query options to the resource(s) retrieved from the data source.
548
     *
549
     * @param SegmentDescriptor $segment The descriptor which holds resource(s) on which query options to be applied.
550
     * @param callable $callback Function, what must be called
551
     *
552
     */
553 6
    private function applyQueryOptions(RequestDescription $request, SegmentDescriptor $segment, $callback = null)
554
    {
555
        // For non-GET methods
556 6
        if ($callback) {
557
            $callback($this, $segment);
558
            return;
559
        }
560
561
        //TODO: I'm not really happy with this..i think i'd rather keep the result the QueryResult
562
        //not even bother with the setCountValue stuff (shouldn't counts be on segments?)
563
        //and just work with the QueryResult in the object model serializer
564 6
        $result = $segment->getResult();
565
566 6
        if (!$result instanceof QueryResult) {
567
            //If the segment isn't a query result, then there's no paging or counting to be done
568
            return;
569
        }
570
571
572
        // Note $inlinecount=allpages means include the total count regardless of paging..so we set the counts first
573
        // regardless if POData does the paging or not.
574 6
        if ($request->queryType == QueryType::ENTITIES_WITH_COUNT) {
575 2
            if ($this->providers->handlesOrderedPaging()) {
576 1
                $request->setCountValue($result->count);
577
            } else {
578 1
                $request->setCountValue(count($result->results));
0 ignored issues
show
Bug introduced by
It seems like $result->results can also be of type null; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

578
                $request->setCountValue(count(/** @scrutinizer ignore-type */ $result->results));
Loading history...
579
            }
580
        }
581
582
        //Have POData perform paging if necessary
583 6
        if (!$this->providers->handlesOrderedPaging() && !empty($result->results)) {
584 4
            $result->results = $this->performPaging($request, $result->results);
585
        }
586
587
        //a bit surprising, but $skip and $top affects $count so update it here, not above
588
        //IE  data.svc/Collection/$count?$top=10 returns 10 even if Collection has 11+ entries
589 6
        if ($request->queryType == QueryType::COUNT) {
590 2
            if ($this->providers->handlesOrderedPaging()) {
591 1
                $request->setCountValue($result->count);
592
            } else {
593 1
                $request->setCountValue(count($result->results));
594
            }
595
        }
596
597 6
        $segment->setResult($result->results);
598
    }
599
600
    /**
601
     * If the provider does not perform the paging (ordering, top, skip) then this method does it
602
     *
603
     * @param array $result
604
     * @return array
605
     */
606 4
    private function performPaging(RequestDescription $request, array $result)
607
    {
608
        //Apply (implicit and explicit) $orderby option
609 4
        $internalOrderByInfo = $request->getInternalOrderByInfo();
610 4
        if (!is_null($internalOrderByInfo)) {
611
            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
0 ignored issues
show
Bug introduced by
The method getSorterFunction() does not exist on POData\UriProcessor\Quer...ser\InternalOrderByInfo. ( Ignorable by Annotation )

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

611
            $orderByFunction = $internalOrderByInfo->/** @scrutinizer ignore-call */ getSorterFunction()->getReference();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
612
            usort($result, $orderByFunction);
613
        }
614
615
        //Apply $skiptoken option
616 4
        $internalSkipTokenInfo = $request->getInternalSkipTokenInfo();
617 4
        if (!is_null($internalSkipTokenInfo)) {
618
            $matchingIndex = $internalSkipTokenInfo->getIndexOfFirstEntryInTheNextPage($result);
619
            $result = array_slice($result, $matchingIndex);
620
        }
621
622
        //Apply $top and $skip option
623 4
        if (!empty($result)) {
624 4
            $top = $request->getTopCount();
625 4
            $skip = $request->getSkipCount();
626 4
            if (is_null($skip)) {
627 4
                $skip = 0;
628
            }
629
630 4
            $result = array_slice($result, $skip, $top);
631
        }
632
633 4
        return $result;
634
    }
635
636
637
    /**
638
     * Perform expansion.
639
     *
640
     * @return void
641
     */
642 6
    private function handleExpansion(RequestDescription $request)
643
    {
644 6
        $node = $request->getRootProjectionNode();
645 6
        if (!is_null($node) && $node->isExpansionSpecified()) {
646
            $result = $request->getTargetResult();
647
            if (!is_null($result) || is_iterable($result) && !empty($result)) {
648
                $needPop = $this->_pushSegmentForRoot($request);
649
                $this->_executeExpansion($request, $result);
650
                $this->_popSegment($needPop);
651
            }
652
        }
653
    }
654
655
    /**
656
     * Execute queries for expansion.
657
     *
658
     * @param array(mixed)/mixed $result Resource(s) whose navigation properties needs to be expanded.
659
     *
660
     *
661
     * @return void
662
     */
663
    private function _executeExpansion(RequestDescription $request, $result)
664
    {
665
        $expandedProjectionNodes = $this->_getExpandedProjectionNodes($request);
666
        foreach ($expandedProjectionNodes as $expandedProjectionNode) {
667
            $isCollection = $expandedProjectionNode->getResourceProperty()->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE;
668
            $projectedProperty = $expandedProjectionNode->getResourceProperty();
669
            $expandedPropertyName = $projectedProperty->getName();
670
            $currentResourceSet = $this->_getCurrentResourceSetWrapper($request)->getResourceSet();
671
            $currentResourceSetType = $currentResourceSet->getResourceType()->getInstanceType();
672
            $expandedPropertyReflection = $currentResourceSetType->getProperty($expandedPropertyName);
0 ignored issues
show
Bug introduced by
The method getProperty() does not exist on POData\Providers\Metadata\Type\IType. ( Ignorable by Annotation )

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

672
            /** @scrutinizer ignore-call */ 
673
            $expandedPropertyReflection = $currentResourceSetType->getProperty($expandedPropertyName);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
673
            $expandedPropertyReflection->setAccessible(true);
674
            $resourceSetOfProjectedProperty = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
675
676
            if (is_iterable($result)) {
677
                foreach ($result as $entry) {
678
                    // Check for null entry
679
                    if ($isCollection) {
680
                        $result1 = $this->providers->getRelatedResourceSet(
681
                            QueryType::ENTITIES, //it's always entities for an expansion
682
                            $currentResourceSet,
683
                            $entry,
684
                            $resourceSetOfProjectedProperty,
685
                            $projectedProperty,
686
                            null, // $filter
687
                            null, // $orderby
688
                            null, // $top
689
                            null  // $skip
690
                        )->results;
691
                        if (!empty($result1)) {
692
                            $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
0 ignored issues
show
Unused Code introduced by
The assignment to $internalOrderByInfo is dead and can be removed.
Loading history...
693
                            /*if (!is_null($internalOrderByInfo)) {
694
                                $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
695
                                usort($result1, $orderByFunction);
696
                                unset($internalOrderByInfo);
697
                                $takeCount = $expandedProjectionNode->getTakeCount();
698
                                if (!is_null($takeCount)) {
699
                                    $result1 = array_slice($result1, 0, $takeCount);
700
                                }
701
                            }*/
702
703
                            $expandedPropertyReflection->setValue($entry, $result1);
704
705
                            $projectedProperty = $expandedProjectionNode->getResourceProperty();
706
                            $needPop = $this->_pushSegmentForNavigationProperty(
707
                                $request,
708
                                $projectedProperty
709
                            );
710
                            $this->_executeExpansion($request, $result1);
711
                            $this->_popSegment($needPop);
712
                        } else {
713
                            $expandedPropertyReflection->setValue($entry, $result1);
714
                        }
715
                    } else {
716
                        $result1 = $this->providers->getRelatedResourceReference(
717
                            $currentResourceSet,
718
                            $entry,
719
                            $resourceSetOfProjectedProperty,
720
                            $projectedProperty
721
                        );
722
723
                        if (is_array($entry)) $entry[$expandedPropertyName] = $result1;
724
                        else $expandedPropertyReflection->setValue($entry, $result1);
725
726
                        if (!is_null($result1)) {
727
                            $projectedProperty3 = $expandedProjectionNode->getResourceProperty();
728
                            $needPop = $this->_pushSegmentForNavigationProperty(
729
                                $request,
730
                                $projectedProperty3
731
                            );
732
                            $this->_executeExpansion($request, $result1);
0 ignored issues
show
Bug introduced by
$result1 of type object is incompatible with the type array expected by parameter $result of POData\UriProcessor\UriP...or::_executeExpansion(). ( Ignorable by Annotation )

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

732
                            $this->_executeExpansion($request, /** @scrutinizer ignore-type */ $result1);
Loading history...
733
                            $this->_popSegment($needPop);
734
                        }
735
                    }
736
                }
737
            } else {
738
                if ($isCollection) {
739
                    $currentResourceSet2 = $this->_getCurrentResourceSetWrapper($request)->getResourceSet();
740
                    $resourceSetOfProjectedProperty2 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
741
                    $projectedProperty4 = $expandedProjectionNode->getResourceProperty();
742
                    $result1 = $this->providers->getRelatedResourceSet(
743
                        QueryType::ENTITIES, //it's always entities for an expansion
744
                        $currentResourceSet2,
745
                        $result,
0 ignored issues
show
Bug introduced by
$result of type array is incompatible with the type object expected by parameter $sourceEntity of POData\Providers\Provide...getRelatedResourceSet(). ( Ignorable by Annotation )

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

745
                        /** @scrutinizer ignore-type */ $result,
Loading history...
746
                        $resourceSetOfProjectedProperty2,
747
                        $projectedProperty4,
748
                        null, // $filter
749
                        null, // $orderby
750
                        null, // $top
751
                        null  // $skip
752
                    )->results;
753
                    if (!empty($result1)) {
754
                        $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
755
                        /*
756
                        if (!is_null($internalOrderByInfo)) {
757
                            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
758
                            usort($result1, $orderByFunction);
759
                            unset($internalOrderByInfo);
760
                            $takeCount = $expandedProjectionNode->getTakeCount();
761
                            if (!is_null($takeCount)) {
762
                                $result1 = array_slice($result1, 0, $takeCount);
763
                            }
764
                        }
765
                        */
766
767
                        $expandedPropertyReflection->setValue($result, $result1);
768
                        $projectedProperty7 = $expandedProjectionNode->getResourceProperty();
769
                        $needPop = $this->_pushSegmentForNavigationProperty(
770
                            $request,
771
                            $projectedProperty7
772
                        );
773
                        $this->_executeExpansion($request, $result1);
774
                        $this->_popSegment($needPop);
775
                    } else {
776
                        $expandedPropertyReflection->setValue($result, $result1);
777
                    }
778
                } else {
779
                    $currentResourceSet3 = $this->_getCurrentResourceSetWrapper($request)->getResourceSet();
780
                    $resourceSetOfProjectedProperty3 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
781
                    $projectedProperty5 = $expandedProjectionNode->getResourceProperty();
782
                    $result1 = $this->providers->getRelatedResourceReference(
783
                        $currentResourceSet3,
784
                        $result,
0 ignored issues
show
Bug introduced by
$result of type array is incompatible with the type object expected by parameter $sourceEntity of POData\Providers\Provide...atedResourceReference(). ( Ignorable by Annotation )

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

784
                        /** @scrutinizer ignore-type */ $result,
Loading history...
785
                        $resourceSetOfProjectedProperty3,
786
                        $projectedProperty5
787
                    );
788
                    $expandedPropertyReflection->setValue($result, $result1);
789
                    if (!is_null($result1)) {
790
                        $projectedProperty6 = $expandedProjectionNode->getResourceProperty();
791
                        $needPop = $this->_pushSegmentForNavigationProperty(
792
                            $request,
793
                            $projectedProperty6
794
                        );
795
                        $this->_executeExpansion($request, $result1);
796
                        $this->_popSegment($needPop);
797
                    }
798
                }
799
            }
800
        }
801
    }
802
803
    /**
804
     * Resource set wrapper for the resource being retireved.
805
     *
806
     * @return ResourceSetWrapper
807
     */
808
    private function _getCurrentResourceSetWrapper(RequestDescription $request)
809
    {
810
        $count = count($this->_segmentResourceSetWrappers);
811
        if ($count == 0) {
812
            return $request->getTargetResourceSetWrapper();
813
        } else {
814
            return $this->_segmentResourceSetWrappers[$count - 1];
815
        }
816
    }
817
818
    /**
819
     * Pushes a segment for the root of the tree
820
     * Note: Calls to this method should be balanced with calls to popSegment.
821
     *
822
     * @return bool true if the segment was pushed, false otherwise.
823
     */
824
    private function _pushSegmentForRoot($request)
825
    {
826
        $segmentName = $request->getContainerName();
827
        $segmentResourceSetWrapper
828
            = $request->getTargetResourceSetWrapper();
829
        return $this->_pushSegment($request, $segmentName, $segmentResourceSetWrapper);
830
    }
831
832
    /**
833
     * Pushes a segment for the current navigation property being written out.
834
     * Note: Refer 'ObjectModelSerializerNotes.txt' for more details about
835
     * 'Segment Stack' and this method.
836
     * Note: Calls to this method should be balanced with calls to popSegment.
837
     *
838
     * @param ResourceProperty &$resourceProperty Current navigation property
839
     *                                            being written out
840
     *
841
     * @return bool true if a segment was pushed, false otherwise
842
     *
843
     * @throws InvalidOperationException If this function invoked with non-navigation
844
     *                                   property instance.
845
     */
846
    private function _pushSegmentForNavigationProperty(RequestDescription $request, ResourceProperty &$resourceProperty)
847
    {
848
        if ($resourceProperty->getTypeKind() == ResourceTypeKind::ENTITY) {
0 ignored issues
show
introduced by
The condition $resourceProperty->getTy...esourceTypeKind::ENTITY is always false.
Loading history...
849
            $this->assert(
850
                !empty($this->_segmentNames),
851
                '!is_empty($this->_segmentNames'
852
            );
853
            $currentResourceSetWrapper = $this->_getCurrentResourceSetWrapper($request);
854
            $currentResourceType = $currentResourceSetWrapper->getResourceType();
855
            $currentResourceSetWrapper = $this->service
856
                ->getProvidersWrapper()
857
                ->getResourceSetWrapperForNavigationProperty(
858
                    $currentResourceSetWrapper,
859
                    $currentResourceType,
860
                    $resourceProperty
861
                );
862
863
            $this->assert(
864
                !is_null($currentResourceSetWrapper),
865
                '!null($currentResourceSetWrapper)'
866
            );
867
            return $this->_pushSegment(
868
                $request,
869
                $resourceProperty->getName(),
870
                $currentResourceSetWrapper
871
            );
872
        } else {
873
            throw new InvalidOperationException(
874
                'pushSegmentForNavigationProperty should not be called with non-entity type'
875
            );
876
        }
877
    }
878
879
    /**
880
     * Gets collection of expanded projection nodes under the current node.
881
     *
882
     * @return ExpandedProjectionNode[] List of nodes describing expansions for the current segment
883
     *
884
     */
885 6
    private function _getExpandedProjectionNodes($request)
886
    {
887 6
        $expandedProjectionNode = $this->_getCurrentExpandedProjectionNode($request);
888 6
        $expandedProjectionNodes = array();
889 6
        if (!is_null($expandedProjectionNode)) {
890
            foreach ($expandedProjectionNode->getChildNodes() as $node) {
891
                if ($node instanceof ExpandedProjectionNode) {
892
                    $expandedProjectionNodes[] = $node;
893
                }
894
            }
895
        }
896
897 6
        return $expandedProjectionNodes;
898
    }
899
900
    /**
901
     * Find a 'ExpandedProjectionNode' instance in the projection tree
902
     * which describes the current segment.
903
     *
904
     * @return ExpandedProjectionNode|null
905
     */
906 6
    private function _getCurrentExpandedProjectionNode($request)
907
    {
908 6
        $expandedProjectionNode
909 6
            = $request->getRootProjectionNode();
910 6
        if (!is_null($expandedProjectionNode)) {
911
            $depth = count($this->_segmentNames);
912
            if ($depth != 0) {
913
                for ($i = 1; $i < $depth; $i++) {
914
                    $expandedProjectionNode
915
                        = $expandedProjectionNode->findNode($this->_segmentNames[$i]);
916
                        $this->assert(
917
                            !is_null($expandedProjectionNode),
918
                            '!is_null($expandedProjectionNode)'
919
                        );
920
                        $this->assert(
921
                            $expandedProjectionNode instanceof ExpandedProjectionNode,
922
                            '$expandedProjectionNode instanceof ExpandedProjectionNode'
923
                        );
924
                }
925
            }
926
        }
927
928 6
        return $expandedProjectionNode;
929
    }
930
931
    /**
932
     * Pushes information about the segment whose instance is going to be
933
     * retrieved from the IDSQP implementation
934
     * Note: Calls to this method should be balanced with calls to popSegment.
935
     *
936
     * @param string             $segmentName         Name of segment to push.
937
     * @param ResourceSetWrapper &$resourceSetWrapper The resource set wrapper
938
     *                                                to push.
939
     *
940
     * @return bool true if the segment was push, false otherwise
941
     */
942
    private function _pushSegment($request, $segmentName, ResourceSetWrapper &$resourceSetWrapper)
943
    {
944
        $rootProjectionNode = $request->getRootProjectionNode();
945
        if (!is_null($rootProjectionNode)
946
            && $rootProjectionNode->isExpansionSpecified()
947
        ) {
948
            array_push($this->_segmentNames, $segmentName);
949
            array_push($this->_segmentResourceSetWrappers, $resourceSetWrapper);
950
            return true;
951
        }
952
953
        return false;
954
    }
955
956
    /**
957
     * Pops segment information from the 'Segment Stack'
958
     * Note: Calls to this method should be balanced with previous calls
959
     * to _pushSegment.
960
     *
961
     * @param boolean $needPop Is a pop required. Only true if last push
962
     *                         was successful.
963
     *
964
     * @return void
965
     *
966
     * @throws InvalidOperationException If found un-balanced call
967
     *                                   with _pushSegment
968
     */
969
    private function _popSegment($needPop)
970
    {
971
        if ($needPop) {
972
            if (!empty($this->_segmentNames)) {
973
                array_pop($this->_segmentNames);
974
                array_pop($this->_segmentResourceSetWrappers);
975
            } else {
976
                throw new InvalidOperationException(
977
                    'Found non-balanced call to _pushSegment and popSegment'
978
                );
979
            }
980
        }
981
    }
982
983
    /**
984
     * Assert that the given condition is true.
985
     *
986
     * @param boolean $condition         Constion to assert.
987
     * @param string  $conditionAsString Message to show incase assertion fails.
988
     *
989
     * @return void
990
     *
991
     * @throws InvalidOperationException
992
     */
993
    protected function assert($condition, $conditionAsString)
994
    {
995
        if (!$condition) {
996
            throw new InvalidOperationException(
997
                "Unexpected state, expecting $conditionAsString"
998
            );
999
        }
1000
    }
1001
}
1002