Test Failed
Pull Request — master (#110)
by Alex
08:48
created

IronicSerialiser::writeUrlElements()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 33
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
380
                } else {
381
                    $query = $entryObject;
382
                }
383
                $urls->urls[$i] = $this->writeUrlElement($query);
384
                ++$i;
385
            }
386
387
            if ($i > 0 && true === $entryObjects->hasMore) {
388
                $stackSegment = $this->getRequest()->getTargetResourceSetWrapper()->getName();
389
                $lastObject = end($entryObjects->results);
390
                $segment = $this->getNextLinkUri($lastObject);
391
                $nextLink = new ODataLink();
392
                $nextLink->name = ODataConstants::ATOM_LINK_NEXT_ATTRIBUTE_STRING;
393
                $nextLink->url = rtrim($this->absoluteServiceUri, '/') . '/' . $stackSegment . $segment;
394
                $urls->nextPageLink = $nextLink;
395
            }
396
        }
397
398
        if ($this->getRequest()->queryType == QueryType::ENTITIES_WITH_COUNT()) {
399
            $urls->count = $this->getRequest()->getCountValue();
400
        }
401
402
        return $urls;
403
    }
404
405
    /**
406
     * Write top level complex resource.
407
     *
408
     * @param QueryResult &$complexValue The complex object to be written
409
     * @param string $propertyName The name of the complex property
410
     * @param ResourceType &$resourceType Describes the type of complex object
411
     *
412
     * @return ODataPropertyContent
413
     */
414
    public function writeTopLevelComplexObject(QueryResult &$complexValue, $propertyName, ResourceType &$resourceType)
415
    {
416
        $result = $complexValue->results;
417
418
        $propertyContent = new ODataPropertyContent();
419
        $odataProperty = new ODataProperty();
420
        $odataProperty->name = $propertyName;
421
        $odataProperty->typeName = $resourceType->getFullName();
422
        if (null != $result) {
423
            $internalContent = $this->writeComplexValue($resourceType, $result);
0 ignored issues
show
Documentation introduced by
$result is of type array<integer,object>, but the function expects a object.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
424
            $odataProperty->value = $internalContent;
425
        }
426
427
        $propertyContent->properties[$propertyName] = $odataProperty;
428
429
        return $propertyContent;
430
    }
431
432
    /**
433
     * Write top level bag resource.
434
     *
435
     * @param QueryResult &$BagValue The bag object to be
436
     *                                    written
437
     * @param string $propertyName The name of the
438
     *                                    bag property
439
     * @param ResourceType &$resourceType Describes the type of
440
     *                                    bag object
441
     * @return ODataPropertyContent
442
     */
443
    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...
444
    {
445
        $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...
446
447
        $propertyContent = new ODataPropertyContent();
448
        $odataProperty = new ODataProperty();
449
        $odataProperty->name = $propertyName;
450
        $odataProperty->typeName = 'Collection('.$resourceType->getFullName().')';
451
        $odataProperty->value = $this->writeBagValue($resourceType, $result);
452
453
        $propertyContent->properties[$propertyName] = $odataProperty;
454
        return $propertyContent;
455
    }
456
457
    /**
458
     * Write top level primitive value.
459
     *
460
     * @param QueryResult &$primitiveValue              The primitive value to be
461
     *                                            written
462
     * @param ResourceProperty &$resourceProperty Resource property describing the
463
     *                                            primitive property to be written
464
     * @return ODataPropertyContent
465
     */
466
    public function writeTopLevelPrimitive(QueryResult &$primitiveValue, ResourceProperty &$resourceProperty = null)
467
    {
468
        assert(null != $resourceProperty, 'Resource property must not be null');
469
        $propertyContent = new ODataPropertyContent();
470
471
        $odataProperty = new ODataProperty();
472
        $odataProperty->name = $resourceProperty->getName();
473
        $iType = $resourceProperty->getInstanceType();
474
        assert($iType instanceof IType, get_class($iType));
475
        $odataProperty->typeName = $iType->getFullTypeName();
476
        if (null == $primitiveValue->results) {
477
            $odataProperty->value = null;
478
        } else {
479
            $rType = $resourceProperty->getResourceType()->getInstanceType();
480
            assert($rType instanceof IType, get_class($rType));
481
            $odataProperty->value = $this->primitiveToString($rType, $primitiveValue->results);
482
        }
483
484
        $propertyContent->properties[$odataProperty->name] = $odataProperty;
485
486
        return $propertyContent;
487
    }
