Test Failed
Pull Request — master (#112)
by Alex
03:22
created

IronicSerialiser   F

Complexity

Total Complexity 119

Size/Duplication

Total Lines 994
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 35

Importance

Changes 10
Bugs 0 Features 0
Metric Value
wmc 119
lcom 1
cbo 35
dl 0
loc 994
rs 1.0434
c 10
b 0
f 0

33 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
F writeTopLevelElement() 0 130 14
B writeTopLevelElements() 0 57 9
A writeUrlElement() 0 16 2
C writeUrlElements() 0 33 7
A writeTopLevelComplexObject() 0 17 2
A writeTopLevelBagObject() 0 13 1
A writeTopLevelPrimitive() 0 22 2
A getRequest() 0 6 1
A setRequest() 0 5 1
A getService() 0 4 1
A getStack() 0 4 1
A getUpdated() 0 4 1
B getEntryInstanceKey() 0 27 4
B writeMediaData() 0 38 4
A getProjectionNodes() 0 9 3
B getCurrentExpandedProjectionNode() 0 28 4
A shouldExpandSegment() 0 11 2
A needNextPageLink() 0 15 4
A getCurrentResourceSetWrapper() 0 7 2
A getNextLinkUri() 0 17 2
C getNextPageLinkQueryParametersForRootResourceSet() 0 36 8
A loadStackIfEmpty() 0 7 2
B primitiveToString() 0 16 7
A writePrimitiveProperties() 0 18 4
C expandNavigationProperty() 0 37 8
A setService() 0 6 1
C writeBagValue() 0 26 8
B writeComplexValue() 0 45 6
A isMatchPrimitive() 0 10 3
A getMetadata() 0 7 2
A getLightStack() 0 4 1
A getModelSerialiser() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like IronicSerialiser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use IronicSerialiser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Serialisers;
4
5
use Carbon\Carbon;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\Facades\App;
9
use POData\Common\InvalidOperationException;
10
use POData\Common\Messages;
11
use POData\Common\ODataConstants;
12
use POData\Common\ODataException;
13
use POData\IService;
14
use POData\ObjectModel\IObjectSerialiser;
15
use POData\ObjectModel\ODataBagContent;
16
use POData\ObjectModel\ODataCategory;
17
use POData\ObjectModel\ODataEntry;
18
use POData\ObjectModel\ODataFeed;
19
use POData\ObjectModel\ODataLink;
20
use POData\ObjectModel\ODataMediaLink;
21
use POData\ObjectModel\ODataNavigationPropertyInfo;
22
use POData\ObjectModel\ODataProperty;
23
use POData\ObjectModel\ODataPropertyContent;
24
use POData\ObjectModel\ODataTitle;
25
use POData\ObjectModel\ODataURL;
26
use POData\ObjectModel\ODataURLCollection;
27
use POData\Providers\Metadata\IMetadataProvider;
28
use POData\Providers\Metadata\ResourceEntityType;
29
use POData\Providers\Metadata\ResourceProperty;
30
use POData\Providers\Metadata\ResourcePropertyKind;
31
use POData\Providers\Metadata\ResourceSet;
32
use POData\Providers\Metadata\ResourceSetWrapper;
33
use POData\Providers\Metadata\ResourceType;
34
use POData\Providers\Metadata\ResourceTypeKind;
35
use POData\Providers\Metadata\Type\Binary;
36
use POData\Providers\Metadata\Type\Boolean;
37
use POData\Providers\Metadata\Type\DateTime;
38
use POData\Providers\Metadata\Type\IType;
39
use POData\Providers\Metadata\Type\StringType;
40
use POData\Providers\Query\QueryResult;
41
use POData\Providers\Query\QueryType;
42
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
43
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ProjectionNode;
44
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\RootProjectionNode;
45
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
46
use POData\UriProcessor\RequestDescription;
47
use POData\UriProcessor\SegmentStack;
48
49
class IronicSerialiser implements IObjectSerialiser
50
{
51
    const PK = 'PrimaryKey';
52
53
    /**
54
     * The service implementation.
55
     *
56
     * @var IService
57
     */
58
    protected $service;
59
60
    /**
61
     * Request description instance describes OData request the
62
     * the client has submitted and result of the request.
63
     *
64
     * @var RequestDescription
65
     */
66
    protected $request;
67
68
    /**
69
     * Collection of complex type instances used for cycle detection.
70
     *
71
     * @var array
72
     */
73
    protected $complexTypeInstanceCollection;
74
75
    /**
76
     * Absolute service Uri.
77
     *
78
     * @var string
79
     */
80
    protected $absoluteServiceUri;
81
82
    /**
83
     * Absolute service Uri with slash.
84
     *
85
     * @var string
86
     */
87
    protected $absoluteServiceUriWithSlash;
88
89
    /**
90
     * Holds reference to segment stack being processed.
91
     *
92
     * @var SegmentStack
93
     */
94
    protected $stack;
95
96
    /**
97
     * Lightweight stack tracking for recursive descent fill
98
     */
99
    protected $lightStack = [];
100
101
    /**
102
     * @var ModelSerialiser
103
     */
104
    private $modelSerialiser;
105
106
107
108
    /**
109
     * @var IMetadataProvider
110
     */
111
    private $metaProvider;
112
113
    /*
114
     * Update time to insert into ODataEntry/ODataFeed fields
115
     * @var Carbon;
116
     */
117
    private $updated;
118
119
    /*
120
     * Has base URI already been written out during serialisation?
121
     * @var bool;
122
     */
123
    private $isBaseWritten = false;
124
125
    /**
126
     * @param IService                  $service    Reference to the data service instance
127
     * @param RequestDescription|null   $request    Type instance describing the client submitted request
128
     */
129
    public function __construct(IService $service, RequestDescription $request = null)
130
    {
131
        $this->service = $service;
132
        $this->request = $request;
133
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
134
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
135
        $this->stack = new SegmentStack($request);
136
        $this->complexTypeInstanceCollection = [];
137
        $this->modelSerialiser = new ModelSerialiser();
138
        $this->updated = Carbon::now();
139
    }
140
141
    /**
142
     * Write a top level entry resource.
143
     *
144
     * @param QueryResult $entryObject Reference to the entry object to be written
145
     *
146
     * @return ODataEntry|null
147
     */
148
    public function writeTopLevelElement(QueryResult $entryObject)
149
    {
150
        if (!isset($entryObject->results)) {
151
            array_pop($this->lightStack);
152
            return null;
153
        }
154
        assert($entryObject instanceof QueryResult, get_class($entryObject));
155
        assert($entryObject->results instanceof Model, get_class($entryObject->results));
156
        $this->loadStackIfEmpty();
157
158
        $baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
159
        $this->isBaseWritten = true;
160
161
        $stackCount = count($this->lightStack);
162
        $topOfStack = $this->lightStack[$stackCount-1];
163
        $payloadClass = get_class($entryObject->results);
164
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
165
        // need gubbinz to unpack an abstract resource type
166
        if ($resourceType->isAbstract()) {
167
            $derived = $this->getMetadata()->getDerivedTypes($resourceType);
168
            assert(0 < count($derived));
169
            foreach ($derived as $rawType) {
170
                if (!$rawType->isAbstract()) {
171
                    $name = $rawType->getInstanceType()->getName();
172
                    if ($payloadClass == $name) {
173
                        $resourceType = $rawType;
174
                        break;
175
                    }
176
                }
177
            }
178
            // despite all set up, checking, etc, if we haven't picked a concrete resource type,
179
            // wheels have fallen off, so blow up
180
            assert(!$resourceType->isAbstract(), 'Concrete resource type not selected for payload '.$payloadClass);
181
        }
182
183
        // make sure we're barking up right tree
184
        assert($resourceType instanceof ResourceEntityType, get_class($resourceType));
185
        $targClass = $resourceType->getInstanceType()->getName();
186
        assert(
187
            $entryObject->results instanceof $targClass,
188
            'Object being serialised not instance of expected class, '.$targClass. ', is actually '.$payloadClass
189
        );
190
191
        $rawProp = $resourceType->getAllProperties();
192
        $relProp = [];
193
        $nonRelProp = [];
194
        foreach ($rawProp as $prop) {
195
            if ($prop->getResourceType() instanceof ResourceEntityType) {
196
                $relProp[] = $prop;
197
            } else {
198
                $nonRelProp[$prop->getName()] = $prop;
199
            }
200
        }
201
202
        $resourceSet = $resourceType->getCustomState();
203
        assert($resourceSet instanceof ResourceSet);
204
        $title = $resourceType->getName();
205
        $type = $resourceType->getFullName();
206
207
        $relativeUri = $this->getEntryInstanceKey(
208
            $entryObject->results,
209
            $resourceType,
210
            $resourceSet->getName()
211
        );
212
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
213
214
        list($mediaLink, $mediaLinks) = $this->writeMediaData(
215
            $entryObject->results,
216
            $type,
217
            $relativeUri,
218
            $resourceType
219
        );
220
221
        $propertyContent = $this->writePrimitiveProperties($entryObject->results, $nonRelProp);
222
223
        $links = [];
224
        foreach ($relProp as $prop) {
225
            $nuLink = new ODataLink();
226
            $propKind = $prop->getKind();
227
228
            assert(
229
                ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind
230
                || ResourcePropertyKind::RESOURCE_REFERENCE == $propKind,
231
                '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
232
                .' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE'
233
            );
234
            $propTail = ResourcePropertyKind::RESOURCE_REFERENCE == $propKind ? 'entry' : 'feed';
235
            $propType = 'application/atom+xml;type='.$propTail;
236
            $propName = $prop->getName();
237
            $nuLink->title = $propName;
238
            $nuLink->name = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
239
            $nuLink->url = $relativeUri . '/' . $propName;
240
            $nuLink->type = $propType;
241
242
            $navProp = new ODataNavigationPropertyInfo($prop, $this->shouldExpandSegment($propName));
243
            if ($navProp->expanded) {
244
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
245
            }
246
247
            $links[] = $nuLink;
248
        }
249
250
        $odata = new ODataEntry();
251
        $odata->resourceSetName = $resourceSet->getName();
252
        $odata->id = $absoluteUri;
253
        $odata->title = new ODataTitle($title);
254
        $odata->type = new ODataCategory($type);
255
        $odata->propertyContent = $propertyContent;
256
        $odata->isMediaLinkEntry = $resourceType->isMediaLinkEntry();
257
        $odata->editLink = new ODataLink();
258
        $odata->editLink->url = $relativeUri;
259
        $odata->editLink->name = 'edit';
260
        $odata->editLink->title = $title;
261
        $odata->mediaLink = $mediaLink;
262
        $odata->mediaLinks = $mediaLinks;
263
        $odata->links = $links;
264
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
265
        $odata->baseURI = $baseURI;
266
267
        $newCount = count($this->lightStack);
268
        assert(
269
            $newCount == $stackCount,
270
            'Should have ' . $stackCount . ' elements in stack, have ' . $newCount . ' elements'
271
        );
272
        $this->lightStack[$newCount-1]['count']--;
273
        if (0 == $this->lightStack[$newCount-1]['count']) {
274
            array_pop($this->lightStack);
275
        }
276
        return $odata;
277
    }
278
279
    /**
280
     * Write top level feed element.
281
     *
282
     * @param QueryResult &$entryObjects Array of entry resources to be written
283
     *
284
     * @return ODataFeed
285
     */
286
    public function writeTopLevelElements(QueryResult &$entryObjects)
287
    {
288
        $res = $entryObjects->results;
289
        assert(is_array($res) || $res instanceof Collection, '!is_array($entryObjects->results)');
290
291
        $this->loadStackIfEmpty();
292
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
293
294
        $title = $this->getRequest()->getContainerName();
295
        $relativeUri = $this->getRequest()->getIdentifier();
296
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
297
298
        $selfLink = new ODataLink();
299
        $selfLink->name = 'self';
300
        $selfLink->title = $relativeUri;
301
        $selfLink->url = $relativeUri;
302
303
        $odata = new ODataFeed();
304
        $odata->title = new ODataTitle($title);
305
        $odata->id = $absoluteUri;
306
        $odata->selfLink = $selfLink;
307
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
308
        $odata->baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
309
        $this->isBaseWritten = true;
310
311
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
312
            $odata->rowCount = $this->getRequest()->getCountValue();
313
        }
314
        foreach ($res as $entry) {
315
            if (!$entry instanceof QueryResult) {
316
                $query = new QueryResult();
317
                $query->results = $entry;
318
            } else {
319
                $query = $entry;
320
            }
321
            assert($query instanceof QueryResult, get_class($query));
322
            assert($query->results instanceof Model, get_class($query->results));
323
            $odata->entries[] = $this->writeTopLevelElement($query);
324
        }
325
326
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
327
        $requestTop = $this->getRequest()->getTopOptionCount();
328
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
329
        $requestTop = (null === $requestTop) ? $pageSize + 1 : $requestTop;
330
331
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
332
            $stackSegment = $setName;
333
            $lastObject = end($entryObjects->results);
334
            $segment = $this->getNextLinkUri($lastObject);
335
            $nextLink = new ODataLink();
336
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
337
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
338
            $odata->nextPageLink = $nextLink;
339
        }
340
341
        return $odata;
342
    }
