Completed
Push — master ( b6f561...db9bd8 )
by Neomerx
03:16
created

ParametersMapper::applyQueryParameters()   C

Complexity

Conditions 11
Paths 90

Size

Total Lines 88
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 11

Importance

Changes 0
Metric Value
dl 0
loc 88
ccs 42
cts 42
cp 1
rs 5.2653
c 0
b 0
f 0
cc 11
eloc 45
nc 90
nop 2
crap 11

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 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\Api\CrudInterface;
21
use Limoncello\Flute\Contracts\Http\Query\AttributeInterface;
22
use Limoncello\Flute\Contracts\Http\Query\FilterParameterInterface;
23
use Limoncello\Flute\Contracts\Http\Query\ParametersMapperInterface;
24
use Limoncello\Flute\Contracts\Http\Query\QueryParserInterface;
25
use Limoncello\Flute\Contracts\Http\Query\RelationshipInterface;
26
use Limoncello\Flute\Contracts\Schema\JsonSchemesInterface;
27
use Limoncello\Flute\Contracts\Schema\SchemaInterface;
28
use Limoncello\Flute\Exceptions\LogicException;
29
use Neomerx\JsonApi\Document\Error;
30
use Neomerx\JsonApi\Exceptions\JsonApiException;
31
32
/**
33
 * @package Limoncello\Flute
34
 *
35
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
36
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
37
 */
