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

IronicSerialiser::isMatchPrimitive()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Serialisers;
4
5
use POData\Common\InvalidOperationException;
6
use POData\Common\Messages;
7
use POData\Common\ODataConstants;
8
use POData\Common\ODataException;
9
use POData\IService;
10
use POData\ObjectModel\IObjectSerialiser;
11
use POData\ObjectModel\ODataBagContent;
12
use POData\ObjectModel\ODataEntry;
13
use POData\ObjectModel\ODataFeed;
14
use POData\ObjectModel\ODataLink;
15
use POData\ObjectModel\ODataMediaLink;
16
use POData\ObjectModel\ODataNavigationPropertyInfo;
17
use POData\ObjectModel\ODataProperty;
18
use POData\ObjectModel\ODataPropertyContent;
19
use POData\ObjectModel\ODataURL;
20
use POData\ObjectModel\ODataURLCollection;
21
use POData\Providers\Metadata\ResourceEntityType;
22
use POData\Providers\Metadata\ResourceProperty;
23
use POData\Providers\Metadata\ResourcePropertyKind;
24
use POData\Providers\Metadata\ResourceSet;
25
use POData\Providers\Metadata\ResourceSetWrapper;
26
use POData\Providers\Metadata\ResourceType;
27
use POData\Providers\Metadata\ResourceTypeKind;
28
use POData\Providers\Metadata\Type\Binary;
29
use POData\Providers\Metadata\Type\Boolean;
30
use POData\Providers\Metadata\Type\DateTime;
31
use POData\Providers\Metadata\Type\IType;
32
use POData\Providers\Metadata\Type\StringType;
33
use POData\Providers\Query\QueryResult;
34
use POData\Providers\Query\QueryType;
35
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
36
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ProjectionNode;
37
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
38
use POData\UriProcessor\RequestDescription;
39
use POData\UriProcessor\SegmentStack;
40
41
class IronicSerialiser implements IObjectSerialiser
42
{
43
    /**
44
     * The service implementation.
45
     *
46
     * @var IService
47
     */
48
    protected $service;
49
50
    /**
51
     * Request description instance describes OData request the
52
     * the client has submitted and result of the request.
53
     *
54
     * @var RequestDescription
55
     */
56
    protected $request;
57
58
    /**
59
     * Collection of complex type instances used for cycle detection.
60
     *
61
     * @var array
62
     */
63
    protected $complexTypeInstanceCollection;
64
65
    /**
66
     * Absolute service Uri.
67
     *
68
     * @var string
69
     */
70
    protected $absoluteServiceUri;
71
72
    /**
73
     * Absolute service Uri with slash.
74
     *
75
     * @var string
76
     */
77
    protected $absoluteServiceUriWithSlash;
78
79
    /**
80
     * Holds reference to segment stack being processed.
81
     *
82
     * @var SegmentStack
83
     */
84
    protected $stack;
85
86
    /**
87
     * Lightweight stack tracking for recursive descent fill
88
     */
89
    private $lightStack = [];
90
91
    private $modelSerialiser;
92
93
    /**
94
     * @param IService           $service Reference to the data service instance
95
     * @param RequestDescription $request Type instance describing the client submitted request
96
     */
97
    public function __construct(IService $service, RequestDescription $request = null)
98
    {
99
        $this->service = $service;
100
        $this->request = $request;
101
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
102
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
103
        $this->stack = new SegmentStack($request);
104
        $this->complexTypeInstanceCollection = [];
105
        $this->modelSerialiser = new ModelSerialiser();
106
    }
107
108
    /**
109
     * Write a top level entry resource.
110
     *
111
     * @param mixed $entryObject Reference to the entry object to be written
112
     *
113
     * @return ODataEntry
114
     */
115
    public function writeTopLevelElement(QueryResult $entryObject)
116
    {
117
        if (!isset($entryObject->results)) {
118
            array_pop($this->lightStack);
119
            return null;
120
        }
121
122
        $this->loadStackIfEmpty();
123
124
        $stackCount = count($this->lightStack);
125
        $topOfStack = $this->lightStack[$stackCount-1];
126
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack[0]);
127
        $rawProp = $resourceType->getAllProperties();
128
        $relProp = [];
129
        $nonRelProp = [];
130
        foreach ($rawProp as $prop) {
131
            if ($prop->getResourceType() instanceof ResourceEntityType) {
132
                $relProp[] = $prop;
133
            } else {
134
                $nonRelProp[$prop->getName()] = $prop;
135
            }
136
        }
137
138
        $resourceSet = $resourceType->getCustomState();
139
        assert($resourceSet instanceof ResourceSet);
140
        $title = $resourceType->getName();
141
        $type = $resourceType->getFullName();
142
143
        $relativeUri = $this->getEntryInstanceKey(
144
            $entryObject->results,
145
            $resourceType,
146
            $resourceSet->getName()
147
        );
148
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
149
150
        list($mediaLink, $mediaLinks) = $this->writeMediaData($entryObject->results, $type, $relativeUri, $resourceType);
151
152
        $propertyContent = $this->writePrimitiveProperties($entryObject->results, $nonRelProp);
153
154
        $links = [];
155
        foreach ($relProp as $prop) {
156
            $nuLink = new ODataLink();
157
            $propKind = $prop->getKind();
158
159
            assert(
160
                ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind
161
                || ResourcePropertyKind::RESOURCE_REFERENCE == $propKind,
162
                '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
163
                .' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE'
164
            );
165
            $propTail = ResourcePropertyKind::RESOURCE_REFERENCE == $propKind ? 'entry' : 'feed';
166
            $propType = 'application/atom+xml;type='.$propTail;
167
            $propName = $prop->getName();
168
            $nuLink->title = $propName;
169
            $nuLink->name = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
170
            $nuLink->url = $relativeUri . '/' . $propName;
171
            $nuLink->type = $propType;
172
173
            $navProp = new ODataNavigationPropertyInfo($prop, $this->shouldExpandSegment($propName));
174
            if ($navProp->expanded) {
175
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
176
            }
177
178
            $links[] = $nuLink;
179
        }
180
181
        $odata = new ODataEntry();
182
        $odata->resourceSetName = $resourceSet->getName();
183
        $odata->id = $absoluteUri;
184
        $odata->title = $title;
185
        $odata->type = $type;
186
        $odata->propertyContent = $propertyContent;
187
        $odata->isMediaLinkEntry = $resourceType->isMediaLinkEntry();
188
        $odata->editLink = $relativeUri;
189
        $odata->mediaLink = $mediaLink;
190
        $odata->mediaLinks = $mediaLinks;
191
        $odata->links = $links;
192
193
        $newCount = count($this->lightStack);
194
        assert($newCount == $stackCount, "Should have $stackCount elements in stack, have $newCount elements");
195
        array_pop($this->lightStack);
196
        return $odata;
197
    }
