Completed
Push — master ( 8498b7...ac4bb4 )
by Alex
02:53
created

CynicSerialiser::expandNavigationProperty()   D

Complexity

Conditions 9
Paths 60

Size

Total Lines 40
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 40
rs 4.909
cc 9
eloc 31
nc 60
nop 5
1
<?php
2
3
namespace POData\ObjectModel;
4
5
use Carbon\Carbon;
6
use Illuminate\Support\Collection;
7
use POData\Common\InvalidOperationException;
8
use POData\Common\Messages;
9
use POData\Common\ODataConstants;
10
use POData\Common\ODataException;
11
use POData\IService;
12
use POData\Providers\Metadata\ResourceComplexType;
13
use POData\Providers\Metadata\ResourceEntityType;
14
use POData\Providers\Metadata\ResourcePrimitiveType;
15
use POData\Providers\Metadata\ResourceProperty;
16
use POData\Providers\Metadata\ResourcePropertyKind;
17
use POData\Providers\Metadata\ResourceSet;
18
use POData\Providers\Metadata\ResourceSetWrapper;
19
use POData\Providers\Metadata\ResourceType;
20
use POData\Providers\Metadata\ResourceTypeKind;
21
use POData\Providers\Metadata\Type\Binary;
22
use POData\Providers\Metadata\Type\Boolean;
23
use POData\Providers\Metadata\Type\DateTime;
24
use POData\Providers\Metadata\Type\IType;
25
use POData\Providers\Metadata\Type\StringType;
26
use POData\Providers\Query\QueryResult;
27
use POData\Providers\Query\QueryType;
28
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
29
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ProjectionNode;
30
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\RootProjectionNode;
31
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
32
use POData\UriProcessor\RequestDescription;
33
use POData\UriProcessor\SegmentStack;
34
35
class CynicSerialiser implements IObjectSerialiser
36
{
37
    /**
38
     * The service implementation.
39
     *
40
     * @var IService
41
     */
42
    protected $service;
43
44
    /**
45
     * Request description instance describes OData request the
46
     * the client has submitted and result of the request.
47
     *
48
     * @var RequestDescription
49
     */
50
    protected $request;
51
52
    /**
53
     * Collection of complex type instances used for cycle detection.
54
     *
55
     * @var array
56
     */
57
    protected $complexTypeInstanceCollection;
58
59
    /**
60
     * Absolute service Uri.
61
     *
62
     * @var string
63
     */
64
    protected $absoluteServiceUri;
65
66
    /**
67
     * Absolute service Uri with slash.
68
     *
69
     * @var string
70
     */
71
    protected $absoluteServiceUriWithSlash;
72
73
    /**
74
     * Holds reference to segment stack being processed.
75
     *
76
     * @var SegmentStack
77
     */
78
    protected $stack;
79
80
    /**
81
     * Lightweight stack tracking for recursive descent fill.
82
     */
83
    private $lightStack = [];
84
85
    /*
86
     * Update time to insert into ODataEntry/ODataFeed fields
87
     * @var Carbon;
88
     */
89
    private $updated;
90
91
    /*
92
     * Has base URI already been written out during serialisation?
93
     * @var bool;
94
     */
95
    private $isBaseWritten = false;
96
97
    /**
98
     * @param IService                $service Reference to the data service instance
99
     * @param RequestDescription|null $request Type instance describing the client submitted request
100
     */
101
    public function __construct(IService $service, RequestDescription $request = null)
102
    {
103
        $this->service = $service;
104
        $this->request = $request;
105
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
106
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
107
        $this->stack = new SegmentStack($request);
108
        $this->complexTypeInstanceCollection = [];
109
        $this->updated = Carbon::now();
110
    }
111
112
    /**
113
     * Write a top level entry resource.
114
     *
115
     * @param QueryResult $entryObject Results property contains reference to the entry object to be written
116
     *
117
     * @return ODataEntry|null
118
     */
119
    public function writeTopLevelElement(QueryResult $entryObject)
120
    {
121
        if (!isset($entryObject->results)) {
122
            array_pop($this->lightStack);
123
            return null;
124
        }
125
126
        assert(is_object($entryObject->results));
127
        $this->loadStackIfEmpty();
128
129
        $baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
130
        $this->isBaseWritten = true;
131
132
        $stackCount = count($this->lightStack);
133
        $topOfStack = $this->lightStack[$stackCount-1];
134
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
135
        assert($resourceType instanceof ResourceType, get_class($resourceType));
136
        $rawProp = $resourceType->getAllProperties();
137
        $relProp = [];
138
        $nonRelProp = [];
139
        $last = end($this->lightStack);
140
        $projNodes = ($last['type'] == $last['property']) ? $this->getProjectionNodes() : null;
141
142
        foreach ($rawProp as $prop) {
143
            $propName = $prop->getName();
144
            if ($prop->getResourceType() instanceof ResourceEntityType) {
145
                $relProp[$propName] = $prop;
146
            } else {
147
                $nonRelProp[$propName] = $prop;
148
            }
149
        }
150
        $rawCount = count($rawProp);
151
        $relCount = count($relProp);
152
        $nonRelCount = count($nonRelProp);
153
        assert(
154
            $rawCount == $relCount + $nonRelCount,
155
            'Raw property count '.$rawCount.', does not equal sum of relProp count, '.$relCount
156
            .', and nonRelPropCount,'.$nonRelCount
157
        );
158
159
        // now mask off against projNodes
160
        if (null !== $projNodes) {
161
            $keys = [];
162
            foreach ($projNodes as $node) {
163
                $keys[$node->getPropertyName()] = '';
164
            }
165
166
            $relProp = array_intersect_key($relProp, $keys);
167
            $nonRelProp = array_intersect_key($nonRelProp, $keys);
168
        }
169
170
        $resourceSet = $resourceType->getCustomState();
171
        assert($resourceSet instanceof ResourceSet);
172
        $title = $resourceType->getName();
173
        $type = $resourceType->getFullName();
174
175
        $relativeUri = $this->getEntryInstanceKey(
176
            $entryObject->results,
177
            $resourceType,
178
            $resourceSet->getName()
179
        );
180
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
181
182
        list($mediaLink, $mediaLinks) = $this->writeMediaData(
183
            $entryObject->results,
184
            $type,
185
            $relativeUri,
186
            $resourceType
187
        );
188
189
        $propertyContent = $this->writeProperties($entryObject->results, $nonRelProp);
190
191
        $links = [];
192
        foreach ($relProp as $prop) {
193
            $nuLink = new ODataLink();
194
            $propKind = $prop->getKind();
195
196
            assert(
197
                ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind
198
                || ResourcePropertyKind::RESOURCE_REFERENCE == $propKind,
199
                '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
200
                .' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE'
201
            );
202
            $propTail = ResourcePropertyKind::RESOURCE_REFERENCE == $propKind ? 'entry' : 'feed';
203
            $propType = 'application/atom+xml;type='.$propTail;
204
            $propName = $prop->getName();
205
            $nuLink->title = $propName;
206
            $nuLink->name = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
207
            $nuLink->url = $relativeUri . '/' . $propName;
208
            $nuLink->type = $propType;
209
210
            $navProp = new ODataNavigationPropertyInfo($prop, $this->shouldExpandSegment($propName));
211
            if ($navProp->expanded) {
212
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
213
            }
214
215
            $links[] = $nuLink;
216
        }
217
218
        $odata = new ODataEntry();
219
        $odata->resourceSetName = $resourceSet->getName();
220
        $odata->id = $absoluteUri;
221
        $odata->title = new ODataTitle($title);
222
        $odata->type = new ODataCategory($type);
223
        $odata->propertyContent = $propertyContent;
224
        $odata->isMediaLinkEntry = true === $resourceType->isMediaLinkEntry() ? true : null;
225
        $odata->editLink = new ODataLink();
226
        $odata->editLink->url = $relativeUri;
227
        $odata->editLink->name = 'edit';
228
        $odata->editLink->title = $title;
229
        $odata->mediaLink = $mediaLink;
230
        $odata->mediaLinks = $mediaLinks;
231
        $odata->links = $links;
232
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
233
        $odata->baseURI = $baseURI;
234
235
        $newCount = count($this->lightStack);
236
        assert(
237
            $newCount == $stackCount,
238
            'Should have ' . $stackCount . 'elements in stack, have ' . $newCount . 'elements'
239
        );
240
        $this->lightStack[$newCount-1]['count']--;
241
        if (0 == $this->lightStack[$newCount-1]['count']) {
242
            array_pop($this->lightStack);
243
        }
244
        return $odata;
245
    }
246
247
    /**
248
     * Write top level feed element.
249
     *
250
     * @param QueryResult &$entryObjects Results property contains array of entry resources to be written
251
     *
252
     * @return ODataFeed
253
     */
254
    public function writeTopLevelElements(QueryResult &$entryObjects)
255
    {
256
        $res = $entryObjects->results;
257
        assert(is_array($res) || $res instanceof Collection, '!is_array($entryObjects->results)');
258
259
        $this->loadStackIfEmpty();
260
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
261
262
        $title = $this->getRequest()->getContainerName();
263
        $relativeUri = $this->getRequest()->getIdentifier();
264
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
265
266
        $selfLink = new ODataLink();
267
        $selfLink->name = 'self';
268
        $selfLink->title = $title;
269
        $selfLink->url = $relativeUri;
270
271
        $odata = new ODataFeed();
272
        $odata->title = new ODataTitle($title);
273
        $odata->id = $absoluteUri;
274
        $odata->selfLink = $selfLink;
275
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
276
        $odata->baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
277
        $this->isBaseWritten = true;
278
279
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
280
            $odata->rowCount = $this->getRequest()->getCountValue();
281
        }
282
        foreach ($res as $entry) {
283
            if (!$entry instanceof QueryResult) {
284
                $query = new QueryResult();
285
                $query->results = $entry;
286
            } else {
287
                $query = $entry;
288
            }
289
            $odata->entries[] = $this->writeTopLevelElement($query);
290
        }
291
292
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
293
        $requestTop = $this->getRequest()->getTopOptionCount();
294
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
295
        $requestTop = (null === $requestTop) ? $pageSize + 1 : $requestTop;
296
297
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
298
            $stackSegment = $setName;
299
            $lastObject = end($entryObjects->results);
300
            $segment = $this->getNextLinkUri($lastObject);
301
            $nextLink = new ODataLink();
302
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
303
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
304
            $odata->nextPageLink = $nextLink;
305
        }
