Test Failed
Pull Request — master (#81)
by Alex
03:16
created

IronicSerialiser::getNextLinkUri()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
rs 9.4285
cc 2
eloc 13
nc 2
nop 2
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Serialisers;
4
5
use POData\Common\Messages;
6
use POData\Common\ODataConstants;
7
use POData\Common\ODataException;
8
use POData\IService;
9
use POData\ObjectModel\IObjectSerialiser;
10
use POData\ObjectModel\ODataBagContent;
11
use POData\ObjectModel\ODataEntry;
12
use POData\ObjectModel\ODataFeed;
13
use POData\ObjectModel\ODataLink;
14
use POData\ObjectModel\ODataMediaLink;
15
use POData\ObjectModel\ODataNavigationPropertyInfo;
16
use POData\ObjectModel\ODataProperty;
17
use POData\ObjectModel\ODataPropertyContent;
18
use POData\ObjectModel\ODataURL;
19
use POData\ObjectModel\ODataURLCollection;
20
use POData\Providers\Metadata\ResourceEntityType;
21
use POData\Providers\Metadata\ResourceProperty;
22
use POData\Providers\Metadata\ResourcePropertyKind;
23
use POData\Providers\Metadata\ResourceSet;
24
use POData\Providers\Metadata\ResourceSetWrapper;
25
use POData\Providers\Metadata\ResourceType;
26
use POData\Providers\Metadata\ResourceTypeKind;
27
use POData\Providers\Metadata\Type\Binary;
28
use POData\Providers\Metadata\Type\Boolean;
29
use POData\Providers\Metadata\Type\DateTime;
30
use POData\Providers\Metadata\Type\IType;
31
use POData\Providers\Metadata\Type\StringType;
32
use POData\Providers\Query\QueryResult;
33
use POData\Providers\Query\QueryType;
34
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
35
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ProjectionNode;
36
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
37
use POData\UriProcessor\RequestDescription;
38
use POData\UriProcessor\SegmentStack;
39
40
class IronicSerialiser implements IObjectSerialiser
41
{
42
    /**
43
     * The service implementation.
44
     *
45
     * @var IService
46
     */
47
    protected $service;
48
49
    /**
50
     * Request description instance describes OData request the
51
     * the client has submitted and result of the request.
52
     *
53
     * @var RequestDescription
54
     */
55
    protected $request;
56
57
    /**
58
     * Collection of complex type instances used for cycle detection.
59
     *
60
     * @var array
61
     */
62
    protected $complexTypeInstanceCollection;
63
64
    /**
65
     * Absolute service Uri.
66
     *
67
     * @var string
68
     */
69
    protected $absoluteServiceUri;
70
71
    /**
72
     * Absolute service Uri with slash.
73
     *
74
     * @var string
75
     */
76
    protected $absoluteServiceUriWithSlash;
77
78
    /**
79
     * Holds reference to segment stack being processed.
80
     *
81
     * @var SegmentStack
82
     */
83
    protected $stack;
84
85
    /**
86
     * Lightweight stack tracking for recursive descent fill
87
     */
88
    private $lightStack = [];
89
90
    private $modelSerialiser;
91
92
    /**
93
     * @param IService           $service Reference to the data service instance
94
     * @param RequestDescription $request Type instance describing the client submitted request
95
     */
96
    public function __construct(IService $service, RequestDescription $request = null)
97
    {
98
        $this->service = $service;
99
        $this->request = $request;
100
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
101
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
102
        $this->stack = new SegmentStack($request);
103
        $this->complexTypeInstanceCollection = [];
104
        $this->modelSerialiser = new ModelSerialiser();
105
    }
106
107
    /**
108
     * Write a top level entry resource.
109
     *
110
     * @param mixed $entryObject Reference to the entry object to be written
111
     *
112
     * @return ODataEntry
113
     */
114
    public function writeTopLevelElement(QueryResult $entryObject)
115
    {
116
        if (!isset($entryObject->results)) {
117
            array_pop($this->lightStack);
118
            return null;
119
        }
120
121
        $this->loadStackIfEmpty();
122
123
        $stackCount = count($this->lightStack);
124
        $topOfStack = $this->lightStack[$stackCount-1];
125
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack[0]);
126
        $rawProp = $resourceType->getAllProperties();
127
        $relProp = [];
128
        $nonRelProp = [];
129
        foreach ($rawProp as $prop) {
130
            if ($prop->getResourceType() instanceof ResourceEntityType) {
131
                $relProp[] = $prop;
132
            } else {
133
                $nonRelProp[$prop->getName()] = $prop;
134
            }
135
        }
136
137
        $resourceSet = $resourceType->getCustomState();
138
        assert($resourceSet instanceof ResourceSet);
139
        $title = $resourceType->getName();
140
        $type = $resourceType->getFullName();
141
142
        $relativeUri = $this->getEntryInstanceKey(
143
            $entryObject->results,
144
            $resourceType,
145
            $resourceSet->getName()
146
        );
147
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
148
149
        list($mediaLink, $mediaLinks) = $this->writeMediaData($entryObject->results, $type, $relativeUri, $resourceType);
150
151
        $propertyContent = $this->writePrimitiveProperties($entryObject->results, $nonRelProp);
152
153
        $links = [];
154
        foreach ($relProp as $prop) {
155
            $nuLink = new ODataLink();
156
            $propKind = $prop->getKind();
157
158
            assert(
159
                ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind
160
                || ResourcePropertyKind::RESOURCE_REFERENCE == $propKind,
161
                '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
162
                .' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE'
163
            );
164
            $propTail = ResourcePropertyKind::RESOURCE_REFERENCE == $propKind ? 'entry' : 'feed';
165
            $propType = 'application/atom+xml;type='.$propTail;
166
            $propName = $prop->getName();
167
            $nuLink->title = $propName;
168
            $nuLink->name = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
169
            $nuLink->url = $relativeUri . '/' . $propName;
170
            $nuLink->type = $propType;
171
172
            $navProp = new ODataNavigationPropertyInfo($prop, $this->shouldExpandSegment($propName));
173
            if ($navProp->expanded) {
174
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
175
            }
176
177
            $links[] = $nuLink;
178
        }
179
180
        $odata = new ODataEntry();
181
        $odata->resourceSetName = $resourceSet->getName();
182
        $odata->id = $absoluteUri;
183
        $odata->title = $title;
184
        $odata->type = $type;
185
        $odata->propertyContent = $propertyContent;
186
        $odata->isMediaLinkEntry = $resourceType->isMediaLinkEntry();
187
        $odata->editLink = $relativeUri;
188
        $odata->mediaLink = $mediaLink;
189
        $odata->mediaLinks = $mediaLinks;
190
        $odata->links = $links;
191
192
        $newCount = count($this->lightStack);
193
        assert($newCount == $stackCount, "Should have $stackCount elements in stack, have $newCount elements");
194
        array_pop($this->lightStack);
195
        return $odata;
196
    }
197
198
    /**
199
     * Write top level feed element.
200
     *
201
     * @param array &$entryObjects Array of entry resources to be written
202
     *
203
     * @return ODataFeed
204
     */
205
    public function writeTopLevelElements(QueryResult &$entryObjects)
206
    {
207
        assert(is_array($entryObjects->results), '!is_array($entryObjects->results)');
208
209
        $this->loadStackIfEmpty();
210
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
211
212
        $title = $this->getRequest()->getContainerName();
213
        $relativeUri = $this->getRequest()->getIdentifier();
214
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
215
216
        $selfLink = new ODataLink();
217
        $selfLink->name = 'self';
218
        $selfLink->title = $relativeUri;
219
        $selfLink->url = $relativeUri;
220
221
        $odata = new ODataFeed();
222
        $odata->title = $title;
223
        $odata->id = $absoluteUri;
224
        $odata->selfLink = $selfLink;
225
226
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
227
            $odata->rowCount = $this->getRequest()->getCountValue();
228
        }
229
        foreach ($entryObjects->results as $entry) {
230
            if (!$entry instanceof QueryResult) {
231
                $query = new QueryResult();
232
                $query->results = $entry;
233
            } else {
234
                $query = $entry;
235
            }
236
            $odata->entries[] = $this->writeTopLevelElement($query);
237
        }
238
239
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
240
        $requestTop = $this->getRequest()->getTopOptionCount();
241
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
242
        $requestTop = (null == $requestTop) ? $pageSize + 1 : $requestTop;
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $requestTop of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison === instead.
Loading history...
243
244
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
0 ignored issues
show
Bug introduced by
The property hasMore does not seem to exist in POData\Providers\Query\QueryResult.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
245
            $stackSegment = $setName;
246
            $lastObject = end($entryObjects->results);
247
            $segment = $this->getNextLinkUri($lastObject, $absoluteUri);
248
            $nextLink = new ODataLink();
249
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
250
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
251
            $odata->nextPageLink = $nextLink;
252
        }
