Passed
Pull Request — master (#171)
by Alex
04:02
created

getNextPageLinkQueryParametersForRootResourceSet()   B

Complexity

Conditions 8
Paths 32

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 35
rs 8.4444
c 0
b 0
f 0
cc 8
nc 32
nop 0
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
    private $propertiesCache = [];
52
53
    /**
54
     * @var RootProjectionNode
55
     */
56
    private $rootNode = null;
57
58
    /**
59
     * The service implementation.
60
     *
61
     * @var IService
62
     */
63
    protected $service;
64
65
    /**
66
     * Request description instance describes OData request the
67
     * the client has submitted and result of the request.
68
     *
69
     * @var RequestDescription
70
     */
71
    protected $request;
72
73
    /**
74
     * Collection of complex type instances used for cycle detection.
75
     *
76
     * @var array
77
     */
78
    protected $complexTypeInstanceCollection;
79
80
    /**
81
     * Absolute service Uri.
82
     *
83
     * @var string
84
     */
85
    protected $absoluteServiceUri;
86
87
    /**
88
     * Absolute service Uri with slash.
89
     *
90
     * @var string
91
     */
92
    protected $absoluteServiceUriWithSlash;
93
94
    /**
95
     * Holds reference to segment stack being processed.
96
     *
97
     * @var SegmentStack
98
     */
99
    protected $stack;
100
101
    /**
102
     * Lightweight stack tracking for recursive descent fill.
103
     */
104
    protected $lightStack = [];
105
106
    /**
107
     * @var ModelSerialiser
108
     */
109
    private $modelSerialiser;
110
111
112
113
    /**
114
     * @var IMetadataProvider
115
     */
116
    private $metaProvider;
117
118
    /*
119
     * Update time to insert into ODataEntry/ODataFeed fields
120
     * @var Carbon;
121
     */
122
    private $updated;
123
124
    /*
125
     * Has base URI already been written out during serialisation?
126
     * @var bool;
127
     */
128
    private $isBaseWritten = false;
129
130
    /**
131
     * @param IService                $service Reference to the data service instance
132
     * @param RequestDescription|null $request Type instance describing the client submitted request
133
     */
134
    public function __construct(IService $service, RequestDescription $request = null)
135
    {
136
        $this->service = $service;
137
        $this->request = $request;
138
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
139
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
140
        $this->stack = new SegmentStack($request);
141
        $this->complexTypeInstanceCollection = [];
142
        $this->modelSerialiser = new ModelSerialiser();
143
        $this->updated = Carbon::now();
144
    }
145
146
    /**
147
     * Write a top level entry resource.
148
     *
149
     * @param QueryResult $entryObject Reference to the entry object to be written
150
     *
151
     * @return ODataEntry|null
152
     */
153
    public function writeTopLevelElement(QueryResult $entryObject)
154
    {
155
        if (!isset($entryObject->results)) {
156
            array_pop($this->lightStack);
157
            return null;
158
        }
159
        if (!$entryObject->results instanceof Model) {
160
            $res = $entryObject->results;
161
            $msg = is_array($res) ? 'Entry object must be single Model' : get_class($res);
162
            throw new InvalidOperationException($msg);
163
        }
164
165
        $this->loadStackIfEmpty();
166
167
        $baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
168
        $this->isBaseWritten = true;
169
170
        $stackCount = count($this->lightStack);
171
        $topOfStack = $this->lightStack[$stackCount-1];
172
        $payloadClass = get_class($entryObject->results);
173
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
174
175
        // need gubbinz to unpack an abstract resource type
176
        $resourceType = $this->getConcreteTypeFromAbstractType($resourceType, $payloadClass);
177
178
        // make sure we're barking up right tree
179
        if (!$resourceType instanceof ResourceEntityType) {
180
            throw new InvalidOperationException(get_class($resourceType));
181
        }
182
        $targClass = $resourceType->getInstanceType()->getName();
183
        if (!($entryObject->results instanceof $targClass)) {
184
            $msg = 'Object being serialised not instance of expected class, '
185
                   . $targClass . ', is actually ' . $payloadClass;
186
            throw new InvalidOperationException($msg);
187
        }
188
189
        if (!array_key_exists($targClass, $this->propertiesCache)) {
190
            $rawProp = $resourceType->getAllProperties();
191
            $relProp = [];
192
            $nonRelProp = [];
193
            foreach ($rawProp as $prop) {
194
                $propType = $prop->getResourceType();
195
                if ($propType instanceof ResourceEntityType) {
196
                    $relProp[] = $prop;
197
                } else {
198
                    $nonRelProp[$prop->getName()] = ['prop' => $prop, 'type' => $propType->getInstanceType()];
199
                }
200
            }
201
            $this->propertiesCache[$targClass] = ['rel' => $relProp, 'nonRel' => $nonRelProp];
202
        }
203
        unset($relProp);
204
        unset($nonRelProp);
205
        $relProp = $this->propertiesCache[$targClass]['rel'];
206
        $nonRelProp = $this->propertiesCache[$targClass]['nonRel'];
207
208
        $resourceSet = $resourceType->getCustomState();
209
        if (!$resourceSet instanceof ResourceSet) {
210
            throw new InvalidOperationException('');
211
        }
212
        $title = $resourceType->getName();
213
        $type = $resourceType->getFullName();
214
215
        $relativeUri = $this->getEntryInstanceKey(
216
            $entryObject->results,
217
            $resourceType,
218
            $resourceSet->getName()
219
        );
220
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
221
222
        list($mediaLink, $mediaLinks) = $this->writeMediaData(
223
            $entryObject->results,
224
            $type,
225
            $relativeUri,
226
            $resourceType
227
        );
228
229
        $propertyContent = $this->writePrimitiveProperties($entryObject->results, $nonRelProp);
230
231
        $links = [];
232
        foreach ($relProp as $prop) {
233
            $nuLink = new ODataLink();
234
            $propKind = $prop->getKind();
235
236
            if (!(ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind
237
                  || ResourcePropertyKind::RESOURCE_REFERENCE == $propKind)) {
238
                $msg = '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
239
                       .' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE';
240
                throw new InvalidOperationException($msg);
241
            }
242
            $propTail = ResourcePropertyKind::RESOURCE_REFERENCE == $propKind ? 'entry' : 'feed';
243
            $propType = 'application/atom+xml;type=' . $propTail;
244
            $propName = $prop->getName();
245
            $nuLink->title = $propName;
246
            $nuLink->name = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
247
            $nuLink->url = $relativeUri . '/' . $propName;
248
            $nuLink->type = $propType;
249
            $nuLink->isExpanded = false;
250
            $nuLink->isCollection = 'feed' === $propTail;
251
252
            $shouldExpand = $this->shouldExpandSegment($propName);
253
254
            $navProp = new ODataNavigationPropertyInfo($prop, $shouldExpand);
255
            if ($navProp->expanded) {
256
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
257
            }
258
            $nuLink->isExpanded = isset($nuLink->expandedResult);
259
            if (null === $nuLink->isCollection) {
260
                throw new InvalidOperationException('');
261
            }
262
263
            $links[] = $nuLink;
264
        }
265
266
        $odata = new ODataEntry();
267
        $odata->resourceSetName = $resourceSet->getName();
268
        $odata->id = $absoluteUri;
269
        $odata->title = new ODataTitle($title);
270
        $odata->type = new ODataCategory($type);
271
        $odata->propertyContent = $propertyContent;
272
        $odata->isMediaLinkEntry = $resourceType->isMediaLinkEntry();
273
        $odata->editLink = new ODataLink();
274
        $odata->editLink->url = $relativeUri;
275
        $odata->editLink->name = 'edit';
276
        $odata->editLink->title = $title;
277
        $odata->mediaLink = $mediaLink;
0 ignored issues
show
Documentation Bug introduced by
It seems like $mediaLink can also be of type array. However, the property $mediaLink is declared as type POData\ObjectModel\ODataMediaLink. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
278
        $odata->mediaLinks = $mediaLinks;
0 ignored issues
show
Documentation Bug introduced by
It seems like $mediaLinks can also be of type POData\ObjectModel\ODataMediaLink. However, the property $mediaLinks is declared as type POData\ObjectModel\ODataMediaLink[]. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
279
        $odata->links = $links;
280
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
281
        $odata->baseURI = $baseURI;
282
283
        $newCount = count($this->lightStack);
284
        if ($newCount != $stackCount) {
285
            $msg = 'Should have ' . $stackCount . ' elements in stack, have ' . $newCount . ' elements';
286
            throw new InvalidOperationException($msg);
287
        }
288
        $this->lightStack[$newCount-1]['count']--;
289
        if (0 == $this->lightStack[$newCount-1]['count']) {
290
            array_pop($this->lightStack);
291
        }
292
        return $odata;
293
    }
294
295
    /**
296
     * Write top level feed element.
297
     *
298
     * @param QueryResult &$entryObjects Array of entry resources to be written
299
     *
300
     * @return ODataFeed
301
     */
302
    public function writeTopLevelElements(QueryResult & $entryObjects)
303
    {
304
        $res = $entryObjects->results;
305
        if (!(is_array($res) || $res instanceof Collection)) {
306
            throw new InvalidOperationException('!is_array($entryObjects->results)');
307
        }
308
        if (is_array($res) && 0 == count($res)) {
309
            $entryObjects->hasMore = false;
310
        }
311
        if ($res instanceof Collection && 0 == $res->count()) {
312
            $entryObjects->hasMore = false;
313
        }
314
315
        $this->loadStackIfEmpty();
316
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
317
318
        $title = $this->getRequest()->getContainerName();
319
        $relativeUri = $this->getRequest()->getIdentifier();
320
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
321
322
        $selfLink = new ODataLink();
323
        $selfLink->name = 'self';
324
        $selfLink->title = $relativeUri;
325
        $selfLink->url = $relativeUri;
326
327
        $odata = new ODataFeed();
328
        $odata->title = new ODataTitle($title);
329
        $odata->id = $absoluteUri;
330
        $odata->selfLink = $selfLink;
331
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
332
        $odata->baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
333
        $this->isBaseWritten = true;
334
335
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
336
            $odata->rowCount = $this->getRequest()->getCountValue();
337
        }
338
        foreach ($res as $entry) {
339
            if (!$entry instanceof QueryResult) {
340
                $query = new QueryResult();
341
                $query->results = $entry;
342
            } else {
343
                $query = $entry;
344
            }
345
            if (!$query instanceof QueryResult) {
346
                throw new InvalidOperationException(get_class($query));
347
            }
348
            if (!$query->results instanceof Model) {
349
                $res = $query->results;
350
                $msg = is_array($res) ? 'Entry object must be single Model' : get_class($res);
351
                throw new InvalidOperationException($msg);
352
            }
353
            $odata->entries[] = $this->writeTopLevelElement($query);
354
        }
355
356
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
357
        $requestTop = $this->getRequest()->getTopOptionCount();
358
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
359
        $requestTop = (null === $requestTop) ? $pageSize+1 : $requestTop;
360
361
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
362
            $stackSegment = $setName;
363
            $lastObject = end($entryObjects->results);
364
            $segment = $this->getNextLinkUri($lastObject);
365
            $nextLink = new ODataLink();
366
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
367
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
368
            $odata->nextPageLink = $nextLink;
369
        }
