Completed
Pull Request — master (#39)
by Alex
04:58
created

UriProcessor::executePut()   B

Complexity

Conditions 4
Paths 1

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 19
nc 1
nop 0
1
<?php
2
3
namespace POData\UriProcessor;
4
5
use POData\Providers\ProvidersWrapper;
6
use POData\Providers\Metadata\ResourcePropertyKind;
7
use POData\Providers\Metadata\ResourceTypeKind;
8
use POData\Providers\Metadata\ResourceSetWrapper;
9
use POData\Providers\Metadata\ResourceProperty;
10
use POData\Providers\Query\QueryType;
11
use POData\UriProcessor\QueryProcessor\QueryProcessor;
12
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
13
use POData\UriProcessor\ResourcePathProcessor\ResourcePathProcessor;
14
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\SegmentDescriptor;
15
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
16
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetSource;
17
use POData\IService;
18
use POData\Common\Messages;
19
use POData\Common\ODataException;
20
use POData\Common\InvalidOperationException;
21
use POData\Common\ODataConstants;
22
use POData\Providers\Query\QueryResult;
23
use POData\OperationContext\HTTPRequestMethod;
24
25
/**
26
 * Class UriProcessor.
27
 *
28
 * A type to process client's requets URI
29
 * The syntax of request URI is:
30
 *  Scheme Host Port ServiceRoot ResourcePath ? QueryOption
31
 * For more details refer:
32
 * http://www.odata.org/developers/protocols/uri-conventions#UriComponents
33
 */
34
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(
230
                $resourceSet,
231
                $segment->getResult(),
232
                $keyDescriptor,
233
                $data,
234
                false
235
            );
236
            $segment->setResult($queryResult);
237
            return $queryResult;
238
        });
239
    }
240
241
    /**
242
     * Execute the client submitted request against the data source (DELETE).
243
     */
244
    protected function executeDelete()
245
    {
246
        return $this->executeBase(function($uriProcessor, $segment) {
247
            $requestMethod = $uriProcessor->getService()->getOperationContext()->incomingRequest()->getMethod();
248
            $resourceSet = $segment->getTargetResourceSetWrapper();
249
            $keyDescriptor = $segment->getKeyDescriptor();
250
            if (!$resourceSet || !$keyDescriptor) {
251
                $url = $uriProcessor->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
252
                throw ODataException::createBadRequestError(
253
                    Messages::badRequestInvalidUriForThisVerb($url, $requestMethod)
254
                );
255
            }
256
            return $uriProcessor->providers->deleteResource($resourceSet, $segment->getResult());
257
        });
258
    }
259
260
    /**
261
     * Execute the client submitted request against the data source.
262
     *
263
     * @param callable $callback Function, what must be called
264
     */
265
    protected function executeBase($callback = null)
266
    {
267
        $segments = $this->request->getSegments();
268
269
        foreach ($segments as $segment) {
270
            $requestTargetKind = $segment->getTargetKind();
271
272
            if ($segment->getTargetSource() == TargetSource::ENTITY_SET) {
273
                $this->handleSegmentTargetsToResourceSet($segment);
274
            } elseif ($requestTargetKind == TargetKind::RESOURCE()) {
275
                if (is_null($segment->getPrevious()->getResult())) {
276
                    throw ODataException::createResourceNotFoundError(
277
                        $segment->getPrevious()->getIdentifier()
278
                    );
279
                }
280
                $this->_handleSegmentTargetsToRelatedResource($segment);
281
            } elseif ($requestTargetKind == TargetKind::LINK()) {
282
                $segment->setResult($segment->getPrevious()->getResult());
283
            } elseif ($segment->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
284
                // we are done, $count will the last segment and
285
                // taken care by _applyQueryOptions method
286
                $segment->setResult($this->request->getCountValue());
287
                break;
288
            } else {
289
                if ($requestTargetKind == TargetKind::MEDIA_RESOURCE()) {
290
                    if (is_null($segment->getPrevious()->getResult())) {
291
                        throw ODataException::createResourceNotFoundError(
292
                            $segment->getPrevious()->getIdentifier()
293
                        );
294
                    }
295
                    // For MLE and Named Stream the result of last segment
296
                    // should be that of previous segment, this is required
297
                    // while retrieving content type or stream from IDSSP
298
                    $segment->setResult($segment->getPrevious()->getResult());
299
                    // we are done, as named stream property or $value on
300
                    // media resource will be the last segment
301
                    break;
302
                }
303
304
                $value = $segment->getPrevious()->getResult();
305
                while (!is_null($segment)) {
306
                    //TODO: what exactly is this doing here?  Once a null's found it seems everything will be null
307
                    if (!is_null($value)) {
308
                        $value = null;
309
                    } else {
310
                        // This is theoretically impossible to reach, but should that be changed, this will need to call
311
                        // ResourceType::getPropertyValue... somehow
312
                        try {
313
                            //see #88
314
                            $property = new \ReflectionProperty($value, $segment->getIdentifier());
315
                            $value = $property->getValue($value);
316
                        } catch (\ReflectionException $reflectionException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
317
318
                        }
319
                    }
320
321
                    $segment->setResult($value);
322
                    $segment = $segment->getNext();
323
                    if (!is_null($segment) && ODataConstants::URI_VALUE_SEGMENT == $segment->getIdentifier()) {
324
                        $segment->setResult($value);
325
                        $segment = $segment->getNext();
326
                    }
327
                }
328
329
                break;
330
            }
331
332
            if (is_null($segment->getNext())
333
                || ODataConstants::URI_COUNT_SEGMENT == $segment->getNext()->getIdentifier()
334
            ) {
335
                $this->applyQueryOptions($segment, $callback);
336
            }
337
        }
338
339
            // Apply $select and $expand options to result set, this function will be always applied
340
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
341
            // not delegate $expand/$select operation to IDSQP2 implementation
342
        $this->handleExpansion();
343
    }
