Completed
Pull Request — master (#124)
by Alex
01:56
created

IronicSerialiser::primitiveToString()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 23
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 23
rs 6.7272
cc 7
eloc 17
nc 6
nop 2
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
    private $propertiesCache = [];
54
55
    /**
56
     * @var RootProjectionNode
57
     */
58
    private $rootNode = null;
59
60
    /**
61
     * The service implementation.
62
     *
63
     * @var IService
64
     */
65
    protected $service;
66
67
    /**
68
     * Request description instance describes OData request the
69
     * the client has submitted and result of the request.
70
     *
71
     * @var RequestDescription
72
     */
73
    protected $request;
74
75
    /**
76
     * Collection of complex type instances used for cycle detection.
77
     *
78
     * @var array
79
     */
80
    protected $complexTypeInstanceCollection;
81
82
    /**
83
     * Absolute service Uri.
84
     *
85
     * @var string
86
     */
87
    protected $absoluteServiceUri;
88
89
    /**
90
     * Absolute service Uri with slash.
91
     *
92
     * @var string
93
     */
94
    protected $absoluteServiceUriWithSlash;
95
96
    /**
97
     * Holds reference to segment stack being processed.
98
     *
99
     * @var SegmentStack
100
     */
101
    protected $stack;
102
103
    /**
104
     * Lightweight stack tracking for recursive descent fill.
105
     */
106
    protected $lightStack = [];
107
108
    /**
109
     * @var ModelSerialiser
110
     */
111
    private $modelSerialiser;
112
113
114
115
    /**
116
     * @var IMetadataProvider
117
     */
118
    private $metaProvider;
119
120
    /*
121
     * Update time to insert into ODataEntry/ODataFeed fields
122
     * @var Carbon;
123
     */
124
    private $updated;
125
126
    /*
127
     * Has base URI already been written out during serialisation?
128
     * @var bool;
129
     */
130
    private $isBaseWritten = false;
131
132
    /**
133
     * @param IService                $service Reference to the data service instance
134
     * @param RequestDescription|null $request Type instance describing the client submitted request
135
     */
136
    public function __construct(IService $service, RequestDescription $request = null)
137
    {
138
        $this->service = $service;
139
        $this->request = $request;
140
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
141
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
142
        $this->stack = new SegmentStack($request);
143
        $this->complexTypeInstanceCollection = [];
144
        $this->modelSerialiser = new ModelSerialiser();
145
        $this->updated = Carbon::now();
146
    }
147
148
    /**
149
     * Write a top level entry resource.
150
     *
151
     * @param QueryResult $entryObject Reference to the entry object to be written
152
     *
153
     * @return ODataEntry|null
154
     */
155
    public function writeTopLevelElement(QueryResult $entryObject)
156
    {
157
        if (!isset($entryObject->results)) {
158
            array_pop($this->lightStack);
159
            return null;
160
        }
161
        assert($entryObject instanceof QueryResult, get_class($entryObject));
162
        assert($entryObject->results instanceof Model, get_class($entryObject->results));
163
        $this->loadStackIfEmpty();
164
165
        $baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
166
        $this->isBaseWritten = true;
167
168
        $stackCount = count($this->lightStack);
169
        $topOfStack = $this->lightStack[$stackCount-1];
170
        $payloadClass = get_class($entryObject->results);
171
        $resourceType = $this->getService()->getProvidersWrapper()->resolveResourceType($topOfStack['type']);
172
173
        // need gubbinz to unpack an abstract resource type
174
        if ($resourceType->isAbstract()) {
175
            $derived = $this->getMetadata()->getDerivedTypes($resourceType);
176
            assert(0 < count($derived));
177
            foreach ($derived as $rawType) {
178
                if (!$rawType->isAbstract()) {
179
                    $name = $rawType->getInstanceType()->getName();
180
                    if ($payloadClass == $name) {
181
                        $resourceType = $rawType;
182
                        break;
183
                    }
184
                }
185
            }
186
            // despite all set up, checking, etc, if we haven't picked a concrete resource type,
187
            // wheels have fallen off, so blow up
188
            assert(!$resourceType->isAbstract(), 'Concrete resource type not selected for payload ' . $payloadClass);
189
        }
190
191
        // make sure we're barking up right tree
192
        assert($resourceType instanceof ResourceEntityType, get_class($resourceType));
193
        $targClass = $resourceType->getInstanceType()->getName();
194
        assert(
195
            $entryObject->results instanceof $targClass,
196
            'Object being serialised not instance of expected class, ' . $targClass . ', is actually ' . $payloadClass
197
        );
198
199
        if (!array_key_exists($targClass, $this->propertiesCache)) {
200
            $rawProp = $resourceType->getAllProperties();
201
            $relProp = [];
202
            $nonRelProp = [];
203
            foreach ($rawProp as $prop) {
204
                $propType = $prop->getResourceType();
205
                if ($propType instanceof ResourceEntityType) {
206
                    $relProp[] = $prop;
207
                } else {
208
                    $nonRelProp[$prop->getName()] = ['prop' => $prop, 'type' => $propType->getInstanceType()];
209
                }
210
            }
211
            $this->propertiesCache[$targClass] = ['rel' => $relProp, 'nonRel' => $nonRelProp];
212
        }
213
        unset($relProp);
214
        unset($nonRelProp);
215
        $relProp = $this->propertiesCache[$targClass]['rel'];
216
        $nonRelProp = $this->propertiesCache[$targClass]['nonRel'];
217
218
        $resourceSet = $resourceType->getCustomState();
219
        assert($resourceSet instanceof ResourceSet);
220
        $title = $resourceType->getName();
221
        $type = $resourceType->getFullName();
222
223
        $relativeUri = $this->getEntryInstanceKey(
224
            $entryObject->results,
225
            $resourceType,
226
            $resourceSet->getName()
227
        );
228
        $absoluteUri = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
229
230
        list($mediaLink, $mediaLinks) = $this->writeMediaData(
231
            $entryObject->results,
232
            $type,
233
            $relativeUri,
234
            $resourceType
235
        );
236
237
        $propertyContent = $this->writePrimitiveProperties($entryObject->results, $nonRelProp);
238
239
        $links = [];
240
        foreach ($relProp as $prop) {
241
            $nuLink = new ODataLink();
242
            $propKind = $prop->getKind();
243
244
            assert(
245
                ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind
246
                || ResourcePropertyKind::RESOURCE_REFERENCE == $propKind,
247
                '$propKind != ResourcePropertyKind::RESOURCESET_REFERENCE &&'
248
                .' $propKind != ResourcePropertyKind::RESOURCE_REFERENCE'
249
            );
250
            $propTail = ResourcePropertyKind::RESOURCE_REFERENCE == $propKind ? 'entry' : 'feed';
251
            $propType = 'application/atom+xml;type=' . $propTail;
252
            $propName = $prop->getName();
253
            $nuLink->title = $propName;
254
            $nuLink->name = ODataConstants::ODATA_RELATED_NAMESPACE . $propName;
255
            $nuLink->url = $relativeUri . '/' . $propName;
256
            $nuLink->type = $propType;
257
258
            $navProp = new ODataNavigationPropertyInfo($prop, $this->shouldExpandSegment($propName));
259
            if ($navProp->expanded) {
260
                $this->expandNavigationProperty($entryObject, $prop, $nuLink, $propKind, $propName);
261
            }
262
263
            $links[] = $nuLink;
264
        }
265
266
        $odata = new ODataEntry();
267
        $odata->resourceSetName = $resourceSet->getName();
268
        $odata->id = $absoluteUri;
269
        $odata->title = new ODataTitle($title);
270
        $odata->type = new ODataCategory($type);
271
        $odata->propertyContent = $propertyContent;
272
        $odata->isMediaLinkEntry = $resourceType->isMediaLinkEntry();
273
        $odata->editLink = new ODataLink();
274
        $odata->editLink->url = $relativeUri;
275
        $odata->editLink->name = 'edit';
276
        $odata->editLink->title = $title;
277
        $odata->mediaLink = $mediaLink;
278
        $odata->mediaLinks = $mediaLinks;
279
        $odata->links = $links;
280
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
281
        $odata->baseURI = $baseURI;
282
283
        $newCount = count($this->lightStack);
284
        assert(
285
            $newCount == $stackCount,
286
            'Should have ' . $stackCount . ' elements in stack, have ' . $newCount . ' elements'
287
        );
288
        $this->lightStack[$newCount-1]['count']--;
289
        if (0 == $this->lightStack[$newCount-1]['count']) {
290
            array_pop($this->lightStack);
291
        }
292
        return $odata;
293
    }
294
295
    /**
296
     * Write top level feed element.
297
     *
298
     * @param QueryResult &$entryObjects Array of entry resources to be written
299
     *
300
     * @return ODataFeed
301
     */
302
    public function writeTopLevelElements(QueryResult & $entryObjects)
303
    {
304
        $res = $entryObjects->results;
305
        assert(is_array($res) || $res instanceof Collection, '!is_array($entryObjects->results)');
306
307
        $this->loadStackIfEmpty();
308
        $setName = $this->getRequest()->getTargetResourceSetWrapper()->getName();
309
310
        $title = $this->getRequest()->getContainerName();
311
        $relativeUri = $this->getRequest()->getIdentifier();
312
        $absoluteUri = $this->getRequest()->getRequestUrl()->getUrlAsString();
313
314
        $selfLink = new ODataLink();
315
        $selfLink->name = 'self';
316
        $selfLink->title = $relativeUri;
317
        $selfLink->url = $relativeUri;
318
319
        $odata = new ODataFeed();
320
        $odata->title = new ODataTitle($title);
321
        $odata->id = $absoluteUri;
322
        $odata->selfLink = $selfLink;
323
        $odata->updated = $this->getUpdated()->format(DATE_ATOM);
324
        $odata->baseURI = $this->isBaseWritten ? null : $this->absoluteServiceUriWithSlash;
325
        $this->isBaseWritten = true;
326
327
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
328
            $odata->rowCount = $this->getRequest()->getCountValue();
329
        }
330
        foreach ($res as $entry) {
331
            if (!$entry instanceof QueryResult) {
332
                $query = new QueryResult();
333
                $query->results = $entry;
334
            } else {
335
                $query = $entry;
336
            }
337
            assert($query instanceof QueryResult, get_class($query));
338
            assert($query->results instanceof Model, get_class($query->results));
339
            $odata->entries[] = $this->writeTopLevelElement($query);
340
        }
341
342
        $resourceSet = $this->getRequest()->getTargetResourceSetWrapper()->getResourceSet();
343
        $requestTop = $this->getRequest()->getTopOptionCount();
344
        $pageSize = $this->getService()->getConfiguration()->getEntitySetPageSize($resourceSet);
345
        $requestTop = (null === $requestTop) ? $pageSize+1 : $requestTop;
346
347
        if (true === $entryObjects->hasMore && $requestTop > $pageSize) {
348
            $stackSegment = $setName;
349
            $lastObject = end($entryObjects->results);
350
            $segment = $this->getNextLinkUri($lastObject);
351
            $nextLink = new ODataLink();
352
            $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
353
            $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
354
            $odata->nextPageLink = $nextLink;
355
        }
356
357
        return $odata;
358
    }