370
371
        return $odata;
372
    }
373
374
    /**
375
     * Write top level url element.
376
     *
377
     * @param QueryResult $entryObject The entry resource whose url to be written
378
     *
379
     * @return ODataURL
380
     */
381
    public function writeUrlElement(QueryResult $entryObject)
382
    {
383
        $url = new ODataURL();
384
        if (null !== $entryObject->results) {
385
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
386
            $relativeUri = $this->getEntryInstanceKey(
387
                $entryObject->results,
388
                $currentResourceType,
389
                $this->getCurrentResourceSetWrapper()->getName()
390
            );
391
392
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
393
        }
394
395
        return $url;
396
    }
397
398
    /**
399
     * Write top level url collection.
400
     *
401
     * @param QueryResult $entryObjects Array of entry resources whose url to be written
402
     *
403
     * @return ODataURLCollection
404
     */
405
    public function writeUrlElements(QueryResult $entryObjects)
406
    {
407
        $urls = new ODataURLCollection();
408
        if (!empty($entryObjects->results)) {
409
            $i = 0;
410
            foreach ($entryObjects->results as $entryObject) {
411
                if (!$entryObject instanceof QueryResult) {
412
                    $query = new QueryResult();
413
                    $query->results = $entryObject;
414
                } else {
415
                    $query = $entryObject;
416
                }
417
                $urls->urls[$i] = $this->writeUrlElement($query);
418
                ++$i;
419
            }
420
421
            if ($i > 0 && true === $entryObjects->hasMore) {
422
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
423
                $lastObject = end($entryObjects->results);
0 ignored issues
show
Bug introduced by
It seems like $entryObjects->results can also be of type 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

423
                $lastObject = end(/** @scrutinizer ignore-type */ $entryObjects->results);
Loading history...
424
                $segment = $this->getNextLinkUri($lastObject);
425
                $nextLink = new ODataLink();
426
                $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
427
                $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
428
                $urls->nextPageLink = $nextLink;
429
            }
430
        }
431
432
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
433
            $urls->count = $this->getRequest()->getCountValue();
434
        }
