Test Failed
Push — master ( a1e735...534e7d )
by Bálint
13:42 queued 13s
created

UriProcessor::applyQueryOptions()   B

Complexity

Conditions 9
Paths 20

Size

Total Lines 45
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

681
            $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...
682
            usort($result, $orderByFunction);
683
        }
684
685
        //Apply $skiptoken option
686
        $internalSkipTokenInfo = $this->request->getInternalSkipTokenInfo();
687
        if (!is_null($internalSkipTokenInfo)) {
688
            $matchingIndex = $internalSkipTokenInfo->getIndexOfFirstEntryInTheNextPage($result);
689
            $result = array_slice($result, $matchingIndex);
690
        }
691
692
        //Apply $top and $skip option
693
        if (!empty($result)) {
694
            $top  = $this->request->getTopCount();
695
            $skip = $this->request->getSkipCount();
696
            if (is_null($skip)) {
697
                $skip = 0;
698
            }
699
700
            $result = array_slice($result, $skip, $top);
701
        }
702
703
        return $result;
704
    }
705
706
707
    /**
708
     * Perform expansion.
709
     *
710
     * @return void
711
     */
712
    private function handleExpansion()
713
    {
714
        $node = $this->request->getRootProjectionNode();
715
        if (!is_null($node) && $node->isExpansionSpecified()) {
716
            $result = $this->request->getTargetResult();
717
            if (!is_null($result) || is_iterable($result) && !empty($result)) {
718
                $needPop = $this->_pushSegmentForRoot();
719
                $this->_executeExpansion($result);
720
                $this->_popSegment($needPop);
721
            }
722
        }
723
    }
724
725
    /**
726
     * Execute queries for expansion.
727
     *
728
     * @param array(mixed)/mixed $result Resource(s) whose navigation properties needs to be expanded.
729
     *
730
     *
731
     * @return void
732
     */
733
    private function _executeExpansion($result)
734
    {
735
        $expandedProjectionNodes = $this->_getExpandedProjectionNodes($this->request);
736
        foreach ($expandedProjectionNodes as $expandedProjectionNode) {
737
            $isCollection = $expandedProjectionNode->getResourceProperty()->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE;
738
            $expandedPropertyName = $expandedProjectionNode->getResourceProperty()->getName();
739
            if (is_iterable($result)) {
740
                foreach ($result as $entry) {
741
                    // Check for null entry
742
                    if ($isCollection) {
743
                        $currentResourceSet = $this->_getCurrentResourceSetWrapper()->getResourceSet();
744
                        $resourceSetOfProjectedProperty = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
745
                        $projectedProperty1 = $expandedProjectionNode->getResourceProperty();
746
                        $result1 = $this->providers->getRelatedResourceSet(
747
                            QueryType::ENTITIES, //it's always entities for an expansion
748
                            $currentResourceSet,
749
                            $entry,
750
                            $resourceSetOfProjectedProperty,
751
                            $projectedProperty1,
752
                            null, // $filter
753
                            null, // $orderby
754
                            null, // $top
755
                            null  // $skip
756
                        )->results;
757
                        if (!empty($result1)) {
758
                            $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
0 ignored issues
show
Unused Code introduced by
The assignment to $internalOrderByInfo is dead and can be removed.
Loading history...
759
                            /*if (!is_null($internalOrderByInfo)) {
760
                                $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
761
                                usort($result1, $orderByFunction);
762
                                unset($internalOrderByInfo);
763
                                $takeCount = $expandedProjectionNode->getTakeCount();
764
                                if (!is_null($takeCount)) {
765
                                    $result1 = array_slice($result1, 0, $takeCount);
766
                                }
767
                            }*/
768
769
                            $entry->$expandedPropertyName = $result1;
770
                            $projectedProperty = $expandedProjectionNode->getResourceProperty();
771
                            $needPop = $this->_pushSegmentForNavigationProperty(
772
                                $projectedProperty
773
                            );
774
                            $this->_executeExpansion($result1);
775
                            $this->_popSegment($needPop);
776
                        } else {
777
                            $entry->$expandedPropertyName = array();
778
                        }
779
                    } else {
780
                        $currentResourceSet1 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
781
                        $resourceSetOfProjectedProperty1 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
782
                        $projectedProperty2 = $expandedProjectionNode->getResourceProperty();
783
                        $result1 = $this->providers->getRelatedResourceReference(
784
                            $currentResourceSet1,
785
                            $entry,
786
                            $resourceSetOfProjectedProperty1,
787
                            $projectedProperty2
788
                        );
789
                        $entry->$expandedPropertyName = $result1;
790
                        if (!is_null($result1)) {
791
                            $projectedProperty3 = $expandedProjectionNode->getResourceProperty();
792
                            $needPop = $this->_pushSegmentForNavigationProperty(
793
                                $projectedProperty3
794
                            );
795
                            $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

795
                            $this->_executeExpansion(/** @scrutinizer ignore-type */ $result1);
Loading history...
796
                            $this->_popSegment($needPop);
797
                        }
798
                    }
799
                }
800
            } else {
801
                if ($isCollection) {
802
                    $currentResourceSet2 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
803
                    $resourceSetOfProjectedProperty2 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
804
                    $projectedProperty4 = $expandedProjectionNode->getResourceProperty();
805
                    $result1 = $this->providers->getRelatedResourceSet(
806
                        QueryType::ENTITIES, //it's always entities for an expansion
807
                        $currentResourceSet2,
808
                        $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

808
                        /** @scrutinizer ignore-type */ $result,
Loading history...
809
                        $resourceSetOfProjectedProperty2,
810
                        $projectedProperty4,
811
                        null, // $filter
812
                        null, // $orderby
813
                        null, // $top
814
                        null  // $skip
815
                    )->results;
816
                    if (!empty($result1)) {
817
                        $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
818
                        /*
819
                        if (!is_null($internalOrderByInfo)) {
820
                            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
821
                            usort($result1, $orderByFunction);
822
                            unset($internalOrderByInfo);
823
                            $takeCount = $expandedProjectionNode->getTakeCount();
824
                            if (!is_null($takeCount)) {
825
                                $result1 = array_slice($result1, 0, $takeCount);
826
                            }
827
                        }
828
                        */
829
830
                        $result->$expandedPropertyName = $result1;
831
                        $projectedProperty7 = $expandedProjectionNode->getResourceProperty();
832
                        $needPop = $this->_pushSegmentForNavigationProperty(
833
                            $projectedProperty7
834
                        );
835
                        $this->_executeExpansion($result1);
836
                        $this->_popSegment($needPop);
837
                    } else {
838
                        $result->$expandedPropertyName = array();
839
                    }
840
                } else {
841
                    $currentResourceSet3 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
842
                    $resourceSetOfProjectedProperty3 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
843
                    $projectedProperty5 = $expandedProjectionNode->getResourceProperty();
844
                    $result1 = $this->providers->getRelatedResourceReference(
845
                        $currentResourceSet3,
846
                        $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

846
                        /** @scrutinizer ignore-type */ $result,
Loading history...
847
                        $resourceSetOfProjectedProperty3,
848
                        $projectedProperty5
849
                    );
850
                    $result->$expandedPropertyName = $result1;
851
                    if (!is_null($result1)) {
852
                        $projectedProperty6 = $expandedProjectionNode->getResourceProperty();
853
                        $needPop = $this->_pushSegmentForNavigationProperty(
854
                            $projectedProperty6
855
                        );
856
                        $this->_executeExpansion($result1);
857
                        $this->_popSegment($needPop);
858
                    }
859
                }
860
            }
861
        }
862
    }
