Passed
Pull Request — master (#205)
by Alex
04:56
created

IronicSerialiser::writeTopLevelElement()   C

Complexity

Conditions 10
Paths 15

Size

Total Lines 103
Code Lines 71

Duplication

Lines 0
Ratio 0 %

Importance

Changes 11
Bugs 0 Features 0
Metric Value
eloc 71
c 11
b 0
f 0
dl 0
loc 103
rs 6.766
cc 10
nc 15
nop 1

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
    use SerialiseDepWrapperTrait;
52
    use SerialisePropertyCacheTrait;
53
    use SerialiseNavigationTrait;
54
55
    /**
56
     * Collection of complex type instances used for cycle detection.
57
     *
58
     * @var array
59
     */
60
    protected $complexTypeInstanceCollection;
61
62
    /**
63
     * Update time to insert into ODataEntry/ODataFeed fields
64
     * @var Carbon
65
     */
66
    private $updated;
67
68
    /**
69
     * Has base URI already been written out during serialisation?
70
     * @var bool
71
     */
72
    private $isBaseWritten = false;
73
74
    /**
75
     * @param IService                $service Reference to the data service instance
76
     * @param RequestDescription|null $request Type instance describing the client submitted request
77
     * @throws \Exception
78
     */
79
    public function __construct(IService $service, RequestDescription $request = null)
80
    {
81
        $this->service = $service;
82
        $this->request = $request;
83
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
84
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
85
        $this->stack = new SegmentStack($request);
86
        $this->complexTypeInstanceCollection = [];
87
        $this->modelSerialiser = new ModelSerialiser();
88
        $this->updated = Carbon::now();
89
    }
90
91
    /**
92
     * Write a top level entry resource.
93
     *
94
     * @param QueryResult $entryObject Reference to the entry object to be written
95
     *
96
     * @return ODataEntry|null
97
     * @throws InvalidOperationException
98
     * @throws \ReflectionException
99
     * @throws ODataException
100
     */
101
    public function writeTopLevelElement(QueryResult $entryObject)
102
    {
103
        if (!isset($entryObject->results)) {
104
            array_pop($this->lightStack);
105
            return null;
106
        }
107
        if (!$entryObject->results instanceof Model) {
108
            $res = $entryObject->results;
109
            $msg = is_array($res) ? 'Entry object must be single Model' : get_class($res);
110
            throw new InvalidOperationException($msg);
111
        }
112
113
        $this->loadStackIfEmpty();
114
        $baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
115
        $this->isBaseWritten = true;
116
117
        $stackCount = count($this->lightStack);
118
        $topOfStack = $this->lightStack[$stackCount-1];
119
        $payloadClass = get_class($entryObject->results);
120
        /** @var ResourceEntityType $resourceType */
121
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
122
123
        // need gubbinz to unpack an abstract resource type
124
        $resourceType = $this->getConcreteTypeFromAbstractType($resourceType, $payloadClass);
125
126
        // make sure we're barking up right tree
127
        if (!$resourceType instanceof ResourceEntityType) {
128
            throw new InvalidOperationException(get_class($resourceType));
129
        }
130
131
        /** @var Model $res */
132
        $res = $entryObject->results;
133
        $targClass = $resourceType->getInstanceType()->getName();
134
        if (!($res instanceof $targClass)) {
135
            $msg = 'Object being serialised not instance of expected class, '
136
                   . $targClass . ', is actually ' . $payloadClass;
137
            throw new InvalidOperationException($msg);
138
        }
139
140
        $this->checkRelationPropertiesCached($targClass, $resourceType);
141
        /** @var ResourceProperty[] $relProp */
142
        $relProp = $this->propertiesCache[$targClass]['rel'];
143
        /** @var ResourceProperty[] $nonRelProp */
144
        $nonRelProp = $this->propertiesCache[$targClass]['nonRel'];
145
146
        $resourceSet = $resourceType->getCustomState();
147
        if (!$resourceSet instanceof ResourceSet) {
148
            throw new InvalidOperationException('');
149
        }
150
        $title = $resourceType->getName();
151
        $type = $resourceType->getFullName();
152
153
        $relativeUri = $this->getEntryInstanceKey(
154
            $res,
155
            $resourceType,
156
            $resourceSet->getName()
157
        );
158
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
159
160
        /** var $mediaLink ODataMediaLink|null */
161
        $mediaLink = null;
162
        /** var $mediaLinks ODataMediaLink[] */
163
        $mediaLinks = [];
164
        $this->writeMediaData(
165
            $res,
166
            $type,
167
            $relativeUri,
168
            $resourceType,
169
            $mediaLink,
170
            $mediaLinks
171
        );
172
173
        $propertyContent = $this->writePrimitiveProperties($res, $nonRelProp);
174
175
        $links = $this->buildLinksFromRels($entryObject, $relProp, $relativeUri);
176
177
        $odata = new ODataEntry();
178
        $odata->resourceSetName = $resourceSet->getName();
179
        $odata->id = $absoluteUri;
180
        $odata->title = new ODataTitle($title);
181
        $odata->type = new ODataCategory($type);
182
        $odata->propertyContent = $propertyContent;
183
        $odata->isMediaLinkEntry = $resourceType->isMediaLinkEntry();
184
        $odata->editLink = new ODataLink();
185
        $odata->editLink->url = $relativeUri;
186
        $odata->editLink->name = 'edit';
187
        $odata->editLink->title = $title;
188
        $odata->mediaLink = $mediaLink;
189
        $odata->mediaLinks = $mediaLinks;
190
        $odata->links = $links;
191
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
192
        $odata->baseURI = $baseURI;
193
194
        $newCount = count($this->lightStack);
195
        if ($newCount != $stackCount) {
196
            $msg = 'Should have ' . $stackCount . ' elements in stack, have ' . $newCount . ' elements';
197
            throw new InvalidOperationException($msg);
198
        }
199
        $this->lightStack[$newCount-1]['count']--;
200
        if (0 == $this->lightStack[$newCount-1]['count']) {
201
            array_pop($this->lightStack);
202
        }
203
        return $odata;
204
    }
205
206
    /**
207
     * Write top level feed element.
208
     *
209
     * @param QueryResult &$entryObjects Array of entry resources to be written
210
     *
211
     * @return ODataFeed
212
     * @throws InvalidOperationException
213
     * @throws ODataException
214
     * @throws \ReflectionException
215
     */
216
    public function writeTopLevelElements(QueryResult &$entryObjects)
217
    {
218
        $res = $entryObjects->results;
219
        $isArray = is_array($res);
220
        $isColl = !$isArray && $res instanceof Collection;
221
        if (!($isArray || $isColl)) {
222
            throw new InvalidOperationException('!is_array($entryObjects->results)');
223
        }
224
        if ($isArray && 0 == count($res)) {
225
            $entryObjects->hasMore = false;
226
        }
227
        if ($isColl && 0 == $res->count()) {
0 ignored issues
show
Bug introduced by
The method count() does not exist on null. ( Ignorable by Annotation )

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

227
        if ($isColl && 0 == $res->/** @scrutinizer ignore-call */ count()) {

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...
228
            $entryObjects->hasMore = false;
229
        }
230
231
        $this->loadStackIfEmpty();
232
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
233
234
        $title = $this->getRequest()->getContainerName();
235
        $relativeUri = $this->getRequest()->getIdentifier();
236
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
237
238
        $selfLink = new ODataLink();
239
        $selfLink->name = 'self';
240
        $selfLink->title = $relativeUri;
241
        $selfLink->url = $relativeUri;
242
243
        $odata = new ODataFeed();
244
        $odata->title = new ODataTitle($title);
245
        $odata->id = $absoluteUri;
246
        $odata->selfLink = $selfLink;
247
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
248
        $odata->baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
249
        $this->isBaseWritten = true;
250
251
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
252
            $odata->rowCount = $this->getRequest()->getCountValue();
253
        }
254
        foreach ($res as $entry) {
255
            if (!$entry instanceof QueryResult) {
256
                $query = new QueryResult();
257
                $query->results = $entry;
258
            } else {
259
                $query = $entry;
260
            }
261
            if (!$query instanceof QueryResult) {
262
                throw new InvalidOperationException(get_class($query));
263
            }
264
            $odata->entries[] = $this->writeTopLevelElement($query);
265
        }
266
267
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
268
        $requestTop = $this->getRequest()->getTopOptionCount();
269
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
270
        $requestTop = (null === $requestTop) ? $pageSize+1 : $requestTop;
271
272
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
273
            $stackSegment = $setName;
274
            $lastObject = end($entryObjects->results);
275
            $segment = $this->getNextLinkUri($lastObject);
276
            $nextLink = new ODataLink();
277
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
278
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
279
            $odata->nextPageLink = $nextLink;
280
        }
281
282
        return $odata;
283
    }