253
254
        return $odata;
255
    }
256
257
    /**
258
     * Write top level url element.
259
     *
260
     * @param mixed $entryObject The entry resource whose url to be written
261
     *
262
     * @return ODataURL
263
     */
264
    public function writeUrlElement(QueryResult $entryObject)
265
    {
266
        $url = new ODataURL();
267
        if (!is_null($entryObject->results)) {
268
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
269
            $relativeUri = $this->getEntryInstanceKey(
270
                $entryObject->results,
271
                $currentResourceType,
272
                $this->getCurrentResourceSetWrapper()->getName()
273
            );
274
275
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
276
        }
277
278
        return $url;
279
    }
280
281
    /**
282
     * Write top level url collection.
283
     *
284
     * @param array $entryObjects Array of entry resources
285
     *                            whose url to be written
286
     *
287
     * @return ODataURLCollection
288
     */
289
    public function writeUrlElements(QueryResult $entryObjects)
290
    {
291
        $urls = new ODataURLCollection();
292
        if (!empty($entryObjects->results)) {
293
            $i = 0;
294
            foreach ($entryObjects->results as $entryObject) {
295
                $urls->urls[$i] = $this->writeUrlElement($entryObject);
296
                ++$i;
297
            }
298
299
            if ($i > 0 && true === $entryObjects->hasMore) {
0 ignored issues
show
Bug introduced by
The property hasMore does not seem to exist in POData\Providers\Query\QueryResult.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
300
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
301
                $lastObject = end($entryObjects->results);
302
                $segment = $this->getNextLinkUri($lastObject, $this->getRequest()->getRequestUrl()->getUrlAsString());
303
                $nextLink = new ODataLink();
304
                $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
305
                $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
306
                $urls->nextPageLink = $nextLink;
307
            }
308
        }
309
310
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
311
            $urls->count = $this->getRequest()->getCountValue();
312
        }