344
345
    /**
346
     * Query for a resource set pointed by the given segment descriptor and update the descriptor with the result.
347
     *
348
     * @param SegmentDescriptor $segment Describes the resource set to query
349
     */
350
    private function handleSegmentTargetsToResourceSet(SegmentDescriptor $segment)
351
    {
352
        if ($segment->isSingleResult()) {
353
            $entityInstance = $this->providers->getResourceFromResourceSet(
354
                $segment->getTargetResourceSetWrapper(),
355
                $segment->getKeyDescriptor()
356
            );
357
358
            $segment->setResult($entityInstance);
359
        } else {
360
            $skip = (null == $this->request) ? 0 : $this->request->getSkipCount();
361
            $skip = (null == $skip) ? 0 :$skip;
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $skip of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
362
            $queryResult = $this->providers->getResourceSet(
363
                $this->request->queryType,
364
                $segment->getTargetResourceSetWrapper(),
365
                $this->request->getFilterInfo(),
366
                $this->request->getInternalOrderByInfo(),
367
                $this->request->getTopCount(),
368
                $skip,
369
                null
370
            );
371
            $segment->setResult($queryResult);
372
        }
373
    }
374
375
    /**
376
     * Query for a related resource set or resource set reference pointed by the
377
     * given segment descriptor and update the descriptor with the result.
378
     *
379
     * @param SegmentDescriptor &$segment Describes the related resource
380
     *                                    to query
381
     */
382
    private function _handleSegmentTargetsToRelatedResource(SegmentDescriptor $segment)
383
    {
384
        $projectedProperty = $segment->getProjectedProperty();
385
        $projectedPropertyKind = $projectedProperty->getKind();
386
387
        if ($projectedPropertyKind == ResourcePropertyKind::RESOURCESET_REFERENCE) {
388
            if ($segment->isSingleResult()) {
389
                $entityInstance = $this->providers->getResourceFromRelatedResourceSet(
390
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
391
                    $segment->getPrevious()->getResult(),
392
                    $segment->getTargetResourceSetWrapper(),
393
                    $projectedProperty,
394
                    $segment->getKeyDescriptor()
395
                );
396
397
                $segment->setResult($entityInstance);
398
            } else {
399
                $queryResult = $this->providers->getRelatedResourceSet(
400
                    $this->request->queryType,
401
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
402
                    $segment->getPrevious()->getResult(),
403
                    $segment->getTargetResourceSetWrapper(),
404
                    $segment->getProjectedProperty(),
405
                    $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...
406
                    //TODO: why are these null?  see #98
407
                    null, // $orderby
408
                    null, // $top
409
                    null  // $skip
410
                );
411
412
                $segment->setResult($queryResult);
413
            }
414
        } elseif ($projectedPropertyKind == ResourcePropertyKind::RESOURCE_REFERENCE) {
415
            $entityInstance = $this->providers->getRelatedResourceReference(
416
                $segment->getPrevious()->getTargetResourceSetWrapper(),
417
                $segment->getPrevious()->getResult(),
418
                $segment->getTargetResourceSetWrapper(),
419
                $segment->getProjectedProperty()
420
            );
421
422
            $segment->setResult($entityInstance);
423
        } 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...
424
            //Unexpected state
425
        }
