Completed
Pull Request — master (#269)
by Christopher
14:37
created

CynicSerialiser::setService()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData\ObjectModel;
6
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
use ReflectionException;
35
36
/**
37
 * Class CynicSerialiser.
38
 * @package POData\ObjectModel
39
 */
40
class CynicSerialiser implements IObjectSerialiser
41
{
42
    /**
43
     * The service implementation.
44
     *
45
     * @var IService
46
     */
47
    protected $service;
48
49
    /**
50
     * Request description instance describes OData request the
51
     * the client has submitted and result of the request.
52
     *
53
     * @var RequestDescription
54
     */
55
    protected $request;
56
57
    /**
58
     * Collection of complex type instances used for cycle detection.
59
     *
60
     * @var array
61
     */
62
    protected $complexTypeInstanceCollection;
63
64
    /**
65
     * Absolute service Uri.
66
     *
67
     * @var string
68
     */
69
    protected $absoluteServiceUri;
70
71
    /**
72
     * Absolute service Uri with slash.
73
     *
74
     * @var string
75
     */
76
    protected $absoluteServiceUriWithSlash;
77
78
    /**
79
     * Holds reference to segment stack being processed.
80
     *
81
     * @var SegmentStack
82
     */
83
    protected $stack;
84
85
    /**
86
     * Lightweight stack tracking for recursive descent fill.
87
     */
88
    private $lightStack = [];
89
90
    /**
91
     * Update time to insert into ODataEntry/ODataFeed fields.
92
     * @var \DateTime;
93
     */
94
    private $updated;
95
96
    /**
97
     * Has base URI already been written out during serialisation?
98
     * @var bool;
99
     */
100
    private $isBaseWritten = false;
101
102
    /**
103
     * @param  IService                $service Reference to the data service instance
104
     * @param  RequestDescription|null $request Type instance describing the client submitted request
105
     * @throws \Exception
106
     */
107
    public function __construct(IService $service, RequestDescription $request = null)
108
    {
109
        $this->service                       = $service;
110
        $this->request                       = $request;
111
        $this->absoluteServiceUri            = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
112
        $this->absoluteServiceUriWithSlash   = rtrim($this->absoluteServiceUri, '/') . '/';
113
        $this->stack                         = new SegmentStack($request);
114
        $this->complexTypeInstanceCollection = [];
115
        $this->updated                       = DateTime::now();
116
    }
117
118
    /**
119
     * Write top level feed element.
120
     *
121
     * @param QueryResult &$entryObjects Results property contains array of entry resources to be written
122
     *
123
     * @throws ODataException
124
     * @throws InvalidOperationException
125
     * @return ODataFeed
126
     */
127
    public function writeTopLevelElements(QueryResult &$entryObjects)
128
    {
129
        $res = $entryObjects->results;
130
        if (!(is_array($res))) {
131
            throw new InvalidOperationException('!is_array($entryObjects->results)');
132
        }
133
134
        if (is_array($res) && 0 == count($entryObjects->results)) {
135
            $entryObjects->hasMore = false;
136
        }
137
138
        $this->loadStackIfEmpty();
139
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
140
141
        $title       = $this->getRequest()->getContainerName();
142
        $relativeUri = $this->getRequest()->getIdentifier();
143
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
144
145
        $selfLink        = new ODataLink('self', $title, null, $relativeUri);
146
147
        $odata               = new ODataFeed();
148
        $odata->title        = new ODataTitle($title);
149
        $odata->id           = $absoluteUri;
150
        $odata->setSelfLink($selfLink);
151
        $odata->updated      = $this->getUpdated()->format(DATE_ATOM);
152
        $odata->baseURI      = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
153
        $this->isBaseWritten = true;
154
155
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
156
            $odata->rowCount = $this->getRequest()->getCountValue();
157
        }
158
        foreach ($res as $entry) {
159
            if (!$entry instanceof QueryResult) {
160
                $query          = new QueryResult();
161
                $query->results = $entry;
162
            } else {
163
                $query = $entry;
164
            }
165
            $odata->entries[] = $this->writeTopLevelElement($query);
166
        }
167
168
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
169
        $requestTop  = $this->getRequest()->getTopOptionCount();
170
        $pageSize    = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
171
        $requestTop  = (null === $requestTop) ? $pageSize + 1 : $requestTop;
172
173
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
174
            $stackSegment        = $setName;
175
            $lastObject          = end($entryObjects->results);
176
            $segment             = $this->getNextLinkUri($lastObject);
177
            $nextLink            = new ODataLink(
178
                ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING,
179
                null,
180
                null,
181
                rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment
182
            );
183
            $odata->nextPageLink = $nextLink;
184
        }
185
186
        return $odata;
187
    }
188
189
    /**
190
     * Load processing stack if it's currently empty.
191
     *
192
     * @return void
193
     */
194
    private function loadStackIfEmpty()