198
199
    /**
200
     * Write top level feed element.
201
     *
202
     * @param array &$entryObjects Array of entry resources to be written
203
     *
204
     * @return ODataFeed
205
     */
206
    public function writeTopLevelElements(QueryResult &$entryObjects)
207
    {
208
        assert(is_array($entryObjects->results), '!is_array($entryObjects->results)');
209
210
        $this->loadStackIfEmpty();
211
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
212
213
        $title = $this->getRequest()->getContainerName();
214
        $relativeUri = $this->getRequest()->getIdentifier();
215
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
216
217
        $selfLink = new ODataLink();
218
        $selfLink->name = 'self';
219
        $selfLink->title = $relativeUri;
220
        $selfLink->url = $relativeUri;
221
222
        $odata = new ODataFeed();
223
        $odata->title = $title;
224
        $odata->id = $absoluteUri;
225
        $odata->selfLink = $selfLink;
226
227
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
228
            $odata->rowCount = $this->getRequest()->getCountValue();
229
        }
230
        foreach ($entryObjects->results as $entry) {
231
            if (!$entry instanceof QueryResult) {
232
                $query = new QueryResult();
233
                $query->results = $entry;
234
            } else {
235
                $query = $entry;
236
            }
237
            $odata->entries[] = $this->writeTopLevelElement($query);
238
        }
239
240
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
241
        $requestTop = $this->getRequest()->getTopOptionCount();
242
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
243
        $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...
244
245
        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...
246
            $stackSegment = $setName;
247
            $lastObject = end($entryObjects->results);
248
            $segment = $this->getNextLinkUri($lastObject, $absoluteUri);
249
            $nextLink = new ODataLink();
250
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
251
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
252
            $odata->nextPageLink = $nextLink;
253
        }
254
255
        return $odata;
256
    }
