parseExpandAndSelectClause()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 11
c 1
b 0
f 0
nc 1
nop 8
dl 0
loc 23
rs 9.9

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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