Completed
Push — develop ( e8233d...5c8fb2 )
by Neomerx
16:15 queued 11:26
created

Crud::fetchResourcesWithoutRelationships()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 12
cts 12
cp 1
rs 8.9197
c 0
b 0
f 0
cc 4
eloc 17
nc 4
nop 3
crap 4
1
<?php namespace Limoncello\Flute\Api;
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 ArrayObject;
20
use Closure;
21
use Doctrine\DBAL\Connection;
22
use Doctrine\DBAL\DBALException;
23
use Doctrine\DBAL\Driver\PDOConnection;
24
use Doctrine\DBAL\Platforms\AbstractPlatform;
25
use Doctrine\DBAL\Query\QueryBuilder;
26
use Doctrine\DBAL\Types\Type;
27
use Generator;
28
use Limoncello\Container\Traits\HasContainerTrait;
29
use Limoncello\Contracts\Data\ModelSchemaInfoInterface;
30
use Limoncello\Contracts\Data\RelationshipTypes;
31
use Limoncello\Contracts\L10n\FormatterFactoryInterface;
32
use Limoncello\Flute\Adapters\ModelQueryBuilder;
33
use Limoncello\Flute\Contracts\Api\CrudInterface;
34
use Limoncello\Flute\Contracts\Api\RelationshipPaginationStrategyInterface;
35
use Limoncello\Flute\Contracts\FactoryInterface;
36
use Limoncello\Flute\Contracts\Http\Query\FilterParameterInterface;
37
use Limoncello\Flute\Contracts\Models\ModelStorageInterface;
38
use Limoncello\Flute\Contracts\Models\PaginatedDataInterface;
39
use Limoncello\Flute\Contracts\Models\TagStorageInterface;
40
use Limoncello\Flute\Exceptions\InvalidArgumentException;
41
use Limoncello\Flute\L10n\Messages;
42
use Limoncello\Flute\Package\FluteSettings;
43
use Neomerx\JsonApi\Contracts\Document\DocumentInterface;
44
use Psr\Container\ContainerExceptionInterface;
45
use Psr\Container\ContainerInterface;
46
use Psr\Container\NotFoundExceptionInterface;
47
use Traversable;
48
49
/**
50
 * @package Limoncello\Flute
51
 *
52
 * @SuppressWarnings(PHPMD.TooManyMethods)
53
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
54
 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
55
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
56
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
57
 */
58
class Crud implements CrudInterface
59
{
60
    use HasContainerTrait;
61
62
    /** Internal constant. Path constant. */
63
    protected const ROOT_PATH = '';
64
65
    /** Internal constant. Path constant. */
66
    protected const PATH_SEPARATOR = DocumentInterface::PATH_SEPARATOR;
67
68
    /**
69
     * @var FactoryInterface
70
     */
71
    private $factory;
72
73
    /**
74
     * @var string
75
     */
76
    private $modelClass;
77
78
    /**
79
     * @var ModelSchemaInfoInterface
80
     */
81
    private $modelSchemas;
82
83
    /**
84
     * @var RelationshipPaginationStrategyInterface
85
     */
86
    private $relPagingStrategy;
87
88
    /**
89
     * @var Connection
90
     */
91
    private $connection;
92
93
    /**
94
     * @var iterable|null
95
     */
96
    private $filterParameters = null;
97
98
    /**
99
     * @var bool
100
     */
101
    private $areFiltersWithAnd = true;
102
103
    /**
104
     * @var iterable|null
105
     */
106
    private $sortingParameters = null;
107
108
    /**
109
     * @var array
110
     */
111
    private $relFiltersAndSorts = [];
112
113
    /**
114
     * @var iterable|null
115
     */
116
    private $includePaths = null;
117
118
    /**
119
     * @var int|null
120
     */
121
    private $pagingOffset = null;
122
123
    /**
124
     * @var Closure|null
125
     */
126
    private $columnMapper = null;
127
128
    /**
129
     * @var bool
130
     */
131
    private $isFetchTyped;
132
133
    /**
134
     * @var int|null
135
     */
136
    private $pagingLimit = null;
137
138
    /** internal constant */
139
    private const REL_FILTERS_AND_SORTS__FILTERS = 0;
140
141
    /** internal constant */
142
    private const REL_FILTERS_AND_SORTS__SORTS = 1;
143
144
    /**
145
     * @param ContainerInterface $container
146
     * @param string             $modelClass
147
     *
148
     * @throws ContainerExceptionInterface
149
     * @throws NotFoundExceptionInterface
150
     */
151 62
    public function __construct(ContainerInterface $container, string $modelClass)
152
    {
153 62
        $this->setContainer($container);
154
155 62
        $this->modelClass        = $modelClass;
156 62
        $this->factory           = $this->getContainer()->get(FactoryInterface::class);
157 62
        $this->modelSchemas      = $this->getContainer()->get(ModelSchemaInfoInterface::class);
158 62
        $this->relPagingStrategy = $this->getContainer()->get(RelationshipPaginationStrategyInterface::class);
159 62
        $this->connection        = $this->getContainer()->get(Connection::class);
160
161 62
        $this->clearBuilderParameters()->clearFetchParameters();
162
    }
163
164
    /**
165
     * @param Closure $mapper
166
     *
167
     * @return self
168
     */
169 1
    public function withColumnMapper(Closure $mapper): self
170
    {
171 1
        $this->columnMapper = $mapper;
172
173 1
        return $this;
174
    }
175
176
    /**
177
     * @inheritdoc
178
     */
179 44
    public function withFilters(iterable $filterParameters): CrudInterface
180
    {
181 44
        $this->filterParameters = $filterParameters;
0 ignored issues
show
Documentation Bug introduced by
It seems like $filterParameters of type object<Limoncello\Flute\Contracts\Api\iterable> is incompatible with the declared type object<Limoncello\Flute\Api\iterable>|null of property $filterParameters.

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...
182
183 44
        return $this;
184
    }
185
186
    /**
187
     * @inheritdoc
188
     */
189 24
    public function withIndexFilter($index): CrudInterface
190
    {
191 24
        if (is_int($index) === false && is_string($index) === false) {
192 3
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
193
        }
194
195 21
        $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
196 21
        $this->withFilters([
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...UALS => array($index))) is of type array<?,array<string|int...0":"integer|string"}>>>, 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...
197
            $pkName => [
198 21
                FilterParameterInterface::OPERATION_EQUALS => [$index],
199
            ],
200
        ]);
201
202 21
        return $this;
203
    }
204
205
    /**
206
     * @inheritdoc
207
     */
208
    public function withIndexesFilter(array $indexes): CrudInterface
209
    {
210 2
        assert(call_user_func(function () use ($indexes) {
211 2
            $allOk = true;
212
213 2
            foreach ($indexes as $index) {
214 2
                $allOk = ($allOk === true && (is_string($index) === true || is_int($index) === true));
215
            }
216
217 2
            return $allOk;
218 2
        }) === true);
219
220 2
        $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
221 2
        $this->withFilters([
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...RATION_IN => $indexes)) is of type array<?,array<string|integer,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...
222
            $pkName => [
223 2
                FilterParameterInterface::OPERATION_IN => $indexes,
224
            ],
225
        ]);
226
227 2
        return $this;
228
    }
229
230
    /**
231
     * @inheritdoc
232
     */
233 7
    public function withRelationshipFilters(string $name, iterable $filters): CrudInterface
234
    {
235 7
        assert($this->getModelSchemas()->hasRelationship($this->getModelClass(), $name) === true);
236
237 7
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__FILTERS] = $filters;
238
239 7
        return $this;
240
    }