313
314
        return $urls;
315
    }
316
317
    /**
318
     * Write top level complex resource.
319
     *
320
     * @param mixed &$complexValue The complex object to be
321
     *                                    written
322
     * @param string $propertyName The name of the
323
     *                                    complex property
324
     * @param ResourceType &$resourceType Describes the type of
325
     *                                    complex object
326
     *
327
     * @return ODataPropertyContent
328
     */
329
    public function writeTopLevelComplexObject(QueryResult &$complexValue, $propertyName, ResourceType &$resourceType)
330
    {
331
        $result = $complexValue->results;
332
333
        $propertyContent = new ODataPropertyContent();
334
        $odataProperty = new ODataProperty();
335
        $odataProperty->name = $propertyName;
336
        $odataProperty->typeName = $resourceType->getFullName();
337
        if (null != $result) {
338
            $internalContent = $this->writeComplexValue($resourceType, $result);
0 ignored issues
show
Documentation introduced by
$result is of type array<integer,object>, 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...
339
            $odataProperty->value = $internalContent;
340
        }
341
342
        $propertyContent->properties[] = $odataProperty;
343
344
        return $propertyContent;
345
    }
346
347
    /**
348
     * Write top level bag resource.
349
     *
350
     * @param mixed &$BagValue The bag object to be
351
     *                                    written
352
     * @param string $propertyName The name of the
353
     *                                    bag property
354
     * @param ResourceType &$resourceType Describes the type of
355
     *                                    bag object
356
     * @return ODataPropertyContent
357
     */