359
360
    /**
361
     * Write top level url element.
362
     *
363
     * @param QueryResult $entryObject The entry resource whose url to be written
364
     *
365
     * @return ODataURL
366
     */
367
    public function writeUrlElement(QueryResult $entryObject)
368
    {
369
        $url = new ODataURL();
370
        if (null !== $entryObject->results) {
371
            $currentResourceType = $this->getCurrentResourceSetWrapper()->getResourceType();
372
            $relativeUri = $this->getEntryInstanceKey(
373
                $entryObject->results,
374
                $currentResourceType,
375
                $this->getCurrentResourceSetWrapper()->getName()
376
            );
377
378
            $url->url = rtrim($this->absoluteServiceUri, '/') . '/' . $relativeUri;
379
        }
380
381
        return $url;
382
    }
383
384
    /**
385
     * Write top level url collection.
386
     *
387
     * @param QueryResult $entryObjects Array of entry resources whose url to be written
388
     *
389
     * @return ODataURLCollection
390
     */
391
    public function writeUrlElements(QueryResult $entryObjects)
392
    {
393
        $urls = new ODataURLCollection();
394
        if (!empty($entryObjects->results)) {
395
            $i = 0;
396
            foreach ($entryObjects->results as $entryObject) {
397
                if (!$entryObject instanceof QueryResult) {
398
                    $query = new QueryResult();
399
                    $query->results = $entryObject;
400
                } else {
401
                    $query = $entryObject;
402
                }
403
                $urls->urls[$i] = $this->writeUrlElement($query);
404
                ++$i;
405
            }
406
407
            if ($i > 0 && true === $entryObjects->hasMore) {
408
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
409
                $lastObject = end($entryObjects->results);
410
                $segment = $this->getNextLinkUri($lastObject);
411
                $nextLink = new ODataLink();
412
                $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
413
                $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
414
                $urls->nextPageLink = $nextLink;
415
            }
416
        }
417
418
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
419
            $urls->count = $this->getRequest()->getCountValue();
420
        }
