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

parseExpandAndSelectClause()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

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
namespace POData\UriProcessor\QueryProcessor\ExpandProjectionParser;
4
5
use POData\Common\Messages;
6
use POData\Common\ODataException;
7
use POData\Providers\Metadata\ResourceAssociationSet;
8
use POData\Providers\Metadata\ResourceEntityType;
9
use POData\Providers\Metadata\ResourceProperty;
10
use POData\Providers\Metadata\ResourcePropertyKind;
11
use POData\Providers\Metadata\ResourceSet;
12
use POData\Providers\Metadata\ResourceSetWrapper;
13
use POData\Providers\Metadata\ResourceType;
14
use POData\Providers\Metadata\ResourceTypeKind;
15
use POData\Providers\ProvidersWrapper;
16
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionLexer;
17
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionTokenId;
18
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
19
use POData\UriProcessor\QueryProcessor\OrderByParser\OrderByParser;
20
21
/**
22
 * Class ExpandProjectionParser.
23
 *
24
 * Class used to parse and validate $expand and $select query options and
25
 * create a 'Projection Tree' from these options, Syntax of the clause is:
26
 *
27
 * ExpandOrSelectPath : PathSegment [, PathSegment]
28
 * PathSegment        : SubPathSegment [\ SubPathSegment]
29
 * SubPathSegment     : DottedIdentifier
30
 * SubPathSegment     : * (Only if the SubPathSegment is last segment and
31
 *                      belongs to select path)
32
 * DottedIdentifier   : Identifier [. Identifier]
33
 * Identifier         : NavigationProperty
34
 * Identifier         : NonNavigationProperty (Only if if the SubPathSegment
35
 *                      is last segment and belongs to select path)
36
 */
