Test Failed
Push — master ( 9f5b29...e53146 )
by Bálint
05:26
created

UriProcessor::executeBatch()   B

Complexity

Conditions 10
Paths 8

Size

Total Lines 64
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 42
c 2
b 1
f 0
dl 0
loc 64
rs 7.6666
cc 10
nc 8
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

696
                            $this->_executeExpansion(/** @scrutinizer ignore-type */ $result1);
Loading history...
697
                            $this->_popSegment($needPop);
698
                        }
699
                    }
700
                }
701
            } else {
702
                if ($isCollection) {
703
                    $currentResourceSet2 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
704
                    $resourceSetOfProjectedProperty2 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
705
                    $projectedProperty4 = $expandedProjectionNode->getResourceProperty();
706
                    $result1 = $this->providers->getRelatedResourceSet(
707
                        QueryType::ENTITIES, //it's always entities for an expansion
708
                        $currentResourceSet2,
709
                        $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

709
                        /** @scrutinizer ignore-type */ $result,
Loading history...
710
                        $resourceSetOfProjectedProperty2,
711
                        $projectedProperty4,
712
                        null, // $filter
713
                        null, // $orderby
714
                        null, // $top
715
                        null  // $skip
716
                    )->results;
717
                    if (!empty($result1)) {
718
                        $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
719
                        /*
720
                        if (!is_null($internalOrderByInfo)) {
721
                            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
722
                            usort($result1, $orderByFunction);
723
                            unset($internalOrderByInfo);
724
                            $takeCount = $expandedProjectionNode->getTakeCount();
725
                            if (!is_null($takeCount)) {
726
                                $result1 = array_slice($result1, 0, $takeCount);
727
                            }
728
                        }
729
                        */
730
731
                        $result->$expandedPropertyName = $result1;
732
                        $projectedProperty7 = $expandedProjectionNode->getResourceProperty();
733
                        $needPop = $this->_pushSegmentForNavigationProperty(
734
                            $projectedProperty7
735
                        );
736
                        $this->_executeExpansion($result1);
737
                        $this->_popSegment($needPop);
738
                    } else {
739
                        $result->$expandedPropertyName = array();
740
                    }
741
                } else {
742
                    $currentResourceSet3 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
743
                    $resourceSetOfProjectedProperty3 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
744
                    $projectedProperty5 = $expandedProjectionNode->getResourceProperty();
745
                    $result1 = $this->providers->getRelatedResourceReference(
746
                        $currentResourceSet3,
747
                        $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

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