421
422
        return $urls;
423
    }
424
425
    /**
426
     * Write top level complex resource.
427
     *
428
     * @param QueryResult  &$complexValue The complex object to be written
429
     * @param string       $propertyName  The name of the complex property
430
     * @param ResourceType &$resourceType Describes the type of complex object
431
     *
432
     * @return ODataPropertyContent
433
     */
434
    public function writeTopLevelComplexObject(QueryResult & $complexValue, $propertyName, ResourceType & $resourceType)
435
    {
436
        $result = $complexValue->results;
437
438
        $propertyContent = new ODataPropertyContent();
439
        $odataProperty = new ODataProperty();
440
        $odataProperty->name = $propertyName;
441
        $odataProperty->typeName = $resourceType->getFullName();
442
        if (null != $result) {
443
            $internalContent = $this->writeComplexValue($resourceType, $result);
0 ignored issues
show
Bug introduced by
It seems like $result defined by $complexValue->results on line 436 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...
444
            $odataProperty->value = $internalContent;
445
        }
446
447
        $propertyContent->properties[$propertyName] = $odataProperty;
448
449
        return $propertyContent;
450
    }
451
452
    /**
453
     * Write top level bag resource.
454
     *
455
     * @param QueryResult  &$BagValue     The bag object to be
456
     *                                    written
457
     * @param string       $propertyName  The name of the
458
     *                                    bag property
459
     * @param ResourceType &$resourceType Describes the type of
460
     *                                    bag object
461
     *
462
     * @return ODataPropertyContent
463
     */