426
    }
427
428
    /**
429
     * Applies the query options to the resource(s) retrieved from the data source.
430
     *
431
     * @param SegmentDescriptor $segment  The descriptor which holds resource(s) on which query options to be applied
432
     * @param callable          $callback Function, what must be called
433
     */
434
    private function applyQueryOptions(SegmentDescriptor $segment, $callback = null)
435
    {
436
        // For non-GET methods
437
        if ($callback) {
438
            $callback($this, $segment);
439
440
            return;
441
        }
442
443
        //TODO: I'm not really happy with this..i think i'd rather keep the result the QueryResult
444
        //not even bother with the setCountValue stuff (shouldn't counts be on segments?)
445
        //and just work with the QueryResult in the object model serializer
446
        $result = $segment->getResult();
447
448
        if (!$result instanceof QueryResult) {
449
            //If the segment isn't a query result, then there's no paging or counting to be done
450
            return;
451
        }
452
453
        // Note $inlinecount=allpages means include the total count regardless of paging..so we set the counts first
454
        // regardless if POData does the paging or not.
455 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...
456
            if ($this->providers->handlesOrderedPaging()) {
457
                $this->request->setCountValue($result->count);
458
            } else {
459
                $this->request->setCountValue(count($result->results));
460
            }
461
        }
462
463
        //Have POData perform paging if necessary
464
        if (!$this->providers->handlesOrderedPaging() && !empty($result->results)) {
465
            $result->results = $this->performPaging($result->results);
466
        }
467
468
        //a bit surprising, but $skip and $top affects $count so update it here, not above
469
        //IE  data.svc/Collection/$count?$top=10 returns 10 even if Collection has 11+ entries
470 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...
471
            if ($this->providers->handlesOrderedPaging()) {
472
                $this->request->setCountValue($result->count);
473
            } else {
474
                $this->request->setCountValue(count($result->results));
475
            }
476
        }
477
478
        $segment->setResult($result->results);
479
    }
480
481
    /**
482
     * If the provider does not perform the paging (ordering, top, skip) then this method does it.
483
     *
484
     * @param array $result
485
     *
486
     * @return array
487
     */
488
    private function performPaging(array $result)
489
    {
490
        //Apply (implicit and explicit) $orderby option
491
        $internalOrderByInfo = $this->request->getInternalOrderByInfo();
492
        if (!is_null($internalOrderByInfo)) {
493
            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
494
            usort($result, $orderByFunction);
495
        }
496
497
        //Apply $skiptoken option
498
        $internalSkipTokenInfo = $this->request->getInternalSkipTokenInfo();
499
        if (!is_null($internalSkipTokenInfo)) {
500
            $matchingIndex = $internalSkipTokenInfo->getIndexOfFirstEntryInTheNextPage($result);
501
            $result = array_slice($result, $matchingIndex);
502
        }
503
504
        //Apply $top and $skip option
505
        if (!empty($result)) {
506
            $top = $this->request->getTopCount();
507
            $skip = $this->request->getSkipCount();
508
            if (is_null($skip)) {
509
                $skip = 0;
510
            }
511
512
            $result = array_slice($result, $skip, $top);
513
        }
514
515
        return $result;
516
    }
517
518
    /**
519
     * Perform expansion.
520
     */
521
    private function handleExpansion()
522
    {
523
        $node = $this->request->getRootProjectionNode();
524
        if (!is_null($node) && $node->isExpansionSpecified()) {
525
            $result = $this->request->getTargetResult();
526
            if (!is_null($result) || is_array($result) && !empty($result)) {
527
                $needPop = $this->_pushSegmentForRoot();
528
                $this->_executeExpansion($result);
529
                $this->_popSegment($needPop);
530
            }
531
        }
532
    }
533
534
    /**
535
     * Execute queries for expansion.
536
     *
537
     * @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...
538
     */
539
    private function _executeExpansion($result)
