Passed
Pull Request — master (#172)
by Alex
05:36
created

IronicSerialiser::expandNavigationProperty()   C

Complexity

Conditions 11
Paths 192

Size

Total Lines 53
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 42
dl 0
loc 53
rs 6.55
c 0
b 0
f 0
cc 11
nc 192
nop 5

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        $baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
167
        $this->isBaseWritten = true;
168
169
        $stackCount = count($this->lightStack);
170
        $topOfStack = $this->lightStack[$stackCount-1];
171
        $payloadClass = get_class($entryObject->results);
172
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
173
174
        // need gubbinz to unpack an abstract resource type
175
        $resourceType = $this->getConcreteTypeFromAbstractType($resourceType, $payloadClass);
176
177
        // make sure we're barking up right tree
178
        if (!$resourceType instanceof ResourceEntityType) {
179
            throw new InvalidOperationException(get_class($resourceType));
180
        }
181
        $targClass = $resourceType->getInstanceType()->getName();
182
        if (!($entryObject->results instanceof $targClass)) {
183
            $msg = 'Object being serialised not instance of expected class, '
184
                   . $targClass . ', is actually ' . $payloadClass;
185
            throw new InvalidOperationException($msg);
186
        }
187
188
        if (!array_key_exists($targClass, $this->propertiesCache)) {
189
            $rawProp = $resourceType->getAllProperties();
190
            $relProp = [];
191
            $nonRelProp = [];
192
            foreach ($rawProp as $prop) {
193
                $propType = $prop->getResourceType();
194
                if ($propType instanceof ResourceEntityType) {
195
                    $relProp[] = $prop;
196
                } else {
197
                    $nonRelProp[$prop->getName()] = ['prop' => $prop, 'type' => $propType->getInstanceType()];
198
                }
199
            }
200
            $this->propertiesCache[$targClass] = ['rel' => $relProp, 'nonRel' => $nonRelProp];
201
        }
202
        unset($relProp);
203
        unset($nonRelProp);
204
        $relProp = $this->propertiesCache[$targClass]['rel'];
205
        $nonRelProp = $this->propertiesCache[$targClass]['nonRel'];
206
207
        $resourceSet = $resourceType->getCustomState();
208
        if (!$resourceSet instanceof ResourceSet) {
209
            throw new InvalidOperationException('');
210
        }
211
        $title = $resourceType->getName();
212
        $type = $resourceType->getFullName();
213
214
        $relativeUri = $this->getEntryInstanceKey(
215
            $entryObject->results,
216
            $resourceType,
217
            $resourceSet->getName()
218
        );
219
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
220
221
        list($mediaLink, $mediaLinks) = $this->writeMediaData(
222
            $entryObject->results,
223
            $type,
224
            $relativeUri,
225
            $resourceType
226
        );
227
228
        $propertyContent = $this->writePrimitiveProperties($entryObject->results, $nonRelProp);
229
230
        $links = [];
231
        foreach ($relProp as $prop) {
232
            $nuLink = new ODataLink();
233
            $propKind = $prop->getKind();
234
235
            if (!(ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind
236
                  || ResourcePropertyKind::RESOURCE_REFERENCE == $propKind)) {
237
                $msg = '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
238
                       .' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE';
239
                throw new InvalidOperationException($msg);
240
            }
241
            $propTail = ResourcePropertyKind::RESOURCE_REFERENCE == $propKind ? 'entry' : 'feed';
242
            $propType = 'application/atom+xml;type=' . $propTail;
243
            $propName = $prop->getName();
244
            $nuLink->title = $propName;
245
            $nuLink->name = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
246
            $nuLink->url = $relativeUri . '/' . $propName;
247
            $nuLink->type = $propType;
248
            $nuLink->isExpanded = false;
249
            $nuLink->isCollection = 'feed' === $propTail;
250
251
            $shouldExpand = $this->shouldExpandSegment($propName);
252
253
            $navProp = new ODataNavigationPropertyInfo($prop, $shouldExpand);
254
            if ($navProp->expanded) {
255
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
256
            }
257
            $nuLink->isExpanded = isset($nuLink->expandedResult);
258
            if (null === $nuLink->isCollection) {
259
                throw new InvalidOperationException('');
260
            }
261
262
            $links[] = $nuLink;
263
        }
264
265
        $odata = new ODataEntry();
266
        $odata->resourceSetName = $resourceSet->getName();
267
        $odata->id = $absoluteUri;
268
        $odata->title = new ODataTitle($title);
269
        $odata->type = new ODataCategory($type);
270
        $odata->propertyContent = $propertyContent;
271
        $odata->isMediaLinkEntry = $resourceType->isMediaLinkEntry();
272
        $odata->editLink = new ODataLink();
273
        $odata->editLink->url = $relativeUri;
274
        $odata->editLink->name = 'edit';
275
        $odata->editLink->title = $title;
276
        $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...
277
        $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...
278
        $odata->links = $links;
279
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
280
        $odata->baseURI = $baseURI;
281
282
        $newCount = count($this->lightStack);
283
        if ($newCount != $stackCount) {
284
            $msg = 'Should have ' . $stackCount . ' elements in stack, have ' . $newCount . ' elements';
285
            throw new InvalidOperationException($msg);
286
        }
287
        $this->lightStack[$newCount-1]['count']--;
288
        if (0 == $this->lightStack[$newCount-1]['count']) {
289
            array_pop($this->lightStack);
290
        }
291
        return $odata;
292
    }
