Passed
Pull Request — master (#206)
by Alex
04:54
created

IronicSerialiser::buildFeedNextPageLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 1
c 2
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 2
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Serialisers;
4
5
use Carbon\Carbon;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\Facades\App;
9
use POData\Common\InvalidOperationException;
10
use POData\Common\Messages;
11
use POData\Common\ODataConstants;
12
use POData\Common\ODataException;
13
use POData\IService;
14
use POData\ObjectModel\IObjectSerialiser;
15
use POData\ObjectModel\ODataBagContent;
16
use POData\ObjectModel\ODataCategory;
17
use POData\ObjectModel\ODataEntry;
18
use POData\ObjectModel\ODataFeed;
19
use POData\ObjectModel\ODataLink;
20
use POData\ObjectModel\ODataMediaLink;
21
use POData\ObjectModel\ODataNavigationPropertyInfo;
22
use POData\ObjectModel\ODataProperty;
23
use POData\ObjectModel\ODataPropertyContent;
24
use POData\ObjectModel\ODataTitle;
25
use POData\ObjectModel\ODataURL;
26
use POData\ObjectModel\ODataURLCollection;
27
use POData\Providers\Metadata\IMetadataProvider;
28
use POData\Providers\Metadata\ResourceEntityType;
29
use POData\Providers\Metadata\ResourceProperty;
30
use POData\Providers\Metadata\ResourcePropertyKind;
31
use POData\Providers\Metadata\ResourceSet;
32
use POData\Providers\Metadata\ResourceSetWrapper;
33
use POData\Providers\Metadata\ResourceType;
34
use POData\Providers\Metadata\ResourceTypeKind;
35
use POData\Providers\Metadata\Type\Binary;
36
use POData\Providers\Metadata\Type\Boolean;
37
use POData\Providers\Metadata\Type\DateTime;
38
use POData\Providers\Metadata\Type\IType;
39
use POData\Providers\Metadata\Type\StringType;
40
use POData\Providers\Query\QueryResult;
41
use POData\Providers\Query\QueryType;
42
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
43
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ProjectionNode;
44
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\RootProjectionNode;
45
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
46
use POData\UriProcessor\RequestDescription;
47
use POData\UriProcessor\SegmentStack;
48
49
class IronicSerialiser implements IObjectSerialiser
50
{
51
    use SerialiseDepWrapperTrait;
52
    use SerialisePropertyCacheTrait;
53
    use SerialiseNavigationTrait;
54
    use SerialiseLowLevelWritersTrait;
55
56
    /**
57
     * Update time to insert into ODataEntry/ODataFeed fields
58
     * @var Carbon
59
     */
60
    private $updated;
61
62
    /**
63
     * Has base URI already been written out during serialisation?
64
     * @var bool
65
     */
66
    private $isBaseWritten = false;
67
68
    /**
69
     * @param IService                $service Reference to the data service instance
70
     * @param RequestDescription|null $request Type instance describing the client submitted request
71
     * @throws \Exception
72
     */
73
    public function __construct(IService $service, RequestDescription $request = null)
74
    {
75
        $this->service = $service;
76
        $this->request = $request;
77
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
78
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
79
        $this->stack = new SegmentStack($request);
80
        $this->complexTypeInstanceCollection = [];
81
        $this->modelSerialiser = new ModelSerialiser();
82
        $this->updated = Carbon::now();
83
    }
84
85
    /**
86
     * Write a top level entry resource.
87
     *
88
     * @param QueryResult $entryObject Reference to the entry object to be written
89
     *
90
     * @return ODataEntry|null
91
     * @throws InvalidOperationException
92
     * @throws \ReflectionException
93
     * @throws ODataException
94
     */
95
    public function writeTopLevelElement(QueryResult $entryObject)
96
    {
97
        if (!isset($entryObject->results)) {
98
            array_pop($this->lightStack);
99
            return null;
100
        }
101
        if (!$entryObject->results instanceof Model) {
102
            $res = $entryObject->results;
103
            $msg = is_array($res) ? 'Entry object must be single Model' : get_class($res);
104
            throw new InvalidOperationException($msg);
105
        }
106
107
        $this->loadStackIfEmpty();
108
        $baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
109
        $this->isBaseWritten = true;
110
111
        $stackCount = count($this->lightStack);
112
        $topOfStack = $this->lightStack[$stackCount-1];
113
        $payloadClass = get_class($entryObject->results);
114
        /** @var ResourceEntityType $resourceType */
115
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
116
117
        // need gubbinz to unpack an abstract resource type
118
        $resourceType = $this->getConcreteTypeFromAbstractType($resourceType, $payloadClass);
119
120
        // make sure we're barking up right tree
121
        if (!$resourceType instanceof ResourceEntityType) {
122
            throw new InvalidOperationException(get_class($resourceType));
123
        }
124
125
        /** @var Model $res */
126
        $res = $entryObject->results;
127
        $targClass = $resourceType->getInstanceType()->getName();
128
        if (!($res instanceof $targClass)) {
129
            $msg = 'Object being serialised not instance of expected class, '
130
                   . $targClass . ', is actually ' . $payloadClass;
131
            throw new InvalidOperationException($msg);
132
        }
133
134
        $this->checkRelationPropertiesCached($targClass, $resourceType);
135
        /** @var ResourceProperty[] $relProp */
136
        $relProp = $this->propertiesCache[$targClass]['rel'];
137
        /** @var ResourceProperty[] $nonRelProp */
138
        $nonRelProp = $this->propertiesCache[$targClass]['nonRel'];
139
140
        $resourceSet = $resourceType->getCustomState();
141
        if (!$resourceSet instanceof ResourceSet) {
142
            throw new InvalidOperationException('');
143
        }
144
        $title = $resourceType->getName();
145
        $type = $resourceType->getFullName();
146
147
        $relativeUri = $this->getEntryInstanceKey(
148
            $res,
149
            $resourceType,
150
            $resourceSet->getName()
151
        );
152
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
153
154
        /** var $mediaLink ODataMediaLink|null */
155
        $mediaLink = null;
156
        /** var $mediaLinks ODataMediaLink[] */
157
        $mediaLinks = [];
158
        $this->writeMediaData(
159
            $res,
160
            $type,
161
            $relativeUri,
162
            $resourceType,
163
            $mediaLink,
164
            $mediaLinks
165
        );
166
167
        $propertyContent = $this->writePrimitiveProperties($res, $nonRelProp);
168
169
        $links = $this->buildLinksFromRels($entryObject, $relProp, $relativeUri);
170
171
        $odata = new ODataEntry();
172
        $odata->resourceSetName = $resourceSet->getName();
173
        $odata->id = $absoluteUri;
174
        $odata->title = new ODataTitle($title);
175
        $odata->type = new ODataCategory($type);
176
        $odata->propertyContent = $propertyContent;
177
        $odata->isMediaLinkEntry = $resourceType->isMediaLinkEntry();
178
        $odata->editLink = new ODataLink();
179
        $odata->editLink->url = $relativeUri;
180
        $odata->editLink->name = 'edit';
181
        $odata->editLink->title = $title;
182
        $odata->mediaLink = $mediaLink;
183
        $odata->mediaLinks = $mediaLinks;
184
        $odata->links = $links;
185
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
186
        $odata->baseURI = $baseURI;
187
188
        $newCount = count($this->lightStack);
189
        if ($newCount != $stackCount) {
190
            $msg = 'Should have ' . $stackCount . ' elements in stack, have ' . $newCount . ' elements';
191
            throw new InvalidOperationException($msg);
192
        }
193
        $this->lightStack[$newCount-1]['count']--;
194
        if (0 == $this->lightStack[$newCount-1]['count']) {
195
            array_pop($this->lightStack);
196
        }
197
        return $odata;
198
    }
199
200
    /**
201
     * Write top level feed element.
202
     *
203
     * @param QueryResult &$entryObjects Array of entry resources to be written
204
     *
205
     * @return ODataFeed
206
     * @throws InvalidOperationException
207
     * @throws ODataException
208
     * @throws \ReflectionException
209
     */
210
    public function writeTopLevelElements(QueryResult &$entryObjects)
211
    {
212
        $res = $entryObjects->results;
213
        if (!(is_array($res) || $res instanceof Collection)) {
214
            throw new InvalidOperationException('!is_array($entryObjects->results)');
215
        }
216
        if (is_array($res) && 0 == count($res)) {
217
            $entryObjects->hasMore = false;
218
        }
219
        if ($res instanceof Collection && 0 == $res->count()) {
220
            $entryObjects->hasMore = false;
221
        }
222
223
        $this->loadStackIfEmpty();
224
225
        $title = $this->getRequest()->getContainerName();
226
        $relativeUri = $this->getRequest()->getIdentifier();
227
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
228
229
        $selfLink = new ODataLink();
230
        $selfLink->name = 'self';
231
        $selfLink->title = $relativeUri;
232
        $selfLink->url = $relativeUri;
233
234
        $odata = new ODataFeed();
235
        $odata->title = new ODataTitle($title);
236
        $odata->id = $absoluteUri;
237
        $odata->selfLink = $selfLink;
238
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
239
        $odata->baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
240
        $this->isBaseWritten = true;
241
242
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
243
            $odata->rowCount = $this->getRequest()->getCountValue();
244
        }
245
        $this->buildEntriesFromElements($res, $odata);
246
247
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
248
        $requestTop = $this->getRequest()->getTopOptionCount();
249
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
250
        $requestTop = (null === $requestTop) ? $pageSize+1 : $requestTop;
251
252
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
253
            $this->buildNextPageLink($entryObjects, $odata);
254
        }
255
256
        return $odata;
257
    }
