Passed
Push — master ( 4ab488...bcfbc7 )
by Bálint
03:58
created

UriProcessor::executePost()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 29
c 0
b 0
f 0
rs 9.0777
cc 6
nc 3
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
     * Gets reference to the request submitted by client.
123
     *
124
     * @return RequestDescription
125
     */
126
    public function getRequest()
127
    {
128
        return $this->request;
129
    }
130
131
    /**
132
     * Execute the client submitted request against the data source.
133
     */
134
    public function execute()
135
    {
136
        $operationContext = $this->service->getOperationContext();
137
        if (!$operationContext) {
0 ignored issues
show
introduced by
$operationContext is of type POData\OperationContext\IOperationContext, thus it always evaluated to true.
Loading history...
138
            $this->executeBase();
139
            return;
140
        }
141
142
        $requestMethod = $operationContext->incomingRequest()->getMethod();
143
        if ($requestMethod == HTTPRequestMethod::GET) {
0 ignored issues
show
introduced by
The condition $requestMethod == POData...\HTTPRequestMethod::GET is always false.
Loading history...
144
            $this->executeGet();
145
        } elseif ($requestMethod == HTTPRequestMethod::PUT) {
0 ignored issues
show
introduced by
The condition $requestMethod == POData...\HTTPRequestMethod::PUT is always false.
Loading history...
146
            $this->executePut();
147
        } elseif ($requestMethod == HTTPRequestMethod::POST) {
0 ignored issues
show
introduced by
The condition $requestMethod == POData...HTTPRequestMethod::POST is always false.
Loading history...
148
            if ($this->request->getLastSegment()->getTargetKind() == TargetKind::BATCH()) {
149
                $this->executeBatch();
150
            } else {
151
                $this->executePost();
152
            }
153
        } elseif ($requestMethod == HTTPRequestMethod::DELETE) {
0 ignored issues
show
introduced by
The condition $requestMethod == POData...TPRequestMethod::DELETE is always false.
Loading history...
154
            $this->executeDelete();
155
        } else {
156
            throw ODataException::createNotImplementedError(Messages::unsupportedMethod($requestMethod));
157
        }
158
    }
159
160
    /**
161
     * Execute the client submitted request against the data source (GET)
162
     */
163
    protected function executeGet()
164
    {
165
        return $this->executeBase();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->executeBase() 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...
166
    }
167
168
    /**
169
     * Execute the client submitted request against the data source (PUT)
170
     */
171
    protected function executePut()
172
    {
173
        return $this->executeBase(function($uriProcessor, $segment) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->executeBase(function(...) { /* ... */ }) 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...
174
            $requestMethod = $uriProcessor->service->getOperationContext()->incomingRequest()->getMethod();
175
            $resourceSet = $segment->getTargetResourceSetWrapper();
176
            $keyDescriptor = $segment->getKeyDescriptor();
177
            $data = $uriProcessor->request->getData();
178
179
            if (!$resourceSet || !$keyDescriptor) {
180
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
181
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
182
            }
183
184
            if (!$data) {
185
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
186
            }
187
188
            return $uriProcessor->providers->putResource($resourceSet, $keyDescriptor, $data);
189
        });
190
    }
191
192
    /**
193
     * Execute the client submitted request against the data source (POST)
194
     */
195
    protected function executePost()
196
    {
197
        $callback = function($uriProcessor, $segment) {
198
            $requestMethod = $uriProcessor->service->getOperationContext()->incomingRequest()->getMethod();
199
            $resourceSet = $segment->getTargetResourceSetWrapper();
200
            $data = $uriProcessor->request->getData();
201
202
            if (!$resourceSet) {
203
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
204
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
205
            }
206
207
            if (!$data) {
208
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
209
            }
210
211
            $entity = $uriProcessor->providers->postResource($resourceSet, $data);
212
213
            $segment->setSingleResult(true);
214
            $segment->setResult($entity);
215
216
            return $entity;
217
        };
218
219
        $segments = $this->request->getSegments();
220
221
        foreach ($segments as $segment) {
222
            if (is_null($segment->getNext()) || $segment->getNext()->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
223
                $this->applyQueryOptions($segment, $callback);
224
            }
225
        }
226
    }
227
228
    /**
229
     * Execute the client submitted request against the data source (DELETE)
230
     */
231
    protected function executeDelete()
232
    {
233
        return $this->executeBase(function($uriProcessor, $segment) {
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->executeBase(function(...) { /* ... */ }) 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...
234
            $requestMethod = $uriProcessor->service->getOperationContext()->incomingRequest()->getMethod();
235
            $resourceSet = $segment->getTargetResourceSetWrapper();
236
            $keyDescriptor = $segment->getKeyDescriptor();
237
238
            if (!$resourceSet || !$keyDescriptor) {
239
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
240
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
241
            }
242
243
            return $uriProcessor->providers->deleteResource($resourceSet, $keyDescriptor);
244
        });
245
    }