343
344
    /**
345
     * Write top level url element.
346
     *
347
     * @param QueryResult $entryObject The entry resource whose url to be written
348
     *
349
     * @return ODataURL
350
     */
351
    public function writeUrlElement(QueryResult $entryObject)
352
    {
353
        $url = new ODataURL();
354
        if (!is_null($entryObject->results)) {
355
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
356
            $relativeUri = $this->getEntryInstanceKey(
357
                $entryObject->results,
358
                $currentResourceType,
359
                $this->getCurrentResourceSetWrapper()->getName()
360
            );
361
362
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
363
        }
364
365
        return $url;
366
    }
367
368
    /**
369
     * Write top level url collection.
370
     *
371
     * @param QueryResult $entryObjects Array of entry resources whose url to be written
372
     *
373
     * @return ODataURLCollection
374
     */
375
    public function writeUrlElements(QueryResult $entryObjects)
376
    {
377
        $urls = new ODataURLCollection();
378
        if (!empty($entryObjects->results)) {
379
            $i = 0;
380
            foreach ($entryObjects->results as $entryObject) {
381
                if (!$entryObject instanceof QueryResult) {
382
                    $query = new QueryResult();
383
                    $query->results = $entryObject;
384
                } else {
385
                    $query = $entryObject;
386
                }
387
                $urls->urls[$i] = $this->writeUrlElement($query);
388
                ++$i;
389
            }
390
391
            if ($i > 0 && true === $entryObjects->hasMore) {
392
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
393
                $lastObject = end($entryObjects->results);
394
                $segment = $this->getNextLinkUri($lastObject);
395
                $nextLink = new ODataLink();
396
                $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
397
                $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
398
                $urls->nextPageLink = $nextLink;
399
            }
400
        }
401
402
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
403
            $urls->count = $this->getRequest()->getCountValue();
404
        }
