Test Failed
Push — master ( 7228dc...961f38 )
by Bálint
12:45
created

UriProcessor::execute()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 18
c 2
b 0
f 0
dl 0
loc 23
rs 8.8333
cc 7
nc 7
nop 0
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
    private function __construct(IService $service)
79
    {
80
        $this->service = $service;
81
        $this->providers = $service->getProvidersWrapper();
82
        $this->_segmentNames = array();
83
        $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
    public static function process(IService $service)
96
    {
97
        $absoluteRequestUri = $service->getHost()->getAbsoluteRequestUri();
98
        $absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri();
99
100
        if (!$absoluteServiceUri->isBaseOf($absoluteRequestUri)) {
101
            throw ODataException::createInternalServerError(
102
                Messages::uriProcessorRequestUriDoesNotHaveTheRightBaseUri(
103
                    $absoluteRequestUri->getUrlAsString(),
104
                    $absoluteServiceUri->getUrlAsString()
105
                )
106
            );
107
        }
108
109
        $uriProcessor = new UriProcessor($service);
110
        //Parse the resource path part of the request Uri.
111
        $uriProcessor->request = ResourcePathProcessor::process($service);
112
113
        $uriProcessor->request->setUriProcessor($uriProcessor);
114
115
        //Parse the query string options of the request Uri.
116
        QueryProcessor::process($uriProcessor->request, $service);
117
118
        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
    public function getRequest()
151
    {
152
        return $this->request;
153
    }
154
155
    /**
156
     * Execute the client submitted request against the data source.
157
     */
158
    public function execute()
159
    {
160
        $operationContext = $this->service->getOperationContext();
161
        if (!$operationContext) {
0 ignored issues
show
introduced by
$operationContext is of type POData\OperationContext\IOperationContext, thus it always evaluated to true.
Loading history...
162
            $this->executeBase($this->request);
163
            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);
0 ignored issues
show
Unused Code introduced by
The call to POData\UriProcessor\UriProcessor::executeBatch() has too many arguments starting with $this->request. ( Ignorable by Annotation )

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

173
                $this->/** @scrutinizer ignore-call */ 
174
                       executeBatch($this->request);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
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($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($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
            $result = $uriProcessor->providers->putResource($resourceSet, $keyDescriptor, $data);
213
214
            $segment->setSingleResult(true);
215
            $segment->setResult($result);
216
217
            return $result;
218
        };
219
220
        $segments = $request->getSegments();
221
222
        foreach ($segments as $segment) {
223
            if (is_null($segment->getNext()) || $segment->getNext()->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
224
                $this->applyQueryOptions($segment, $callback);
225
            }
226
        }
227
            //?? TODO : TEST
228
            // Apply $select and $expand options to result set, this function will be always applied
229
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
230
            // not delegate $expand/$select operation to IDSQP2 implementation
231
        $this->handleExpansion($request);
232
    }
233
234
    /**
235
     * Execute the client submitted request against the data source (POST)
236
     */
237
    protected function executePost($request)
238
    {
239
        $callback = function($uriProcessor, $segment) use ($request) {
240
            $requestMethod = $request->getRequestMethod();
241
            $resourceSet = $segment->getTargetResourceSetWrapper();
242
            $data = $request->getData();
243
244
            if (!$resourceSet) {
245
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
246
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
247
            }
248
249
            if (!$data) {
250
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
251
            }
252
253
            $result = $uriProcessor->providers->postResource($resourceSet, $data);
254
255
            $segment->setSingleResult(true);
256
            $segment->setResult($result);
257
258
            return $result;
259
        };
260
261
        $segments = $request->getSegments();
262
263
        foreach ($segments as $segment) {
264
            if (is_null($segment->getNext()) || $segment->getNext()->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
265
                $this->applyQueryOptions($segment, $callback);
266
            }
267
        }
268
            //?? TODO : TEST
269
            // Apply $select and $expand options to result set, this function will be always applied
270
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
271
            // not delegate $expand/$select operation to IDSQP2 implementation
272
        $this->handleExpansion($request);
273
    }
274
275
    /**
276
     * Execute the client submitted request against the data source (DELETE)
277
     */
278
    protected function executeDelete($request)
279
    {
280
        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...
281
            $requestMethod = $request->getRequestMethod();
282
            $resourceSet = $segment->getTargetResourceSetWrapper();
283
            $keyDescriptor = $segment->getKeyDescriptor();
284
285
            if (!$resourceSet || !$keyDescriptor) {
286
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
287
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
288
            }
289
290
            return $uriProcessor->providers->deleteResource($resourceSet, $keyDescriptor);
291
        });
292
    }
293
294
    /**
295
     * Execute the client submitted batch request against the data source (POST)
296
     */
297
    protected function executeBatch()
298
    {
299
        $callback = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $callback is dead and can be removed.
Loading history...
300
        $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...
301
            $requestMethod = $uriProcessor->service->getOperationContext()->incomingRequest()->getMethod();
302
            $resourceSet = $segment->getTargetResourceSetWrapper();
303
            $data = $uriProcessor->request->getData();
304
305
            if (!$resourceSet) {
306
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
307
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
308
            }
309
310
            if (!$data) {
311
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
312
            }
313
314
            $entity = $uriProcessor->providers->postResource($resourceSet, $data);
315
316
            $segment->setSingleResult(true);
317
            $segment->setResult($entity);
318
319
            return $entity;
320
        };
321
322
        foreach ($this->request->getParts() as $request) {
323
            $this->providers->getExpressionProvider()->clear();
0 ignored issues
show
Bug introduced by
The method clear() does not exist on POData\Providers\Expression\IExpressionProvider. ( Ignorable by Annotation )

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

323
            $this->providers->getExpressionProvider()->/** @scrutinizer ignore-call */ clear();

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

600
            $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...
601
            usort($result, $orderByFunction);
602
        }
603
604
        //Apply $skiptoken option
605
        $internalSkipTokenInfo = $this->request->getInternalSkipTokenInfo();
606
        if (!is_null($internalSkipTokenInfo)) {
607
            $matchingIndex = $internalSkipTokenInfo->getIndexOfFirstEntryInTheNextPage($result);
608
            $result = array_slice($result, $matchingIndex);
609
        }
610
611
        //Apply $top and $skip option
612
        if (!empty($result)) {
613
            $top  = $this->request->getTopCount();
614
            $skip = $this->request->getSkipCount();
615
            if (is_null($skip)) {
616
                $skip = 0;
617
            }
618
619
            $result = array_slice($result, $skip, $top);
620
        }
621
622
        return $result;
623
    }