258
259
    /**
260
     * Write top level url element.
261
     *
262
     * @param QueryResult $entryObject The entry resource whose url to be written
263
     *
264
     * @return ODataURL
265
     * @throws InvalidOperationException
266
     * @throws ODataException
267
     * @throws \ReflectionException
268
     */
269
    public function writeUrlElement(QueryResult $entryObject)
270
    {
271
        $url = new ODataURL();
272
        /** @var Model|null $res */
273
        $res = $entryObject->results;
274
        if (null !== $res) {
275
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
276
            $relativeUri = $this->getEntryInstanceKey(
277
                $res,
278
                $currentResourceType,
279
                $this->getCurrentResourceSetWrapper()->getName()
280
            );
281
282
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
283
        }
284
285
        return $url;
286
    }
287
288
    /**
289
     * Write top level url collection.
290
     *
291
     * @param QueryResult $entryObjects Array of entry resources whose url to be written
292
     *
293
     * @return ODataURLCollection
294
     * @throws InvalidOperationException
295
     * @throws ODataException
296
     * @throws \ReflectionException
297
     */
298
    public function writeUrlElements(QueryResult $entryObjects)
299
    {
300
        $urls = new ODataURLCollection();
301
        if (!empty($entryObjects->results)) {
302
            $i = 0;
303
            foreach ($entryObjects->results as $entryObject) {
304
                if (!$entryObject instanceof QueryResult) {
305
                    $query = new QueryResult();
306
                    $query->results = $entryObject;
307
                } else {
308
                    $query = $entryObject;
309
                }
310
                $urls->urls[$i] = $this->writeUrlElement($query);
311
                ++$i;
312
            }
313
314
            if ($i > 0 && true === $entryObjects->hasMore) {
315
                $this->buildNextPageLink($entryObjects, $urls);
316
            }
317
        }
318
319
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
320
            $urls->count = $this->getRequest()->getCountValue();
321
        }