38
class ParametersMapper implements ParametersMapperInterface
39
{
40
    /** Message */
41
    public const MSG_ERR_INVALID_OPERATION = 'Invalid Operation.';
42
43
    /** Message */
44
    public const MSG_ERR_INVALID_FIELD = 'Invalid field.';
45
46
    /** Message */
47
    public const MSG_ERR_ROOT_SCHEME_IS_NOT_SET = 'Root Scheme is not set.';
48
49
    /** Message */
50
    private const MSG_PARAM_INCLUDE = QueryParserInterface::PARAM_INCLUDE;
51
52
    /** Message */
53
    private const MSG_PARAM_FILTER = QueryParserInterface::PARAM_FILTER;
54
55
    /** internal constant */
56
    private const REL_FILTER_INDEX = 0;
57
58
    /** internal constant */
59
    private const REL_SORT_INDEX = 1;
60
61
    /**
62
     * @var SchemaInterface
63
     */
64
    private $rootScheme;
65
66
    /**
67
     * @var JsonSchemesInterface
68
     */
69
    private $jsonSchemes;
70
71
    /**
72
     * @var array|null
73
     */
74
    private $messages;
75
76
    /**
77
     * @var iterable
78
     */
79
    private $filters;
80
81
    /**
82
     * @var iterable
83
     */
84
    private $sorts;
85
86
    /**
87
     * @var iterable
88
     */
89
    private $includes;
90
91
    /**
92
     * @param JsonSchemesInterface $jsonSchemes
93
     * @param array|null           $messages
94
     */
95 19
    public function __construct(JsonSchemesInterface $jsonSchemes, array $messages = null)
96
    {
97 19
        $this->jsonSchemes = $jsonSchemes;
98 19
        $this->messages    = $messages;
99
100 19
        $this->withoutFilters()->withoutSorts()->withoutIncludes();
101
    }
102
103
    /**
104
     * @inheritdoc
105
     */
106 18
    public function selectRootSchemeByResourceType(string $resourceType): ParametersMapperInterface
107
    {
108 18
        $this->rootScheme = $this->getJsonSchemes()->getSchemaByResourceType($resourceType);
109
110 18
        return $this;
111
    }
112
113
    /**
114
     * @inheritdoc
115
     */
116 19
    public function withFilters(iterable $filters): ParametersMapperInterface
117
    {
118
        // Sample format
119
        // [
120
        //     'attribute' => [
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
121
        //         'op1' => [arg1],
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...
122
        //         'op2' => [arg1, arg1],
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
123
        //     ],
124
        //     'relationship' => [
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
125
        //         'op1' => [arg1],
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...
126
        //         'op2' => [arg1, arg1],
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
127
        //     ],
128
        //     'relationship.attribute' => [
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
129
        //         'op1' => [arg1],
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...
130
        //         'op2' => [arg1, arg1],
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
131
        //     ],
132
        // ];
133
134 19
        $this->filters = $filters;
0 ignored issues
show
Documentation Bug introduced by
It seems like $filters of type object<Limoncello\Flute\...ts\Http\Query\iterable> is incompatible with the declared type object<Limoncello\Flute\Http\Query\iterable> of property $filters.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
135
136 19
        return $this;
137
    }
138
139
    /**
140
     * @return self
141
     */
142 19
    public function withoutFilters(): self
143
    {
144 19
        $this->withFilters([]);
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<Limoncello\Flute\...ts\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...
145
146 19
        return $this;
147
    }
148
149
    /**
150
     * @inheritdoc
151
     */
152 19
    public function withSorts(iterable $sorts): ParametersMapperInterface
153
    {
154
        // Sample format (name => isAsc)
155
        // [
156
        //     'attribute'              => true,
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% 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...
157
        //     'relationship'           => false,
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% 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...
158
        //     'relationship.attribute' => true,
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% 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...
159
        // ];
160
161 19
        $this->sorts = $sorts;
0 ignored issues
show
Documentation Bug introduced by
It seems like $sorts of type object<Limoncello\Flute\...ts\Http\Query\iterable> is incompatible with the declared type object<Limoncello\Flute\Http\Query\iterable> of property $sorts.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
162
163 19
        return $this;
164
    }
165
166
    /**
167
     * @return self
168
     */
169 19
    public function withoutSorts(): self
170
    {
171 19
        $this->withSorts([]);
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<Limoncello\Flute\...ts\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...
172
173 19
        return $this;
174
    }
175
176
    /**
177
     * @inheritdoc
178
     */
179 19
    public function withIncludes(iterable $includes): ParametersMapperInterface
180
    {
181
        // Sample format
182
        // [
183
        //     ['relationship'],
184
        //     ['relationship', 'next_relationship'],
0 ignored issues
show
Unused Code Comprehensibility introduced by
75% 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...
185
        // ];
186
187 19
        $this->includes = $includes;
0 ignored issues
show
Documentation Bug introduced by
It seems like $includes of type object<Limoncello\Flute\...ts\Http\Query\iterable> is incompatible with the declared type object<Limoncello\Flute\Http\Query\iterable> of property $includes.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
188
189 19
        return $this;
190
    }
191
192
    /**
193
     * @return self
194
     */
195 19
    public function withoutIncludes(): self
196
    {
197 19
        $this->withIncludes([]);
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<Limoncello\Flute\...ts\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...
198
199 19
        return $this;
200
    }
201
202
    /**
203
     * @inheritdoc
204
     */
205 16
    public function getMappedFilters(): iterable
206
    {
207 16
        foreach ($this->getFilters() as $field => $operationsAndArgs) {
208 7
            assert(is_string($field));
209
210
            /** @var RelationshipInterface|null $relationship */
211
            /** @var AttributeInterface $attribute */
212 7
            list ($relationship, $attribute) = $this->mapToRelationshipAndAttribute($field);
213
214 7
            $filter = new FilterParameter(
215 7
                $attribute,
216 7
                $this->parseOperationsAndArguments(static::MSG_PARAM_FILTER, $operationsAndArgs),
0 ignored issues
show
Documentation introduced by
$this->parseOperationsAn...ER, $operationsAndArgs) 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...
217 7
                $relationship
218
            );
219
220 7
            yield $filter;
221
        }
222
    }
223
224
    /**
225
     * @inheritdoc
226
     */
227 16
    public function getMappedSorts(): iterable
228
    {
229 16
        foreach ($this->getSorts() as $field => $isAsc) {
230 3
            assert(is_string($field) === true && empty($field) === false && is_bool($isAsc) === true);
231
232
            /** @var RelationshipInterface|null $relationship */
233
            /** @var AttributeInterface $attribute */
234 3
            list ($relationship, $attribute) = $this->mapToRelationshipAndAttribute($field);
235
236 3
            $sort = new SortParameter($attribute, $isAsc, $relationship);
237
238 3
            yield $sort;
239
        }
240
    }
241
242
    /**
243
     * @inheritdoc
244
     */