257
258
    /**
259
     * Write top level url element.
260
     *
261
     * @param mixed $entryObject The entry resource whose url to be written
262
     *
263
     * @return ODataURL
264
     */
265
    public function writeUrlElement(QueryResult $entryObject)
266
    {
267
        $url = new ODataURL();
268
        if (!is_null($entryObject->results)) {
269
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
270
            $relativeUri = $this->getEntryInstanceKey(
271
                $entryObject->results,
272
                $currentResourceType,
273
                $this->getCurrentResourceSetWrapper()->getName()
274
            );
275
276
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
277
        }
278
279
        return $url;
280
    }
281
282
    /**
283
     * Write top level url collection.
284
     *
285
     * @param array $entryObjects Array of entry resources
286
     *                            whose url to be written
287
     *
288
     * @return ODataURLCollection
289
     */
290
    public function writeUrlElements(QueryResult $entryObjects)
291
    {
292
        $urls = new ODataURLCollection();
293
        if (!empty($entryObjects->results)) {
294
            $i = 0;
295
            foreach ($entryObjects->results as $entryObject) {
296
                $urls->urls[$i] = $this->writeUrlElement($entryObject);
297
                ++$i;
298
            }
299
300
            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...
301
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
302
                $lastObject = end($entryObjects->results);
303
                $segment = $this->getNextLinkUri($lastObject, $this->getRequest()->getRequestUrl()->getUrlAsString());
304
                $nextLink = new ODataLink();
305
                $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
306
                $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
307
                $urls->nextPageLink = $nextLink;
308
            }
309
        }
310
311
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
312
            $urls->count = $this->getRequest()->getCountValue();
313
        }
314
315
        return $urls;
316
    }
317
318
    /**
319
     * Write top level complex resource.
320
     *
321
     * @param mixed &$complexValue The complex object to be
322
     *                                    written
323
     * @param string $propertyName The name of the
324
     *                                    complex property
325
     * @param ResourceType &$resourceType Describes the type of
326
     *                                    complex object
327
     *
328
     * @return ODataPropertyContent
329
     */
330
    public function writeTopLevelComplexObject(QueryResult &$complexValue, $propertyName, ResourceType &$resourceType)
331
    {
332
        $result = $complexValue->results;
333
334
        $propertyContent = new ODataPropertyContent();
335
        $odataProperty = new ODataProperty();
336
        $odataProperty->name = $propertyName;
337
        $odataProperty->typeName = $resourceType->getFullName();
338
        if (null != $result) {
339
            $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...
340
            $odataProperty->value = $internalContent;
341
        }
342
343
        $propertyContent->properties[] = $odataProperty;
344
345
        return $propertyContent;
346
    }
347
348
    /**
349
     * Write top level bag resource.
350
     *
351
     * @param mixed &$BagValue The bag object to be
352
     *                                    written
353
     * @param string $propertyName The name of the
354
     *                                    bag property
355
     * @param ResourceType &$resourceType Describes the type of
356
     *                                    bag object
357
     * @return ODataPropertyContent
358
     */
359
    public function writeTopLevelBagObject(QueryResult &$BagValue, $propertyName, ResourceType &$resourceType)
360
    {
361
        $result = $BagValue->results;
362
363
        $propertyContent = new ODataPropertyContent();
364
        $odataProperty = new ODataProperty();
365
        $odataProperty->name = $propertyName;
366
        $odataProperty->typeName = 'Collection('.$resourceType->getFullName().')';
367
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
368
369
        $propertyContent->properties[] = $odataProperty;
370
        return $propertyContent;
371
    }
372
373
    /**
374
     * Write top level primitive value.
375
     *
376
     * @param mixed &$primitiveValue              The primitive value to be
377
     *                                            written
378
     * @param ResourceProperty &$resourceProperty Resource property describing the
379
     *                                            primitive property to be written
380
     * @return ODataPropertyContent
381
     */
382
    public function writeTopLevelPrimitive(QueryResult &$primitiveValue, ResourceProperty &$resourceProperty = null)
383
    {
384
        assert(null != $resourceProperty, "Resource property must not be null");
385
        $propertyContent = new ODataPropertyContent();
386
387
        $odataProperty = new ODataProperty();
388
        $odataProperty->name = $resourceProperty->getName();
389
        $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...
390
        if (null == $primitiveValue->results) {
391
            $odataProperty->value = null;
392
        } else {
393
            $rType = $resourceProperty->getResourceType()->getInstanceType();
394
            $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 393 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...
395
        }
396
397
        $propertyContent->properties[] = $odataProperty;
398
399
        return $propertyContent;
400
    }
