Completed
Push — develop ( 46135c...90e0de )
by Neomerx
03:10
created

ParametersMapper::selectRootSchemeByResourceType()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
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\RelationshipInterface;
25
use Limoncello\Flute\Contracts\Schema\JsonSchemasInterface;
26
use Limoncello\Flute\Contracts\Schema\SchemaInterface;
27
use Limoncello\Flute\Contracts\Validation\JsonApiQueryValidatingParserInterface;
28
use Limoncello\Flute\Exceptions\InvalidQueryParametersException;
29
use Limoncello\Flute\Exceptions\LogicException;
30
use Neomerx\JsonApi\Contracts\Http\Query\BaseQueryParserInterface;
31
use Neomerx\JsonApi\Document\Error;
32
33
/**
34
 * @package Limoncello\Flute
35
 *
36
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
37
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
38
 */
39
class ParametersMapper implements ParametersMapperInterface
40
{
41
    /** Message */
42
    public const MSG_ERR_INVALID_OPERATION = 'Invalid Operation.';
43
44
    /** Message */
45
    public const MSG_ERR_INVALID_FIELD = 'Invalid field.';
46
47
    /** Message */
48
    public const MSG_ERR_ROOT_SCHEMA_IS_NOT_SET = 'Root Schema is not set.';
49
50
    /** Message */
51
    private const MSG_PARAM_INCLUDE = BaseQueryParserInterface::PARAM_INCLUDE;
52
53
    /** Message */
54
    private const MSG_PARAM_FILTER = BaseQueryParserInterface::PARAM_FILTER;
55
56
    /** internal constant */
57
    private const REL_FILTER_INDEX = 0;
58
59
    /** internal constant */
60
    private const REL_SORT_INDEX = 1;
61
62
    /**
63
     * @var SchemaInterface
64
     */
65
    private $rootSchema;
66
67
    /**
68
     * @var JsonSchemasInterface
69
     */
70
    private $jsonSchemas;
71
72
    /**
73
     * @var array|null
74
     */
75
    private $messages;
76
77
    /**
78
     * @var iterable
79
     */
80
    private $filters;
81
82
    /**
83
     * @var iterable
84
     */
85
    private $sorts;
86
87
    /**
88
     * @var iterable
89
     */
90
    private $includes;
91
92
    /**
93
     * @param JsonSchemasInterface $jsonSchemas
94
     * @param array|null           $messages
95
     */
96 25
    public function __construct(JsonSchemasInterface $jsonSchemas, array $messages = null)
97
    {
98 25
        $this->jsonSchemas = $jsonSchemas;
99 25
        $this->messages    = $messages;
100
101 25
        $this->withoutFilters()->withoutSorts()->withoutIncludes();
102
    }
103
104
    /**
105
     * @inheritdoc
106
     */
107 23
    public function selectRootSchemaByResourceType(string $resourceType): ParametersMapperInterface
108
    {
109 23
        $this->rootSchema = $this->getJsonSchemas()->getSchemaByResourceType($resourceType);
110
111 23
        return $this;
112
    }
113
114
    /**
115
     * @inheritdoc
116
     */
117 25
    public function withFilters(iterable $filters): ParametersMapperInterface
118
    {
119
        // Sample format
120
        // [
121
        //     '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...
122
        //         '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...
123
        //         '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...
124
        //     ],
125
        //     '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...
126
        //         '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...
127
        //         '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...
128
        //     ],
129
        //     '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...
130
        //         '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...
131
        //         '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...
132
        //     ],
133
        // ];
134
135 25
        $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...
136
137 25
        return $this;
138
    }
139
140
    /**
141
     * @return self
142
     */
143 25
    public function withoutFilters(): self
144
    {
145 25
        $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...
146
147 25
        return $this;
148
    }
149
150
    /**
151
     * @inheritdoc
152
     */
153 25
    public function withSorts(iterable $sorts): ParametersMapperInterface
154
    {
155
        // Sample format (name => isAsc)
156
        // [
157
        //     '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...
158
        //     '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...
159
        //     '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...
160
        // ];
161
162 25
        $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...
163
164 25
        return $this;
165
    }
166
167
    /**
168
     * @return self
169
     */
170 25
    public function withoutSorts(): self
171
    {
172 25
        $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...
173
174 25
        return $this;
175
    }
176
177
    /**
178
     * @inheritdoc
179
     */
180 25
    public function withIncludes(iterable $includes): ParametersMapperInterface
181
    {
182
        // Sample format
183
        // [
184
        //     ['relationship'],
185
        //     ['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...
186
        // ];
187
188 25
        $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...
189
190 25
        return $this;
191
    }
192
193
    /**
194
     * @return self
195
     */
196 25
    public function withoutIncludes(): self
197
    {
198 25
        $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...
199
200 25
        return $this;
201
    }
202
203
    /**
204
     * @inheritdoc
205
     */
206 18
    public function getMappedFilters(): iterable
207
    {
208 18
        foreach ($this->getFilters() as $field => $operationsAndArgs) {
209 10
            assert(is_string($field));
210
211
            /** @var RelationshipInterface|null $relationship */
212
            /** @var AttributeInterface $attribute */
213 10
            list ($relationship, $attribute) = $this->mapToRelationshipAndAttribute($field);
214
215 9
            $filter = new FilterParameter(
216 9
                $attribute,
217 9
                $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...
218 9
                $relationship
219
            );
220
221 9
            yield $filter;
222
        }
223
    }
224
225
    /**
226
     * @inheritdoc
227
     */
228 15
    public function getMappedSorts(): iterable
229
    {
230 15
        foreach ($this->getSorts() as $field => $isAsc) {
231 5
            assert(is_string($field) === true && empty($field) === false && is_bool($isAsc) === true);
232
233
            /** @var RelationshipInterface|null $relationship */
234
            /** @var AttributeInterface $attribute */
235 5
            list ($relationship, $attribute) = $this->mapToRelationshipAndAttribute($field);
236
237 5
            $sort = new SortParameter($attribute, $isAsc, $relationship);
238
239 5
            yield $sort;
240
        }
241
    }
242
243
    /**
244
     * @inheritdoc
245
     */
246 17
    public function getMappedIncludes(): iterable
247
    {
248 17
        $fromSchema        = $this->getRootSchema();
249 16
        $getMappedRelLinks = function (iterable $links) use ($fromSchema) : iterable {
250 5
            foreach ($links as $link) {
251 5
                assert(is_string($link));
252 5
                $fromSchemaClass = get_class($fromSchema);
253 5
                if ($this->getJsonSchemas()->hasRelationshipSchema($fromSchemaClass, $link)) {
254 4
                    $toSchema     = $this->getJsonSchemas()->getRelationshipSchema($fromSchemaClass, $link);
255 4
                    $relationship = new Relationship($link, $fromSchema, $toSchema);
256
257 4
                    yield $relationship;
258
259 4
                    $fromSchema = $toSchema;
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $fromSchema, 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...
260 4
                    continue;
261
                }
262
263 1
                $error = $this->createQueryError(static::MSG_PARAM_INCLUDE, static::MSG_ERR_INVALID_FIELD);
264 1
                throw new InvalidQueryParametersException($error);
265
            }
266 16
        };
267
268 16
        foreach ($this->getIncludes() as $links) {
269 5
            yield $getMappedRelLinks($links);
270
        }
271
    }
272
273
    /**
274
     * @inheritdoc
275
     *
276
     * @SuppressWarnings(PHPMD.ElseExpression)
277
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
278
     * @SuppressWarnings(PHPMD.NPathComplexity)
279
     */
280 15
    public function applyQueryParameters(
281
        JsonApiQueryValidatingParserInterface $parser,
282
        CrudInterface $api
283
    ): CrudInterface {
284
        //
285
        // Paging
286
        //
287 15
        $api->withPaging($parser->getPagingOffset(), $parser->getPagingLimit());
288
289
        //
290
        // Includes
291
        //
292
293
        // the two functions below compose a 2D array of relationship names in a form of iterable
294
        // [
295
        //     ['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...
296
        //     ['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...
297
        //     ['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...
298
        // ]
299 15
        $includeAsModelNames = function (iterable $relationships): iterable {
300 3
            foreach ($relationships as $relationship) {
301 3
                assert($relationship instanceof RelationshipInterface);
302 3
                yield $relationship->getNameInModel();
303
            }
304 15
        };
305 15
        $mappedIncludes      = $this->getMappedIncludes();
306 15
        $getIncludes         = function () use ($mappedIncludes, $includeAsModelNames) : iterable {
307 14
            foreach ($mappedIncludes as $relationships) {
308 3
                yield $includeAsModelNames($relationships);
309
            }
310 15
        };
311
312
        //
313
        // Filters and Sorts
314
        //
315
316 15
        $parser->areFiltersWithAnd() === true ? $api->combineWithAnd() : $api->combineWithOr();
317
318
        $this
319 15
            ->withFilters($parser->getFilters())
320 14
            ->withSorts($parser->getSorts())
321 14
            ->withIncludes($parser->getIncludes());
322
323 14
        $attributeFilters = [];
324 14
        $attributeSorts   = [];
325
326
        // As relationship filters and sorts should be applied together (in one SQL JOIN)
327
        // we have to iterate through all filters and merge related to the same relationship.
328 14
        $relFiltersAndSorts = [];
329
330 14
        foreach ($this->getMappedFilters() as $filter) {
331
            /** @var FilterParameterInterface $filter */
332 6
            $attributeName = $filter->getAttribute()->getNameInModel();
333 6
            if ($filter->getRelationship() === null) {
334 3
                $attributeFilters[$attributeName] = $filter->getOperationsWithArguments();
335
            } else {
336 5
                $relationshipName = $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...
337
338 5
                $relFiltersAndSorts[$relationshipName][self::REL_FILTER_INDEX][$attributeName] =
339 6
                    $filter->getOperationsWithArguments();
340
            }
341
        }
342 14
        foreach ($this->getMappedSorts() as $sort) {
343
            /** @var SortParameter $sort */
344 4
            $attributeName = $sort->getAttribute()->getNameInModel();
345 4
            if ($sort->getRelationship() === null) {
346 3
                $attributeSorts[$attributeName] = $sort->isAsc();
347
            } else {
348 1
                $relationshipName = $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...
349
350 4
                $relFiltersAndSorts[$relationshipName][self::REL_SORT_INDEX][$attributeName] = $sort->isAsc();
351
            }
352
        }
353
354 14
        $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...
355 14
            ->withSorts($attributeSorts)
356 14
            ->withIncludes($getIncludes());
357
358 14
        foreach ($relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
359 5
            if (array_key_exists(self::REL_FILTER_INDEX, $filtersAndSorts) === true) {
360 5
                $api->withRelationshipFilters($relationshipName, $filtersAndSorts[self::REL_FILTER_INDEX]);
361
            }
362 5
            if (array_key_exists(self::REL_SORT_INDEX, $filtersAndSorts) === true) {
363 5
                $api->withRelationshipSorts($relationshipName, $filtersAndSorts[self::REL_SORT_INDEX]);
364
            }
365
        }
366
367 14
        return $api;
368
    }
369
370
    /**
371
     * @param string $field
372
     *
373
     * @return array
374
     */
375 14
    private function mapToRelationshipAndAttribute(string $field): array
376
    {
377 14
        $rootSchema = $this->getRootSchema();
378 14
        if ($rootSchema->hasAttributeMapping($field) === true) {
379 10
            $relationship = null;
380 10
            $schema       = $rootSchema;
381 10
            $attribute    = new Attribute($field, $schema);
382
383 10
            return [$relationship, $attribute];
384 8
        } elseif ($rootSchema->hasRelationshipMapping($field) === true) {
385 5
            $fromSchema   = $rootSchema;
386 5
            $toSchema     = $this->getJsonSchemas()->getRelationshipSchema(get_class($fromSchema), $field);
387 5
            $relationship = new Relationship($field, $fromSchema, $toSchema);
388 5
            $attribute    = new Attribute($toSchema::RESOURCE_ID, $toSchema);
389
390 5
            return [$relationship, $attribute];
391 5
        } elseif (count($mightBeRelAndAttr = explode('.', $field, 2)) === 2) {
392
            // Last chance. It could be a dot ('.') separated relationship with an attribute.
393
394 4
            $mightBeRel  = $mightBeRelAndAttr[0];
395 4
            $mightBeAttr = $mightBeRelAndAttr[1];
396
397 4
            $fromSchema = $rootSchema;
398 4
            if ($fromSchema->hasRelationshipMapping($mightBeRel)) {
399 4
                $toSchema = $this->getJsonSchemas()->getRelationshipSchema(get_class($fromSchema), $mightBeRel);
400 4
                if ($toSchema::hasAttributeMapping($mightBeAttr) === true) {
401 4
                    $relationship = new Relationship($mightBeRel, $fromSchema, $toSchema);
402 4
                    $attribute    = new Attribute($mightBeAttr, $toSchema);
403
404 4
                    return [$relationship, $attribute];
405
                }
406
            }
407
        }
408
409 1
        $error = $this->createQueryError($field, static::MSG_ERR_INVALID_FIELD);
410 1
        throw new InvalidQueryParametersException($error);
411
    }
412
413
    /**
414
     * @param string   $parameterName
415
     * @param iterable $value
416
     *
417
     * @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...
418
     *
419
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
420
     */
421 9
    private function parseOperationsAndArguments(string $parameterName, iterable $value): iterable
422
    {
423
        // in this case we interpret it as an [operation => [arg1, arg2]]
424 9
        foreach ($value as $operationName => $arguments) {
425 9
            assert(is_array($arguments) || $arguments instanceof Generator);
426
427
            switch ($operationName) {
428 9
                case '=':
429 9
                case 'eq':
430 9
                case 'equals':
431 2
                    $operation = FilterParameterInterface::OPERATION_EQUALS;
432 2
                    break;
433 9
                case '!=':
434 9
                case 'neq':
435 9
                case 'not-equals':
436 1
                    $operation = FilterParameterInterface::OPERATION_NOT_EQUALS;
437 1
                    break;
438 9
                case '<':
439 9
                case 'lt':
440 9
                case 'less-than':
441 2
                    $operation = FilterParameterInterface::OPERATION_LESS_THAN;
442 2
                    break;
443 9
                case '<=':
444 9
                case 'lte':
445 9
                case 'less-or-equals':
446 1
                    $operation = FilterParameterInterface::OPERATION_LESS_OR_EQUALS;
447 1
                    break;
448 9
                case '>':
449 9
                case 'gt':
450 8
                case 'greater-than':
451 2
                    $operation = FilterParameterInterface::OPERATION_GREATER_THAN;
452 2
                    break;
453 8
                case '>=':
454 8
                case 'gte':
455 8
                case 'greater-or-equals':
456 1
                    $operation = FilterParameterInterface::OPERATION_GREATER_OR_EQUALS;
457 1
                    break;
458 8
                case 'like':
459 6
                    $operation = FilterParameterInterface::OPERATION_LIKE;
460 6
                    break;
461 6
                case 'not-like':
462 2
                    $operation = FilterParameterInterface::OPERATION_NOT_LIKE;
463 2
                    break;
464 6
                case 'in':
465 5
                    $operation = FilterParameterInterface::OPERATION_IN;
466 5
                    break;
467 2
                case 'not-in':
468 1
                    $operation = FilterParameterInterface::OPERATION_NOT_IN;
469 1
                    break;
470 2
                case 'is-null':
471 1
                    $operation = FilterParameterInterface::OPERATION_IS_NULL;
472 1
                    $arguments = [];
473 1
                    break;
474 2
                case 'not-null':
475 1
                    $operation = FilterParameterInterface::OPERATION_IS_NOT_NULL;
476 1
                    $arguments = [];
477 1
                    break;
478
                default:
479 1
                    $error = $this->createQueryError($parameterName, static::MSG_ERR_INVALID_OPERATION);
480 1
                    throw new InvalidQueryParametersException($error);
481
            }
482
483 8
            yield $operation => $arguments;
484
        }
485
    }
486
487
    /**
488
     * @return SchemaInterface
489
     */
490 22
    private function getRootSchema(): SchemaInterface
491
    {
492 22
        if ($this->rootSchema === null) {
493 1
            throw new LogicException($this->getMessage(static::MSG_ERR_ROOT_SCHEMA_IS_NOT_SET));
494
        }
495
496 21
        return $this->rootSchema;
497
    }
498
499
    /**
500
     * @return JsonSchemasInterface
501
     */
502 23
    private function getJsonSchemas(): JsonSchemasInterface
503
    {
504 23
        return $this->jsonSchemas;
505
    }
506
507
    /**
508
     * @return iterable
509
     */
510 18
    private function getFilters(): iterable
511
    {
512 18
        return $this->filters;
513
    }
514
515
    /**
516
     * @return iterable
517
     */
518 15
    private function getSorts(): iterable
519
    {
520 15
        return $this->sorts;
521
    }
522
523
    /**
524
     * @return iterable
525
     */
526 16
    private function getIncludes(): iterable
527
    {
528 16
        return $this->includes;
529
    }
530
531
    /**
532
     * @param string $name
533
     * @param string $title
534
     *
535
     * @return Error
536
     */
537 3
    private function createQueryError(string $name, string $title): Error
538
    {
539 3
        $title  = $this->getMessage($title);
540 3
        $source = [Error::SOURCE_PARAMETER => $name];
541 3
        $error  = new Error(null, null, null, null, $title, null, $source);
542
543 3
        return $error;
544
    }
545
546
    /**
547
     * @param string $message
548
     *
549
     * @return string
550
     */
551 4
    private function getMessage(string $message): string
552
    {
553 4
        $hasTranslation = $this->messages !== null && array_key_exists($message, $this->messages) === false;
554
555 4
        return $hasTranslation === true ? $this->messages[$message] : $message;
556
    }
557
}
558