488
489
    /**
490
     * Gets reference to the request submitted by client.
491
     *
492
     * @return RequestDescription
493
     */
494
    public function getRequest()
495
    {
496
        assert(null != $this->request, 'Request not yet set');
497
498
        return $this->request;
499
    }
500
501
    /**
502
     * Sets reference to the request submitted by client.
503
     *
504
     * @param RequestDescription $request
505
     */
506
    public function setRequest(RequestDescription $request)
507
    {
508
        $this->request = $request;
509
        $this->stack->setRequest($request);
510
    }
511
512
    /**
513
     * Gets the data service instance.
514
     *
515
     * @return IService
516
     */
517
    public function getService()
518
    {
519
        return $this->service;
520
    }
521
522
    /**
523
     * Gets the segment stack instance.
524
     *
525
     * @return SegmentStack
526
     */
527
    public function getStack()
528
    {
529
        return $this->stack;
530
    }
531
532
    /**
533
     * Get update timestamp
534
     *
535
     * @return Carbon
536
     */
537
    public function getUpdated()
538
    {
539
        return $this->updated;
540
    }
541
542
    protected function getEntryInstanceKey($entityInstance, ResourceType $resourceType, $containerName)
543
    {
544
        $typeName = $resourceType->getName();
545
        $keyProperties = $resourceType->getKeyProperties();
546
        assert(count($keyProperties) != 0, 'count($keyProperties) == 0');
547
        $keyString = $containerName . '(';
548
        $comma = null;
549
        foreach ($keyProperties as $keyName => $resourceProperty) {
550
            $keyType = $resourceProperty->getInstanceType();
551
            assert($keyType instanceof IType, '$keyType not instanceof IType');
552
            $keyName = $resourceProperty->getName();
553
            $keyValue = (self::PK == $keyName) ? $entityInstance->getKey() : $entityInstance->$keyName;
554
            if (!isset($keyValue)) {
555
                throw ODataException::createInternalServerError(
556
                    Messages::badQueryNullKeysAreNotSupported($typeName, $keyName)
557
                );
558
            }
559
560
            $keyValue = $keyType->convertToOData($keyValue);
561
            $keyString .= $comma . $keyName . '=' . $keyValue;
562
            $comma = ',';
563
        }
564
565
        $keyString .= ')';
566
567
        return $keyString;
568
    }
569
570
    /**
571
     * @param $entryObject
572
     * @param $type
573
     * @param $relativeUri
574
     * @param $resourceType
575
     * @return array<ODataMediaLink|null|array>
576
     */
577
    protected function writeMediaData($entryObject, $type, $relativeUri, ResourceType $resourceType)
578
    {
579
        $context = $this->getService()->getOperationContext();
580
        $streamProviderWrapper = $this->getService()->getStreamProviderWrapper();
581
        assert(null != $streamProviderWrapper, 'Retrieved stream provider must not be null');
582
583
        $mediaLink = null;
584
        if ($resourceType->isMediaLinkEntry()) {
585
            $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...
586
            $mediaLink = new ODataMediaLink($type, '/$value', $relativeUri . '/$value', '*/*', $eTag, 'edit-media');
587
        }
588
        $mediaLinks = [];
589
        if ($resourceType->hasNamedStream()) {
590
            $namedStreams = $resourceType->getAllNamedStreams();
591
            foreach ($namedStreams as $streamTitle => $resourceStreamInfo) {
592
                $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...
593
                    $entryObject,
594
                    $resourceStreamInfo,
595
                    $context,
596
                    $relativeUri
597
                );
598
                $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...
599
                    $entryObject,
600
                    $resourceStreamInfo,
601
                    $context
602
                );
603
                $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...
604
                    $entryObject,
605
                    $resourceStreamInfo,
606
                    $context
607
                );
608
609
                $nuLink = new ODataMediaLink($streamTitle, $readUri, $readUri, $mediaContentType, $eTag);