401
402
    /**
403
     * Gets reference to the request submitted by client.
404
     *
405
     * @return RequestDescription
406
     */
407
    public function getRequest()
408
    {
409
        assert(null != $this->request, 'Request not yet set');
410
411
        return $this->request;
412
    }
413
414
    /**
415
     * Sets reference to the request submitted by client.
416
     *
417
     * @param RequestDescription $request
418
     */
419
    public function setRequest(RequestDescription $request)
420
    {
421
        $this->request = $request;
422
        $this->stack->setRequest($request);
423
    }
424
425
    /**
426
     * Gets the data service instance.
427
     *
428
     * @return IService
429
     */
430
    public function getService()
431
    {
432
        return $this->service;
433
    }
434
435
    /**
436
     * Gets the segment stack instance.
437
     *
438
     * @return SegmentStack
439
     */
440
    public function getStack()
441
    {
442
        return $this->stack;
443
    }
444
445
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
446
    {
447
        $typeName = $resourceType->getName();
448
        $keyProperties = $resourceType->getKeyProperties();
449
        assert(count($keyProperties) != 0, 'count($keyProperties) == 0');
450
        $keyString = $containerName . '(';
451
        $comma = null;
452
        foreach ($keyProperties as $keyName => $resourceProperty) {
453
            $keyType = $resourceProperty->getInstanceType();
454
            assert($keyType instanceof IType, '$keyType not instanceof IType');
455
            $keyName = $resourceProperty->getName();
456
            $keyValue = $entityInstance->$keyName;
457
            if (!isset($keyValue)) {
458
                throw ODataException::createInternalServerError(
459
                    Messages::badQueryNullKeysAreNotSupported($typeName, $keyName)
460
                );
461
            }
462
463
            $keyValue = $keyType->convertToOData($keyValue);
464
            $keyString .= $comma . $keyName . '=' . $keyValue;
465
            $comma = ',';
466
        }
467
468
        $keyString .= ')';
469
470
        return $keyString;
471
    }
472
473
    /**
474
     * @param $entryObject
475
     * @param $type
476
     * @param $relativeUri
477
     * @param $resourceType
478
     * @return array
479
     */
480
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
481
    {
482
        $context = $this->getService()->getOperationContext();
483
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
484
        assert(null != $streamProviderWrapper, "Retrieved stream provider must not be null");
485
486
        $mediaLink = null;
487
        if ($resourceType->isMediaLinkEntry()) {
488
            $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...
489
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag);
490
        }
491
        $mediaLinks = [];
492
        if ($resourceType->hasNamedStream()) {
493
            $namedStreams = $resourceType->getAllNamedStreams();
494
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
495
                $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...
496
                    $entryObject,
497
                    $resourceStreamInfo,
498
                    $context,
499
                    $relativeUri
500
                );
501
                $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...
502
                    $entryObject,
503
                    $resourceStreamInfo,
504
                    $context
505
                );
506
                $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...
507
                    $entryObject,
508
                    $resourceStreamInfo,
509
                    $context
510
                );
511
512
                $nuLink = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
513
                $mediaLinks[] = $nuLink;
514
            }
515
        }
516
        return [$mediaLink, $mediaLinks];
517
    }
518
519
    /**
520
     * Gets collection of projection nodes under the current node.
521
     *
522
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes
523
     *                                                        describing projections for the current segment, If this method returns
524
     *                                                        null it means no projections are to be applied and the entire resource
525
     *                                                        for the current segment should be serialized, If it returns non-null
526
     *                                                        only the properties described by the returned projection segments should
527
     *                                                        be serialized
528
     */
529
    protected function getProjectionNodes()
530
    {
531
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
532
        if (is_null($expandedProjectionNode) || $expandedProjectionNode->canSelectAllProperties()) {
533
            return null;
534
        }
535
536
        return $expandedProjectionNode->getChildNodes();
537
    }
538
539
    /**
540
     * Find a 'ExpandedProjectionNode' instance in the projection tree
541
     * which describes the current segment.
542
     *
543
     * @return ExpandedProjectionNode|null
544
     */
545
    protected function getCurrentExpandedProjectionNode()
