Completed
Push — master ( 67f7ff...a4bc63 )
by Alex
45s
created

UriProcessor::handleSegmentTargetsToResourceSet()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 24
rs 8.9713
cc 3
eloc 18
nc 3
nop 1
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
class UriProcessor
35
{
36
    /**
37
     * Description of the OData request that a client has submitted.
38
     *
39
     * @var RequestDescription
40
     */
41
    private $request;
42
43
    /**
44
     * Holds reference to the data service instance.
45
     *
46
     * @var IService
47
     */
48
    private $service;
49
50
    /**
51
     * Holds reference to the wrapper over IDSMP and IDSQP implementation.
52
     *
53
     * @var ProvidersWrapper
54
     */
55
    private $providers;
56
57
    /**
58
     * Collection of segment names.
59
     *
60
     * @var string[]
61
     */
62
    private $_segmentNames;
63
64
    /**
65
     * Collection of segment ResourceSetWrapper instances.
66
     *
67
     * @var ResourceSetWrapper[]
68
     */
69
    private $_segmentResourceSetWrappers;
70
71
    /**
72
     * Constructs a new instance of UriProcessor.
73
     *
74
     * @param IService $service Reference to the data service instance
75
     */
76
    private function __construct(IService $service)
77
    {
78
        $this->service = $service;
79
        $this->providers = $service->getProvidersWrapper();
80
        $this->_segmentNames = array();
81
        $this->_segmentResourceSetWrappers = array();
82
    }
83
84
    /**
85
     * Process the resource path and query options of client's request uri.
86
     *
87
     * @param IService $service Reference to the data service instance
88
     *
89
     * @return URIProcessor
90
     *
91
     * @throws ODataException
92
     */
93
    public static function process(IService $service)
94
    {
95
        $absoluteRequestUri = $service->getHost()->getAbsoluteRequestUri();
96
        $absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri();
97
98
        if (!$absoluteServiceUri->isBaseOf($absoluteRequestUri)) {
99
            throw ODataException::createInternalServerError(
100
                Messages::uriProcessorRequestUriDoesNotHaveTheRightBaseUri(
101
                    $absoluteRequestUri->getUrlAsString(),
102
                    $absoluteServiceUri->getUrlAsString()
103
                )
104
            );
105
        }
106
107
        $uriProcessor = new self($service);
108
        //Parse the resource path part of the request Uri.
109
        $uriProcessor->request = ResourcePathProcessor::process($service);
110
111
        $uriProcessor->request->setUriProcessor($uriProcessor);
112
113
        //Parse the query string options of the request Uri.
114
        QueryProcessor::process($uriProcessor->request, $service);
115
116
        return $uriProcessor;
117
    }
118
119
    /**
120
     * Gets reference to the request submitted by client.
121
     *
122
     * @return RequestDescription
123
     */
124
    public function getRequest()
125
    {
126
        return $this->request;
127
    }
128
129
    /**
130
     * Gets the data service instance
131
     *
132
     * @return IService
133
     */
134
    public function getService()
135
    {
136
        return $this->service;
137
    }
138
139
    /**
140
     * Execute the client submitted request against the data source.
141
     */
142
    public function execute()
143
    {
144
        $service = $this->getService();
145
        $operationContext = !isset($service) ? null : $service->getOperationContext();
146
        if (!$operationContext) {
147
            $this->executeBase();
148
149
            return;
150
        }
151
152
        $requestMethod = $operationContext->incomingRequest()->getMethod();
153
        if ($requestMethod == HTTPRequestMethod::GET()) {
154
            $this->executeGet();
155
        } elseif ($requestMethod == HTTPRequestMethod::POST()) {
156
            $this->executePost();
157
        } elseif ($requestMethod == HTTPRequestMethod::PUT()) {
158
            $this->executePut();
159
        } elseif ($requestMethod == HTTPRequestMethod::DELETE()) {
160
            $this->executeDelete();
161
            //TODO: we probably need these verbes eventually.
162
        /*} elseif ($requestMethod == HTTPRequestMethod::PATCH()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
163
            $this->executePatch();
164
        } elseif ($requestMethod == HTTPRequestMethod::MERGE()) {
165
            $this->executeMerge();*/
166
        } else {
167
            throw ODataException::createNotImplementedError(Messages::onlyReadSupport($requestMethod));
168
        }
169
    }
170
171
 
172
173
    /**
174
     * Execute the client submitted request against the data source (GET).
175
     */
176
    protected function executeGet()
177
    {
178
        return $this->executeBase();
179
    }
180
            /**
181
             * Execute the client submitted request against the data source (POST).
182
             */
183
    protected function executePost()
184
    {
185
        $segments = $this->request->getSegments();
186
187
        foreach ($segments as $segment) {
188
            $requestTargetKind = $segment->getTargetKind();
189
            if ($requestTargetKind == TargetKind::RESOURCE()) {
190
                $requestMethod = $this->getService()->getOperationContext()->incomingRequest()->getMethod();
191
                $resourceSet = $segment->getTargetResourceSetWrapper();
192
                $keyDescriptor = $segment->getKeyDescriptor();
193
                $data = $this->request->getData();
194
                if (!$resourceSet) {
195
                    $url = $this->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
196
                    throw ODataException::createBadRequestError(
197
                        Messages::badRequestInvalidUriForThisVerb($url, $requestMethod)
198
                    );
199
                }
200
                if (!$data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
201
                    throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
202
                }
203
204
                $queryResult = $this->providers->createResourceforResourceSet($resourceSet, $keyDescriptor, $data);
0 ignored issues
show
Documentation introduced by
$data is of type array, but the function expects a object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
205
                $segment->setResult($queryResult);
206
            }
207
        }
208
        //return $this->executeBase();
0 ignored issues
show
Unused Code Comprehensibility introduced by
75% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
209
    }
210
    /**
211
     * Execute the client submitted request against the data source (PUT).
212
     */
213
    protected function executePut()
214
    {
215
        return $this->executeBase(function($uriProcessor, $segment) {
216
            $requestMethod = $uriProcessor->getService()->getOperationContext()->incomingRequest()->getMethod();
217
            $resourceSet = $segment->getTargetResourceSetWrapper();
218
            $keyDescriptor = $segment->getKeyDescriptor();
219
            $data = $uriProcessor->request->getData();
220
            if (!$resourceSet || !$keyDescriptor) {
221
                $url = $uriProcessor->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
222
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
223
            }
224
225
            if (!$data) {
226
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
227
            }
228
229
            $queryResult = $uriProcessor->providers->updateResource($resourceSet, $segment->getResult(), $keyDescriptor, $data, false);
230
            $segment->setResult($queryResult);
231
            return $queryResult;
232
        });
233
    }
234
235
    /**
236
     * Execute the client submitted request against the data source (DELETE).
237
     */
238
    protected function executeDelete()
239
    {
240
        return $this->executeBase(function($uriProcessor, $segment) {
241
            $requestMethod = $uriProcessor->getService()->getOperationContext()->incomingRequest()->getMethod();
242
            $resourceSet = $segment->getTargetResourceSetWrapper();
243
            $keyDescriptor = $segment->getKeyDescriptor();
244
            if (!$resourceSet || !$keyDescriptor) {
245
                $url = $uriProcessor->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
246
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
247
            }
248
            return $uriProcessor->providers->deleteResource($resourceSet, $segment->getResult());
249
        });
250
    }
251
252
    /**
253
     * Execute the client submitted request against the data source.
254
     *
255
     * @param callable $callback Function, what must be called
256
     */
257
    protected function executeBase($callback = null)
258
    {
259
        $segments = $this->request->getSegments();
260
261
        foreach ($segments as $segment) {
262
            $requestTargetKind = $segment->getTargetKind();
263
264
            if ($segment->getTargetSource() == TargetSource::ENTITY_SET) {
265
                $this->handleSegmentTargetsToResourceSet($segment);
266
            } elseif ($requestTargetKind == TargetKind::RESOURCE()) {
267
                if (is_null($segment->getPrevious()->getResult())) {
268
                    throw ODataException::createResourceNotFoundError(
269
                        $segment->getPrevious()->getIdentifier()
270
                    );
271
                }
272
                $this->_handleSegmentTargetsToRelatedResource($segment);
273
            } elseif ($requestTargetKind == TargetKind::LINK()) {
274
                $segment->setResult($segment->getPrevious()->getResult());
275
            } elseif ($segment->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
276
                // we are done, $count will the last segment and
277
                // taken care by _applyQueryOptions method
278
                $segment->setResult($this->request->getCountValue());
279
                break;
280
            } else {
281
                if ($requestTargetKind == TargetKind::MEDIA_RESOURCE()) {
282
                    if (is_null($segment->getPrevious()->getResult())) {
283
                        throw ODataException::createResourceNotFoundError(
284
                            $segment->getPrevious()->getIdentifier()
285
                        );
286
                    }
287
                    // For MLE and Named Stream the result of last segment
288
                    // should be that of previous segment, this is required
289
                    // while retrieving content type or stream from IDSSP
290
                    $segment->setResult($segment->getPrevious()->getResult());
291
                    // we are done, as named stream property or $value on
292
                    // media resource will be the last segment
293
                    break;
294
                }
295
296
                $value = $segment->getPrevious()->getResult();
297
                while (!is_null($segment)) {
298
                    //TODO: what exactly is this doing here?  Once a null's found it seems everything will be null
299
                    if (!is_null($value)) {
300
                        $value = null;
301
                    } else {
302
                        // This is theoretically impossible to reach, but should that be changed, this will need to call ResourceType::getPropertyValue... somehow
303
                        try {
304
                            //see #88
305
                            $property = new \ReflectionProperty($value, $segment->getIdentifier());
306
                            $value = $property->getValue($value);
307
                        } catch (\ReflectionException $reflectionException) {
308
                            //throw ODataException::createInternalServerError(Messages::orderByParserFailedToAccessOrInitializeProperty($resourceProperty->getName(), $resourceType->getName()));
0 ignored issues
show
Unused Code Comprehensibility introduced by
68% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
309
                        }
310
                    }
311
312
                    $segment->setResult($value);
313
                    $segment = $segment->getNext();
314
                    if (!is_null($segment) && $segment->getIdentifier() == ODataConstants::URI_VALUE_SEGMENT) {
315
                        $segment->setResult($value);
316
                        $segment = $segment->getNext();
317
                    }
318
                }
319
320
                break;
321
            }
322
323
            if (is_null($segment->getNext()) || $segment->getNext()->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
324
                $this->applyQueryOptions($segment, $callback);
325
            }
326
        }
327
328
            // Apply $select and $expand options to result set, this function will be always applied
329
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
330
            // not delegate $expand/$select operation to IDSQP2 implementation
331
        $this->handleExpansion();
332
    }
333
334
    /**
335
     * Query for a resource set pointed by the given segment descriptor and update the descriptor with the result.
336
     *
337
     * @param SegmentDescriptor $segment Describes the resource set to query
338
     */
339
    private function handleSegmentTargetsToResourceSet(SegmentDescriptor $segment)
340
    {
341
        if ($segment->isSingleResult()) {
342
            $entityInstance = $this->providers->getResourceFromResourceSet(
343
                $segment->getTargetResourceSetWrapper(),
344
                $segment->getKeyDescriptor()
345
            );
346
347
            $segment->setResult($entityInstance);
348
        } else {
349
            $skip = (null == $this->request->getInternalSkipTokenInfo()) ? 0 :
350
                $this->request->getInternalSkipTokenInfo()->getSkipTokenInfo()->getOrderByKeysInToken()[0][0];
351
            $queryResult = $this->providers->getResourceSet(
352
                $this->request->queryType,
353
                $segment->getTargetResourceSetWrapper(),
354
                $this->request->getFilterInfo(),
355
                $this->request->getInternalOrderByInfo(),
356
                $this->request->getTopCount(),
357
                $skip,
358
                null
359
            );
360
            $segment->setResult($queryResult);
361
        }
362
    }
363
364
    /**
365
     * Query for a related resource set or resource set reference pointed by the
366
     * given segment descriptor and update the descriptor with the result.
367
     *
368
     * @param SegmentDescriptor &$segment Describes the related resource
369
     *                                    to query
370
     */
371
    private function _handleSegmentTargetsToRelatedResource(SegmentDescriptor $segment)
372
    {
373
        $projectedProperty = $segment->getProjectedProperty();
374
        $projectedPropertyKind = $projectedProperty->getKind();
375
376
        if ($projectedPropertyKind == ResourcePropertyKind::RESOURCESET_REFERENCE) {
377
            if ($segment->isSingleResult()) {
378
                $entityInstance = $this->providers->getResourceFromRelatedResourceSet(
379
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
380
                    $segment->getPrevious()->getResult(),
381
                    $segment->getTargetResourceSetWrapper(),
382
                    $projectedProperty,
383
                    $segment->getKeyDescriptor()
384
                );
385
386
                $segment->setResult($entityInstance);
387
            } else {
388
                $queryResult = $this->providers->getRelatedResourceSet(
389
                    $this->request->queryType,
390
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
391
                    $segment->getPrevious()->getResult(),
392
                    $segment->getTargetResourceSetWrapper(),
393
                    $segment->getProjectedProperty(),
394
                    $this->request->getFilterInfo(),
0 ignored issues
show
Bug introduced by
It seems like $this->request->getFilterInfo() can be null; however, getRelatedResourceSet() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
395
                    //TODO: why are these null?  see #98
396
                    null, // $orderby
397
                    null, // $top
398
                    null  // $skip
399
                );
400
401
                $segment->setResult($queryResult);
402
            }
403
        } elseif ($projectedPropertyKind == ResourcePropertyKind::RESOURCE_REFERENCE) {
404
            $entityInstance = $this->providers->getRelatedResourceReference(
405
                $segment->getPrevious()->getTargetResourceSetWrapper(),
406
                $segment->getPrevious()->getResult(),
407
                $segment->getTargetResourceSetWrapper(),
408
                $segment->getProjectedProperty()
409
            );
410
411
            $segment->setResult($entityInstance);
412
        } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
413
            //Unexpected state
414
        }
415
    }