284
285
    /**
286
     * Write top level url element.
287
     *
288
     * @param QueryResult $entryObject The entry resource whose url to be written
289
     *
290
     * @return ODataURL
291
     * @throws InvalidOperationException
292
     * @throws ODataException
293
     * @throws \ReflectionException
294
     */
295
    public function writeUrlElement(QueryResult $entryObject)
296
    {
297
        $url = new ODataURL();
298
        /** @var Model|null $res */
299
        $res = $entryObject->results;
300
        if (null !== $res) {
301
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
302
            $relativeUri = $this->getEntryInstanceKey(
303
                $res,
304
                $currentResourceType,
305
                $this->getCurrentResourceSetWrapper()->getName()
306
            );
307
308
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
309
        }
310
311
        return $url;
312
    }
313
314
    /**
315
     * Write top level url collection.
316
     *
317
     * @param QueryResult $entryObjects Array of entry resources whose url to be written
318
     *
319
     * @return ODataURLCollection
320
     * @throws InvalidOperationException
321
     * @throws ODataException
322
     * @throws \ReflectionException
323
     */
324
    public function writeUrlElements(QueryResult $entryObjects)
325
    {
326
        $urls = new ODataURLCollection();
327
        if (!empty($entryObjects->results)) {
328
            $i = 0;
329
            foreach ($entryObjects->results as $entryObject) {
330
                if (!$entryObject instanceof QueryResult) {
331
                    $query = new QueryResult();
332
                    $query->results = $entryObject;
333
                } else {
334
                    $query = $entryObject;
335
                }
336
                $urls->urls[$i] = $this->writeUrlElement($query);
337
                ++$i;
338
            }
339
340
            if ($i > 0 && true === $entryObjects->hasMore) {
341
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
342
                $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

342
                $lastObject = end(/** @scrutinizer ignore-type */ $entryObjects->results);
Loading history...
343
                $segment = $this->getNextLinkUri($lastObject);
344
                $nextLink = new ODataLink();
345
                $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
346
                $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
347
                $urls->nextPageLink = $nextLink;
348
            }
349
        }
350
351
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
352
            $urls->count = $this->getRequest()->getCountValue();
353
        }