610
                $mediaLinks[] = $nuLink;
611
            }
612
        }
613
        return [$mediaLink, $mediaLinks];
614
    }
615
616
    /**
617
     * Gets collection of projection nodes under the current node.
618
     *
619
     * @return ProjectionNode[]|ExpandedProjectionNode[]|null List of nodes describing projections for the current
620
     *                                                        segment, If this method returns null it means no
621
     *                                                        projections are to be applied and the entire resource for
622
     *                                                        the current segment should be serialized, If it returns
623
     *                                                        non-null only the properties described by the returned
624
     *                                                        projection segments should be serialized
625
     */
626
    protected function getProjectionNodes()
627
    {
628
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
629
        if (is_null($expandedProjectionNode) || $expandedProjectionNode->canSelectAllProperties()) {
630
            return null;
631
        }
632
633
        return $expandedProjectionNode->getChildNodes();
634
    }
635
636
    /**
637
     * Find a 'ExpandedProjectionNode' instance in the projection tree
638
     * which describes the current segment.
639
     *
640
     * @return null|RootProjectionNode|ExpandedProjectionNode
641
     */
642
    protected function getCurrentExpandedProjectionNode()
643
    {
644
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
645
        if (is_null($expandedProjectionNode)) {
646
            return null;
647
        } else {
648
            $segmentNames = $this->getLightStack();
649
            $depth = count($segmentNames);
650
            // $depth == 1 means serialization of root entry
651
            //(the resource identified by resource path) is going on,
652
            //so control won't get into the below for loop.
653
            //we will directly return the root node,
654
            //which is 'ExpandedProjectionNode'
655
            // for resource identified by resource path.
656
            if (0 != $depth) {
657
                for ($i = 1; $i < $depth; ++$i) {
658
                    $expandedProjectionNode = $expandedProjectionNode->findNode($segmentNames[$i]['prop']);
659
                    assert(!is_null($expandedProjectionNode), 'is_null($expandedProjectionNode)');
660
                    assert(
661
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
662
                        '$expandedProjectionNode not instanceof ExpandedProjectionNode'
663
                    );
664
                }
665
            }
666
        }
667
668
        return $expandedProjectionNode;
669
    }
670
671
    /**
672
     * Check whether to expand a navigation property or not.
673
     *
674
     * @param string $navigationPropertyName Name of naviagtion property in question
675
     *
676
     * @return bool True if the given navigation should be expanded, otherwise false
677
     */
678
    protected function shouldExpandSegment($navigationPropertyName)
679
    {
680
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
681
        if (is_null($expandedProjectionNode)) {
682
            return false;
683
        }
684
        $expandedProjectionNode = $expandedProjectionNode->findNode($navigationPropertyName);
685
686
        // null is a valid input to an instanceof call as of PHP 5.6 - will always return false
687
        return $expandedProjectionNode instanceof ExpandedProjectionNode;
688
    }
689
690
    /**
691
     * Wheter next link is needed for the current resource set (feed)
692
     * being serialized.
693
     *
694
     * @param int $resultSetCount Number of entries in the current
695
     *                            resource set
696
     *
697
     * @return bool true if the feed must have a next page link
698
     */
699
    protected function needNextPageLink($resultSetCount)
700
    {
701
        $currentResourceSet = $this->getCurrentResourceSetWrapper();
702
        $recursionLevel = count($this->getStack()->getSegmentNames());
703
        $pageSize = $currentResourceSet->getResourceSetPageSize();
704
705
        if (1 == $recursionLevel) {
706
            //presence of $top option affect next link for root container
707
            $topValueCount = $this->getRequest()->getTopOptionCount();
708
            if (!is_null($topValueCount) && ($topValueCount <= $pageSize)) {
709
                return false;
710
            }
711
        }
712
        return $resultSetCount == $pageSize;
713
    }
714
715
    /**
716
     * Resource set wrapper for the resource being serialized.
717
     *
718
     * @return ResourceSetWrapper
719
     */
720
    protected function getCurrentResourceSetWrapper()
721
    {
722
        $segmentWrappers = $this->getStack()->getSegmentWrappers();
723
        $count = count($segmentWrappers);
724
725
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $segmentWrappers[$count - 1];
726
    }
