Passed
Pull Request — master (#204)
by Alex
04:35
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
54
    /**
55
     * @var RootProjectionNode
56
     */
57
    private $rootNode = null;
58
59
    /**
60
     * Collection of complex type instances used for cycle detection.
61
     *
62
     * @var array
63
     */
64
    protected $complexTypeInstanceCollection;
65
66
    /**
67
     * Update time to insert into ODataEntry/ODataFeed fields
68
     * @var Carbon
69
     */
70
    private $updated;
71
72
    /**
73
     * Has base URI already been written out during serialisation?
74
     * @var bool
75
     */
76
    private $isBaseWritten = false;
77
78
    /**
79
     * @param IService                $service Reference to the data service instance
80
     * @param RequestDescription|null $request Type instance describing the client submitted request
81
     * @throws \Exception
82
     */
83
    public function __construct(IService $service, RequestDescription $request = null)
84
    {
85
        $this->service = $service;
86
        $this->request = $request;
87
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
88
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
89
        $this->stack = new SegmentStack($request);
90
        $this->complexTypeInstanceCollection = [];
91
        $this->modelSerialiser = new ModelSerialiser();
92
        $this->updated = Carbon::now();
93
    }
94
95
    /**
96
     * Write a top level entry resource.
97
     *
98
     * @param QueryResult $entryObject Reference to the entry object to be written
99
     *
100
     * @return ODataEntry|null
101
     * @throws InvalidOperationException
102
     * @throws \ReflectionException
103
     * @throws ODataException
104
     */
105
    public function writeTopLevelElement(QueryResult $entryObject)
106
    {
107
        if (!isset($entryObject->results)) {
108
            array_pop($this->lightStack);
109
            return null;
110
        }
111
        if (!$entryObject->results instanceof Model) {
112
            $res = $entryObject->results;
113
            $msg = is_array($res) ? 'Entry object must be single Model' : get_class($res);
114
            throw new InvalidOperationException($msg);
115
        }
116
117
        $this->loadStackIfEmpty();
118
        $baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
119
        $this->isBaseWritten = true;
120
121
        $stackCount = count($this->lightStack);
122
        $topOfStack = $this->lightStack[$stackCount-1];
123
        $payloadClass = get_class($entryObject->results);
124
        /** @var ResourceEntityType $resourceType */
125
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
126
127
        // need gubbinz to unpack an abstract resource type
128
        $resourceType = $this->getConcreteTypeFromAbstractType($resourceType, $payloadClass);
129
130
        // make sure we're barking up right tree
131
        if (!$resourceType instanceof ResourceEntityType) {
132
            throw new InvalidOperationException(get_class($resourceType));
133
        }
134
135
        /** @var Model $res */
136
        $res = $entryObject->results;
137
        $targClass = $resourceType->getInstanceType()->getName();
138
        if (!($res instanceof $targClass)) {
139
            $msg = 'Object being serialised not instance of expected class, '
140
                   . $targClass . ', is actually ' . $payloadClass;
141
            throw new InvalidOperationException($msg);
142
        }
143
144
        $this->checkRelationPropertiesCached($targClass, $resourceType);
145
        /** @var ResourceProperty[] $relProp */
146
        $relProp = $this->propertiesCache[$targClass]['rel'];
147
        /** @var ResourceProperty[] $nonRelProp */
148
        $nonRelProp = $this->propertiesCache[$targClass]['nonRel'];
149
150
        $resourceSet = $resourceType->getCustomState();
151
        if (!$resourceSet instanceof ResourceSet) {
152
            throw new InvalidOperationException('');
153
        }
154
        $title = $resourceType->getName();
155
        $type = $resourceType->getFullName();
156
157
        $relativeUri = $this->getEntryInstanceKey(
158
            $res,
159
            $resourceType,
160
            $resourceSet->getName()
161
        );
162
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
163
164
        /** var $mediaLink ODataMediaLink|null */
165
        $mediaLink = null;
166
        /** var $mediaLinks ODataMediaLink[] */
167
        $mediaLinks = [];
168
        $this->writeMediaData(
169
            $res,
170
            $type,
171
            $relativeUri,
172
            $resourceType,
173
            $mediaLink,
174
            $mediaLinks
175
        );
176
177
        $propertyContent = $this->writePrimitiveProperties($res, $nonRelProp);
178
179
        $links = $this->buildLinksFromRels($entryObject, $relProp, $relativeUri);
180
181
        $odata = new ODataEntry();
182
        $odata->resourceSetName = $resourceSet->getName();
183
        $odata->id = $absoluteUri;
184
        $odata->title = new ODataTitle($title);
185
        $odata->type = new ODataCategory($type);
186
        $odata->propertyContent = $propertyContent;
187
        $odata->isMediaLinkEntry = $resourceType->isMediaLinkEntry();
188
        $odata->editLink = new ODataLink();
189
        $odata->editLink->url = $relativeUri;
190
        $odata->editLink->name = 'edit';
191
        $odata->editLink->title = $title;
192
        $odata->mediaLink = $mediaLink;
193
        $odata->mediaLinks = $mediaLinks;
194
        $odata->links = $links;
195
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
196
        $odata->baseURI = $baseURI;
197
198
        $newCount = count($this->lightStack);
199
        if ($newCount != $stackCount) {
200
            $msg = 'Should have ' . $stackCount . ' elements in stack, have ' . $newCount . ' elements';
201
            throw new InvalidOperationException($msg);
202
        }
203
        $this->lightStack[$newCount-1]['count']--;
204
        if (0 == $this->lightStack[$newCount-1]['count']) {
205
            array_pop($this->lightStack);
206
        }
207
        return $odata;
208
    }
209
210
    /**
211
     * Write top level feed element.
212
     *
213
     * @param QueryResult &$entryObjects Array of entry resources to be written
214
     *
215
     * @return ODataFeed
216
     * @throws InvalidOperationException
217
     * @throws ODataException
218
     * @throws \ReflectionException
219
     */
220
    public function writeTopLevelElements(QueryResult &$entryObjects)
221
    {
222
        $res = $entryObjects->results;
223
        if (!(is_array($res) || $res instanceof Collection)) {
224
            throw new InvalidOperationException('!is_array($entryObjects->results)');
225
        }
226
        if (is_array($res) && 0 == count($res)) {
227
            $entryObjects->hasMore = false;
228
        }
229
        if ($res instanceof Collection && 0 == $res->count()) {
230
            $entryObjects->hasMore = false;
231
        }
232
233
        $this->loadStackIfEmpty();
234
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
235
236
        $title = $this->getRequest()->getContainerName();
237
        $relativeUri = $this->getRequest()->getIdentifier();
238
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
239
240
        $selfLink = new ODataLink();
241
        $selfLink->name = 'self';
242
        $selfLink->title = $relativeUri;
243
        $selfLink->url = $relativeUri;
244
245
        $odata = new ODataFeed();
246
        $odata->title = new ODataTitle($title);
247
        $odata->id = $absoluteUri;
248
        $odata->selfLink = $selfLink;
249
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
250
        $odata->baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
251
        $this->isBaseWritten = true;
252
253
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
254
            $odata->rowCount = $this->getRequest()->getCountValue();
255
        }
256
        foreach ($res as $entry) {
257
            if (!$entry instanceof QueryResult) {
258
                $query = new QueryResult();
259
                $query->results = $entry;
260
            } else {
261
                $query = $entry;
262
            }
263
            if (!$query instanceof QueryResult) {
264
                throw new InvalidOperationException(get_class($query));
265
            }
266
            $odata->entries[] = $this->writeTopLevelElement($query);
267
        }
268
269
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
270
        $requestTop = $this->getRequest()->getTopOptionCount();
271
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
272
        $requestTop = (null === $requestTop) ? $pageSize+1 : $requestTop;
273
274
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
275
            $stackSegment = $setName;
276
            $lastObject = end($entryObjects->results);
277
            $segment = $this->getNextLinkUri($lastObject);
278
            $nextLink = new ODataLink();
279
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
280
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
281
            $odata->nextPageLink = $nextLink;
282
        }