306
307
        return $odata;
308
    }
309
310
    /**
311
     * Write top level url element.
312
     *
313
     * @param QueryResult $entryObject Results property contains the entry resource whose url to be written
314
     *
315
     * @return ODataURL
316
     */
317
    public function writeUrlElement(QueryResult $entryObject)
318
    {
319
        $url = new ODataURL();
320
        if (null !== $entryObject->results) {
321
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
322
            $relativeUri = $this->getEntryInstanceKey(
323
                $entryObject->results,
324
                $currentResourceType,
325
                $this->getCurrentResourceSetWrapper()->getName()
326
            );
327
328
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
329
        }
330
331
        return $url;
332
    }
333
334
    /**
335
     * Write top level url collection.
336
     *
337
     * @param QueryResult $entryObjects Results property contains the array of entry resources whose urls are
338
     *                                  to be written
339
     *
340
     * @return ODataURLCollection
341
     */
342
    public function writeUrlElements(QueryResult $entryObjects)
343
    {
344
        $urls = new ODataURLCollection();
345
        if (!empty($entryObjects->results)) {
346
            $i = 0;
347
            foreach ($entryObjects->results as $entryObject) {
348
                if (!$entryObject instanceof QueryResult) {
349
                    $query = new QueryResult();
350
                    $query->results = $entryObject;
351
                } else {
352
                    $query = $entryObject;
353
                }
354
                $urls->urls[$i] = $this->writeUrlElement($query);
355
                ++$i;
356
            }
357
358
            if ($i > 0 && true === $entryObjects->hasMore) {
359
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
360
                $lastObject = end($entryObjects->results);
361
                $segment = $this->getNextLinkUri($lastObject);
362
                $nextLink = new ODataLink();
363
                $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
364
                $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
365
                $nextLink->url = ltrim($nextLink->url, '/');
366
                $urls->nextPageLink = $nextLink;
367
            }
368
        }
369
370
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
371
            $urls->count = $this->getRequest()->getCountValue();
372
        }
