Passed
Branch master (950424)
by Christopher
11:06
created

RequestExpander::popSegment()   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 1
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
        $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()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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]);
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

264
                    /** @scrutinizer ignore-call */ 
265
                    $expandedProjectionNode = $expandedProjectionNode->findNode($names[$i]);
Loading history...
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;
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...
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