241
242
    /**
243
     * @inheritdoc
244
     */
245 1
    public function withRelationshipSorts(string $name, iterable $sorts): CrudInterface
246
    {
247 1
        assert($this->getModelSchemas()->hasRelationship($this->getModelClass(), $name) === true);
248
249 1
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__SORTS] = $sorts;
250
251 1
        return $this;
252
    }
253
254
    /**
255
     * @inheritdoc
256
     */
257 16
    public function combineWithAnd(): CrudInterface
258
    {
259 16
        $this->areFiltersWithAnd = true;
260
261 16
        return $this;
262
    }
263
264
    /**
265
     * @inheritdoc
266
     */
267 2
    public function combineWithOr(): CrudInterface
268
    {
269 2
        $this->areFiltersWithAnd = false;
270
271 2
        return $this;
272
    }
273
274
    /**
275
     * @return bool
276
     */
277 37
    private function hasColumnMapper(): bool
278
    {
279 37
        return $this->columnMapper !== null;
280
    }
281
282
    /**
283
     * @return Closure
284
     */
285 1
    private function getColumnMapper(): Closure
286
    {
287 1
        return $this->columnMapper;
288
    }
289
290
    /**
291
     * @return bool
292
     */
293 48
    private function hasFilters(): bool
294
    {
295 48
        return empty($this->filterParameters) === false;
296
    }
297
298
    /**
299
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be iterable|null?

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...
300
     */
301 37
    private function getFilters(): iterable
302
    {
303 37
        return $this->filterParameters;
304
    }
305
306
    /**
307
     * @return bool
308
     */
309 37
    private function areFiltersWithAnd(): bool
310
    {
311 37
        return $this->areFiltersWithAnd;
312
    }
313
314
    /**
315
     * @inheritdoc
316
     */
317 17
    public function withSorts(iterable $sortingParameters): CrudInterface
318
    {
319 17
        $this->sortingParameters = $sortingParameters;
0 ignored issues
show
Documentation Bug introduced by
It seems like $sortingParameters of type object<Limoncello\Flute\Contracts\Api\iterable> is incompatible with the declared type object<Limoncello\Flute\Api\iterable>|null of property $sortingParameters.

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...
320
321 17
        return $this;
322
    }
323
324
    /**
325
     * @return bool
326
     */
327 37
    private function hasSorts(): bool
328
    {
329 37
        return empty($this->sortingParameters) === false;
330
    }
331
332
    /**
333
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be iterable|null?

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...
334
     */
335 14
    private function getSorts(): ?iterable
336
    {
337 14
        return $this->sortingParameters;
338
    }
339
340
    /**
341
     * @inheritdoc
342
     */
343 20
    public function withIncludes(iterable $includePaths): CrudInterface
344
    {
345 20
        $this->includePaths = $includePaths;
0 ignored issues
show
Documentation Bug introduced by
It seems like $includePaths of type object<Limoncello\Flute\Contracts\Api\iterable> is incompatible with the declared type object<Limoncello\Flute\Api\iterable>|null of property $includePaths.

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...
346
347 20
        return $this;
348
    }
349
350
    /**
351
     * @return bool
352
     */
353 37
    private function hasIncludes(): bool
354
    {
355 37
        return empty($this->includePaths) === false;
356
    }
357
358
    /**
359
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be iterable|null?

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...
360
     */
361 20
    private function getIncludes(): iterable
362
    {
363 20
        return $this->includePaths;
364
    }
365
366
    /**
367
     * @inheritdoc
368
     */
369 20
    public function withPaging(int $offset, int $limit): CrudInterface
370
    {
371 20
        $this->pagingOffset = $offset;
372 20
        $this->pagingLimit  = $limit;
373
374 20
        return $this;
375
    }
376
377
    /**
378
     * @inheritdoc
379
     */
380 1
    public function withoutPaging(): CrudInterface
381
    {
382 1
        $this->pagingOffset = null;
383 1
        $this->pagingLimit  = null;
384
385 1
        return $this;
386
    }
387
388
    /**
389
     * @return self
390
     */
391 62
    public function shouldBeTyped(): self
392
    {
393 62
        $this->isFetchTyped = true;
394
395 62
        return $this;
396
    }
397
398
    /**
399
     * @return self
400
     */
401 4
    public function shouldBeUntyped(): self
402
    {
403 4
        $this->isFetchTyped = false;
404
405 4
        return $this;
406
    }
407
408
    /**
409
     * @return bool
410
     */
411 46
    private function hasPaging(): bool
412
    {
413 46
        return $this->pagingOffset !== null && $this->pagingLimit !== null;
414
    }
415
416
    /**
417
     * @return int
418
     */
419 18
    private function getPagingOffset(): int
420
    {
421 18
        return $this->pagingOffset;
422
    }
423
424
    /**
425
     * @return int
426
     */
427 18
    private function getPagingLimit(): int
428
    {
429 18
        return $this->pagingLimit;
430
    }
431
432
    /**
433
     * @return bool
434
     */
435 44
    private function isFetchTyped(): bool
436
    {
437 44
        return $this->isFetchTyped;
438
    }
439
440
    /**
441
     * @return Connection
442
     */
443 49
    protected function getConnection(): Connection
444
    {
445 49
        return $this->connection;
446
    }
447
448
    /**
449
     * @param string $modelClass
450
     *
451
     * @return ModelQueryBuilder
452
     */
453 49
    protected function createBuilder(string $modelClass): ModelQueryBuilder
454
    {
455 49
        return $this->createBuilderFromConnection($this->getConnection(), $modelClass);
456
    }
457
458
    /**
459
     * @param Connection $connection
460
     * @param string     $modelClass
461
     *
462
     * @return ModelQueryBuilder
463
     */
464 49
    private function createBuilderFromConnection(Connection $connection, string $modelClass): ModelQueryBuilder
465
    {
466 49
        return $this->getFactory()->createModelQueryBuilder($connection, $modelClass, $this->getModelSchemas());
467
    }
468
469
    /**
470
     * @param ModelQueryBuilder $builder
471
     *
472
     * @return Crud
473
     */
474 37
    protected function applyColumnMapper(ModelQueryBuilder $builder): self
475
    {
476 37
        if ($this->hasColumnMapper() === true) {
477 1
            $builder->setColumnToDatabaseMapper($this->getColumnMapper());
478
        }
479
480 37
        return $this;
481
    }
482
483
    /**
484
     * @param ModelQueryBuilder $builder
485
     *
486
     * @return Crud
487
     *
488
     * @throws DBALException
489
     */
490 37
    protected function applyAliasFilters(ModelQueryBuilder $builder): self
