Passed
Push — master ( 0669ef...efaea1 )
by Alex
04:43 queued 01:07
created

RequestExpander::getRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
        if ($result instanceof QueryResult) {
120
            $result = $result->results;
121
        }
122
123
        $originalIsArray = is_array($result);
124
125
        if (!$originalIsArray) {
126
            $result = [$result];
127
        }
128
129
        $expandedProjectionNodes = $this->getExpandedProjectionNodes();
130
        foreach ($expandedProjectionNodes as $expandedProjectionNode) {
131
            $resourceType = $expandedProjectionNode->getResourceType();
132
            $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE
133
                            == $expandedProjectionNode->getResourceProperty()->getKind();
134
            $expandedPropertyName = $expandedProjectionNode->getResourceProperty()->getName();
135
136
            foreach ($result as $entry) {
137
                if ($isCollection) {
138
                    $result1 = $this->executeCollectionExpansionGetRelated($expandedProjectionNode, $entry);
139
                    if (!empty($result1)) {
140
                        $this->executeCollectionExpansionProcessExpansion(
141
                            $entry,
142
                            $result1,
143
                            $expandedProjectionNode,
144
                            $resourceType,
145
                            $expandedPropertyName
146
                        );
147
                    } else {
148
                        $resultSet = $originalIsArray ? [] : $result1;
149
                        $resourceType->setPropertyValue($entry, $expandedPropertyName, $resultSet);
150
                    }
151
                } else {
152
                    $this->executeSingleExpansionGetRelated(
153
                        $expandedProjectionNode,
154
                        $entry,
155
                        $resourceType,
156
                        $expandedPropertyName
157
                    );
158
                }
159
            }
160
        }
161
    }
162
163
    /**
164
     * Resource set wrapper for the resource being retrieved.
165
     *
166
     * @return ResourceSetWrapper
167
     */
168
    private function getCurrentResourceSetWrapper()
169
    {
170
        $wraps = $this->getStack()->getSegmentWrappers();
171
        $count = count($wraps);
172
173
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $wraps[$count - 1];
174
    }
175
176
    /**
177
     * Pushes a segment for the root of the tree
178
     * Note: Calls to this method should be balanced with calls to popSegment.
179
     *
180
     * @return bool true if the segment was pushed, false otherwise
181
     */
182
    private function pushSegmentForRoot()
183
    {
184
        $segmentName = $this->getRequest()->getContainerName();
185
        $segmentResourceSetWrapper = $this->getRequest()->getTargetResourceSetWrapper();
186
187
        return $this->pushSegment($segmentName, $segmentResourceSetWrapper);
188
    }
189
190
    /**
191
     * Pushes a segment for the current navigation property being written out.
192
     * Note: Refer 'ObjectModelSerializerNotes.txt' for more details about
193
     * 'Segment Stack' and this method.
194
     * Note: Calls to this method should be balanced with calls to popSegment.
195
     *
196
     * @param ResourceProperty &$resourceProperty Current navigation property
197
     *                                            being written out
198
     *
199
     * @throws InvalidOperationException If this function invoked with non-navigation
200
     *                                   property instance
201
     *
202
     * @return bool true if a segment was pushed, false otherwise
203
     */
204
    private function pushSegmentForNavigationProperty(ResourceProperty &$resourceProperty)
205
    {
206
        if ($resourceProperty->getTypeKind() == ResourceTypeKind::ENTITY()) {
207
            assert(!empty($this->getStack()->getSegmentNames()), '!is_empty($this->getStack()->getSegmentNames())');
208
            $currentResourceSetWrapper = $this->getCurrentResourceSetWrapper();
209
            $currentResourceType = $currentResourceSetWrapper->getResourceType();
210
            $currentResourceSetWrapper = $this->getService()
211
                ->getProvidersWrapper()
212
                ->getResourceSetWrapperForNavigationProperty(
213
                    $currentResourceSetWrapper,
214
                    $currentResourceType,
215
                    $resourceProperty
216
                );
217
218
            assert(null !== $currentResourceSetWrapper, '!null($currentResourceSetWrapper)');
219
220
            return $this->pushSegment(
221
                $resourceProperty->getName(),
222
                $currentResourceSetWrapper
223
            );
224
        } else {
225
            throw new InvalidOperationException(
226
                'pushSegmentForNavigationProperty should not be called with non-entity type'
227
            );
228
        }
229
    }
230
231
    /**
232
     * Gets collection of expanded projection nodes under the current node.
233
     *
234
     * @return ExpandedProjectionNode[] List of nodes describing expansions for the current segment
235
     */
236
    protected function getExpandedProjectionNodes()
237
    {
238
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
239
        $expandedProjectionNodes = [];
240
        if (null !== $expandedProjectionNode) {
241
            foreach ($expandedProjectionNode->getChildNodes() as $node) {
242
                if ($node instanceof ExpandedProjectionNode) {
243
                    $expandedProjectionNodes[] = $node;
244
                }
245
            }
246
        }
247
248
        return $expandedProjectionNodes;
249
    }
250
251
    /**
252
     * Find a 'ExpandedProjectionNode' instance in the projection tree
253
     * which describes the current segment.
254
     *
255
     * @return ExpandedProjectionNode|null
256
     */
257
    private function getCurrentExpandedProjectionNode()