546
    {
547
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
548
        if (is_null($expandedProjectionNode)) {
549
            return null;
550
        } else {
551
            $segmentNames = $this->getStack()->getSegmentNames();
552
            $depth = count($segmentNames);
553
            // $depth == 1 means serialization of root entry
554
            //(the resource identified by resource path) is going on,
555
            //so control won't get into the below for loop.
556
            //we will directly return the root node,
557
            //which is 'ExpandedProjectionNode'
558
            // for resource identified by resource path.
559
            if (0 != $depth) {
560
                for ($i = 1; $i < $depth; ++$i) {
561
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segmentNames[$i]);
562
                    assert(!is_null($expandedProjectionNode), 'is_null($expandedProjectionNode)');
563
                    assert(
564
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
565
                        '$expandedProjectionNode not instanceof ExpandedProjectionNode'
566
                    );
567
                }
568
            }
569
        }
570
571
        return $expandedProjectionNode;
572
    }
573
574
    /**
575
     * Check whether to expand a navigation property or not.
576
     *
577
     * @param string $navigationPropertyName Name of naviagtion property in question
578
     *
579
     * @return bool True if the given navigation should be
580
     *              explanded otherwise false
581
     */
582
    protected function shouldExpandSegment($navigationPropertyName)
583
    {
584
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
585
        if (is_null($expandedProjectionNode)) {
586
            return false;
587
        }
588
589
        $expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName);
590
591
        // null is a valid input to an instanceof call as of PHP 5.6 - will always return false
592
        return $expandedProjectionNode instanceof ExpandedProjectionNode;
593
    }
594
595
    /**
596
     * Wheter next link is needed for the current resource set (feed)
597
     * being serialized.
598
     *
599
     * @param int $resultSetCount Number of entries in the current
600
     *                            resource set
601
     *
602
     * @return bool true if the feed must have a next page link
603
     */
604
    protected function needNextPageLink($resultSetCount)
605
    {
606
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
607
        $recursionLevel = count($this->getStack()->getSegmentNames());
608
        $pageSize = $currentResourceSet->getResourceSetPageSize();
609
610
        if (1 == $recursionLevel) {
611
            //presence of $top option affect next link for root container
612
            $topValueCount = $this->getRequest()->getTopOptionCount();
613
            if (!is_null($topValueCount) && ($topValueCount <= $pageSize)) {
614
                return false;
615
            }
616
        }
617
        return $resultSetCount == $pageSize;
618
    }
619
620
    /**
621
     * Resource set wrapper for the resource being serialized.
622
     *
623
     * @return ResourceSetWrapper
624
     */
625
    protected function getCurrentResourceSetWrapper()
626
    {
627
        $segmentWrappers = $this->getStack()->getSegmentWrappers();
628
        $count = count($segmentWrappers);
629
630
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $segmentWrappers[$count - 1];
631
    }
632
633
    /**
634
     * Get next page link from the given entity instance.
635
     *
636
     * @param mixed  &$lastObject Last object serialized to be
637
     *                            used for generating $skiptoken
638
     * @param string $absoluteUri Absolute response URI
639
     *
640
     * @return string for the link for next page
641
     */
642
    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...
643
    {
644
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
645
        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
646
        assert(null != $internalOrderByInfo);
647
        assert(is_object($internalOrderByInfo));
648
        assert($internalOrderByInfo instanceof InternalOrderByInfo, get_class($internalOrderByInfo));
649
        $numSegments = count($internalOrderByInfo->getOrderByPathSegments());
650
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
651
652
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
653
        assert(!is_null($skipToken), '!is_null($skipToken)');
654
        $token = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
655
        $skipToken = '?'.$queryParameterString.$token.$skipToken;
656
657
        return $skipToken;
658
    }
659
660
    /**
661
     * Builds the string corresponding to query parameters for top level results
662
     * (result set identified by the resource path) to be put in next page link.
663
     *
664
     * @return string|null string representing the query parameters in the URI
665
     *                     query parameter format, NULL if there
666
     *                     is no query parameters
667
     *                     required for the next link of top level result set
668
     */
669
    protected function getNextPageLinkQueryParametersForRootResourceSet()