491
    {
492 37
        if ($this->hasFilters() === true) {
493 26
            $filters = $this->getFilters();
494 26
            $this->areFiltersWithAnd() === true ?
495 26
                $builder->addFiltersWithAndToAlias($filters) : $builder->addFiltersWithOrToAlias($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 493 can also be of type null; however, Limoncello\Flute\Adapter...FiltersWithAndToAlias() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 493 can also be of type null; however, Limoncello\Flute\Adapter...dFiltersWithOrToAlias() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
496
        }
497
498 37
        return $this;
499
    }
500
501
    /**
502
     * @param ModelQueryBuilder $builder
503
     *
504
     * @return self
505
     *
506
     * @throws DBALException
507
     */
508 5
    protected function applyTableFilters(ModelQueryBuilder $builder): self
509
    {
510 5
        if ($this->hasFilters() === true) {
511 5
            $filters = $this->getFilters();
512 5
            $this->areFiltersWithAnd() === true ?
513 5
                $builder->addFiltersWithAndToTable($filters) : $builder->addFiltersWithOrToTable($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 511 can also be of type null; however, Limoncello\Flute\Adapter...FiltersWithAndToTable() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 511 can also be of type null; however, Limoncello\Flute\Adapter...dFiltersWithOrToTable() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
514
        }
515
516 5
        return $this;
517
    }
518
519
    /**
520
     * @param ModelQueryBuilder $builder
521
     *
522
     * @return self
523
     *
524
     * @throws DBALException
525
     */
526 37
    protected function applyRelationshipFiltersAndSorts(ModelQueryBuilder $builder): self
527
    {
528
        // While joining tables we select distinct rows. This flag used to apply `distinct` no more than once.
529 37
        $distinctApplied = false;
530
531 37
        foreach ($this->relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
532 7
            assert(is_string($relationshipName) === true && is_array($filtersAndSorts) === true);
533 7
            $builder->addRelationshipFiltersAndSortsWithAnd(
534 7
                $relationshipName,
535 7
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__FILTERS] ?? [],
536 7
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__SORTS] ?? []
537
            );
538
539 7
            if ($distinctApplied === false) {
540 7
                $builder->distinct();
541 7
                $distinctApplied = true;
542
            }
543
        }
544
545 37
        return $this;
546
    }
547
548
    /**
549
     * @param ModelQueryBuilder $builder
550
     *
551
     * @return self
552
     */
553 37
    protected function applySorts(ModelQueryBuilder $builder): self
554
    {
555 37
        if ($this->hasSorts() === true) {
556 4
            $builder->addSorts($this->getSorts());
0 ignored issues
show
Bug introduced by
It seems like $this->getSorts() targeting Limoncello\Flute\Api\Crud::getSorts() can also be of type null; however, Limoncello\Flute\Adapter...ueryBuilder::addSorts() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
557
        }
558
559 37
        return $this;
560
    }
561
562
    /**
563
     * @param ModelQueryBuilder $builder
564
     *
565
     * @return self
566
     */
567 46
    protected function applyPaging(ModelQueryBuilder $builder): self
568
    {
569 46
        if ($this->hasPaging() === true) {
570 18
            $builder->setFirstResult($this->getPagingOffset());
571 18
            $builder->setMaxResults($this->getPagingLimit() + 1);
572
        }
573
574 46
        return $this;
575
    }
576
577
    /**
578
     * @return self
579
     */
580 62
    protected function clearBuilderParameters(): self
581
    {
582 62
        $this->columnMapper       = null;
583 62
        $this->filterParameters   = null;
584 62
        $this->areFiltersWithAnd  = true;
585 62
        $this->sortingParameters  = null;
586 62
        $this->pagingOffset       = null;
587 62
        $this->pagingLimit        = null;
588 62
        $this->relFiltersAndSorts = [];
589
590 62
        return $this;
591
    }
592
593
    /**
594
     * @return self
595
     */
596 62
    private function clearFetchParameters(): self
597
    {
598 62
        $this->includePaths = null;
599 62
        $this->shouldBeTyped();
600
601 62
        return $this;
602
    }
603
604
    /**
605
     * @param ModelQueryBuilder $builder
606
     *
607
     * @return ModelQueryBuilder
608
     */
609 2
    protected function builderOnCount(ModelQueryBuilder $builder): ModelQueryBuilder
610
    {
611 2
        return $builder;
612
    }
613
614
    /**
615
     * @param ModelQueryBuilder $builder
616
     *
617
     * @return ModelQueryBuilder
618
     */
619 37
    protected function builderOnIndex(ModelQueryBuilder $builder): ModelQueryBuilder
620
    {
621 37
        return $builder;
622
    }
623
624
    /**
625
     * @param ModelQueryBuilder $builder
626
     *
627
     * @return ModelQueryBuilder
628
     */
629 10
    protected function builderOnReadRelationship(ModelQueryBuilder $builder): ModelQueryBuilder
630
    {
631 10
        return $builder;
632
    }
633
634
    /**
635
     * @param ModelQueryBuilder $builder
636
     *
637
     * @return ModelQueryBuilder
638
     */
639 4
    protected function builderSaveResourceOnCreate(ModelQueryBuilder $builder): ModelQueryBuilder
640
    {
641 4
        return $builder;
642
    }
643
644
    /**
645
     * @param ModelQueryBuilder $builder
646
     *
647
     * @return ModelQueryBuilder
648
     */
649 4
    protected function builderSaveResourceOnUpdate(ModelQueryBuilder $builder): ModelQueryBuilder
650
    {
651 4
        return $builder;
652
    }
653
654
    /**
655
     * @param string            $relationshipName
656
     * @param ModelQueryBuilder $builder
657
     *
658
     * @return ModelQueryBuilder
659
     *
660
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
661
     */
662 2
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
663
        $relationshipName,
0 ignored issues
show
Unused Code introduced by
The parameter $relationshipName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
664
        ModelQueryBuilder $builder
665
    ): ModelQueryBuilder {
666 2
        return $builder;
667
    }
668
669
    /**
670
     * @param string            $relationshipName
671
     * @param ModelQueryBuilder $builder
672
     *
673
     * @return ModelQueryBuilder
674
     *
675
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
676
     */
677 2
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
678
        $relationshipName,
0 ignored issues
show
Unused Code introduced by
The parameter $relationshipName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
679
        ModelQueryBuilder $builder
680
    ): ModelQueryBuilder {
681 2
        return $builder;
682
    }
683
684
    /**
685
     * @param string            $relationshipName
686
     * @param ModelQueryBuilder $builder
687
     *
688
     * @return ModelQueryBuilder
689
     *
690
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
691
     */
692 2
    protected function builderCleanRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
693
        $relationshipName,
0 ignored issues
show
Unused Code introduced by
The parameter $relationshipName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
694
        ModelQueryBuilder $builder
695
    ): ModelQueryBuilder {
696 2
        return $builder;
697
    }
698
699
    /**
700
     * @param ModelQueryBuilder $builder
701
     *
702
     * @return ModelQueryBuilder
703
     */
704 5
    protected function builderOnDelete(ModelQueryBuilder $builder): ModelQueryBuilder
705
    {
706 5
        return $builder;
707
    }
708
709
    /**
710
     * @param PaginatedDataInterface|mixed|null $data
711
     *
712
     * @return void
713
     *
714
     * @SuppressWarnings(PHPMD.ElseExpression)
715
     *
716
     * @throws DBALException
717
     */
718 20
    private function loadRelationships($data): void
719
    {
720 20
        $isPaginated = $data instanceof PaginatedDataInterface;
721 20
        $hasData     = ($isPaginated === true && empty($data->getData()) === false) ||
722 20
            ($isPaginated === false && $data !== null);
723
724 20
        if ($hasData === true && $this->hasIncludes() === true) {
725 20
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemas());
726 20
            $modelsAtPath = $this->getFactory()->createTagStorage();
727
728
            // we gonna send these objects via function params so it is an equivalent for &array
729 20
            $classAtPath = new ArrayObject();
730 20
            $idsAtPath   = new ArrayObject();
731
732 20
            $registerModelAtRoot = function ($model) use ($modelStorage, $modelsAtPath, $idsAtPath): void {
733 20
                self::registerModelAtPath(
734 20
                    $model,
735 20
                    static::ROOT_PATH,
736 20
                    $this->getModelSchemas(),
737 20
                    $modelStorage,
738 20
                    $modelsAtPath,
739 20
                    $idsAtPath
740
                );
741 20
            };
742
743 20
            $model = null;
744 20
            if ($isPaginated === true) {
745 13
                foreach ($data->getData() as $model) {
746 13
                    $registerModelAtRoot($model);
747
                }
748
            } else {
749 7
                $model = $data;
750 7
                $registerModelAtRoot($model);
751
            }
752 20
            assert($model !== null);
753 20
            $classAtPath[static::ROOT_PATH] = get_class($model);
754
755 20
            foreach ($this->getPaths($this->getIncludes()) as list ($parentPath, $childPaths)) {
0 ignored issues
show
Bug introduced by
It seems like $this->getIncludes() can be null; however, getPaths() 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...
756 9
                $this->loadRelationshipsLayer(
757 9
                    $modelsAtPath,
758 9
                    $classAtPath,
759 9
                    $idsAtPath,
760 9
                    $modelStorage,
761 9
                    $parentPath,
762 9
                    $childPaths
763
                );
764
            }
765
        }