727
728
    /**
729
     * Get next page link from the given entity instance.
730
     *
731
     * @param mixed &$lastObject Last object serialized to be
732
     *                            used for generating $skiptoken
733
     * @return string for the link for next page
734
     * @throws ODataException
735
     *
736
     */
737
    protected function getNextLinkUri(&$lastObject)
738
    {
739
        $currentExpandedProjectionNode = $this->getCurrentExpandedProjectionNode();
740
        $internalOrderByInfo = $currentExpandedProjectionNode->getInternalOrderByInfo();
741
        assert(null != $internalOrderByInfo);
742
        assert(is_object($internalOrderByInfo));
743
        assert($internalOrderByInfo instanceof InternalOrderByInfo, get_class($internalOrderByInfo));
744
        $numSegments = count($internalOrderByInfo->getOrderByPathSegments());
745
        $queryParameterString = $this->getNextPageLinkQueryParametersForRootResourceSet();
746
747
        $skipToken = $internalOrderByInfo->buildSkipTokenValue($lastObject);
748
        assert(!is_null($skipToken), '!is_null($skipToken)');
749
        $token = (1 < $numSegments) ? '$skiptoken=' : '$skip=';
750
        $skipToken = '?'.$queryParameterString.$token.$skipToken;
751
752
        return $skipToken;
753
    }
754
755
    /**
756
     * Builds the string corresponding to query parameters for top level results
757
     * (result set identified by the resource path) to be put in next page link.
758
     *
759
     * @return string|null string representing the query parameters in the URI
760
     *                     query parameter format, NULL if there
761
     *                     is no query parameters
762
     *                     required for the next link of top level result set
763
     */
764
    protected function getNextPageLinkQueryParametersForRootResourceSet()
765
    {
766
        $queryParameterString = null;
767
        foreach ([ODataConstants::HTTPQUERY_STRING_FILTER,
768
                     ODataConstants::HTTPQUERY_STRING_EXPAND,
769
                     ODataConstants::HTTPQUERY_STRING_ORDERBY,
770
                     ODataConstants::HTTPQUERY_STRING_INLINECOUNT,
771
                     ODataConstants::HTTPQUERY_STRING_SELECT, ] as $queryOption) {
772
            $value = $this->getService()->getHost()->getQueryStringItem($queryOption);
773
            if (!is_null($value)) {
774
                if (!is_null($queryParameterString)) {
775
                    $queryParameterString = $queryParameterString . '&';
776
                }
777
778
                $queryParameterString .= $queryOption . '=' . $value;
779
            }
780
        }
781
782
        $topCountValue = $this->getRequest()->getTopOptionCount();
783
        if (!is_null($topCountValue)) {
784
            $remainingCount = $topCountValue - $this->getRequest()->getTopCount();
785
            if (0 < $remainingCount) {
786
                if (!is_null($queryParameterString)) {
787
                    $queryParameterString .= '&';
788
                }
789
790
                $queryParameterString .= ODataConstants::HTTPQUERY_STRING_TOP . '=' . $remainingCount;
791
            }
792
        }
793
794
        if (!is_null($queryParameterString)) {
795
            $queryParameterString .= '&';
796
        }
797
798
        return $queryParameterString;
799
    }
800
801
    private function loadStackIfEmpty()
802
    {
803
        if (0 == count($this->lightStack)) {
804
            $typeName = $this->getRequest()->getTargetResourceType()->getName();
805
            array_push($this->lightStack, ['type' => $typeName, 'property' => $typeName, 'count' => 1]);
806
        }
807
    }
808
809
    /**
810
     * Convert the given primitive value to string.
811
     * Note: This method will not handle null primitive value.
812
     *
813
     * @param IType &$primitiveResourceType        Type of the primitive property
814
     *                                             whose value need to be converted
815
     * @param mixed        $primitiveValue         Primitive value to convert
816
     *
817
     * @return string
818
     */
819
    private function primitiveToString(IType &$type, $primitiveValue)