37
class ExpandProjectionParser
38
{
39
    /**
40
     * The wrapper of IMetadataProvider and IQueryProvider
41
     * .
42
     *
43
     * @var ProvidersWrapper
44
     */
45
    private $providerWrapper;
46
47
    /**
48
     * Holds reference to the root of 'Projection Tree'.
49
     *
50
     * @var RootProjectionNode
51
     */
52
    private $rootProjectionNode;
53
54
    /**
55
     * Creates new instance of ExpandProjectionParser.
56
     *
57
     * @param ProvidersWrapper $providerWrapper Reference to metadata and query provider wrapper
58
     */
59
    private function __construct(ProvidersWrapper $providerWrapper)
60
    {
61
        $this->providerWrapper = $providerWrapper;
62
    }
63
64
    /**
65
     * Parse the given expand and select clause, validate them
66
     * and build 'Projection Tree'.
67
     *
68
     * @param ResourceSetWrapper  $resourceSetWrapper The resource set identified by the resource path uri
69
     * @param ResourceType        $resourceType       The resource type of entities identified by the resource path uri
70
     * @param InternalOrderByInfo $internalOrderInfo  The top level sort information, this will be set if the $skip,
71
     *                                                $top is specified in the
72
     *                                                request uri or Server side paging is
73
     *                                                enabled for top level resource
74
     * @param int                 $skipCount          The value of $skip option applied to the top level resource
75
     *                                                set identified by the
76
     *                                                resource path uri
77
     *                                                null means $skip
78
     *                                                option is not present
79
     * @param int                 $takeCount          The minimum among the value of $top option applied to and
80
     *                                                page size configured
81
     *                                                for the top level
82
     *                                                resource
83
     *                                                set identified
84
     *                                                by the resource
85
     *                                                path uri.
86
     *                                                null means $top option
87
     *                                                is not present and/or
88
     *                                                page size is not
89
     *                                                configured for top
90
     *                                                level resource set
91
     * @param string              $expand             The value of $expand clause
92
     * @param string              $select             The value of $select clause
93
     * @param ProvidersWrapper    $providerWrapper    Reference to metadata and query provider wrapper
94
     *
95
     * @throws ODataException If any error occur while parsing expand and/or select clause
96
     *
97
     * @return RootProjectionNode Returns root of the 'Projection Tree'
98
     */
99
    public static function parseExpandAndSelectClause(
100
        ResourceSetWrapper $resourceSetWrapper,
101
        ResourceType $resourceType,
102
        $internalOrderInfo,
103
        $skipCount,
104
        $takeCount,
105
        $expand,
106
        $select,
107
        ProvidersWrapper $providerWrapper
108
    ) {
109
        $parser = new self($providerWrapper);
110
        $parser->rootProjectionNode = new RootProjectionNode(
111
            $resourceSetWrapper,
112
            $internalOrderInfo,
113
            $skipCount,
114
            $takeCount,
115
            null,
116
            $resourceType
117
        );
118
        $parser->parseExpand($expand);
119
        $parser->parseSelect($select);
120
121
        return $parser->rootProjectionNode;
122
    }
123
124
    /**
125
     * Read the given expand clause and build 'Projection Tree',
126
     * do nothing if the clause is null.
127
     *
128
     * @param string $expand Value of $expand clause
129
     *
130
     * @throws ODataException If any error occurs while reading expand clause
131
     *                        or building the projection tree
132
     */
133
    private function parseExpand($expand)
134
    {
135
        if (null !== $expand) {
136
            $pathSegments = $this->readExpandOrSelect($expand, false);
137
            $this->buildProjectionTree($pathSegments);
138
            $this->rootProjectionNode->setExpansionSpecified();
139
        }
140
    }
141
142
    /**
143
     * Read the given select clause and apply selection to the
144
     * 'Projection Tree', mark the entire tree as selected if this
145
     * clause is null
146
     * Note: _parseExpand should to be called before the invocation
147
     * of this function so that basic 'Projection Tree' with expand
148
     * information will be ready.
149
     *
150
     * @param string $select Value of $select clause
151
     *
152
     * @throws ODataException If any error occurs while reading expand clause
153
     *                        or applying selection to projection tree
154
     */
155
    private function parseSelect($select)
156
    {
157
        if (null === $select) {
158
            $this->rootProjectionNode->markSubtreeAsSelected();
159
        } else {
160
            $pathSegments = $this->readExpandOrSelect($select, true);
161
            $this->applySelectionToProjectionTree($pathSegments);
162
            $this->rootProjectionNode->setSelectionSpecified();
163
            $this->rootProjectionNode->removeNonSelectedNodes();
164
            $this->rootProjectionNode->removeNodesAlreadyIncludedImplicitly();
165
            //TODO: Move sort to parseExpandAndSelectClause function
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
166
            $this->rootProjectionNode->sortNodes();
167
        }
168
    }
169
170
    /**
171
     * Build 'Projection Tree' from the given expand path segments.
172
     *
173
     * @param array<array> $expandPathSegments Collection of expand paths
174
     *
175
     * @throws ODataException If any error occurs while processing the expand path segments
176
     */
177
    private function buildProjectionTree($expandPathSegments)
178
    {
179
        foreach ($expandPathSegments as $expandSubPathSegments) {
180
            $currentNode = $this->rootProjectionNode;
181
            foreach ($expandSubPathSegments as $expandSubPathSegment) {
182
                $resourceSetWrapper = $currentNode->getResourceSetWrapper();
0 ignored issues
show
Bug introduced by
The method getResourceSetWrapper() does not exist on POData\UriProcessor\Quer...onParser\ProjectionNode. Did you maybe mean getResourceProperty()? ( Ignorable by Annotation )

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

182
                /** @scrutinizer ignore-call */ 
183
                $resourceSetWrapper = $currentNode->getResourceSetWrapper();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
183
                $resourceType = $currentNode->getResourceType();
0 ignored issues
show
Bug introduced by
The method getResourceType() does not exist on POData\UriProcessor\Quer...onParser\ProjectionNode. Did you maybe mean getResourceProperty()? ( Ignorable by Annotation )

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

183
                /** @scrutinizer ignore-call */ 
184
                $resourceType = $currentNode->getResourceType();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
184
                assert($resourceType instanceof ResourceEntityType);
0 ignored issues
show
Bug introduced by
The call to assert() has too few arguments starting with description. ( Ignorable by Annotation )

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

184
                /** @scrutinizer ignore-call */ 
185
                assert($resourceType instanceof ResourceEntityType);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
185
                $resourceProperty = $resourceType->resolveProperty($expandSubPathSegment);
186
                assert($resourceProperty instanceof ResourceProperty);
187
                $keyType = ResourceAssociationSet::keyNameFromTypeAndProperty($resourceType, $resourceProperty);
188
                $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

188
                $assoc = $this->getProviderWrapper()->getMetaProvider()->/** @scrutinizer ignore-call */ resolveAssociationSet($keyType);
Loading history...
189
                $concreteType = isset($assoc) ? $assoc->getEnd2()->getConcreteType() : $resourceType;
190
                if (null === $resourceProperty) {
191
                    throw ODataException::createSyntaxError(
192
                        Messages::expandProjectionParserPropertyNotFound(
193
                            $resourceType->getFullName(),
194
                            $expandSubPathSegment,
195
                            false
196
                        )
197
                    );
198
                } elseif ($resourceProperty->getTypeKind() != ResourceTypeKind::ENTITY()) {
199
                    throw ODataException::createBadRequestError(
200
                        Messages::expandProjectionParserExpandCanOnlyAppliedToEntity(
201
                            $resourceType->getFullName(),
202
                            $expandSubPathSegment
203
                        )
204
                    );
205
                }
206
                assert($resourceType instanceof ResourceEntityType);
207
208
                $resourceSetWrapper = $this->providerWrapper
209
                    ->getResourceSetWrapperForNavigationProperty(
210
                        $resourceSetWrapper,
211
                        $resourceType,
212
                        $resourceProperty
213
                    );
214
215
                if (null === $resourceSetWrapper) {
216
                    throw ODataException::createBadRequestError(
217
                        Messages::badRequestInvalidPropertyNameSpecified(
218
                            $resourceType->getFullName(),
219
                            $expandSubPathSegment
220
                        )
221
                    );
222
                }
223
224
                $singleResult
225
                    = $resourceProperty->isKindOf(
226
                        ResourcePropertyKind::RESOURCE_REFERENCE
0 ignored issues
show
Bug introduced by
POData\Providers\Metadat...ind::RESOURCE_REFERENCE of type integer is incompatible with the type POData\Providers\Metadata\ResourcePropertyKind expected by parameter $kind of POData\Providers\Metadat...rceProperty::isKindOf(). ( Ignorable by Annotation )

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

226
                        /** @scrutinizer ignore-type */ ResourcePropertyKind::RESOURCE_REFERENCE
Loading history...
227
                    );
228
                $resourceSetWrapper->checkResourceSetRightsForRead($singleResult);
229
                $pageSize = $resourceSetWrapper->getResourceSetPageSize();
230
                $internalOrderByInfo = null;
231
                if ($pageSize != 0 && !$singleResult) {
232
                    $this->rootProjectionNode->setPagedExpandedResult(true);
233
                    $rt = $resourceSetWrapper->getResourceType();
234
                    $payloadType = $rt->isAbstract() ? $concreteType : $rt;
235
236
                    $keys = array_keys($rt->getKeyProperties());
237
                    $orderBy = null;
238
                    foreach ($keys as $key) {
239
                        $orderBy = $orderBy . $key . ', ';
240
                    }
241
242
                    $orderBy = rtrim($orderBy, ', ');
243
                    $internalOrderByInfo = OrderByParser::parseOrderByClause(
244
                        $resourceSetWrapper,
245
                        $payloadType,
246
                        $orderBy,
247
                        $this->providerWrapper
248
                    );
249
                }
250
251
                $node = $currentNode->findNode($expandSubPathSegment);
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

251
                /** @scrutinizer ignore-call */ 
252
                $node = $currentNode->findNode($expandSubPathSegment);
Loading history...
252
                if (null === $node) {
253
                    $maxResultCount = $this->providerWrapper
254
                        ->getConfiguration()->getMaxResultsPerCollection();
255
                    $node = new ExpandedProjectionNode(
256
                        $expandSubPathSegment,
257
                        $resourceSetWrapper,
258
                        $internalOrderByInfo,
259
                        null,
260
                        $pageSize == 0 ? null : $pageSize,
261
                        $maxResultCount == PHP_INT_MAX ? null : $maxResultCount,
262
                        $resourceProperty
263
                    );
264
                    $currentNode->addNode($node);
0 ignored issues
show
introduced by
The method addNode() 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
                    $currentNode->/** @scrutinizer ignore-call */ 
265
                                  addNode($node);
Loading history...
265
                }
266
267
                $currentNode = $node;
268
            }
269
        }
270
    }
271
272
    /**
273
     * Modify the 'Projection Tree' to include selection details.
274
     *
275
     * @param array<array<string>> $selectPathSegments Collection of select
276
     *                                                 paths
277
     *
278
     * @throws ODataException If any error occurs while processing select
279
     *                        path segments
280
     */
281
    private function applySelectionToProjectionTree($selectPathSegments)
282
    {
283
        foreach ($selectPathSegments as $selectSubPathSegments) {
284
            $currentNode = $this->rootProjectionNode;
285
            $subPathCount = count($selectSubPathSegments);
286
            foreach ($selectSubPathSegments as $index => $selectSubPathSegment) {
287
                if (!($currentNode instanceof RootProjectionNode)
288
                    && !($currentNode instanceof ExpandedProjectionNode)
289
                ) {
290
                    throw ODataException::createBadRequestError(
291
                        Messages::expandProjectionParserPropertyWithoutMatchingExpand(
292
                            $currentNode->getPropertyName()
293
                        )
294
                    );
295
                }
296
297
                $currentNode->setSelectionFound();
298
                $isLastSegment = ($index == $subPathCount - 1);
299
                if ($selectSubPathSegment === '*') {
300
                    $currentNode->setSelectAllImmediateProperties();
301
                    break;
302
                }
303
304
                $currentResourceType = $currentNode->getResourceType();
305
                $resourceProperty
306
                    = $currentResourceType->resolveProperty(
307
                        $selectSubPathSegment
308
                    );
309
                if (null === $resourceProperty) {
310
                    throw ODataException::createSyntaxError(
311
                        Messages::expandProjectionParserPropertyNotFound(
312
                            $currentResourceType->getFullName(),
313
                            $selectSubPathSegment,
314
                            true
315
                        )
316
                    );
317
                }
318
319
                if (!$isLastSegment) {
320
                    if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG)) {
0 ignored issues
show
Bug introduced by
POData\Providers\Metadat...sourcePropertyKind::BAG of type integer is incompatible with the type POData\Providers\Metadata\ResourcePropertyKind expected by parameter $kind of POData\Providers\Metadat...rceProperty::isKindOf(). ( Ignorable by Annotation )

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

320
                    if ($resourceProperty->isKindOf(/** @scrutinizer ignore-type */ ResourcePropertyKind::BAG)) {
Loading history...
321
                        throw ODataException::createBadRequestError(
322
                            Messages::expandProjectionParserBagPropertyAsInnerSelectSegment(
323
                                $currentResourceType->getFullName(),
324
                                $selectSubPathSegment
325
                            )
326
                        );
327
                    } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) {
328
                        throw ODataException::createBadRequestError(
329
                            Messages::expandProjectionParserPrimitivePropertyUsedAsNavigationProperty(
330
                                $currentResourceType->getFullName(),
331
                                $selectSubPathSegment
332
                            )
333
                        );
334
                    } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE)) {
335
                        throw ODataException::createBadRequestError(
336
                            Messages::expandProjectionParserComplexPropertyAsInnerSelectSegment(
337
                                $currentResourceType->getFullName(),
338
                                $selectSubPathSegment
339
                            )
340
                        );
341
                    } elseif ($resourceProperty->getKind() != ResourcePropertyKind::RESOURCE_REFERENCE
342
                              && $resourceProperty->getKind() != ResourcePropertyKind::RESOURCESET_REFERENCE) {
343
                        throw ODataException::createInternalServerError(
344
                            Messages::expandProjectionParserUnexpectedPropertyType()
345
                        );
346
                    }
347
                }