416
417
    /**
418
     * Applies the query options to the resource(s) retrieved from the data source.
419
     *
420
     * @param SegmentDescriptor $segment  The descriptor which holds resource(s) on which query options to be applied
421
     * @param callable          $callback Function, what must be called
422
     */
423
    private function applyQueryOptions(SegmentDescriptor $segment, $callback = null)
424
    {
425
        // For non-GET methods
426
        if ($callback) {
427
            $callback($this, $segment);
428
429
            return;
430
        }
431
432
        //TODO: I'm not really happy with this..i think i'd rather keep the result the QueryResult
433
        //not even bother with the setCountValue stuff (shouldn't counts be on segments?)
434
        //and just work with the QueryResult in the object model serializer
435
        $result = $segment->getResult();
436
437
        if (!$result instanceof QueryResult) {
438
            //If the segment isn't a query result, then there's no paging or counting to be done
439
            return;
440
        }
441
442
        // Note $inlinecount=allpages means include the total count regardless of paging..so we set the counts first
443
        // regardless if POData does the paging or not.
444 View Code Duplication
        if ($this->request->queryType == QueryType::ENTITIES_WITH_COUNT()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
445
            if ($this->providers->handlesOrderedPaging()) {
446
                $this->request->setCountValue($result->count);
447
            } else {
448
                $this->request->setCountValue(count($result->results));
449
            }
450
        }
451
452
        //Have POData perform paging if necessary
453
        if (!$this->providers->handlesOrderedPaging() && !empty($result->results)) {
454
            $result->results = $this->performPaging($result->results);
455
        }
456
457
        //a bit surprising, but $skip and $top affects $count so update it here, not above
458
        //IE  data.svc/Collection/$count?$top=10 returns 10 even if Collection has 11+ entries
459 View Code Duplication
        if ($this->request->queryType == QueryType::COUNT()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
460
            if ($this->providers->handlesOrderedPaging()) {
461
                $this->request->setCountValue($result->count);
462
            } else {
463
                $this->request->setCountValue(count($result->results));
464
            }
465
        }
466
467
        $segment->setResult($result->results);
468
    }