283
284
        return $odata;
285
    }
286
287
    /**
288
     * Write top level url element.
289
     *
290
     * @param QueryResult $entryObject The entry resource whose url to be written
291
     *
292
     * @return ODataURL
293
     * @throws InvalidOperationException
294
     * @throws ODataException
295
     * @throws \ReflectionException
296
     */
297
    public function writeUrlElement(QueryResult $entryObject)
298
    {
299
        $url = new ODataURL();
300
        /** @var Model|null $res */
301
        $res = $entryObject->results;
302
        if (null !== $res) {
303
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
304
            $relativeUri = $this->getEntryInstanceKey(
305
                $res,
306
                $currentResourceType,
307
                $this->getCurrentResourceSetWrapper()->getName()
308
            );
309
310
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
311
        }
312
313
        return $url;
314
    }
315
316
    /**
317
     * Write top level url collection.
318
     *
319
     * @param QueryResult $entryObjects Array of entry resources whose url to be written
320
     *
321
     * @return ODataURLCollection
322
     * @throws InvalidOperationException
323
     * @throws ODataException
324
     * @throws \ReflectionException
325
     */
326
    public function writeUrlElements(QueryResult $entryObjects)
327
    {
328
        $urls = new ODataURLCollection();
329
        if (!empty($entryObjects->results)) {
330
            $i = 0;
331
            foreach ($entryObjects->results as $entryObject) {
332
                if (!$entryObject instanceof QueryResult) {
333
                    $query = new QueryResult();
334
                    $query->results = $entryObject;
335
                } else {
336
                    $query = $entryObject;
337
                }
338
                $urls->urls[$i] = $this->writeUrlElement($query);
339
                ++$i;
340
            }
341
342
            if ($i > 0 && true === $entryObjects->hasMore) {
343
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
344
                $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

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

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

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

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

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