670
    {
671
        $queryParameterString = null;
672
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
673
                     ODataConstants::HTTPQUERY_STRING_EXPAND,
674
                     ODataConstants::HTTPQUERY_STRING_ORDERBY,
675
                     ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
676
                     ODataConstants::HTTPQUERY_STRING_SELECT, ] as $queryOption) {
677
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
678
            if (!is_null($value)) {
679
                if (!is_null($queryParameterString)) {
680
                    $queryParameterString = $queryParameterString . '&';
681
                }
682
683
                $queryParameterString .= $queryOption . '=' . $value;
684
            }
685
        }
686
687
        $topCountValue = $this->getRequest()->getTopOptionCount();
688
        if (!is_null($topCountValue)) {
689
            $remainingCount = $topCountValue - $this->getRequest()->getTopCount();
690
            if (0 < $remainingCount) {
691
                if (!is_null($queryParameterString)) {
692
                    $queryParameterString .= '&';
693
                }
694
695
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
696
            }
697
        }
698
699
        if (!is_null($queryParameterString)) {
700
            $queryParameterString .= '&';
701
        }
702
703
        return $queryParameterString;
704
    }
705
706
    private function loadStackIfEmpty()
707
    {
708
        if (0 == count($this->lightStack)) {
709
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
710
            array_push($this->lightStack, [$typeName, $typeName]);
711
        }
712
    }
713
714
    /**
715
     * Convert the given primitive value to string.
716
     * Note: This method will not handle null primitive value.
717
     *
718
     * @param IType &$primitiveResourceType        Type of the primitive property
719
     *                                             whose value need to be converted
720
     * @param mixed        $primitiveValue         Primitive value to convert
721
     *
722
     * @return string
723
     */
724
    private function primitiveToString(IType &$type, $primitiveValue)
725
    {
726
        if ($type instanceof Boolean) {
727
            $stringValue = (true === $primitiveValue) ? 'true' : 'false';
728
        } elseif ($type instanceof Binary) {
729
            $stringValue = base64_encode($primitiveValue);
730
        } elseif ($type instanceof DateTime && $primitiveValue instanceof \DateTime) {
731
            $stringValue = $primitiveValue->format(\DateTime::ATOM);
732
        } elseif ($type instanceof StringType) {
733
            $stringValue = utf8_encode($primitiveValue);
734
        } else {
735
            $stringValue = strval($primitiveValue);
736
        }
737
738
        return $stringValue;
739
    }
740
741
    /**
742
     * @param $entryObject
743
     * @param $nonRelProp
744
     * @return ODataPropertyContent
745
     */
746
    private function writePrimitiveProperties($entryObject, $nonRelProp)
747
    {
748
        $propertyContent = new ODataPropertyContent();
749
        $cereal = $this->modelSerialiser->bulkSerialise($entryObject);
750
        foreach ($cereal as $corn => $flake) {
751
            if (!array_key_exists($corn, $nonRelProp)) {
752
                continue;
753
            }
754
            $corn = strval($corn);
755
            $rType = $nonRelProp[$corn]->getResourceType()->getInstanceType();
756
            $subProp = new ODataProperty();
757
            $subProp->name = $corn;
758
            $subProp->value = isset($flake) ? $this->primitiveToString($rType, $flake) : null;
759
            $subProp->typeName = $nonRelProp[$corn]->getResourceType()->getFullName();
760
            $propertyContent->properties[] = $subProp;
761
        }
762
        return $propertyContent;
763
    }
764
765
    /**
766
     * @param $entryObject
767
     * @param $prop
768
     * @param $nuLink
769
     * @param $propKind
770
     * @param $propName
771
     */
772
    private function expandNavigationProperty(QueryResult $entryObject, $prop, $nuLink, $propKind, $propName)
773
    {
774
        $nextName = $prop->getResourceType()->getName();
775
        $nuLink->isExpanded = true;
776
        $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind;
777
        $nuLink->isCollection = $isCollection;
778
        $value = $entryObject->results->$propName;
779
        $result = new QueryResult();
780
        $result->results = $value;
781
        array_push($this->lightStack, [$nextName, $propName]);
782
        if (!$isCollection) {
783
            $expandedResult = $this->writeTopLevelElement($result);
784
        } else {
785
            $expandedResult = $this->writeTopLevelElements($result);
786
        }
787
        $nuLink->expandedResult = $expandedResult;
788
        if (!isset($nuLink->expandedResult)) {
789
            $nuLink->isCollection = null;
790
            $nuLink->isExpanded = null;
791
        } else {
792
            if (isset($nuLink->expandedResult->selfLink)) {
793
                $nuLink->expandedResult->selfLink->title = $propName;
794
                $nuLink->expandedResult->selfLink->url = $nuLink->url;
795
                $nuLink->expandedResult->title = $propName;
796
                $nuLink->expandedResult->id = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->url;
797
            }
798
        }
799
    }