464
    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...
465
    {
466
        $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...
467
468
        $propertyContent = new ODataPropertyContent();
469
        $odataProperty = new ODataProperty();
470
        $odataProperty->name = $propertyName;
471
        $odataProperty->typeName = 'Collection(' . $resourceType->getFullName() . ')';
472
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
473
474
        $propertyContent->properties[$propertyName] = $odataProperty;
475
        return $propertyContent;
476
    }
477
478
    /**
479
     * Write top level primitive value.
480
     *
481
     * @param  QueryResult          &$primitiveValue   The primitive value to be
482
     *                                                 written
483
     * @param  ResourceProperty     &$resourceProperty Resource property describing the
484
     *                                                 primitive property to be written
485
     * @return ODataPropertyContent
486
     */
487
    public function writeTopLevelPrimitive(QueryResult & $primitiveValue, ResourceProperty & $resourceProperty = null)
488
    {
489
        assert(null != $resourceProperty, 'Resource property must not be null');
490
        $propertyContent = new ODataPropertyContent();
491
492
        $odataProperty = new ODataProperty();
493
        $odataProperty->name = $resourceProperty->getName();
494
        $iType = $resourceProperty->getInstanceType();
495
        assert($iType instanceof IType, get_class($iType));
496
        $odataProperty->typeName = $iType->getFullTypeName();
497
        if (null == $primitiveValue->results) {
498
            $odataProperty->value = null;
499
        } else {
500
            $rType = $resourceProperty->getResourceType()->getInstanceType();
501
            assert($rType instanceof IType, get_class($rType));
502
            $odataProperty->value = $this->primitiveToString($rType, $primitiveValue->results);
503
        }
504
505
        $propertyContent->properties[$odataProperty->name] = $odataProperty;
506
507
        return $propertyContent;
508
    }
509
510
    /**
511
     * Gets reference to the request submitted by client.
512
     *
513
     * @return RequestDescription
514
     */
515
    public function getRequest()
516
    {
517
        assert(null != $this->request, 'Request not yet set');
518
519
        return $this->request;
520
    }
521
522
    /**
523
     * Sets reference to the request submitted by client.
524
     *
525
     * @param RequestDescription $request
526
     */
527
    public function setRequest(RequestDescription $request)
528
    {
529
        $this->request = $request;
530
        $this->stack->setRequest($request);
531
    }
532
533
    /**
534
     * Gets the data service instance.
535
     *
536
     * @return IService
537
     */
538
    public function getService()
539
    {
540
        return $this->service;
541
    }
542
543
    /**
544
     * Gets the segment stack instance.
545
     *
546
     * @return SegmentStack
547
     */
548
    public function getStack()
549
    {
550
        return $this->stack;
551
    }
552
553
    /**
554
     * Get update timestamp.
555
     *
556
     * @return Carbon
557
     */
558
    public function getUpdated()
559
    {
560
        return $this->updated;
561
    }