258
    {
259
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
260
        if (null !== $expandedProjectionNode) {
261
            $names = $this->getStack()->getSegmentNames();
262
            $depth = count($names);
263
            if (0 != $depth) {
264
                for ($i = 1; $i < $depth; ++$i) {
265
                    $expandedProjectionNode = $expandedProjectionNode->findNode($names[$i]);
0 ignored issues
show
introduced by
The method findNode() does not exist on POData\UriProcessor\Quer...onParser\ProjectionNode. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

265
                    /** @scrutinizer ignore-call */ 
266
                    $expandedProjectionNode = $expandedProjectionNode->findNode($names[$i]);
Loading history...
266
                    assert(null !== $expandedProjectionNode, '!is_null($expandedProjectionNode)');
267
                    assert(
268
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
269
                        '$expandedProjectionNode instanceof ExpandedProjectionNode'
270
                    );
271
                }
272
            }
273
        }
274
275
        return $expandedProjectionNode;
276
    }
277
278
    /**
279
     * Pushes information about the segment whose instance is going to be
280
     * retrieved from the IDSQP implementation
281
     * Note: Calls to this method should be balanced with calls to popSegment.
282
     *
283
     * @param string             $segmentName         Name of segment to push
284
     * @param ResourceSetWrapper &$resourceSetWrapper The resource set wrapper
285
     *                                                to push
286
     *
287
     * @return bool true if the segment was push, false otherwise
288
     */
289
    private function pushSegment($segmentName, ResourceSetWrapper &$resourceSetWrapper)
290
    {
291
        return $this->getStack()->pushSegment($segmentName, $resourceSetWrapper);
292
    }
293
294
    /**
295
     * Pops segment information from the 'Segment Stack'
296
     * Note: Calls to this method should be balanced with previous calls
297
     * to _pushSegment.
298
     *
299
     * @param bool $needPop Is a pop required. Only true if last push
300
     *                      was successful
301
     *
302
     * @throws InvalidOperationException If found un-balanced call
303
     *                                   with _pushSegment
304
     */
305
    private function popSegment($needPop)
306
    {
307
        $this->getStack()->popSegment($needPop);
308
    }
309
310
    /**
311
     * @param ExpandedProjectionNode $expandedProjectionNode
312
     * @param $entry
313
     *
314
     * @return object[]|null
315
     */
316
    private function executeCollectionExpansionGetRelated($expandedProjectionNode, $entry)
317
    {
318
        $currentResourceSet = $this->getCurrentResourceSetWrapper()->getResourceSet();
319
        $resourceSetOfProjectedProperty = $expandedProjectionNode
320
            ->getResourceSetWrapper()
321
            ->getResourceSet();
322
        $projectedProperty = $expandedProjectionNode->getResourceProperty();
323
        $result = $this->getProviders()->getRelatedResourceSet(
324
            QueryType::ENTITIES(), //it's always entities for an expansion
325
            $currentResourceSet,
326
            $entry,
327
            $resourceSetOfProjectedProperty,
328
            $projectedProperty,
329
            null, // $filter
330
            null, // $orderby
331
            null, // $top
332
            null  // $skip
333
        )->results;
334
335
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type object which is incompatible with the documented return type array<mixed,object>|null.
Loading history...
336
    }
337
338
    /**
339
     * @param ExpandedProjectionNode $expandedProjectionNode
340
     * @param $entry
341
     * @param \POData\Providers\Metadata\ResourceType $resourceType
342
     * @param string                                  $expandedPropertyName
343
     *
344
     * @throws InvalidOperationException
345
     * @throws \POData\Common\ODataException
346
     */
347
    private function executeSingleExpansionGetRelated(
348
        $expandedProjectionNode,
349
        $entry,
350
        $resourceType,
351
        $expandedPropertyName
352
    ) {
353
        $currentResourceSet = $this->getCurrentResourceSetWrapper()->getResourceSet();
354
        $resourceSetOfProjectedProperty = $expandedProjectionNode
355
            ->getResourceSetWrapper()
356
            ->getResourceSet();
357
        $projectedProperty = $expandedProjectionNode->getResourceProperty();
358
        $result = $this->getProviders()->getRelatedResourceReference(
359
            $currentResourceSet,
360
            $entry,
361
            $resourceSetOfProjectedProperty,
362
            $projectedProperty
363
        );
364
        $resourceType->setPropertyValue($entry, $expandedPropertyName, $result);
365
        if (null !== $result) {
366
            $this->pushPropertyToNavigation($result, $expandedProjectionNode);
367
        }
368
    }
369
370
    /**
371
     * @param $entry
372
     * @param $result
373
     * @param ExpandedProjectionNode                  $expandedProjectionNode
374
     * @param \POData\Providers\Metadata\ResourceType $resourceType
375
     * @param string                                  $expandedPropertyName
376
     *
377
     * @throws InvalidOperationException
378
     */
379
    private function executeCollectionExpansionProcessExpansion(
380
        $entry,
381
        $result,
382
        $expandedProjectionNode,
383
        $resourceType,
384
        $expandedPropertyName
385
    ) {
386
        $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
387
        if (null !== $internalOrderByInfo) {
388
            $orderByFunction = $internalOrderByInfo->getSorterFunction();
389
            usort($result, $orderByFunction);
390
            unset($internalOrderByInfo);
391
            $takeCount = $expandedProjectionNode->getTakeCount();
392
            if (null !== $takeCount) {
393
                $result = array_slice($result, 0, $takeCount);
394
            }
395
        }
396
397
        $resourceType->setPropertyValue($entry, $expandedPropertyName, $result);
398
        $this->pushPropertyToNavigation($result, $expandedProjectionNode);
399
    }
400
401
    /**
402
     * @param $result
403
     * @param ExpandedProjectionNode $expandedProjectionNode
404
     *
405
     * @throws InvalidOperationException
406
     */
407
    private function pushPropertyToNavigation($result, $expandedProjectionNode)
408
    {
409
        $projectedProperty = $expandedProjectionNode->getResourceProperty();
410
        $needPop = $this->pushSegmentForNavigationProperty($projectedProperty);
411
        $this->executeExpansion($result);
412
        $this->popSegment(true === $needPop);
413
    }
414
}
415