358
    public function writeTopLevelBagObject(QueryResult &$BagValue, $propertyName, ResourceType &$resourceType)
359
    {
360
        $result = $BagValue->results;
361
362
        $propertyContent = new ODataPropertyContent();
363
        $odataProperty = new ODataProperty();
364
        $odataProperty->name = $propertyName;
365
        $odataProperty->typeName = 'Collection('.$resourceType->getFullName().')';
366
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
367
368
        $propertyContent->properties[] = $odataProperty;
369
        return $propertyContent;
370
    }
371
372
    /**
373
     * Write top level primitive value.
374
     *
375
     * @param mixed &$primitiveValue              The primitive value to be
376
     *                                            written
377
     * @param ResourceProperty &$resourceProperty Resource property describing the
378
     *                                            primitive property to be written
379
     * @return ODataPropertyContent
380
     */
381
    public function writeTopLevelPrimitive(QueryResult &$primitiveValue, ResourceProperty &$resourceProperty = null)
382
    {
383
        assert(null != $resourceProperty, "Resource property must not be null");
384
        $propertyContent = new ODataPropertyContent();
385
386
        $odataProperty = new ODataProperty();
387
        $odataProperty->name = $resourceProperty->getName();
388
        $odataProperty->typeName = $resourceProperty->getInstanceType()->getFullTypeName();
0 ignored issues
show
Bug introduced by
The method getFullTypeName does only exist in POData\Providers\Metadata\Type\IType, but not in ReflectionClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
389
        if (null == $primitiveValue->results) {
390
            $odataProperty->value = null;
391
        } else {
392
            $rType = $resourceProperty->getResourceType()->getInstanceType();
393
            $odataProperty->value = $this->primitiveToString($rType, $primitiveValue->results);
0 ignored issues
show
Bug introduced by
It seems like $rType defined by $resourceProperty->getRe...pe()->getInstanceType() on line 392 can also be of type object<ReflectionClass> or string; however, AlgoWeb\PODataLaravel\Se...er::primitiveToString() does only seem to accept object<POData\Providers\Metadata\Type\IType>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
394
        }
395
396
        $propertyContent->properties[] = $odataProperty;
397
398
        return $propertyContent;
399
    }
400
401
    /**
402
     * Gets reference to the request submitted by client.
403
     *
404
     * @return RequestDescription
405
     */
406
    public function getRequest()
407
    {
408
        assert(null != $this->request, 'Request not yet set');
409
410
        return $this->request;
411
    }
412
413
    /**
414
     * Sets reference to the request submitted by client.
415
     *
416
     * @param RequestDescription $request
417
     */
418
    public function setRequest(RequestDescription $request)
419
    {
420
        $this->request = $request;
421
        $this->stack->setRequest($request);
422
    }
423
424
    /**
425
     * Gets the data service instance.
426
     *
427
     * @return IService
428
     */
429
    public function getService()
430
    {
431
        return $this->service;
432
    }
433
434
    /**
435
     * Gets the segment stack instance.
436
     *
437
     * @return SegmentStack
438
     */
439
    public function getStack()
440
    {
441
        return $this->stack;
442
    }
443
444
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
445
    {
446
        $typeName = $resourceType->getName();
447
        $keyProperties = $resourceType->getKeyProperties();
448
        assert(count($keyProperties) != 0, 'count($keyProperties) == 0');
449
        $keyString = $containerName . '(';
450
        $comma = null;
451
        foreach ($keyProperties as $keyName => $resourceProperty) {
452
            $keyType = $resourceProperty->getInstanceType();
453
            assert($keyType instanceof IType, '$keyType not instanceof IType');
454
            $keyName = $resourceProperty->getName();
455
            $keyValue = $entityInstance->$keyName;
456
            if (!isset($keyValue)) {
457
                throw ODataException::createInternalServerError(
458
                    Messages::badQueryNullKeysAreNotSupported($typeName, $keyName)
459
                );
460
            }
461
462
            $keyValue = $keyType->convertToOData($keyValue);
463
            $keyString .= $comma . $keyName . '=' . $keyValue;
464
            $comma = ',';
465
        }
466
467
        $keyString .= ')';
468
469
        return $keyString;
470
    }