435
436
        return $urls;
437
    }
438
439
    /**
440
     * Write top level complex resource.
441
     *
442
     * @param QueryResult  &$complexValue The complex object to be written
443
     * @param string       $propertyName  The name of the complex property
444
     * @param ResourceType &$resourceType Describes the type of complex object
445
     *
446
     * @return ODataPropertyContent
447
     */
448
    public function writeTopLevelComplexObject(QueryResult & $complexValue, $propertyName, ResourceType & $resourceType)
449
    {
450
        $result = $complexValue->results;
451
452
        $propertyContent = new ODataPropertyContent();
453
        $odataProperty = new ODataProperty();
454
        $odataProperty->name = $propertyName;
455
        $odataProperty->typeName = $resourceType->getFullName();
456
        if (null != $result) {
457
            $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

457
            $internalContent = $this->writeComplexValue($resourceType, /** @scrutinizer ignore-type */ $result);
Loading history...
458
            $odataProperty->value = $internalContent;
459
        }
460
461
        $propertyContent->properties[$propertyName] = $odataProperty;
462
463
        return $propertyContent;
464
    }
465
466
    /**
467
     * Write top level bag resource.
468
     *
469
     * @param QueryResult  &$BagValue     The bag object to be
470
     *                                    written
471
     * @param string       $propertyName  The name of the
472
     *                                    bag property
473
     * @param ResourceType &$resourceType Describes the type of
474
     *                                    bag object
475
     *
476
     * @return ODataPropertyContent
477
     */