293
294
    /**
295
     * Write top level feed element.
296
     *
297
     * @param QueryResult &$entryObjects Array of entry resources to be written
298
     *
299
     * @return ODataFeed
300
     */
301
    public function writeTopLevelElements(QueryResult & $entryObjects)
302
    {
303
        $res = $entryObjects->results;
304
        if (!(is_array($res) || $res instanceof Collection)) {
305
            throw new InvalidOperationException('!is_array($entryObjects->results)');
306
        }
307
        if (is_array($res) && 0 == count($res)) {
308
            $entryObjects->hasMore = false;
309
        }
310
        if ($res instanceof Collection && 0 == $res->count()) {
311
            $entryObjects->hasMore = false;
312
        }
313
314
        $this->loadStackIfEmpty();
315
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
316
317
        $title = $this->getRequest()->getContainerName();
318
        $relativeUri = $this->getRequest()->getIdentifier();
319
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
320
321
        $selfLink = new ODataLink();
322
        $selfLink->name = 'self';
323
        $selfLink->title = $relativeUri;
324
        $selfLink->url = $relativeUri;
325
326
        $odata = new ODataFeed();
327
        $odata->title = new ODataTitle($title);
328
        $odata->id = $absoluteUri;
329
        $odata->selfLink = $selfLink;
330
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
331
        $odata->baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
332
        $this->isBaseWritten = true;
333
334
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
335
            $odata->rowCount = $this->getRequest()->getCountValue();
336
        }
337
        foreach ($res as $entry) {
338
            if (!$entry instanceof QueryResult) {
339
                $query = new QueryResult();
340
                $query->results = $entry;
341
            } else {
342
                $query = $entry;
343
            }
344
            if (!$query instanceof QueryResult) {
345
                throw new InvalidOperationException(get_class($query));
346
            }
347
            $odata->entries[] = $this->writeTopLevelElement($query);
348
        }
349
350
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
351
        $requestTop = $this->getRequest()->getTopOptionCount();
352
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
353
        $requestTop = (null === $requestTop) ? $pageSize+1 : $requestTop;
354
355
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
356
            $stackSegment = $setName;
357
            $lastObject = end($entryObjects->results);
358
            $segment = $this->getNextLinkUri($lastObject);
359
            $nextLink = new ODataLink();
360
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
361
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
362
            $odata->nextPageLink = $nextLink;
363
        }
364
365
        return $odata;
366
    }
367
368
    /**
369
     * Write top level url element.
370
     *
371
     * @param QueryResult $entryObject The entry resource whose url to be written
372
     *
373
     * @return ODataURL
374
     */
375
    public function writeUrlElement(QueryResult $entryObject)
376
    {
377
        $url = new ODataURL();
378
        if (null !== $entryObject->results) {
379
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
380
            $relativeUri = $this->getEntryInstanceKey(
381
                $entryObject->results,
382
                $currentResourceType,
383
                $this->getCurrentResourceSetWrapper()->getName()
384
            );
385
386
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
387
        }
388
389
        return $url;
390
    }
391
392
    /**
393
     * Write top level url collection.
394
     *
395
     * @param QueryResult $entryObjects Array of entry resources whose url to be written
396
     *
397
     * @return ODataURLCollection
398
     */
399
    public function writeUrlElements(QueryResult $entryObjects)
400
    {
401
        $urls = new ODataURLCollection();
402
        if (!empty($entryObjects->results)) {
403
            $i = 0;
404
            foreach ($entryObjects->results as $entryObject) {
405
                if (!$entryObject instanceof QueryResult) {
406
                    $query = new QueryResult();
407
                    $query->results = $entryObject;
408
                } else {
409
                    $query = $entryObject;
410
                }
411
                $urls->urls[$i] = $this->writeUrlElement($query);
412
                ++$i;
413
            }
414
415
            if ($i > 0 && true === $entryObjects->hasMore) {
416
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
417
                $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

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

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

628
            /** @scrutinizer ignore-call */ 
629
            $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...
629
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
630
        }
631
        $mediaLinks = [];
632
        if ($resourceType->hasNamedStream()) {
633
            $namedStreams = $resourceType->getAllNamedStreams();
634
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
635
                $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

635
                /** @scrutinizer ignore-call */ 
636
                $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...
636
                    $entryObject,
637
                    $resourceStreamInfo,
638
                    $context,
639
                    $relativeUri
640
                );
641
                $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

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

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