354
355
        return $urls;
356
    }
357
358
    /**
359
     * Write top level complex resource.
360
     *
361
     * @param QueryResult  &$complexValue The complex object to be written
362
     * @param string       $propertyName  The name of the complex property
363
     * @param ResourceType &$resourceType Describes the type of complex object
364
     *
365
     * @return ODataPropertyContent
366
     * @throws InvalidOperationException
367
     * @throws \ReflectionException
368
     */
369
    public function writeTopLevelComplexObject(QueryResult &$complexValue, $propertyName, ResourceType &$resourceType)
370
    {
371
        $result = $complexValue->results;
372
373
        $propertyContent = new ODataPropertyContent();
374
        $odataProperty = new ODataProperty();
375
        $odataProperty->name = $propertyName;
376
        $odataProperty->typeName = $resourceType->getFullName();
377
        if (null != $result) {
378
            $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

378
            $internalContent = $this->writeComplexValue($resourceType, /** @scrutinizer ignore-type */ $result);
Loading history...
379
            $odataProperty->value = $internalContent;
380
        }
381
382
        $propertyContent->properties[$propertyName] = $odataProperty;
383
384
        return $propertyContent;
385
    }
386
387
    /**
388
     * Write top level bag resource.
389
     *
390
     * @param QueryResult  &$BagValue     The bag object to be
391
     *                                    written
392
     * @param string       $propertyName  The name of the
393
     *                                    bag property
394
     * @param ResourceType &$resourceType Describes the type of
395
     *                                    bag object
396
     *
397
     * @return ODataPropertyContent
398
     * @throws InvalidOperationException
399
     * @throws \ReflectionException
400
     */
401
    public function writeTopLevelBagObject(QueryResult &$BagValue, $propertyName, ResourceType &$resourceType)
402
    {
403
        $result = $BagValue->results;
404
405
        $propertyContent = new ODataPropertyContent();
406
        $odataProperty = new ODataProperty();
407
        $odataProperty->name = $propertyName;
408
        $odataProperty->typeName = 'Collection(' . $resourceType->getFullName() . ')';
409
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
410
411
        $propertyContent->properties[$propertyName] = $odataProperty;
412
        return $propertyContent;
413
    }