766
    }
767
768
    /**
769
     * A helper to remember all model related data. Helps to ensure we consistently handle models in CRUD.
770
     *
771
     * @param mixed                    $model
772
     * @param string                   $path
773
     * @param ModelSchemaInfoInterface $modelSchemas
774
     * @param ModelStorageInterface    $modelStorage
775
     * @param TagStorageInterface      $modelsAtPath
776
     * @param ArrayObject              $idsAtPath
777
     *
778
     * @return mixed
779
     */
780 20
    private static function registerModelAtPath(
781
        $model,
782
        string $path,
783
        ModelSchemaInfoInterface $modelSchemas,
784
        ModelStorageInterface $modelStorage,
785
        TagStorageInterface $modelsAtPath,
786
        ArrayObject $idsAtPath
787
    ) {
788 20
        $uniqueModel = $modelStorage->register($model);
789 20
        if ($uniqueModel !== null) {
790 20
            $modelsAtPath->register($uniqueModel, $path);
791 20
            $pkName             = $modelSchemas->getPrimaryKey(get_class($uniqueModel));
792 20
            $modelId            = $uniqueModel->{$pkName};
793 20
            $idsAtPath[$path][] = $modelId;
794
        }
795
796 20
        return $uniqueModel;
797
    }
798
799
    /**
800
     * @param iterable $paths (string[])
801
     *
802
     * @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...
803
     */
804 20
    private static function getPaths(iterable $paths): iterable
805
    {
806
        // The idea is to normalize paths. It means build all intermediate paths.
807
        // e.g. if only `a.b.c` path it given it will be normalized to `a`, `a.b` and `a.b.c`.
808
        // Path depths store depth of each path (e.g. 0 for root, 1 for `a`, 2 for `a.b` and etc).
809
        // It is needed for yielding them in correct order (from top level to bottom).
810 20
        $normalizedPaths = [];
811 20
        $pathsDepths     = [];
812 20
        foreach ($paths as $path) {
813 9
            assert(is_array($path) || $path instanceof Traversable);
814 9
            $parentDepth = 0;
815 9
            $tmpPath     = static::ROOT_PATH;
816 9
            foreach ($path as $pathPiece) {
817 9
                assert(is_string($pathPiece));
818 9
                $parent                    = $tmpPath;
819 9
                $tmpPath                   = empty($tmpPath) === true ?
820 9
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
821 9
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
822 9
                $pathsDepths[$parent]      = $parentDepth++;
823
            }
824
        }
825
826
        // Here we collect paths in form of parent => [list of children]
827
        // e.g. '' => ['a', 'c', 'b'], 'b' => ['bb', 'aa'] and etc
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% 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...
828 20
        $parentWithChildren = [];
829 20
        foreach ($normalizedPaths as $path => list ($parent, $childPath)) {
830 9
            $parentWithChildren[$parent][] = $childPath;
831
        }
832
833
        // And finally sort by path depth and yield parent with its children. Top level paths first then deeper ones.
834 20
        asort($pathsDepths, SORT_NUMERIC);
835 20
        foreach ($pathsDepths as $parent => $depth) {
836 9
            assert($depth !== null); // suppress unused
837 9
            $childPaths = $parentWithChildren[$parent];
838 9
            yield [$parent, $childPaths];
839
        }
840
    }
841
842
    /**
843
     * @inheritdoc
844
     */
845 2
    public function createIndexBuilder(iterable $columns = null): QueryBuilder
846
    {
847 2
        return $this->createIndexModelBuilder($columns);
848
    }
849
850
    /**
851
     * @inheritdoc
852
     */
853 5
    public function createDeleteBuilder(): QueryBuilder
854
    {
855 5
        return $this->createDeleteModelBuilder();
856
    }
857
858
    /**
859
     * @param iterable|null $columns
860
     *
861
     * @return ModelQueryBuilder
862
     *
863
     * @throws DBALException
864
     */
865 37
    protected function createIndexModelBuilder(iterable $columns = null): ModelQueryBuilder
866
    {
867 37
        $builder = $this->createBuilder($this->getModelClass());
868
869
        $this
870 37
            ->applyColumnMapper($builder);
871
872
        $builder
873 37
            ->selectModelColumns($columns)
874 37
            ->fromModelTable();
875
876
        $this
877 37
            ->applyAliasFilters($builder)
878 37
            ->applySorts($builder)
879 37
            ->applyRelationshipFiltersAndSorts($builder)
880 37
            ->applyPaging($builder);
881
882 37
        $result = $this->builderOnIndex($builder);
883
884 37
        $this->clearBuilderParameters();
885
886 37
        return $result;
887
    }
888
889
    /**
890
     * @return ModelQueryBuilder
891
     *
892
     * @throws DBALException
893
     */
894 5
    protected function createDeleteModelBuilder(): ModelQueryBuilder
895
    {
896
        $builder = $this
897 5
            ->createBuilder($this->getModelClass())
898 5
            ->deleteModels();
899
900 5
        $this->applyTableFilters($builder);
901
902 5
        $result = $this->builderOnDelete($builder);
903
904 5
        $this->clearBuilderParameters();
905
906 5
        return $result;
907
    }
908
909
    /**
910
     * @inheritdoc
911
     */
912 17
    public function index(): PaginatedDataInterface
913
    {
914 17
        $builder = $this->createIndexModelBuilder();
915 17
        $data    = $this->fetchResources($builder, $builder->getModelClass());
916
917 17
        return $data;
918
    }
919
920
    /**
921
     * @inheritdoc
922
     */
923 4
    public function indexIdentities(): array
924
    {
925 4
        $pkName  = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
926 4
        $builder = $this->createIndexModelBuilder([$pkName]);
0 ignored issues
show
Documentation introduced by
array($pkName) is of type array<integer,?,{"0":"?"}>, but the function expects a object<Limoncello\Flute\Api\iterable>|null.

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...
927
        /** @var Generator $data */
928 4
        $data   = $this->fetchColumn($builder, $builder->getModelClass(), $pkName);
929 4
        $result = iterator_to_array($data);
930
931 4
        return $result;
932
    }
933
934
    /**
935
     * @inheritdoc
936
     */
937 14
    public function read($index)
938
    {
939 14
        $this->withIndexFilter($index);
940
941 12
        $builder = $this->createIndexModelBuilder();
942 12
        $data    = $this->fetchResource($builder, $builder->getModelClass());
943
944 12
        return $data;
945
    }
946
947
    /**
948
     * @inheritdoc
949
     */
950 2
    public function count(): ?int
951
    {
952 2
        $result = $this->builderOnCount(
953 2
            $this->createCountBuilderFromBuilder($this->createIndexModelBuilder())
954 2
        )->execute()->fetchColumn();
955
956 2
        return $result === false ? null : $result;
957
    }
