Passed
Pull Request — master (#120)
by Alex
03:44
created

ExpandProjectionParser   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 380
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 13
dl 0
loc 380
rs 8.3157
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B parseExpandAndSelectClause() 0 24 1
A parseExpand() 0 8 2
A parseSelect() 0 14 2
C buildProjectionTree() 0 91 12
D applySelectionToProjectionTree() 0 91 17
C readExpandOrSelect() 0 35 8

How to fix   Complexity   

Complex Class

Complex classes like ExpandProjectionParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ExpandProjectionParser, and based on these observations, apply Extract Interface, too.

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\ResourcePropertyKind;
8
use POData\Providers\Metadata\ResourceSet;
9
use POData\Providers\Metadata\ResourceSetWrapper;
10
use POData\Providers\Metadata\ResourceType;
11
use POData\Providers\Metadata\ResourceTypeKind;
12
use POData\Providers\ProvidersWrapper;
13
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionLexer;
14
use POData\UriProcessor\QueryProcessor\ExpressionParser\ExpressionTokenId;
15
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
16
use POData\UriProcessor\QueryProcessor\OrderByParser\OrderByParser;
17
18
/**
19
 * Class ExpandProjectionParser.
20
 *
21
 * Class used to parse and validate $expand and $select query options and
22
 * create a 'Projection Tree' from these options, Syntax of the clause is:
23
 *
24
 * ExpandOrSelectPath : PathSegment [, PathSegment]
25
 * PathSegment        : SubPathSegment [\ SubPathSegment]
26
 * SubPathSegment     : DottedIdentifier
27
 * SubPathSegment     : * (Only if the SubPathSegment is last segment and
28
 *                      belongs to select path)
29
 * DottedIdentifier   : Identifier [. Identifier]
30
 * Identifier         : NavigationProperty
31
 * Identifier         : NonNavigationProperty (Only if if the SubPathSegment
32
 *                      is last segment and belongs to select path)
33
 */
34
class ExpandProjectionParser
0 ignored issues
show
Coding Style introduced by
Since you have declared the constructor as private, maybe you should also declare the class as final.
Loading history...
35
{
36
    /**
37
     * The wrapper of IMetadataProvider and IQueryProvider
38
     * .
39
     *
40
     * @var ProvidersWrapper
41
     */
42
    private $providerWrapper;
43
44
    /**
45
     * Holds reference to the root of 'Projection Tree'.
46
     *
47
     * @var RootProjectionNode
48
     */
49
    private $rootProjectionNode;
50
51
    /**
52
     * Creates new instance of ExpandProjectionParser.
53
     *
54
     * @param ProvidersWrapper $providerWrapper Reference to metadata and query provider wrapper
55
     */
56
    private function __construct(ProvidersWrapper $providerWrapper)
57
    {
58
        $this->providerWrapper = $providerWrapper;
59
    }
60
61
    /**
62
     * Parse the given expand and select clause, validate them
63
     * and build 'Projection Tree'.
64
     *
65
     * @param ResourceSetWrapper  $resourceSetWrapper The resource set identified by the resource path uri
66
     * @param ResourceType        $resourceType       The resource type of entities identified by the resource path uri
67
     * @param InternalOrderByInfo $internalOrderInfo  The top level sort information, this will be set if the $skip, $top is
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 124 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
68
     *                                                specified in the
69
     *                                                request uri or Server
70
     *                                                side paging is
71
     *                                                enabled for top level
72
     *                                                resource
73
     * @param int                 $skipCount          The value of $skip option applied to the top level resource
74
     *                                                set identified by the
75
     *                                                resource path uri
76
     *                                                null means $skip
77
     *                                                option is not present
78
     * @param int                 $takeCount          The minimum among the value of $top option applied to and
79
     *                                                page size configured
80
     *                                                for the top level
81
     *                                                resource
82
     *                                                set identified
83
     *                                                by the resource
84
     *                                                path uri.
85
     *                                                null means $top option
86
     *                                                is not present and/or
87
     *                                                page size is not
88
     *                                                configured for top
89
     *                                                level resource set
90
     * @param string              $expand             The value of $expand clause
91
     * @param string              $select             The value of $select clause
92
     * @param ProvidersWrapper    $providerWrapper    Reference to metadata and query provider wrapper
93
     *
94
     * @throws ODataException If any error occur while parsing expand and/or select clause
95
     *
96
     * @return RootProjectionNode Returns root of the 'Projection Tree'
97
     */
98
    public static function parseExpandAndSelectClause(
99
        ResourceSetWrapper $resourceSetWrapper,
100
        ResourceType $resourceType,
101
        $internalOrderInfo,
102
        $skipCount,
103
        $takeCount,
104
        $expand,
105
        $select,
106
        ProvidersWrapper $providerWrapper
107
    ) {
108
        $parser = new self($providerWrapper);
109
        $parser->rootProjectionNode = new RootProjectionNode(
110
            $resourceSetWrapper,
111
            $internalOrderInfo,
112
            $skipCount,
113
            $takeCount,
114
            null,
115
            $resourceType
116
        );
117
        $parser->parseExpand($expand);
118
        $parser->parseSelect($select);
119
120
        return $parser->rootProjectionNode;
121
    }
122
123
    /**
124
     * Read the given expand clause and build 'Projection Tree',
125
     * do nothing if the clause is null.
126
     *
127
     * @param string $expand Value of $expand clause
128
     *
129
     * @throws ODataException If any error occurs while reading expand clause
130
     *                        or building the projection tree
131
     */
132
    private function parseExpand($expand)
133
    {
134
        if (!is_null($expand)) {
135
            $pathSegments = $this->readExpandOrSelect($expand, false);
136
            $this->buildProjectionTree($pathSegments);
137
            $this->rootProjectionNode->setExpansionSpecified();
138
        }
139
    }
140
141
    /**
142
     * Read the given select clause and apply selection to the
143
     * 'Projection Tree', mark the entire tree as selected if this
144
     * clause is null
145
     * Note: _parseExpand should to be called before the invocation
146
     * of this function so that basic 'Projection Tree' with expand
147
     * information will be ready.
148
     *
149
     * @param string $select Value of $select clause
150
     *
151
     * @throws ODataException If any error occurs while reading expand clause
152
     *                        or applying selection to projection tree
153
     */
154
    private function parseSelect($select)
155
    {
156
        if (is_null($select)) {
157
            $this->rootProjectionNode->markSubtreeAsSelected();
158
        } else {
159
            $pathSegments = $this->readExpandOrSelect($select, true);
160
            $this->applySelectionToProjectionTree($pathSegments);
161
            $this->rootProjectionNode->setSelectionSpecified();
162
            $this->rootProjectionNode->removeNonSelectedNodes();
163
            $this->rootProjectionNode->removeNodesAlreadyIncludedImplicitly();
164
            //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...
165
            $this->rootProjectionNode->sortNodes();
166
        }
167
    }
168
169
    /**
170
     * Build 'Projection Tree' from the given expand path segments.
171
     *
172
     * @param array<array>      $expandPathSegments Collection of expand paths
173
     *
174
     * @throws ODataException                       If any error occurs while processing the expand path segments
175
     */
176
    private function buildProjectionTree($expandPathSegments)
177
    {
178
        foreach ($expandPathSegments as $expandSubPathSegments) {
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $expandSubPathSegments exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
179
            $currentNode = $this->rootProjectionNode;
180
            foreach ($expandSubPathSegments as $expandSubPathSegment) {
181
                $resourceSetWrapper = $currentNode->getResourceSetWrapper();
182
                $resourceType = $currentNode->getResourceType();
183
                $resourceProperty
184
                    = $resourceType->resolveProperty(
185
                        $expandSubPathSegment
186
                    );
187
                if (is_null($resourceProperty)) {
188
                    throw ODataException::createSyntaxError(
189
                        Messages::expandProjectionParserPropertyNotFound(
190
                            $resourceType->getFullName(),
191
                            $expandSubPathSegment,
192
                            false
193
                        )
194
                    );
195
                } elseif ($resourceProperty->getTypeKind() != ResourceTypeKind::ENTITY) {
196
                    throw ODataException::createBadRequestError(
197
                        Messages::expandProjectionParserExpandCanOnlyAppliedToEntity(
198
                            $resourceType->getFullName(),
199
                            $expandSubPathSegment
200
                        )
201
                    );
202
                }
203
204
                $resourceSetWrapper = $this->providerWrapper
205
                    ->getResourceSetWrapperForNavigationProperty(
206
                        $resourceSetWrapper,
207
                        $resourceType,
208
                        $resourceProperty
209
                    );
210
211
                if (is_null($resourceSetWrapper)) {
212
                    throw ODataException::createBadRequestError(
213
                        Messages::badRequestInvalidPropertyNameSpecified(
214
                            $resourceType->getFullName(),
215
                            $expandSubPathSegment
216
                        )
217
                    );
218
                }
219
220
                $singleResult
221
                    = $resourceProperty->isKindOf(
222
                        ResourcePropertyKind::RESOURCE_REFERENCE
223
                    );
224
                $resourceSetWrapper->checkResourceSetRightsForRead($singleResult);
225
                $pageSize = $resourceSetWrapper->getResourceSetPageSize();
226
                $internalOrderByInfo = null;
227
                if ($pageSize != 0 && !$singleResult) {
228
                    $this->rootProjectionNode->setPagedExpandedResult(true);
229
                    $rt = $resourceSetWrapper->getResourceType();
230
                    //assert($rt != null)
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
231
                    $keys = array_keys($rt->getKeyProperties());
232
                    //assert(!empty($keys))
0 ignored issues
show
Unused Code Comprehensibility introduced by
88% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
233
                    $orderBy = null;
234
                    foreach ($keys as $key) {
235
                        $orderBy = $orderBy . $key . ', ';
236
                    }
237
238
                    $orderBy = rtrim($orderBy, ', ');
239
                    $internalOrderByInfo = OrderByParser::parseOrderByClause(
240
                        $resourceSetWrapper,
241
                        $rt,
242
                        $orderBy,
243
                        $this->providerWrapper
244
                    );
245
                }
246
247
                $node = $currentNode->findNode($expandSubPathSegment);
248
                if (is_null($node)) {
249
                    $maxResultCount = $this->providerWrapper
250
                        ->getConfiguration()->getMaxResultsPerCollection();
251
                    $node = new ExpandedProjectionNode(
252
                        $expandSubPathSegment,
253
                        $resourceProperty,
254
                        $resourceSetWrapper,
255
                        $internalOrderByInfo,
0 ignored issues
show
Bug introduced by
It seems like $internalOrderByInfo defined by null on line 226 can be null; however, POData\UriProcessor\Quer...tionNode::__construct() 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...
256
                        null,
257
                        $pageSize == 0 ? null : $pageSize,
258
                        $maxResultCount == PHP_INT_MAX ? null : $maxResultCount
259
                    );
260
                    $currentNode->addNode($node);
261
                }
262
263
                $currentNode = $node;
264
            }
265
        }
266
    }
267
268
    /**
269
     * Modify the 'Projection Tree' to include selection details.
270
     *
271
     * @param array<array<string>> $selectPathSegments Collection of select
272
     *                                                 paths
273
     *
274
     * @throws ODataException If any error occurs while processing select
275
     *                        path segments
276
     */
277
    private function applySelectionToProjectionTree($selectPathSegments)
278
    {
279
        foreach ($selectPathSegments as $selectSubPathSegments) {
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $selectSubPathSegments exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
280
            $currentNode = $this->rootProjectionNode;
281
            $subPathCount = count($selectSubPathSegments);
282
            foreach ($selectSubPathSegments as $index => $selectSubPathSegment) {
283
                if (!($currentNode instanceof RootProjectionNode)
284
                    && !($currentNode instanceof ExpandedProjectionNode)
285
                ) {
286
                    throw ODataException::createBadRequestError(
287
                        Messages::expandProjectionParserPropertyWithoutMatchingExpand(
288
                            $currentNode->getPropertyName()
289
                        )
290
                    );
291
                }
292
293
                $currentNode->setSelectionFound();
294
                $isLastSegment = ($index == $subPathCount - 1);
295
                if ($selectSubPathSegment === '*') {
296
                    $currentNode->setSelectAllImmediateProperties();
297
                    break;
298
                }
299
300
                $currentResourceType = $currentNode->getResourceType();
301
                $resourceProperty
302
                    = $currentResourceType->resolveProperty(
303
                        $selectSubPathSegment
304
                    );
305
                if (is_null($resourceProperty)) {
306
                    throw ODataException::createSyntaxError(
307
                        Messages::expandProjectionParserPropertyNotFound(
308
                            $currentResourceType->getFullName(),
309
                            $selectSubPathSegment,
310
                            true
311
                        )
312
                    );
313
                }
314
315
                if (!$isLastSegment) {
316
                    if ($resourceProperty->isKindOf(ResourcePropertyKind::BAG)) {
317
                        throw ODataException::createBadRequestError(
318
                            Messages::expandProjectionParserBagPropertyAsInnerSelectSegment(
319
                                $currentResourceType->getFullName(),
320
                                $selectSubPathSegment
321
                            )
322
                        );
323
                    } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::PRIMITIVE)) {
324
                        throw ODataException::createBadRequestError(
325
                            Messages::expandProjectionParserPrimitivePropertyUsedAsNavigationProperty(
326
                                $currentResourceType->getFullName(),
327
                                $selectSubPathSegment
328
                            )
329
                        );
330
                    } elseif ($resourceProperty->isKindOf(ResourcePropertyKind::COMPLEX_TYPE)) {
331
                        throw ODataException::createBadRequestError(
332
                            Messages::expandProjectionParserComplexPropertyAsInnerSelectSegment(
333
                                $currentResourceType->getFullName(),
334
                                $selectSubPathSegment
335
                            )
336
                        );
337
                    } elseif ($resourceProperty->getKind() != ResourcePropertyKind::RESOURCE_REFERENCE && $resourceProperty->getKind() != ResourcePropertyKind::RESOURCESET_REFERENCE) {
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 184 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
338
                        throw ODataException::createInternalServerError(
339
                            Messages::expandProjectionParserUnexpectedPropertyType()
340
                        );
341
                    }
342
                }
343
344
                $node = $currentNode->findNode($selectSubPathSegment);
345
                if (is_null($node)) {
346
                    if (!$isLastSegment) {
347
                        throw ODataException::createBadRequestError(
348
                            Messages::expandProjectionParserPropertyWithoutMatchingExpand(
349
                                $selectSubPathSegment
350
                            )
351
                        );
352
                    }
353
354
                    $node = new ProjectionNode($selectSubPathSegment, $resourceProperty);
355
                    $currentNode->addNode($node);
356
                }
357
358
                $currentNode = $node;
359
                if ($currentNode instanceof ExpandedProjectionNode
360
                    && $isLastSegment
361
                ) {
362
                    $currentNode->setSelectionFound();
363
                    $currentNode->markSubtreeAsSelected();
364
                }
365
            }
366
        }
367
    }
