Passed
Push — master ( 6b2979...464731 )
by Alex
04:00 queued 11s
created

CynicSerialiser::writeTopLevelElement()   D

Complexity

Conditions 14
Paths 241

Size

Total Lines 134
Code Lines 96

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 14
eloc 96
c 2
b 0
f 0
nc 241
nop 1
dl 0
loc 134
rs 4.0239

How to fix   Long Method    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
            $absoluteUri,
344
            null,
345
            new ODataTitle($resourceType->getName()),
346
            new ODataLink('edit', $resourceType->getName(), null, $relativeUri),
347
            new ODataCategory($type),
348
            $propertyContent,
349
            $mediaLinks,
350
            $mediaLink,
351
            $links,
352
            null,
353
            true === $resourceType->isMediaLinkEntry(),
354
            $resourceSet->getName(),
355
            $this->getUpdated()->format(DATE_ATOM),
356
            $baseURI
357
        );
358
359
        $newCount = count($this->lightStack);
360
        assert(
361
            $newCount == $stackCount,
362
            'Should have ' . $stackCount . 'elements in stack, have ' . $newCount . 'elements'
363
        );
364
        --$this->lightStack[$newCount - 1]['count'];
365
        if (0 == $this->lightStack[$newCount - 1]['count']) {
366
            array_pop($this->lightStack);
367
        }
368
369
        return $odata;
370
    }
371
372
    /**
373
     * Gets the data service instance.
374
     *
375
     * @return IService
376
     */
377
    public function getService()
378
    {
379
        return $this->service;
380
    }
381
382
    /**
383
     * Sets the data service instance.
384
     *
385
     * @param IService $service
386
     */
387
    public function setService(IService $service)
388
    {
389
        $this->service                     = $service;
390
        $this->absoluteServiceUri          = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
391
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
392
    }
393
394
    /**
395
     * Gets collection of projection nodes under the current node.
396
     *
397
     * @throws InvalidOperationException
398
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes describing projections for the current
399
     *                                                        segment, If this method returns null it means no
400
     *                                                        projections are to be applied and the entire resource for
401
     *                                                        the current segment should be serialized, If it returns
402
     *                                                        non-null only the properties described by the returned
403
     *                                                        projection segments should be serialized
404
     */
405
    protected function getProjectionNodes()
406
    {
407
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
408
        if (null === $expandedProjectionNode || $expandedProjectionNode->canSelectAllProperties()) {
409
            return null;
410
        }
411
412
        return $expandedProjectionNode->getChildNodes();
413
    }
414
415
    /**
416
     * Find a 'ExpandedProjectionNode' instance in the projection tree
417
     * which describes the current segment.
418
     *
419
     * @throws InvalidOperationException
420
     * @return RootProjectionNode|ExpandedProjectionNode|null
421
     */
422
    protected function getCurrentExpandedProjectionNode()
423
    {
424
        /** @var RootProjectionNode|null $expandedProjectionNode */
425
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
426
        if (null === $expandedProjectionNode) {
427
            return null;
428
        } else {
429
            $segmentNames = $this->getStack()->getSegmentNames();
430
            $depth        = count($segmentNames);
431
            // $depth == 1 means serialization of root entry
432
            //(the resource identified by resource path) is going on,
433
            //so control won't get into the below for loop.
434
            //we will directly return the root node,
435
            //which is 'ExpandedProjectionNode'
436
            // for resource identified by resource path.
437
            if (0 != $depth) {
438
                for ($i = 1; $i < $depth; ++$i) {
439
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segmentNames[$i]);
440
                    if (null === $expandedProjectionNode) {
441
                        throw new InvalidOperationException('is_null($expandedProjectionNode)');
442
                    }
443
                    if (!$expandedProjectionNode instanceof ExpandedProjectionNode) {
444
                        $msg = '$expandedProjectionNode not instanceof ExpandedProjectionNode';
445
                        throw new InvalidOperationException($msg);
446
                    }
447
                }
448
            }
449
        }
450
451
        return $expandedProjectionNode;
452
    }
453
454
    /**
455
     * Gets the segment stack instance.
456
     *
457
     * @return SegmentStack
458
     */
459
    public function getStack()
460
    {
461
        return $this->stack;
462
    }
463
464
    /**
465
     * @param  object              $entityInstance
466
     * @param  ResourceType        $resourceType
467
     * @param  string              $containerName
468
     * @throws ReflectionException
469
     * @throws ODataException
470
     * @return string
471
     */
472
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
473
    {
474
        assert(is_object($entityInstance));
475
        $typeName      = $resourceType->getName();
476
        $keyProperties = $resourceType->getKeyProperties();
477
        assert(0 != count($keyProperties), 'count($keyProperties) == 0');
478
        $keyString = $containerName . '(';
479
        $comma     = null;
480
        foreach ($keyProperties as $keyName => $resourceProperty) {
481
            $keyType = $resourceProperty->getInstanceType();
482
            assert($keyType instanceof IType, '$keyType not instanceof IType');
483
            $keyName  = $resourceProperty->getName();
484
            $keyValue = $entityInstance->{$keyName};
485
            if (!isset($keyValue)) {
486
                $msg = Messages::badQueryNullKeysAreNotSupported($typeName, $keyName);
487
                throw ODataException::createInternalServerError($msg);
488
            }
489
490
            $keyValue = $keyType->convertToOData(strval($keyValue));
491
            $keyString .= $comma . $keyName . '=' . $keyValue;
492
            $comma = ',';
493
        }
494
495
        $keyString .= ')';
496
497
        return $keyString;
498
    }
499
500
    /**
501
     * @param $entryObject
502
     * @param $type
503
     * @param $relativeUri
504
     * @param $resourceType
505
     *
506
     * @return ODataMediaLink[]|null[]
507
     */
508
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
509
    {
510
        $context               = $this->getService()->getOperationContext();
511
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
512
        assert(null != $streamProviderWrapper, 'Retrieved stream provider must not be null');
513
514
        $mediaLink = null;
515
        if ($resourceType->isMediaLinkEntry()) {
516
            $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

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

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

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

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

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