373
374
        return $urls;
375
    }
376
377
    /**
378
     * Write top level complex resource.
379
     *
380
     * @param QueryResult  &$complexValue Results property contains the complex object to be written
381
     * @param string       $propertyName  The name of the complex property
382
     * @param ResourceType &$resourceType Describes the type of complex object
383
     *
384
     * @return ODataPropertyContent
385
     */
386
    public function writeTopLevelComplexObject(QueryResult &$complexValue, $propertyName, ResourceType &$resourceType)
387
    {
388
        $result = $complexValue->results;
389
390
        $propertyContent = new ODataPropertyContent();
391
        $odataProperty = new ODataProperty();
392
        $odataProperty->name = $propertyName;
393
        $odataProperty->typeName = $resourceType->getFullName();
394
        if (null !== $result) {
395
            assert(is_object($result), 'Supplied $customObject must be an object');
396
            $internalContent = $this->writeComplexValue($resourceType, $result);
397
            $odataProperty->value = $internalContent;
398
        }
399
400
        $propertyContent->properties[$propertyName] = $odataProperty;
401
402
        return $propertyContent;
403
    }
404
405
    /**
406
     * Write top level bag resource.
407
     *
408
     * @param QueryResult  $bagValue
409
     * @param string       $propertyName  The name of the bag property
410
     * @param ResourceType &$resourceType Describes the type of bag object
411
     *
412
     * @return ODataPropertyContent
413
     */