246
247
    /**
248
     * Execute the client submitted batch request against the data source (POST)
249
     */
250
    protected function executeBatch()
251
    {
252
        $callback = null;
253
        $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...
254
            $requestMethod = $uriProcessor->service->getOperationContext()->incomingRequest()->getMethod();
255
            $resourceSet = $segment->getTargetResourceSetWrapper();
256
            $data = $uriProcessor->request->getData();
257
258
            if (!$resourceSet) {
259
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
260
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
261
            }
262
263
            if (!$data) {
264
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
265
            }
266
267
            $entity = $uriProcessor->providers->postResource($resourceSet, $data);
268
269
            $segment->setSingleResult(true);
270
            $segment->setResult($entity);
271
272
            return $entity;
273
        };
274
275
        foreach ($this->request->getParts() as $request) {
276
277
            $segments = $request->getSegments();
278
279
            foreach ($segments as $segment) {
280
281
                $requestTargetKind = $segment->getTargetKind();
282
283
                if ($segment->getTargetSource() == TargetSource::ENTITY_SET) {
284
                    $this->handleSegmentTargetsToResourceSet($segment);
285
                } else if ($requestTargetKind == TargetKind::RESOURCE) {
286
                    if (is_null($segment->getPrevious()->getResult())) {
287
                        throw ODataException::createResourceNotFoundError(
288
                            $segment->getPrevious()->getIdentifier()
289
                        );
290
                    }
291
                    $this->_handleSegmentTargetsToRelatedResource($segment);
292
                } else if ($requestTargetKind == TargetKind::LINK) {
293
                    $segment->setResult($segment->getPrevious()->getResult());
294
                } else if ($segment->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
295
                    // we are done, $count will the last segment and
296
                    // taken care by _applyQueryOptions method
297
                    $segment->setResult($this->request->getCountValue());
298
                    break;
299
                } else {
300
                    if ($requestTargetKind == TargetKind::MEDIA_RESOURCE) {
301
                        if (is_null($segment->getPrevious()->getResult())) {
302
                            throw ODataException::createResourceNotFoundError(
303
                                $segment->getPrevious()->getIdentifier()
304
                            );
305
                        }
306
                        // For MLE and Named Stream the result of last segment
307
                        // should be that of previous segment, this is required
308
                        // while retrieving content type or stream from IDSSP
309
                        $segment->setResult($segment->getPrevious()->getResult());
310
                        // we are done, as named stream property or $value on
311
                        // media resource will be the last segment
312
                        break;
313
                    }
314
315
                    $value = $segment->getPrevious()->getResult();
316
                    while (!is_null($segment)) {
317
                        //TODO: what exactly is this doing here?  Once a null's found it seems everything will be null
318
                        if (!is_null($value)) {
319
                            $value = null;
320
                        } else {
321
                            try {
322
                                //see #88
323
                                $property = new \ReflectionProperty($value, $segment->getIdentifier());
324
                                $value = $property->getValue($value);
325
                            } catch (\ReflectionException $reflectionException) {
326
                                //throw ODataException::createInternalServerError(Messages::orderByParserFailedToAccessOrInitializeProperty($resourceProperty->getName(), $resourceType->getName()));
327
                            }
328
                        }
329
330
                        $segment->setResult($value);
331
                        $segment = $segment->getNext();
332
                        if (!is_null($segment) && $segment->getIdentifier() == ODataConstants::URI_VALUE_SEGMENT) {
333
                            $segment->setResult($value);
334
                            $segment = $segment->getNext();
335
                        }
336
                    }
337
338
                    break;
339
340
                }
341
342
                if (is_null($segment->getNext()) || $segment->getNext()->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
343
                    $this->applyQueryOptions($segment, $callback);
344
                }
345
            }
346
347
            // Apply $select and $expand options to result set, this function will be always applied
348
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
349
            // not delegate $expand/$select operation to IDSQP2 implementation
350
            $this->handleExpansion();
351
        }
352
353
        return;
354
        return $this->executeBase(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...
355
            $requestMethod = $uriProcessor->service->getOperationContext()->incomingRequest()->getMethod();
356
            $resourceSet = $segment->getTargetResourceSetWrapper();
357
            $data = $uriProcessor->request->getData();
358
359
            if (!$resourceSet) {
360
                $url = $uriProcessor->service->getHost()->getAbsoluteRequestUri()->getUrlAsString();
361
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
362
            }
363
364
            if (!$data) {
365
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
366
            }
367
368
            $entity = $uriProcessor->providers->postResource($resourceSet, $data);
369
370
            $segment->setSingleResult(true);
371
            $segment->setResult($entity);
372
373
            return $entity;
374
        });
