applySelectionToProjectionTree()   C
last analyzed

Complexity

Conditions 17
Paths 11

Size

Total Lines 88
Code Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 57
nc 11
nop 1
dl 0
loc 88
rs 5.2166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace POData\UriProcessor\QueryProcessor\ExpandProjectionParser;
6
7
use POData\Common\InvalidOperationException;
8
use POData\Common\Messages;
9
use POData\Common\ODataException;
10
use POData\Providers\Metadata\ResourceAssociationSet;
11
use POData\Providers\Metadata\ResourceEntityType;
12
use POData\Providers\Metadata\ResourceProperty;
13
use POData\Providers\Metadata\ResourcePropertyKind;
14
use POData\Providers\Metadata\ResourceSetWrapper;
15
use POData\Providers\Metadata\ResourceType;
16
use POData\Providers\Metadata\ResourceTypeKind;
17
use POData\Providers\ProvidersWrapper;
18
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionLexer;
19
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionTokenId;
20
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
21
use POData\UriProcessor\QueryProcessor\OrderByParser\OrderByParser;
22
use ReflectionException;
23
24
/**
25
 * Class ExpandProjectionParser.
26
 *
27
 * Class used to parse and validate $expand and $select query options and
28
 * create a 'Projection Tree' from these options, Syntax of the clause is:
29
 *
30
 * ExpandOrSelectPath : PathSegment [, PathSegment]
31
 * PathSegment        : SubPathSegment [\ SubPathSegment]
32
 * SubPathSegment     : DottedIdentifier
33
 * SubPathSegment     : * (Only if the SubPathSegment is last segment and
34
 *                      belongs to select path)
35
 * DottedIdentifier   : Identifier [. Identifier]
36
 * Identifier         : NavigationProperty
37
 * Identifier         : NonNavigationProperty (Only if if the SubPathSegment
38
 *                      is last segment and belongs to select path)
39
 */
