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