322
323
        return $urls;
324
    }
325
326
    /**
327
     * Write top level complex resource.
328
     *
329
     * @param QueryResult  &$complexValue The complex object to be written
330
     * @param string       $propertyName  The name of the complex property
331
     * @param ResourceType &$resourceType Describes the type of complex object
332
     *
333
     * @return ODataPropertyContent
334
     * @throws InvalidOperationException
335
     * @throws \ReflectionException
336
     */
337
    public function writeTopLevelComplexObject(QueryResult &$complexValue, $propertyName, ResourceType &$resourceType)
338
    {
339
        $result = $complexValue->results;
340
341
        $propertyContent = new ODataPropertyContent();
342
        $odataProperty = new ODataProperty();
343
        $odataProperty->name = $propertyName;
344
        $odataProperty->typeName = $resourceType->getFullName();
345
        if (null != $result) {
346
            $internalContent = $this->writeComplexValue($resourceType, $result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type array<mixed,object>; however, parameter $result of AlgoWeb\PODataLaravel\Se...er::writeComplexValue() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

346
            $internalContent = $this->writeComplexValue($resourceType, /** @scrutinizer ignore-type */ $result);
Loading history...
347
            $odataProperty->value = $internalContent;
348
        }
349
350
        $propertyContent->properties[$propertyName] = $odataProperty;
351
352
        return $propertyContent;
353
    }
354
355
    /**
356
     * Write top level bag resource.
357
     *
358
     * @param QueryResult  &$BagValue     The bag object to be
359
     *                                    written
360
     * @param string       $propertyName  The name of the
361
     *                                    bag property
362
     * @param ResourceType &$resourceType Describes the type of
363
     *                                    bag object
364
     *
365
     * @return ODataPropertyContent
366
     * @throws InvalidOperationException
367
     * @throws \ReflectionException
368
     */
369
    public function writeTopLevelBagObject(QueryResult &$BagValue, $propertyName, ResourceType &$resourceType)
370
    {
371
        $result = $BagValue->results;
372
373
        $propertyContent = new ODataPropertyContent();
374
        $odataProperty = new ODataProperty();
375
        $odataProperty->name = $propertyName;
376
        $odataProperty->typeName = 'Collection(' . $resourceType->getFullName() . ')';
377
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
378
379
        $propertyContent->properties[$propertyName] = $odataProperty;
380
        return $propertyContent;
381
    }
382
383
    /**
384
     * Write top level primitive value.
385
     *
386
     * @param  QueryResult          &$primitiveValue   The primitive value to be
387
     *                                                 written
388
     * @param  ResourceProperty     &$resourceProperty Resource property describing the
389
     *                                                 primitive property to be written
390
     * @return ODataPropertyContent
391
     * @throws InvalidOperationException
392
     * @throws \ReflectionException
393
     */
394
    public function writeTopLevelPrimitive(QueryResult &$primitiveValue, ResourceProperty &$resourceProperty = null)
395
    {
396
        if (null === $resourceProperty) {
397
            throw new InvalidOperationException('Resource property must not be null');
398
        }
399
        $propertyContent = new ODataPropertyContent();
400
401
        $odataProperty = new ODataProperty();
402
        $odataProperty->name = $resourceProperty->getName();
403
        $iType = $resourceProperty->getInstanceType();
404
        if (!$iType instanceof IType) {
405
            throw new InvalidOperationException(get_class($iType));
406
        }
407
        $odataProperty->typeName = $iType->getFullTypeName();
408
        if (null == $primitiveValue->results) {
409
            $odataProperty->value = null;
410
        } else {
411
            $rType = $resourceProperty->getResourceType()->getInstanceType();
412
            if (!$rType instanceof IType) {
413
                throw new InvalidOperationException(get_class($rType));
414
            }
415
            $odataProperty->value = $this->primitiveToString($rType, $primitiveValue->results);
416
        }
417
418
        $propertyContent->properties[$odataProperty->name] = $odataProperty;
419
420
        return $propertyContent;
421
    }
422
423
    /**
424
     * Get update timestamp.
425
     *
426
     * @return Carbon
427
     */
428
    public function getUpdated()
429
    {
430
        return $this->updated;
431
    }
432
433
    /**
434
     * @param Model $entityInstance
435
     * @param ResourceType $resourceType
436
     * @param string $containerName
437
     * @return string
438
     * @throws InvalidOperationException
439
     * @throws ODataException
440
     * @throws \ReflectionException
441
     */
442
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
443
    {
444
        $typeName = $resourceType->getName();
445
        $keyProperties = $resourceType->getKeyProperties();
446
        if (0 == count($keyProperties)) {
447
            throw new InvalidOperationException('count($keyProperties) == 0');
448
        }
449
        $keyString = $containerName . '(';
450
        $comma = null;
451
        foreach ($keyProperties as $keyName => $resourceProperty) {
452
            $keyType = $resourceProperty->getInstanceType();
453
            if (!$keyType instanceof IType) {
454
                throw new InvalidOperationException('$keyType not instanceof IType');
455
            }
456
            $keyName = $resourceProperty->getName();
457
            $keyValue = $entityInstance->$keyName;
458
            if (!isset($keyValue)) {
459
                throw ODataException::createInternalServerError(
460
                    Messages::badQueryNullKeysAreNotSupported($typeName, $keyName)
461
                );
462
            }
463
464
            $keyValue = $keyType->convertToOData($keyValue);
465
            $keyString .= $comma . $keyName . '=' . $keyValue;
466
            $comma = ',';
467
        }
468
469
        $keyString .= ')';
470
471
        return $keyString;
472
    }
473
474
    /**
475
     * @param $entryObject
476
     * @param $type
477
     * @param $relativeUri
478
     * @param ResourceType $resourceType
479
     * @param ODataMediaLink|null $mediaLink
480
     * @param ODataMediaLink[] $mediaLinks
481
     * @return void
482
     * @throws InvalidOperationException
483
     */
484
    protected function writeMediaData(
485
        $entryObject,
486
        $type,
487
        $relativeUri,
488
        ResourceType $resourceType,
489
        ODataMediaLink &$mediaLink = null,
490
        array &$mediaLinks = []
491
    ) {
492
        $context = $this->getService()->getOperationContext();
493
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
494
        if (null == $streamProviderWrapper) {
495
            throw new InvalidOperationException('Retrieved stream provider must not be null');
496
        }
497
498
        /** @var ODataMediaLink|null $mediaLink */
499
        $mediaLink = null;
500
        if ($resourceType->isMediaLinkEntry()) {
501
            $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()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

501
            /** @scrutinizer ignore-call */ 
502
            $eTag = $streamProviderWrapper->getStreamETag2($entryObject, null, $context);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
502
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
503
        }
504
        /** @var ODataMediaLink[] $mediaLinks */
505
        $mediaLinks = [];
506
        if ($resourceType->hasNamedStream()) {
507
            $namedStreams = $resourceType->getAllNamedStreams();
508
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
509
                $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 getReadStreamUri()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

509
                /** @scrutinizer ignore-call */ 
510
                $readUri = $streamProviderWrapper->getReadStreamUri2(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
510
                    $entryObject,
511
                    $resourceStreamInfo,
512
                    $context,
513
                    $relativeUri
514
                );
515
                $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()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

515
                /** @scrutinizer ignore-call */ 
516
                $mediaContentType = $streamProviderWrapper->getStreamContentType2(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
516
                    $entryObject,
517
                    $resourceStreamInfo,
518
                    $context
519
                );
520
                $eTag = $streamProviderWrapper->getStreamETag2(
521
                    $entryObject,
522
                    $resourceStreamInfo,
523
                    $context
524
                );
525
526
                $nuLink = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
527
                $mediaLinks[] = $nuLink;
528
            }
529
        }
530
    }