471
472
    /**
473
     * @param $entryObject
474
     * @param $type
475
     * @param $relativeUri
476
     * @param $resourceType
477
     * @return array
478
     */
479
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
480
    {
481
        $context = $this->getService()->getOperationContext();
482
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
483
        assert(null != $streamProviderWrapper, "Retrieved stream provider must not be null");
484
485
        $mediaLink = null;
486
        if ($resourceType->isMediaLinkEntry()) {
487
            $eTag = $streamProviderWrapper->getStreamETag2($entryObject, null, $context);
0 ignored issues
show
Bug introduced by
The method getStreamETag2() does not exist on POData\Providers\Stream\StreamProviderWrapper. Did you maybe mean getStreamETag()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
488
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag);
489
        }
490
        $mediaLinks = [];
491
        if ($resourceType->hasNamedStream()) {
492
            $namedStreams = $resourceType->getAllNamedStreams();
493
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
494
                $readUri = $streamProviderWrapper->getReadStreamUri2(
0 ignored issues
show
Bug introduced by
The method getReadStreamUri2() does not exist on POData\Providers\Stream\StreamProviderWrapper. Did you maybe mean getReadStream()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
495
                    $entryObject,
496
                    $resourceStreamInfo,
497
                    $context,
498
                    $relativeUri
499
                );
500
                $mediaContentType = $streamProviderWrapper->getStreamContentType2(
0 ignored issues
show
Bug introduced by
The method getStreamContentType2() does not exist on POData\Providers\Stream\StreamProviderWrapper. Did you maybe mean getStreamContentType()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
501
                    $entryObject,
502
                    $resourceStreamInfo,
503
                    $context
504
                );
505
                $eTag = $streamProviderWrapper->getStreamETag2(
0 ignored issues
show
Bug introduced by
The method getStreamETag2() does not exist on POData\Providers\Stream\StreamProviderWrapper. Did you maybe mean getStreamETag()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
506
                    $entryObject,
507
                    $resourceStreamInfo,
508
                    $context
509
                );
510
511
                $nuLink = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
512
                $mediaLinks[] = $nuLink;
513
            }
514
        }
515
        return [$mediaLink, $mediaLinks];
516
    }
517
518
    /**
519
     * Gets collection of projection nodes under the current node.
520
     *
521
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes
522
     *                                                        describing projections for the current segment, If this method returns
523
     *                                                        null it means no projections are to be applied and the entire resource
524
     *                                                        for the current segment should be serialized, If it returns non-null
525
     *                                                        only the properties described by the returned projection segments should
526
     *                                                        be serialized
527
     */
528
    protected function getProjectionNodes()
529
    {
530
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
531
        if (is_null($expandedProjectionNode) || $expandedProjectionNode->canSelectAllProperties()) {
532
            return null;
533
        }
534
535
        return $expandedProjectionNode->getChildNodes();
536
    }
537
538
    /**
539
     * Find a 'ExpandedProjectionNode' instance in the projection tree
540
     * which describes the current segment.
541
     *
542
     * @return ExpandedProjectionNode|null
543
     */
544
    protected function getCurrentExpandedProjectionNode()
545
    {
546
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
547
        if (is_null($expandedProjectionNode)) {
548
            return null;
549
        } else {
550
            $segmentNames = $this->getStack()->getSegmentNames();
551
            $depth = count($segmentNames);
552
            // $depth == 1 means serialization of root entry
553
            //(the resource identified by resource path) is going on,
554
            //so control won't get into the below for loop.
555
            //we will directly return the root node,
556
            //which is 'ExpandedProjectionNode'
557
            // for resource identified by resource path.
558
            if (0 != $depth) {
559
                for ($i = 1; $i < $depth; ++$i) {
560
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segmentNames[$i]);
561
                    assert(!is_null($expandedProjectionNode), 'is_null($expandedProjectionNode)');
562
                    assert(
563
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
564
                        '$expandedProjectionNode not instanceof ExpandedProjectionNode'
565
                    );
566
                }
567
            }
568
        }
569
570
        return $expandedProjectionNode;
571
    }