405
406
        return $urls;
407
    }
408
409
    /**
410
     * Write top level complex resource.
411
     *
412
     * @param QueryResult &$complexValue The complex object to be written
413
     * @param string $propertyName The name of the complex property
414
     * @param ResourceType &$resourceType Describes the type of complex object
415
     *
416
     * @return ODataPropertyContent
417
     */
418
    public function writeTopLevelComplexObject(QueryResult &$complexValue, $propertyName, ResourceType &$resourceType)
419
    {
420
        $result = $complexValue->results;
421
422
        $propertyContent = new ODataPropertyContent();
423
        $odataProperty = new ODataProperty();
424
        $odataProperty->name = $propertyName;
425
        $odataProperty->typeName = $resourceType->getFullName();
426
        if (null != $result) {
427
            $internalContent = $this->writeComplexValue($resourceType, $result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $complexValue->results on line 420 can also be of type array<integer,object>; however, AlgoWeb\PODataLaravel\Se...er::writeComplexValue() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
428
            $odataProperty->value = $internalContent;
429
        }
430
431
        $propertyContent->properties[$propertyName] = $odataProperty;
432
433
        return $propertyContent;
434
    }
435
436
    /**
437
     * Write top level bag resource.
438
     *
439
     * @param QueryResult &$BagValue The bag object to be
440
     *                                    written
441
     * @param string $propertyName The name of the
442
     *                                    bag property
443
     * @param ResourceType &$resourceType Describes the type of
444
     *                                    bag object
445
     * @return ODataPropertyContent
446
     */
447
    public function writeTopLevelBagObject(QueryResult &$BagValue, $propertyName, ResourceType &$resourceType)
0 ignored issues
show
Coding Style Naming introduced by
The parameter $BagValue is not named in camelCase.

This check marks parameter names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
448
    {
449
        $result = $BagValue->results;
0 ignored issues
show
Coding Style introduced by
$BagValue does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

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

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

Loading history...
450
451
        $propertyContent = new ODataPropertyContent();
452
        $odataProperty = new ODataProperty();
453
        $odataProperty->name = $propertyName;
454
        $odataProperty->typeName = 'Collection('.$resourceType->getFullName().')';
455
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
456
457
        $propertyContent->properties[$propertyName] = $odataProperty;
458
        return $propertyContent;
459
    }
460
461
    /**
462
     * Write top level primitive value.
463
     *
464
     * @param QueryResult &$primitiveValue              The primitive value to be
465
     *                                            written
466
     * @param ResourceProperty &$resourceProperty Resource property describing the
467
     *                                            primitive property to be written
468
     * @return ODataPropertyContent
469
     */
470
    public function writeTopLevelPrimitive(QueryResult &$primitiveValue, ResourceProperty &$resourceProperty = null)
471
    {
472
        assert(null != $resourceProperty, 'Resource property must not be null');
473
        $propertyContent = new ODataPropertyContent();
474
475
        $odataProperty = new ODataProperty();
476
        $odataProperty->name = $resourceProperty->getName();
477
        $iType = $resourceProperty->getInstanceType();
478
        assert($iType instanceof IType, get_class($iType));
479
        $odataProperty->typeName = $iType->getFullTypeName();
480
        if (null == $primitiveValue->results) {
481
            $odataProperty->value = null;
482
        } else {
483
            $rType = $resourceProperty->getResourceType()->getInstanceType();
484
            assert($rType instanceof IType, get_class($rType));
485
            $odataProperty->value = $this->primitiveToString($rType, $primitiveValue->results);
486
        }
487
488
        $propertyContent->properties[$odataProperty->name] = $odataProperty;
489
490
        return $propertyContent;
491
    }
492
493
    /**
494
     * Gets reference to the request submitted by client.
495
     *
496
     * @return RequestDescription
497
     */
498
    public function getRequest()
499
    {
500
        assert(null != $this->request, 'Request not yet set');
501
502
        return $this->request;
503
    }
504
505
    /**
506
     * Sets reference to the request submitted by client.
507
     *
508
     * @param RequestDescription $request
509
     */
510
    public function setRequest(RequestDescription $request)
511
    {
512
        $this->request = $request;
513
        $this->stack->setRequest($request);
514
    }
515
516
    /**
517
     * Gets the data service instance.
518
     *
519
     * @return IService
520
     */
521
    public function getService()
522
    {
523
        return $this->service;
524
    }
525
526
    /**
527
     * Gets the segment stack instance.
528
     *
529
     * @return SegmentStack
530
     */
531
    public function getStack()
532
    {
533
        return $this->stack;
534
    }
535
536
    /**
537
     * Get update timestamp
538
     *
539
     * @return Carbon
540
     */
541
    public function getUpdated()
542
    {
543
        return $this->updated;
544
    }
545
546
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
547
    {
548
        $typeName = $resourceType->getName();
549
        $keyProperties = $resourceType->getKeyProperties();
550
        assert(count($keyProperties) != 0, 'count($keyProperties) == 0');
551
        $keyString = $containerName . '(';
552
        $comma = null;
553
        foreach ($keyProperties as $keyName => $resourceProperty) {
554
            $keyType = $resourceProperty->getInstanceType();
555
            assert($keyType instanceof IType, '$keyType not instanceof IType');
556
            $keyName = $resourceProperty->getName();
557
            $keyValue = (self::PK == $keyName) ? $entityInstance->getKey() : $entityInstance->$keyName;
558
            if (!isset($keyValue)) {
559
                throw ODataException::createInternalServerError(
560
                    Messages::badQueryNullKeysAreNotSupported($typeName, $keyName)
561
                );
562
            }
563
564
            $keyValue = $keyType->convertToOData($keyValue);
565
            $keyString .= $comma . $keyName . '=' . $keyValue;
566
            $comma = ',';
567
        }
568
569
        $keyString .= ')';
570
571
        return $keyString;
572
    }
573
574
    /**
575
     * @param $entryObject
576
     * @param $type
577
     * @param $relativeUri
578
     * @param $resourceType
579
     * @return array<ODataMediaLink|null|array>
580
     */
581
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
582
    {
583
        $context = $this->getService()->getOperationContext();
584
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
585
        assert(null != $streamProviderWrapper, 'Retrieved stream provider must not be null');
586
587
        $mediaLink = null;
588
        if ($resourceType->isMediaLinkEntry()) {
589
            $eTag = $streamProviderWrapper->getStreamETag2($entryObject, null, $context);
0 ignored issues
show
Bug introduced by
The method getStreamETag2() does not exist on POData\Providers\Stream\StreamProviderWrapper. Did you maybe mean getStreamETag()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
590
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
591
        }
592
        $mediaLinks = [];
593
        if ($resourceType->hasNamedStream()) {
594
            $namedStreams = $resourceType->getAllNamedStreams();
595
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
596
                $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 getReadStream()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
597
                    $entryObject,
598
                    $resourceStreamInfo,
599
                    $context,
600
                    $relativeUri
601
                );
602
                $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()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
603
                    $entryObject,
604
                    $resourceStreamInfo,
605
                    $context
606
                );
607
                $eTag = $streamProviderWrapper->getStreamETag2(
0 ignored issues
show
Bug introduced by
The method getStreamETag2() does not exist on POData\Providers\Stream\StreamProviderWrapper. Did you maybe mean getStreamETag()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
608
                    $entryObject,
609
                    $resourceStreamInfo,
610
                    $context
611
                );
612
613
                $nuLink = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
614
                $mediaLinks[] = $nuLink;
615
            }