478
    public function writeTopLevelBagObject(QueryResult & $BagValue, $propertyName, ResourceType & $resourceType)
479
    {
480
        $result = $BagValue->results;
481
482
        $propertyContent = new ODataPropertyContent();
483
        $odataProperty = new ODataProperty();
484
        $odataProperty->name = $propertyName;
485
        $odataProperty->typeName = 'Collection(' . $resourceType->getFullName() . ')';
486
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
487
488
        $propertyContent->properties[$propertyName] = $odataProperty;
489
        return $propertyContent;
490
    }
491
492
    /**
493
     * Write top level primitive value.
494
     *
495
     * @param  QueryResult          &$primitiveValue   The primitive value to be
496
     *                                                 written
497
     * @param  ResourceProperty     &$resourceProperty Resource property describing the
498
     *                                                 primitive property to be written
499
     * @return ODataPropertyContent
500
     */
501
    public function writeTopLevelPrimitive(QueryResult & $primitiveValue, ResourceProperty & $resourceProperty = null)
502
    {
503
        if (null === $resourceProperty) {
504
            throw new InvalidOperationException('Resource property must not be null');
505
        }
506
        $propertyContent = new ODataPropertyContent();
507
508
        $odataProperty = new ODataProperty();
509
        $odataProperty->name = $resourceProperty->getName();
510
        $iType = $resourceProperty->getInstanceType();
511
        if (!$iType instanceof IType) {
512
            throw new InvalidOperationException(get_class($iType));
513
        }
514
        $odataProperty->typeName = $iType->getFullTypeName();
515
        if (null == $primitiveValue->results) {
516
            $odataProperty->value = null;
517
        } else {
518
            $rType = $resourceProperty->getResourceType()->getInstanceType();
519
            if (!$rType instanceof IType) {
520
                throw new InvalidOperationException(get_class($rType));
521
            }
522
            $odataProperty->value = $this->primitiveToString($rType, $primitiveValue->results);
523
        }
524
525
        $propertyContent->properties[$odataProperty->name] = $odataProperty;
526
527
        return $propertyContent;
528
    }
529
530
    /**
531
     * Gets reference to the request submitted by client.
532
     *
533
     * @return RequestDescription
534
     */
535
    public function getRequest()
536
    {
537
        if (null == $this->request) {
538
            throw new InvalidOperationException('Request not yet set');
539
        }
540
541
        return $this->request;
542
    }
543
544
    /**
545
     * Sets reference to the request submitted by client.
546
     *
547
     * @param RequestDescription $request
548
     */
549
    public function setRequest(RequestDescription $request)
550
    {
551
        $this->request = $request;
552
        $this->stack->setRequest($request);
553
    }
554
555
    /**
556
     * Gets the data service instance.
557
     *
558
     * @return IService
559
     */
560
    public function getService()
561
    {
562
        return $this->service;
563
    }
564
565
    /**
566
     * Gets the segment stack instance.
567
     *
568
     * @return SegmentStack
569
     */
570
    public function getStack()
571
    {
572
        return $this->stack;
573
    }
574
575
    /**
576
     * Get update timestamp.
577
     *
578
     * @return Carbon
579
     */
580
    public function getUpdated()
581
    {
582
        return $this->updated;
583
    }
584
585
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
586
    {
587
        $typeName = $resourceType->getName();
588
        $keyProperties = $resourceType->getKeyProperties();
589
        if (0 == count($keyProperties)) {
590
            throw new InvalidOperationException('count($keyProperties) == 0');
591
        }
592
        $keyString = $containerName . '(';
593
        $comma = null;
594
        foreach ($keyProperties as $keyName => $resourceProperty) {
595
            $keyType = $resourceProperty->getInstanceType();
596
            if (!$keyType instanceof IType) {
597
                throw new InvalidOperationException('$keyType not instanceof IType');
598
            }
599
            $keyName = $resourceProperty->getName();
600
            $keyValue = $entityInstance->$keyName;
601
            if (!isset($keyValue)) {
602
                throw ODataException::createInternalServerError(
603
                    Messages::badQueryNullKeysAreNotSupported($typeName, $keyName)
604
                );
605
            }
606
607
            $keyValue = $keyType->convertToOData($keyValue);
608
            $keyString .= $comma . $keyName . '=' . $keyValue;
609
            $comma = ',';
610
        }
611
612
        $keyString .= ')';
613
614
        return $keyString;
615
    }
616
617
    /**
618
     * @param $entryObject
619
     * @param $type
620
     * @param $relativeUri
621
     * @param $resourceType
622
     * @return array<ODataMediaLink|null|array>
623
     */