414
    public function writeTopLevelBagObject(QueryResult &$bagValue, $propertyName, ResourceType &$resourceType)
415
    {
416
        $result = $bagValue->results;
417
418
        $propertyContent = new ODataPropertyContent();
419
        $odataProperty = new ODataProperty();
420
        $odataProperty->name = $propertyName;
421
        $odataProperty->typeName = 'Collection('.$resourceType->getFullName().')';
422
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
423
424
        $propertyContent->properties[$propertyName] = $odataProperty;
425
        return $propertyContent;
426
    }
427
428
    /**
429
     * Write top level primitive value.
430
     *
431
     * @param QueryResult      &$primitiveValue   Results property contains the primitive value to be written
432
     * @param ResourceProperty &$resourceProperty Resource property describing the primitive property to be written
433
     *
434
     * @return ODataPropertyContent
435
     */
436
    public function writeTopLevelPrimitive(QueryResult &$primitiveValue, ResourceProperty &$resourceProperty = null)
437
    {
438
        assert(null !== $resourceProperty, 'Resource property must not be null');
439
        $result = new ODataPropertyContent();
440
        $property = new ODataProperty();
441
        $property->name = $resourceProperty->getName();
442
443
        $iType = $resourceProperty->getInstanceType();
444
        assert($iType instanceof IType, get_class($iType));
445
        $property->typeName = $iType->getFullTypeName();
446
        if (null !== $primitiveValue->results) {
447
            $rType = $resourceProperty->getResourceType()->getInstanceType();
448
            assert($rType instanceof IType, get_class($rType));
449
            $property->value = $this->primitiveToString($rType, $primitiveValue->results);
450
        }
451
452
        $result->properties[$property->name] = $property;
453
        return $result;
454
    }
455
456
    /**
457
     * Gets reference to the request submitted by client.
458
     *
459
     * @return RequestDescription
460
     */
461
    public function getRequest()
462
    {
463
        assert(null !== $this->request, 'Request not yet set');
464
465
        return $this->request;
466
    }
467
468
    /**
469
     * Sets reference to the request submitted by client.
470
     *
471
     * @param  RequestDescription $request
472
     * @return void
473
     */
474
    public function setRequest(RequestDescription $request)
475
    {
476
        $this->request = $request;
477
        $this->stack->setRequest($request);
478
    }
479
480
    /**
481
     * Gets the data service instance.
482
     *
483
     * @return IService
484
     */
485
    public function getService()
486
    {
487
        return $this->service;
488
    }
489
490
    /**
491
     * Sets the data service instance.
492
     *
493
     * @param  IService $service
494
     * @return void
495
     */
496
    public function setService(IService $service)
497
    {
498
        $this->service = $service;
499
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
500
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
501
    }
502
503
    /**
504
     * Gets the segment stack instance.
505
     *
506
     * @return SegmentStack
507
     */
508
    public function getStack()
509
    {
510
        return $this->stack;
511
    }
512
513
    /**
514
     * Get update timestamp.
515
     *
516
     * @return Carbon
517
     */
518
    public function getUpdated()
519
    {
520
        return $this->updated;
521
    }
522
523
    /**
524
     * @param ResourceType $resourceType
525
     * @param $result
526
     * @return ODataBagContent|null
527
     */
528
    protected function writeBagValue(ResourceType &$resourceType, $result)