414
415
    /**
416
     * Write top level primitive value.
417
     *
418
     * @param  QueryResult          &$primitiveValue   The primitive value to be
419
     *                                                 written
420
     * @param  ResourceProperty     &$resourceProperty Resource property describing the
421
     *                                                 primitive property to be written
422
     * @return ODataPropertyContent
423
     * @throws InvalidOperationException
424
     * @throws \ReflectionException
425
     */
426
    public function writeTopLevelPrimitive(QueryResult &$primitiveValue, ResourceProperty &$resourceProperty = null)
427
    {
428
        if (null === $resourceProperty) {
429
            throw new InvalidOperationException('Resource property must not be null');
430
        }
431
        $propertyContent = new ODataPropertyContent();
432
433
        $odataProperty = new ODataProperty();
434
        $odataProperty->name = $resourceProperty->getName();
435
        $iType = $resourceProperty->getInstanceType();
436
        if (!$iType instanceof IType) {
437
            throw new InvalidOperationException(get_class($iType));
438
        }
439
        $odataProperty->typeName = $iType->getFullTypeName();
440
        if (null == $primitiveValue->results) {
441
            $odataProperty->value = null;
442
        } else {
443
            $rType = $resourceProperty->getResourceType()->getInstanceType();
444
            if (!$rType instanceof IType) {
445
                throw new InvalidOperationException(get_class($rType));
446
            }
447
            $odataProperty->value = $this->primitiveToString($rType, $primitiveValue->results);
448
        }
449
450
        $propertyContent->properties[$odataProperty->name] = $odataProperty;
451
452
        return $propertyContent;
453
    }
454
455
    /**
456
     * Get update timestamp.
457
     *
458
     * @return Carbon
459
     */
460
    public function getUpdated()
461
    {
462
        return $this->updated;
463
    }
464
465
    /**
466
     * @param Model $entityInstance
467
     * @param ResourceType $resourceType
468
     * @param string $containerName
469
     * @return string
470
     * @throws InvalidOperationException
471
     * @throws ODataException
472
     * @throws \ReflectionException
473
     */
474
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
475
    {
476
        $typeName = $resourceType->getName();
477
        $keyProperties = $resourceType->getKeyProperties();
478
        if (0 == count($keyProperties)) {
479
            throw new InvalidOperationException('count($keyProperties) == 0');
480
        }
481
        $keyString = $containerName . '(';
482
        $comma = null;
483
        foreach ($keyProperties as $keyName => $resourceProperty) {
484
            $keyType = $resourceProperty->getInstanceType();
485
            if (!$keyType instanceof IType) {
486
                throw new InvalidOperationException('$keyType not instanceof IType');
487
            }
488
            $keyName = $resourceProperty->getName();
489
            $keyValue = $entityInstance->$keyName;
490
            if (!isset($keyValue)) {
491
                throw ODataException::createInternalServerError(
492
                    Messages::badQueryNullKeysAreNotSupported($typeName, $keyName)
493
                );
494
            }
495
496
            $keyValue = $keyType->convertToOData($keyValue);
497
            $keyString .= $comma . $keyName . '=' . $keyValue;
498
            $comma = ',';
499
        }
500
501
        $keyString .= ')';
502
503
        return $keyString;
504
    }
505
506
    /**
507
     * @param $entryObject
508
     * @param $type
509
     * @param $relativeUri
510
     * @param ResourceType $resourceType
511
     * @param ODataMediaLink|null $mediaLink
512
     * @param ODataMediaLink[] $mediaLinks
513
     * @return void
514
     * @throws InvalidOperationException
515
     */
516
    protected function writeMediaData(
517
        $entryObject,
518
        $type,
519
        $relativeUri,
520
        ResourceType $resourceType,
521
        ODataMediaLink &$mediaLink = null,
522
        array &$mediaLinks = []
523
    ) {
524
        $context = $this->getService()->getOperationContext();
525
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
526
        if (null == $streamProviderWrapper) {
527
            throw new InvalidOperationException('Retrieved stream provider must not be null');
528
        }
529
530
        /** @var ODataMediaLink|null $mediaLink */
531
        $mediaLink = null;
532
        if ($resourceType->isMediaLinkEntry()) {
533
            $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

533
            /** @scrutinizer ignore-call */ 
534
            $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...
534
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
535
        }
536
        /** @var ODataMediaLink[] $mediaLinks */
537
        $mediaLinks = [];
538
        if ($resourceType->hasNamedStream()) {
539
            $namedStreams = $resourceType->getAllNamedStreams();
540
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
541
                $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

541
                /** @scrutinizer ignore-call */ 
542
                $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...
542
                    $entryObject,
543
                    $resourceStreamInfo,
544
                    $context,
545
                    $relativeUri
546
                );
547
                $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

547
                /** @scrutinizer ignore-call */ 
548
                $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...
548
                    $entryObject,
549
                    $resourceStreamInfo,
550
                    $context
551
                );