40
class ExpandProjectionParser
41
{
42
    /**
43
     * The wrapper of IMetadataProvider and IQueryProvider
44
     * .
45
     *
46
     * @var ProvidersWrapper
47
     */
48
    private $providerWrapper;
49
50
    /**
51
     * Holds reference to the root of 'Projection Tree'.
52
     *
53
     * @var RootProjectionNode
54
     */
55
    private $rootProjectionNode;
56
57
    /**
58
     * Creates new instance of ExpandProjectionParser.
59
     *
60
     * @param ProvidersWrapper $providerWrapper Reference to metadata and query provider wrapper
61
     */
62
    private function __construct(ProvidersWrapper $providerWrapper)
63
    {
64
        $this->providerWrapper = $providerWrapper;
65
    }
66
67
    /**
68
     * Parse the given expand and select clause, validate them
69
     * and build 'Projection Tree'.
70
     *
71
     * @param ResourceSetWrapper  $resourceSetWrapper The resource set identified by the resource path uri
72
     * @param ResourceType        $resourceType       The resource type of entities identified by the resource path uri
73
     * @param InternalOrderByInfo $internalOrderInfo  The top level sort information, this will be set if the $skip,
74
     *                                                $top is specified in the
75
     *                                                request uri or Server side paging is
76
     *                                                enabled for top level resource
77
     * @param int                 $skipCount          The value of $skip option applied to the top level resource
78
     *                                                set identified by the
79
     *                                                resource path uri
80
     *                                                null means $skip
81
     *                                                option is not present
82
     * @param int                 $takeCount          The minimum among the value of $top option applied to and
83
     *                                                page size configured
84
     *                                                for the top level
85
     *                                                resource
86
     *                                                set identified
87
     *                                                by the resource
88
     *                                                path uri.
89
     *                                                null means $top option
90
     *                                                is not present and/or
91
     *                                                page size is not
92
     *                                                configured for top
93
     *                                                level resource set
94
     * @param string              $expand             The value of $expand clause
95
     * @param string              $select             The value of $select clause
96
     * @param ProvidersWrapper    $providerWrapper    Reference to metadata and query provider wrapper
97
     *
98
     * @throws ODataException            If any error occur while parsing expand and/or select clause
99
     * @throws InvalidOperationException
100
     * @throws ReflectionException
101
     * @return RootProjectionNode        Returns root of the 'Projection Tree'
102
     */
103
    public static function parseExpandAndSelectClause(
104
        ResourceSetWrapper $resourceSetWrapper,
105
        ResourceType $resourceType,
106
        ?InternalOrderByInfo $internalOrderInfo,
107
        ?int $skipCount,
108
        ?int $takeCount,
109
        ?string $expand,
110
        ?string $select,
111
        ProvidersWrapper $providerWrapper
112
    ): RootProjectionNode {
113
        $parser                     = new self($providerWrapper);
114
        $parser->rootProjectionNode = new RootProjectionNode(
115
            $resourceSetWrapper,
116
            $internalOrderInfo,
117
            $skipCount,
118
            $takeCount,
119
            null,
120
            $resourceType
121
        );
122
        $parser->parseExpand($expand);
123
        $parser->parseSelect($select);
124
125
        return $parser->rootProjectionNode;
126
    }
127
128
    /**
129
     * Read the given expand clause and build 'Projection Tree',
130
     * do nothing if the clause is null.
131
     *
132
     * @param string $expand Value of $expand clause
133
     *
134
     * @throws ODataException            If any error occurs while reading expand clause
135
     *                                   or building the projection tree
136
     * @throws InvalidOperationException
137
     * @throws ReflectionException
138
     */
139
    private function parseExpand(?string $expand): void
140
    {
141
        if (null !== $expand) {
142
            $pathSegments = $this->readExpandOrSelect($expand, false);
143
            $this->buildProjectionTree($pathSegments);
144
            $this->rootProjectionNode->setExpansionSpecified();
145
        }
146
    }
147
148
    /**
149
     * Read expand or select clause.
150
     *
151
     * @param string $value    expand or select clause to read
152
     * @param bool   $isSelect true means $value is value of select clause
153
     *                         else value of expand clause
154
     *
155
     * @throws ODataException
156
     * @return array<array>   An array of 'PathSegment's, each of which is array of 'SubPathSegment's
157
     */
158
    private function readExpandOrSelect(string $value, bool $isSelect): array
159
    {
160
        $pathSegments = [];
161
        $lexer        = new ExpressionLexer($value);
162
        $i            = 0;
163
        while ($lexer->getCurrentToken()->getId() != ExpressionTokenId::END()) {
164
            $lastSegment = false;
165
            if ($isSelect
166
                && $lexer->getCurrentToken()->getId() == ExpressionTokenId::STAR()
167
            ) {
168
                $lastSegment    = true;
169
                $subPathSegment = $lexer->getCurrentToken()->Text;
170
                $lexer->nextToken();
171
            } else {
172
                $subPathSegment = $lexer->readDottedIdentifier();
173
            }
174
175
            if (!array_key_exists($i, $pathSegments)) {
176
                $pathSegments[$i] = [];
177
            }
178
179
            $pathSegments[$i][] = $subPathSegment;
180
            $tokenId            = $lexer->getCurrentToken()->getId();
181
            if ($tokenId != ExpressionTokenId::END()) {
182
                if ($lastSegment || $tokenId != ExpressionTokenId::SLASH()) {
183
                    $lexer->validateToken(ExpressionTokenId::COMMA());
184
                    ++$i;
185
                }
186
187
                $lexer->nextToken();
188
            }
189
        }
190
191
        return $pathSegments;
192
    }
193
194
    /**
195
     * Build 'Projection Tree' from the given expand path segments.
196
     *
197
     * @param array<array> $expandPathSegments Collection of expand paths
198
     *
199
     * @throws ODataException            If any error occurs while processing the expand path segments
200
     * @throws InvalidOperationException
201
     * @throws ReflectionException
202
     */
203
    private function buildProjectionTree(array $expandPathSegments): void
204
    {
205
        foreach ($expandPathSegments as $expandSubPathSegments) {
206
            $currentNode = $this->rootProjectionNode;
207
            foreach ($expandSubPathSegments as $expandSubPathSegment) {
208
                $resourceSetWrapper = $currentNode->getResourceSetWrapper();
209
                $resourceType       = $currentNode->getResourceType();
210
                assert($resourceType instanceof ResourceEntityType);
211
                $resourceProperty = $resourceType->resolveProperty($expandSubPathSegment);
212
                assert($resourceProperty instanceof ResourceProperty);
213
                $keyType      = ResourceAssociationSet::keyNameFromTypeAndProperty($resourceType, $resourceProperty);
214
                $assoc        = $this->getProviderWrapper()->getMetaProvider()->resolveAssociationSet($keyType);
0 ignored issues
show
Bug introduced by
The method resolveAssociationSet() does not exist on POData\Providers\Metadata\IMetadataProvider. Since it exists in all sub-types, consider adding an abstract or default implementation to POData\Providers\Metadata\IMetadataProvider. ( Ignorable by Annotation )

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

214
                $assoc        = $this->getProviderWrapper()->getMetaProvider()->/** @scrutinizer ignore-call */ resolveAssociationSet($keyType);
Loading history...
215
                $concreteType = isset($assoc) ? $assoc->getEnd2()->getConcreteType() : $resourceType;
216
                if (null === $resourceProperty) {
217
                    throw ODataException::createSyntaxError(
218
                        Messages::expandProjectionParserPropertyNotFound(
219
                            $resourceType->getFullName(),
220
                            $expandSubPathSegment,
221
                            false
222
                        )
223
                    );
224
                } elseif ($resourceProperty->getTypeKind() != ResourceTypeKind::ENTITY()) {
225
                    throw ODataException::createBadRequestError(
226
                        Messages::expandProjectionParserExpandCanOnlyAppliedToEntity(
227
                            $resourceType->getFullName(),
228
                            $expandSubPathSegment
229
                        )
230
                    );
231
                }
232
                assert($resourceType instanceof ResourceEntityType);
233
234
                $resourceSetWrapper = $this->providerWrapper
235
                    ->getResourceSetWrapperForNavigationProperty(
236
                        $resourceSetWrapper,
237
                        $resourceType,
238
                        $resourceProperty
239
                    );
240
241
                if (null === $resourceSetWrapper) {
242
                    throw ODataException::createBadRequestError(
243
                        Messages::badRequestInvalidPropertyNameSpecified(
244
                            $resourceType->getFullName(),
245
                            $expandSubPathSegment
246
                        )
247
                    );
248
                }
249
250
                $singleResult = $resourceProperty->isKindOf(ResourcePropertyKind::RESOURCE_REFERENCE());
251
                $resourceSetWrapper->checkResourceSetRightsForRead($singleResult);
252
                $pageSize            = $resourceSetWrapper->getResourceSetPageSize();
253
                $internalOrderByInfo = null;
254
                if ($pageSize != 0 && !$singleResult) {
255
                    $this->rootProjectionNode->setPagedExpandedResult(true);
256
                    $rt          = $resourceSetWrapper->getResourceType();
257
                    $payloadType = $rt->isAbstract() ? $concreteType : $rt;
258
259
                    $keys    = array_keys($rt->getKeyProperties());
260
                    $orderBy = null;
261
                    foreach ($keys as $key) {
262
                        $orderBy = $orderBy . $key . ', ';
263
                    }
264
265
                    $orderBy             = rtrim($orderBy, ', ');
0 ignored issues
show
Bug introduced by
It seems like $orderBy can also be of type null; however, parameter $string of rtrim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

265
                    $orderBy             = rtrim(/** @scrutinizer ignore-type */ $orderBy, ', ');
Loading history...
266
                    $internalOrderByInfo = OrderByParser::parseOrderByClause(
267
                        $resourceSetWrapper,
268
                        $payloadType,
269
                        $orderBy,
270
                        $this->providerWrapper
271
                    );
272
                }
273
274
                $node = $currentNode->findNode($expandSubPathSegment);
275
                if (null === $node) {
276
                    $maxResultCount = $this->providerWrapper
277
                        ->getConfiguration()->getMaxResultsPerCollection();
278
                    $node = new ExpandedProjectionNode(
279
                        $expandSubPathSegment,
280
                        $resourceSetWrapper,
281
                        $internalOrderByInfo,
282
                        null,
283
                        $pageSize == 0 ? null : $pageSize,
284
                        $maxResultCount == PHP_INT_MAX ? null : $maxResultCount,
285
                        $resourceProperty
286
                    );
287
                    $currentNode->addNode($node);
288
                }
289
290
                $currentNode = $node;
291
            }
292
        }
293
    }
294
295
    /**
296
     * @return ProvidersWrapper
297
     */
298
    public function getProviderWrapper(): ProvidersWrapper
299
    {
300
        return $this->providerWrapper;
301
    }
302
303
    /**
304
     * Read the given select clause and apply selection to the
305
     * 'Projection Tree', mark the entire tree as selected if this
306
     * clause is null
307
     * Note: _parseExpand should to be called before the invocation
308
     * of this function so that basic 'Projection Tree' with expand
309
     * information will be ready.
310
     *
311
     * @param string $select Value of $select clause
312
     *
313
     * @throws ODataException If any error occurs while reading expand clause
314
     *                        or applying selection to projection tree
315
     */
316
    private function parseSelect(?string $select): void
317
    {
318
        if (null === $select) {
319
            $this->rootProjectionNode->markSubtreeAsSelected();
320
        } else {
321
            $pathSegments = $this->readExpandOrSelect($select, true);
322
            $this->applySelectionToProjectionTree($pathSegments);
323
            $this->rootProjectionNode->setSelectionSpecified();
324
            $this->rootProjectionNode->removeNonSelectedNodes();
325
            $this->rootProjectionNode->removeNodesAlreadyIncludedImplicitly();
326
            //TODO: Move sort to parseExpandAndSelectClause function
327
            $this->rootProjectionNode->sortNodes();
328
        }
329
    }
330
331
    /**
332
     * Modify the 'Projection Tree' to include selection details.
333
     *
334
     * @param array<array<string>> $selectPathSegments Collection of select
335
     *                                                 paths
336
     *
337
     * @throws ODataException If any error occurs while processing select
338
     *                        path segments
339
     */
340
    private function applySelectionToProjectionTree(array $selectPathSegments): void
341
    {
342
        foreach ($selectPathSegments as $selectSubPathSegments) {
343
            $currentNode  = $this->rootProjectionNode;
344
            $subPathCount = count($selectSubPathSegments);
345
            foreach ($selectSubPathSegments as $index => $selectSubPathSegment) {
346
                if (!($currentNode instanceof RootProjectionNode)
347
                    && !($currentNode instanceof ExpandedProjectionNode)
348
                ) {
349
                    throw ODataException::createBadRequestError(
350
                        Messages::expandProjectionParserPropertyWithoutMatchingExpand(
351
                            $currentNode->getPropertyName()
352
                        )
353
                    );
354
                }
355
356
                $currentNode->setSelectionFound();
357
                $isLastSegment = ($index == $subPathCount - 1);
358
                if ($selectSubPathSegment === '*') {
359
                    $currentNode->setSelectAllImmediateProperties();
360
                    break;
361
                }
362
363
                $currentResourceType = $currentNode->getResourceType();
364
                $resourceProperty
365
                    = $currentResourceType->resolveProperty(
366
                        $selectSubPathSegment
367
                    );
368
                if (null === $resourceProperty) {
369
                    throw ODataException::createSyntaxError(
370
                        Messages::expandProjectionParserPropertyNotFound(
371
                            $currentResourceType->getFullName(),
372
                            $selectSubPathSegment,
373
                            true
374
                        )
375
                    );
376
                }
377
378
                if (!$isLastSegment) {
379
                    if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG())) {
380
                        throw ODataException::createBadRequestError(
381
                            Messages::expandProjectionParserBagPropertyAsInnerSelectSegment(
382
                                $currentResourceType->getFullName(),
383
                                $selectSubPathSegment
384
                            )
385
                        );
386
                    } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE())) {
387
                        throw ODataException::createBadRequestError(
388
                            Messages::expandProjectionParserPrimitivePropertyUsedAsNavigationProperty(
389
                                $currentResourceType->getFullName(),
390
                                $selectSubPathSegment
391
                            )
392
                        );
393
                    } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE())) {
394
                        throw ODataException::createBadRequestError(
395
                            Messages::expandProjectionParserComplexPropertyAsInnerSelectSegment(
396
                                $currentResourceType->getFullName(),
397
                                $selectSubPathSegment
398
                            )
399
                        );
400
                    } elseif ($resourceProperty->getKind() != ResourcePropertyKind::RESOURCE_REFERENCE()
401
                        && $resourceProperty->getKind() != ResourcePropertyKind::RESOURCESET_REFERENCE()) {
402
                        throw ODataException::createInternalServerError(
403
                            Messages::expandProjectionParserUnexpectedPropertyType()
404
                        );
405
                    }
406
                }
407
408
                $node = $currentNode->findNode($selectSubPathSegment);
409
                if (null === $node) {
410
                    if (!$isLastSegment) {
411
                        throw ODataException::createBadRequestError(
412
                            Messages::expandProjectionParserPropertyWithoutMatchingExpand(
413
                                $selectSubPathSegment
414
                            )
415
                        );
416
                    }
417
418
                    $node = new ProjectionNode($selectSubPathSegment, $resourceProperty);
419
                    $currentNode->addNode($node);
420
                }
421
422
                $currentNode = $node;
423
                if ($currentNode instanceof ExpandedProjectionNode
424
                    && $isLastSegment
425
                ) {
426
                    $currentNode->setSelectionFound();
427
                    $currentNode->markSubtreeAsSelected();
428
                }
429
            }
430
        }
431
    }
432
}
433