572
573
    /**
574
     * Check whether to expand a navigation property or not.
575
     *
576
     * @param string $navigationPropertyName Name of naviagtion property in question
577
     *
578
     * @return bool True if the given navigation should be
579
     *              explanded otherwise false
580
     */
581
    protected function shouldExpandSegment($navigationPropertyName)
582
    {
583
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
584
        if (is_null($expandedProjectionNode)) {
585
            return false;
586
        }
587
588
        $expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName);
589
590
        // null is a valid input to an instanceof call as of PHP 5.6 - will always return false
591
        return $expandedProjectionNode instanceof ExpandedProjectionNode;
592
    }
593
594
    /**
595
     * Wheter next link is needed for the current resource set (feed)
596
     * being serialized.
597
     *
598
     * @param int $resultSetCount Number of entries in the current
599
     *                            resource set
600
     *
601
     * @return bool true if the feed must have a next page link
602
     */
603
    protected function needNextPageLink($resultSetCount)
604
    {
605
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
606
        $recursionLevel = count($this->getStack()->getSegmentNames());
607
        $pageSize = $currentResourceSet->getResourceSetPageSize();
608
609
        if (1 == $recursionLevel) {
610
            //presence of $top option affect next link for root container
611
            $topValueCount = $this->getRequest()->getTopOptionCount();
612
            if (!is_null($topValueCount) && ($topValueCount <= $pageSize)) {
613
                return false;
614
            }
615
        }
616
        return $resultSetCount == $pageSize;
617
    }
618
619
    /**
620
     * Resource set wrapper for the resource being serialized.
621
     *
622
     * @return ResourceSetWrapper
623
     */
624
    protected function getCurrentResourceSetWrapper()
625
    {
626
        $segmentWrappers = $this->getStack()->getSegmentWrappers();
627
        $count = count($segmentWrappers);
628
629
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $segmentWrappers[$count - 1];
630
    }
631
632
    /**
633
     * Get next page link from the given entity instance.
634
     *
635
     * @param mixed  &$lastObject Last object serialized to be
636
     *                            used for generating $skiptoken
637
     * @param string $absoluteUri Absolute response URI
638
     *
639
     * @return string for the link for next page
640
     */
641
    protected function getNextLinkUri(&$lastObject, $absoluteUri)
0 ignored issues
show
Unused Code introduced by
The parameter $absoluteUri is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
642
    {
643
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
644
        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
645
        assert(null != $internalOrderByInfo);
646
        assert(is_object($internalOrderByInfo));
647
        assert($internalOrderByInfo instanceof InternalOrderByInfo, get_class($internalOrderByInfo));
648
        $numSegments = count($internalOrderByInfo->getOrderByPathSegments());
649
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
650
651
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
652
        assert(!is_null($skipToken), '!is_null($skipToken)');
653
        $token = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
654
        $skipToken = '?'.$queryParameterString.$token.$skipToken;
655
656
        return $skipToken;
657
    }
658
659
    /**
660
     * Builds the string corresponding to query parameters for top level results
661
     * (result set identified by the resource path) to be put in next page link.
662
     *
663
     * @return string|null string representing the query parameters in the URI
664
     *                     query parameter format, NULL if there
665
     *                     is no query parameters
666
     *                     required for the next link of top level result set
667
     */
668
    protected function getNextPageLinkQueryParametersForRootResourceSet()
669
    {
670
        $queryParameterString = null;
671
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
672
                     ODataConstants::HTTPQUERY_STRING_EXPAND,
673
                     ODataConstants::HTTPQUERY_STRING_ORDERBY,
674
                     ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
675
                     ODataConstants::HTTPQUERY_STRING_SELECT, ] as $queryOption) {
676
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
677
            if (!is_null($value)) {
678
                if (!is_null($queryParameterString)) {
679
                    $queryParameterString = $queryParameterString . '&';
680
                }
681
682
                $queryParameterString .= $queryOption . '=' . $value;
683
            }
684
        }
685
686
        $topCountValue = $this->getRequest()->getTopOptionCount();