531
532
    /**
533
     * Wheter next link is needed for the current resource set (feed)
534
     * being serialized.
535
     *
536
     * @param int $resultSetCount Number of entries in the current
537
     *                            resource set
538
     *
539
     * @return bool true if the feed must have a next page link
540
     * @throws InvalidOperationException
541
     */
542
    protected function needNextPageLink($resultSetCount)
543
    {
544
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
545
        $recursionLevel = count($this->getStack()->getSegmentNames());
546
        $pageSize = $currentResourceSet->getResourceSetPageSize();
547
548
        if (1 == $recursionLevel) {
549
            //presence of $top option affect next link for root container
550
            $topValueCount = $this->getRequest()->getTopOptionCount();
551
            if (null !== $topValueCount && ($topValueCount <= $pageSize)) {
552
                return false;
553
            }
554
        }
555
        return $resultSetCount == $pageSize;
556
    }
557
558
    /**
559
     * Get next page link from the given entity instance.
560
     *
561
     * @param  mixed          &$lastObject Last object serialized to be
562
     *                                     used for generating
563
     *                                     $skiptoken
564
     * @throws ODataException
565
     * @return string         for the link for next page
566
     * @throws InvalidOperationException
567
     */
568
    protected function getNextLinkUri(&$lastObject)
569
    {
570
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
571
        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
572
        if (null === $internalOrderByInfo) {
573
            throw new InvalidOperationException('Null');
574
        }
575
        if (!$internalOrderByInfo instanceof InternalOrderByInfo) {
0 ignored issues
show
introduced by
$internalOrderByInfo is always a sub-type of POData\UriProcessor\Quer...ser\InternalOrderByInfo.
Loading history...
576
            throw new InvalidOperationException(get_class($internalOrderByInfo));
577
        }
578
        $numSegments = count($internalOrderByInfo->getOrderByPathSegments());
579
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
580
581
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
582
        if (empty($skipToken)) {
583
            throw new InvalidOperationException('!is_null($skipToken)');
584
        }
585
        $token = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
586
        $skipToken = (1 < $numSegments) ? $skipToken : intval(trim($skipToken, '\''));
587
        $skipToken = '?' . $queryParameterString . $token . $skipToken;
588
589
        return $skipToken;
590
    }