800
801
    /**
802
     * Gets the data service instance.
803
     *
804
     * @return IService
805
     */
806
    public function setService(IService $service)
807
    {
808
        $this->service = $service;
809
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
810
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
811
    }
812
813
    /**
814
     * @param ResourceType $resourceType
815
     * @param $result
816
     * @return ODataBagContent
817
     */
818
    protected function writeBagValue(ResourceType &$resourceType, $result)
819
    {
820
        assert(null == $result || is_array($result), 'Bag parameter must be null or array');
821
        $typeKind = $resourceType->getResourceTypeKind();
822
        assert(
823
            ResourceTypeKind::PRIMITIVE == $typeKind || ResourceTypeKind::COMPLEX == $typeKind,
824
            '$bagItemResourceTypeKind != ResourceTypeKind::PRIMITIVE'
825
            .' && $bagItemResourceTypeKind != ResourceTypeKind::COMPLEX'
826
        );
827
        if (null == $result) {
828
            return null;
829
        }
830
        $bag = new ODataBagContent();
831
        foreach ($result as $value) {
832
            if (isset($value)) {
833
                if (ResourceTypeKind::PRIMITIVE == $typeKind) {
834
                    $instance = $resourceType->getInstanceType();
835
                    $bag->propertyContents[] = $this->primitiveToString($instance, $value);
0 ignored issues
show
Bug introduced by
It seems like $instance defined by $resourceType->getInstanceType() on line 834 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...
836
                } elseif (ResourceTypeKind::COMPLEX == $typeKind) {
837
                    $bag->propertyContents[] = $this->writeComplexValue($resourceType, $value);
838
                }
839
            }
840
        }
841
        return $bag;
842
    }
843
844
    /**
845
     * @param ResourceType $resourceType
846
     * @param object $result
847
     * @param string $propertyName
848
     * @return ODataPropertyContent
849
     */
850
    protected function writeComplexValue(ResourceType &$resourceType, &$result, $propertyName = null)
851
    {
852
        assert(is_object($result), 'Supplied $customObject must be an object');
853
854
        $count = count($this->complexTypeInstanceCollection);
855
        for ($i = 0; $i < $count; ++$i) {
856
            if ($this->complexTypeInstanceCollection[$i] === $result) {
857
                throw new InvalidOperationException(
858
                    Messages::objectModelSerializerLoopsNotAllowedInComplexTypes($propertyName)
859
                );
860
            }
861
        }
862
863
        $this->complexTypeInstanceCollection[$count] = &$result;
864
865
        $internalContent = new ODataPropertyContent();
866
        $resourceProperties = $resourceType->getAllProperties();
867
        // first up, handle primitive properties
868
        foreach ($resourceProperties as $prop) {
869
            $resourceKind = $prop->getKind();
870
            $propName = $prop->getName();
871
            $internalProperty = new ODataProperty();
872
            $internalProperty->name = $propName;
873
            if (static::isMatchPrimitive($resourceKind)) {
874
                $iType = $prop->getInstanceType();
875
                $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...
876
877
                $rType = $prop->getResourceType()->getInstanceType();
878
                $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 877 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...
879
880
                $internalContent->properties[] = $internalProperty;
881
            } elseif (ResourcePropertyKind::COMPLEX_TYPE == $resourceKind) {
882
                $rType = $prop->getResourceType();
883
                $internalProperty->typeName = $rType->getFullName();
884
                $internalProperty->value = $this->writeComplexValue($rType, $result->$propName, $propName);
885
886
                $internalContent->properties[] = $internalProperty;
887
            }
888
        }
889
890
        unset($this->complexTypeInstanceCollection[$count]);
891
        return $internalContent;
892
    }
893
894
    public static function isMatchPrimitive($resourceKind)
895
    {
896
        if (16 > $resourceKind) {
897
            return false;
898
        }
899
        if (28 < $resourceKind) {
900
            return false;
901
        }
902
        return 0 == ($resourceKind % 4);
903
    }
904
}
905