375
    }
376
377
    /**
378
     * Execute the client submitted request against the data source
379
     *
380
     * @param callable $callback Function, what must be called
381
     */
382
    protected function executeBase($callback = null)
383
    {
384
        $segments = $this->request->getSegments();
385
386
        foreach ($segments as $segment) {
387
388
            $requestTargetKind = $segment->getTargetKind();
389
390
            if ($segment->getTargetSource() == TargetSource::ENTITY_SET) {
391
                $this->handleSegmentTargetsToResourceSet($segment);
392
            } else if ($requestTargetKind == TargetKind::RESOURCE()) {
393
                if (is_null($segment->getPrevious()->getResult())) {
394
                    throw ODataException::createResourceNotFoundError(
395
                        $segment->getPrevious()->getIdentifier()
396
                    );
397
                }
398
                $this->_handleSegmentTargetsToRelatedResource($segment);
399
            } else if ($requestTargetKind == TargetKind::LINK) {
400
                $segment->setResult($segment->getPrevious()->getResult());
401
            } else if ($segment->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
402
                // we are done, $count will the last segment and
403
                // taken care by _applyQueryOptions method
404
                $segment->setResult($this->request->getCountValue());
405
                break;
406
            } else {
407
                if ($requestTargetKind == TargetKind::MEDIA_RESOURCE) {
408
                    if (is_null($segment->getPrevious()->getResult())) {
409
                        throw ODataException::createResourceNotFoundError(
410
                            $segment->getPrevious()->getIdentifier()
411
                        );
412
                    }
413
                    // For MLE and Named Stream the result of last segment
414
                    // should be that of previous segment, this is required
415
                    // while retrieving content type or stream from IDSSP
416
                    $segment->setResult($segment->getPrevious()->getResult());
417
                    // we are done, as named stream property or $value on
418
                    // media resource will be the last segment
419
                    break;
420
                }
421
422
                $value = $segment->getPrevious()->getResult();
423
                while (!is_null($segment)) {
424
                    //TODO: what exactly is this doing here?  Once a null's found it seems everything will be null
425
                    if (!is_null($value)) {
426
                        $value = null;
427
                    } else {
428
                        try {
429
                            //see #88
430
                            $property = new \ReflectionProperty($value, $segment->getIdentifier());
431
                            $value = $property->getValue($value);
432
                        } catch (\ReflectionException $reflectionException) {
433
                            //throw ODataException::createInternalServerError(Messages::orderByParserFailedToAccessOrInitializeProperty($resourceProperty->getName(), $resourceType->getName()));
434
                        }
435
                    }
436
437
                    $segment->setResult($value);
438
                    $segment = $segment->getNext();
439
                    if (!is_null($segment) && $segment->getIdentifier() == ODataConstants::URI_VALUE_SEGMENT) {
440
                        $segment->setResult($value);
441
                        $segment = $segment->getNext();
442
                    }
443
                }
444
445
                break;
446
447
            }
448
449
            if (is_null($segment->getNext()) || $segment->getNext()->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
450
                $this->applyQueryOptions($segment, $callback);
451
            }
452
        }
453
454
            // Apply $select and $expand options to result set, this function will be always applied
455
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
456
            // not delegate $expand/$select operation to IDSQP2 implementation
457
        $this->handleExpansion();
458
    }
459
460
    /**
461
     * Query for a resource set pointed by the given segment descriptor and update the descriptor with the result.
462
     *
463
     * @param SegmentDescriptor $segment Describes the resource set to query
464
     * @return void
465
     *
466
     */
467
    private function handleSegmentTargetsToResourceSet(SegmentDescriptor $segment) {
468
        if ($segment->isSingleResult()) {
469
            $entityInstance = $this->providers->getResourceFromResourceSet(
470
                $segment->getTargetResourceSetWrapper(),
471
                $segment->getKeyDescriptor()
472
            );
473
474
            $segment->setResult($entityInstance);
475
476
        } else {
477
478
            $internalskiptokentinfo = $this->request->getInternalSkipTokenInfo();
479
480
            $queryResult = $this->providers->getResourceSet(
481
                $this->request->queryType,
482
                $segment->getTargetResourceSetWrapper(),
483
                $this->request->getFilterInfo(),
484
                $this->request->getInternalOrderByInfo(),
485
                $this->request->getTopCount(),
486
                $this->request->getSkipCount(),
487
                $internalskiptokentinfo ? $internalskiptokentinfo->getSkipTokenInfo() : null,
488
                $this->_getExpandedProjectionNodes()
489
            );
490
            $segment->setResult($queryResult);
491
        }
492
    }
