Passed
Pull Request — master (#269)
by Christopher
02:57
created

CynicSerialiser::expandNavigationProperty()   C

Complexity

Conditions 12
Paths 64

Size

Total Lines 50
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 12
eloc 36
c 4
b 0
f 0
nc 64
nop 5
dl 0
loc 50
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
146
        $selfLink        = new ODataLink('self', $title, null, $relativeUri);
147
        $title        = new ODataTitle($title);
148
        $id           = $absoluteUri;
149
        $updated      = $this->getUpdated()->format(DATE_ATOM);
150
        $baseURI      = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
151
        $entries      = [];
152
        $this->isBaseWritten = true;
153
        $nextLink = null;
154
155
        $rowCount = $this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT() ?
156
            $this->getRequest()->getCountValue() :
157
            null;
158
        foreach ($res as $entry) {
159
            $query = $entry instanceof QueryResult ? $entry : new QueryResult($entry);
160
            $entries[] = $this->writeTopLevelElement($query);
161
        }
162
163
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
164
        $requestTop  = $this->getRequest()->getTopOptionCount();
165
        $pageSize    = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
166
        $requestTop  = (null === $requestTop) ? $pageSize + 1 : $requestTop;
167
168
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
169
            $stackSegment        = $setName;
170
            $lastObject          = end($entryObjects->results);
171
            $segment             = $this->getNextLinkUri($lastObject);
172
            $nextLink            = new ODataNextPageLink(
173
                rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment
174
            );
175
        }
176
177
        return new ODataFeed($id,$title,$selfLink,$rowCount,$nextLink,$entries,$updated,$baseURI);
178
    }
179
180
    /**
181
     * Load processing stack if it's currently empty.
182
     *
183
     * @return void
184
     */
185
    private function loadStackIfEmpty()
186
    {
187
        if (0 == count($this->lightStack)) {
188
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
189
            array_push($this->lightStack, ['type' => $typeName, 'property' => $typeName, 'count' => 1]);
190
        }
191
    }
192
193
    /**
194
     * Gets reference to the request submitted by client.
195
     *
196
     * @return RequestDescription
197
     */
198
    public function getRequest()
199
    {
200
        assert(null !== $this->request, 'Request not yet set');
201
202
        return $this->request;
203
    }
204
205
    /**
206
     * Sets reference to the request submitted by client.
207
     *
208
     * @param RequestDescription $request
209
     */
210
    public function setRequest(RequestDescription $request)
211
    {
212
        $this->request = $request;
213
        $this->stack->setRequest($request);
214
    }
215
216
    /**
217
     * Get update timestamp.
218
     *
219
     * @return \DateTime
220
     */
221
    public function getUpdated()
222
    {
223
        return $this->updated;
224
    }
225
226
    /**
227
     * Write a top level entry resource.
228
     *
229
     * @param QueryResult $entryObject Results property contains reference to the entry object to be written
230
     *
231
     * @throws ODataException
232
     * @throws ReflectionException
233
     * @throws InvalidOperationException
234
     * @return ODataEntry|null
235
     */
236
    public function writeTopLevelElement(QueryResult $entryObject)