562
563
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
564
    {
565
        $typeName = $resourceType->getName();
566
        $keyProperties = $resourceType->getKeyProperties();
567
        assert(count($keyProperties) != 0, 'count($keyProperties) == 0');
568
        $keyString = $containerName . '(';
569
        $comma = null;
570
        foreach ($keyProperties as $keyName => $resourceProperty) {
571
            $keyType = $resourceProperty->getInstanceType();
572
            assert($keyType instanceof IType, '$keyType not instanceof IType');
573
            $keyName = $resourceProperty->getName();
574
            $keyValue = (self::PK == $keyName) ? $entityInstance->getKey() : $entityInstance->$keyName;
575
            if (!isset($keyValue)) {
576
                throw ODataException::createInternalServerError(
577
                    Messages::badQueryNullKeysAreNotSupported($typeName, $keyName)
578
                );
579
            }
580
581
            $keyValue = $keyType->convertToOData($keyValue);
582
            $keyString .= $comma . $keyName . '=' . $keyValue;
583
            $comma = ',';
584
        }
585
586
        $keyString .= ')';
587
588
        return $keyString;
589
    }
590
591
    /**
592
     * @param $entryObject
593
     * @param $type
594
     * @param $relativeUri
595
     * @param $resourceType
596
     * @return array<ODataMediaLink|null|array>
597
     */
598
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
599
    {
600
        $context = $this->getService()->getOperationContext();
601
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
602
        assert(null != $streamProviderWrapper, 'Retrieved stream provider must not be null');
603
604
        $mediaLink = null;
605
        if ($resourceType->isMediaLinkEntry()) {
606
            $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...
607
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
608
        }
609
        $mediaLinks = [];
610
        if ($resourceType->hasNamedStream()) {
611
            $namedStreams = $resourceType->getAllNamedStreams();
612
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
613
                $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...
614
                    $entryObject,
615
                    $resourceStreamInfo,
616
                    $context,
617
                    $relativeUri
618
                );
619
                $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...
620
                    $entryObject,
621
                    $resourceStreamInfo,
622
                    $context
623
                );
624
                $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...
625
                    $entryObject,
626
                    $resourceStreamInfo,
627
                    $context
628
                );
629
630
                $nuLink = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
631
                $mediaLinks[] = $nuLink;
632
            }
633
        }
634
        return [$mediaLink, $mediaLinks];
635
    }
636
637
    /**
638
     * Gets collection of projection nodes under the current node.
639
     *
640
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes describing projections for the current
641
     *                                                        segment, If this method returns null it means no
642
     *                                                        projections are to be applied and the entire resource for
643
     *                                                        the current segment should be serialized, If it returns
644
     *                                                        non-null only the properties described by the returned
645
     *                                                        projection segments should be serialized
646
     */
647
    protected function getProjectionNodes()
648
    {
649
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
650
        if (null === $expandedProjectionNode || $expandedProjectionNode->canSelectAllProperties()) {
651
            return null;
652
        }
653
654
        return $expandedProjectionNode->getChildNodes();
655
    }
656
657
    /**
658
     * Find a 'ExpandedProjectionNode' instance in the projection tree
659
     * which describes the current segment.
660
     *
661
     * @return null|RootProjectionNode|ExpandedProjectionNode
662
     */
663
    protected function getCurrentExpandedProjectionNode()
664
    {
665
        if (null === $this->rootNode) {
666
            $this->rootNode = $this->getRequest()->getRootProjectionNode();
667
        }
668
        $expandedProjectionNode = $this->rootNode;
669
        if (null === $expandedProjectionNode) {
670
            return null;
671
        } else {
672
            $segmentNames = $this->getLightStack();
673
            $depth = count($segmentNames);
674
            // $depth == 1 means serialization of root entry
675
            //(the resource identified by resource path) is going on,
676
            //so control won't get into the below for loop.
677
            //we will directly return the root node,
678
            //which is 'ExpandedProjectionNode'
679
            // for resource identified by resource path.
680
            if (0 != $depth) {
681
                for ($i = 1; $i < $depth; ++$i) {
682
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segmentNames[$i]['prop']);
683
                    assert(null !== $expandedProjectionNode, 'is_null($expandedProjectionNode)');
684
                    assert(
685
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
686
                        '$expandedProjectionNode not instanceof ExpandedProjectionNode'
687
                    );
688
                }
689
            }
690
        }
691
692
        return $expandedProjectionNode;
693
    }
694
695
    /**
696
     * Check whether to expand a navigation property or not.
697
     *
698
     * @param string $navigationPropertyName Name of naviagtion property in question
699
     *
700
     * @return bool True if the given navigation should be expanded, otherwise false
701
     */