493
494
    /**
495
     * Query for a related resource set or resource set reference pointed by the
496
     * given segment descriptor and update the descriptor with the result.
497
     *
498
     * @param SegmentDescriptor &$segment Describes the related resource
499
     *                                              to query.
500
     *
501
     * @return void
502
     */
503
    private function _handleSegmentTargetsToRelatedResource(SegmentDescriptor $segment) {
504
        $projectedProperty = $segment->getProjectedProperty();
505
        $projectedPropertyKind = $projectedProperty->getKind();
506
507
        if ($projectedPropertyKind == ResourcePropertyKind::RESOURCESET_REFERENCE) {
0 ignored issues
show
introduced by
The condition $projectedPropertyKind =...::RESOURCESET_REFERENCE is always false.
Loading history...
508
            if ($segment->isSingleResult()) {
509
                $entityInstance = $this->providers->getResourceFromRelatedResourceSet(
510
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
511
                    $segment->getPrevious()->getResult(),
512
                    $segment->getTargetResourceSetWrapper(),
513
                    $projectedProperty,
514
                    $segment->getKeyDescriptor()
515
                );
516
517
                $segment->setResult($entityInstance);
518
            } else {
519
                $queryResult = $this->providers->getRelatedResourceSet(
520
                    $this->request->queryType,
521
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
522
                    $segment->getPrevious()->getResult(),
523
                    $segment->getTargetResourceSetWrapper(),
524
                    $segment->getProjectedProperty(),
525
                    $this->request->getFilterInfo(),
526
                    //TODO: why are these null?  see #98
527
                    null, // $orderby
528
                    null, // $top
529
                    null  // $skip
530
                );
531
532
                $segment->setResult($queryResult);
533
            }
534
        } else if ($projectedPropertyKind == ResourcePropertyKind::RESOURCE_REFERENCE) {
0 ignored issues
show
introduced by
The condition $projectedPropertyKind =...ind::RESOURCE_REFERENCE is always false.
Loading history...
535
            $entityInstance = $this->providers->getRelatedResourceReference(
536
                $segment->getPrevious()->getTargetResourceSetWrapper(),
537
                $segment->getPrevious()->getResult(),
538
                $segment->getTargetResourceSetWrapper(),
539
                $segment->getProjectedProperty()
540
            );
541
542
            $segment->setResult($entityInstance);
543
        } else {
544
            //Unexpected state
545
        }
546
    }
547
548
    /**
549
     * Applies the query options to the resource(s) retrieved from the data source.
550
     *
551
     * @param SegmentDescriptor $segment The descriptor which holds resource(s) on which query options to be applied.
552
     * @param callable $callback Function, what must be called
553
     *
554
     */
555
    private function applyQueryOptions(SegmentDescriptor $segment, $callback = null)
556
    {
557
        // For non-GET methods
558
        if ($callback) {
559
            $callback($this, $segment);
560
            return;
561
        }
562
563
        //TODO: I'm not really happy with this..i think i'd rather keep the result the QueryResult
564
        //not even bother with the setCountValue stuff (shouldn't counts be on segments?)
565
        //and just work with the QueryResult in the object model serializer
566
        $result = $segment->getResult();
567
568
        if (!$result instanceof QueryResult) {
569
            //If the segment isn't a query result, then there's no paging or counting to be done
570
            return;
571
        }
572
573
574
        // Note $inlinecount=allpages means include the total count regardless of paging..so we set the counts first
575
        // regardless if POData does the paging or not.
576
        if ($this->request->queryType == QueryType::ENTITIES_WITH_COUNT()) {
577
            if ($this->providers->handlesOrderedPaging()) {
578
                $this->request->setCountValue($result->count);
579
            } else {
580
                $this->request->setCountValue(count($result->results));
581
            }
582
        }
583
584
        //Have POData perform paging if necessary
585
        if (!$this->providers->handlesOrderedPaging() && !empty($result->results)) {
586
            $result->results = $this->performPaging($result->results);
587
        }
588
589
        //a bit surprising, but $skip and $top affects $count so update it here, not above
590
        //IE  data.svc/Collection/$count?$top=10 returns 10 even if Collection has 11+ entries
591
        if ($this->request->queryType == QueryType::COUNT()) {
592
            if ($this->providers->handlesOrderedPaging()) {
593
                $this->request->setCountValue($result->count);
594
            } else {
595
                $this->request->setCountValue(count($result->results));
596
            }
597
        }
598
599
        $segment->setResult($result->results);
600
    }
601
602
    /**
603
     * If the provider does not perform the paging (ordering, top, skip) then this method does it
604
     *
605
     * @param array $result
606
     * @return array
607
     */
608
    private function performPaging(array $result)
609
    {
610
        //Apply (implicit and explicit) $orderby option
611
        $internalOrderByInfo = $this->request->getInternalOrderByInfo();
612
        if (!is_null($internalOrderByInfo)) {
613
            $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

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

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