687
        if (!is_null($topCountValue)) {
688
            $remainingCount = $topCountValue - $this->getRequest()->getTopCount();
689
            if (0 < $remainingCount) {
690
                if (!is_null($queryParameterString)) {
691
                    $queryParameterString .= '&';
692
                }
693
694
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
695
            }
696
        }
697
698
        if (!is_null($queryParameterString)) {
699
            $queryParameterString .= '&';
700
        }
701
702
        return $queryParameterString;
703
    }
704
705
    private function loadStackIfEmpty()
706
    {
707
        if (0 == count($this->lightStack)) {
708
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
709
            array_push($this->lightStack, [$typeName, $typeName]);
710
        }
711
    }
712
713
    /**
714
     * Convert the given primitive value to string.
715
     * Note: This method will not handle null primitive value.
716
     *
717
     * @param IType &$primitiveResourceType        Type of the primitive property
718
     *                                             whose value need to be converted
719
     * @param mixed        $primitiveValue         Primitive value to convert
720
     *
721
     * @return string
722
     */
723
    private function primitiveToString(IType &$type, $primitiveValue)
724
    {
725
        if ($type instanceof Boolean) {
726
            $stringValue = (true === $primitiveValue) ? 'true' : 'false';
727
        } elseif ($type instanceof Binary) {
728
            $stringValue = base64_encode($primitiveValue);
729
        } elseif ($type instanceof DateTime && $primitiveValue instanceof \DateTime) {
730
            $stringValue = $primitiveValue->format(\DateTime::ATOM);
731
        } elseif ($type instanceof StringType) {
732
            $stringValue = utf8_encode($primitiveValue);
733
        } else {
734
            $stringValue = strval($primitiveValue);
735
        }
736
737
        return $stringValue;
738
    }
739
740
    /**
741
     * @param $entryObject
742
     * @param $nonRelProp
743
     * @return ODataPropertyContent
744
     */
745
    private function writePrimitiveProperties($entryObject, $nonRelProp)
746
    {
747
        $propertyContent = new ODataPropertyContent();
748
        $cereal = $this->modelSerialiser->bulkSerialise($entryObject);
749
        foreach ($cereal as $corn => $flake) {
750
            if (!array_key_exists($corn, $nonRelProp)) {
751
                continue;
752
            }
753
            $corn = strval($corn);
754
            $rType = $nonRelProp[$corn]->getResourceType()->getInstanceType();
755
            $subProp = new ODataProperty();
756
            $subProp->name = $corn;
757
            $subProp->value = isset($flake) ? $this->primitiveToString($rType, $flake) : null;
758
            $subProp->typeName = $nonRelProp[$corn]->getResourceType()->getFullName();
759
            $propertyContent->properties[] = $subProp;
760
        }
761
        return $propertyContent;
762
    }
763
764
    /**
765
     * @param $entryObject
766
     * @param $prop
767
     * @param $nuLink
768
     * @param $propKind
769
     * @param $propName
770
     */
771
    private function expandNavigationProperty(QueryResult $entryObject, $prop, $nuLink, $propKind, $propName)
772
    {
773
        $nextName = $prop->getResourceType()->getName();
774
        $nuLink->isExpanded = true;
775
        $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind;
776
        $nuLink->isCollection = $isCollection;
777
        $value = $entryObject->results->$propName;
778
        $result = new QueryResult();
779
        $result->results = $value;
780
        array_push($this->lightStack, [$nextName, $propName]);
781
        if (!$isCollection) {
782
            $expandedResult = $this->writeTopLevelElement($result);
783
        } else {
784
            $expandedResult = $this->writeTopLevelElements($result);
785
        }
786
        $nuLink->expandedResult = $expandedResult;
787
        if (!isset($nuLink->expandedResult)) {
788
            $nuLink->isCollection = null;
789
            $nuLink->isExpanded = null;
790
        } else {
791
            if (isset($nuLink->expandedResult->selfLink)) {
792
                $nuLink->expandedResult->selfLink->title = $propName;
793
                $nuLink->expandedResult->selfLink->url = $nuLink->url;
794
                $nuLink->expandedResult->title = $propName;
795
                $nuLink->expandedResult->id = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->url;
796
            }
797
        }
798
    }