245 16
    public function getMappedIncludes(): iterable
246
    {
247 16
        $fromScheme        = $this->getRootScheme();
248 16
        $getMappedRelLinks = function (iterable $links) use ($fromScheme) : iterable {
249 4
            foreach ($links as $link) {
250 4
                assert(is_string($link));
251 4
                $fromSchemaClass = get_class($fromScheme);
252 4
                if ($this->getJsonSchemes()->hasRelationshipSchema($fromSchemaClass, $link)) {
253 4
                    $toScheme     = $this->getJsonSchemes()->getRelationshipSchema($fromSchemaClass, $link);
254 4
                    $relationship = new Relationship($link, $fromScheme, $toScheme);
255
256 4
                    yield $relationship;
257
258 4
                    $fromScheme = $toScheme;
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $fromScheme, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
259 4
                    continue;
260
                }
261
262
                $error = $this->createQueryError(static::MSG_PARAM_INCLUDE, static::MSG_ERR_INVALID_FIELD);
263
                throw new JsonApiException($error);
264
            }
265 16
        };
266
267 16
        foreach ($this->getIncludes() as $links) {
268 4
            yield $getMappedRelLinks($links);
269
        }
270
    }
271
272
    /**
273
     * @inheritdoc
274
     */
275 15
    public function applyQueryParameters(QueryParserInterface $parser, CrudInterface $api): CrudInterface
