Completed
Pull Request — master (#45)
by Alex
04:10
created

UriProcessor::handleExpansion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace POData\UriProcessor;
4
5
use Illuminate\Support\Str;
6
use POData\Providers\ProvidersWrapper;
7
use POData\Providers\Metadata\ResourcePropertyKind;
8
use POData\Providers\Metadata\ResourceTypeKind;
9
use POData\Providers\Metadata\ResourceSetWrapper;
10
use POData\Providers\Metadata\ResourceProperty;
11
use POData\Providers\Query\QueryType;
12
use POData\UriProcessor\QueryProcessor\QueryProcessor;
13
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
14
use POData\UriProcessor\ResourcePathProcessor\ResourcePathProcessor;
15
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\SegmentDescriptor;
16
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetKind;
17
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\TargetSource;
18
use POData\IService;
19
use POData\Common\Messages;
20
use POData\Common\ODataException;
21
use POData\Common\InvalidOperationException;
22
use POData\Common\ODataConstants;
23
use POData\Providers\Query\QueryResult;
24
use POData\OperationContext\HTTPRequestMethod;
25
26
/**
27
 * Class UriProcessor.
28
 *
29
 * A type to process client's requets URI
30
 * The syntax of request URI is:
31
 *  Scheme Host Port ServiceRoot ResourcePath ? QueryOption
32
 * For more details refer:
33
 * http://www.odata.org/developers/protocols/uri-conventions#UriComponents
34
 */
35
class UriProcessor
36
{
37
    /**
38
     * Description of the OData request that a client has submitted.
39
     *
40
     * @var RequestDescription
41
     */
42
    private $request;
43
44
    /**
45
     * Holds reference to the data service instance.
46
     *
47
     * @var IService
48
     */
49
    private $service;
50
51
    /**
52
     * Holds reference to the wrapper over IDSMP and IDSQP implementation.
53
     *
54
     * @var ProvidersWrapper
55
     */
56
    private $providers;
57
58
    /**
59
     * Holds reference to request expander
60
     *
61
     * @var RequestExpander
62
     */
63
    private $expander;
64
65
    /**
66
     * Constructs a new instance of UriProcessor.
67
     *
68
     * @param IService $service Reference to the data service instance
69
     */
70
    private function __construct(IService $service)
71
    {
72
        $this->service = $service;
73
        $this->providers = $service->getProvidersWrapper();
74
    }
75
76
    /**
77
     * Process the resource path and query options of client's request uri.
78
     *
79
     * @param IService $service Reference to the data service instance
80
     *
81
     * @return URIProcessor
82
     *
83
     * @throws ODataException
84
     */
85
    public static function process(IService $service)
86
    {
87
        $absoluteRequestUri = $service->getHost()->getAbsoluteRequestUri();
88
        $absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri();
89
90
        if (!$absoluteServiceUri->isBaseOf($absoluteRequestUri)) {
91
            throw ODataException::createInternalServerError(
92
                Messages::uriProcessorRequestUriDoesNotHaveTheRightBaseUri(
93
                    $absoluteRequestUri->getUrlAsString(),
94
                    $absoluteServiceUri->getUrlAsString()
95
                )
96
            );
97
        }
98
99
        $uriProcessor = new self($service);
100
        //Parse the resource path part of the request Uri.
101
        $uriProcessor->request = ResourcePathProcessor::process($service);
102
        $uriProcessor->expander = new RequestExpander(
103
            $uriProcessor->getRequest(),
104
            $uriProcessor->getService(),
105
            $uriProcessor->getProviders()
106
        );
107
108
        $uriProcessor->getRequest()->setUriProcessor($uriProcessor);
109
110
        //Parse the query string options of the request Uri.
111
        QueryProcessor::process($uriProcessor->request, $service);
112
113
        return $uriProcessor;
114
    }
115
116
    /**
117
     * Gets reference to the request submitted by client.
118
     *
119
     * @return RequestDescription
120
     */
121
    public function getRequest()
122
    {
123
        return $this->request;
124
    }
125
126
    /**
127
     * Gets reference to the request submitted by client.
128
     *
129
     * @return ProvidersWrapper
130
     */
131
    public function getProviders()
132
    {
133
        return $this->providers;
134
    }
135
136
    /**
137
     * Gets the data service instance
138
     *
139
     * @return IService
140
     */
141
    public function getService()
142
    {
143
        return $this->service;
144
    }
145
146
    /**
147
     * Gets the request expander instance
148
     *
149
     * @return RequestExpander
150
     */
151
    public function getExpander()
152
    {
153
        return $this->expander;
154
    }
155
156
    /**
157
     * Execute the client submitted request against the data source.
158
     */
159
    public function execute()
160
    {
161
        $service = $this->getService();
162
        $operationContext = !isset($service) ? null : $service->getOperationContext();
163
        if (!$operationContext) {
164
            $this->executeBase();
165
166
            return;
167
        }
168
169
        $requestMethod = $operationContext->incomingRequest()->getMethod();
170
        if ($requestMethod == HTTPRequestMethod::GET()) {
171
            $this->executeGet();
172
        } elseif ($requestMethod == HTTPRequestMethod::POST()) {
173
            $this->executePost();
174
        } elseif ($requestMethod == HTTPRequestMethod::PUT()) {
175
            $this->executePut();
176
        } elseif ($requestMethod == HTTPRequestMethod::DELETE()) {
177
            $this->executeDelete();
178
            //TODO: we probably need these verbs eventually.
179
        /*} 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...
180
            $this->executePatch();
181
        } elseif ($requestMethod == HTTPRequestMethod::MERGE()) {
182
            $this->executeMerge();*/
183
        } else {
184
            throw ODataException::createNotImplementedError(Messages::onlyReadSupport($requestMethod));
185
        }
186
    }
187
188
    /**
189
     * Execute the client submitted request against the data source (GET).
190
     */
191
    protected function executeGet()
192
    {
193
        return $this->executeBase();
194
    }
195
196
    /**
197
     * Execute the client submitted request against the data source (POST).
198
     */
199
    protected function executePost()
200
    {
201
        $segments = $this->getRequest()->getSegments();
202
203
        foreach ($segments as $segment) {
204
            $requestTargetKind = $segment->getTargetKind();
205
            if ($requestTargetKind == TargetKind::RESOURCE()) {
206
                $requestMethod = $this->getService()->getOperationContext()->incomingRequest()->getMethod();
207
                $resourceSet = $segment->getTargetResourceSetWrapper();
208
                $keyDescriptor = $segment->getKeyDescriptor();
209
                if (!$resourceSet) {
210
                    $url = $this->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
211
                    throw ODataException::createBadRequestError(
212
                        Messages::badRequestInvalidUriForThisVerb($url, $requestMethod)
213
                    );
214
                }
215
                $data = $this->getRequest()->getData();
216
                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...
217
                    throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
218
                }
219
                $queryResult = $this->getProviders()->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...
220
                $segment->setResult($queryResult);
221
            }
222
        }
223
        //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...
224
    }
225
226
    /**
227
     * Execute the client submitted request against the data source (PUT).
228
     */
229
    protected function executePut()
230
    {
231
        return $this->executeBase(function($uriProcessor, $segment) {
232
            $requestMethod = $uriProcessor->getService()->getOperationContext()->incomingRequest()->getMethod();
233
            $resourceSet = $segment->getTargetResourceSetWrapper();
234
            $keyDescriptor = $segment->getKeyDescriptor();
235
            if (!$resourceSet || !$keyDescriptor) {
236
                $url = $uriProcessor->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
237
                throw ODataException::createBadRequestError(Messages::badRequestInvalidUriForThisVerb($url, $requestMethod));
238
            }
239
240
            $data = $uriProcessor->getRequest()->getData();
241
            if (!$data) {
242
                throw ODataException::createBadRequestError(Messages::noDataForThisVerb($requestMethod));
243
            }
244
245
            $queryResult = $uriProcessor->getProviders()->updateResource(
246
                $resourceSet,
247
                $segment->getResult(),
248
                $keyDescriptor,
249
                $data,
250
                false
251
            );
252
            $segment->setResult($queryResult);
253
            return $queryResult;
254
        });
255
    }
256
257
    /**
258
     * Execute the client submitted request against the data source (DELETE).
259
     */
260
    protected function executeDelete()
261
    {
262
        return $this->executeBase(function($uriProcessor, $segment) {
263
            $requestMethod = $uriProcessor->getService()->getOperationContext()->incomingRequest()->getMethod();
264
            $resourceSet = $segment->getTargetResourceSetWrapper();
265
            $keyDescriptor = $segment->getKeyDescriptor();
266
            if (!$resourceSet || !$keyDescriptor) {
267
                $url = $uriProcessor->getService()->getHost()->getAbsoluteRequestUri()->getUrlAsString();
268
                throw ODataException::createBadRequestError(
269
                    Messages::badRequestInvalidUriForThisVerb($url, $requestMethod)
270
                );
271
            }
272
            return $uriProcessor->getProviders()->deleteResource($resourceSet, $segment->getResult());
273
        });
274
    }
275
276
    /**
277
     * Execute the client submitted request against the data source.
278
     *
279
     * @param callable $callback Function, what must be called
280
     */
281
    protected function executeBase($callback = null)
282
    {
283
        $segments = $this->getRequest()->getSegments();
284
285
        foreach ($segments as $segment) {
286
            $requestTargetKind = $segment->getTargetKind();
287
288
            if ($segment->getTargetSource() == TargetSource::ENTITY_SET) {
289
                $this->handleSegmentTargetsToResourceSet($segment);
290
            } elseif ($requestTargetKind == TargetKind::RESOURCE()) {
291
                if (is_null($segment->getPrevious()->getResult())) {
292
                    throw ODataException::createResourceNotFoundError(
293
                        $segment->getPrevious()->getIdentifier()
294
                    );
295
                }
296
                $this->_handleSegmentTargetsToRelatedResource($segment);
297
            } elseif ($requestTargetKind == TargetKind::LINK()) {
298
                $segment->setResult($segment->getPrevious()->getResult());
299
            } elseif ($segment->getIdentifier() == ODataConstants::URI_COUNT_SEGMENT) {
300
                // we are done, $count will the last segment and
301
                // taken care by _applyQueryOptions method
302
                $segment->setResult($this->getRequest()->getCountValue());
303
                break;
304
            } else {
305
                if ($requestTargetKind == TargetKind::MEDIA_RESOURCE()) {
306
                    if (is_null($segment->getPrevious()->getResult())) {
307
                        throw ODataException::createResourceNotFoundError(
308
                            $segment->getPrevious()->getIdentifier()
309
                        );
310
                    }
311
                    // For MLE and Named Stream the result of last segment
312
                    // should be that of previous segment, this is required
313
                    // while retrieving content type or stream from IDSSP
314
                    $segment->setResult($segment->getPrevious()->getResult());
315
                    // we are done, as named stream property or $value on
316
                    // media resource will be the last segment
317
                    break;
318
                }
319
320
                $value = $segment->getPrevious()->getResult();
321
                while (!is_null($segment)) {
322
                    //TODO: what exactly is this doing here?  Once a null's found it seems everything will be null
323
                    if (!is_null($value)) {
324
                        $value = null;
325
                    } else {
326
                        // This is theoretically impossible to reach, but should that be changed, this will need to call
327
                        // ResourceType::getPropertyValue... somehow
328
                        try {
329
                            //see #88
330
                            $property = new \ReflectionProperty($value, $segment->getIdentifier());
331
                            $value = $property->getValue($value);
332
                        } catch (\ReflectionException $reflectionException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
333
334
                        }
335
                    }
336
337
                    $segment->setResult($value);
338
                    $segment = $segment->getNext();
339
                    if (!is_null($segment) && ODataConstants::URI_VALUE_SEGMENT == $segment->getIdentifier()) {
340
                        $segment->setResult($value);
341
                        $segment = $segment->getNext();
342
                    }
343
                }
344
345
                break;
346
            }
347
348
            if (is_null($segment->getNext())
349
                || ODataConstants::URI_COUNT_SEGMENT == $segment->getNext()->getIdentifier()
350
            ) {
351
                $this->applyQueryOptions($segment, $callback);
352
            }
353
        }
354
355
            // Apply $select and $expand options to result set, this function will be always applied
356
            // irrespective of return value of IDSQP2::canApplyQueryOptions which means library will
357
            // not delegate $expand/$select operation to IDSQP2 implementation
358
        $this->handleExpansion();
359
    }
360
361
    /**
362
     * Query for a resource set pointed by the given segment descriptor and update the descriptor with the result.
363
     *
364
     * @param SegmentDescriptor $segment Describes the resource set to query
365
     */
366
    private function handleSegmentTargetsToResourceSet(SegmentDescriptor $segment)
367
    {
368
        if ($segment->isSingleResult()) {
369
            $entityInstance = $this->getProviders()->getResourceFromResourceSet(
370
                $segment->getTargetResourceSetWrapper(),
371
                $segment->getKeyDescriptor()
372
            );
373
374
            $segment->setResult($entityInstance);
375
        } else {
376
            $skip = (null == $this->getRequest()) ? 0 : $this->getRequest()->getSkipCount();
377
            $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...
378
            $queryResult = $this->getProviders()->getResourceSet(
379
                $this->getRequest()->queryType,
380
                $segment->getTargetResourceSetWrapper(),
381
                $this->getRequest()->getFilterInfo(),
382
                $this->getRequest()->getInternalOrderByInfo(),
383
                $this->getRequest()->getTopCount(),
384
                $skip,
385
                null
386
            );
387
            $segment->setResult($queryResult);
388
        }
389
    }
390
391
    /**
392
     * Query for a related resource set or resource set reference pointed by the
393
     * given segment descriptor and update the descriptor with the result.
394
     *
395
     * @param SegmentDescriptor &$segment Describes the related resource
396
     *                                    to query
397
     */
398
    private function _handleSegmentTargetsToRelatedResource(SegmentDescriptor $segment)
399
    {
400
        $projectedProperty = $segment->getProjectedProperty();
401
        $projectedPropertyKind = $projectedProperty->getKind();
402
403
        if ($projectedPropertyKind == ResourcePropertyKind::RESOURCESET_REFERENCE) {
404
            if ($segment->isSingleResult()) {
405
                $entityInstance = $this->getProviders()->getResourceFromRelatedResourceSet(
406
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
407
                    $segment->getPrevious()->getResult(),
408
                    $segment->getTargetResourceSetWrapper(),
409
                    $projectedProperty,
410
                    $segment->getKeyDescriptor()
411
                );
412
413
                $segment->setResult($entityInstance);
414
            } else {
415
                $queryResult = $this->getProviders()->getRelatedResourceSet(
416
                    $this->getRequest()->queryType,
417
                    $segment->getPrevious()->getTargetResourceSetWrapper(),
418
                    $segment->getPrevious()->getResult(),
419
                    $segment->getTargetResourceSetWrapper(),
420
                    $segment->getProjectedProperty(),
421
                    $this->getRequest()->getFilterInfo(),
0 ignored issues
show
Bug introduced by
It seems like $this->getRequest()->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...
422
                    //TODO: why are these null?  see #98
423
                    null, // $orderby
424
                    null, // $top
425
                    null  // $skip
426
                );
427
428
                $segment->setResult($queryResult);
429
            }
430
        } elseif ($projectedPropertyKind == ResourcePropertyKind::RESOURCE_REFERENCE) {
431
432
            $entityInstance = $this->getProviders()->getRelatedResourceReference(
433
                $segment->getPrevious()->getTargetResourceSetWrapper(),
434
                $segment->getPrevious()->getResult(),
435
                $segment->getTargetResourceSetWrapper(),
436
                $segment->getProjectedProperty()
437
            );
438
439
            $segment->setResult($entityInstance);
440
        } 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...
441
            //Unexpected state
442
        }
443
    }