237
    {
238
        if (!isset($entryObject->results)) {
239
            array_pop($this->lightStack);
240
241
            return null;
242
        }
243
244
        assert(is_object($entryObject->results));
245
        $this->loadStackIfEmpty();
246
247
        $baseURI             = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
248
        $this->isBaseWritten = true;
249
250
        $stackCount   = count($this->lightStack);
251
        $topOfStack   = $this->lightStack[$stackCount - 1];
252
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
253
        assert($resourceType instanceof ResourceType, get_class($resourceType));
254
        $rawProp    = $resourceType->getAllProperties();
255
        $relProp    = [];
256
        $nonRelProp = [];
257
        $last       = end($this->lightStack);
258
        $projNodes  = ($last['type'] == $last['property']) ? $this->getProjectionNodes() : null;
259
260
        foreach ($rawProp as $prop) {
261
            $propName = $prop->getName();
262
            if ($prop->getResourceType() instanceof ResourceEntityType) {
263
                $relProp[$propName] = $prop;
264
            } else {
265
                $nonRelProp[$propName] = $prop;
266
            }
267
        }
268
        $rawCount    = count($rawProp);
269
        $relCount    = count($relProp);
270
        $nonRelCount = count($nonRelProp);
271
        assert(
272
            $rawCount == $relCount + $nonRelCount,
273
            'Raw property count ' . $rawCount . ', does not equal sum of relProp count, ' . $relCount
274
            . ', and nonRelPropCount,' . $nonRelCount
275
        );
276
277
        // now mask off against projNodes
278
        if (null !== $projNodes) {
279
            $keys = [];
280
            foreach ($projNodes as $node) {
281
                $keys[$node->getPropertyName()] = '';
282
            }
283
284
            $relProp    = array_intersect_key($relProp, $keys);
285
            $nonRelProp = array_intersect_key($nonRelProp, $keys);
286
        }
287
288
        $resourceSet = $resourceType->getCustomState();
289
        assert($resourceSet instanceof ResourceSet);
290
        $type  = $resourceType->getFullName();
291
292
        $relativeUri = $this->getEntryInstanceKey(
293
            $entryObject->results,
294
            $resourceType,
295
            $resourceSet->getName()
296
        );
297
        $absoluteUri = rtrim(strval($this->absoluteServiceUri), '/') . '/' . $relativeUri;
298
        $mediaLinks = $this->writeMediaData(
299
            $entryObject->results,
300
            $type,
301
            $relativeUri,
302
            $resourceType
303
        );
304
        $mediaLink = array_shift($mediaLinks);
305
306
307
        $propertyContent = $this->writeProperties($entryObject->results, $nonRelProp);
308
309
        $links = [];
310
        foreach ($relProp as $prop) {
311
            $propKind = $prop->getKind();
312
313
            assert(
314
                ResourcePropertyKind::RESOURCESET_REFERENCE() == $propKind
315
                || ResourcePropertyKind::RESOURCE_REFERENCE() == $propKind,
316
                '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
317
                . ' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE'
318
            );
319
            $propTail             = ResourcePropertyKind::RESOURCE_REFERENCE() == $propKind ? 'entry' : 'feed';
320
            $propType             = 'application/atom+xml;type=' . $propTail;
321
            $propName             = $prop->getName();
322
            $nuLink               = new ODataLink(
323
                ODataConstants::ODATA_RELATED_NAMESPACE . $propName,
324
                $propName,
325
                $propType,
326
                $relativeUri . '/' . $propName,
327
                'feed' === $propTail
328
            );
329
330
331
            $shouldExpand = $this->shouldExpandSegment($propName);
332
333
            if ($shouldExpand) {
334
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
335
            }
336
            $nuLink->setIsExpanded(null !== $nuLink->getExpandedResult() && null !== $nuLink->getExpandedResult()->getData());
337
            assert(null !== $nuLink->isCollection());
338
339
            $links[] = $nuLink;
340
        }
341
342
        $odata                   = new ODataEntry();
343
        $odata->resourceSetName  = $resourceSet->getName();
344
        $odata->id               = $absoluteUri;
345
        $odata->title            = new ODataTitle($resourceType->getName());
346
        $odata->type             = new ODataCategory($type);
347
        $odata->propertyContent  = $propertyContent;
348
        $odata->isMediaLinkEntry = true === $resourceType->isMediaLinkEntry() ? true : null;
349
        $odata->editLink         = new ODataLink('edit', $resourceType->getName(), null, $relativeUri);
350
        assert(!is_array($mediaLink));
351
        $odata->mediaLink        = $mediaLink;
352
        $odata->mediaLinks       = $mediaLinks;
353
        $odata->links            = $links;
354
        $odata->updated          = $this->getUpdated()->format(DATE_ATOM);
355
        $odata->baseURI          = $baseURI;
356
357
        $newCount = count($this->lightStack);
358
        assert(
359
            $newCount == $stackCount,
360
            'Should have ' . $stackCount . 'elements in stack, have ' . $newCount . 'elements'
361
        );
362
        --$this->lightStack[$newCount - 1]['count'];
363
        if (0 == $this->lightStack[$newCount - 1]['count']) {
364
            array_pop($this->lightStack);
365
        }
366
367
        return $odata;
368
    }
369
370
    /**
371
     * Gets the data service instance.
372
     *
373
     * @return IService
374
     */
375
    public function getService()
376
    {
377
        return $this->service;
378
    }
379
380
    /**
381
     * Sets the data service instance.
382
     *
383
     * @param IService $service
384
     */
385
    public function setService(IService $service)
386
    {
387
        $this->service                     = $service;
388
        $this->absoluteServiceUri          = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
389
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
390
    }
391
392
    /**
393
     * Gets collection of projection nodes under the current node.
394
     *
395
     * @throws InvalidOperationException
396
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes describing projections for the current
397
     *                                                        segment, If this method returns null it means no
398
     *                                                        projections are to be applied and the entire resource for
399
     *                                                        the current segment should be serialized, If it returns
400
     *                                                        non-null only the properties described by the returned
401
     *                                                        projection segments should be serialized
402
     */