799
800
    /**
801
     * Gets the data service instance.
802
     *
803
     * @return IService
804
     */
805
    public function setService(IService $service)
806
    {
807
        $this->service = $service;
808
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
809
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
810
    }
811
812
    /**
813
     * @param ResourceType $resourceType
814
     * @param $result
815
     * @return ODataBagContent
816
     */
817
    protected function writeBagValue(ResourceType &$resourceType, $result)
818
    {
819
        assert(null == $result || is_array($result), 'Bag parameter must be null or array');
820
        $typeKind = $resourceType->getResourceTypeKind();
821
        assert(
822
            ResourceTypeKind::PRIMITIVE == $typeKind || ResourceTypeKind::COMPLEX == $typeKind,
823
            '$bagItemResourceTypeKind != ResourceTypeKind::PRIMITIVE'
824
            .' && $bagItemResourceTypeKind != ResourceTypeKind::COMPLEX'
825
        );
826
        if (null == $result) {
827
            return null;
828
        }
829
        $bag = new ODataBagContent();
830
        foreach ($result as $value) {
831
            if (isset($value)) {
832
                if (ResourceTypeKind::PRIMITIVE == $typeKind) {
833
                    $instance = $resourceType->getInstanceType();
834
                    $bag->propertyContents[] = $this->primitiveToString($instance, $value);
0 ignored issues
show
Bug introduced by
It seems like $instance defined by $resourceType->getInstanceType() on line 833 can also be of type object<ReflectionClass> or string; however, AlgoWeb\PODataLaravel\Se...er::primitiveToString() does only seem to accept object<POData\Providers\Metadata\Type\IType>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
835
                } elseif (ResourceTypeKind::COMPLEX == $typeKind) {
836
                    $bag->propertyContents[] = $this->writeComplexValue($resourceType, $value);
837
                }
838
            }
839
        }
840
        return $bag;
841
    }
842
843
    /**
844
     * @param ResourceType $resourceType
845
     * @param object $result
846
     * @return ODataPropertyContent
847
     */
848
    protected function writeComplexValue(ResourceType &$resourceType, $result)
849
    {
850
        assert(is_object($result), 'Supplied $customObject must be an object');
851
        $internalContent = new ODataPropertyContent();
852
        $resourceProperties = $resourceType->getAllProperties();
853
        // first up, handle primitive properties
854
        foreach ($resourceProperties as $prop) {
855
            $resourceKind = $prop->getKind();
856
            $propName = $prop->getName();
857
            $internalProperty = new ODataProperty();
858
            $internalProperty->name = $propName;
859
            if (static::isMatchPrimitive($resourceKind)) {
860
                $iType = $prop->getInstanceType();
861
                $internalProperty->typeName = $iType->getFullTypeName();
0 ignored issues
show
Bug introduced by
The method getFullTypeName does only exist in POData\Providers\Metadata\Type\IType, but not in ReflectionClass.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
862
863
                $rType = $prop->getResourceType()->getInstanceType();
864
                $internalProperty->value = $this->primitiveToString($rType, $result->$propName);
0 ignored issues
show
Bug introduced by
It seems like $rType defined by $prop->getResourceType()->getInstanceType() on line 863 can also be of type object<ReflectionClass> or string; however, AlgoWeb\PODataLaravel\Se...er::primitiveToString() does only seem to accept object<POData\Providers\Metadata\Type\IType>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
865
866
                $internalContent->properties[] = $internalProperty;
867
            } elseif (ResourcePropertyKind::COMPLEX_TYPE == $resourceKind) {
868
                $rType = $prop->getResourceType();
869
                $internalProperty->typeName = $rType->getFullName();
870
                $internalProperty->value = $this->writeComplexValue($rType, $result->$propName);
871
872
                $internalContent->properties[] = $internalProperty;
873
            }
874
        }
875
        return $internalContent;
876
    }
877
878
    public static function isMatchPrimitive($resourceKind)
879
    {
880
        if (16 > $resourceKind) {
881
            return false;
882
        }
883
        if (28 < $resourceKind) {
884
            return false;
885
        }
886
        return 0 == ($resourceKind % 4);
887
    }
888
}
889