529
    {
530
        assert(null === $result || is_array($result), 'Bag parameter must be null or array');
531
        $typeKind = $resourceType->getResourceTypeKind();
532
        assert(
533
            ResourceTypeKind::PRIMITIVE() == $typeKind || ResourceTypeKind::COMPLEX() == $typeKind,
534
            '$bagItemResourceTypeKind != ResourceTypeKind::PRIMITIVE'
535
            .' && $bagItemResourceTypeKind != ResourceTypeKind::COMPLEX'
536
        );
537
        if (null == $result) {
538
            return null;
539
        }
540
        $bag = new ODataBagContent();
541
        foreach ($result as $value) {
542
            if (isset($value)) {
543
                if (ResourceTypeKind::PRIMITIVE() == $typeKind) {
544
                    $instance = $resourceType->getInstanceType();
545
                    assert($instance instanceof IType, get_class($instance));
546
                    $bag->propertyContents[] = $this->primitiveToString($instance, $value);
547
                } elseif (ResourceTypeKind::COMPLEX() == $typeKind) {
548
                    $bag->propertyContents[] = $this->writeComplexValue($resourceType, $value);
549
                }
550
            }
551
        }
552
        return $bag;
553
    }
554
555
    /**
556
     * @param  ResourceType         $resourceType
557
     * @param  object               $result
558
     * @param  string|null          $propertyName
559
     * @return ODataPropertyContent
560
     */
561
    protected function writeComplexValue(ResourceType &$resourceType, &$result, $propertyName = null)
562
    {
563
        assert(is_object($result), 'Supplied $customObject must be an object');
564
565
        $count = count($this->complexTypeInstanceCollection);
566
        for ($i = 0; $i < $count; ++$i) {
567
            if ($this->complexTypeInstanceCollection[$i] === $result) {
568
                throw new InvalidOperationException(
569
                    Messages::objectModelSerializerLoopsNotAllowedInComplexTypes($propertyName)
570
                );
571
            }
572
        }
573
574
        $this->complexTypeInstanceCollection[$count] = &$result;
575
576
        $internalContent = new ODataPropertyContent();
577
        $resourceProperties = $resourceType->getAllProperties();
578
        // first up, handle primitive properties
579
        foreach ($resourceProperties as $prop) {
580
            $resourceKind = $prop->getKind();
581
            $propName = $prop->getName();
582
            $internalProperty = new ODataProperty();
583
            $internalProperty->name = $propName;
584
            $raw = $result->$propName;
585
            if (static::isMatchPrimitive($resourceKind)) {
586
                $iType = $prop->getInstanceType();
587
                assert($iType instanceof IType, get_class($iType));
588
                $internalProperty->typeName = $iType->getFullTypeName();
589
590
                $rType = $prop->getResourceType()->getInstanceType();
591
                assert($rType instanceof IType, get_class($rType));
592
                if (null !== $raw) {
593
                    $internalProperty->value = $this->primitiveToString($rType, $raw);
594
                }
595
            } elseif (ResourcePropertyKind::COMPLEX_TYPE == $resourceKind) {
596
                $rType = $prop->getResourceType();
597
                $internalProperty->typeName = $rType->getFullName();
598
                if (null !== $raw) {
599
                    $internalProperty->value = $this->writeComplexValue($rType, $raw, $propName);
600
                }
601
            }
602
            $internalContent->properties[$propName] = $internalProperty;
603
        }
604
605
        unset($this->complexTypeInstanceCollection[$count]);
606
        return $internalContent;
607
    }
608
609
    /**
610
     * Check whether to expand a navigation property or not.
611
     *
612
     * @param string $navigationPropertyName Name of naviagtion property in question
613
     *
614
     * @return bool True if the given navigation should be expanded, otherwise false
615
     */
616
    protected function shouldExpandSegment($navigationPropertyName)
617
    {
618
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
619
        if (null === $expandedProjectionNode) {
620
            return false;
621
        }
622
623
        $expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName);
624
625
        // null is a valid input to an instanceof call as of PHP 5.6 - will always return false
626
        return $expandedProjectionNode instanceof ExpandedProjectionNode;
627
    }