444
445
    /**
446
     * Applies the query options to the resource(s) retrieved from the data source.
447
     *
448
     * @param SegmentDescriptor $segment  The descriptor which holds resource(s) on which query options to be applied
449
     * @param callable          $callback Function, what must be called
450
     */
451
    private function applyQueryOptions(SegmentDescriptor $segment, $callback = null)
452
    {
453
        // For non-GET methods
454
        if ($callback) {
455
            $callback($this, $segment);
456
457
            return;
458
        }
459
460
        //TODO: I'm not really happy with this..i think i'd rather keep the result the QueryResult
461
        //not even bother with the setCountValue stuff (shouldn't counts be on segments?)
462
        //and just work with the QueryResult in the object model serializer
463
        $result = $segment->getResult();
464
465
        if (!$result instanceof QueryResult) {
466
            //If the segment isn't a query result, then there's no paging or counting to be done
467
            return;
468
        }
469
470
        // Note $inlinecount=allpages means include the total count regardless of paging..so we set the counts first
471
        // regardless if POData does the paging or not.
472 View Code Duplication
        if ($this->getRequest()->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...
473
            if ($this->getProviders()->handlesOrderedPaging()) {
474
                $this->getRequest()->setCountValue($result->count);
475
            } else {
476
                $this->getRequest()->setCountValue(count($result->results));
477
            }
478
        }
479
480
        //Have POData perform paging if necessary
481
        if (!$this->getProviders()->handlesOrderedPaging() && !empty($result->results)) {
482
            $result->results = $this->performPaging($result->results);
483
        }
484
485
        //a bit surprising, but $skip and $top affects $count so update it here, not above
486
        //IE  data.svc/Collection/$count?$top=10 returns 10 even if Collection has 11+ entries
487 View Code Duplication
        if ($this->getRequest()->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...
488
            if ($this->getProviders()->handlesOrderedPaging()) {
489
                $this->getRequest()->setCountValue($result->count);
490
            } else {
491
                $this->getRequest()->setCountValue(count($result->results));
492
            }
493
        }
494
495
        $segment->setResult($result->results);
496
    }