552
                $eTag = $streamProviderWrapper->getStreamETag2(
553
                    $entryObject,
554
                    $resourceStreamInfo,
555
                    $context
556
                );
557
558
                $nuLink = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
559
                $mediaLinks[] = $nuLink;
560
            }
561
        }
562
    }
563
564
    /**
565
     * Wheter next link is needed for the current resource set (feed)
566
     * being serialized.
567
     *
568
     * @param int $resultSetCount Number of entries in the current
569
     *                            resource set
570
     *
571
     * @return bool true if the feed must have a next page link
572
     * @throws InvalidOperationException
573
     */
574
    protected function needNextPageLink($resultSetCount)
575
    {
576
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
577
        $recursionLevel = count($this->getStack()->getSegmentNames());
578
        $pageSize = $currentResourceSet->getResourceSetPageSize();
579
580
        if (1 == $recursionLevel) {
581
            //presence of $top option affect next link for root container
582
            $topValueCount = $this->getRequest()->getTopOptionCount();
583
            if (null !== $topValueCount && ($topValueCount <= $pageSize)) {
584
                return false;
585
            }
586
        }
587
        return $resultSetCount == $pageSize;
588
    }
589
590
    /**
591
     * Get next page link from the given entity instance.
592
     *
593
     * @param  mixed          &$lastObject Last object serialized to be
594
     *                                     used for generating
595
     *                                     $skiptoken
596
     * @throws ODataException
597
     * @return string         for the link for next page
598
     * @throws InvalidOperationException
599
     */
600
    protected function getNextLinkUri(&$lastObject)
601
    {
602
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
603
        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
604
        if (null === $internalOrderByInfo) {
605
            throw new InvalidOperationException('Null');
606
        }
607
        if (!$internalOrderByInfo instanceof InternalOrderByInfo) {
0 ignored issues
show
introduced by
$internalOrderByInfo is always a sub-type of POData\UriProcessor\Quer...ser\InternalOrderByInfo.
Loading history...
608
            throw new InvalidOperationException(get_class($internalOrderByInfo));
609
        }
610
        $numSegments = count($internalOrderByInfo->getOrderByPathSegments());
611
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
612
613
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
614
        if (empty($skipToken)) {
615
            throw new InvalidOperationException('!is_null($skipToken)');
616
        }
617
        $token = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
618
        $skipToken = (1 < $numSegments) ? $skipToken : intval(trim($skipToken, '\''));
619
        $skipToken = '?' . $queryParameterString . $token . $skipToken;
620
621
        return $skipToken;
622
    }
623
624
    /**
625
     * Builds the string corresponding to query parameters for top level results
626
     * (result set identified by the resource path) to be put in next page link.
627
     *
628
     * @return string|null string representing the query parameters in the URI
629
     *                     query parameter format, NULL if there
630
     *                     is no query parameters
631
     *                     required for the next link of top level result set
632
     * @throws InvalidOperationException
633
     */
634
    protected function getNextPageLinkQueryParametersForRootResourceSet()
635
    {
636
        /** @var string|null $queryParameterString */
637
        $queryParameterString = null;
638
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
639
                     ODataConstants::HTTPQUERY_STRING_EXPAND,
640
                     ODataConstants::HTTPQUERY_STRING_ORDERBY,
641
                     ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
642
                     ODataConstants::HTTPQUERY_STRING_SELECT, ] as $queryOption) {
643
            /** @var string|null $value */
644
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
645
            if (null !== $value) {
646
                if (null !== $queryParameterString) {
647
                    $queryParameterString = /** @scrutinizer ignore-type */$queryParameterString . '&';
648
                }
649
650
                $queryParameterString .= $queryOption . '=' . $value;
651
            }
652
        }
653
654
        $topCountValue = $this->getRequest()->getTopOptionCount();