195
    {
196
        if (0 == count($this->lightStack)) {
197
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
198
            array_push($this->lightStack, ['type' => $typeName, 'property' => $typeName, 'count' => 1]);
199
        }
200
    }
201
202
    /**
203
     * Gets reference to the request submitted by client.
204
     *
205
     * @return RequestDescription
206
     */
207
    public function getRequest()
208
    {
209
        assert(null !== $this->request, 'Request not yet set');
210
211
        return $this->request;
212
    }
213
214
    /**
215
     * Sets reference to the request submitted by client.
216
     *
217
     * @param RequestDescription $request
218
     */
219
    public function setRequest(RequestDescription $request)
220
    {
221
        $this->request = $request;
222
        $this->stack->setRequest($request);
223
    }
224
225
    /**
226
     * Get update timestamp.
227
     *
228
     * @return \DateTime
229
     */
230
    public function getUpdated()
231
    {
232
        return $this->updated;
233
    }
234
235
    /**
236
     * Write a top level entry resource.
237
     *
238
     * @param QueryResult $entryObject Results property contains reference to the entry object to be written
239
     *
240
     * @throws ODataException
241
     * @throws ReflectionException
242
     * @throws InvalidOperationException
243
     * @return ODataEntry|null
244
     */
245
    public function writeTopLevelElement(QueryResult $entryObject)
246
    {
247
        if (!isset($entryObject->results)) {
248
            array_pop($this->lightStack);
249
250
            return null;
251
        }
252
253
        assert(is_object($entryObject->results));
254
        $this->loadStackIfEmpty();
255
256
        $baseURI             = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
257
        $this->isBaseWritten = true;
258
259
        $stackCount   = count($this->lightStack);
260
        $topOfStack   = $this->lightStack[$stackCount - 1];
261
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
262
        assert($resourceType instanceof ResourceType, get_class($resourceType));
263
        $rawProp    = $resourceType->getAllProperties();
264
        $relProp    = [];
265
        $nonRelProp = [];
266
        $last       = end($this->lightStack);
267
        $projNodes  = ($last['type'] == $last['property']) ? $this->getProjectionNodes() : null;
268
269
        foreach ($rawProp as $prop) {
270
            $propName = $prop->getName();
271
            if ($prop->getResourceType() instanceof ResourceEntityType) {
272
                $relProp[$propName] = $prop;
273
            } else {
274
                $nonRelProp[$propName] = $prop;
275
            }
276
        }
277
        $rawCount    = count($rawProp);
278
        $relCount    = count($relProp);
279
        $nonRelCount = count($nonRelProp);
280
        assert(
281
            $rawCount == $relCount + $nonRelCount,
282
            'Raw property count ' . $rawCount . ', does not equal sum of relProp count, ' . $relCount
283
            . ', and nonRelPropCount,' . $nonRelCount
284
        );
285
286
        // now mask off against projNodes
287
        if (null !== $projNodes) {
288
            $keys = [];
289
            foreach ($projNodes as $node) {
290
                $keys[$node->getPropertyName()] = '';
291
            }
292
293
            $relProp    = array_intersect_key($relProp, $keys);
294
            $nonRelProp = array_intersect_key($nonRelProp, $keys);
295
        }
296
297
        $resourceSet = $resourceType->getCustomState();
298
        assert($resourceSet instanceof ResourceSet);
299
        $type  = $resourceType->getFullName();
300
301
        $relativeUri = $this->getEntryInstanceKey(
302
            $entryObject->results,
303
            $resourceType,
304
            $resourceSet->getName()
305
        );
306
        $absoluteUri = rtrim(strval($this->absoluteServiceUri), '/') . '/' . $relativeUri;
307
        $mediaLinks = $this->writeMediaData(
308
            $entryObject->results,
309
            $type,
310
            $relativeUri,
311
            $resourceType
312
        );
313
        $mediaLink = array_shift($mediaLinks);
314
315
316
        $propertyContent = $this->writeProperties($entryObject->results, $nonRelProp);
317
318
        $links = [];
319
        foreach ($relProp as $prop) {
320
            $propKind = $prop->getKind();
321
322
            assert(
323
                ResourcePropertyKind::RESOURCESET_REFERENCE() == $propKind
324
                || ResourcePropertyKind::RESOURCE_REFERENCE() == $propKind,
325
                '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
326
                . ' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE'
327
            );
328
            $propTail             = ResourcePropertyKind::RESOURCE_REFERENCE() == $propKind ? 'entry' : 'feed';
329
            $propType             = 'application/atom+xml;type=' . $propTail;
330
            $propName             = $prop->getName();
331
            $nuLink               = new ODataLink(
332
                ODataConstants::ODATA_RELATED_NAMESPACE . $propName,
333
                $propName,
334
                $propType,
335
                $relativeUri . '/' . $propName,
336
                'feed' === $propTail
337
            );
338
339
340
            $shouldExpand = $this->shouldExpandSegment($propName);
341
342
            $navProp = new ODataNavigationPropertyInfo($prop, $shouldExpand);
343
            if ($navProp->expanded) {
344
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
345
            }
346
            $nuLink->setIsExpanded(null !== $nuLink->getExpandedResult() && null !== $nuLink->getExpandedResult()->getData());
347
            assert(null !== $nuLink->isCollection());
348
349
            $links[] = $nuLink;
350
        }
351
352
        $odata                   = new ODataEntry();
353
        $odata->resourceSetName  = $resourceSet->getName();
354
        $odata->id               = $absoluteUri;
355
        $odata->title            = new ODataTitle($resourceType->getName());
356
        $odata->type             = new ODataCategory($type);
357
        $odata->propertyContent  = $propertyContent;
358
        $odata->isMediaLinkEntry = true === $resourceType->isMediaLinkEntry() ? true : null;
359
        $odata->editLink         = new ODataLink('edit', $resourceType->getName(), null, $relativeUri);
360
        assert(!is_array($mediaLink));
361
        $odata->mediaLink        = $mediaLink;
362
        $odata->mediaLinks       = $mediaLinks;
363
        $odata->links            = $links;
364
        $odata->updated          = $this->getUpdated()->format(DATE_ATOM);
365
        $odata->baseURI          = $baseURI;
366
367
        $newCount = count($this->lightStack);
368
        assert(
369
            $newCount == $stackCount,
370
            'Should have ' . $stackCount . 'elements in stack, have ' . $newCount . 'elements'
371
        );
372
        --$this->lightStack[$newCount - 1]['count'];
373
        if (0 == $this->lightStack[$newCount - 1]['count']) {
374
            array_pop($this->lightStack);
375
        }
376
377
        return $odata;
378
    }