540
    {
541
        $expandedProjectionNodes = $this->_getExpandedProjectionNodes();
542
        foreach ($expandedProjectionNodes as $expandedProjectionNode) {
543
            $resourceType = $expandedProjectionNode->getResourceType();
544
            $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $expandedProjectionNode->getResourceProperty()->getKind();
545
            $expandedPropertyName = $expandedProjectionNode->getResourceProperty()->getName();
546
            if (is_array($result)) {
547
                foreach ($result as $entry) {
548
                    // Check for null entry
549 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...
550
                        $currentResourceSet = $this->_getCurrentResourceSetWrapper()->getResourceSet();
551
                        $resourceSetOfProjectedProperty = $expandedProjectionNode
552
                            ->getResourceSetWrapper()
553
                            ->getResourceSet();
554
                        $projectedProperty1 = $expandedProjectionNode->getResourceProperty();
555
                        $result1 = $this->providers->getRelatedResourceSet(
556
                            QueryType::ENTITIES(), //it's always entities for an expansion
557
                            $currentResourceSet,
558
                            $entry,
559
                            $resourceSetOfProjectedProperty,
560
                            $projectedProperty1,
561
                            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...
562
                            null, // $orderby
563
                            null, // $top
564
                            null  // $skip
565
                        )->results;
566
                        if (!empty($result1)) {
567
                            $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
568
                            if (!is_null($internalOrderByInfo)) {
569
                                $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
570
                                usort($result1, $orderByFunction);
571
                                unset($internalOrderByInfo);
572
                                $takeCount = $expandedProjectionNode->getTakeCount();
573
                                if (!is_null($takeCount)) {
574
                                    $result1 = array_slice($result1, 0, $takeCount);
575
                                }
576
                            }
577
578
                            $resourceType->setPropertyValue($entry, $expandedPropertyName, $result1);
579
                            $projectedProperty = $expandedProjectionNode->getResourceProperty();
580
                            $needPop = $this->_pushSegmentForNavigationProperty(
581
                                $projectedProperty
582
                            );
583
                            $this->_executeExpansion($result1);
584
                            $this->_popSegment($needPop);
585
                        } else {
586
                            $resourceType->setPropertyValue($entry, $expandedPropertyName, array());
587
                        }
588
                    } else {
589
                        $currentResourceSet1 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
590
                        $resourceSetOfProjectedProperty1 = $expandedProjectionNode
591
                            ->getResourceSetWrapper()
592
                            ->getResourceSet();
593
                        $projectedProperty2 = $expandedProjectionNode->getResourceProperty();
594
                        $result1 = $this->providers->getRelatedResourceReference(
595
                            $currentResourceSet1,
596
                            $entry,
597
                            $resourceSetOfProjectedProperty1,
598
                            $projectedProperty2
599
                        );
600
                        $resourceType->setPropertyValue($entry, $expandedPropertyName, $result1);
601
                        if (!is_null($result1)) {
602
                            $projectedProperty3 = $expandedProjectionNode->getResourceProperty();
603
                            $needPop = $this->_pushSegmentForNavigationProperty(
604
                                $projectedProperty3
605
                            );
606
                            $this->_executeExpansion($result1);
607
                            $this->_popSegment($needPop);
608
                        }
609
                    }
610
                }
611 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...
612
                if ($isCollection) {
613
                    $currentResourceSet2 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
614
                    $resourceSetOfProjectedProperty2 = $expandedProjectionNode
615
                        ->getResourceSetWrapper()
616
                        ->getResourceSet();
617
                    $projectedProperty4 = $expandedProjectionNode->getResourceProperty();
618
                    $result1 = $this->providers->getRelatedResourceSet(
619
                        QueryType::ENTITIES(), //it's always entities for an expansion
620
                        $currentResourceSet2,
621
                        $result,
622
                        $resourceSetOfProjectedProperty2,
623
                        $projectedProperty4,
624
                        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...
625
                        null, // $orderby
626
                        null, // $top
627
                        null  // $skip
628
                    )->results;
629
                    if (!empty($result1)) {
630
                        $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
631
                        if (!is_null($internalOrderByInfo)) {
632
                            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
633
                            usort($result1, $orderByFunction);
634
                            unset($internalOrderByInfo);
635
                            $takeCount = $expandedProjectionNode->getTakeCount();
636
                            if (!is_null($takeCount)) {
637
                                $result1 = array_slice($result1, 0, $takeCount);
638
                            }
639
                        }
640
                        $resourceType->setPropertyValue($result, $expandedPropertyName, $result1);
641
                        $projectedProperty7 = $expandedProjectionNode->getResourceProperty();
642
                        $needPop = $this->_pushSegmentForNavigationProperty(
643
                            $projectedProperty7
644
                        );
645
                        $this->_executeExpansion($result1);
646
                        $this->_popSegment($needPop);
647
                    } else {
648
                        $resourceType->setPropertyValue($result, $expandedPropertyName, $result1);
649
                    }
650
                } else {
651
                    $currentResourceSet3 = $this->_getCurrentResourceSetWrapper()->getResourceSet();
652
                    $resourceSetOfProjectedProperty3 = $expandedProjectionNode
653
                        ->getResourceSetWrapper()
654
                        ->getResourceSet();
655
                    $projectedProperty5 = $expandedProjectionNode->getResourceProperty();
656
                    $result1 = $this->providers->getRelatedResourceReference(
657
                        $currentResourceSet3,
658
                        $result,
659
                        $resourceSetOfProjectedProperty3,
660
                        $projectedProperty5
661
                    );
662
                    $resourceType->setPropertyValue($result, $expandedPropertyName, $result1);
663
                    if (!is_null($result1)) {
664
                        $projectedProperty6 = $expandedProjectionNode->getResourceProperty();
665
                        $needPop = $this->_pushSegmentForNavigationProperty(
666
                            $projectedProperty6
667
                        );
668
                        $this->_executeExpansion($result1);
669
                        $this->_popSegment($needPop);
670
                    }
671
                }
672
            }
673
        }