820
    {
821
        if ($type instanceof Boolean) {
822
            $stringValue = (true === $primitiveValue) ? 'true' : 'false';
823
        } elseif ($type instanceof Binary) {
824
            $stringValue = base64_encode($primitiveValue);
825
        } elseif ($type instanceof DateTime && $primitiveValue instanceof \DateTime) {
826
            $stringValue = $primitiveValue->format(\DateTime::ATOM);
827
        } elseif ($type instanceof StringType) {
828
            $stringValue = utf8_encode($primitiveValue);
829
        } else {
830
            $stringValue = strval($primitiveValue);
831
        }
832
833
        return $stringValue;
834
    }
835
836
    /**
837
     * @param $entryObject
838
     * @param $nonRelProp
839
     * @return ODataPropertyContent
840
     */
841
    private function writePrimitiveProperties($entryObject, $nonRelProp)
842
    {
843
        $propertyContent = new ODataPropertyContent();
844
        $cereal = $this->getModelSerialiser()->bulkSerialise($entryObject);
845
        foreach ($cereal as $corn => $flake) {
846
            if (!array_key_exists($corn, $nonRelProp)) {
847
                continue;
848
            }
849
            $corn = strval($corn);
850
            $rType = $nonRelProp[$corn]->getResourceType()->getInstanceType();
851
            $subProp = new ODataProperty();
852
            $subProp->name = $corn;
853
            $subProp->value = isset($flake) ? $this->primitiveToString($rType, $flake) : null;
854
            $subProp->typeName = $nonRelProp[$corn]->getResourceType()->getFullName();
855
            $propertyContent->properties[$corn] = $subProp;
856
        }
857
        return $propertyContent;
858
    }
859
860
    /**
861
     * @param $entryObject
862
     * @param $prop
863
     * @param $nuLink
864
     * @param $propKind
865
     * @param $propName
866
     */
867
    private function expandNavigationProperty(QueryResult $entryObject, $prop, $nuLink, $propKind, $propName)
868
    {
869
        $nextName = $prop->getResourceType()->getName();
870
        $nuLink->isExpanded = true;
871
        $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE == $propKind;
872
        $nuLink->isCollection = $isCollection;
873
        $value = $entryObject->results->$propName;
874
875
        $result = new QueryResult();
876
        $result->results = $value;
877
        $resultCount = $isCollection ? count($value) : 1;
878
879
        if (0 < $resultCount) {
880
            $newStackLine = ['type' => $nextName, 'prop' => $propName, 'count' => $resultCount];
881
            array_push($this->lightStack, $newStackLine);
882
            if (!$isCollection) {
883
                $expandedResult = $this->writeTopLevelElement($result);
884
            } else {
885
                $expandedResult = $this->writeTopLevelElements($result);
886
            }
887
            $nuLink->expandedResult = $expandedResult;
888
        }
889
        if (!isset($nuLink->expandedResult)) {
890
            $nuLink->isCollection = null;
891
            $nuLink->isExpanded = null;
892
        } else {
893
            if (isset($nuLink->expandedResult->selfLink)) {
894
                $nuLink->expandedResult->selfLink->title = $propName;
895
                $nuLink->expandedResult->selfLink->url = $nuLink->url;
896
                $nuLink->expandedResult->title = new ODataTitle($propName);
897
                $nuLink->expandedResult->id = rtrim($this->absoluteServiceUri, '/') . '/' . $nuLink->url;
898
            }
899
        }
900
    }
901
902
    /**
903
     * Gets the data service instance.
904
     *
905
     * @return void
906
     */
907
    public function setService(IService $service)
908
    {
909
        $this->service = $service;
910
        $this->absoluteServiceUri = $service->getHost()->getAbsoluteServiceUri()->getUrlAsString();
911
        $this->absoluteServiceUriWithSlash = rtrim($this->absoluteServiceUri, '/') . '/';
912
    }
913
914
    /**
915
     * @param ResourceType $resourceType
916
     * @param $result
917
     * @return ODataBagContent|null
918
     */
919
    protected function writeBagValue(ResourceType &$resourceType, $result)