379
380
    /**
381
     * Gets the data service instance.
382
     *
383
     * @return IService
384
     */
385
    public function getService()
386
    {
387
        return $this->service;
388
    }
389
390
    /**
391
     * Sets the data service instance.
392
     *
393
     * @param IService $service
394
     */
395
    public function setService(IService $service)
396
    {
397
        $this->service                     = $service;
398
        $this->absoluteServiceUri          = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
399
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
400
    }
401
402
    /**
403
     * Gets collection of projection nodes under the current node.
404
     *
405
     * @throws InvalidOperationException
406
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes describing projections for the current
407
     *                                                        segment, If this method returns null it means no
408
     *                                                        projections are to be applied and the entire resource for
409
     *                                                        the current segment should be serialized, If it returns
410
     *                                                        non-null only the properties described by the returned
411
     *                                                        projection segments should be serialized
412
     */
413
    protected function getProjectionNodes()
414
    {
415
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
416
        if (null === $expandedProjectionNode || $expandedProjectionNode->canSelectAllProperties()) {
417
            return null;
418
        }
419
420
        return $expandedProjectionNode->getChildNodes();
421
    }
422
423
    /**
424
     * Find a 'ExpandedProjectionNode' instance in the projection tree
425
     * which describes the current segment.
426
     *
427
     * @throws InvalidOperationException
428
     * @return RootProjectionNode|ExpandedProjectionNode|null
429
     */
430
    protected function getCurrentExpandedProjectionNode()
431
    {
432
        /** @var RootProjectionNode|null $expandedProjectionNode */
433
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
434
        if (null === $expandedProjectionNode) {
435
            return null;
436
        } else {
437
            $segmentNames = $this->getStack()->getSegmentNames();
438
            $depth        = count($segmentNames);
439
            // $depth == 1 means serialization of root entry
440
            //(the resource identified by resource path) is going on,
441
            //so control won't get into the below for loop.
442
            //we will directly return the root node,
443
            //which is 'ExpandedProjectionNode'
444
            // for resource identified by resource path.
445
            if (0 != $depth) {
446
                for ($i = 1; $i < $depth; ++$i) {
447
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segmentNames[$i]);
448
                    if (null === $expandedProjectionNode) {
449
                        throw new InvalidOperationException('is_null($expandedProjectionNode)');
450
                    }
451
                    if (!$expandedProjectionNode instanceof ExpandedProjectionNode) {
452
                        $msg = '$expandedProjectionNode not instanceof ExpandedProjectionNode';
453
                        throw new InvalidOperationException($msg);
454
                    }
455
                }
456
            }
457
        }
458
459
        return $expandedProjectionNode;
460
    }
461
462
    /**
463
     * Gets the segment stack instance.
464
     *
465
     * @return SegmentStack
466
     */
467
    public function getStack()
468
    {
469
        return $this->stack;
470
    }
471
472
    /**
473
     * @param  object              $entityInstance
474
     * @param  ResourceType        $resourceType
475
     * @param  string              $containerName
476
     * @throws ReflectionException
477
     * @throws ODataException
478
     * @return string
479
     */