863
864
    /**
865
     * Resource set wrapper for the resource being retireved.
866
     *
867
     * @return ResourceSetWrapper
868
     */
869
    private function _getCurrentResourceSetWrapper()
870
    {
871
        $count = count($this->_segmentResourceSetWrappers);
872
        if ($count == 0) {
873
            return $this->request->getTargetResourceSetWrapper();
874
        } else {
875
            return $this->_segmentResourceSetWrappers[$count - 1];
876
        }
877
    }
878
879
    /**
880
     * Pushes a segment for the root of the tree
881
     * Note: Calls to this method should be balanced with calls to popSegment.
882
     *
883
     * @return bool true if the segment was pushed, false otherwise.
884
     */
885
    private function _pushSegmentForRoot()
886
    {
887
        $segmentName = $this->request->getContainerName();
888
        $segmentResourceSetWrapper
889
            = $this->request->getTargetResourceSetWrapper();
890
        return $this->_pushSegment($segmentName, $segmentResourceSetWrapper);
891
    }
892
893
    /**
894
     * Pushes a segment for the current navigation property being written out.
895
     * Note: Refer 'ObjectModelSerializerNotes.txt' for more details about
896
     * 'Segment Stack' and this method.
897
     * Note: Calls to this method should be balanced with calls to popSegment.
898
     *
899
     * @param ResourceProperty &$resourceProperty Current navigation property
900
     *                                            being written out
901
     *
902
     * @return bool true if a segment was pushed, false otherwise
903
     *
904
     * @throws InvalidOperationException If this function invoked with non-navigation
905
     *                                   property instance.
906
     */
907
    private function _pushSegmentForNavigationProperty(ResourceProperty &$resourceProperty)
908
    {
909
        if ($resourceProperty->getTypeKind() == ResourceTypeKind::ENTITY) {
0 ignored issues
show
introduced by
The condition $resourceProperty->getTy...esourceTypeKind::ENTITY is always false.
Loading history...
910
            $this->assert(
911
                !empty($this->_segmentNames),
912
                '!is_empty($this->_segmentNames'
913
            );
914
            $currentResourceSetWrapper = $this->_getCurrentResourceSetWrapper();
915
            $currentResourceType = $currentResourceSetWrapper->getResourceType();
916
            $currentResourceSetWrapper = $this->service
917
                ->getProvidersWrapper()
918
                ->getResourceSetWrapperForNavigationProperty(
919
                    $currentResourceSetWrapper,
920
                    $currentResourceType,
921
                    $resourceProperty
922
                );
923
924
            $this->assert(
925
                !is_null($currentResourceSetWrapper),
926
                '!null($currentResourceSetWrapper)'
927
            );
928
            return $this->_pushSegment(
929
                $resourceProperty->getName(),
930
                $currentResourceSetWrapper
931
            );
932
        } else {
933
            throw new InvalidOperationException(
934
                'pushSegmentForNavigationProperty should not be called with non-entity type'
935
            );
936
        }
937
    }