348
349
                $node = $currentNode->findNode($selectSubPathSegment);
350
                if (null === $node) {
351
                    if (!$isLastSegment) {
352
                        throw ODataException::createBadRequestError(
353
                            Messages::expandProjectionParserPropertyWithoutMatchingExpand(
354
                                $selectSubPathSegment
355
                            )
356
                        );
357
                    }
358
359
                    $node = new ProjectionNode($selectSubPathSegment, $resourceProperty);
360
                    $currentNode->addNode($node);
361
                }
362
363
                $currentNode = $node;
364
                if ($currentNode instanceof ExpandedProjectionNode
365
                    && $isLastSegment
366
                ) {
367
                    $currentNode->setSelectionFound();
368
                    $currentNode->markSubtreeAsSelected();
369
                }
370
            }
371
        }
372
    }
373
374
    /**
375
     * Read expand or select clause.
376
     *
377
     * @param string $value    expand or select clause to read
378
     * @param bool   $isSelect true means $value is value of select clause
379
     *                         else value of expand clause
380
     *
381
     * @return array<array> An array of 'PathSegment's, each of which is array of 'SubPathSegment's
382
     */
383
    private function readExpandOrSelect($value, $isSelect)
384
    {
385
        $pathSegments = [];
386
        $lexer = new ExpressionLexer($value);
387
        $i = 0;
388
        while ($lexer->getCurrentToken()->Id != ExpressionTokenId::END) {
389
            $lastSegment = false;
390
            if ($isSelect
391
                && $lexer->getCurrentToken()->Id == ExpressionTokenId::STAR
392
            ) {
393
                $lastSegment = true;
394
                $subPathSegment = $lexer->getCurrentToken()->Text;
395
                $lexer->nextToken();
396
            } else {
397
                $subPathSegment = $lexer->readDottedIdentifier();
398
            }
399
400
            if (!array_key_exists($i, $pathSegments)) {
401
                $pathSegments[$i] = [];
402
            }
403
404
            $pathSegments[$i][] = $subPathSegment;
405
            $tokenId = $lexer->getCurrentToken()->Id;
406
            if ($tokenId != ExpressionTokenId::END) {
407
                if ($lastSegment || $tokenId != ExpressionTokenId::SLASH) {
408
                    $lexer->validateToken(ExpressionTokenId::COMMA);
0 ignored issues
show
Bug introduced by
POData\UriProcessor\Quer...xpressionTokenId::COMMA of type integer is incompatible with the type POData\UriProcessor\Quer...arser\ExpressionTokenId expected by parameter $tokenId of POData\UriProcessor\Quer...nLexer::validateToken(). ( Ignorable by Annotation )

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

408
                    $lexer->validateToken(/** @scrutinizer ignore-type */ ExpressionTokenId::COMMA);
Loading history...
409
                    ++$i;
410
                }
411
412
                $lexer->nextToken();
413
            }
414
        }
415
416
        return $pathSegments;
417
    }
418
419
    /**
420
     * @return ProvidersWrapper
421
     */
422
    public function getProviderWrapper()
423
    {
424
        return $this->providerWrapper;
425
    }
426
}
427