616
        }
617
        return [$mediaLink, $mediaLinks];
618
    }
619
620
    /**
621
     * Gets collection of projection nodes under the current node.
622
     *
623
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes describing projections for the current
624
     *                                                        segment, If this method returns null it means no
625
     *                                                        projections are to be applied and the entire resource for
626
     *                                                        the current segment should be serialized, If it returns
627
     *                                                        non-null only the properties described by the returned
628
     *                                                        projection segments should be serialized
629
     */
630
    protected function getProjectionNodes()
631
    {
632
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
633
        if (is_null($expandedProjectionNode) || $expandedProjectionNode->canSelectAllProperties()) {
634
            return null;
635
        }
636
637
        return $expandedProjectionNode->getChildNodes();
638
    }
639
640
    /**
641
     * Find a 'ExpandedProjectionNode' instance in the projection tree
642
     * which describes the current segment.
643
     *
644
     * @return null|RootProjectionNode|ExpandedProjectionNode
645
     */
646
    protected function getCurrentExpandedProjectionNode()
647
    {
648
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
649
        if (is_null($expandedProjectionNode)) {
650
            return null;
651
        } else {
652
            $segmentNames = $this->getLightStack();
653
            $depth = count($segmentNames);
654
            // $depth == 1 means serialization of root entry
655
            //(the resource identified by resource path) is going on,
656
            //so control won't get into the below for loop.
657
            //we will directly return the root node,
658
            //which is 'ExpandedProjectionNode'
659
            // for resource identified by resource path.
660
            if (0 != $depth) {
661
                for ($i = 1; $i < $depth; ++$i) {
662
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segmentNames[$i]['prop']);
663
                    assert(!is_null($expandedProjectionNode), 'is_null($expandedProjectionNode)');
664
                    assert(
665
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
666
                        '$expandedProjectionNode not instanceof ExpandedProjectionNode'
667
                    );
668
                }
669
            }
670
        }