702
    protected function shouldExpandSegment($navigationPropertyName)
703
    {
704
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
705
        if (null === $expandedProjectionNode) {
706
            return false;
707
        }
708
        $expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName);
709
710
        // null is a valid input to an instanceof call as of PHP 5.6 - will always return false
711
        return $expandedProjectionNode instanceof ExpandedProjectionNode;
712
    }
713
714
    /**
715
     * Wheter next link is needed for the current resource set (feed)
716
     * being serialized.
717
     *
718
     * @param int $resultSetCount Number of entries in the current
719
     *                            resource set
720
     *
721
     * @return bool true if the feed must have a next page link
722
     */
723
    protected function needNextPageLink($resultSetCount)
724
    {
725
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
726
        $recursionLevel = count($this->getStack()->getSegmentNames());
727
        $pageSize = $currentResourceSet->getResourceSetPageSize();
728
729
        if (1 == $recursionLevel) {
730
            //presence of $top option affect next link for root container
731
            $topValueCount = $this->getRequest()->getTopOptionCount();
732
            if (null !== $topValueCount && ($topValueCount <= $pageSize)) {
733
                return false;
734
            }
735
        }
736
        return $resultSetCount == $pageSize;
737
    }
738
739
    /**
740
     * Resource set wrapper for the resource being serialized.
741
     *
742
     * @return ResourceSetWrapper
743
     */
744
    protected function getCurrentResourceSetWrapper()
745
    {
746
        $segmentWrappers = $this->getStack()->getSegmentWrappers();
747
        $count = count($segmentWrappers);
748
749
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $segmentWrappers[$count-1];
750
    }
751
752
    /**
753
     * Get next page link from the given entity instance.
754
     *
755
     * @param  mixed          &$lastObject Last object serialized to be
756
     *                                     used for generating
757
     *                                     $skiptoken
758
     * @throws ODataException
759
     * @return string         for the link for next page
760
     */
761
    protected function getNextLinkUri(&$lastObject)
762
    {
763
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
764
        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
765
        assert(null != $internalOrderByInfo);
766
        assert(is_object($internalOrderByInfo));
767
        assert($internalOrderByInfo instanceof InternalOrderByInfo, get_class($internalOrderByInfo));
768
        $numSegments = count($internalOrderByInfo->getOrderByPathSegments());
769
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
770
771
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
772
        assert(null !== $skipToken, '!is_null($skipToken)');
773
        $token = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
774
        $skipToken = '?' . $queryParameterString . $token . $skipToken;
775
776
        return $skipToken;
777
    }
778
779
    /**
780
     * Builds the string corresponding to query parameters for top level results
781
     * (result set identified by the resource path) to be put in next page link.
782
     *
783
     * @return string|null string representing the query parameters in the URI
784
     *                     query parameter format, NULL if there
785
     *                     is no query parameters
786
     *                     required for the next link of top level result set
787
     */
788
    protected function getNextPageLinkQueryParametersForRootResourceSet()
789
    {
790
        $queryParameterString = null;
791
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
792
                     ODataConstants::HTTPQUERY_STRING_EXPAND,
793
                     ODataConstants::HTTPQUERY_STRING_ORDERBY,
794
                     ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
795
                     ODataConstants::HTTPQUERY_STRING_SELECT, ] as $queryOption) {
796
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
797
            if (null !== $value) {
798
                if (null !== $queryParameterString) {
799
                    $queryParameterString = $queryParameterString . '&';
800
                }
801
802
                $queryParameterString .= $queryOption . '=' . $value;
803
            }
804
        }
805
806
        $topCountValue = $this->getRequest()->getTopOptionCount();
807
        if (null !== $topCountValue) {
808
            $remainingCount = $topCountValue-$this->getRequest()->getTopCount();
809
            if (0 < $remainingCount) {
810
                if (null !== $queryParameterString) {
811
                    $queryParameterString .= '&';
812
                }
813
814
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
815
            }
816
        }
817
818
        if (null !== $queryParameterString) {
819
            $queryParameterString .= '&';
820
        }
821
822
        return $queryParameterString;
823
    }
824
825
    private function loadStackIfEmpty()
826
    {
827
        if (0 == count($this->lightStack)) {
828
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
829
            array_push($this->lightStack, ['type' => $typeName, 'property' => $typeName, 'count' => 1]);
830
        }
831
    }