655
        if (null !== $topCountValue) {
656
            $remainingCount = $topCountValue-$this->getRequest()->getTopCount();
657
            if (0 < $remainingCount) {
658
                if (null !== $queryParameterString) {
659
                    $queryParameterString .= '&';
660
                }
661
662
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
663
            }
664
        }
665
666
        if (null !== $queryParameterString) {
667
            $queryParameterString .= '&';
668
        }
669
670
        return $queryParameterString;
671
    }
672
673
    /**
674
     * Convert the given primitive value to string.
675
     * Note: This method will not handle null primitive value.
676
     *
677
     * @param IType &$type                  Type of the primitive property needing conversion
678
     * @param mixed $primitiveValue         Primitive value to convert
679
     *
680
     * @return string
681
     */
682
    private function primitiveToString(IType &$type, $primitiveValue)
683
    {
684
        // kludge to enable switching on type of $type without getting tripped up by mocks as we would with get_class
685
        // switch (true) means we unconditionally enter, and then lean on case statements to match given block
686
        switch (true) {
687
            case $type instanceof StringType:
688
                $stringValue = utf8_encode($primitiveValue);
689
                break;
690
            case $type instanceof Boolean:
691
                $stringValue = (true === $primitiveValue) ? 'true' : 'false';
692
                break;
693
            case $type instanceof Binary:
694
                $stringValue = base64_encode($primitiveValue);
695
                break;
696
            case $type instanceof DateTime && $primitiveValue instanceof \DateTime:
697
                $stringValue = $primitiveValue->format(\DateTime::ATOM);
698
                break;
699
            default:
700
                $stringValue = strval($primitiveValue);
701
        }
702
703
        return $stringValue;
704
    }
705
706
    /**
707
     * @param $entryObject
708
     * @param $nonRelProp
709
     * @return ODataPropertyContent
710
     * @throws InvalidOperationException
711
     */
712
    private function writePrimitiveProperties(Model $entryObject, $nonRelProp)
713
    {
714
        $propertyContent = new ODataPropertyContent();
715
        $cereal = $this->getModelSerialiser()->bulkSerialise($entryObject);
716
        foreach ($cereal as $corn => $flake) {
717
            if (!array_key_exists($corn, $nonRelProp)) {
718
                continue;
719
            }
720
            $corn = strval($corn);
721
            $rType = $nonRelProp[$corn]['type'];
722
            /** @var ResourceProperty $nrp */
723
            $nrp = $nonRelProp[$corn]['prop'];
724
            $subProp = new ODataProperty();
725
            $subProp->name = $corn;
726
            $subProp->value = isset($flake) ? $this->primitiveToString($rType, $flake) : null;
727
            $subProp->typeName = $nrp->getResourceType()->getFullName();
728
            $propertyContent->properties[$corn] = $subProp;
729
        }
730
        return $propertyContent;
731
    }
732
733
    /**
734
     * @param QueryResult $entryObject
735
     * @param ResourceProperty $prop
736
     * @param $nuLink
737
     * @param $propKind
738
     * @param $propName
739
     * @throws InvalidOperationException
740
     * @throws ODataException
741
     * @throws \ReflectionException
742
     */
743
    private function expandNavigationProperty(
744
        QueryResult $entryObject,
745
        ResourceProperty $prop,
746
        $nuLink,
747
        $propKind,
748
        $propName
749
    ) {
750
        $nextName = $prop->getResourceType()->getName();
751
        $nuLink->isExpanded = true;
752
        $value = $entryObject->results->$propName;
753
        $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind;
754
        $nuLink->isCollection = $isCollection;
755
756
        if (is_array($value)) {
757
            if (1 == count($value) && !$isCollection) {
758
                $value = $value[0];
759
            } else {
760
                $value = collect($value);
761
            }
762
        }
763
764
        $result = new QueryResult();
765
        $result->results = $value;
766
        $nullResult = null === $value;
767
        $isSingleton = $value instanceof Model;
768
        $resultCount = $nullResult ? 0 : ($isSingleton ? 1 : $value->count());
769
770
        if (0 < $resultCount) {
771
            $newStackLine = ['type' => $nextName, 'prop' => $propName, 'count' => $resultCount];
772
            array_push($this->lightStack, $newStackLine);
773
            if (!$isCollection) {
774
                $nuLink->type = 'application/atom+xml;type=entry';
775
                $expandedResult = $this->writeTopLevelElement($result);
776
            } else {
777
                $nuLink->type = 'application/atom+xml;type=feed';
778
                $expandedResult = $this->writeTopLevelElements($result);
779
            }
780
            $nuLink->expandedResult = $expandedResult;
781
        } else {
782
            $type = $this->getService()->getProvidersWrapper()->resolveResourceType($nextName);
783
            if (!$isCollection) {
784
                $result = new ODataEntry();
785
                $result->resourceSetName = $type->getName();
786
            } else {
787
                $result = new ODataFeed();
788
                $result->selfLink = new ODataLink();
789
                $result->selfLink->name = ODataConstants::ATOM_SELF_RELATION_ATTRIBUTE_VALUE;
790
            }
791
            $nuLink->expandedResult = $result;
792
        }
793
        if (isset($nuLink->expandedResult->selfLink)) {
794
            $nuLink->expandedResult->selfLink->title = $propName;
795
            $nuLink->expandedResult->selfLink->url = $nuLink->url;
796
            $nuLink->expandedResult->title = new ODataTitle($propName);
797
            $nuLink->expandedResult->id = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->url;
798
        }
799
        if (!isset($nuLink->expandedResult)) {
800
            throw new InvalidOperationException('');
801
        }
802
    }