674
    }
675
676
    /**
677
     * Resource set wrapper for the resource being retireved.
678
     *
679
     * @return ResourceSetWrapper
680
     */
681 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...
682
    {
683
        $count = count($this->_segmentResourceSetWrappers);
684
        if ($count == 0) {
685
            return $this->request->getTargetResourceSetWrapper();
686
        } else {
687
            return $this->_segmentResourceSetWrappers[$count - 1];
688
        }
689
    }
690
691
    /**
692
     * Pushes a segment for the root of the tree
693
     * Note: Calls to this method should be balanced with calls to popSegment.
694
     *
695
     * @return bool true if the segment was pushed, false otherwise
696
     */
697
    private function _pushSegmentForRoot()
698
    {
699
        $segmentName = $this->request->getContainerName();
700
        $segmentResourceSetWrapper
701
            = $this->request->getTargetResourceSetWrapper();
702
703
        return $this->_pushSegment($segmentName, $segmentResourceSetWrapper);
0 ignored issues
show
Bug introduced by
It seems like $segmentResourceSetWrapper defined by $this->request->getTargetResourceSetWrapper() on line 701 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...
704
    }
705
706
    /**
707
     * Pushes a segment for the current navigation property being written out.
708
     * Note: Refer 'ObjectModelSerializerNotes.txt' for more details about
709
     * 'Segment Stack' and this method.
710
     * Note: Calls to this method should be balanced with calls to popSegment.
711
     *
712
     * @param ResourceProperty &$resourceProperty Current navigation property
713
     *                                            being written out
714
     *
715
     * @return bool true if a segment was pushed, false otherwise
716
     *
717
     * @throws InvalidOperationException If this function invoked with non-navigation
718
     *                                   property instance
719
     */
720 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...
721
    {
722
        if ($resourceProperty->getTypeKind() == ResourceTypeKind::ENTITY) {
723
            $this->assert(
724
                !empty($this->_segmentNames),
725
                '!is_empty($this->_segmentNames'
726
            );
727
            $currentResourceSetWrapper = $this->_getCurrentResourceSetWrapper();
728
            $currentResourceType = $currentResourceSetWrapper->getResourceType();
729
            $currentResourceSetWrapper = $this->getService()
730
                ->getProvidersWrapper()
731
                ->getResourceSetWrapperForNavigationProperty(
732
                    $currentResourceSetWrapper,
0 ignored issues
show
Bug introduced by
It seems like $currentResourceSetWrapper defined by $this->getService()->get...ype, $resourceProperty) on line 729 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...
733
                    $currentResourceType,
734
                    $resourceProperty
735
                );
736
737
            $this->assert(
738
                !is_null($currentResourceSetWrapper),
739
                '!null($currentResourceSetWrapper)'
740
            );
741
742
            return $this->_pushSegment(
743
                $resourceProperty->getName(),
744
                $currentResourceSetWrapper
0 ignored issues
show
Bug introduced by
It seems like $currentResourceSetWrapper defined by $this->getService()->get...ype, $resourceProperty) on line 729 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...
745
            );