497
498
    /**
499
     * If the provider does not perform the paging (ordering, top, skip) then this method does it.
500
     *
501
     * @param array $result
502
     *
503
     * @return array
504
     */
505
    private function performPaging(array $result)
506
    {
507
        //Apply (implicit and explicit) $orderby option
508
        $internalOrderByInfo = $this->getRequest()->getInternalOrderByInfo();
509
        if (!is_null($internalOrderByInfo)) {
510
            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
511
            usort($result, $orderByFunction);
512
        }
513
514
        //Apply $skiptoken option
515
        $internalSkipTokenInfo = $this->getRequest()->getInternalSkipTokenInfo();
516
        if (!is_null($internalSkipTokenInfo)) {
517
            $matchingIndex = $internalSkipTokenInfo->getIndexOfFirstEntryInTheNextPage($result);
518
            $result = array_slice($result, $matchingIndex);
519
        }
520
521
        //Apply $top and $skip option
522
        if (!empty($result)) {
523
            $top = $this->getRequest()->getTopCount();
524
            $skip = $this->getRequest()->getSkipCount();
525
            if (is_null($skip)) {
526
                $skip = 0;
527
            }
528
529
            $result = array_slice($result, $skip, $top);
530
        }
531
532
        return $result;
533
    }
534
535
    /**
536
     * Perform expansion.
537
     */
538
    private function handleExpansion()
539
    {
540
        $this->getExpander()->handleExpansion();
541
    }
542
}
543