480
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
481
    {
482
        assert(is_object($entityInstance));
483
        $typeName      = $resourceType->getName();
484
        $keyProperties = $resourceType->getKeyProperties();
485
        assert(0 != count($keyProperties), 'count($keyProperties) == 0');
486
        $keyString = $containerName . '(';
487
        $comma     = null;
488
        foreach ($keyProperties as $keyName => $resourceProperty) {
489
            $keyType = $resourceProperty->getInstanceType();
490
            assert($keyType instanceof IType, '$keyType not instanceof IType');
491
            $keyName  = $resourceProperty->getName();
492
            $keyValue = $entityInstance->{$keyName};
493
            if (!isset($keyValue)) {
494
                $msg = Messages::badQueryNullKeysAreNotSupported($typeName, $keyName);
495
                throw ODataException::createInternalServerError($msg);
496
            }
497
498
            $keyValue = $keyType->convertToOData(strval($keyValue));
499
            $keyString .= $comma . $keyName . '=' . $keyValue;
500
            $comma = ',';
501
        }
502
503
        $keyString .= ')';
504
505
        return $keyString;
506
    }
507
508
    /**
509
     * @param $entryObject
510
     * @param $type
511
     * @param $relativeUri
512
     * @param $resourceType
513
     *
514
     * @return ODataMediaLink[]|null[]
515
     */
516
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
517
    {
518
        $context               = $this->getService()->getOperationContext();
519
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
520
        assert(null != $streamProviderWrapper, 'Retrieved stream provider must not be null');
521
522
        $mediaLink = null;
523
        if ($resourceType->isMediaLinkEntry()) {
524
            $eTag      = $streamProviderWrapper->getStreamETag2($entryObject, $context, null);
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

524
            /** @scrutinizer ignore-call */ 
525
            $eTag      = $streamProviderWrapper->getStreamETag2($entryObject, $context, null);

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...
525
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
526
        }
527
        $mediaLinks = [];
528
        if ($resourceType->hasNamedStream()) {
529
            $namedStreams = $resourceType->getAllNamedStreams();
530
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
531
                $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

531
                /** @scrutinizer ignore-call */ 
532
                $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...
532
                    $entryObject,
533
                    $context,
534
                    $resourceStreamInfo,
535
                    $relativeUri
536
                );
537
                $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

537
                /** @scrutinizer ignore-call */ 
538
                $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...
538
                    $entryObject,
539
                    $context,
540
                    $resourceStreamInfo
541
                );
542
                $eTag = $streamProviderWrapper->getStreamETag2(
543
                    $entryObject,
544
                    $context,
545
                    $resourceStreamInfo
546
                );
547
548
                $nuLink       = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
549
                $mediaLinks[] = $nuLink;
550
            }
551
        }
552
553
        return array_merge([$mediaLink], $mediaLinks);
554
    }
555
556
    /**
557
     * @param $entryObject
558
     * @param array<string, ResourceProperty> $nonRelProp
559
     *
560
     * @throws ReflectionException
561
     * @throws InvalidOperationException
562
     * @return ODataPropertyContent
563
     */
564
    private function writeProperties($entryObject, $nonRelProp)
565
    {
566
        $properties = [];
567
        foreach ($nonRelProp as $corn => $flake) {
568
            /** @var ResourceType $resource */
569
            $resource = $nonRelProp[$corn]->getResourceType();
570
            if ($resource instanceof ResourceEntityType) {
571
                continue;
572
            }
573
            $result            = $entryObject->{$corn};
574
            $isBag             = $flake->isKindOf(ResourcePropertyKind::BAG());
575
            $typePrepend       = $isBag ? 'Collection(' : '';
576
            $typeAppend        = $isBag ? ')' : '';
577
            $nonNull           = null !== $result;
578
            $name     = strval($corn);
579
            $typeName = $typePrepend . $resource->getFullName() . $typeAppend;
580
            $value = null;
581
            if ($nonNull && is_array($result)) {
582
                $value = $this->writeBagValue($resource, $result);
583
            } elseif ($resource instanceof ResourcePrimitiveType && $nonNull) {
584
                $rType = $resource->getInstanceType();
585
                if (!$rType instanceof IType) {
586
                    throw new InvalidOperationException(get_class($rType));
587
                }
588
                $value = $this->primitiveToString($rType, $result);
589
            } elseif ($resource instanceof ResourceComplexType && $nonNull) {
590
                $value = $this->writeComplexValue($resource, $result, $flake->getName());
591
            }
592
            $properties[$corn] = new ODataProperty($name, $typeName, $value);
593
        }
594
595
        return new ODataPropertyContent($properties);
596
    }
597
598
    /**
599
     * @param ResourceType $resourceType
600
     * @param $result
601
     *
602
     * @throws ReflectionException
603
     * @throws InvalidOperationException
604
     * @return ODataBagContent|null
605
     */
606
    protected function writeBagValue(ResourceType &$resourceType, $result)