276
    {
277
        //
278
        // Paging
279
        //
280 15
        $api->withPaging($parser->getPagingOffset(), $parser->getPagingLimit());
281
282
        //
283
        // Includes
284
        //
285
286
        // the two functions below compose a 2D array of relationship names in a form of iterable
287
        // [
288
        //     ['rel1_name1', 'rel1_name2', 'rel1_name3', ],
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% 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...
289
        //     ['rel2_name1', ],
0 ignored issues
show
Unused Code Comprehensibility introduced by
72% 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...
290
        //     ['rel3_name1', 'rel3_name2', 'rel3_name3', ],
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% 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...
291
        // ]
292 15
        $includeAsModelNames = function (iterable $relationships): iterable {
293 3
            foreach ($relationships as $relationship) {
294 3
                assert($relationship instanceof RelationshipInterface);
295 3
                yield $relationship->getNameInModel();
296
            }
297 15
        };
298 15
        $mappedIncludes     = $this->getMappedIncludes();
299 15
        $getIncludes         = function () use ($mappedIncludes, $includeAsModelNames) : iterable {
300 15
            foreach ($mappedIncludes as $relationships) {
301 3
                yield $includeAsModelNames($relationships);
302
            }
303 15
        };
304
305
        //
306
        // Filters and Sorts
307
        //
308
309 15
        $parser->areFiltersWithAnd() === true ? $api->combineWithAnd() : $api->combineWithOr();
310
311
        $this
312 15
            ->withFilters($parser->getFilters())
313 15
            ->withSorts($parser->getSorts())
314 15
            ->withIncludes($parser->getIncludes());
315
316 15
        $attributeFilters = [];
317 15
        $attributeSorts   = [];
318
319
        // As relationship filters and sorts should be applied together (in one SQL JOIN)
320
        // we have to iterate through all filters and merge related to the same relationship.
321 15
        $relFiltersAndSorts = [];
322
323 15
        foreach ($this->getMappedFilters() as $filter) {
324
            /** @var FilterParameterInterface $filter */
325 6
            $attributeName = $filter->getAttribute()->getNameInModel();
326 6
            if ($filter->getRelationship() === null) {
327 3
                $attributeFilters[$attributeName] = $filter->getOperationsWithArguments();
328
            } else {
329
                $relationshipName                                                              =
330 4
                    $filter->getRelationship()->getNameInModel();
0 ignored issues
show
Bug introduced by
The method getNameInModel cannot be called on $filter->getRelationship() (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
331 4
                $relFiltersAndSorts[$relationshipName][self::REL_FILTER_INDEX][$attributeName] =
332 6
                    $filter->getOperationsWithArguments();
333
            }
334
        }
335 15
        foreach ($this->getMappedSorts() as $sort) {
336
            /** @var SortParameter $sort */
337 2
            $attributeName = $sort->getAttribute()->getNameInModel();
338 2
            if ($sort->getRelationship() === null) {
339 1
                $attributeSorts[$attributeName] = $sort->isAsc();
340
            } else {
341
                $relationshipName                                                            =
342 1
                    $sort->getRelationship()->getNameInModel();
0 ignored issues
show
Bug introduced by
The method getNameInModel cannot be called on $sort->getRelationship() (of type null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
343 1
                $relFiltersAndSorts[$relationshipName][self::REL_SORT_INDEX][$attributeName] =
344 2
                    $sort->isAsc();
345
            }
346
        }
347
348 15
        $api->withFilters($attributeFilters)
0 ignored issues
show
Documentation introduced by
$attributeFilters is of type array, but the function expects a object<Limoncello\Flute\Contracts\Api\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...
349 15
            ->withSorts($attributeSorts)
350 15
            ->withIncludes($getIncludes());
351
352 15
        foreach ($relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
353 4
            if (array_key_exists(self::REL_FILTER_INDEX, $filtersAndSorts) === true) {
354 4
                $api->withRelationshipFilters($relationshipName, $filtersAndSorts[self::REL_FILTER_INDEX]);
355
            }
356 4
            if (array_key_exists(self::REL_SORT_INDEX, $filtersAndSorts) === true) {
357 4
                $api->withRelationshipSorts($relationshipName, $filtersAndSorts[self::REL_SORT_INDEX]);
358
            }
359
        }
360
361 15
        return $api;
362
    }
363
364
    /**
365
     * @param string $field
366
     *
367
     * @return array
368
     */
369 9
    private function mapToRelationshipAndAttribute(string $field): array
370
    {
371 9
        $rootSchema = $this->getRootScheme();
372 9
        if ($rootSchema->hasAttributeMapping($field) === true) {
373 6
            $relationship = null;
374 6
            $scheme       = $rootSchema;
375 6
            $attribute    = new Attribute($field, $scheme);
376
377 6
            return [$relationship, $attribute];
378 6
        } elseif ($rootSchema->hasRelationshipMapping($field) === true) {
379 4
            $fromScheme   = $rootSchema;
380 4
            $toScheme     = $this->getJsonSchemes()->getRelationshipSchema(get_class($fromScheme), $field);
381 4
            $relationship = new Relationship($field, $fromScheme, $toScheme);
382 4
            $attribute    = new Attribute($toScheme::RESOURCE_ID, $toScheme);
383
384 4
            return [$relationship, $attribute];
385 4
        } elseif (count($mightBeRelAndAttr = explode('.', $field, 2)) === 2) {
386
            // Last chance. It could be a dot ('.') separated relationship with an attribute.
387
388 4
            $mightBeRel  = $mightBeRelAndAttr[0];
389 4
            $mightBeAttr = $mightBeRelAndAttr[1];
390
391 4
            $fromScheme = $rootSchema;
392 4
            if ($fromScheme->hasRelationshipMapping($mightBeRel)) {
393 4
                $toScheme = $this->getJsonSchemes()->getRelationshipSchema(get_class($fromScheme), $mightBeRel);
394 4
                if ($toScheme::hasAttributeMapping($mightBeAttr) === true) {
395 4
                    $relationship = new Relationship($mightBeRel, $fromScheme, $toScheme);
396 4
                    $attribute    = new Attribute($mightBeAttr, $toScheme);
397
398 4
                    return [$relationship, $attribute];
399
                }
400
            }
401
        }
402
403
        $error = $this->createQueryError($field, static::MSG_ERR_INVALID_FIELD);
404
        throw new JsonApiException($error);
405
    }
406
407
    /**
408
     * @param string   $parameterName
409
     * @param iterable $value
410
     *
411
     * @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...
412
     */
413 7
    private function parseOperationsAndArguments(string $parameterName, iterable $value): iterable
414
    {
415
        // in this case we interpret it as an [operation => [arg1, arg2]]
416 7
        foreach ($value as $operationName => $arguments) {
417 7
            assert(is_array($arguments) || $arguments instanceof Generator);
418
419
            switch ($operationName) {
420 7
                case '=':
421 7
                case 'eq':
422 7
                case 'equals':
423 1
                    $operation = FilterParameterInterface::OPERATION_EQUALS;
424 1
                    break;
425 7
                case '!=':
426 7
                case 'neq':
427 7
                case 'not-equals':
428
                    $operation = FilterParameterInterface::OPERATION_NOT_EQUALS;
429
                    break;
430 7
                case '<':
431 7
                case 'lt':
432 7
                case 'less-than':
433 1
                    $operation = FilterParameterInterface::OPERATION_LESS_THAN;
434 1
                    break;
435 7
                case '<=':
436 7
                case 'lte':
437 7
                case 'less-or-equals':
438
                    $operation = FilterParameterInterface::OPERATION_LESS_OR_EQUALS;
439
                    break;
440 7
                case '>':
441 7
                case 'gt':
442 6
                case 'greater-than':
443 1
                    $operation = FilterParameterInterface::OPERATION_GREATER_THAN;
444 1
                    break;
445 6
                case '>=':
446 6
                case 'gte':
447 6
                case 'greater-or-equals':
448
                    $operation = FilterParameterInterface::OPERATION_GREATER_OR_EQUALS;
449
                    break;
450 6
                case 'like':
451 5
                    $operation = FilterParameterInterface::OPERATION_LIKE;
452 5
                    break;
453 4
                case 'not-like':
454 1
                    $operation = FilterParameterInterface::OPERATION_NOT_LIKE;
455 1
                    break;
456 4
                case 'in':
457 4
                    $operation = FilterParameterInterface::OPERATION_IN;
458 4
                    break;
459
                case 'not-in':
460
                    $operation = FilterParameterInterface::OPERATION_NOT_IN;
461
                    break;
462
                case 'is-null':
463
                    $operation = FilterParameterInterface::OPERATION_IS_NULL;
464
                    $arguments = [];
465
                    break;
466
                case 'not-null':
467
                    $operation = FilterParameterInterface::OPERATION_IS_NOT_NULL;
468
                    $arguments = [];
469
                    break;
470
                default:
471
                    $error = $this->createQueryError($parameterName, static::MSG_ERR_INVALID_OPERATION);
472
                    throw new JsonApiException($error);
473
            }
474
475 7
            yield $operation => $arguments;
476
        }
477
    }
478
479
    /**
480
     * @return SchemaInterface
481
     */
482 18
    private function getRootScheme(): SchemaInterface
483
    {
484 18
        if ($this->rootScheme === null) {
485
            throw new LogicException($this->getMessage(static::MSG_ERR_ROOT_SCHEME_IS_NOT_SET));
486
        }
487
488 18
        return $this->rootScheme;
489
    }
490
491
    /**
492
     * @return JsonSchemesInterface
493
     */
494 18
    private function getJsonSchemes(): JsonSchemesInterface
495
    {
496 18
        return $this->jsonSchemes;
497
    }
498
499
    /**
500
     * @return iterable
501
     */
502 16
    private function getFilters(): iterable
503
    {
504 16
        return $this->filters;
505
    }
506
507
    /**
508
     * @return iterable
509
     */
510 16
    private function getSorts(): iterable
511
    {
512 16
        return $this->sorts;
513
    }
514
515
    /**
516
     * @return iterable
517
     */
518 16
    private function getIncludes(): iterable
519
    {
520 16
        return $this->includes;
521
    }
522
523
    /**
524
     * @param string $name
525
     * @param string $title
526
     *
527
     * @return Error
528
     */
529
    private function createQueryError(string $name, string $title): Error
530
    {
531
        $title  = $this->getMessage($title);
532
        $source = [Error::SOURCE_PARAMETER => $name];
533
        $error  = new Error(null, null, null, null, $title, null, $source);
534
535
        return $error;
536
    }
537
538
    /**
539
     * @param string $message
540
     *
541
     * @return string
542
     */
543
    private function getMessage(string $message): string
544
    {
545
        $hasTranslation = $this->messages !== null && array_key_exists($message, $this->messages) === false;
546
547
        return $hasTranslation === true ? $this->messages[$message] : $message;
548
    }
549
}
550