591
592
    /**
593
     * Builds the string corresponding to query parameters for top level results
594
     * (result set identified by the resource path) to be put in next page link.
595
     *
596
     * @return string|null string representing the query parameters in the URI
597
     *                     query parameter format, NULL if there
598
     *                     is no query parameters
599
     *                     required for the next link of top level result set
600
     * @throws InvalidOperationException
601
     */
602
    protected function getNextPageLinkQueryParametersForRootResourceSet()
603
    {
604
        /** @var string|null $queryParameterString */
605
        $queryParameterString = null;
606
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
607
                     ODataConstants::HTTPQUERY_STRING_EXPAND,
608
                     ODataConstants::HTTPQUERY_STRING_ORDERBY,
609
                     ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
610
                     ODataConstants::HTTPQUERY_STRING_SELECT, ] as $queryOption) {
611
            /** @var string|null $value */
612
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
613
            if (null !== $value) {
614
                if (null !== $queryParameterString) {
615
                    $queryParameterString = /** @scrutinizer ignore-type */$queryParameterString . '&';
616
                }
617
618
                $queryParameterString .= $queryOption . '=' . $value;
619
            }
620
        }
621
622
        $topCountValue = $this->getRequest()->getTopOptionCount();
623
        if (null !== $topCountValue) {
624
            $remainingCount = $topCountValue-$this->getRequest()->getTopCount();
625
            if (0 < $remainingCount) {
626
                if (null !== $queryParameterString) {
627
                    $queryParameterString .= '&';
628
                }
629
630
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
631
            }
632
        }