624
625
626
    /**
627
     * Perform expansion.
628
     *
629
     * @return void
630
     */
631
    private function handleExpansion($request)
632
    {
633
        $node = $request->getRootProjectionNode();
634
        if (!is_null($node) && $node->isExpansionSpecified()) {
635
            $result = $request->getTargetResult();
636
            if (!is_null($result) || is_iterable($result) && !empty($result)) {
637
                $needPop = $this->_pushSegmentForRoot($request);
638
                $this->_executeExpansion($result);
639
                $this->_popSegment($needPop);
640
            }
641
        }
642
    }
643
644
    /**
645
     * Execute queries for expansion.
646
     *
647
     * @param array(mixed)/mixed $result Resource(s) whose navigation properties needs to be expanded.
648
     *
649
     *
650
     * @return void
651
     */
652
    private function _executeExpansion($result)
653
    {
654
        $expandedProjectionNodes = $this->_getExpandedProjectionNodes($this->request);
655
        foreach ($expandedProjectionNodes as $expandedProjectionNode) {
656
            $isCollection = $expandedProjectionNode->getResourceProperty()->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE;
657
            $expandedPropertyName = $expandedProjectionNode->getResourceProperty()->getName();
658
            if (is_iterable($result)) {
659
                foreach ($result as $entry) {
660
                    // Check for null entry
661
                    if ($isCollection) {
662
                        $currentResourceSet = $this->_getCurrentResourceSetWrapper()->getResourceSet();
663
                        $resourceSetOfProjectedProperty = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
664
                        $projectedProperty1 = $expandedProjectionNode->getResourceProperty();
665
                        $result1 = $this->providers->getRelatedResourceSet(
666
                            QueryType::ENTITIES, //it's always entities for an expansion
667
                            $currentResourceSet,
668
                            $entry,
669
                            $resourceSetOfProjectedProperty,
670
                            $projectedProperty1,
671
                            null, // $filter
672
                            null, // $orderby
673
                            null, // $top
674
                            null  // $skip
675
                        )->results;
676
                        if (!empty($result1)) {
677
                            $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
0 ignored issues
show
Unused Code introduced by
The assignment to $internalOrderByInfo is dead and can be removed.
Loading history...
678
                            /*if (!is_null($internalOrderByInfo)) {
679
                                $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
680
                                usort($result1, $orderByFunction);
681
                                unset($internalOrderByInfo);
682
                                $takeCount = $expandedProjectionNode->getTakeCount();
683
                                if (!is_null($takeCount)) {
684
                                    $result1 = array_slice($result1, 0, $takeCount);
685
                                }
686
                            }*/
687
688
                            $entry->$expandedPropertyName = $result1;
689
                            $projectedProperty = $expandedProjectionNode->getResourceProperty();
690
                            $needPop = $this->_pushSegmentForNavigationProperty(
691
                                $projectedProperty
692
                            );
693
                            $this->_executeExpansion($result1);
694
                            $this->_popSegment($needPop);
695
                        } else {
696
                            $entry->$expandedPropertyName = array();
697
                        }
698
                    } else {
699
                        $currentResourceSet1 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
700
                        $resourceSetOfProjectedProperty1 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
701
                        $projectedProperty2 = $expandedProjectionNode->getResourceProperty();
702
                        $result1 = $this->providers->getRelatedResourceReference(
703
                            $currentResourceSet1,
704
                            $entry,
705
                            $resourceSetOfProjectedProperty1,
706
                            $projectedProperty2
707
                        );
708
                        $entry->$expandedPropertyName = $result1;
709
                        if (!is_null($result1)) {
710
                            $projectedProperty3 = $expandedProjectionNode->getResourceProperty();
711
                            $needPop = $this->_pushSegmentForNavigationProperty(
712
                                $projectedProperty3
713
                            );
714
                            $this->_executeExpansion($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

714
                            $this->_executeExpansion(/** @scrutinizer ignore-type */ $result1);
Loading history...
715
                            $this->_popSegment($needPop);
716
                        }
717
                    }
718
                }
719
            } else {
720
                if ($isCollection) {
721
                    $currentResourceSet2 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
722
                    $resourceSetOfProjectedProperty2 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
723
                    $projectedProperty4 = $expandedProjectionNode->getResourceProperty();
724
                    $result1 = $this->providers->getRelatedResourceSet(
725
                        QueryType::ENTITIES, //it's always entities for an expansion
726
                        $currentResourceSet2,
727
                        $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

727
                        /** @scrutinizer ignore-type */ $result,
Loading history...
728
                        $resourceSetOfProjectedProperty2,
729
                        $projectedProperty4,
730
                        null, // $filter
731
                        null, // $orderby
732
                        null, // $top
733
                        null  // $skip
734
                    )->results;
735
                    if (!empty($result1)) {
736
                        $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
737
                        /*
738
                        if (!is_null($internalOrderByInfo)) {
739
                            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
740
                            usort($result1, $orderByFunction);
741
                            unset($internalOrderByInfo);
742
                            $takeCount = $expandedProjectionNode->getTakeCount();
743
                            if (!is_null($takeCount)) {
744
                                $result1 = array_slice($result1, 0, $takeCount);
745
                            }
746
                        }
747
                        */
748
749
                        $result->$expandedPropertyName = $result1;
750
                        $projectedProperty7 = $expandedProjectionNode->getResourceProperty();
751
                        $needPop = $this->_pushSegmentForNavigationProperty(
752
                            $projectedProperty7
753
                        );
754
                        $this->_executeExpansion($result1);
755
                        $this->_popSegment($needPop);
756
                    } else {
757
                        $result->$expandedPropertyName = array();
758
                    }
759
                } else {
760
                    $currentResourceSet3 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
761
                    $resourceSetOfProjectedProperty3 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
762
                    $projectedProperty5 = $expandedProjectionNode->getResourceProperty();
763
                    $result1 = $this->providers->getRelatedResourceReference(
764
                        $currentResourceSet3,
765
                        $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

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