624
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
625
    {
626
        $context = $this->getService()->getOperationContext();
627
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
628
        if (null == $streamProviderWrapper) {
629
            throw new InvalidOperationException('Retrieved stream provider must not be null');
630
        }
631
632
        $mediaLink = null;
633
        if ($resourceType->isMediaLinkEntry()) {
634
            $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

634
            /** @scrutinizer ignore-call */ 
635
            $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...
635
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
636
        }
637
        $mediaLinks = [];
638
        if ($resourceType->hasNamedStream()) {
639
            $namedStreams = $resourceType->getAllNamedStreams();
640
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
641
                $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

641
                /** @scrutinizer ignore-call */ 
642
                $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...
642
                    $entryObject,
643
                    $resourceStreamInfo,
644
                    $context,
645
                    $relativeUri
646
                );
647
                $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

647
                /** @scrutinizer ignore-call */ 
648
                $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...
648
                    $entryObject,
649
                    $resourceStreamInfo,
650
                    $context
651
                );
652
                $eTag = $streamProviderWrapper->getStreamETag2(
653
                    $entryObject,
654
                    $resourceStreamInfo,
655
                    $context
656
                );
657
658
                $nuLink = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
659
                $mediaLinks[] = $nuLink;
660
            }
661
        }
662
        return [$mediaLink, $mediaLinks];
663
    }
664
665
    /**
666
     * Gets collection of projection nodes under the current node.
667
     *
668
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes describing projections for the current
669
     *                                                        segment, If this method returns null it means no
670
     *                                                        projections are to be applied and the entire resource for
671
     *                                                        the current segment should be serialized, If it returns
672
     *                                                        non-null only the properties described by the returned
673
     *                                                        projection segments should be serialized
674
     */
675
    protected function getProjectionNodes()
676
    {
677
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
678
        if (null === $expandedProjectionNode || $expandedProjectionNode->canSelectAllProperties()) {
679
            return null;
680
        }
681
682
        return $expandedProjectionNode->getChildNodes();
683
    }
684
685
    /**
686
     * Find a 'ExpandedProjectionNode' instance in the projection tree
687
     * which describes the current segment.
688
     *
689
     * @return null|RootProjectionNode|ExpandedProjectionNode
690
     */
691
    protected function getCurrentExpandedProjectionNode()
692
    {
693
        if (null === $this->rootNode) {
694
            $this->rootNode = $this->getRequest()->getRootProjectionNode();
695
        }
696
        $expandedProjectionNode = $this->rootNode;
697
        if (null === $expandedProjectionNode) {
698
            return null;
699
        } else {
700
            $segmentNames = $this->getLightStack();
701
            $depth = count($segmentNames);
702
            // $depth == 1 means serialization of root entry
703
            //(the resource identified by resource path) is going on,
704
            //so control won't get into the below for loop.
705
            //we will directly return the root node,
706
            //which is 'ExpandedProjectionNode'
707
            // for resource identified by resource path.
708
            if (0 != $depth) {
709
                for ($i = 2; $i < $depth; ++$i) {
710
                    $segName = $segmentNames[$i]['prop'];
711
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segName);
712
                    if (null === $expandedProjectionNode) {
713
                        throw new InvalidOperationException('is_null($expandedProjectionNode)');
714
                    }
715
                    if (!$expandedProjectionNode instanceof ExpandedProjectionNode) {
716
                        $msg = '$expandedProjectionNode not instanceof ExpandedProjectionNode';
717
                        throw new InvalidOperationException($msg);
718
                    }
719
                }
720
            }
721
        }
722
723
        return $expandedProjectionNode;
724
    }
725
726
    /**
727
     * Check whether to expand a navigation property or not.
728
     *
729
     * @param string $navigationPropertyName Name of naviagtion property in question
730
     *
731
     * @return bool True if the given navigation should be expanded, otherwise false
732
     */
733
    protected function shouldExpandSegment($navigationPropertyName)
734
    {
735
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
736
        if (null === $expandedProjectionNode) {
737
            return false;
738
        }
739
        $expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName);
740
741
        // null is a valid input to an instanceof call as of PHP 5.6 - will always return false
742
        return $expandedProjectionNode instanceof ExpandedProjectionNode;
743
    }
744
745
    /**
746
     * Wheter next link is needed for the current resource set (feed)
747
     * being serialized.
748
     *
749
     * @param int $resultSetCount Number of entries in the current
750
     *                            resource set
751
     *
752
     * @return bool true if the feed must have a next page link
753
     */
754
    protected function needNextPageLink($resultSetCount)
755
    {
756
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
757
        $recursionLevel = count($this->getStack()->getSegmentNames());
758
        $pageSize = $currentResourceSet->getResourceSetPageSize();
759
760
        if (1 == $recursionLevel) {
761
            //presence of $top option affect next link for root container
762
            $topValueCount = $this->getRequest()->getTopOptionCount();
763
            if (null !== $topValueCount && ($topValueCount <= $pageSize)) {
764
                return false;
765
            }
766
        }
767
        return $resultSetCount == $pageSize;
768
    }
769
770
    /**
771
     * Resource set wrapper for the resource being serialized.
772
     *
773
     * @return ResourceSetWrapper
774
     */