607
    {
608
        $bagNullOrArray = null === $result || is_array($result);
609
        if (!$bagNullOrArray) {
610
            throw new InvalidOperationException('Bag parameter must be null or array');
611
        }
612
        $typeKind               = $resourceType->getResourceTypeKind();
613
        $typePrimitiveOrComplex = ResourceTypeKind::PRIMITIVE() == $typeKind
614
            || ResourceTypeKind::COMPLEX() == $typeKind;
615
        if (!$typePrimitiveOrComplex) {
616
            throw new InvalidOperationException('$bagItemResourceTypeKind != ResourceTypeKind::PRIMITIVE'
617
                . ' && $bagItemResourceTypeKind != ResourceTypeKind::COMPLEX');
618
        }
619
        if (null == $result) {
620
            return null;
621
        }
622
        $bag = new ODataBagContent();
623
        foreach ($result as $value) {
624
            if (isset($value)) {
625
                if (ResourceTypeKind::PRIMITIVE() == $typeKind) {
626
                    $instance = $resourceType->getInstanceType();
627
                    if (!$instance instanceof IType) {
628
                        throw new InvalidOperationException(get_class($instance));
629
                    }
630
                    $bag->addPropertyContent($this->primitiveToString($instance, $value));
631
                } elseif (ResourceTypeKind::COMPLEX() == $typeKind) {
632
                    $bag->addPropertyContent($this->writeComplexValue($resourceType, $value));
633
                }
634
            }
635
        }
636
637
        return $bag;
638
    }
639
640
    /**
641
     * Convert the given primitive value to string.
642
     * Note: This method will not handle null primitive value.
643
     *
644
     * @param IType &$type          Type of the primitive property
645
     *                              whose value need to be converted
646
     * @param mixed $primitiveValue Primitive value to convert
647
     *
648
     * @return string
649
     */
650
    private function primitiveToString(IType &$type, $primitiveValue)
651
    {
652
        if ($type instanceof Boolean) {
653
            $stringValue = (true === $primitiveValue) ? 'true' : 'false';
654
        } elseif ($type instanceof Binary) {
655
            $stringValue = base64_encode($primitiveValue);
656
        } elseif ($type instanceof DateTime && $primitiveValue instanceof \DateTime) {
657
            $stringValue = $primitiveValue->format(\DateTime::ATOM);
658
        } elseif ($type instanceof StringType) {
659
            $stringValue = mb_convert_encoding(strval($primitiveValue), 'UTF-8');
660
        } else {
661
            $stringValue = strval($primitiveValue);
662
        }
663
664
        return $stringValue;
665
    }
666
667
    /**
668
     * @param ResourceType $resourceType
669
     * @param object       $result
670
     * @param string|null  $propertyName
671
     *
672
     * @throws ReflectionException
673
     * @throws InvalidOperationException
674
     * @return ODataPropertyContent
675
     */
676
    protected function writeComplexValue(ResourceType &$resourceType, &$result, $propertyName = null)
677
    {
678
        assert(is_object($result), 'Supplied $customObject must be an object');
679
680
        $count = count($this->complexTypeInstanceCollection);
681
        for ($i = 0; $i < $count; ++$i) {
682
            if ($this->complexTypeInstanceCollection[$i] === $result) {
683
                throw new InvalidOperationException(
684
                    Messages::objectModelSerializerLoopsNotAllowedInComplexTypes($propertyName)
685
                );
686
            }
687
        }
688
689
        $this->complexTypeInstanceCollection[$count] = &$result;
690
691
        $properties         = [];
692
        $resourceProperties = $resourceType->getAllProperties();
693
        // first up, handle primitive properties
694
        foreach ($resourceProperties as $prop) {
695
            $resourceKind           = $prop->getKind();
696
            $propName               = $prop->getName();
697
            $name = $propName;
698
            $typeName = null;
699
            $value = null;
700
            $raw                    = $result->{$propName};
701
            if (static::isMatchPrimitive($resourceKind)) {
702
                $iType = $prop->getInstanceType();
703
                if (!$iType instanceof IType) {
704
                    throw new InvalidOperationException(get_class($iType));
705
                }
706
707
                $typeName = $iType->getFullTypeName();
708
709
                $rType = $prop->getResourceType()->getInstanceType();
710
                if (!$rType instanceof IType) {
711
                    throw new InvalidOperationException(get_class($rType));
712
                }
713
                if (null !== $raw) {
714
                    $value = $this->primitiveToString($rType, $raw);
715
                }
716
            } elseif (ResourcePropertyKind::COMPLEX_TYPE() == $resourceKind) {
717
                $rType                      = $prop->getResourceType();
718
                $typeName = $rType->getFullName();
719
                if (null !== $raw) {
720
                    $value = $this->writeComplexValue($rType, $raw, $propName);
721
                }
722
            }
723
            $properties[$propName] = new ODataProperty($name, $typeName, $value);
724
        }
725
726
        unset($this->complexTypeInstanceCollection[$count]);
727
728
        return new ODataPropertyContent($properties);
729
    }