938
939
    /**
940
     * Gets collection of expanded projection nodes under the current node.
941
     *
942
     * @return ExpandedProjectionNode[] List of nodes describing expansions for the current segment
943
     *
944
     */
945
    private function _getExpandedProjectionNodes($request)
946
    {
947
        $expandedProjectionNode = $this->_getCurrentExpandedProjectionNode($request);
948
        $expandedProjectionNodes = array();
949
        if (!is_null($expandedProjectionNode)) {
950
            foreach ($expandedProjectionNode->getChildNodes() as $node) {
951
                if ($node instanceof ExpandedProjectionNode) {
952
                    $expandedProjectionNodes[] = $node;
953
                }
954
            }
955
        }
956
957
        return $expandedProjectionNodes;
958
    }
959
960
    /**
961
     * Find a 'ExpandedProjectionNode' instance in the projection tree
962
     * which describes the current segment.
963
     *
964
     * @return ExpandedProjectionNode|null
965
     */
966
    private function _getCurrentExpandedProjectionNode($request)
967
    {
968
        $expandedProjectionNode
969
            = $request->getRootProjectionNode();
970
        if (!is_null($expandedProjectionNode)) {
971
            $depth = count($this->_segmentNames);
972
            if ($depth != 0) {
973
                for ($i = 1; $i < $depth; $i++) {
974
                    $expandedProjectionNode
975
                        = $expandedProjectionNode->findNode($this->_segmentNames[$i]);
976
                        $this->assert(
977
                            !is_null($expandedProjectionNode),
978
                            '!is_null($expandedProjectionNode)'
979
                        );
980
                        $this->assert(
981
                            $expandedProjectionNode instanceof ExpandedProjectionNode,
982
                            '$expandedProjectionNode instanceof ExpandedProjectionNode'
983
                        );
984
                }
985
            }
986
        }
987
988
        return $expandedProjectionNode;
989
    }
990
991
    /**
992
     * Pushes information about the segment whose instance is going to be
993
     * retrieved from the IDSQP implementation
994
     * Note: Calls to this method should be balanced with calls to popSegment.
995
     *
996
     * @param string             $segmentName         Name of segment to push.
997
     * @param ResourceSetWrapper &$resourceSetWrapper The resource set wrapper
998
     *                                                to push.
999
     *
1000
     * @return bool true if the segment was push, false otherwise
1001
     */
1002
    private function _pushSegment($segmentName, ResourceSetWrapper &$resourceSetWrapper)
1003
    {
1004
        $rootProjectionNode = $this->request->getRootProjectionNode();
1005
        if (!is_null($rootProjectionNode)
1006
            && $rootProjectionNode->isExpansionSpecified()
1007
        ) {
1008
            array_push($this->_segmentNames, $segmentName);
1009
            array_push($this->_segmentResourceSetWrappers, $resourceSetWrapper);
1010
            return true;
1011
        }
1012
1013
        return false;
1014
    }
1015
1016
    /**
1017
     * Pops segment information from the 'Segment Stack'
1018
     * Note: Calls to this method should be balanced with previous calls
1019
     * to _pushSegment.
1020
     *
1021
     * @param boolean $needPop Is a pop required. Only true if last push
1022
     *                         was successful.
1023
     *
1024
     * @return void
1025
     *
1026
     * @throws InvalidOperationException If found un-balanced call
1027
     *                                   with _pushSegment
1028
     */
1029
    private function _popSegment($needPop)
1030
    {
1031
        if ($needPop) {
1032
            if (!empty($this->_segmentNames)) {
1033
                array_pop($this->_segmentNames);
1034
                array_pop($this->_segmentResourceSetWrappers);
1035
            } else {
1036
                throw new InvalidOperationException(
1037
                    'Found non-balanced call to _pushSegment and popSegment'
1038
                );
1039
            }
1040
        }
1041
    }
1042
1043
    /**
1044
     * Assert that the given condition is true.
1045
     *
1046
     * @param boolean $condition         Constion to assert.
1047
     * @param string  $conditionAsString Message to show incase assertion fails.
1048
     *
1049
     * @return void
1050
     *
1051
     * @throws InvalidOperationException
1052
     */
1053
    protected function assert($condition, $conditionAsString)
1054
    {
1055
        if (!$condition) {
1056
            throw new InvalidOperationException(
1057
                "Unexpected state, expecting $conditionAsString"
1058
            );
1059
        }
1060
    }
1061
}
1062