469
470
    /**
471
     * If the provider does not perform the paging (ordering, top, skip) then this method does it.
472
     *
473
     * @param array $result
474
     *
475
     * @return array
476
     */
477
    private function performPaging(array $result)
478
    {
479
        //Apply (implicit and explicit) $orderby option
480
        $internalOrderByInfo = $this->request->getInternalOrderByInfo();
481
        if (!is_null($internalOrderByInfo)) {
482
            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
483
            usort($result, $orderByFunction);
484
        }
485
486
        //Apply $skiptoken option
487
        $internalSkipTokenInfo = $this->request->getInternalSkipTokenInfo();
488
        if (!is_null($internalSkipTokenInfo)) {
489
            $matchingIndex = $internalSkipTokenInfo->getIndexOfFirstEntryInTheNextPage($result);
490
            $result = array_slice($result, $matchingIndex);
491
        }
492
493
        //Apply $top and $skip option
494
        if (!empty($result)) {
495
            $top = $this->request->getTopCount();
496
            $skip = $this->request->getSkipCount();
497
            if (is_null($skip)) {
498
                $skip = 0;
499
            }
500
501
            $result = array_slice($result, $skip, $top);
502
        }
503
504
        return $result;
505
    }
506
507
    /**
508
     * Perform expansion.
509
     */