746
        } else {
747
            throw new InvalidOperationException(
748
                'pushSegmentForNavigationProperty should not be called with non-entity type'
749
            );
750
        }
751
    }
752
753
    /**
754
     * Gets collection of expanded projection nodes under the current node.
755
     *
756
     * @return ExpandedProjectionNode[] List of nodes describing expansions for the current segment
757
     */
758
    private function _getExpandedProjectionNodes()
759
    {
760
        $expandedProjectionNode = $this->_getCurrentExpandedProjectionNode();
761
        $expandedProjectionNodes = array();
762
        if (!is_null($expandedProjectionNode)) {
763
            foreach ($expandedProjectionNode->getChildNodes() as $node) {
764
                if ($node instanceof ExpandedProjectionNode) {
765
                    $expandedProjectionNodes[] = $node;
766
                }
767
            }
768
        }
769
770
        return $expandedProjectionNodes;
771
    }
772
773
    /**
774
     * Find a 'ExpandedProjectionNode' instance in the projection tree
775
     * which describes the current segment.
776
     *
777
     * @return ExpandedProjectionNode|null
778
     */
779 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...
780
    {
781
        $expandedProjectionNode
782
            = $this->request->getRootProjectionNode();
783
        if (!is_null($expandedProjectionNode)) {
784
            $depth = count($this->_segmentNames);
785
            if ($depth != 0) {
786
                for ($i = 1; $i < $depth; ++$i) {
787
                    $expandedProjectionNode
788
                        = $expandedProjectionNode->findNode($this->_segmentNames[$i]);
789
                    $this->assert(
790
                        !is_null($expandedProjectionNode),
791
                        '!is_null($expandedProjectionNode)'
792
                    );
793
                    $this->assert(
794
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
795
                        '$expandedProjectionNode instanceof ExpandedProjectionNode'
796
                    );
797
                }
798
            }
799
        }
800
801
        return $expandedProjectionNode;
802
    }
803
804
    /**
805
     * Pushes information about the segment whose instance is going to be
806
     * retrieved from the IDSQP implementation
807
     * Note: Calls to this method should be balanced with calls to popSegment.
808
     *
809
     * @param string             $segmentName         Name of segment to push
810
     * @param ResourceSetWrapper &$resourceSetWrapper The resource set wrapper
811
     *                                                to push
812
     *
813
     * @return bool true if the segment was push, false otherwise
814
     */
815 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...
816
    {
817
        $rootProjectionNode = $this->request->getRootProjectionNode();
818
        if (!is_null($rootProjectionNode)
819
            && $rootProjectionNode->isExpansionSpecified()
820
        ) {
821
            array_push($this->_segmentNames, $segmentName);
822
            array_push($this->_segmentResourceSetWrappers, $resourceSetWrapper);
823
824
            return true;
825
        }
826
827
        return false;
828
    }
829
830
    /**
831
     * Pops segment information from the 'Segment Stack'
832
     * Note: Calls to this method should be balanced with previous calls
833
     * to _pushSegment.
834
     *
835
     * @param bool $needPop Is a pop required. Only true if last push
836
     *                      was successful
837
     *
838
     * @throws InvalidOperationException If found un-balanced call
839
     *                                   with _pushSegment
840
     */
841 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...
842
    {
843
        if ($needPop) {
844
            if (!empty($this->_segmentNames)) {
845
                array_pop($this->_segmentNames);
846
                array_pop($this->_segmentResourceSetWrappers);
847
            } else {
848
                throw new InvalidOperationException(
849
                    'Found non-balanced call to _pushSegment and popSegment'
850
                );
851
            }
852
        }
853
    }
854
855
    /**
856
     * Assert that the given condition is true.
857
     *
858
     * @param bool   $condition         Condition to assert
859
     * @param string $conditionAsString Message to show if assertion fails
860
     *
861
     * @throws InvalidOperationException
862
     */
863
    protected function assert($condition, $conditionAsString)
864
    {
865
        if (!$condition) {
866
            throw new InvalidOperationException(
867
                "Unexpected state, expecting $conditionAsString"
868
            );
869
        }
870
    }
871
}
872