Completed
Push — master ( cb02b0...de8dbf )
by Alex
15s queued 12s
created

CynicSerialiser::writeTopLevelElements()   B

Complexity

Conditions 11
Paths 97

Size

Total Lines 60
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 43
c 1
b 0
f 0
nc 97
nop 1
dl 0
loc 60
rs 7.3166

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
     */
106
    public function __construct(IService $service, RequestDescription $request = null)
107
    {
108
        $this->service                       = $service;
109
        $this->request                       = $request;
110
        $this->absoluteServiceUri            = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
111
        $this->absoluteServiceUriWithSlash   = rtrim($this->absoluteServiceUri, '/') . '/';
112
        $this->stack                         = new SegmentStack($request);
113
        $this->complexTypeInstanceCollection = [];
114
        $this->updated                       = DateTime::now();
115
    }
116
117
    /**
118
     * Write top level feed element.
119
     *
120
     * @param QueryResult &$entryObjects Results property contains array of entry resources to be written
121
     *
122
     * @throws ODataException
123
     * @throws ReflectionException
124
     * @throws InvalidOperationException
125
     * @return ODataFeed
126
     */
127
    public function writeTopLevelElements(QueryResult &$entryObjects)
128
    {
129
        $res = $entryObjects->results;
130
        if (!(is_array($res))) {
131
            throw new InvalidOperationException('!is_array($entryObjects->results)');
132
        }
133
134
        if (is_array($res) && 0 == count($entryObjects->results)) {
135
            $entryObjects->hasMore = false;
136
        }
137
138
        $this->loadStackIfEmpty();
139
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
140
141
        $title       = $this->getRequest()->getContainerName();
142
        $relativeUri = $this->getRequest()->getIdentifier();
143
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
144
145
        $selfLink        = new ODataLink();
146
        $selfLink->name  = 'self';
147
        $selfLink->title = $title;
148
        $selfLink->url   = $relativeUri;
149
150
        $odata               = new ODataFeed();
151
        $odata->title        = new ODataTitle($title);
152
        $odata->id           = $absoluteUri;
153
        $odata->selfLink     = $selfLink;
154
        $odata->updated      = $this->getUpdated()->format(DATE_ATOM);
155
        $odata->baseURI      = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
156
        $this->isBaseWritten = true;
157
158
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
159
            $odata->rowCount = $this->getRequest()->getCountValue();
160
        }
161
        foreach ($res as $entry) {
162
            if (!$entry instanceof QueryResult) {
163
                $query          = new QueryResult();
164
                $query->results = $entry;
165
            } else {
166
                $query = $entry;
167
            }
168
            $odata->entries[] = $this->writeTopLevelElement($query);
169
        }
170
171
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
172
        $requestTop  = $this->getRequest()->getTopOptionCount();
173
        $pageSize    = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
174
        $requestTop  = (null === $requestTop) ? $pageSize + 1 : $requestTop;
175
176
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
177
            $stackSegment        = $setName;
178
            $lastObject          = end($entryObjects->results);
179
            $segment             = $this->getNextLinkUri($lastObject);
180
            $nextLink            = new ODataLink();
181
            $nextLink->name      = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
182
            $nextLink->url       = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
183
            $odata->nextPageLink = $nextLink;
184
        }
185
186
        return $odata;
187
    }
188
189
    /**
190
     * Load processing stack if it's currently empty.
191
     *
192
     * @return void
193
     */
194
    private function loadStackIfEmpty()
195
    {
196
        if (0 == count($this->lightStack)) {
197
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
198
            array_push($this->lightStack, ['type' => $typeName, 'property' => $typeName, 'count' => 1]);
199
        }
200
    }
201
202
    /**
203
     * Gets reference to the request submitted by client.
204
     *
205
     * @return RequestDescription
206
     */
207
    public function getRequest()
208
    {
209
        assert(null !== $this->request, 'Request not yet set');
210
211
        return $this->request;
212
    }
213
214
    /**
215
     * Sets reference to the request submitted by client.
216
     *
217
     * @param RequestDescription $request
218
     */
219
    public function setRequest(RequestDescription $request)
220
    {
221
        $this->request = $request;
222
        $this->stack->setRequest($request);
223
    }
224
225
    /**
226
     * Get update timestamp.
227
     *
228
     * @return \DateTime
229
     */
230
    public function getUpdated()
231
    {
232
        return $this->updated;
233
    }
234
235
    /**
236
     * Write a top level entry resource.
237
     *
238
     * @param QueryResult $entryObject Results property contains reference to the entry object to be written
239
     *
240
     * @throws ODataException
241
     * @throws ReflectionException
242
     * @throws InvalidOperationException
243
     * @return ODataEntry|null
244
     */
245
    public function writeTopLevelElement(QueryResult $entryObject)