958
959
    /**
960
     * @param string        $relationshipName
961
     * @param iterable|null $relationshipFilters
962
     * @param iterable|null $relationshipSorts
963
     * @param iterable|null $columns
964
     *
965
     * @return ModelQueryBuilder
966
     *
967
     * @throws DBALException
968
     */
969 10
    public function createReadRelationshipBuilder(
970
        string $relationshipName,
971
        iterable $relationshipFilters = null,
972
        iterable $relationshipSorts = null,
973
        iterable $columns = null
974
    ): ModelQueryBuilder {
975 10
        assert(
976 10
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $relationshipName),
977 10
            "Relationship `$relationshipName` do not exist in model `" . $this->getModelClass() . '`'
978
        );
979
980
        // as we read data from a relationship our main table and model would be the table/model in the relationship
981
        // so 'root' model(s) will be located in the reverse relationship.
982
983
        list ($targetModelClass, $reverseRelName) =
984 10
            $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $relationshipName);
985
986
        $builder = $this
987 10
            ->createBuilder($targetModelClass)
988 10
            ->selectModelColumns($columns)
989 10
            ->fromModelTable();
990
991
        // 'root' filters would be applied to the data in the reverse relationship ...
992 10
        if ($this->hasFilters() === true) {
993 10
            $filters = $this->getFilters();
994 10
            $sorts   = $this->getSorts();
995 10
            $this->areFiltersWithAnd() ?
996 9
                $builder->addRelationshipFiltersAndSortsWithAnd($reverseRelName, $filters, $sorts) :
997 1
                $builder->addRelationshipFiltersAndSortsWithOr($reverseRelName, $filters, $sorts);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 993 can also be of type null; however, Limoncello\Flute\Adapter...FiltersAndSortsWithOr() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
998
        }
999
        // ... and the input filters to actual data we select
1000 10
        if ($relationshipFilters !== null) {
1001 7
            $builder->addFiltersWithAndToAlias($relationshipFilters);
1002
        }
1003 10
        if ($relationshipSorts !== null) {
1004 3
            $builder->addSorts($relationshipSorts);
1005
        }
1006
1007 10
        $this->applyPaging($builder);
1008
1009
        // While joining tables we select distinct rows.
1010 10
        $builder->distinct();
1011
1012 10
        return $this->builderOnReadRelationship($builder);
1013
    }
1014
1015
    /**
1016
     * @inheritdoc
1017
     */
1018 9
    public function indexRelationship(
1019
        string $name,
1020
        iterable $relationshipFilters = null,
1021
        iterable $relationshipSorts = null
1022
    ) {
1023 9
        assert(
1024 9
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $name),
1025 9
            "Relationship `$name` do not exist in model `" . $this->getModelClass() . '`'
1026
        );
1027
1028
        // depending on the relationship type we expect the result to be either single resource or a collection
1029 9
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1030 9
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
1031 9
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
1032
1033 9
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts);
1034
1035 9
        $modelClass = $builder->getModelClass();
1036 9
        $data       = $isExpectMany === true ?
1037 9
            $this->fetchResources($builder, $modelClass) : $this->fetchResource($builder, $modelClass);
1038
1039 9
        return $data;
1040
    }
1041
1042
    /**
1043
     * @inheritdoc
1044
     */
1045 2
    public function indexRelationshipIdentities(
1046
        string $name,
1047
        iterable $relationshipFilters = null,
1048
        iterable $relationshipSorts = null
1049
    ): array {
1050 2
        assert(
1051 2
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $name),
1052 2
            "Relationship `$name` do not exist in model `" . $this->getModelClass() . '`'
1053
        );
1054
1055
        // depending on the relationship type we expect the result to be either single resource or a collection
1056 2
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1057 2
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
1058 2
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
1059 2
        if ($isExpectMany === false) {
1060 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1061
        }
1062
1063 1
        list ($targetModelClass) = $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $name);
1064 1
        $targetPk = $this->getModelSchemas()->getPrimaryKey($targetModelClass);
1065
1066 1
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts, [$targetPk]);
0 ignored issues
show
Documentation introduced by
array($targetPk) is of type array<integer,?,{"0":"?"}>, but the function expects a object<Limoncello\Flute\Api\iterable>|null.

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...
1067
1068 1
        $modelClass = $builder->getModelClass();
1069
        /** @var Generator $data */
1070 1
        $data   = $this->fetchColumn($builder, $modelClass, $targetPk);
1071 1
        $result = iterator_to_array($data);
1072
1073 1
        return $result;
1074
    }
1075
1076
    /**
1077
     * @inheritdoc
1078
     */
1079 3
    public function readRelationship(
1080
        $index,
1081
        string $name,
1082
        iterable $relationshipFilters = null,
1083
        iterable $relationshipSorts = null
1084
    ) {
1085 3
        return $this->withIndexFilter($index)->indexRelationship($name, $relationshipFilters, $relationshipSorts);
1086
    }
1087
1088
    /**
1089
     * @inheritdoc
1090
     */
1091 6
    public function hasInRelationship($parentId, string $name, $childId): bool
1092
    {
1093 6
        if ($parentId !== null && is_scalar($parentId) === false) {
1094 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1095
        }
1096 5
        if ($childId !== null && is_scalar($childId) === false) {
1097 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1098
        }
1099
1100 4
        $parentPkName  = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1101 4
        $parentFilters = [$parentPkName => [FilterParameterInterface::OPERATION_EQUALS => [$parentId]]];
1102 4
        list($childClass) = $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $name);
1103 4
        $childPkName  = $this->getModelSchemas()->getPrimaryKey($childClass);
1104 4
        $childFilters = [$childPkName => [FilterParameterInterface::OPERATION_EQUALS => [$childId]]];
1105
1106
        $data = $this
1107 4
            ->clearBuilderParameters()
1108 4
            ->clearFetchParameters()
1109 4
            ->withFilters($parentFilters)
0 ignored issues
show
Documentation introduced by
$parentFilters is of type array<?,array<string|int...ble|string|boolean"}>>>, 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...
1110 4
            ->indexRelationship($name, $childFilters);
0 ignored issues
show
Documentation introduced by
$childFilters is of type array<?,array<string|int...ble|string|boolean"}>>>, but the function expects a object<Limoncello\Flute\...acts\Api\iterable>|null.

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...
1111
1112 4
        $has = empty($data->getData()) === false;
1113
1114 4
        return $has;
1115
    }
1116
1117
    /**
1118
     * @inheritdoc
1119
     */
1120 1
    public function delete(): int
1121
    {
1122 1
        $deleted = $this->createDeleteBuilder()->execute();
1123
1124 1
        $this->clearFetchParameters();
1125
1126 1
        return (int)$deleted;
1127
    }
1128
1129
    /**
1130
     * @inheritdoc
1131
     */
1132 6
    public function remove($index): bool
1133
    {
1134 6
        $this->withIndexFilter($index);
1135
1136 5
        $deleted = $this->createDeleteBuilder()->execute();
1137
1138 4
        $this->clearFetchParameters();
1139
1140 4
        return (int)$deleted > 0;
1141
    }
1142
1143
    /**
1144
     * @inheritdoc
1145
     */
1146 5
    public function create($index, iterable $attributes, iterable $toMany): string
