Passed
Pull Request — master (#204)
by Alex
05:48
created

IronicSerialiser   F

Complexity

Total Complexity 147

Size/Duplication

Total Lines 1072
Duplicated Lines 0 %

Importance

Changes 28
Bugs 0 Features 0
Metric Value
wmc 147
eloc 499
c 28
b 0
f 0
dl 0
loc 1072
rs 2

28 Methods

Rating   Name   Duplication   Size   Complexity  
A isMatchPrimitive() 0 9 3
A needNextPageLink() 0 14 4
A writeTopLevelBagObject() 0 12 1
A writePrimitiveProperties() 0 19 4
A __construct() 0 10 1
A getUpdated() 0 3 1
D writeTopLevelElements() 0 65 15
A getProjectionNodes() 0 8 3
B getCurrentExpandedProjectionNode() 0 32 7
A getEntryInstanceKey() 0 30 5
C expandNavigationProperty() 0 58 11
A setService() 0 5 1
B writeBagValue() 0 29 10
A writeTopLevelComplexObject() 0 16 2
B getConcreteTypeFromAbstractType() 0 23 7
A getLightStack() 0 3 1
A getCurrentResourceSetWrapper() 0 6 2
B getNextPageLinkQueryParametersForRootResourceSet() 0 37 8
B writeUrlElements() 0 32 7
A getNextLinkUri() 0 22 6
D writeTopLevelElement() 0 136 16
A writeUrlElement() 0 17 2
B primitiveToString() 0 22 7
B writeComplexValue() 0 51 9
A writeMediaData() 0 44 5
A writeTopLevelPrimitive() 0 27 5
A loadStackIfEmpty() 0 5 2
A shouldExpandSegment() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like IronicSerialiser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use IronicSerialiser, and based on these observations, apply Extract Interface, too.

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

391
                $lastObject = end(/** @scrutinizer ignore-type */ $entryObjects->results);
Loading history...
392
                $segment = $this->getNextLinkUri($lastObject);
393
                $nextLink = new ODataLink();
394
                $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
395
                $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
396
                $urls->nextPageLink = $nextLink;
397
            }
398
        }
399
400
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
401
            $urls->count = $this->getRequest()->getCountValue();
402
        }
403
404
        return $urls;
405
    }
406
407
    /**
408
     * Write top level complex resource.
409
     *
410
     * @param QueryResult  &$complexValue The complex object to be written
411
     * @param string       $propertyName  The name of the complex property
412
     * @param ResourceType &$resourceType Describes the type of complex object
413
     *
414
     * @return ODataPropertyContent
415
     * @throws InvalidOperationException
416
     * @throws \ReflectionException
417
     */
418
    public function writeTopLevelComplexObject(QueryResult &$complexValue, $propertyName, ResourceType &$resourceType)
419
    {
420
        $result = $complexValue->results;
421
422
        $propertyContent = new ODataPropertyContent();
423
        $odataProperty = new ODataProperty();
424
        $odataProperty->name = $propertyName;
425
        $odataProperty->typeName = $resourceType->getFullName();
426
        if (null != $result) {
427
            $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

427
            $internalContent = $this->writeComplexValue($resourceType, /** @scrutinizer ignore-type */ $result);
Loading history...
428
            $odataProperty->value = $internalContent;
429
        }
430
431
        $propertyContent->properties[$propertyName] = $odataProperty;
432
433
        return $propertyContent;
434
    }
435
436
    /**
437
     * Write top level bag resource.
438
     *
439
     * @param QueryResult  &$BagValue     The bag object to be
440
     *                                    written
441
     * @param string       $propertyName  The name of the
442
     *                                    bag property
443
     * @param ResourceType &$resourceType Describes the type of
444
     *                                    bag object
445
     *
446
     * @return ODataPropertyContent
447
     * @throws InvalidOperationException
448
     * @throws \ReflectionException
449
     */
450
    public function writeTopLevelBagObject(QueryResult &$BagValue, $propertyName, ResourceType &$resourceType)
451
    {
452
        $result = $BagValue->results;
453
454
        $propertyContent = new ODataPropertyContent();
455
        $odataProperty = new ODataProperty();
456
        $odataProperty->name = $propertyName;
457
        $odataProperty->typeName = 'Collection(' . $resourceType->getFullName() . ')';
458
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
459
460
        $propertyContent->properties[$propertyName] = $odataProperty;
461
        return $propertyContent;
462
    }