246
    {
247
        if (!isset($entryObject->results)) {
248
            array_pop($this->lightStack);
249
250
            return null;
251
        }
252
253
        assert(is_object($entryObject->results));
254
        $this->loadStackIfEmpty();
255
256
        $baseURI             = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
257
        $this->isBaseWritten = true;
258
259
        $stackCount   = count($this->lightStack);
260
        $topOfStack   = $this->lightStack[$stackCount - 1];
261
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
262
        assert($resourceType instanceof ResourceType, get_class($resourceType));
263
        $rawProp    = $resourceType->getAllProperties();
264
        $relProp    = [];
265
        $nonRelProp = [];
266
        $last       = end($this->lightStack);
267
        $projNodes  = ($last['type'] == $last['property']) ? $this->getProjectionNodes() : null;
268
269
        foreach ($rawProp as $prop) {
270
            $propName = $prop->getName();
271
            if ($prop->getResourceType() instanceof ResourceEntityType) {
272
                $relProp[$propName] = $prop;
273
            } else {
274
                $nonRelProp[$propName] = $prop;
275
            }
276
        }
277
        $rawCount    = count($rawProp);
278
        $relCount    = count($relProp);
279
        $nonRelCount = count($nonRelProp);
280
        assert(
281
            $rawCount == $relCount + $nonRelCount,
282
            'Raw property count ' . $rawCount . ', does not equal sum of relProp count, ' . $relCount
283
            . ', and nonRelPropCount,' . $nonRelCount
284
        );
285
286
        // now mask off against projNodes
287
        if (null !== $projNodes) {
288
            $keys = [];
289
            foreach ($projNodes as $node) {
290
                $keys[$node->getPropertyName()] = '';
291
            }
292
293
            $relProp    = array_intersect_key($relProp, $keys);
294
            $nonRelProp = array_intersect_key($nonRelProp, $keys);
295
        }
296
297
        $resourceSet = $resourceType->getCustomState();
298
        assert($resourceSet instanceof ResourceSet);
299
        $title = $resourceType->getName();
300
        $type  = $resourceType->getFullName();
301
302
        $relativeUri = $this->getEntryInstanceKey(
303
            $entryObject->results,
304
            $resourceType,
305
            $resourceSet->getName()
306
        );
307
        $absoluteUri = rtrim(strval($this->absoluteServiceUri), '/') . '/' . $relativeUri;
308
309
        list($mediaLink, $mediaLinks) = $this->writeMediaData(
310
            $entryObject->results,
311
            $type,
312
            $relativeUri,
313
            $resourceType
314
        );
315
316
        $propertyContent = $this->writeProperties($entryObject->results, $nonRelProp);
317
318
        $links = [];
319
        foreach ($relProp as $prop) {
320
            $nuLink   = new ODataLink();
321
            $propKind = $prop->getKind();
322
323
            assert(
324
                ResourcePropertyKind::RESOURCESET_REFERENCE() == $propKind
325
                || ResourcePropertyKind::RESOURCE_REFERENCE() == $propKind,
326
                '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
327
                . ' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE'
328
            );
329
            $propTail             = ResourcePropertyKind::RESOURCE_REFERENCE() == $propKind ? 'entry' : 'feed';
330
            $nuLink->isCollection = 'feed' === $propTail;
331
            $propType             = 'application/atom+xml;type=' . $propTail;
332
            $propName             = $prop->getName();
333
            $nuLink->title        = $propName;
334
            $nuLink->name         = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
335
            $nuLink->url          = $relativeUri . '/' . $propName;
336
            $nuLink->type         = $propType;
337
338
            $shouldExpand = $this->shouldExpandSegment($propName);
339
340
            $navProp = new ODataNavigationPropertyInfo($prop, $shouldExpand);
341
            if ($navProp->expanded) {
342
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
343
            }
344
            $nuLink->isExpanded = isset($nuLink->expandedResult);
345
            assert(null !== $nuLink->isCollection);
346
347
            $links[] = $nuLink;
348
        }
349
350
        $odata                   = new ODataEntry();
351
        $odata->resourceSetName  = $resourceSet->getName();
352
        $odata->id               = $absoluteUri;
353
        $odata->title            = new ODataTitle($title);
354
        $odata->type             = new ODataCategory($type);
355
        $odata->propertyContent  = $propertyContent;
356
        $odata->isMediaLinkEntry = true === $resourceType->isMediaLinkEntry() ? true : null;
357
        $odata->editLink         = new ODataLink();
358
        $odata->editLink->url    = $relativeUri;
359
        $odata->editLink->name   = 'edit';
360
        $odata->editLink->title  = $title;
361
        $odata->mediaLink        = $mediaLink;
0 ignored issues
show
Documentation Bug introduced by
It seems like $mediaLink can also be of type array. However, the property $mediaLink is declared as type POData\ObjectModel\ODataMediaLink. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
362
        $odata->mediaLinks       = $mediaLinks;
0 ignored issues
show
Documentation Bug introduced by
It seems like $mediaLinks can also be of type POData\ObjectModel\ODataMediaLink. However, the property $mediaLinks is declared as type POData\ObjectModel\ODataMediaLink[]. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

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

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

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

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

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

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