1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace POData\UriProcessor; |
4
|
|
|
|
5
|
|
|
use POData\Common\InvalidOperationException; |
6
|
|
|
use POData\IService; |
7
|
|
|
use POData\Providers\Metadata\ResourceProperty; |
8
|
|
|
use POData\Providers\Metadata\ResourcePropertyKind; |
9
|
|
|
use POData\Providers\Metadata\ResourceSetWrapper; |
10
|
|
|
use POData\Providers\Metadata\ResourceTypeKind; |
11
|
|
|
use POData\Providers\ProvidersWrapper; |
12
|
|
|
use POData\Providers\Query\QueryResult; |
13
|
|
|
use POData\Providers\Query\QueryType; |
14
|
|
|
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode; |
15
|
|
|
|
16
|
|
|
class RequestExpander |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* Description of the OData request that a client has submitted. |
20
|
|
|
* |
21
|
|
|
* @var RequestDescription |
22
|
|
|
*/ |
23
|
|
|
private $request; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Holds reference to the data service instance. |
27
|
|
|
* |
28
|
|
|
* @var IService |
29
|
|
|
*/ |
30
|
|
|
private $service; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Holds reference to the wrapper over IDSMP and IDSQP implementation. |
34
|
|
|
* |
35
|
|
|
* @var ProvidersWrapper |
36
|
|
|
*/ |
37
|
|
|
private $providers; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Holds reference to segment stack being processed. |
41
|
|
|
* |
42
|
|
|
* @var SegmentStack |
43
|
|
|
*/ |
44
|
|
|
private $stack; |
45
|
|
|
|
46
|
|
|
public function __construct(RequestDescription $request, IService $service, ProvidersWrapper $wrapper) |
47
|
|
|
{ |
48
|
|
|
$this->request = $request; |
49
|
|
|
$this->service = $service; |
50
|
|
|
$this->providers = $wrapper; |
51
|
|
|
$this->stack = new SegmentStack($request); |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Gets reference to the request submitted by client. |
56
|
|
|
* |
57
|
|
|
* @return RequestDescription |
58
|
|
|
*/ |
59
|
|
|
public function getRequest() |
60
|
|
|
{ |
61
|
|
|
return $this->request; |
62
|
|
|
} |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* Gets reference to the request submitted by client. |
66
|
|
|
* |
67
|
|
|
* @return ProvidersWrapper |
68
|
|
|
*/ |
69
|
|
|
public function getProviders() |
70
|
|
|
{ |
71
|
|
|
return $this->providers; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Gets the data service instance. |
76
|
|
|
* |
77
|
|
|
* @return IService |
78
|
|
|
*/ |
79
|
|
|
public function getService() |
80
|
|
|
{ |
81
|
|
|
return $this->service; |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Gets the segment stack instance. |
86
|
|
|
* |
87
|
|
|
* @return SegmentStack |
88
|
|
|
*/ |
89
|
|
|
public function getStack() |
90
|
|
|
{ |
91
|
|
|
return $this->stack; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Perform expansion. |
96
|
|
|
* |
97
|
|
|
* @return void |
98
|
|
|
*/ |
99
|
|
|
public function handleExpansion() |
100
|
|
|
{ |
101
|
|
|
$node = $this->getRequest()->getRootProjectionNode(); |
102
|
|
|
if (null !== $node && $node->isExpansionSpecified()) { |
103
|
|
|
$result = $this->getRequest()->getTargetResult(); |
104
|
|
|
if (null !== $result && (!is_array($result) || !empty($result))) { |
105
|
|
|
$needPop = $this->pushSegmentForRoot(); |
106
|
|
|
$this->executeExpansion($result); |
107
|
|
|
$this->popSegment(true === $needPop); |
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Execute queries for expansion. |
114
|
|
|
* |
115
|
|
|
* @param array|mixed $result Resource(s) whose navigation properties needs to be expanded |
116
|
|
|
*/ |
117
|
|
|
private function executeExpansion($result) |
118
|
|
|
{ |
119
|
|
|
$expandedProjectionNodes = $this->getExpandedProjectionNodes(); |
120
|
|
|
foreach ($expandedProjectionNodes as $expandedProjectionNode) { |
121
|
|
|
$resourceType = $expandedProjectionNode->getResourceType(); |
122
|
|
|
$isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE |
123
|
|
|
== $expandedProjectionNode->getResourceProperty()->getKind(); |
124
|
|
|
$expandedPropertyName = $expandedProjectionNode->getResourceProperty()->getName(); |
125
|
|
|
$originalIsArray = is_array($result); |
126
|
|
|
|
127
|
|
|
if (!$originalIsArray) { |
128
|
|
|
$result = [$result]; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
foreach ($result as $entry) { |
132
|
|
|
// Check for null entry |
133
|
|
|
if ($entry instanceof QueryResult && empty($entry->results)) { |
134
|
|
|
continue; |
135
|
|
|
} |
136
|
|
|
if ($isCollection) { |
137
|
|
|
$result1 = $this->executeCollectionExpansionGetRelated($expandedProjectionNode, $entry); |
138
|
|
|
if (!empty($result1)) { |
139
|
|
|
$this->executeCollectionExpansionProcessExpansion( |
140
|
|
|
$entry, |
141
|
|
|
$result1, |
142
|
|
|
$expandedProjectionNode, |
143
|
|
|
$resourceType, |
144
|
|
|
$expandedPropertyName |
145
|
|
|
); |
146
|
|
|
} else { |
147
|
|
|
$resultSet = $originalIsArray ? [] : $result1; |
148
|
|
|
$resourceType->setPropertyValue($entry, $expandedPropertyName, $resultSet); |
149
|
|
|
} |
150
|
|
|
} else { |
151
|
|
|
$this->executeSingleExpansionGetRelated( |
152
|
|
|
$expandedProjectionNode, |
153
|
|
|
$entry, |
154
|
|
|
$resourceType, |
155
|
|
|
$expandedPropertyName |
156
|
|
|
); |
157
|
|
|
} |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Resource set wrapper for the resource being retrieved. |
164
|
|
|
* |
165
|
|
|
* @return ResourceSetWrapper |
166
|
|
|
*/ |
167
|
|
View Code Duplication |
private function getCurrentResourceSetWrapper() |
|
|
|
|
168
|
|
|
{ |
169
|
|
|
$wraps = $this->getStack()->getSegmentWrappers(); |
170
|
|
|
$count = count($wraps); |
171
|
|
|
|
172
|
|
|
return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $wraps[$count - 1]; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* Pushes a segment for the root of the tree |
177
|
|
|
* Note: Calls to this method should be balanced with calls to popSegment. |
178
|
|
|
* |
179
|
|
|
* @return bool true if the segment was pushed, false otherwise |
180
|
|
|
*/ |
181
|
|
|
private function pushSegmentForRoot() |
182
|
|
|
{ |
183
|
|
|
$segmentName = $this->getRequest()->getContainerName(); |
184
|
|
|
$segmentResourceSetWrapper = $this->getRequest()->getTargetResourceSetWrapper(); |
185
|
|
|
|
186
|
|
|
return $this->pushSegment($segmentName, $segmentResourceSetWrapper); |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Pushes a segment for the current navigation property being written out. |
191
|
|
|
* Note: Refer 'ObjectModelSerializerNotes.txt' for more details about |
192
|
|
|
* 'Segment Stack' and this method. |
193
|
|
|
* Note: Calls to this method should be balanced with calls to popSegment. |
194
|
|
|
* |
195
|
|
|
* @param ResourceProperty &$resourceProperty Current navigation property |
196
|
|
|
* being written out |
197
|
|
|
* |
198
|
|
|
* @throws InvalidOperationException If this function invoked with non-navigation |
199
|
|
|
* property instance |
200
|
|
|
* |
201
|
|
|
* @return bool true if a segment was pushed, false otherwise |
202
|
|
|
*/ |
203
|
|
|
private function pushSegmentForNavigationProperty(ResourceProperty &$resourceProperty) |
204
|
|
|
{ |
205
|
|
|
if ($resourceProperty->getTypeKind() == ResourceTypeKind::ENTITY()) { |
206
|
|
|
assert(!empty($this->getStack()->getSegmentNames()), '!is_empty($this->getStack()->getSegmentNames())'); |
207
|
|
|
$currentResourceSetWrapper = $this->getCurrentResourceSetWrapper(); |
208
|
|
|
$currentResourceType = $currentResourceSetWrapper->getResourceType(); |
209
|
|
|
$currentResourceSetWrapper = $this->getService() |
210
|
|
|
->getProvidersWrapper() |
211
|
|
|
->getResourceSetWrapperForNavigationProperty( |
212
|
|
|
$currentResourceSetWrapper, |
213
|
|
|
$currentResourceType, |
214
|
|
|
$resourceProperty |
215
|
|
|
); |
216
|
|
|
|
217
|
|
|
assert(null !== $currentResourceSetWrapper, '!null($currentResourceSetWrapper)'); |
218
|
|
|
|
219
|
|
|
return $this->pushSegment( |
220
|
|
|
$resourceProperty->getName(), |
221
|
|
|
$currentResourceSetWrapper |
222
|
|
|
); |
223
|
|
|
} else { |
224
|
|
|
throw new InvalidOperationException( |
225
|
|
|
'pushSegmentForNavigationProperty should not be called with non-entity type' |
226
|
|
|
); |
227
|
|
|
} |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Gets collection of expanded projection nodes under the current node. |
232
|
|
|
* |
233
|
|
|
* @return ExpandedProjectionNode[] List of nodes describing expansions for the current segment |
234
|
|
|
*/ |
235
|
|
|
protected function getExpandedProjectionNodes() |
236
|
|
|
{ |
237
|
|
|
$expandedProjectionNode = $this->getCurrentExpandedProjectionNode(); |
238
|
|
|
$expandedProjectionNodes = []; |
239
|
|
|
if (null !== $expandedProjectionNode) { |
240
|
|
|
foreach ($expandedProjectionNode->getChildNodes() as $node) { |
241
|
|
|
if ($node instanceof ExpandedProjectionNode) { |
242
|
|
|
$expandedProjectionNodes[] = $node; |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
return $expandedProjectionNodes; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Find a 'ExpandedProjectionNode' instance in the projection tree |
252
|
|
|
* which describes the current segment. |
253
|
|
|
* |
254
|
|
|
* @return ExpandedProjectionNode|null |
255
|
|
|
*/ |
256
|
|
View Code Duplication |
private function getCurrentExpandedProjectionNode() |
|
|
|
|
257
|
|
|
{ |
258
|
|
|
$expandedProjectionNode = $this->getRequest()->getRootProjectionNode(); |
259
|
|
|
if (null !== $expandedProjectionNode) { |
260
|
|
|
$names = $this->getStack()->getSegmentNames(); |
261
|
|
|
$depth = count($names); |
262
|
|
|
if (0 != $depth) { |
263
|
|
|
for ($i = 1; $i < $depth; ++$i) { |
264
|
|
|
$expandedProjectionNode = $expandedProjectionNode->findNode($names[$i]); |
|
|
|
|
265
|
|
|
assert(null !== $expandedProjectionNode, '!is_null($expandedProjectionNode)'); |
266
|
|
|
assert( |
267
|
|
|
$expandedProjectionNode instanceof ExpandedProjectionNode, |
268
|
|
|
'$expandedProjectionNode instanceof ExpandedProjectionNode' |
269
|
|
|
); |
270
|
|
|
} |
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
return $expandedProjectionNode; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Pushes information about the segment whose instance is going to be |
279
|
|
|
* retrieved from the IDSQP implementation |
280
|
|
|
* Note: Calls to this method should be balanced with calls to popSegment. |
281
|
|
|
* |
282
|
|
|
* @param string $segmentName Name of segment to push |
283
|
|
|
* @param ResourceSetWrapper &$resourceSetWrapper The resource set wrapper |
284
|
|
|
* to push |
285
|
|
|
* |
286
|
|
|
* @return bool true if the segment was push, false otherwise |
287
|
|
|
*/ |
288
|
|
|
private function pushSegment($segmentName, ResourceSetWrapper &$resourceSetWrapper) |
289
|
|
|
{ |
290
|
|
|
return $this->getStack()->pushSegment($segmentName, $resourceSetWrapper); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Pops segment information from the 'Segment Stack' |
295
|
|
|
* Note: Calls to this method should be balanced with previous calls |
296
|
|
|
* to _pushSegment. |
297
|
|
|
* |
298
|
|
|
* @param bool $needPop Is a pop required. Only true if last push |
299
|
|
|
* was successful |
300
|
|
|
* |
301
|
|
|
* @throws InvalidOperationException If found un-balanced call |
302
|
|
|
* with _pushSegment |
303
|
|
|
*/ |
304
|
|
|
private function popSegment($needPop) |
305
|
|
|
{ |
306
|
|
|
$this->getStack()->popSegment($needPop); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* @param ExpandedProjectionNode $expandedProjectionNode |
311
|
|
|
* @param $entry |
312
|
|
|
* |
313
|
|
|
* @return object[]|null |
314
|
|
|
*/ |
315
|
|
|
private function executeCollectionExpansionGetRelated($expandedProjectionNode, $entry) |
316
|
|
|
{ |
317
|
|
|
$currentResourceSet = $this->getCurrentResourceSetWrapper()->getResourceSet(); |
318
|
|
|
$resourceSetOfProjectedProperty = $expandedProjectionNode |
319
|
|
|
->getResourceSetWrapper() |
320
|
|
|
->getResourceSet(); |
321
|
|
|
$projectedProperty = $expandedProjectionNode->getResourceProperty(); |
322
|
|
|
$result = $this->getProviders()->getRelatedResourceSet( |
323
|
|
|
QueryType::ENTITIES(), //it's always entities for an expansion |
324
|
|
|
$currentResourceSet, |
325
|
|
|
$entry, |
326
|
|
|
$resourceSetOfProjectedProperty, |
327
|
|
|
$projectedProperty, |
328
|
|
|
null, // $filter |
329
|
|
|
null, // $orderby |
330
|
|
|
null, // $top |
331
|
|
|
null // $skip |
332
|
|
|
)->results; |
333
|
|
|
|
334
|
|
|
return $result; |
|
|
|
|
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* @param ExpandedProjectionNode $expandedProjectionNode |
339
|
|
|
* @param $entry |
340
|
|
|
* @param \POData\Providers\Metadata\ResourceType $resourceType |
341
|
|
|
* @param string $expandedPropertyName |
342
|
|
|
* |
343
|
|
|
* @throws InvalidOperationException |
344
|
|
|
* @throws \POData\Common\ODataException |
345
|
|
|
*/ |
346
|
|
|
private function executeSingleExpansionGetRelated( |
347
|
|
|
$expandedProjectionNode, |
348
|
|
|
$entry, |
349
|
|
|
$resourceType, |
350
|
|
|
$expandedPropertyName |
351
|
|
|
) { |
352
|
|
|
$currentResourceSet = $this->getCurrentResourceSetWrapper()->getResourceSet(); |
353
|
|
|
$resourceSetOfProjectedProperty = $expandedProjectionNode |
354
|
|
|
->getResourceSetWrapper() |
355
|
|
|
->getResourceSet(); |
356
|
|
|
$projectedProperty = $expandedProjectionNode->getResourceProperty(); |
357
|
|
|
$result = $this->getProviders()->getRelatedResourceReference( |
358
|
|
|
$currentResourceSet, |
359
|
|
|
$entry, |
360
|
|
|
$resourceSetOfProjectedProperty, |
361
|
|
|
$projectedProperty |
362
|
|
|
); |
363
|
|
|
$resourceType->setPropertyValue($entry, $expandedPropertyName, $result); |
364
|
|
|
if (null !== $result) { |
365
|
|
|
$this->pushPropertyToNavigation($result, $expandedProjectionNode); |
366
|
|
|
} |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* @param $entry |
371
|
|
|
* @param $result |
372
|
|
|
* @param ExpandedProjectionNode $expandedProjectionNode |
373
|
|
|
* @param \POData\Providers\Metadata\ResourceType $resourceType |
374
|
|
|
* @param string $expandedPropertyName |
375
|
|
|
* |
376
|
|
|
* @throws InvalidOperationException |
377
|
|
|
*/ |
378
|
|
|
private function executeCollectionExpansionProcessExpansion( |
379
|
|
|
$entry, |
380
|
|
|
$result, |
381
|
|
|
$expandedProjectionNode, |
382
|
|
|
$resourceType, |
383
|
|
|
$expandedPropertyName |
384
|
|
|
) { |
385
|
|
|
$internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo(); |
386
|
|
|
if (null !== $internalOrderByInfo) { |
387
|
|
|
$orderByFunction = $internalOrderByInfo->getSorterFunction(); |
388
|
|
|
usort($result, $orderByFunction); |
389
|
|
|
unset($internalOrderByInfo); |
390
|
|
|
$takeCount = $expandedProjectionNode->getTakeCount(); |
391
|
|
|
if (null !== $takeCount) { |
392
|
|
|
$result = array_slice($result, 0, $takeCount); |
393
|
|
|
} |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
$resourceType->setPropertyValue($entry, $expandedPropertyName, $result); |
397
|
|
|
$this->pushPropertyToNavigation($result, $expandedProjectionNode); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
/** |
401
|
|
|
* @param $result |
402
|
|
|
* @param ExpandedProjectionNode $expandedProjectionNode |
403
|
|
|
* |
404
|
|
|
* @throws InvalidOperationException |
405
|
|
|
*/ |
406
|
|
|
private function pushPropertyToNavigation($result, $expandedProjectionNode) |
407
|
|
|
{ |
408
|
|
|
$projectedProperty = $expandedProjectionNode->getResourceProperty(); |
409
|
|
|
$needPop = $this->pushSegmentForNavigationProperty($projectedProperty); |
410
|
|
|
$this->executeExpansion($result); |
411
|
|
|
$this->popSegment(true === $needPop); |
412
|
|
|
} |
413
|
|
|
} |
414
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.