671
672
        return $expandedProjectionNode;
673
    }
674
675
    /**
676
     * Check whether to expand a navigation property or not.
677
     *
678
     * @param string $navigationPropertyName Name of naviagtion property in question
679
     *
680
     * @return bool True if the given navigation should be expanded, otherwise false
681
     */
682
    protected function shouldExpandSegment($navigationPropertyName)
683
    {
684
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
685
        if (is_null($expandedProjectionNode)) {
686
            return false;
687
        }
688
        $expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName);
689
690
        // null is a valid input to an instanceof call as of PHP 5.6 - will always return false
691
        return $expandedProjectionNode instanceof ExpandedProjectionNode;
692
    }
693
694
    /**
695
     * Wheter next link is needed for the current resource set (feed)
696
     * being serialized.
697
     *
698
     * @param int $resultSetCount Number of entries in the current
699
     *                            resource set
700
     *
701
     * @return bool true if the feed must have a next page link
702
     */
703
    protected function needNextPageLink($resultSetCount)
704
    {
705
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
706
        $recursionLevel = count($this->getStack()->getSegmentNames());
707
        $pageSize = $currentResourceSet->getResourceSetPageSize();
708
709
        if (1 == $recursionLevel) {
710
            //presence of $top option affect next link for root container
711
            $topValueCount = $this->getRequest()->getTopOptionCount();
712
            if (!is_null($topValueCount) && ($topValueCount <= $pageSize)) {
713
                return false;
714
            }
715
        }
716
        return $resultSetCount == $pageSize;
717
    }