775
    protected function getCurrentResourceSetWrapper()
776
    {
777
        $segmentWrappers = $this->getStack()->getSegmentWrappers();
778
        $count = count($segmentWrappers);
779
780
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $segmentWrappers[$count-1];
781
    }
782
783
    /**
784
     * Get next page link from the given entity instance.
785
     *
786
     * @param  mixed          &$lastObject Last object serialized to be
787
     *                                     used for generating
788
     *                                     $skiptoken
789
     * @throws ODataException
790
     * @return string         for the link for next page
791
     */
792
    protected function getNextLinkUri(&$lastObject)
793
    {
794
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
795
        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
796
        if (!$internalOrderByInfo instanceof InternalOrderByInfo) {
0 ignored issues
show
introduced by
$internalOrderByInfo is always a sub-type of POData\UriProcessor\Quer...ser\InternalOrderByInfo.
Loading history...
797
            throw new InvalidOperationException(get_class($internalOrderByInfo));
798
        }
799
        $numSegments = count($internalOrderByInfo->getOrderByPathSegments());
800
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
801
802
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
803
        if (empty($skipToken)) {
804
            throw new InvalidOperationException('!is_null($skipToken)');
805
        }
806
        $token = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
807
        $skipToken = (1 < $numSegments) ? $skipToken : intval(trim($skipToken, '\''));
808
        $skipToken = '?' . $queryParameterString . $token . $skipToken;
809
810
        return $skipToken;
811
    }
812
813
    /**
814
     * Builds the string corresponding to query parameters for top level results
815
     * (result set identified by the resource path) to be put in next page link.
816
     *
817
     * @return string|null string representing the query parameters in the URI
818
     *                     query parameter format, NULL if there
819
     *                     is no query parameters
820
     *                     required for the next link of top level result set
821
     */
822
    protected function getNextPageLinkQueryParametersForRootResourceSet()
823
    {
824
        $queryParameterString = null;
825
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
826
                     ODataConstants::HTTPQUERY_STRING_EXPAND,
827
                     ODataConstants::HTTPQUERY_STRING_ORDERBY,
828
                     ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
829
                     ODataConstants::HTTPQUERY_STRING_SELECT, ] as $queryOption) {
830
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
831
            if (null !== $value) {
832
                if (null !== $queryParameterString) {
833
                    $queryParameterString = $queryParameterString . '&';
0 ignored issues
show
Bug introduced by
Are you sure $queryParameterString of type void can be used in concatenation? ( Ignorable by Annotation )

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

833
                    $queryParameterString = /** @scrutinizer ignore-type */ $queryParameterString . '&';
Loading history...
834
                }
835
836
                $queryParameterString .= $queryOption . '=' . $value;
837
            }
838
        }
839
840
        $topCountValue = $this->getRequest()->getTopOptionCount();
841
        if (null !== $topCountValue) {
842
            $remainingCount = $topCountValue-$this->getRequest()->getTopCount();
843
            if (0 < $remainingCount) {
844
                if (null !== $queryParameterString) {
845
                    $queryParameterString .= '&';
846
                }
847
848
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
849
            }
850
        }
851
852
        if (null !== $queryParameterString) {
853
            $queryParameterString .= '&';
854
        }
855
856
        return $queryParameterString;
857
    }
858
859
    private function loadStackIfEmpty()
860
    {
861
        if (0 == count($this->lightStack)) {
862
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
863
            array_push($this->lightStack, ['type' => $typeName, 'property' => $typeName, 'count' => 1]);
864
        }
865
    }
866
867
    /**
868
     * Convert the given primitive value to string.
869
     * Note: This method will not handle null primitive value.
870
     *
871
     * @param IType &$primitiveResourceType Type of the primitive property
872
     *                                      whose value need to be converted
873
     * @param mixed $primitiveValue         Primitive value to convert
874
     *
875
     * @return string
876
     */
877
    private function primitiveToString(IType & $type, $primitiveValue)
878
    {
879
        // kludge to enable switching on type of $type without getting tripped up by mocks as we would with get_class
880
        // switch (true) means we unconditionally enter, and then lean on case statements to match given block
881
        switch (true) {
882
            case $type instanceof StringType:
883
                $stringValue = utf8_encode($primitiveValue);
884
                break;
885
            case $type instanceof Boolean:
886
                $stringValue = (true === $primitiveValue) ? 'true' : 'false';
887
                break;
888
            case $type instanceof Binary:
889
                $stringValue = base64_encode($primitiveValue);
890
                break;
891
            case $type instanceof DateTime && $primitiveValue instanceof \DateTime:
892
                $stringValue = $primitiveValue->format(\DateTime::ATOM);
893
                break;
894
            default:
895
                $stringValue = strval($primitiveValue);
896
        }
897
898
        return $stringValue;
899
    }
900
901
    /**
902
     * @param $entryObject
903
     * @param $nonRelProp
904
     * @return ODataPropertyContent
905
     */
906
    private function writePrimitiveProperties(Model $entryObject, $nonRelProp)
