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

IronicSerialiser::writeUrlElement()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 10
c 2
b 0
f 0
dl 0
loc 17
rs 9.9332
cc 2
nc 2
nop 1
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 = $this->buildLinksFromRels($entryObject, $relProp, $relativeUri);
194
195
        $odata = new ODataEntry();
196
        $odata->resourceSetName = $resourceSet->getName();
197
        $odata->id = $absoluteUri;
198
        $odata->title = new ODataTitle($title);
199
        $odata->type = new ODataCategory($type);
200
        $odata->propertyContent = $propertyContent;
201
        $odata->isMediaLinkEntry = $resourceType->isMediaLinkEntry();
202
        $odata->editLink = new ODataLink();
203
        $odata->editLink->url = $relativeUri;
204
        $odata->editLink->name = 'edit';
205
        $odata->editLink->title = $title;
206
        $odata->mediaLink = $mediaLink;
207
        $odata->mediaLinks = $mediaLinks;
208
        $odata->links = $links;
209
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
210
        $odata->baseURI = $baseURI;
211
212
        $newCount = count($this->lightStack);
213
        if ($newCount != $stackCount) {
214
            $msg = 'Should have ' . $stackCount . ' elements in stack, have ' . $newCount . ' elements';
215
            throw new InvalidOperationException($msg);
216
        }
217
        $this->lightStack[$newCount-1]['count']--;
218
        if (0 == $this->lightStack[$newCount-1]['count']) {
219
            array_pop($this->lightStack);
220
        }
221
        return $odata;
222
    }
223
224
    /**
225
     * Write top level feed element.
226
     *
227
     * @param QueryResult &$entryObjects Array of entry resources to be written
228
     *
229
     * @return ODataFeed
230
     * @throws InvalidOperationException
231
     * @throws ODataException
232
     * @throws \ReflectionException
233
     */
234
    public function writeTopLevelElements(QueryResult &$entryObjects)
235
    {
236
        $res = $entryObjects->results;
237
        if (!(is_array($res) || $res instanceof Collection)) {
238
            throw new InvalidOperationException('!is_array($entryObjects->results)');
239
        }
240
        if (is_array($res) && 0 == count($res)) {
241
            $entryObjects->hasMore = false;
242
        }
243
        if ($res instanceof Collection && 0 == $res->count()) {
244
            $entryObjects->hasMore = false;
245
        }
246
247
        $this->loadStackIfEmpty();
248
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
249
250
        $title = $this->getRequest()->getContainerName();
251
        $relativeUri = $this->getRequest()->getIdentifier();
252
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
253
254
        $selfLink = new ODataLink();
255
        $selfLink->name = 'self';
256
        $selfLink->title = $relativeUri;
257
        $selfLink->url = $relativeUri;
258
259
        $odata = new ODataFeed();
260
        $odata->title = new ODataTitle($title);
261
        $odata->id = $absoluteUri;
262
        $odata->selfLink = $selfLink;
263
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
264
        $odata->baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
265
        $this->isBaseWritten = true;
266
267
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
268
            $odata->rowCount = $this->getRequest()->getCountValue();
269
        }
270
        foreach ($res as $entry) {
271
            if (!$entry instanceof QueryResult) {
272
                $query = new QueryResult();
273
                $query->results = $entry;
274
            } else {
275
                $query = $entry;
276
            }
277
            if (!$query instanceof QueryResult) {
278
                throw new InvalidOperationException(get_class($query));
279
            }
280
            $odata->entries[] = $this->writeTopLevelElement($query);
281
        }
282
283
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
284
        $requestTop = $this->getRequest()->getTopOptionCount();
285
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
286
        $requestTop = (null === $requestTop) ? $pageSize+1 : $requestTop;
287
288
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
289
            $stackSegment = $setName;
290
            $lastObject = end($entryObjects->results);
291
            $segment = $this->getNextLinkUri($lastObject);
292
            $nextLink = new ODataLink();
293
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
294
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
295
            $odata->nextPageLink = $nextLink;
296
        }
297
298
        return $odata;
299
    }
300
301
    /**
302
     * Write top level url element.
303
     *
304
     * @param QueryResult $entryObject The entry resource whose url to be written
305
     *
306
     * @return ODataURL
307
     * @throws InvalidOperationException
308
     * @throws ODataException
309
     * @throws \ReflectionException
310
     */
311
    public function writeUrlElement(QueryResult $entryObject)
312
    {
313
        $url = new ODataURL();
314
        /** @var Model|null $res */
315
        $res = $entryObject->results;
316
        if (null !== $res) {
317
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
318
            $relativeUri = $this->getEntryInstanceKey(
319
                $res,
320
                $currentResourceType,
321
                $this->getCurrentResourceSetWrapper()->getName()
322
            );
323
324
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
325
        }
326
327
        return $url;
328
    }
329
330
    /**
331
     * Write top level url collection.
332
     *
333
     * @param QueryResult $entryObjects Array of entry resources whose url to be written
334
     *
335
     * @return ODataURLCollection
336
     * @throws InvalidOperationException
337
     * @throws ODataException
338
     * @throws \ReflectionException
339
     */
340
    public function writeUrlElements(QueryResult $entryObjects)
341
    {
342
        $urls = new ODataURLCollection();
343
        if (!empty($entryObjects->results)) {
344
            $i = 0;
345
            foreach ($entryObjects->results as $entryObject) {
346
                if (!$entryObject instanceof QueryResult) {
347
                    $query = new QueryResult();
348
                    $query->results = $entryObject;
349
                } else {
350
                    $query = $entryObject;
351
                }
352
                $urls->urls[$i] = $this->writeUrlElement($query);
353
                ++$i;
354
            }
355
356
            if ($i > 0 && true === $entryObjects->hasMore) {
357
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
358
                $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

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

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

549
            /** @scrutinizer ignore-call */ 
550
            $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...
550
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
551
        }
552
        /** @var ODataMediaLink[] $mediaLinks */
553
        $mediaLinks = [];
554
        if ($resourceType->hasNamedStream()) {
555
            $namedStreams = $resourceType->getAllNamedStreams();
556
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
557
                $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

557
                /** @scrutinizer ignore-call */ 
558
                $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...
558
                    $entryObject,
559
                    $resourceStreamInfo,
560
                    $context,
561
                    $relativeUri
562
                );
563
                $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

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