628
629
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
630
    {
631
        assert(is_object($entityInstance));
632
        $typeName = $resourceType->getName();
633
        $keyProperties = $resourceType->getKeyProperties();
634
        assert(0 != count($keyProperties), 'count($keyProperties) == 0');
635
        $keyString = $containerName . '(';
636
        $comma = null;
637
        foreach ($keyProperties as $keyName => $resourceProperty) {
638
            $keyType = $resourceProperty->getInstanceType();
639
            assert($keyType instanceof IType, '$keyType not instanceof IType');
640
            $keyName = $resourceProperty->getName();
641
            $keyValue = $entityInstance->$keyName;
642
            if (!isset($keyValue)) {
643
                $msg = Messages::badQueryNullKeysAreNotSupported($typeName, $keyName);
644
                throw ODataException::createInternalServerError($msg);
645
            }
646
647
            $keyValue = $keyType->convertToOData($keyValue);
648
            $keyString .= $comma . $keyName . '=' . $keyValue;
649
            $comma = ',';
650
        }
651
652
        $keyString .= ')';
653
654
        return $keyString;
655
    }
656
657
    /**
658
     * Resource set wrapper for the resource being serialized.
659
     *
660
     * @return ResourceSetWrapper
661
     */
662 View Code Duplication
    protected function getCurrentResourceSetWrapper()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
663
    {
664
        $segmentWrappers = $this->getStack()->getSegmentWrappers();
665
        $count = count($segmentWrappers);
666
667
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $segmentWrappers[$count - 1];
668
    }
669
670
    /**
671
     * Wheter next link is needed for the current resource set (feed)
672
     * being serialized.
673
     *
674
     * @param int $resultSetCount Number of entries in the current
675
     *                            resource set
676
     *
677
     * @return bool true if the feed must have a next page link
678
     */
679
    protected function needNextPageLink($resultSetCount)
0 ignored issues
show
Coding Style introduced by
function needNextPageLink() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
680
    {
681
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
682
        $recursionLevel = count($this->getStack()->getSegmentNames());
683
        $pageSize = $currentResourceSet->getResourceSetPageSize();
684
685
        if (1 == $recursionLevel) {
686
            //presence of $top option affect next link for root container
687
            $topValueCount = $this->getRequest()->getTopOptionCount();
688
            if (null !== $topValueCount && ($topValueCount <= $pageSize)) {
689
                return false;
690
            }
691
        }
692
        return $resultSetCount == $pageSize;
693
    }
694
695
    /**
696
     * Get next page link from the given entity instance.
697
     *
698
     * @param mixed &$lastObject Last object serialized to be
699
     *                           used for generating $skiptoken
700
     *
701
     * @return string for the link for next page
702
     */
703
    protected function getNextLinkUri(&$lastObject)
704
    {
705
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
706
        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
707
        assert(null !== $internalOrderByInfo);
708
        assert(is_object($internalOrderByInfo));
709
        assert($internalOrderByInfo instanceof InternalOrderByInfo, get_class($internalOrderByInfo));
710
        $numSegments = count($internalOrderByInfo->getOrderByPathSegments());
711
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
712
713
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
714
        assert(null !== $skipToken, '!is_null($skipToken)');
715
        $token = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
716
        $skipToken = '?'.$queryParameterString.$token.$skipToken;
717
718
        return $skipToken;
719
    }
720
721
    /**
722
     * @param $entryObject
723
     * @param $type
724
     * @param $relativeUri
725
     * @param $resourceType
726
     * @return array<ODataMediaLink|null|array>
727
     */
728
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
729
    {
730
        $context = $this->getService()->getOperationContext();
731
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
732
        assert(null != $streamProviderWrapper, 'Retrieved stream provider must not be null');
733
734
        $mediaLink = null;
735
        if ($resourceType->isMediaLinkEntry()) {
736
            $eTag = $streamProviderWrapper->getStreamETag2($entryObject, null, $context);
737
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
738
        }
739
        $mediaLinks = [];
740
        if ($resourceType->hasNamedStream()) {
741
            $namedStreams = $resourceType->getAllNamedStreams();
742
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
743
                $readUri = $streamProviderWrapper->getReadStreamUri2(
744
                    $entryObject,
745
                    $resourceStreamInfo,
746
                    $context,
747
                    $relativeUri
748
                );
749
                $mediaContentType = $streamProviderWrapper->getStreamContentType2(
750
                    $entryObject,
751
                    $resourceStreamInfo,
752
                    $context
753
                );
754
                $eTag = $streamProviderWrapper->getStreamETag2(
755
                    $entryObject,
756
                    $resourceStreamInfo,
757
                    $context
758
                );
759
760
                $nuLink = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
761
                $mediaLinks[] = $nuLink;
762
            }
763
        }
764
        return [$mediaLink, $mediaLinks];