510
    private function handleExpansion()
511
    {
512
        $node = $this->request->getRootProjectionNode();
513
        if (!is_null($node) && $node->isExpansionSpecified()) {
514
            $result = $this->request->getTargetResult();
515
            if (!is_null($result) || is_array($result) && !empty($result)) {
516
                $needPop = $this->_pushSegmentForRoot();
517
                $this->_executeExpansion($result);
518
                $this->_popSegment($needPop);
519
            }
520
        }
521
    }
522
523
    /**
524
     * Execute queries for expansion.
525
     *
526
     * @param array(mixed)/mixed $result Resource(s) whose navigation properties needs to be expanded
0 ignored issues
show
Documentation introduced by
The doc-type array(mixed)/mixed could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
527
     */
528
    private function _executeExpansion($result)
529
    {
530
        $expandedProjectionNodes = $this->_getExpandedProjectionNodes();
531
        foreach ($expandedProjectionNodes as $expandedProjectionNode) {
532
            $resourceType = $expandedProjectionNode->getResourceType();
533
            $isCollection = $expandedProjectionNode->getResourceProperty()->getKind() == ResourcePropertyKind::RESOURCESET_REFERENCE;
534
            $expandedPropertyName = $expandedProjectionNode->getResourceProperty()->getName();
535
            if (is_array($result)) {
536
                foreach ($result as $entry) {
537
                    // Check for null entry
538 View Code Duplication
                    if ($isCollection) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
539
                        $currentResourceSet = $this->_getCurrentResourceSetWrapper()->getResourceSet();
540
                        $resourceSetOfProjectedProperty = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
541
                        $projectedProperty1 = $expandedProjectionNode->getResourceProperty();
542
                        $result1 = $this->providers->getRelatedResourceSet(
543
                            QueryType::ENTITIES(), //it's always entities for an expansion
544
                            $currentResourceSet,
545
                            $entry,
546
                            $resourceSetOfProjectedProperty,
547
                            $projectedProperty1,
548
                            null, // $filter
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<POData\UriProcess...ssionParser\FilterInfo>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
549
                            null, // $orderby
550
                            null, // $top
551
                            null  // $skip
552
                        )->results;
553
                        if (!empty($result1)) {
554
                            $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
555
                            if (!is_null($internalOrderByInfo)) {
556
                                $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
557
                                usort($result1, $orderByFunction);
558
                                unset($internalOrderByInfo);
559
                                $takeCount = $expandedProjectionNode->getTakeCount();
560
                                if (!is_null($takeCount)) {
561
                                    $result1 = array_slice($result1, 0, $takeCount);
562
                                }
563
                            }
564
565
                            $resourceType->setPropertyValue($entry, $expandedPropertyName, $result1);
566
                            $projectedProperty = $expandedProjectionNode->getResourceProperty();
567
                            $needPop = $this->_pushSegmentForNavigationProperty(
568
                                $projectedProperty
569
                            );
570
                            $this->_executeExpansion($result1);
571
                            $this->_popSegment($needPop);
572
                        } else {
573
                            $resourceType->setPropertyValue($entry, $expandedPropertyName, array());
574
                        }
575
                    } else {
576
                        $currentResourceSet1 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
577
                        $resourceSetOfProjectedProperty1 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
578
                        $projectedProperty2 = $expandedProjectionNode->getResourceProperty();
579
                        $result1 = $this->providers->getRelatedResourceReference(
580
                            $currentResourceSet1,
581
                            $entry,
582
                            $resourceSetOfProjectedProperty1,
583
                            $projectedProperty2
584
                        );
585
                        $resourceType->setPropertyValue($entry, $expandedPropertyName, $result1);
586
                        if (!is_null($result1)) {
587
                            $projectedProperty3 = $expandedProjectionNode->getResourceProperty();
588
                            $needPop = $this->_pushSegmentForNavigationProperty(
589
                                $projectedProperty3
590
                            );
591
                            $this->_executeExpansion($result1);
592
                            $this->_popSegment($needPop);
593
                        }
594
                    }
595
                }
596 View Code Duplication
            } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
597
                if ($isCollection) {
598
                    $currentResourceSet2 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
599
                    $resourceSetOfProjectedProperty2 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
600
                    $projectedProperty4 = $expandedProjectionNode->getResourceProperty();
601
                    $result1 = $this->providers->getRelatedResourceSet(
602
                        QueryType::ENTITIES(), //it's always entities for an expansion
603
                        $currentResourceSet2,
604
                        $result,
605
                        $resourceSetOfProjectedProperty2,
606
                        $projectedProperty4,
607
                        null, // $filter
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<POData\UriProcess...ssionParser\FilterInfo>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
608
                        null, // $orderby
609
                        null, // $top
610
                        null  // $skip
611
                    )->results;
612
                    if (!empty($result1)) {
613
                        $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
614
                        if (!is_null($internalOrderByInfo)) {
615
                            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
616
                            usort($result1, $orderByFunction);
617
                            unset($internalOrderByInfo);
618
                            $takeCount = $expandedProjectionNode->getTakeCount();
619
                            if (!is_null($takeCount)) {
620
                                $result1 = array_slice($result1, 0, $takeCount);
621
                            }
622
                        }
623
                        $resourceType->setPropertyValue($result, $expandedPropertyName, $result1);
624
                        $projectedProperty7 = $expandedProjectionNode->getResourceProperty();
625
                        $needPop = $this->_pushSegmentForNavigationProperty(
626
                            $projectedProperty7
627
                        );
628
                        $this->_executeExpansion($result1);
629
                        $this->_popSegment($needPop);
630
                    } else {
631
                        $resourceType->setPropertyValue($result, $expandedPropertyName, $result1);
632
                    }
633
                } else {
634
                    $currentResourceSet3 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
635
                    $resourceSetOfProjectedProperty3 = $expandedProjectionNode->getResourceSetWrapper()->getResourceSet();
636
                    $projectedProperty5 = $expandedProjectionNode->getResourceProperty();
637
                    $result1 = $this->providers->getRelatedResourceReference(
638
                        $currentResourceSet3,
639
                        $result,
640
                        $resourceSetOfProjectedProperty3,
641
                        $projectedProperty5
642
                    );
643
                    $resourceType->setPropertyValue($result, $expandedPropertyName, $result1);
644
                    if (!is_null($result1)) {
645
                        $projectedProperty6 = $expandedProjectionNode->getResourceProperty();
646
                        $needPop = $this->_pushSegmentForNavigationProperty(
647
                            $projectedProperty6
648
                        );
649
                        $this->_executeExpansion($result1);
650
                        $this->_popSegment($needPop);
651
                    }
652
                }
653
            }