368
369
    /**
370
     * Read expand or select clause.
371
     *
372
     * @param string $value    expand or select clause to read
373
     * @param bool   $isSelect true means $value is value of select clause
374
     *                         else value of expand clause
375
     *
376
     * @return array<array>     An array of 'PathSegment's, each of which is array of 'SubPathSegment's
377
     */
378
    private function readExpandOrSelect($value, $isSelect)
379
    {
380
        $pathSegments = [];
381
        $lexer = new ExpressionLexer($value);
382
        $i = 0;
383
        while ($lexer->getCurrentToken()->Id != ExpressionTokenId::END) {
384
            $lastSegment = false;
385
            if ($isSelect
386
                && $lexer->getCurrentToken()->Id == ExpressionTokenId::STAR
387
            ) {
388
                $lastSegment = true;
389
                $subPathSegment = $lexer->getCurrentToken()->Text;
390
                $lexer->nextToken();
391
            } else {
392
                $subPathSegment = $lexer->readDottedIdentifier();
393
            }
394
395
            if (!array_key_exists($i, $pathSegments)) {
396
                $pathSegments[$i] = [];
397
            }
398
399
            $pathSegments[$i][] = $subPathSegment;
400
            $tokenId = $lexer->getCurrentToken()->Id;
401
            if ($tokenId != ExpressionTokenId::END) {
402
                if ($lastSegment || $tokenId != ExpressionTokenId::SLASH) {
403
                    $lexer->validateToken(ExpressionTokenId::COMMA);
404
                    ++$i;
405
                }
406
407
                $lexer->nextToken();
408
            }
409
        }
410
411
        return $pathSegments;
412
    }
413
}
414