765
    }
766
767
    /**
768
     * @param $entryObject
769
     * @param $prop
770
     * @param $nuLink
771
     * @param $propKind
772
     * @param $propName
773
     */
774
    private function expandNavigationProperty(QueryResult $entryObject, $prop, $nuLink, $propKind, $propName)
775
    {
776
        $nextName = $prop->getResourceType()->getName();
777
        $nuLink->isExpanded = true;
778
        $value = $entryObject->results->$propName;
779
        $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind || !is_object($value);
780
        $nuLink->isCollection = $isCollection;
781
        $nullResult = null === $value;
782
        $resultCount = $nullResult ? 0 : count($value);
783
784
        if (0 < $resultCount) {
785
            $result = new QueryResult();
786
            $result->results = $value;
787
            if (!$nullResult) {
788
                $newStackLine = ['type' => $nextName, 'property' => $propName, 'count' => $resultCount];
789
                array_push($this->lightStack, $newStackLine);
790
                if (isset($value)) {
791
                    if (!$isCollection) {
792
                        $nuLink->type = 'application/atom+xml;type=entry';
793
                        $expandedResult = $this->writeTopLevelElement($result);
794
                    } else {
795
                        $nuLink->type = 'application/atom+xml;type=feed';
796
                        $expandedResult = $this->writeTopLevelElements($result);
797
                    }
798
                    $nuLink->expandedResult = $expandedResult;
799
                }
800
            }
801
        }
802
        if (!isset($nuLink->expandedResult)) {
803
            $nuLink->isCollection = null;
804
            $nuLink->isExpanded = null;
805
        } else {
806
            if (isset($nuLink->expandedResult->selfLink)) {
807
                $nuLink->expandedResult->selfLink->title = $propName;
808
                $nuLink->expandedResult->selfLink->url = $nuLink->url;
809
                $nuLink->expandedResult->title = new ODataTitle($propName);
810
                $nuLink->expandedResult->id = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->url;
811
            }
812
        }
813
    }
814
815
    /**
816
     * Gets collection of projection nodes under the current node.
817
     *
818
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes describing projections for the current
819
     *                                                        segment, If this method returns null it means no
820
     *                                                        projections are to be applied and the entire resource for
821
     *                                                        the current segment should be serialized, If it returns
822
     *                                                        non-null only the properties described by the returned
823
     *                                                        projection segments should be serialized
824
     */
825
    protected function getProjectionNodes()
826
    {
827
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
828
        if (null === $expandedProjectionNode || $expandedProjectionNode->canSelectAllProperties()) {
829
            return null;
830
        }
831
832
        return $expandedProjectionNode->getChildNodes();
833
    }
834
835
    /**
836
     * Find a 'ExpandedProjectionNode' instance in the projection tree
837
     * which describes the current segment.
838
     *
839
     * @return null|RootProjectionNode|ExpandedProjectionNode
840
     */
841 View Code Duplication
    protected function getCurrentExpandedProjectionNode()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
842
    {
843
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
844
        if (null === $expandedProjectionNode) {
845
            return null;
846
        } else {
847
            $segmentNames = $this->getStack()->getSegmentNames();
848
            $depth = count($segmentNames);
849
            // $depth == 1 means serialization of root entry
850
            //(the resource identified by resource path) is going on,
851
            //so control won't get into the below for loop.
852
            //we will directly return the root node,
853
            //which is 'ExpandedProjectionNode'
854
            // for resource identified by resource path.
855
            if (0 != $depth) {
856
                for ($i = 1; $i < $depth; ++$i) {
857
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segmentNames[$i]);
858
                    assert(null !== $expandedProjectionNode, 'is_null($expandedProjectionNode)');
859
                    assert(
860
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
861
                        '$expandedProjectionNode not instanceof ExpandedProjectionNode'
862
                    );
863
                }
864
            }
865
        }
866
867
        return $expandedProjectionNode;
868
    }
869
870
    /**
871
     * Builds the string corresponding to query parameters for top level results
872
     * (result set identified by the resource path) to be put in next page link.
873
     *
874
     * @return string|null string representing the query parameters in the URI
875
     *                     query parameter format, NULL if there
876
     *                     is no query parameters
877
     *                     required for the next link of top level result set
878
     */
879
    protected function getNextPageLinkQueryParametersForRootResourceSet()