654
        }
655
    }
656
657
    /**
658
     * Resource set wrapper for the resource being retireved.
659
     *
660
     * @return ResourceSetWrapper
661
     */
662 View Code Duplication
    private function _getCurrentResourceSetWrapper()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
663
    {
664
        $count = count($this->_segmentResourceSetWrappers);
665
        if ($count == 0) {
666
            return $this->request->getTargetResourceSetWrapper();
667
        } else {
668
            return $this->_segmentResourceSetWrappers[$count - 1];
669
        }
670
    }
671
672
    /**
673
     * Pushes a segment for the root of the tree
674
     * Note: Calls to this method should be balanced with calls to popSegment.
675
     *
676
     * @return bool true if the segment was pushed, false otherwise
677
     */
678
    private function _pushSegmentForRoot()
679
    {
680
        $segmentName = $this->request->getContainerName();
681
        $segmentResourceSetWrapper
682
            = $this->request->getTargetResourceSetWrapper();
683
684
        return $this->_pushSegment($segmentName, $segmentResourceSetWrapper);
0 ignored issues
show
Bug introduced by
It seems like $segmentResourceSetWrapper defined by $this->request->getTargetResourceSetWrapper() on line 682 can be null; however, POData\UriProcessor\UriProcessor::_pushSegment() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
685
    }