718
719
    /**
720
     * Resource set wrapper for the resource being serialized.
721
     *
722
     * @return ResourceSetWrapper
723
     */
724
    protected function getCurrentResourceSetWrapper()
725
    {
726
        $segmentWrappers = $this->getStack()->getSegmentWrappers();
727
        $count = count($segmentWrappers);
728
729
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $segmentWrappers[$count - 1];
730
    }
731
732
    /**
733
     * Get next page link from the given entity instance.
734
     *
735
     * @param mixed &$lastObject Last object serialized to be
736
     *                            used for generating $skiptoken
737
     * @return string for the link for next page
738
     * @throws ODataException
739
     *
740
     */
741
    protected function getNextLinkUri(&$lastObject)
742
    {
743
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
744
        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
745
        assert(null != $internalOrderByInfo);
746
        assert(is_object($internalOrderByInfo));
747
        assert($internalOrderByInfo instanceof InternalOrderByInfo, get_class($internalOrderByInfo));
748
        $numSegments = count($internalOrderByInfo->getOrderByPathSegments());
749
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
750
751
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
752
        assert(!is_null($skipToken), '!is_null($skipToken)');
753
        $token = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
754
        $skipToken = '?'.$queryParameterString.$token.$skipToken;
755
756
        return $skipToken;
757
    }
758
759
    /**
760
     * Builds the string corresponding to query parameters for top level results
761
     * (result set identified by the resource path) to be put in next page link.
762
     *
763
     * @return string|null string representing the query parameters in the URI
764
     *                     query parameter format, NULL if there
765
     *                     is no query parameters
766
     *                     required for the next link of top level result set
767
     */
768
    protected function getNextPageLinkQueryParametersForRootResourceSet()
769
    {
770
        $queryParameterString = null;
771
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
772
                     ODataConstants::HTTPQUERY_STRING_EXPAND,
773
                     ODataConstants::HTTPQUERY_STRING_ORDERBY,
774
                     ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
775
                     ODataConstants::HTTPQUERY_STRING_SELECT, ] as $queryOption) {
776
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
777
            if (!is_null($value)) {
778
                if (!is_null($queryParameterString)) {
779
                    $queryParameterString = $queryParameterString . '&';
780
                }
781
782
                $queryParameterString .= $queryOption . '=' . $value;
783
            }
784
        }
785
786
        $topCountValue = $this->getRequest()->getTopOptionCount();