832
833
    /**
834
     * Convert the given primitive value to string.
835
     * Note: This method will not handle null primitive value.
836
     *
837
     * @param IType &$primitiveResourceType Type of the primitive property
838
     *                                      whose value need to be converted
839
     * @param mixed $primitiveValue         Primitive value to convert
840
     *
841
     * @return string
842
     */
843
    private function primitiveToString(IType & $type, $primitiveValue)
844
    {
845
        // kludge to enable switching on type of $type without getting tripped up by mocks as we would with get_class
846
        // switch (true) means we unconditionally enter, and then lean on case statements to match given block
847
        switch (true) {
848
            case $type instanceof StringType:
849
                $stringValue = utf8_encode($primitiveValue);
850
                break;
851
            case $type instanceof Boolean:
852
                $stringValue = (true === $primitiveValue) ? 'true' : 'false';
853
                break;
854
            case $type instanceof Binary:
855
                $stringValue = base64_encode($primitiveValue);
856
                break;
857
            case $type instanceof DateTime && $primitiveValue instanceof \DateTime:
858
                $stringValue = $primitiveValue->format(\DateTime::ATOM);
859
                break;
860
            default:
861
                $stringValue = strval($primitiveValue);
862
        }
863
864
        return $stringValue;
865
    }
866
867
    /**
868
     * @param $entryObject
869
     * @param $nonRelProp
870
     * @return ODataPropertyContent
871
     */
872
    private function writePrimitiveProperties(Model $entryObject, $nonRelProp)
873
    {
874
        $propertyContent = new ODataPropertyContent();
875
        $cereal = $this->getModelSerialiser()->bulkSerialise($entryObject);
876
        foreach ($cereal as $corn => $flake) {
877
            if (!array_key_exists($corn, $nonRelProp)) {
878
                continue;
879
            }
880
            $corn = strval($corn);
881
            $rType = $nonRelProp[$corn]['type'];
882
            $subProp = new ODataProperty();
883
            $subProp->name = $corn;
884
            $subProp->value = isset($flake) ? $this->primitiveToString($rType, $flake) : null;
885
            $subProp->typeName = $nonRelProp[$corn]['prop']->getResourceType()->getFullName();
886
            $propertyContent->properties[$corn] = $subProp;
887
        }
888
        return $propertyContent;
889
    }
890
891
    /**
892
     * @param $entryObject
893
     * @param $prop
894
     * @param $nuLink
895
     * @param $propKind
896
     * @param $propName
897
     */
898
    private function expandNavigationProperty(QueryResult $entryObject, $prop, $nuLink, $propKind, $propName)
899
    {
900
        $nextName = $prop->getResourceType()->getName();
901
        $nuLink->isExpanded = true;
902
        $value = $entryObject->results->$propName;
903
        $isCombo = is_array($value) || $value instanceof Collection;
904
        $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind || $isCombo;
905
        $nuLink->isCollection = $isCollection;
906
907
        $result = new QueryResult();
908
        $result->results = $value;
909
        $resultCount = $isCollection ? count($value) : 1;
910
911
        if (0 < $resultCount) {
912
            $newStackLine = ['type' => $nextName, 'prop' => $propName, 'count' => $resultCount];
913
            array_push($this->lightStack, $newStackLine);
914
            if (!$isCollection) {
915
                $nuLink->type = 'application/atom+xml;type=entry';
916
                $expandedResult = $this->writeTopLevelElement($result);
917
            } else {
918
                $nuLink->type = 'application/atom+xml;type=feed';
919
                $expandedResult = $this->writeTopLevelElements($result);
920
            }
921
            $nuLink->expandedResult = $expandedResult;
922
        }
923
        if (!isset($nuLink->expandedResult)) {
924
            $nuLink->isCollection = null;
925
            $nuLink->isExpanded = null;
926
        } else {
927
            if (isset($nuLink->expandedResult->selfLink)) {
928
                $nuLink->expandedResult->selfLink->title = $propName;
929
                $nuLink->expandedResult->selfLink->url = $nuLink->url;
930
                $nuLink->expandedResult->title = new ODataTitle($propName);
931
                $nuLink->expandedResult->id = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->url;
932
            }
933
        }
934
    }
935
936
    /**
937
     * Gets the data service instance.
938
     *
939
     * @return void
940
     */
941
    public function setService(IService $service)
942
    {
943
        $this->service = $service;
944
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
945
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
946
    }
947
948
    /**
949
     * @param ResourceType $resourceType
950
     * @param $result
951
     * @return ODataBagContent|null
952
     */
953
    protected function writeBagValue(ResourceType & $resourceType, $result)