633
634
        if (null !== $queryParameterString) {
635
            $queryParameterString .= '&';
636
        }
637
638
        return $queryParameterString;
639
    }
640
641
    /**
642
     * @param QueryResult $entryObject
643
     * @param ResourceProperty $prop
644
     * @param $nuLink
645
     * @param $propKind
646
     * @param $propName
647
     * @throws InvalidOperationException
648
     * @throws ODataException
649
     * @throws \ReflectionException
650
     */
651
    private function expandNavigationProperty(
652
        QueryResult $entryObject,
653
        ResourceProperty $prop,
654
        $nuLink,
655
        $propKind,
656
        $propName
657
    ) {
658
        $nextName = $prop->getResourceType()->getName();
659
        $nuLink->isExpanded = true;
660
        $value = $entryObject->results->$propName;
661
        $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind;
662
        $nuLink->isCollection = $isCollection;
663
664
        if (is_array($value)) {
665
            if (1 == count($value) && !$isCollection) {
666
                $value = $value[0];
667
            } else {
668
                $value = collect($value);
669
            }
670
        }
671
672
        $result = new QueryResult();
673
        $result->results = $value;
674
        $nullResult = null === $value;
675
        $isSingleton = $value instanceof Model;
676
        $resultCount = $nullResult ? 0 : ($isSingleton ? 1 : $value->count());
677
678
        if (0 < $resultCount) {
679
            $newStackLine = ['type' => $nextName, 'prop' => $propName, 'count' => $resultCount];
680
            array_push($this->lightStack, $newStackLine);
681
            if (!$isCollection) {
682
                $nuLink->type = 'application/atom+xml;type=entry';
683
                $expandedResult = $this->writeTopLevelElement($result);
684
            } else {
685
                $nuLink->type = 'application/atom+xml;type=feed';
686
                $expandedResult = $this->writeTopLevelElements($result);
687
            }
688
            $nuLink->expandedResult = $expandedResult;
689
        } else {
690
            $type = $this->getService()->getProvidersWrapper()->resolveResourceType($nextName);
691
            if (!$isCollection) {
692
                $result = new ODataEntry();
693
                $result->resourceSetName = $type->getName();
694
            } else {
695
                $result = new ODataFeed();
696
                $result->selfLink = new ODataLink();
697
                $result->selfLink->name = ODataConstants::ATOM_SELF_RELATION_ATTRIBUTE_VALUE;
698
            }
699
            $nuLink->expandedResult = $result;
700
        }
701
        if (isset($nuLink->expandedResult->selfLink)) {
702
            $nuLink->expandedResult->selfLink->title = $propName;
703
            $nuLink->expandedResult->selfLink->url = $nuLink->url;
704
            $nuLink->expandedResult->title = new ODataTitle($propName);
705
            $nuLink->expandedResult->id = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->url;
706
        }
707
        if (!isset($nuLink->expandedResult)) {
708
            throw new InvalidOperationException('');
709
        }
710
    }