403
    protected function getProjectionNodes()
404
    {
405
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
406
        if (null === $expandedProjectionNode || $expandedProjectionNode->canSelectAllProperties()) {
407
            return null;
408
        }
409
410
        return $expandedProjectionNode->getChildNodes();
411
    }
412
413
    /**
414
     * Find a 'ExpandedProjectionNode' instance in the projection tree
415
     * which describes the current segment.
416
     *
417
     * @throws InvalidOperationException
418
     * @return RootProjectionNode|ExpandedProjectionNode|null
419
     */
420
    protected function getCurrentExpandedProjectionNode()
421
    {
422
        /** @var RootProjectionNode|null $expandedProjectionNode */
423
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
424
        if (null === $expandedProjectionNode) {
425
            return null;
426
        } else {
427
            $segmentNames = $this->getStack()->getSegmentNames();
428
            $depth        = count($segmentNames);
429
            // $depth == 1 means serialization of root entry
430
            //(the resource identified by resource path) is going on,
431
            //so control won't get into the below for loop.
432
            //we will directly return the root node,
433
            //which is 'ExpandedProjectionNode'
434
            // for resource identified by resource path.
435
            if (0 != $depth) {
436
                for ($i = 1; $i < $depth; ++$i) {
437
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segmentNames[$i]);
438
                    if (null === $expandedProjectionNode) {
439
                        throw new InvalidOperationException('is_null($expandedProjectionNode)');
440
                    }
441
                    if (!$expandedProjectionNode instanceof ExpandedProjectionNode) {
442
                        $msg = '$expandedProjectionNode not instanceof ExpandedProjectionNode';
443
                        throw new InvalidOperationException($msg);
444
                    }
445
                }
446
            }
447
        }
448
449
        return $expandedProjectionNode;
450
    }
451
452
    /**
453
     * Gets the segment stack instance.
454
     *
455
     * @return SegmentStack
456
     */
457
    public function getStack()
458
    {
459
        return $this->stack;
460
    }
461
462
    /**
463
     * @param  object              $entityInstance
464
     * @param  ResourceType        $resourceType
465
     * @param  string              $containerName
466
     * @throws ReflectionException
467
     * @throws ODataException
468
     * @return string
469
     */
470
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
471
    {
472
        assert(is_object($entityInstance));
473
        $typeName      = $resourceType->getName();
474
        $keyProperties = $resourceType->getKeyProperties();
475
        assert(0 != count($keyProperties), 'count($keyProperties) == 0');
476
        $keyString = $containerName . '(';
477
        $comma     = null;
478
        foreach ($keyProperties as $keyName => $resourceProperty) {
479
            $keyType = $resourceProperty->getInstanceType();
480
            assert($keyType instanceof IType, '$keyType not instanceof IType');
481
            $keyName  = $resourceProperty->getName();
482
            $keyValue = $entityInstance->{$keyName};
483
            if (!isset($keyValue)) {
484
                $msg = Messages::badQueryNullKeysAreNotSupported($typeName, $keyName);
485
                throw ODataException::createInternalServerError($msg);
486
            }
487
488
            $keyValue = $keyType->convertToOData(strval($keyValue));
489
            $keyString .= $comma . $keyName . '=' . $keyValue;
490
            $comma = ',';
491
        }
492
493
        $keyString .= ')';
494
495
        return $keyString;
496
    }
497
498
    /**
499
     * @param $entryObject
500
     * @param $type
501
     * @param $relativeUri
502
     * @param $resourceType
503
     *
504
     * @return ODataMediaLink[]|null[]
505
     */
506
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
507
    {
508
        $context               = $this->getService()->getOperationContext();
509
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
510
        assert(null != $streamProviderWrapper, 'Retrieved stream provider must not be null');
511
512
        $mediaLink = null;
513
        if ($resourceType->isMediaLinkEntry()) {
514
            $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

514
            /** @scrutinizer ignore-call */ 
515
            $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...
515
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
516
        }
517
        $mediaLinks = [];
518
        if ($resourceType->hasNamedStream()) {
519
            $namedStreams = $resourceType->getAllNamedStreams();
520
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
521
                $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

521
                /** @scrutinizer ignore-call */ 
522
                $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...
522
                    $entryObject,
523
                    $context,
524
                    $resourceStreamInfo,
525
                    $relativeUri
526
                );
527
                $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

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

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

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