1147
    {
1148 5
        if ($index !== null && is_int($index) === false && is_string($index) === false) {
1149 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1150
        }
1151
1152 4
        $allowedChanges = $this->filterAttributesOnCreate($index, $attributes);
1153
        $saveMain       = $this
1154 4
            ->createBuilder($this->getModelClass())
1155 4
            ->createModel($allowedChanges);
0 ignored issues
show
Documentation introduced by
$allowedChanges is of type object<Generator>, but the function expects a object<Limoncello\Flute\Adapters\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...
1156 4
        $saveMain       = $this->builderSaveResourceOnCreate($saveMain);
1157 4
        $saveMain->getSQL(); // prepare
1158
1159 4
        $this->clearBuilderParameters()->clearFetchParameters();
1160
1161 4
        $this->inTransaction(function () use ($saveMain, $toMany, &$index) {
1162 4
            $saveMain->execute();
1163
1164
            // if no index given will use last insert ID as index
1165 4
            $index !== null ?: $index = $saveMain->getConnection()->lastInsertId();
1166
1167 4
            $inserted = 0;
1168 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1169 2
                $secondaryIdBindName = ':secondaryId';
1170 2
                $saveToMany          = $this->builderSaveRelationshipOnCreate(
1171 2
                    $relationshipName,
1172
                    $this
1173 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1174 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
1175
                );
1176 2
                foreach ($secondaryIds as $secondaryId) {
1177 2
                    $inserted += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1178
                }
1179
            }
1180 4
        });
1181
1182 4
        return $index;
1183
    }
1184
1185
    /**
1186
     * @inheritdoc
1187
     */
1188 5
    public function update($index, iterable $attributes, iterable $toMany): int
1189
    {
1190 5
        if (is_int($index) === false && is_string($index) === false) {
1191 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1192
        }
1193
1194 4
        $updated        = 0;
1195 4
        $pkName         = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1196
        $filters        = [
1197
            $pkName => [
1198 4
                FilterParameterInterface::OPERATION_EQUALS => [$index],
1199
            ],
1200
        ];
1201 4
        $allowedChanges = $this->filterAttributesOnUpdate($attributes);
1202
        $saveMain       = $this
1203 4
            ->createBuilder($this->getModelClass())
1204 4
            ->updateModels($allowedChanges)
0 ignored issues
show
Documentation introduced by
$allowedChanges is of type object<Generator>, but the function expects a object<Limoncello\Flute\Adapters\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...
1205 4
            ->addFiltersWithAndToTable($filters);
0 ignored issues
show
Documentation introduced by
$filters is of type array<?,array<string|int...0":"integer|string"}>>>, but the function expects a object<Limoncello\Flute\Adapters\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...
1206 4
        $saveMain       = $this->builderSaveResourceOnUpdate($saveMain);
1207 4
        $saveMain->getSQL(); // prepare
1208
1209 4
        $this->clearBuilderParameters()->clearFetchParameters();
1210
1211 4
        $this->inTransaction(function () use ($saveMain, $toMany, $index, &$updated) {
1212 4
            $updated = $saveMain->execute();
1213
1214 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1215 2
                $cleanToMany = $this->builderCleanRelationshipOnUpdate(
1216 2
                    $relationshipName,
1217
                    $this
1218 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1219 2
                        ->clearToManyRelationship($relationshipName, $index)
1220
                );
1221 2
                $cleanToMany->execute();
1222
1223 2
                $secondaryIdBindName = ':secondaryId';
1224 2
                $saveToMany          = $this->builderSaveRelationshipOnUpdate(
1225 2
                    $relationshipName,
1226
                    $this
1227 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1228 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
1229
                );
1230 2
                foreach ($secondaryIds as $secondaryId) {
1231 2
                    $updated += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1232
                }
1233
            }
1234 4
        });
1235
1236 4
        return (int)$updated;
1237
    }
1238
1239
    /**
1240
     * @return FactoryInterface
1241
     */
1242 49
    protected function getFactory(): FactoryInterface
1243
    {
1244 49
        return $this->factory;
1245
    }
1246
1247
    /**
1248
     * @return string
1249
     */
1250 50
    protected function getModelClass(): string
1251
    {
1252 50
        return $this->modelClass;
1253
    }
1254
1255
    /**
1256
     * @return ModelSchemaInfoInterface
1257
     */
1258 50
    protected function getModelSchemas(): ModelSchemaInfoInterface
1259
    {
1260 50
        return $this->modelSchemas;
1261
    }
1262
1263
    /**
1264
     * @return RelationshipPaginationStrategyInterface
1265
     */
1266 7
    protected function getRelationshipPagingStrategy(): RelationshipPaginationStrategyInterface
1267
    {
1268 7
        return $this->relPagingStrategy;
1269
    }
1270
1271
    /**
1272
     * @param Closure $closure
1273
     *
1274
     * @return void
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use NoType.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1275
     *
1276
     * @throws DBALException
1277
     */
1278 8
    public function inTransaction(Closure $closure): void
1279
    {
1280 8
        $connection = $this->getConnection();
1281 8
        $connection->beginTransaction();
1282
        try {
1283 8
            $isOk = ($closure() === false ? null : true);
1284 8
        } finally {
1285 8
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
1286
        }
1287
    }
1288
1289
    /**
1290
     * @inheritdoc
1291
     */
1292 25
    public function fetchResources(QueryBuilder $builder, string $modelClass): PaginatedDataInterface
1293
    {
1294 25
        $data = $this->fetchPaginatedResourcesWithoutRelationships($builder, $modelClass);
1295
1296 25
        if ($this->hasIncludes() === true) {
1297 13
            $this->loadRelationships($data);
1298 13
            $this->clearFetchParameters();
1299
        }
1300
1301 25
        return $data;
1302
    }
1303
1304
    /**
1305
     * @inheritdoc
1306
     */
1307 13
    public function fetchResource(QueryBuilder $builder, string $modelClass)
1308
    {
1309 13
        $data = $this->fetchResourceWithoutRelationships($builder, $modelClass);
1310
1311 13
        if ($this->hasIncludes() === true) {
1312 7
            $this->loadRelationships($data);
1313 7
            $this->clearFetchParameters();
1314
        }
1315
1316 13
        return $data;
1317
    }
1318
1319
    /**
1320
     * @inheritdoc
1321
     *
1322
     * @SuppressWarnings(PHPMD.ElseExpression)
1323
     */
1324 2
    public function fetchRow(QueryBuilder $builder, string $modelClass): ?array
1325
    {
1326 2
        $model = null;
1327
1328 2
        $statement = $builder->execute();
1329 2
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1330
1331 2
        if (($attributes = $statement->fetch()) !== false) {
1332 2
            if ($this->isFetchTyped() === true) {
1333 1
                $platform  = $builder->getConnection()->getDatabasePlatform();
1334 1
                $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1335 1
                $model     = $this->readRowFromAssoc($attributes, $typeNames, $platform);
1336
            } else {
1337 1
                $model = $attributes;
1338
            }
1339
        }
1340
1341 2
        $this->clearFetchParameters();
1342
1343 2
        return $model;
1344
    }
1345
1346
    /**
1347
     * @inheritdoc
1348
     *
1349
     * @SuppressWarnings(PHPMD.StaticAccess)
1350
     * @SuppressWarnings(PHPMD.ElseExpression)
1351
     */
1352 5
    public function fetchColumn(QueryBuilder $builder, string $modelClass, string $columnName): iterable
1353
    {
1354 5
        $statement = $builder->execute();
1355 5
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1356
1357 5
        if ($this->isFetchTyped() === true) {
1358 4
            $platform = $builder->getConnection()->getDatabasePlatform();
1359 4
            $typeName = $this->getModelSchemas()->getAttributeTypes($modelClass)[$columnName];
1360 4
            $type     = Type::getType($typeName);
1361 4
            while (($attributes = $statement->fetch()) !== false) {
1362 4
                $value     = $attributes[$columnName];
1363 4
                $converted = $type->convertToPHPValue($value, $platform);
1364
1365 4
                yield $converted;
1366
            }
1367
        } else {
1368 1
            while (($attributes = $statement->fetch()) !== false) {
1369 1
                $value = $attributes[$columnName];
1370
1371 1
                yield $value;
1372
            }
1373
        }
1374
1375 5
        $this->clearFetchParameters();
1376
    }