686
687
    /**
688
     * Pushes a segment for the current navigation property being written out.
689
     * Note: Refer 'ObjectModelSerializerNotes.txt' for more details about
690
     * 'Segment Stack' and this method.
691
     * Note: Calls to this method should be balanced with calls to popSegment.
692
     *
693
     * @param ResourceProperty &$resourceProperty Current navigation property
694
     *                                            being written out
695
     *
696
     * @return bool true if a segment was pushed, false otherwise
697
     *
698
     * @throws InvalidOperationException If this function invoked with non-navigation
699
     *                                   property instance
700
     */
701 View Code Duplication
    private function _pushSegmentForNavigationProperty(ResourceProperty & $resourceProperty)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
702
    {
703
        if ($resourceProperty->getTypeKind() == ResourceTypeKind::ENTITY) {
704
            $this->assert(
705
                !empty($this->_segmentNames),
706
                '!is_empty($this->_segmentNames'
707
            );
708
            $currentResourceSetWrapper = $this->_getCurrentResourceSetWrapper();
709
            $currentResourceType = $currentResourceSetWrapper->getResourceType();
710
            $currentResourceSetWrapper = $this->getService()
711
                ->getProvidersWrapper()
712
                ->getResourceSetWrapperForNavigationProperty(
713
                    $currentResourceSetWrapper,
0 ignored issues
show
Bug introduced by
It seems like $currentResourceSetWrapper defined by $this->getService()->get...ype, $resourceProperty) on line 710 can be null; however, POData\Providers\Provide...ForNavigationProperty() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
714
                    $currentResourceType,
715
                    $resourceProperty
716
                );
717
718
            $this->assert(
719
                !is_null($currentResourceSetWrapper),
720
                '!null($currentResourceSetWrapper)'
721
            );
722
723
            return $this->_pushSegment(
724
                $resourceProperty->getName(),
725
                $currentResourceSetWrapper
0 ignored issues
show
Bug introduced by
It seems like $currentResourceSetWrapper defined by $this->getService()->get...ype, $resourceProperty) on line 710 can be null; however, POData\UriProcessor\UriProcessor::_pushSegment() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
726
            );