803
804
    /**
805
     * @param ResourceType $resourceType
806
     * @param $result
807
     * @return ODataBagContent|null
808
     * @throws InvalidOperationException
809
     * @throws \ReflectionException
810
     */
811
    protected function writeBagValue(ResourceType &$resourceType, $result)
812
    {
813
        $isNull = null == $result;
814
        if (!($isNull || is_array($result))) {
815
            throw new InvalidOperationException('Bag parameter must be null or array');
816
        }
817
        $typeKind = $resourceType->getResourceTypeKind();
818
        $kVal = $typeKind;
819
        if (!(ResourceTypeKind::PRIMITIVE() == $kVal || ResourceTypeKind::COMPLEX() == $kVal)) {
820
            $msg = '$bagItemResourceTypeKind != ResourceTypeKind::PRIMITIVE'
821
                   .' && $bagItemResourceTypeKind != ResourceTypeKind::COMPLEX';
822
            throw new InvalidOperationException($msg);
823
        }
824
        if ($isNull) {
825
            return null;
826
        }
827
        $bag = new ODataBagContent();
828
        $result = array_filter($result);
829
        foreach ($result as $value) {
830
            if (ResourceTypeKind::PRIMITIVE() == $kVal) {
831
                $instance = $resourceType->getInstanceType();
832
                if (!$instance instanceof IType) {
833
                    throw new InvalidOperationException(get_class($instance));
834
                }
835
                $bag->propertyContents[] = $this->primitiveToString($instance, $value);
836
            } elseif (ResourceTypeKind::COMPLEX() == $kVal) {
837
                $bag->propertyContents[] = $this->writeComplexValue($resourceType, $value);
838
            }
839
        }
840
        return $bag;
841
    }
842
843
    /**
844
     * @param  ResourceType         $resourceType
845
     * @param  object               $result
846
     * @param  string|null          $propertyName
847
     * @return ODataPropertyContent
848
     * @throws InvalidOperationException
849
     * @throws \ReflectionException
850
     */
851
    protected function writeComplexValue(ResourceType &$resourceType, &$result, $propertyName = null)
852
    {
853
        if (!is_object($result)) {
854
            throw new InvalidOperationException('Supplied $customObject must be an object');
855
        }
856
857
        $count = count($this->complexTypeInstanceCollection);
858
        for ($i = 0; $i < $count; ++$i) {
859
            if ($this->complexTypeInstanceCollection[$i] === $result) {
860
                throw new InvalidOperationException(
861
                    Messages::objectModelSerializerLoopsNotAllowedInComplexTypes($propertyName)
862
                );
863
            }
864
        }
865
866
        $this->complexTypeInstanceCollection[$count] = &$result;
867
868
        $internalContent = new ODataPropertyContent();
869
        $resourceProperties = $resourceType->getAllProperties();
870
        // first up, handle primitive properties
871
        foreach ($resourceProperties as $prop) {
872
            $resourceKind = $prop->getKind();
873
            $propName = $prop->getName();
874
            $internalProperty = new ODataProperty();
875
            $internalProperty->name = $propName;
876
            if (static::isMatchPrimitive($resourceKind)) {
877
                $iType = $prop->getInstanceType();
878
                if (!$iType instanceof IType) {
879
                    throw new InvalidOperationException(get_class($iType));
880
                }
881
                $internalProperty->typeName = $iType->getFullTypeName();
882
883
                $rType = $prop->getResourceType()->getInstanceType();
884
                if (!$rType instanceof IType) {
885
                    throw new InvalidOperationException(get_class($rType));
886
                }
887
888
                $internalProperty->value = $this->primitiveToString($rType, $result->$propName);
889
890
                $internalContent->properties[$propName] = $internalProperty;
891
            } elseif (ResourcePropertyKind::COMPLEX_TYPE == $resourceKind) {
892
                $rType = $prop->getResourceType();
893
                $internalProperty->typeName = $rType->getFullName();
894
                $internalProperty->value = $this->writeComplexValue($rType, $result->$propName, $propName);
895
896
                $internalContent->properties[$propName] = $internalProperty;
897
            }
898
        }
899
900
        unset($this->complexTypeInstanceCollection[$count]);
901
        return $internalContent;
902
    }