1377
1378
    /**
1379
     * @param QueryBuilder $builder
1380
     *
1381
     * @return ModelQueryBuilder
1382
     */
1383 2
    protected function createCountBuilderFromBuilder(QueryBuilder $builder): ModelQueryBuilder
1384
    {
1385 2
        $countBuilder = $this->createBuilder($this->getModelClass());
1386 2
        $countBuilder->setParameters($builder->getParameters());
1387 2
        $countBuilder->select('COUNT(*)')->from('(' . $builder->getSQL() . ') AS RESULT');
1388
1389 2
        return $countBuilder;
1390
    }
1391
1392
    /**
1393
     * @param QueryBuilder $builder
1394
     * @param string       $modelClass
1395
     *
1396
     * @return mixed|null
1397
     *
1398
     * @throws DBALException
1399
     *
1400
     * @SuppressWarnings(PHPMD.ElseExpression)
1401
     */
1402 13
    private function fetchResourceWithoutRelationships(QueryBuilder $builder, string $modelClass)
1403
    {
1404 13
        $model     = null;
1405 13
        $statement = $builder->execute();
1406
1407 13
        if ($this->isFetchTyped() === true) {
1408 11
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1409 11
            if (($attributes = $statement->fetch()) !== false) {
1410 11
                $platform  = $builder->getConnection()->getDatabasePlatform();
1411 11
                $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1412 11
                $model     = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1413
            }
1414
        } else {
1415 2
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1416 2
            if (($fetched = $statement->fetch()) !== false) {
1417 2
                $model = $fetched;
1418
            }
1419
        }
1420
1421 13
        return $model;
1422
    }
1423
1424
    /**
1425
     * @param QueryBuilder $builder
1426
     * @param string       $modelClass
1427
     * @param string       $keyColumnName
1428
     *
1429
     * @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...
1430
     *
1431
     * @throws DBALException
1432
     *
1433
     * @SuppressWarnings(PHPMD.ElseExpression)
1434
     */
1435 8
    private function fetchResourcesWithoutRelationships(
1436
        QueryBuilder $builder,
1437
        string $modelClass,
1438
        string $keyColumnName
1439
    ): iterable {
1440 8
        $statement = $builder->execute();
1441
1442 8
        if ($this->isFetchTyped() === true) {
1443 7
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1444 7
            $platform  = $builder->getConnection()->getDatabasePlatform();
1445 7
            $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1446 7
            while (($attributes = $statement->fetch()) !== false) {
1447 6
                $model = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1448 6
                yield $model->{$keyColumnName} => $model;
1449
            }
1450
        } else {
1451 1
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1452 1
            while (($model = $statement->fetch()) !== false) {
1453 1
                yield $model->{$keyColumnName} => $model;
1454
            }
1455
        }
1456
    }
1457
1458
    /**
1459
     * @param QueryBuilder $builder
1460
     * @param string       $modelClass
1461
     *
1462
     * @return PaginatedDataInterface
1463
     *
1464
     * @throws DBALException
1465
     */
1466 29
    private function fetchPaginatedResourcesWithoutRelationships(
1467
        QueryBuilder $builder,
1468
        string $modelClass
1469
    ): PaginatedDataInterface {
1470 29
        list($models, $hasMore, $limit, $offset) = $this->fetchResourceCollection($builder, $modelClass);
1471
1472 29
        $data = $this->getFactory()
1473 29
            ->createPaginatedData($models)
1474 29
            ->markAsCollection()
1475 29
            ->setOffset($offset)
1476 29
            ->setLimit($limit);
1477
1478 29
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
1479
1480 29
        return $data;
1481
    }
1482
1483
    /**
1484
     * @param QueryBuilder $builder
1485
     * @param string       $modelClass
1486
     *
1487
     * @return array
1488
     *
1489
     * @throws DBALException
1490
     *
1491
     * @SuppressWarnings(PHPMD.ElseExpression)
1492
     */
1493 29
    private function fetchResourceCollection(QueryBuilder $builder, string $modelClass): array
1494
    {
1495 29
        $statement = $builder->execute();
1496
1497 29
        $models           = [];
1498 29
        $counter          = 0;
1499 29
        $hasMoreThanLimit = false;
1500 29
        $limit            = $builder->getMaxResults() !== null ? $builder->getMaxResults() - 1 : null;
1501
1502 29
        if ($this->isFetchTyped() === true) {
1503 28
            $platform  = $builder->getConnection()->getDatabasePlatform();
1504 28
            $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1505 28
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1506 28
            while (($attributes = $statement->fetch()) !== false) {
1507 27
                $counter++;
1508 27
                if ($limit !== null && $counter > $limit) {
1509 6
                    $hasMoreThanLimit = true;
1510 6
                    break;
1511
                }
1512 27
                $models[] = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1513
            }
1514
        } else {
1515 1
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1516 1
            while (($fetched = $statement->fetch()) !== false) {
1517 1
                $counter++;
1518 1
                if ($limit !== null && $counter > $limit) {
1519 1
                    $hasMoreThanLimit = true;
1520 1
                    break;
1521
                }
1522 1
                $models[] = $fetched;
1523
            }
1524
        }
1525
1526 29
        return [$models, $hasMoreThanLimit, $limit, $builder->getFirstResult()];
1527
    }
1528
1529
    /**
1530
     * @param null|string $index
1531
     * @param iterable    $attributes
1532
     *
1533
     * @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...
1534
     */
1535 4
    protected function filterAttributesOnCreate(?string $index, iterable $attributes): iterable
1536
    {
1537 4
        if ($index !== null) {
1538 1
            $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1539 1
            yield $pkName => $index;
1540
        }
1541
1542 4
        $knownAttrAndTypes = $this->getModelSchemas()->getAttributeTypes($this->getModelClass());
1543 4
        foreach ($attributes as $attribute => $value) {
1544 4
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1545 4
                yield $attribute => $value;
1546
            }
1547
        }
1548
    }
1549
1550
    /**
1551
     * @param iterable $attributes
1552
     *
1553
     * @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...
1554
     */
1555 4
    protected function filterAttributesOnUpdate(iterable $attributes): iterable
1556
    {
1557 4
        $knownAttrAndTypes = $this->getModelSchemas()->getAttributeTypes($this->getModelClass());
1558 4
        foreach ($attributes as $attribute => $value) {
1559 4
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1560 4
                yield $attribute => $value;
1561
            }
1562
        }
1563
    }
1564
1565
    /**
1566
     * @param TagStorageInterface   $modelsAtPath
1567
     * @param ArrayObject           $classAtPath
1568
     * @param ArrayObject           $idsAtPath
1569
     * @param ModelStorageInterface $deDup
1570
     * @param string                $parentsPath
1571
     * @param array                 $childRelationships
1572
     *
1573
     * @return void
1574
     *
1575
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1576
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
1577
     *
1578
     * @throws DBALException
1579
     */