463
464
    /**
465
     * Write top level primitive value.
466
     *
467
     * @param  QueryResult          &$primitiveValue   The primitive value to be
468
     *                                                 written
469
     * @param  ResourceProperty     &$resourceProperty Resource property describing the
470
     *                                                 primitive property to be written
471
     * @return ODataPropertyContent
472
     * @throws InvalidOperationException
473
     * @throws \ReflectionException
474
     */
475
    public function writeTopLevelPrimitive(QueryResult &$primitiveValue, ResourceProperty &$resourceProperty = null)
476
    {
477
        if (null === $resourceProperty) {
478
            throw new InvalidOperationException('Resource property must not be null');
479
        }
480
        $propertyContent = new ODataPropertyContent();
481
482
        $odataProperty = new ODataProperty();
483
        $odataProperty->name = $resourceProperty->getName();
484
        $iType = $resourceProperty->getInstanceType();
485
        if (!$iType instanceof IType) {
486
            throw new InvalidOperationException(get_class($iType));
487
        }
488
        $odataProperty->typeName = $iType->getFullTypeName();
489
        if (null == $primitiveValue->results) {
490
            $odataProperty->value = null;
491
        } else {
492
            $rType = $resourceProperty->getResourceType()->getInstanceType();
493
            if (!$rType instanceof IType) {
494
                throw new InvalidOperationException(get_class($rType));
495
            }
496
            $odataProperty->value = $this->primitiveToString($rType, $primitiveValue->results);
497
        }
498
499
        $propertyContent->properties[$odataProperty->name] = $odataProperty;
500
501
        return $propertyContent;
502
    }
503
504
    /**
505
     * Get update timestamp.
506
     *
507
     * @return Carbon
508
     */
509
    public function getUpdated()
510
    {
511
        return $this->updated;
512
    }
513
514
    /**
515
     * @param Model $entityInstance
516
     * @param ResourceType $resourceType
517
     * @param string $containerName
518
     * @return string
519
     * @throws InvalidOperationException
520
     * @throws ODataException
521
     * @throws \ReflectionException
522
     */
523
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
524
    {
525
        $typeName = $resourceType->getName();
526
        $keyProperties = $resourceType->getKeyProperties();
527
        if (0 == count($keyProperties)) {
528
            throw new InvalidOperationException('count($keyProperties) == 0');
529
        }
530
        $keyString = $containerName . '(';
531
        $comma = null;
532
        foreach ($keyProperties as $keyName => $resourceProperty) {
533
            $keyType = $resourceProperty->getInstanceType();
534
            if (!$keyType instanceof IType) {
535
                throw new InvalidOperationException('$keyType not instanceof IType');
536
            }
537
            $keyName = $resourceProperty->getName();
538
            $keyValue = $entityInstance->$keyName;
539
            if (!isset($keyValue)) {
540
                throw ODataException::createInternalServerError(
541
                    Messages::badQueryNullKeysAreNotSupported($typeName, $keyName)
542
                );
543
            }
544
545
            $keyValue = $keyType->convertToOData($keyValue);
546
            $keyString .= $comma . $keyName . '=' . $keyValue;
547
            $comma = ',';
548
        }
549
550
        $keyString .= ')';
551
552
        return $keyString;
553
    }
554
555
    /**
556
     * @param $entryObject
557
     * @param $type
558
     * @param $relativeUri
559
     * @param ResourceType $resourceType
560
     * @param ODataMediaLink|null $mediaLink
561
     * @param ODataMediaLink[] $mediaLinks
562
     * @return void
563
     * @throws InvalidOperationException
564
     */
565
    protected function writeMediaData(
566
        $entryObject,
567
        $type,
568
        $relativeUri,
569
        ResourceType $resourceType,
570
        ODataMediaLink &$mediaLink = null,
571
        array &$mediaLinks = []
572
    ) {
573
        $context = $this->getService()->getOperationContext();
574
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
575
        if (null == $streamProviderWrapper) {
576
            throw new InvalidOperationException('Retrieved stream provider must not be null');
577
        }
578
579
        /** @var ODataMediaLink|null $mediaLink */
580
        $mediaLink = null;
581
        if ($resourceType->isMediaLinkEntry()) {
582
            $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

582
            /** @scrutinizer ignore-call */ 
583
            $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...
583
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
584
        }
585
        /** @var ODataMediaLink[] $mediaLinks */
586
        $mediaLinks = [];
587
        if ($resourceType->hasNamedStream()) {
588
            $namedStreams = $resourceType->getAllNamedStreams();
589
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
590
                $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

590
                /** @scrutinizer ignore-call */ 
591
                $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...
591
                    $entryObject,
592
                    $resourceStreamInfo,
593
                    $context,
594
                    $relativeUri
595
                );
596
                $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

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