730
731
    /**
732
     * Is the supplied resourceKind representing a primitive value?
733
     *
734
     * @param  int|ResourcePropertyKind $resourceKind
735
     * @return bool
736
     */
737
    public static function isMatchPrimitive($resourceKind): bool
738
    {
739
        $value = $resourceKind instanceof ResourcePropertyKind ? $resourceKind->getValue() : $resourceKind;
740
        if (16 > $value) {
741
            return false;
742
        }
743
        if (28 < $value) {
744
            return false;
745
        }
746
747
        return 0 == ($value % 4);
748
    }
749
750
    /**
751
     * Check whether to expand a navigation property or not.
752
     *
753
     * @param string $navigationPropertyName Name of navigation property in question
754
     *
755
     * @throws InvalidOperationException
756
     * @return bool                      True if the given navigation should be expanded, otherwise false
757
     */
758
    protected function shouldExpandSegment($navigationPropertyName)
759
    {
760
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
761
        if (null === $expandedProjectionNode) {
762
            return false;
763
        }
764
765
        $expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName);
766
767
        // null is a valid input to an instanceof call as of PHP 5.6 - will always return false
768
        return $expandedProjectionNode instanceof ExpandedProjectionNode;
769
    }
770
771
    /**
772
     * @param  QueryResult               $entryObject
773
     * @param  ResourceProperty          $prop
774
     * @param  ODataLink                 $nuLink
775
     * @param  ResourcePropertyKind      $propKind
776
     * @param  string                    $propName
777
     * @throws InvalidOperationException
778
     * @throws ODataException
779
     * @throws ReflectionException
780
     */
781
    private function expandNavigationProperty(
782
        QueryResult $entryObject,
783
        ResourceProperty $prop,
784
        ODataLink $nuLink,
785
        ResourcePropertyKind $propKind,
786
        string $propName
787
    ) {
788
        $nextName             = $prop->getResourceType()->getName();
789
        $nuLink->setIsExpanded(true);
790
        $value                = $entryObject->results->{$propName};
791
        $isCollection         = ResourcePropertyKind::RESOURCESET_REFERENCE() == $propKind;
792
        $nuLink->setIsCollection($isCollection);
793
        $nullResult           = null === $value;
794
        $object               = (is_object($value));
795
        $resultCount          = ($nullResult) ? 0 : ($object ? 1 : count($value));
796
797
        if (0 < $resultCount) {
798
            $result          = new QueryResult();
799
            $result->results = $value;
800
            if (!$nullResult) {
801
                $newStackLine = ['type' => $nextName, 'property' => $propName, 'count' => $resultCount];
802
                array_push($this->lightStack, $newStackLine);
803
                if (isset($value)) {
804
                    if (!$isCollection) {
805
                        $nuLink->setType('application/atom+xml;type=entry');
806
                        $expandedResult = $this->writeTopLevelElement($result);
807
                    } else {
808
                        $nuLink->setType('application/atom+xml;type=feed');
809
                        $expandedResult = $this->writeTopLevelElements($result);
810
                    }
811
                    if(null !== $expandedResult) {
812
                        $nuLink->setExpandedResult(new ODataExpandedResult($expandedResult));
813
                    }
814
                }
815
            }
816
        } else {
817
            $type = $this->getService()->getProvidersWrapper()->resolveResourceType($nextName);
818
            if (!$isCollection) {
819
                $result                  = new ODataEntry();
820
                $result->resourceSetName = $type->getName();
821
            } else {
822
                $result                 = new ODataFeed();
823
                $result->setSelfLink(new ODataLink(ODataConstants::ATOM_SELF_RELATION_ATTRIBUTE_VALUE));
824
            }
825
            $nuLink->setExpandedResult(new ODataExpandedResult($result));
826
        }
827
        if (null !== $nuLink->getExpandedResult() && null !== $nuLink->getExpandedResult()->getData() && null !== $nuLink->getExpandedResult()->getData()->getSelfLink()) {
828
            $nuLink->getExpandedResult()->getData()->getSelfLink()->setTitle($propName);
829
            $nuLink->getExpandedResult()->getData()->getSelfLink()->setUrl($nuLink->getUrl());
830
            $nuLink->getExpandedResult()->getData()->title           = new ODataTitle($propName);
831
            $nuLink->getExpandedResult()->getData()->id              = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->getUrl();
832
        }
833
    }
834
835
    /**
836
     * Get next page link from the given entity instance.
837
     *
838
     * @param mixed &$lastObject Last object serialized to be
839
     *                           used for generating $skiptoken
840
     *
841
     * @throws ODataException
842
     * @throws InvalidOperationException
843
     * @return string                    for the link for next page
844
     */
845
    protected function getNextLinkUri($lastObject)