711
712
    public static function isMatchPrimitive($resourceKind)
713
    {
714
        if (16 > $resourceKind) {
715
            return false;
716
        }
717
        if (28 < $resourceKind) {
718
            return false;
719
        }
720
        return 0 == ($resourceKind % 4);
721
    }
722
723
    /**
724
     * @param ResourceEntityType $resourceType
725
     * @param $payloadClass
726
     * @return ResourceEntityType|ResourceType
727
     * @throws InvalidOperationException
728
     * @throws \ReflectionException
729
     */
730
    protected function getConcreteTypeFromAbstractType(ResourceEntityType $resourceType, $payloadClass)
731
    {
732
        if ($resourceType->isAbstract()) {
733
            $derived = $this->getMetadata()->getDerivedTypes($resourceType);
734
            if (0 == count($derived)) {
735
                throw new InvalidOperationException('Supplied abstract type must have at least one derived type');
736
            }
737
            foreach ($derived as $rawType) {
738
                if (!$rawType->isAbstract()) {
739
                    $name = $rawType->getInstanceType()->getName();
740
                    if ($payloadClass == $name) {
741
                        $resourceType = $rawType;
742
                        break;
743
                    }
744
                }
745
            }
746
        }
747
        // despite all set up, checking, etc, if we haven't picked a concrete resource type,
748
        // wheels have fallen off, so blow up
749
        if ($resourceType->isAbstract()) {
750
            throw new InvalidOperationException('Concrete resource type not selected for payload ' . $payloadClass);
751
        }
752
        return $resourceType;
753
    }