787
        if (!is_null($topCountValue)) {
788
            $remainingCount = $topCountValue - $this->getRequest()->getTopCount();
789
            if (0 < $remainingCount) {
790
                if (!is_null($queryParameterString)) {
791
                    $queryParameterString .= '&';
792
                }
793
794
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
795
            }
796
        }
797
798
        if (!is_null($queryParameterString)) {
799
            $queryParameterString .= '&';
800
        }
801
802
        return $queryParameterString;
803
    }
804
805
    private function loadStackIfEmpty()
806
    {
807
        if (0 == count($this->lightStack)) {
808
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
809
            array_push($this->lightStack, ['type' => $typeName, 'property' => $typeName, 'count' => 1]);
810
        }
811
    }
812
813
    /**
814
     * Convert the given primitive value to string.
815
     * Note: This method will not handle null primitive value.
816
     *
817
     * @param IType &$primitiveResourceType        Type of the primitive property
818
     *                                             whose value need to be converted
819
     * @param mixed        $primitiveValue         Primitive value to convert
820
     *
821
     * @return string
822
     */
823
    private function primitiveToString(IType &$type, $primitiveValue)
824
    {
825
        if ($type instanceof Boolean) {
826
            $stringValue = (true === $primitiveValue) ? 'true' : 'false';
827
        } elseif ($type instanceof Binary) {
828
            $stringValue = base64_encode($primitiveValue);
829
        } elseif ($type instanceof DateTime && $primitiveValue instanceof \DateTime) {
830
            $stringValue = $primitiveValue->format(\DateTime::ATOM);
831
        } elseif ($type instanceof StringType) {
832
            $stringValue = utf8_encode($primitiveValue);
833
        } else {
834
            $stringValue = strval($primitiveValue);
835
        }
836
837
        return $stringValue;
838
    }
839
840
    /**
841
     * @param $entryObject
842
     * @param $nonRelProp
843
     * @return ODataPropertyContent
844
     */
845
    private function writePrimitiveProperties($entryObject, $nonRelProp)
846
    {
847
        $propertyContent = new ODataPropertyContent();
848
        $cereal = $this->getModelSerialiser()->bulkSerialise($entryObject);
849
        foreach ($cereal as $corn => $flake) {
850
            if (!array_key_exists($corn, $nonRelProp)) {
851
                continue;
852
            }
853
            $corn = strval($corn);
854
            $rType = $nonRelProp[$corn]->getResourceType()->getInstanceType();
855
            $subProp = new ODataProperty();
856
            $subProp->name = $corn;
857
            $subProp->value = isset($flake) ? $this->primitiveToString($rType, $flake) : null;
858
            $subProp->typeName = $nonRelProp[$corn]->getResourceType()->getFullName();
859
            $propertyContent->properties[$corn] = $subProp;
860
        }
861
        return $propertyContent;
862
    }
863
864
    /**
865
     * @param $entryObject
866
     * @param $prop
867
     * @param $nuLink
868
     * @param $propKind
869
     * @param $propName
870
     */
871
    private function expandNavigationProperty(QueryResult $entryObject, $prop, $nuLink, $propKind, $propName)
872
    {
873
        $nextName = $prop->getResourceType()->getName();
874
        $nuLink->isExpanded = true;
875
        $value = $entryObject->results->$propName;
876
        $isCombo = is_array($value) || $value instanceof Collection;
877
        $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind || $isCombo;
878
        $nuLink->isCollection = $isCollection;
879
880
        $result = new QueryResult();
881
        $result->results = $value;
882
        $resultCount = $isCollection ? count($value) : 1;
883
884
        if (0 < $resultCount) {
885
            $newStackLine = ['type' => $nextName, 'prop' => $propName, 'count' => $resultCount];
886
            array_push($this->lightStack, $newStackLine);
887
            if (!$isCollection) {
888
                $nuLink->type = 'application/atom+xml;type=entry';
889
                $expandedResult = $this->writeTopLevelElement($result);
890
            } else {
891
                $nuLink->type = 'application/atom+xml;type=feed';
892
                $expandedResult = $this->writeTopLevelElements($result);
893
            }
894
            $nuLink->expandedResult = $expandedResult;
895
        }
896
        if (!isset($nuLink->expandedResult)) {
897
            $nuLink->isCollection = null;
898
            $nuLink->isExpanded = null;
899
        } else {
900
            if (isset($nuLink->expandedResult->selfLink)) {
901
                $nuLink->expandedResult->selfLink->title = $propName;
902
                $nuLink->expandedResult->selfLink->url = $nuLink->url;
903
                $nuLink->expandedResult->title = new ODataTitle($propName);
904
                $nuLink->expandedResult->id = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->url;
905
            }
906
        }
907
    }
908
909
    /**
910
     * Gets the data service instance.
911
     *
912
     * @return void
913
     */
