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

IronicSerialiser::getMetadata()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

598
            /** @scrutinizer ignore-call */ 
599
            $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...
599
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
600
        }
601
        /** @var ODataMediaLink[] $mediaLinks */
602
        $mediaLinks = [];
603
        if ($resourceType->hasNamedStream()) {
604
            $namedStreams = $resourceType->getAllNamedStreams();
605
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
606
                $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

606
                /** @scrutinizer ignore-call */ 
607
                $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...
607
                    $entryObject,
608
                    $resourceStreamInfo,
609
                    $context,
610
                    $relativeUri
611
                );
612
                $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

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