727
        } else {
728
            throw new InvalidOperationException(
729
                'pushSegmentForNavigationProperty should not be called with non-entity type'
730
            );
731
        }
732
    }
733
734
    /**
735
     * Gets collection of expanded projection nodes under the current node.
736
     *
737
     * @return ExpandedProjectionNode[] List of nodes describing expansions for the current segment
738
     */
739
    private function _getExpandedProjectionNodes()
740
    {
741
        $expandedProjectionNode = $this->_getCurrentExpandedProjectionNode();
742
        $expandedProjectionNodes = array();
743
        if (!is_null($expandedProjectionNode)) {
744
            foreach ($expandedProjectionNode->getChildNodes() as $node) {
745
                if ($node instanceof ExpandedProjectionNode) {
746
                    $expandedProjectionNodes[] = $node;
747
                }
748
            }
749
        }
750
751
        return $expandedProjectionNodes;
752
    }
753
754
    /**
755
     * Find a 'ExpandedProjectionNode' instance in the projection tree
756
     * which describes the current segment.
757
     *
758
     * @return ExpandedProjectionNode|null
759
     */
760 View Code Duplication
    private function _getCurrentExpandedProjectionNode()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
761
    {
762
        $expandedProjectionNode
763
            = $this->request->getRootProjectionNode();
764
        if (!is_null($expandedProjectionNode)) {
765
            $depth = count($this->_segmentNames);
766
            if ($depth != 0) {
767
                for ($i = 1; $i < $depth; ++$i) {
768
                    $expandedProjectionNode
769
                        = $expandedProjectionNode->findNode($this->_segmentNames[$i]);
770
                    $this->assert(
771
                        !is_null($expandedProjectionNode),
772
                        '!is_null($expandedProjectionNode)'
773
                    );
774
                    $this->assert(
775
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
776
                        '$expandedProjectionNode instanceof ExpandedProjectionNode'
777
                    );
778
                }
779
            }
780
        }
781
782
        return $expandedProjectionNode;
783
    }