907
    {
908
        $propertyContent = new ODataPropertyContent();
909
        $cereal = $this->getModelSerialiser()->bulkSerialise($entryObject);
910
        foreach ($cereal as $corn => $flake) {
911
            if (!array_key_exists($corn, $nonRelProp)) {
912
                continue;
913
            }
914
            $corn = strval($corn);
915
            $rType = $nonRelProp[$corn]['type'];
916
            $subProp = new ODataProperty();
917
            $subProp->name = $corn;
918
            $subProp->value = isset($flake) ? $this->primitiveToString($rType, $flake) : null;
919
            $subProp->typeName = $nonRelProp[$corn]['prop']->getResourceType()->getFullName();
920
            $propertyContent->properties[$corn] = $subProp;
921
        }
922
        return $propertyContent;
923
    }
924
925
    /**
926
     * @param $entryObject
927
     * @param $prop
928
     * @param $nuLink
929
     * @param $propKind
930
     * @param $propName
931
     */
932
    private function expandNavigationProperty(QueryResult $entryObject, $prop, $nuLink, $propKind, $propName)
933
    {
934
        $nextName = $prop->getResourceType()->getName();
935
        $nuLink->isExpanded = true;
936
        $value = $entryObject->results->$propName;
937
        $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind;
938
        $nuLink->isCollection = $isCollection;
939
940
        if (is_array($value)) {
941
            if (1 == count($value) && !$isCollection) {
942
                $value = $value[0];
943
            } else {
944
                $value = collect($value);
945
            }
946
        }
947
948
        $result = new QueryResult();
949
        $result->results = $value;
950
        $nullResult = null === $value;
951
        $isSingleton = $value instanceof Model;
952
        $resultCount = $nullResult ? 0 : ($isSingleton ? 1 : $value->count());
953
954
        if (0 < $resultCount) {
955
            $newStackLine = ['type' => $nextName, 'prop' => $propName, 'count' => $resultCount];
956
            array_push($this->lightStack, $newStackLine);
957
            if (!$isCollection) {
958
                $nuLink->type = 'application/atom+xml;type=entry';
959
                $expandedResult = $this->writeTopLevelElement($result);
960
            } else {
961
                $nuLink->type = 'application/atom+xml;type=feed';
962
                $expandedResult = $this->writeTopLevelElements($result);
963
            }
964
            $nuLink->expandedResult = $expandedResult;
965
        } else {
966
            $type = $this->getService()->getProvidersWrapper()->resolveResourceType($nextName);
967
            if (!$isCollection) {
968
                $result = new ODataEntry();
969
                $result->resourceSetName = $type->getName();
970
            } else {
971
                $result = new ODataFeed();
972
                $result->selfLink = new ODataLink();
973
                $result->selfLink->name = ODataConstants::ATOM_SELF_RELATION_ATTRIBUTE_VALUE;
974
            }
975
            $nuLink->expandedResult = $result;
976
        }
977
        if (isset($nuLink->expandedResult->selfLink)) {
978
            $nuLink->expandedResult->selfLink->title = $propName;
979
            $nuLink->expandedResult->selfLink->url = $nuLink->url;
980
            $nuLink->expandedResult->title = new ODataTitle($propName);
981
            $nuLink->expandedResult->id = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->url;
982
        }
983
        if (!isset($nuLink->expandedResult)) {
984
            throw new InvalidOperationException('');
985
        }
986
    }
987
988
    /**
989
     * Gets the data service instance.
990
     *
991
     * @return void
992
     */
993
    public function setService(IService $service)
994
    {
995
        $this->service = $service;
996
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
997
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
998
    }
999
1000
    /**
1001
     * @param ResourceType $resourceType
1002
     * @param $result
1003
     * @return ODataBagContent|null
1004
     */
1005
    protected function writeBagValue(ResourceType & $resourceType, $result)
1006
    {
1007
        if (!(null == $result || is_array($result))) {
1008
            throw new InvalidOperationException('Bag parameter must be null or array');
1009
        }
1010
        $typeKind = $resourceType->getResourceTypeKind();
1011
        if (!(ResourceTypeKind::PRIMITIVE() == $typeKind || ResourceTypeKind::COMPLEX() == $typeKind)) {
1012
            $msg = '$bagItemResourceTypeKind != ResourceTypeKind::PRIMITIVE'
1013
                   .' && $bagItemResourceTypeKind != ResourceTypeKind::COMPLEX';
1014
            throw new InvalidOperationException($msg);
1015
        }
1016
        if (null == $result) {
1017
            return null;
1018
        }
1019
        $bag = new ODataBagContent();
1020
        foreach ($result as $value) {
1021
            if (isset($value)) {
1022
                if (ResourceTypeKind::PRIMITIVE() == $typeKind) {
1023
                    $instance = $resourceType->getInstanceType();
1024
                    if (!$instance instanceof IType) {
1025
                        throw new InvalidOperationException(get_class($instance));
1026
                    }
1027
                    $bag->propertyContents[] = $this->primitiveToString($instance, $value);
1028
                } elseif (ResourceTypeKind::COMPLEX() == $typeKind) {
1029
                    $bag->propertyContents[] = $this->writeComplexValue($resourceType, $value);
1030
                }
1031
            }
1032
        }
1033
        return $bag;
1034
    }