880
    {
881
        $queryParameterString = null;
882
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
883
                     ODataConstants::HTTPQUERY_STRING_EXPAND,
884
                     ODataConstants::HTTPQUERY_STRING_ORDERBY,
885
                     ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
886
                     ODataConstants::HTTPQUERY_STRING_SELECT, ] as $queryOption) {
887
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
888
            if (null !== $value) {
889
                if (null !== $queryParameterString) {
890
                    $queryParameterString = $queryParameterString . '&';
891
                }
892
893
                $queryParameterString .= $queryOption . '=' . $value;
894
            }
895
        }
896
897
        $topCountValue = $this->getRequest()->getTopOptionCount();
898
        if (null !== $topCountValue) {
899
            $remainingCount = $topCountValue - $this->getRequest()->getTopCount();
900
            if (0 < $remainingCount) {
901
                if (null !== $queryParameterString) {
902
                    $queryParameterString .= '&';
903
                }
904
905
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
906
            }
907
        }
908
909
        if (null !== $queryParameterString) {
910
            $queryParameterString .= '&';
911
        }
912
913
        return $queryParameterString;
914
    }
915
916
    /**
917
     * @param $entryObject
918
     * @param $nonRelProp
919
     * @return ODataPropertyContent
920
     */
921
    private function writeProperties($entryObject, $nonRelProp)
922
    {
923
        $propertyContent = new ODataPropertyContent();
924
        foreach ($nonRelProp as $corn => $flake) {
925
            $resource = $nonRelProp[$corn]->getResourceType();
926
            if ($resource instanceof ResourceEntityType) {
927
                continue;
928
            }
929
            $result = $entryObject->$corn;
930
            $isBag = $flake->isKindOf(ResourcePropertyKind::BAG);
931
            $typePrepend = $isBag ? 'Collection(' : '';
932
            $typeAppend = $isBag ? ')' : '';
933
            $nonNull = null !== $result;
934
            $subProp = new ODataProperty();
935
            $subProp->name = strval($corn);
936
            $subProp->typeName = $typePrepend . $resource->getFullName() . $typeAppend;
937
938
            if ($nonNull && is_array($result)) {
939
                $subProp->value = $this->writeBagValue($resource, $result);
940 View Code Duplication
            } elseif ($resource instanceof ResourcePrimitiveType && $nonNull) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
941
                $rType = $resource->getInstanceType();
942
                assert($rType instanceof IType, get_class($rType));
943
                $subProp->value = $this->primitiveToString($rType, $result);
944
            } elseif ($resource instanceof ResourceComplexType && $nonNull) {
945
                $subProp->value = $this->writeComplexValue($resource, $result, $flake->getName());
946
            }
947
            $propertyContent->properties[$corn] = $subProp;
948
        }
949
        return $propertyContent;
950
    }
951
952
    /**
953
     * @return void
954
     */
955
    private function loadStackIfEmpty()
956
    {
957
        if (0 == count($this->lightStack)) {
958
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
959
            array_push($this->lightStack, ['type' => $typeName, 'property' => $typeName, 'count' => 1]);
960
        }
961
    }
962
963
    /**
964
     * Convert the given primitive value to string.
965
     * Note: This method will not handle null primitive value.
966
     *
967
     * @param IType &$primitiveResourceType Type of the primitive property
968
     *                                      whose value need to be converted
969
     * @param mixed $primitiveValue         Primitive value to convert
970
     *
971
     * @return string
972
     */
973
    private function primitiveToString(IType &$type, $primitiveValue)
974
    {
975
        if ($type instanceof Boolean) {
976
            $stringValue = (true === $primitiveValue) ? 'true' : 'false';
977
        } elseif ($type instanceof Binary) {
978
            $stringValue = base64_encode($primitiveValue);
979
        } elseif ($type instanceof DateTime && $primitiveValue instanceof \DateTime) {
980
            $stringValue = $primitiveValue->format(\DateTime::ATOM);
981
        } elseif ($type instanceof StringType) {
982
            $stringValue = utf8_encode($primitiveValue);
983
        } else {
984
            $stringValue = strval($primitiveValue);
985
        }
986
987
        return $stringValue;
988
    }
989
990
    public static function isMatchPrimitive($resourceKind)
991
    {
992
        if (16 > $resourceKind) {
993
            return false;
994
        }
995
        if (28 < $resourceKind) {
996
            return false;
997
        }
998
        return 0 == ($resourceKind % 4);
999
    }
1000
}
1001