914
    public function setService(IService $service)
915
    {
916
        $this->service = $service;
917
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
918
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
919
    }
920
921
    /**
922
     * @param ResourceType $resourceType
923
     * @param $result
924
     * @return ODataBagContent|null
925
     */
926
    protected function writeBagValue(ResourceType &$resourceType, $result)
927
    {
928
        assert(null == $result || is_array($result), 'Bag parameter must be null or array');
929
        $typeKind = $resourceType->getResourceTypeKind();
930
        assert(
931
            ResourceTypeKind::PRIMITIVE() == $typeKind || ResourceTypeKind::COMPLEX() == $typeKind,
932
            '$bagItemResourceTypeKind != ResourceTypeKind::PRIMITIVE'
933
            .' && $bagItemResourceTypeKind != ResourceTypeKind::COMPLEX'
934
        );
935
        if (null == $result) {
936
            return null;
937
        }
938
        $bag = new ODataBagContent();
939
        foreach ($result as $value) {
940
            if (isset($value)) {
941
                if (ResourceTypeKind::PRIMITIVE() == $typeKind) {
942
                    $instance = $resourceType->getInstanceType();
943
                    assert($instance instanceof IType, get_class($instance));
944
                    $bag->propertyContents[] = $this->primitiveToString($instance, $value);
945
                } elseif (ResourceTypeKind::COMPLEX() == $typeKind) {
946
                    $bag->propertyContents[] = $this->writeComplexValue($resourceType, $value);
947
                }
948
            }
949
        }
950
        return $bag;
951
    }
952
953
    /**
954
     * @param ResourceType  $resourceType
955
     * @param object        $result
956
     * @param string|null   $propertyName
957
     * @return ODataPropertyContent
958
     */
959
    protected function writeComplexValue(ResourceType &$resourceType, &$result, $propertyName = null)
960
    {
961
        assert(is_object($result), 'Supplied $customObject must be an object');
962
963
        $count = count($this->complexTypeInstanceCollection);
964
        for ($i = 0; $i < $count; ++$i) {
965
            if ($this->complexTypeInstanceCollection[$i] === $result) {
966
                throw new InvalidOperationException(
967
                    Messages::objectModelSerializerLoopsNotAllowedInComplexTypes($propertyName)
968
                );
969
            }
970
        }
971
972
        $this->complexTypeInstanceCollection[$count] = &$result;
973
974
        $internalContent = new ODataPropertyContent();
975
        $resourceProperties = $resourceType->getAllProperties();
976
        // first up, handle primitive properties
977
        foreach ($resourceProperties as $prop) {
978
            $resourceKind = $prop->getKind();
979
            $propName = $prop->getName();
980
            $internalProperty = new ODataProperty();
981
            $internalProperty->name = $propName;
982
            if (static::isMatchPrimitive($resourceKind)) {
983
                $iType = $prop->getInstanceType();
984
                assert($iType instanceof IType, get_class($iType));
985
                $internalProperty->typeName = $iType->getFullTypeName();
986
987
                $rType = $prop->getResourceType()->getInstanceType();
988
                assert($rType instanceof IType, get_class($rType));
989
                $internalProperty->value = $this->primitiveToString($rType, $result->$propName);
990
991
                $internalContent->properties[$propName] = $internalProperty;
992
            } elseif (ResourcePropertyKind::COMPLEX_TYPE == $resourceKind) {
993
                $rType = $prop->getResourceType();
994
                $internalProperty->typeName = $rType->getFullName();
995
                $internalProperty->value = $this->writeComplexValue($rType, $result->$propName, $propName);
996
997
                $internalContent->properties[$propName] = $internalProperty;
998
            }
999
        }
1000
1001
        unset($this->complexTypeInstanceCollection[$count]);
1002
        return $internalContent;
1003
    }
1004
1005
    public static function isMatchPrimitive($resourceKind)
1006
    {
1007
        if (16 > $resourceKind) {
1008
            return false;
1009
        }
1010
        if (28 < $resourceKind) {
1011
            return false;
1012
        }
1013
        return 0 == ($resourceKind % 4);
1014
    }
1015
1016
    /*
1017
     * @return IMetadataProvider
1018
     */
1019
    protected function getMetadata()
1020
    {
1021
        if (null == $this->metaProvider) {
1022
            $this->metaProvider = App::make('metadata');
1023
        }
1024
        return $this->metaProvider;
1025
    }
1026
1027
    /**
1028
     * @return array
1029
     */
1030
    protected function getLightStack()
1031
    {
1032
        return $this->lightStack;
1033
    }
1034
1035
    /**
1036
     * @return ModelSerialiser
1037
     */
1038
    public function getModelSerialiser()
1039
    {
1040
        return $this->modelSerialiser;
1041
    }
1042
}
1043