954
    {
955
        assert(null == $result || is_array($result), 'Bag parameter must be null or array');
956
        $typeKind = $resourceType->getResourceTypeKind();
957
        assert(
958
            ResourceTypeKind::PRIMITIVE() == $typeKind || ResourceTypeKind::COMPLEX() == $typeKind,
959
            '$bagItemResourceTypeKind != ResourceTypeKind::PRIMITIVE'
960
            .' && $bagItemResourceTypeKind != ResourceTypeKind::COMPLEX'
961
        );
962
        if (null == $result) {
963
            return null;
964
        }
965
        $bag = new ODataBagContent();
966
        foreach ($result as $value) {
967
            if (isset($value)) {
968
                if (ResourceTypeKind::PRIMITIVE() == $typeKind) {
969
                    $instance = $resourceType->getInstanceType();
970
                    assert($instance instanceof IType, get_class($instance));
971
                    $bag->propertyContents[] = $this->primitiveToString($instance, $value);
972
                } elseif (ResourceTypeKind::COMPLEX() == $typeKind) {
973
                    $bag->propertyContents[] = $this->writeComplexValue($resourceType, $value);
974
                }
975
            }
976
        }
977
        return $bag;
978
    }
979
980
    /**
981
     * @param  ResourceType         $resourceType
982
     * @param  object               $result
983
     * @param  string|null          $propertyName
984
     * @return ODataPropertyContent
985
     */
986
    protected function writeComplexValue(ResourceType & $resourceType, &$result, $propertyName = null)
987
    {
988
        assert(is_object($result), 'Supplied $customObject must be an object');
989
990
        $count = count($this->complexTypeInstanceCollection);
991
        for ($i = 0; $i < $count; ++$i) {
992
            if ($this->complexTypeInstanceCollection[$i] === $result) {
993
                throw new InvalidOperationException(
994
                    Messages::objectModelSerializerLoopsNotAllowedInComplexTypes($propertyName)
995
                );
996
            }
997
        }
998
999
        $this->complexTypeInstanceCollection[$count] = &$result;
1000
1001
        $internalContent = new ODataPropertyContent();
1002
        $resourceProperties = $resourceType->getAllProperties();
1003
        // first up, handle primitive properties
1004
        foreach ($resourceProperties as $prop) {
1005
            $resourceKind = $prop->getKind();
1006
            $propName = $prop->getName();
1007
            $internalProperty = new ODataProperty();
1008
            $internalProperty->name = $propName;
1009
            if (static::isMatchPrimitive($resourceKind)) {
1010
                $iType = $prop->getInstanceType();
1011
                assert($iType instanceof IType, get_class($iType));
1012
                $internalProperty->typeName = $iType->getFullTypeName();
1013
1014
                $rType = $prop->getResourceType()->getInstanceType();
1015
                assert($rType instanceof IType, get_class($rType));
1016
                $internalProperty->value = $this->primitiveToString($rType, $result->$propName);
1017
1018
                $internalContent->properties[$propName] = $internalProperty;
1019
            } elseif (ResourcePropertyKind::COMPLEX_TYPE == $resourceKind) {
1020
                $rType = $prop->getResourceType();
1021
                $internalProperty->typeName = $rType->getFullName();
1022
                $internalProperty->value = $this->writeComplexValue($rType, $result->$propName, $propName);
1023
1024
                $internalContent->properties[$propName] = $internalProperty;
1025
            }
1026
        }
1027
1028
        unset($this->complexTypeInstanceCollection[$count]);
1029
        return $internalContent;
1030
    }
1031
1032
    public static function isMatchPrimitive($resourceKind)
1033
    {
1034
        if (16 > $resourceKind) {
1035
            return false;
1036
        }
1037
        if (28 < $resourceKind) {
1038
            return false;
1039
        }
1040
        return 0 == ($resourceKind % 4);
1041
    }
1042
1043
    /*
1044
     * @return IMetadataProvider
1045
     */
1046
    protected function getMetadata()
1047
    {
1048
        if (null == $this->metaProvider) {
1049
            $this->metaProvider = App::make('metadata');
1050
        }
1051
        return $this->metaProvider;
1052
    }
1053
1054
    /**
1055
     * @return array
1056
     */
1057
    protected function getLightStack()
1058
    {
1059
        return $this->lightStack;
1060
    }
1061
1062
    /**
1063
     * @return ModelSerialiser
1064
     */
1065
    public function getModelSerialiser()
1066
    {
1067
        return $this->modelSerialiser;
1068
    }
1069
}
1070