846
    {
847
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
848
        $internalOrderByInfo           = $currentExpandedProjectionNode->getInternalOrderByInfo();
849
        assert(null !== $internalOrderByInfo);
850
        assert(is_object($internalOrderByInfo));
851
        assert($internalOrderByInfo instanceof InternalOrderByInfo, get_class($internalOrderByInfo));
852
        $numSegments          = count($internalOrderByInfo->getOrderByPathSegments());
853
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
854
855
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
856
        assert(null !== $skipToken, '!is_null($skipToken)');
857
        $token     = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
858
        $skipToken = '?' . $queryParameterString . $token . $skipToken;
859
860
        return $skipToken;
861
    }
862
863
    /**
864
     * Builds the string corresponding to query parameters for top level results
865
     * (result set identified by the resource path) to be put in next page link.
866
     *
867
     * @return string|null string representing the query parameters in the URI
868
     *                     query parameter format, NULL if there
869
     *                     is no query parameters
870
     *                     required for the next link of top level result set
871
     */
872
    protected function getNextPageLinkQueryParametersForRootResourceSet()
873
    {
874
        $queryParameterString = null;
875
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
876
            ODataConstants::HTTPQUERY_STRING_EXPAND,
877
            ODataConstants::HTTPQUERY_STRING_ORDERBY,
878
            ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
879
            ODataConstants::HTTPQUERY_STRING_SELECT,] as $queryOption) {
880
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
881
            if (null !== $value) {
882
                if (null !== $queryParameterString) {
883
                    $queryParameterString = $queryParameterString . '&';
0 ignored issues
show
Bug introduced by
Are you sure $queryParameterString of type void can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

883
                    $queryParameterString = /** @scrutinizer ignore-type */ $queryParameterString . '&';
Loading history...
884
                }
885
886
                $queryParameterString .= $queryOption . '=' . $value;
887
            }
888
        }
889
890
        $topCountValue = $this->getRequest()->getTopOptionCount();
891
        if (null !== $topCountValue) {
892
            $remainingCount = $topCountValue - $this->getRequest()->getTopCount();
893
            if (0 < $remainingCount) {
894
                if (null !== $queryParameterString) {
895
                    $queryParameterString .= '&';
896
                }
897
898
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
899
            }
900
        }
901
902
        if (null !== $queryParameterString) {
903
            $queryParameterString .= '&';
904
        }
905
906
        return $queryParameterString;
907
    }
908
909
    /**
910
     * Write top level url collection.
911
     *
912
     * @param QueryResult $entryObjects Results property contains the array of entry resources whose urls are
913
     *                                  to be written
914
     *
915
     * @throws ODataException
916
     * @throws ReflectionException
917
     * @throws InvalidOperationException
918
     * @return ODataURLCollection
919
     */
920
    public function writeUrlElements(QueryResult $entryObjects)
921
    {
922
        $urls         = [];
923
        $count        = null;
924
        $nextPageLink = null;
925
        if (!empty($entryObjects->results)) {
926
            $i = 0;
927
            foreach ($entryObjects->results as $entryObject) {
928
                if (!$entryObject instanceof QueryResult) {
929
                    $query          = new QueryResult();
930
                    $query->results = $entryObject;
931
                } else {
932
                    $query = $entryObject;
933
                }
934
                $urls[$i] = $this->writeUrlElement($query);
935
                ++$i;
936
            }
937
938
            if ($i > 0 && true === $entryObjects->hasMore) {
939
                $stackSegment       = $this->getRequest()->getTargetResourceSetWrapper()->getName();
940
                $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

940
                $lastObject         = end(/** @scrutinizer ignore-type */ $entryObjects->results);
Loading history...
941
                $segment            = $this->getNextLinkUri($lastObject);
942
                $nextLink           = new ODataLink(
943
                    ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING,
944
                    null,
945
                    null,
946
                    ltrim(rtrim(strval($this->absoluteServiceUri), '/') . '/' . $stackSegment . $segment, '/')
947
                );
948
                $nextPageLink = $nextLink;
949
            }
950
        }
951
952
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
953
            $count = $this->getRequest()->getCountValue();
954
        }
955
956
        return new ODataURLCollection($urls, $nextPageLink, $count);
957
    }
958
959
    /**
960
     * Write top level url element.
961
     *
962
     * @param QueryResult $entryObject Results property contains the entry resource whose url to be written
963
     *
964
     * @throws ReflectionException
965
     * @throws ODataException
966
     * @return ODataURL
967
     */
968
    public function writeUrlElement(QueryResult $entryObject)
969
    {
970
        $url = null;
971
972
        /** @var object|null $results */
973
        $results = $entryObject->results;
974
        if (null !== $results) {
975
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
976
            $relativeUri         = $this->getEntryInstanceKey(
977
                $results,
978
                $currentResourceType,
979
                $this->getCurrentResourceSetWrapper()->getName()
980
            );
981
982
            $url = new ODataURL(rtrim(strval($this->absoluteServiceUri), '/') . '/' . $relativeUri);
983
        }
984
985
        return $url;
986
    }
987
988
    /**
989
     * Resource set wrapper for the resource being serialized.
990
     *
991
     * @return ResourceSetWrapper
992
     */