784
785
    /**
786
     * Pushes information about the segment whose instance is going to be
787
     * retrieved from the IDSQP implementation
788
     * Note: Calls to this method should be balanced with calls to popSegment.
789
     *
790
     * @param string             $segmentName         Name of segment to push
791
     * @param ResourceSetWrapper &$resourceSetWrapper The resource set wrapper
792
     *                                                to push
793
     *
794
     * @return bool true if the segment was push, false otherwise
795
     */
796 View Code Duplication
    private function _pushSegment($segmentName, ResourceSetWrapper & $resourceSetWrapper)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
797
    {
798
        $rootProjectionNode = $this->request->getRootProjectionNode();
799
        if (!is_null($rootProjectionNode)
800
            && $rootProjectionNode->isExpansionSpecified()
801
        ) {
802
            array_push($this->_segmentNames, $segmentName);
803
            array_push($this->_segmentResourceSetWrappers, $resourceSetWrapper);
804
805
            return true;
806
        }
807
808
        return false;
809
    }
810
811
    /**
812
     * Pops segment information from the 'Segment Stack'
813
     * Note: Calls to this method should be balanced with previous calls
814
     * to _pushSegment.
815
     *
816
     * @param bool $needPop Is a pop required. Only true if last push
817
     *                      was successful
818
     *
819
     * @throws InvalidOperationException If found un-balanced call
820
     *                                   with _pushSegment
821
     */
822 View Code Duplication
    private function _popSegment($needPop)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
823
    {
824
        if ($needPop) {
825
            if (!empty($this->_segmentNames)) {
826
                array_pop($this->_segmentNames);
827
                array_pop($this->_segmentResourceSetWrappers);
828
            } else {
829
                throw new InvalidOperationException(
830
                    'Found non-balanced call to _pushSegment and popSegment'
831
                );
832
            }
833
        }
834
    }
835
836
    /**
837
     * Assert that the given condition is true.
838
     *
839
     * @param bool   $condition         Constion to assert
840
     * @param string $conditionAsString Message to show incase assertion fails
841
     *
842
     * @throws InvalidOperationException
843
     */
844
    protected function assert($condition, $conditionAsString)
845
    {
846
        if (!$condition) {
847
            throw new InvalidOperationException(
848
                "Unexpected state, expecting $conditionAsString"
849
            );
850
        }
851
    }
852
}
853