1035
1036
    /**
1037
     * @param  ResourceType         $resourceType
1038
     * @param  object               $result
1039
     * @param  string|null          $propertyName
1040
     * @return ODataPropertyContent
1041
     */
1042
    protected function writeComplexValue(ResourceType & $resourceType, &$result, $propertyName = null)
1043
    {
1044
        if (!is_object($result)) {
1045
            throw new InvalidOperationException('Supplied $customObject must be an object');
1046
        }
1047
1048
        $count = count($this->complexTypeInstanceCollection);
1049
        for ($i = 0; $i < $count; ++$i) {
1050
            if ($this->complexTypeInstanceCollection[$i] === $result) {
1051
                throw new InvalidOperationException(
1052
                    Messages::objectModelSerializerLoopsNotAllowedInComplexTypes($propertyName)
1053
                );
1054
            }
1055
        }
1056
1057
        $this->complexTypeInstanceCollection[$count] = &$result;
1058
1059
        $internalContent = new ODataPropertyContent();
1060
        $resourceProperties = $resourceType->getAllProperties();
1061
        // first up, handle primitive properties
1062
        foreach ($resourceProperties as $prop) {
1063
            $resourceKind = $prop->getKind();
1064
            $propName = $prop->getName();
1065
            $internalProperty = new ODataProperty();
1066
            $internalProperty->name = $propName;
1067
            if (static::isMatchPrimitive($resourceKind)) {
1068
                $iType = $prop->getInstanceType();
1069
                if (!$iType instanceof IType) {
1070
                    throw new InvalidOperationException(get_class($iType));
1071
                }
1072
                $internalProperty->typeName = $iType->getFullTypeName();
1073
1074
                $rType = $prop->getResourceType()->getInstanceType();
1075
                if (!$rType instanceof IType) {
1076
                    throw new InvalidOperationException(get_class($rType));
1077
                }
1078
1079
                $internalProperty->value = $this->primitiveToString($rType, $result->$propName);
1080
1081
                $internalContent->properties[$propName] = $internalProperty;
1082
            } elseif (ResourcePropertyKind::COMPLEX_TYPE == $resourceKind) {
1083
                $rType = $prop->getResourceType();
1084
                $internalProperty->typeName = $rType->getFullName();
1085
                $internalProperty->value = $this->writeComplexValue($rType, $result->$propName, $propName);
1086
1087
                $internalContent->properties[$propName] = $internalProperty;
1088
            }
1089
        }
1090
1091
        unset($this->complexTypeInstanceCollection[$count]);
1092
        return $internalContent;
1093
    }
1094
1095
    public static function isMatchPrimitive($resourceKind)
1096
    {
1097
        if (16 > $resourceKind) {
1098
            return false;
1099
        }
1100
        if (28 < $resourceKind) {
1101
            return false;
1102
        }
1103
        return 0 == ($resourceKind % 4);
1104
    }
1105
1106
    /*
1107
     * @return IMetadataProvider
1108
     */
1109
    protected function getMetadata()
1110
    {
1111
        if (null == $this->metaProvider) {
1112
            $this->metaProvider = App::make('metadata');
1113
        }
1114
        return $this->metaProvider;
1115
    }
1116
1117
    /**
1118
     * @return array
1119
     */
1120
    protected function getLightStack()
1121
    {
1122
        return $this->lightStack;
1123
    }
1124
1125
    /**
1126
     * @return ModelSerialiser
1127
     */
1128
    public function getModelSerialiser()
1129
    {
1130
        return $this->modelSerialiser;
1131
    }
1132
1133
    protected function getConcreteTypeFromAbstractType(ResourceEntityType $resourceType, $payloadClass)
1134
    {
1135
        if ($resourceType->isAbstract()) {
1136
            $derived = $this->getMetadata()->getDerivedTypes($resourceType);
1137
            if (0 == count($derived)) {
1138
                throw new InvalidOperationException('Supplied abstract type must have at least one derived type');
1139
            }
1140
            foreach ($derived as $rawType) {
1141
                if (!$rawType->isAbstract()) {
1142
                    $name = $rawType->getInstanceType()->getName();
1143
                    if ($payloadClass == $name) {
1144
                        $resourceType = $rawType;
1145
                        break;
1146
                    }
1147
                }
1148
            }
1149
        }
1150
        // despite all set up, checking, etc, if we haven't picked a concrete resource type,
1151
        // wheels have fallen off, so blow up
1152
        if ($resourceType->isAbstract()) {
1153
            throw new InvalidOperationException('Concrete resource type not selected for payload ' . $payloadClass);
1154
        }
1155
        return $resourceType;
1156
    }
1157
}
1158