Completed
Push — develop ( ec64f2...9148fa )
by Neomerx
02:26
created

QueryParser::parseFilterLink()   C

Complexity

Conditions 11
Paths 9

Size

Total Lines 39
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 11

Importance

Changes 0
Metric Value
dl 0
loc 39
ccs 22
cts 22
cp 1
rs 5.2653
c 0
b 0
f 0
cc 11
eloc 24
nc 9
nop 0
crap 11

How to fix   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 namespace Limoncello\Flute\Http\Query;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use Generator;
20
use Limoncello\Flute\Contracts\Adapters\PaginationStrategyInterface;
21
use Limoncello\Flute\Contracts\Http\Query\QueryParserInterface;
22
use Neomerx\JsonApi\Contracts\Encoder\Parameters\EncodingParametersInterface;
23
use Neomerx\JsonApi\Encoder\Parameters\EncodingParameters;
24
use Neomerx\JsonApi\Exceptions\JsonApiException;
25
26
/**
27
 * @package Limoncello\Flute
28
 */
29
class QueryParser extends BaseQueryParser implements QueryParserInterface
30
{
31
    /** Message */
32
    public const MSG_ERR_INVALID_OPERATION_ARGUMENTS = 'Invalid Operation Arguments.';
33
34
    /**
35
     * @var PaginationStrategyInterface
36
     */
37
    private $paginationStrategy;
38
39
    /**
40
     * @var array
41
     */
42
    private $filterParameters;
43
44
    /**
45
     * @var bool
46
     */
47
    private $areFiltersWithAnd;
48
49
    /**
50
     * @var int|null
51
     */
52
    private $pagingOffset;
53
54
    /**
55
     * @var int|null
56
     */
57
    private $pagingLimit;
58
59
    /**
60
     * @param PaginationStrategyInterface $paginationStrategy
61
     * @param string[]|null               $messages
62
     */
63 25
    public function __construct(PaginationStrategyInterface $paginationStrategy, array $messages = null)
64
    {
65 25
        $parameters = [];
66 25
        parent::__construct($parameters, $messages);
67
68 25
        $this->paginationStrategy = $paginationStrategy;
69
70 25
        $this->clear();
71
    }
72
73
    /**
74
     * @inheritdoc
75
     */
76 24
    public function parse(array $parameters): QueryParserInterface
77
    {
78 24
        $this->clear();
79
80 24
        parent::setParameters($parameters);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (setParameters() instead of parse()). Are you sure this is correct? If so, you might want to change this to $this->setParameters().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
81
82 24
        $this->parsePagingParameters()->parseFilterLink();
83
84 20
        return $this;
85
    }
86
87
    /**
88
     * @inheritdoc
89
     */
90 18
    public function areFiltersWithAnd(): bool
91
    {
92 18
        return $this->areFiltersWithAnd;
93
    }
94
95
    /**
96
     * @inheritdoc
97
     */
98 17
    public function getFilters(): iterable
99
    {
100 17
        foreach ($this->getFilterParameters() as $field => $operationsWithArgs) {
101 9
            if (is_string($field) === false || empty($field) === true ||
102 9
                is_array($operationsWithArgs) === false || empty($operationsWithArgs) === true
103
            ) {
104
                throw new JsonApiException($this->createParameterError(static::PARAM_FILTER));
105
            }
106
107 9
            yield $field => $this->parseOperationsAndArguments(static::PARAM_FILTER, $operationsWithArgs);
108
        }
109
    }
110
111
    /**
112
     * @inheritdoc
113
     */
114 15
    public function getPagingOffset(): ?int
115
    {
116 15
        return $this->pagingOffset;
117
    }
118
119
    /**
120
     * @inheritdoc
121
     */
122 15
    public function getPagingLimit(): ?int
123
    {
124 15
        return $this->pagingLimit;
125
    }
126
127
    /**
128
     * @inheritdoc
129
     */
130 14
    public function createEncodingParameters(): EncodingParametersInterface
131
    {
132 14
        $paths = null;
133 14
        foreach ($this->getIncludes() as $path => $links) {
134 3
            $paths[] = $path;
135
        }
136
137 14
        $fields = $this->deepReadIterable($this->getParameters()[static::PARAM_FIELDS] ?? []);
138
139
        // encoder uses only these parameters and the rest are ignored
140 14
        return new EncodingParameters($paths, empty($fields) === true ? null : $fields);
141
    }
142
143
    /**
144
     * @return self
145
     */
146 24
    private function parsePagingParameters(): self
147
    {
148 24
        $pagingParams = $this->getParameters()[static::PARAM_PAGE] ?? null;
149
150 24
        list ($this->pagingOffset, $this->pagingLimit) = $this->getPaginationStrategy()->parseParameters($pagingParams);
151 24
        assert(is_int($this->pagingOffset) === true && $this->pagingOffset >= 0);
152 24
        assert(is_int($this->pagingLimit) === true && $this->pagingLimit > 0);
153
154 24
        return $this;
155
    }
156
157
    /**
158
     * Pre-parsing for filter parameters.
159
     *
160
     * @return self
161
     */
162 24
    private function parseFilterLink(): self
163
    {
164 24
        if (array_key_exists(static::PARAM_FILTER, $this->getParameters()) === false) {
165 11
            $this->setFiltersWithAnd()->setFilterParameters([]);
166
167 11
            return $this;
168
        }
169
170 13
        $filterSection = $this->getParameters()[static::PARAM_FILTER];
171 13
        if (is_array($filterSection) === false || empty($filterSection) === true) {
172 2
            throw new JsonApiException($this->createParameterError(static::PARAM_FILTER));
173
        }
174
175 11
        $isWithAnd = true;
176 11
        reset($filterSection);
177
178
        // check if top level element is `AND` or `OR`
179 11
        $firstKey   = key($filterSection);
180 11
        $firstLcKey = strtolower(trim($firstKey));
181 11
        if (($hasOr = ($firstLcKey === 'or')) || $firstLcKey === 'and') {
182 4
            if (count($filterSection) > 1 ||
183 4
                empty($filterSection = $filterSection[$firstKey]) === true ||
184 4
                is_array($filterSection) === false
185
            ) {
186 2
                throw new JsonApiException($this->createParameterError(static::PARAM_FILTER));
187
            } else {
188 2
                $this->setFilterParameters($filterSection);
189 2
                if ($hasOr === true) {
190 2
                    $isWithAnd = false;
191
                }
192
            }
193
        } else {
194 7
            $this->setFilterParameters($filterSection);
195
        }
196
197 9
        $isWithAnd === true ? $this->setFiltersWithAnd() : $this->setFiltersWithOr();
198
199 9
        return $this;
200
    }
201
202
    /**
203
     * @param array $values
204
     *
205
     * @return self
206
     */
207 20
    private function setFilterParameters(array $values): self
208
    {
209 20
        $this->filterParameters = $values;
210
211 20
        return $this;
212
    }
213
214
    /**
215
     * @return array
216
     */
217 17
    private function getFilterParameters(): array
218
    {
219 17
        return $this->filterParameters;
220
    }
221
222
    /**
223
     * @return self
224
     */
225 18
    private function setFiltersWithAnd(): self
226
    {
227 18
        $this->areFiltersWithAnd = true;
228
229 18
        return $this;
230
    }
231
232
    /**
233
     * @return self
234
     */
235 2
    private function setFiltersWithOr(): self
236
    {
237 2
        $this->areFiltersWithAnd = false;
238
239 2
        return $this;
240
    }
241
242
    /**
243
     * @return PaginationStrategyInterface
244
     */
245 24
    private function getPaginationStrategy(): PaginationStrategyInterface
246
    {
247 24
        return $this->paginationStrategy;
248
    }
249
250
    /**
251
     * @return self
252
     */
253 25
    private function clear(): self
254
    {
255 25
        $this->filterParameters  = [];
256 25
        $this->areFiltersWithAnd = true;
257 25
        $this->pagingOffset      = null;
258 25
        $this->pagingLimit       = null;
259
260 25
        return $this;
261
    }
262
263
    /**
264
     * @param string $parameterName
265
     * @param array  $value
266
     *
267
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
268
     */
269 8
    private function parseOperationsAndArguments(string $parameterName, array $value): iterable
270
    {
271
        // in this case we interpret it as an [operation => 'comma separated argument(s)']
272 8
        foreach ($value as $operationName => $arguments) {
273 8
            if (is_string($operationName) === false || empty($operationName) === true ||
274 8
                is_string($arguments) === false || empty($arguments) === true
275
            ) {
276
                $title = static::MSG_ERR_INVALID_OPERATION_ARGUMENTS;
277
                $error = $this->createQueryError($parameterName, $title);
278
                throw new JsonApiException($error);
279
            }
280
281 8
            yield $operationName => $this->splitCommaSeparatedStringAndCheckNoEmpties($parameterName, $arguments);
282
        }
283
    }
284
285
    /**
286
     * @param iterable $input
287
     *
288
     * @return array
289
     */
290 14
    private function deepReadIterable(iterable $input): array
291
    {
292 14
        $result = [];
293
294 14
        foreach ($input as $key => $value) {
295
            $result[$key] = $value instanceof Generator ? $this->deepReadIterable($value) : $value;
0 ignored issues
show
Documentation introduced by
$value is of type object<Generator>, but the function expects a object<Limoncello\Flute\Http\Query\iterable>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
296
        }
297
298 14
        return $result;
299
    }
300
}
301