1580 9
    private function loadRelationshipsLayer(
1581
        TagStorageInterface $modelsAtPath,
1582
        ArrayObject $classAtPath,
1583
        ArrayObject $idsAtPath,
1584
        ModelStorageInterface $deDup,
1585
        string $parentsPath,
1586
        array $childRelationships
1587
    ): void {
1588 9
        $rootClass   = $classAtPath[static::ROOT_PATH];
1589 9
        $parentClass = $classAtPath[$parentsPath];
1590 9
        $parents     = $modelsAtPath->get($parentsPath);
1591
1592
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
1593
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
1594
        // $models.
1595
1596 9
        $pkName = $this->getModelSchemas()->getPrimaryKey($parentClass);
1597
1598 9
        $registerModelAtPath = function ($model, string $path) use ($deDup, $modelsAtPath, $idsAtPath) {
1599 8
            return self::registerModelAtPath(
1600 8
                $model,
1601 8
                $path,
1602 8
                $this->getModelSchemas(),
1603 8
                $deDup,
1604 8
                $modelsAtPath,
1605 8
                $idsAtPath
1606
            );
1607 9
        };
1608
1609 9
        foreach ($childRelationships as $name) {
1610 9
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
1611
1612 9
            $relationshipType = $this->getModelSchemas()->getRelationshipType($parentClass, $name);
1613
            list ($targetModelClass, $reverseRelName) =
1614 9
                $this->getModelSchemas()->getReverseRelationship($parentClass, $name);
1615
1616
            $builder = $this
1617 9
                ->createBuilder($targetModelClass)
1618 9
                ->selectModelColumns()
1619 9
                ->fromModelTable();
1620
1621 9
            $classAtPath[$childrenPath] = $targetModelClass;
1622
1623
            switch ($relationshipType) {
1624 9
                case RelationshipTypes::BELONGS_TO:
1625
                    // for 'belongsTo' relationship all resources could be read at once.
1626 8
                    $parentIds            = $idsAtPath[$parentsPath];
1627 8
                    $clonedBuilder        = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1628 8
                        $reverseRelName,
1629 8
                        [$pkName => [FilterParameterInterface::OPERATION_IN => $parentIds]],
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...TION_IN => $parentIds)) is of type array<?,array>, but the function expects a object<Limoncello\Flute\Adapters\iterable>|null.

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...
1630 8
                        null
1631
                    );
1632 8
                    $unregisteredChildren = $this->fetchResourcesWithoutRelationships(
1633 8
                        $clonedBuilder,
1634 8
                        $clonedBuilder->getModelClass(),
1635 8
                        $this->getModelSchemas()->getPrimaryKey($clonedBuilder->getModelClass())
1636
                    );
1637 8
                    $children             = [];
1638 8
                    foreach ($unregisteredChildren as $index => $unregisteredChild) {
1639 7
                        $children[$index] = $registerModelAtPath($unregisteredChild, $childrenPath);
1640
                    }
1641 8
                    $fkNameToChild = $this->getModelSchemas()->getForeignKey($parentClass, $name);
1642 8
                    foreach ($parents as $parent) {
1643 8
                        $fkToChild       = $parent->{$fkNameToChild};
1644 8
                        $parent->{$name} = $children[$fkToChild] ?? null;
1645
                    }
1646 8
                    break;
1647 7
                case RelationshipTypes::HAS_MANY:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1648 5
                case RelationshipTypes::BELONGS_TO_MANY:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1649
                    // unfortunately we have paging limits for 'many' relationship thus we have read such
1650
                    // relationships for each 'parent' individually
1651 7
                    list ($queryOffset, $queryLimit) = $this->getRelationshipPagingStrategy()
1652 7
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
1653 7
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
1654 7
                    foreach ($parents as $parent) {
1655 7
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1656 7
                            $reverseRelName,
1657 7
                            [$pkName => [FilterParameterInterface::OPERATION_EQUALS => [$parent->{$pkName}]]],
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...y($parent->{$pkName}))) is of type array<?,array<string|int...<integer,?,{"0":"?"}>>>, but the function expects a object<Limoncello\Flute\Adapters\iterable>|null.

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...
1658 7
                            []
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<Limoncello\Flute\Adapters\iterable>|null.

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...
1659
                        );
1660 7
                        $children      = $this->fetchPaginatedResourcesWithoutRelationships(
1661 7
                            $clonedBuilder,
1662 7
                            $clonedBuilder->getModelClass()
1663
                        );
1664
1665 7
                        $deDupedChildren = [];
1666 7
                        foreach ($children->getData() as $child) {
1667 7
                            $deDupedChildren[] = $registerModelAtPath($child, $childrenPath);
1668
                        }
1669
1670 7
                        $paginated = $this->getFactory()
1671 7
                            ->createPaginatedData($deDupedChildren)
1672 7
                            ->markAsCollection()
1673 7
                            ->setOffset($children->getOffset())
1674 7
                            ->setLimit($children->getLimit());
1675 7
                        $children->hasMoreItems() === true ?
1676 7
                            $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
1677
1678 7
                        $parent->{$name} = $paginated;
1679
                    }
1680 9
                    break;
1681
            }
1682
        }
1683
    }
1684
1685
    /**
1686
     * @param string $message
1687
     *
1688
     * @return string
1689
     *
1690
     * @throws ContainerExceptionInterface
1691
     * @throws NotFoundExceptionInterface
1692
     */
1693 8
    private function getMessage(string $message): string
1694
    {
1695
        /** @var FormatterFactoryInterface $factory */
1696 8
        $factory   = $this->getContainer()->get(FormatterFactoryInterface::class);
1697 8
        $formatter = $factory->createFormatter(FluteSettings::GENERIC_NAMESPACE);
1698 8
        $result    = $formatter->formatMessage($message);
1699
1700 8
        return $result;
1701
    }
1702
1703
    /**
1704
     * @param string           $class
1705
     * @param array            $attributes
1706
     * @param Type[]           $typeNames
1707
     * @param AbstractPlatform $platform
1708
     *
1709
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use object.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1710
     *
1711
     * @SuppressWarnings(PHPMD.StaticAccess)
1712
     *
1713
     * @throws DBALException
1714
     */
1715 34
    private function readResourceFromAssoc(
1716
        string $class,
1717
        array $attributes,
1718
        array $typeNames,
1719
        AbstractPlatform $platform
1720
    ) {
1721 34
        $instance = new $class();
1722 34
        foreach ($this->readTypedAttributes($attributes, $typeNames, $platform) as $name => $value) {
0 ignored issues
show
Documentation introduced by
$attributes is of type array, but the function expects a object<Limoncello\Flute\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...
1723 34
            $instance->{$name} = $value;
1724
        }
1725
1726 34
        return $instance;
1727
    }
1728
1729
    /**
1730
     * @param array            $attributes
1731
     * @param Type[]           $typeNames
1732
     * @param AbstractPlatform $platform
1733
     *
1734
     * @return array
1735
     *
1736
     * @SuppressWarnings(PHPMD.StaticAccess)
1737
     *
1738
     * @throws DBALException
1739
     */
1740 1
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
1741
    {
1742 1
        $row = [];
1743 1
        foreach ($this->readTypedAttributes($attributes, $typeNames, $platform) as $name => $value) {
0 ignored issues
show
Documentation introduced by
$attributes is of type array, but the function expects a object<Limoncello\Flute\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...
1744 1
            $row[$name] = $value;
1745
        }
1746
1747 1
        return $row;
1748
    }
1749
1750
    /**
1751
     * @param iterable         $attributes
1752
     * @param array            $typeNames
1753
     * @param AbstractPlatform $platform
1754
     *
1755
     * @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...
1756
     *
1757
     * @throws DBALException
1758
     */
1759 35
    private function readTypedAttributes(iterable $attributes, array $typeNames, AbstractPlatform $platform): iterable
1760
    {
1761 35
        foreach ($attributes as $name => $value) {
1762 35
            yield $name => (array_key_exists($name, $typeNames) === true ?
1763 35
                Type::getType($typeNames[$name])->convertToPHPValue($value, $platform) : $value);
1764
        }
1765
    }
1766
}
1767