Completed
Push — master ( f97b98...ddac18 )
by Alex
01:00
created

RequestExpander::executeExpansion()   C

Complexity

Conditions 7
Paths 11

Size

Total Lines 41
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 29
nc 11
nop 1
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\QueryType;
13
use POData\UriProcessor\QueryProcessor\ExpandProjectionParser\ExpandedProjectionNode;
14
15
class RequestExpander
16
{
17
    /**
18
     * Description of the OData request that a client has submitted.
19
     *
20
     * @var RequestDescription
21
     */
22
    private $request;
23
24
    /**
25
     * Holds reference to the data service instance.
26
     *
27
     * @var IService
28
     */
29
    private $service;
30
31
    /**
32
     * Holds reference to the wrapper over IDSMP and IDSQP implementation.
33
     *
34
     * @var ProvidersWrapper
35
     */
36
    private $providers;
37
38
    /**
39
     * Holds reference to segment stack being processed
40
     *
41
     * @var SegmentStack
42
     */
43
    private $stack;
44
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
    public function handleExpansion()
98
    {
99
        $node = $this->getRequest()->getRootProjectionNode();
100
        if (!is_null($node) && $node->isExpansionSpecified()) {
101
            $result = $this->getRequest()->getTargetResult();
102
            if (!is_null($result) && (!is_array($result) || !empty($result))) {
103
                $needPop = $this->pushSegmentForRoot();
104
                $this->executeExpansion($result);
105
                $this->popSegment(true === $needPop);
106
            }
107
        }
108
    }
109
110
    /**
111
     * Execute queries for expansion.
112
     *
113
     * @param array|mixed $result Resource(s) whose navigation properties needs to be expanded
114
     */
115
    private function executeExpansion($result)
116
    {
117
        $expandedProjectionNodes = $this->getExpandedProjectionNodes();
118
        foreach ($expandedProjectionNodes as $expandedProjectionNode) {
119
            $resourceType = $expandedProjectionNode->getResourceType();
120
            $isCollection = ResourcePropertyKind::RESOURCESET_REFERENCE
121
                            == $expandedProjectionNode->getResourceProperty()->getKind();
122
            $expandedPropertyName = $expandedProjectionNode->getResourceProperty()->getName();
123
            $originalIsArray = is_array($result);
124
125
            if (!$originalIsArray) {
126
                $result = [$result];
127
            }
128
129
            foreach ($result as $entry) {
130
                // Check for null entry
131
                if ($isCollection) {
132
                    $result1 = $this->executeCollectionExpansionGetRelated($expandedProjectionNode, $entry);
133
                    if (!empty($result1)) {
134
                        $this->executeCollectionExpansionProcessExpansion(
135
                            $entry,
136
                            $result1,
137
                            $expandedProjectionNode,
138
                            $resourceType,
139
                            $expandedPropertyName
140
                        );
141
                    } else {
142
                        $resultSet = $originalIsArray ? array() : $result1;
143
                        $resourceType->setPropertyValue($entry, $expandedPropertyName, $resultSet);
144
                    }
145
                } else {
146
                    $this->executeSingleExpansionGetRelated(
147
                        $expandedProjectionNode,
148
                        $entry,
149
                        $resourceType,
150
                        $expandedPropertyName
151
                    );
152
                }
153
            }
154
        }
155
    }
156
157
    /**
158
     * Resource set wrapper for the resource being retrieved.
159
     *
160
     * @return ResourceSetWrapper
161
     */
162
    private function getCurrentResourceSetWrapper()
163
    {
164
        $wraps = $this->getStack()->getSegmentWrappers();
165
        $count = count($wraps);
166
        return 0 == $count ? $this->getRequest()->getTargetResourceSetWrapper() : $wraps[$count - 1];
167
    }
168
169
    /**
170
     * Pushes a segment for the root of the tree
171
     * Note: Calls to this method should be balanced with calls to popSegment.
172
     *
173
     * @return bool true if the segment was pushed, false otherwise
174
     */
175
    private function pushSegmentForRoot()
176
    {
177
        $segmentName = $this->getRequest()->getContainerName();
178
        $segmentResourceSetWrapper = $this->getRequest()->getTargetResourceSetWrapper();
179
180
        return $this->pushSegment($segmentName, $segmentResourceSetWrapper);
0 ignored issues
show
Bug introduced by
It seems like $segmentResourceSetWrapper defined by $this->getRequest()->get...getResourceSetWrapper() on line 178 can be null; however, POData\UriProcessor\RequestExpander::pushSegment() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
181
    }
182
183
    /**
184
     * Pushes a segment for the current navigation property being written out.
185
     * Note: Refer 'ObjectModelSerializerNotes.txt' for more details about
186
     * 'Segment Stack' and this method.
187
     * Note: Calls to this method should be balanced with calls to popSegment.
188
     *
189
     * @param ResourceProperty &$resourceProperty Current navigation property
190
     *                                            being written out
191
     *
192
     * @return bool true if a segment was pushed, false otherwise
193
     *
194
     * @throws InvalidOperationException If this function invoked with non-navigation
195
     *                                   property instance
196
     */
197
    private function pushSegmentForNavigationProperty(ResourceProperty &$resourceProperty)
198
    {
199
        if ($resourceProperty->getTypeKind() == ResourceTypeKind::ENTITY) {
200
            assert(!empty($this->getStack()->getSegmentNames()), '!is_empty($this->getStack()->getSegmentNames())');
201
            $currentResourceSetWrapper = $this->getCurrentResourceSetWrapper();
202
            $currentResourceType = $currentResourceSetWrapper->getResourceType();
203
            $currentResourceSetWrapper = $this->getService()
204
                ->getProvidersWrapper()
205
                ->getResourceSetWrapperForNavigationProperty(
206
                    $currentResourceSetWrapper,
207
                    $currentResourceType,
208
                    $resourceProperty
209
                );
210
211
            assert(!is_null($currentResourceSetWrapper), '!null($currentResourceSetWrapper)');
212
213
            return $this->pushSegment(
214
                $resourceProperty->getName(),
215
                $currentResourceSetWrapper
216
            );
217
        } else {
218
            throw new InvalidOperationException(
219
                'pushSegmentForNavigationProperty should not be called with non-entity type'
220
            );
221
        }
222
    }
223
224
    /**
225
     * Gets collection of expanded projection nodes under the current node.
226
     *
227
     * @return ExpandedProjectionNode[] List of nodes describing expansions for the current segment
228
     */
229
    protected function getExpandedProjectionNodes()
230
    {
231
        $expandedProjectionNode = $this->getCurrentExpandedProjectionNode();
232
        $expandedProjectionNodes = array();
233
        if (!is_null($expandedProjectionNode)) {
234
            foreach ($expandedProjectionNode->getChildNodes() as $node) {
235
                if ($node instanceof ExpandedProjectionNode) {
236
                    $expandedProjectionNodes[] = $node;
237
                }
238
            }
239
        }
240
241
        return $expandedProjectionNodes;
242
    }
243
244
    /**
245
     * Find a 'ExpandedProjectionNode' instance in the projection tree
246
     * which describes the current segment.
247
     *
248
     * @return ExpandedProjectionNode|null
249
     */
250
    private function getCurrentExpandedProjectionNode()
251
    {
252
        $expandedProjectionNode = $this->getRequest()->getRootProjectionNode();
253
        if (!is_null($expandedProjectionNode)) {
254
            $names = $this->getStack()->getSegmentNames();
255
            $depth = count($names);
256
            if (0 != $depth) {
257
                for ($i = 1; $i < $depth; ++$i) {
258
                    $expandedProjectionNode = $expandedProjectionNode->findNode($names[$i]);
259
                    assert(!is_null($expandedProjectionNode), '!is_null($expandedProjectionNode)');
260
                    assert(
261
                        $expandedProjectionNode instanceof ExpandedProjectionNode,
262
                        '$expandedProjectionNode instanceof ExpandedProjectionNode'
263
                    );
264
                }
265
            }
266
        }
267
268
        return $expandedProjectionNode;
269
    }
270
271
    /**
272
     * Pushes information about the segment whose instance is going to be
273
     * retrieved from the IDSQP implementation
274
     * Note: Calls to this method should be balanced with calls to popSegment.
275
     *
276
     * @param string             $segmentName         Name of segment to push
277
     * @param ResourceSetWrapper &$resourceSetWrapper The resource set wrapper
278
     *                                                to push
279
     *
280
     * @return bool true if the segment was push, false otherwise
281
     */
282
    private function pushSegment($segmentName, ResourceSetWrapper &$resourceSetWrapper)
283
    {
284
        $this->getStack()->pushSegment($segmentName, $resourceSetWrapper);
285
    }
286
287
    /**
288
     * Pops segment information from the 'Segment Stack'
289
     * Note: Calls to this method should be balanced with previous calls
290
     * to _pushSegment.
291
     *
292
     * @param bool $needPop Is a pop required. Only true if last push
293
     *                      was successful
294
     *
295
     * @throws InvalidOperationException If found un-balanced call
296
     *                                   with _pushSegment
297
     */
298
    private function popSegment($needPop)
299
    {
300
        $this->getStack()->popSegment($needPop);
301
    }
302
303
    /**
304
     * @param $expandedProjectionNode
305
     * @param $entry
306
     * @return null|\object[]
307
     */
308
    private function executeCollectionExpansionGetRelated($expandedProjectionNode, $entry)
309
    {
310
        $currentResourceSet = $this->getCurrentResourceSetWrapper()->getResourceSet();
311
        $resourceSetOfProjectedProperty = $expandedProjectionNode
312
            ->getResourceSetWrapper()
313
            ->getResourceSet();
314
        $projectedProperty = $expandedProjectionNode->getResourceProperty();
315
        $result = $this->getProviders()->getRelatedResourceSet(
316
            QueryType::ENTITIES(), //it's always entities for an expansion
317
            $currentResourceSet,
318
            $entry,
319
            $resourceSetOfProjectedProperty,
320
            $projectedProperty,
321
            null, // $filter
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a object<POData\UriProcess...ssionParser\FilterInfo>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
322
            null, // $orderby
323
            null, // $top
324
            null  // $skip
325
        )->results;
326
        return $result;
327
    }
328
329
    /**
330
     * @param $expandedProjectionNode
331
     * @param $entry
332
     * @param $resourceType
333
     * @param $expandedPropertyName
334
     * @throws InvalidOperationException
335
     * @throws \POData\Common\ODataException
336
     */
337
    private function executeSingleExpansionGetRelated(
338
        $expandedProjectionNode,
339
        $entry,
340
        $resourceType,
341
        $expandedPropertyName
342
    ) {
343
        $currentResourceSet = $this->getCurrentResourceSetWrapper()->getResourceSet();
344
        $resourceSetOfProjectedProperty = $expandedProjectionNode
345
            ->getResourceSetWrapper()
346
            ->getResourceSet();
347
        $projectedProperty = $expandedProjectionNode->getResourceProperty();
348
        $result = $this->getProviders()->getRelatedResourceReference(
349
            $currentResourceSet,
350
            $entry,
351
            $resourceSetOfProjectedProperty,
352
            $projectedProperty
353
        );
354
        $resourceType->setPropertyValue($entry, $expandedPropertyName, $result);
355
        if (!is_null($result)) {
356
            $this->pushPropertyToNavigation($result, $expandedProjectionNode);
357
        }
358
    }
359
360
    /**
361
     * @param $entry
362
     * @param $result
363
     * @param $expandedProjectionNode
364
     * @param $resourceType
365
     * @param $expandedPropertyName
366
     * @throws InvalidOperationException
367
     */
368
    private function executeCollectionExpansionProcessExpansion(
369
        $entry,
370
        $result,
371
        $expandedProjectionNode,
372
        $resourceType,
373
        $expandedPropertyName
374
    ) {
375
        $internalOrderByInfo = $expandedProjectionNode->getInternalOrderByInfo();
376
        // TODO: Get someome who ain't me to review this
377
        if (!is_null($internalOrderByInfo)) {
378
            $orderByFunction = $internalOrderByInfo->getSorterFunction()->getReference();
379
            usort($result, $orderByFunction);
380
            unset($internalOrderByInfo);
381
            $takeCount = $expandedProjectionNode->getTakeCount();
382
            if (!is_null($takeCount)) {
383
                $result = array_slice($result, 0, $takeCount);
384
            }
385
        }
386
387
        $resourceType->setPropertyValue($entry, $expandedPropertyName, $result);
388
        $this->pushPropertyToNavigation($result, $expandedProjectionNode);
389
    }
390
391
    /**
392
     * @param $result
393
     * @param $expandedProjectionNode
394
     * @throws InvalidOperationException
395
     */
396
    private function pushPropertyToNavigation($result, $expandedProjectionNode)
397
    {
398
        $projectedProperty = $expandedProjectionNode->getResourceProperty();
399
        $needPop = $this->pushSegmentForNavigationProperty($projectedProperty);
400
        $this->executeExpansion($result);
401
        $this->popSegment(true === $needPop);
402
    }
403
}
404