903
904
    public static function isMatchPrimitive($resourceKind)
905
    {
906
        if (16 > $resourceKind) {
907
            return false;
908
        }
909
        if (28 < $resourceKind) {
910
            return false;
911
        }
912
        return 0 == ($resourceKind % 4);
913
    }
914
915
    /**
916
     * @param ResourceEntityType $resourceType
917
     * @param $payloadClass
918
     * @return ResourceEntityType|ResourceType
919
     * @throws InvalidOperationException
920
     * @throws \ReflectionException
921
     */
922
    protected function getConcreteTypeFromAbstractType(ResourceEntityType $resourceType, $payloadClass)
923
    {
924
        if ($resourceType->isAbstract()) {
925
            $derived = $this->getMetadata()->getDerivedTypes($resourceType);
926
            if (0 == count($derived)) {
927
                throw new InvalidOperationException('Supplied abstract type must have at least one derived type');
928
            }
929
            foreach ($derived as $rawType) {
930
                if (!$rawType->isAbstract()) {
931
                    $name = $rawType->getInstanceType()->getName();
932
                    if ($payloadClass == $name) {
933
                        $resourceType = $rawType;
934
                        break;
935
                    }
936
                }
937
            }
938
        }
939
        // despite all set up, checking, etc, if we haven't picked a concrete resource type,
940
        // wheels have fallen off, so blow up
941
        if ($resourceType->isAbstract()) {
942
            throw new InvalidOperationException('Concrete resource type not selected for payload ' . $payloadClass);
943
        }
944
        return $resourceType;
945
    }
946
947
    /**
948
     * @param QueryResult $entryObject
949
     * @param array $relProp
950
     * @param $relativeUri
951
     * @return array
952
     * @throws InvalidOperationException
953
     * @throws ODataException
954
     * @throws \ReflectionException
955
     */
956
    protected function buildLinksFromRels(QueryResult $entryObject, array $relProp, $relativeUri)
957
    {
958
        $links = [];
959
        foreach ($relProp as $prop) {
960
            $nuLink = new ODataLink();
961
            $propKind = $prop->getKind();
962
963
            if (!(ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind
964
                  || ResourcePropertyKind::RESOURCE_REFERENCE == $propKind)) {
965
                $msg = '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
966
                       . ' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE';
967
                throw new InvalidOperationException($msg);
968
            }
969
            $propTail = ResourcePropertyKind::RESOURCE_REFERENCE == $propKind ? 'entry' : 'feed';
970
            $propType = 'application/atom+xml;type=' . $propTail;
971
            $propName = $prop->getName();
972
            $nuLink->title = $propName;
973
            $nuLink->name = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
974
            $nuLink->url = $relativeUri . '/' . $propName;
975
            $nuLink->type = $propType;
976
            $nuLink->isExpanded = false;
977
            $nuLink->isCollection = 'feed' === $propTail;
978
979
            $shouldExpand = $this->shouldExpandSegment($propName);
980
981
            $navProp = new ODataNavigationPropertyInfo($prop, $shouldExpand);
982
            if ($navProp->expanded) {
983
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
984
            }
985
            $nuLink->isExpanded = isset($nuLink->expandedResult);
986
            if (null === $nuLink->isCollection) {
987
                throw new InvalidOperationException('');
988
            }
989
990
            $links[] = $nuLink;
991
        }
992
        return $links;
993
    }
994
}
995