754
755
    /**
756
     * @param QueryResult $entryObject
757
     * @param array $relProp
758
     * @param $relativeUri
759
     * @return array
760
     * @throws InvalidOperationException
761
     * @throws ODataException
762
     * @throws \ReflectionException
763
     */
764
    protected function buildLinksFromRels(QueryResult $entryObject, array $relProp, $relativeUri)
765
    {
766
        $links = [];
767
        foreach ($relProp as $prop) {
768
            $nuLink = new ODataLink();
769
            $propKind = $prop->getKind();
770
771
            if (!(ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind
772
                  || ResourcePropertyKind::RESOURCE_REFERENCE == $propKind)) {
773
                $msg = '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
774
                       . ' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE';
775
                throw new InvalidOperationException($msg);
776
            }
777
            $propTail = ResourcePropertyKind::RESOURCE_REFERENCE == $propKind ? 'entry' : 'feed';
778
            $propType = 'application/atom+xml;type=' . $propTail;
779
            $propName = $prop->getName();
780
            $nuLink->title = $propName;
781
            $nuLink->name = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
782
            $nuLink->url = $relativeUri . '/' . $propName;
783
            $nuLink->type = $propType;
784
            $nuLink->isExpanded = false;
785
            $nuLink->isCollection = 'feed' === $propTail;
786
787
            $shouldExpand = $this->shouldExpandSegment($propName);
788
789
            $navProp = new ODataNavigationPropertyInfo($prop, $shouldExpand);
790
            if ($navProp->expanded) {
791
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
792
            }
793
            $nuLink->isExpanded = isset($nuLink->expandedResult);
794
            if (null === $nuLink->isCollection) {
795
                throw new InvalidOperationException('');
796
            }
797
798
            $links[] = $nuLink;
799
        }
800
        return $links;
801
    }
802
803
    /**
804
     * @param QueryResult $entryObjects
805
     * @param ODataURLCollection $odata
806
     * @throws InvalidOperationException
807
     * @throws ODataException
808
     */
809
    protected function buildUrlsNextPageLink(QueryResult $entryObjects, ODataURLCollection $odata)
810
    {
811
        $this->buildNextPageLink($entryObjects, $odata);
812
    }
813
814
    /**
815
     * @param QueryResult $entryObjects
816
     * @param ODataFeed $odata
817
     * @throws InvalidOperationException
818
     * @throws ODataException
819
     */
820
    protected function buildFeedNextPageLink(QueryResult &$entryObjects, ODataFeed $odata)
821
    {
822
        $this->buildNextPageLink($entryObjects, $odata);
823
    }
824
825
    /**
826
     * @param QueryResult $entryObjects
827
     * @param ODataURLCollection|ODataFeed $odata
828
     * @throws InvalidOperationException
829
     * @throws ODataException
830
     */
831
    protected function buildNextPageLink(QueryResult $entryObjects, $odata)
832
    {
833
        $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
834
        $lastObject = end($entryObjects->results);
0 ignored issues
show
Bug introduced by
It seems like $entryObjects->results can also be of type null and object; however, parameter $array of end() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

834
        $lastObject = end(/** @scrutinizer ignore-type */ $entryObjects->results);
Loading history...
835
        $segment = $this->getNextLinkUri($lastObject);
836
        $nextLink = new ODataLink();
837
        $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
838
        $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
839
        $odata->nextPageLink = $nextLink;
840
    }
841
842
    /**
843
     * @param $res
844
     * @param ODataFeed $odata
845
     * @throws InvalidOperationException
846
     * @throws ODataException
847
     * @throws \ReflectionException
848
     */
849
    protected function buildEntriesFromElements($res, ODataFeed $odata)
850
    {
851
        foreach ($res as $entry) {
852
            if (!$entry instanceof QueryResult) {
853
                $query = new QueryResult();
854
                $query->results = $entry;
855
            } else {
856
                $query = $entry;
857
            }
858
            if (!$query instanceof QueryResult) {
859
                throw new InvalidOperationException(get_class($query));
860
            }
861
            $odata->entries[] = $this->writeTopLevelElement($query);
862
        }
863
    }
864
}
865