993
    protected function getCurrentResourceSetWrapper()
994
    {
995
        $segmentWrappers = $this->getStack()->getSegmentWrappers();
996
        $count           = count($segmentWrappers);
997
998
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $segmentWrappers[$count - 1];
999
    }
1000
1001
    /**
1002
     * Write top level complex resource.
1003
     *
1004
     * @param QueryResult  &$complexValue Results property contains the complex object to be written
1005
     * @param string       $propertyName  The name of the complex property
1006
     * @param ResourceType &$resourceType Describes the type of complex object
1007
     *
1008
     * @throws ReflectionException
1009
     * @throws InvalidOperationException
1010
     * @return ODataPropertyContent
1011
     */
1012
    public function writeTopLevelComplexObject(QueryResult &$complexValue, $propertyName, ResourceType &$resourceType)
1013
    {
1014
        $result = $complexValue->results;
1015
1016
        $name     = $propertyName;
1017
        $typeName = $resourceType->getFullName();
1018
        $value = null;
1019
        if (null !== $result) {
1020
            if (!is_object($result)) {
1021
                throw new InvalidOperationException('Supplied $customObject must be an object');
1022
            }
1023
            $internalContent      = $this->writeComplexValue($resourceType, $result);
1024
            $value = $internalContent;
1025
        }
1026
1027
        return new ODataPropertyContent(
1028
            [
1029
                $propertyName => new ODataProperty($name, $typeName, $value)
1030
            ]
1031
        );
1032
    }
1033
1034
    /**
1035
     * Write top level bag resource.
1036
     *
1037
     * @param QueryResult  $bagValue
1038
     * @param string       $propertyName  The name of the bag property
1039
     * @param ResourceType &$resourceType Describes the type of bag object
1040
     *
1041
     * @throws ReflectionException
1042
     * @throws InvalidOperationException
1043
     * @return ODataPropertyContent
1044
     */
1045
    public function writeTopLevelBagObject(QueryResult &$bagValue, $propertyName, ResourceType &$resourceType)
1046
    {
1047
        $result = $bagValue->results;
1048
1049
        $odataProperty           = new ODataProperty(
1050
            $propertyName,
1051
            'Collection(' . $resourceType->getFullName() . ')',
1052
            $this->writeBagValue($resourceType, $result)
1053
        );
1054
1055
        return new ODataPropertyContent([$propertyName => $odataProperty]);
1056
    }
1057
1058
    /**
1059
     * Write top level primitive value.
1060
     *
1061
     * @param QueryResult      &$primitiveValue   Results property contains the primitive value to be written
1062
     * @param ResourceProperty &$resourceProperty Resource property describing the primitive property to be written
1063
     *
1064
     * @throws ReflectionException
1065
     * @throws InvalidOperationException
1066
     * @return ODataPropertyContent
1067
     */
1068
    public function writeTopLevelPrimitive(QueryResult &$primitiveValue, ResourceProperty &$resourceProperty = null)
1069
    {
1070
        if (null === $resourceProperty) {
1071
            throw new InvalidOperationException('Resource property must not be null');
1072
        }
1073
        $name = $resourceProperty->getName();
1074
        $value = null;
1075
        $iType = $resourceProperty->getInstanceType();
1076
        if (!$iType instanceof IType) {
1077
            throw new InvalidOperationException(get_class($iType));
1078
        }
1079
        $typeName = $iType->getFullTypeName();
1080
        if (null !== $primitiveValue->results) {
1081
            $rType = $resourceProperty->getResourceType()->getInstanceType();
1082
            if (!$rType instanceof IType) {
1083
                throw new InvalidOperationException(get_class($rType));
1084
            }
1085
            $value = $this->primitiveToString($rType, $primitiveValue->results);
1086
        }
1087
1088
        return new ODataPropertyContent(
1089
            [
1090
                $name => new ODataProperty($name, $typeName, $value)
1091
            ]
1092
        );
1093
    }
1094
1095
    /**
1096
     * Wheter next link is needed for the current resource set (feed)
1097
     * being serialized.
1098
     *
1099
     * @param int $resultSetCount Number of entries in the current
1100
     *                            resource set
1101
     *
1102
     * @return bool true if the feed must have a next page link
1103
     */
1104
    protected function needNextPageLink($resultSetCount)
1105
    {
1106
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
1107
        $recursionLevel     = count($this->getStack()->getSegmentNames());
1108
        $pageSize           = $currentResourceSet->getResourceSetPageSize();
1109
1110
        if (1 == $recursionLevel) {
1111
            //presence of $top option affect next link for root container
1112
            $topValueCount = $this->getRequest()->getTopOptionCount();
1113
            if (null !== $topValueCount && ($topValueCount <= $pageSize)) {
1114
                return false;
1115
            }
1116
        }
1117
1118
        return $resultSetCount == $pageSize;
1119
    }
1120
}
1121