920
    {
921
        assert(null == $result || is_array($result), 'Bag parameter must be null or array');
922
        $typeKind = $resourceType->getResourceTypeKind();
923
        assert(
924
            ResourceTypeKind::PRIMITIVE() == $typeKind || ResourceTypeKind::COMPLEX() == $typeKind,
925
            '$bagItemResourceTypeKind != ResourceTypeKind::PRIMITIVE'
926
            .' && $bagItemResourceTypeKind != ResourceTypeKind::COMPLEX'
927
        );
928
        if (null == $result) {
929
            return null;
930
        }
931
        $bag = new ODataBagContent();
932
        foreach ($result as $value) {
933
            if (isset($value)) {
934
                if (ResourceTypeKind::PRIMITIVE() == $typeKind) {
935
                    $instance = $resourceType->getInstanceType();
936
                    assert($instance instanceof IType, get_class($instance));
937
                    $bag->propertyContents[] = $this->primitiveToString($instance, $value);
938
                } elseif (ResourceTypeKind::COMPLEX() == $typeKind) {
939
                    $bag->propertyContents[] = $this->writeComplexValue($resourceType, $value);
940
                }
941
            }
942
        }
943
        return $bag;
944
    }
945
946
    /**
947
     * @param ResourceType  $resourceType
948
     * @param object        $result
949
     * @param string|null   $propertyName
950
     * @return ODataPropertyContent
951
     */
952
    protected function writeComplexValue(ResourceType &$resourceType, &$result, $propertyName = null)
953
    {
954
        assert(is_object($result), 'Supplied $customObject must be an object');
955
956
        $count = count($this->complexTypeInstanceCollection);
957
        for ($i = 0; $i < $count; ++$i) {
958
            if ($this->complexTypeInstanceCollection[$i] === $result) {
959
                throw new InvalidOperationException(
960
                    Messages::objectModelSerializerLoopsNotAllowedInComplexTypes($propertyName)
961
                );
962
            }
963
        }
964
965
        $this->complexTypeInstanceCollection[$count] = &$result;
966
967
        $internalContent = new ODataPropertyContent();
968
        $resourceProperties = $resourceType->getAllProperties();
969
        // first up, handle primitive properties
970
        foreach ($resourceProperties as $prop) {
971
            $resourceKind = $prop->getKind();
972
            $propName = $prop->getName();
973
            $internalProperty = new ODataProperty();
974
            $internalProperty->name = $propName;
975
            if (static::isMatchPrimitive($resourceKind)) {
976
                $iType = $prop->getInstanceType();
977
                assert($iType instanceof IType, get_class($iType));
978
                $internalProperty->typeName = $iType->getFullTypeName();
979
980
                $rType = $prop->getResourceType()->getInstanceType();
981
                assert($rType instanceof IType, get_class($rType));
982
                $internalProperty->value = $this->primitiveToString($rType, $result->$propName);
983
984
                $internalContent->properties[$propName] = $internalProperty;
985
            } elseif (ResourcePropertyKind::COMPLEX_TYPE == $resourceKind) {
986
                $rType = $prop->getResourceType();
987
                $internalProperty->typeName = $rType->getFullName();
988
                $internalProperty->value = $this->writeComplexValue($rType, $result->$propName, $propName);
989
990
                $internalContent->properties[$propName] = $internalProperty;
991
            }
992
        }
993
994
        unset($this->complexTypeInstanceCollection[$count]);
995
        return $internalContent;
996
    }
997
998
    public static function isMatchPrimitive($resourceKind)
999
    {
1000
        if (16 > $resourceKind) {
1001
            return false;
1002
        }
1003
        if (28 < $resourceKind) {
1004
            return false;
1005
        }
1006
        return 0 == ($resourceKind % 4);
1007
    }
1008
1009
    /*
1010
     * @return IMetadataProvider
1011
     */
1012
    protected function getMetadata()
1013
    {
1014
        if (null == $this->metaProvider) {
1015
            $this->metaProvider = App::make('metadata');
1016
        }
1017
        return $this->metaProvider;
1018
    }
1019
1020
    /**
1021
     * @return array
1022
     */
1023
    protected function getLightStack()
1024
    {
1025
        return $this->lightStack;
1026
    }
1027
1028
    /**
1029
     * @return ModelSerialiser
1030
     */
1031
    public function getModelSerialiser()
1032
    {
1033
        return $this->modelSerialiser;
1034
    }
1035
}
1036