Completed
Push — master ( ee198e...323919 )
by Neomerx
02:19
created

Crud::fetchResources()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 9.9
c 0
b 0
f 0
cc 2
nc 2
nop 2
crap 2
1
<?php declare (strict_types = 1);
2
3
namespace Limoncello\Flute\Api;
4
5
/**
6
 * Copyright 2015-2019 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use ArrayObject;
22
use Closure;
23
use Doctrine\DBAL\Connection;
24
use Doctrine\DBAL\DBALException;
25
use Doctrine\DBAL\Driver\PDOConnection;
26
use Doctrine\DBAL\Exception\UniqueConstraintViolationException as UcvException;
27
use Doctrine\DBAL\Platforms\AbstractPlatform;
28
use Doctrine\DBAL\Query\QueryBuilder;
29
use Doctrine\DBAL\Types\Type;
30
use Generator;
31
use Limoncello\Container\Traits\HasContainerTrait;
32
use Limoncello\Contracts\Data\ModelSchemaInfoInterface;
33
use Limoncello\Contracts\Data\RelationshipTypes;
34
use Limoncello\Contracts\L10n\FormatterFactoryInterface;
35
use Limoncello\Flute\Adapters\ModelQueryBuilder;
36
use Limoncello\Flute\Contracts\Api\CrudInterface;
37
use Limoncello\Flute\Contracts\Api\RelationshipPaginationStrategyInterface;
38
use Limoncello\Flute\Contracts\FactoryInterface;
39
use Limoncello\Flute\Contracts\Http\Query\FilterParameterInterface;
40
use Limoncello\Flute\Contracts\Models\ModelStorageInterface;
41
use Limoncello\Flute\Contracts\Models\PaginatedDataInterface;
42
use Limoncello\Flute\Contracts\Models\TagStorageInterface;
43
use Limoncello\Flute\Exceptions\InvalidArgumentException;
44
use Limoncello\Flute\L10n\Messages;
45
use Neomerx\JsonApi\Contracts\Schema\DocumentInterface;
46
use Psr\Container\ContainerExceptionInterface;
47
use Psr\Container\ContainerInterface;
48
use Psr\Container\NotFoundExceptionInterface;
49
use Traversable;
50
51
/**
52
 * @package Limoncello\Flute
53
 *
54
 * @SuppressWarnings(PHPMD.TooManyMethods)
55
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
56
 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
57
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
58
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
59
 */
60
class Crud implements CrudInterface
61
{
62
    use HasContainerTrait;
63
64
    /** Internal constant. Path constant. */
65
    protected const ROOT_PATH = '';
66
67
    /** Internal constant. Path constant. */
68
    protected const PATH_SEPARATOR = DocumentInterface::PATH_SEPARATOR;
69
70
    /**
71
     * @var FactoryInterface
72
     */
73
    private $factory;
74
75
    /**
76
     * @var string
77
     */
78
    private $modelClass;
79
80
    /**
81
     * @var ModelSchemaInfoInterface
82
     */
83
    private $modelSchemas;
84
85
    /**
86
     * @var RelationshipPaginationStrategyInterface
87
     */
88
    private $relPagingStrategy;
89
90
    /**
91
     * @var Connection
92
     */
93
    private $connection;
94
95
    /**
96
     * @var iterable|null
97
     */
98
    private $filterParameters = null;
99
100
    /**
101
     * @var bool
102
     */
103
    private $areFiltersWithAnd = true;
104
105
    /**
106
     * @var iterable|null
107
     */
108
    private $sortingParameters = null;
109
110
    /**
111
     * @var array
112
     */
113
    private $relFiltersAndSorts = [];
114
115
    /**
116
     * @var iterable|null
117
     */
118
    private $includePaths = null;
119
120
    /**
121
     * @var int|null
122
     */
123
    private $pagingOffset = null;
124
125
    /**
126
     * @var Closure|null
127
     */
128
    private $columnMapper = null;
129
130
    /**
131
     * @var bool
132
     */
133
    private $isFetchTyped;
134
135
    /**
136
     * @var int|null
137
     */
138
    private $pagingLimit = null;
139
140
    /** internal constant */
141
    private const REL_FILTERS_AND_SORTS__FILTERS = 0;
142
143
    /** internal constant */
144
    private const REL_FILTERS_AND_SORTS__SORTS = 1;
145
146
    /**
147
     * @param ContainerInterface $container
148
     * @param string             $modelClass
149
     *
150
     * @throws ContainerExceptionInterface
151
     * @throws NotFoundExceptionInterface
152
     */
153 61
    public function __construct(ContainerInterface $container, string $modelClass)
154
    {
155 61
        $this->setContainer($container);
156
157 61
        $this->modelClass        = $modelClass;
158 61
        $this->factory           = $this->getContainer()->get(FactoryInterface::class);
159 61
        $this->modelSchemas      = $this->getContainer()->get(ModelSchemaInfoInterface::class);
160 61
        $this->relPagingStrategy = $this->getContainer()->get(RelationshipPaginationStrategyInterface::class);
161 61
        $this->connection        = $this->getContainer()->get(Connection::class);
162
163 61
        $this->clearBuilderParameters()->clearFetchParameters();
164
    }
165
166
    /**
167
     * @param Closure $mapper
168
     *
169
     * @return self
170
     */
171 1
    public function withColumnMapper(Closure $mapper): self
172
    {
173 1
        $this->columnMapper = $mapper;
174
175 1
        return $this;
176
    }
177
178
    /**
179
     * @inheritdoc
180
     */
181 43
    public function withFilters(iterable $filterParameters): CrudInterface
182
    {
183 43
        $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...
184
185 43
        return $this;
186
    }
187
188
    /**
189
     * @inheritdoc
190
     */
191 21
    public function withIndexFilter(string $index): CrudInterface
192
    {
193 21
        $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
194 21
        $this->withFilters([
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...UALS => array($index))) is of type array<string,array<strin...tring,{"0":"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...
195
            $pkName => [
196 21
                FilterParameterInterface::OPERATION_EQUALS => [$index],
197
            ],
198
        ]);
199
200 21
        return $this;
201
    }
202
203
    /**
204
     * @inheritdoc
205
     */
206 3
    public function withIndexesFilter(array $indexes): CrudInterface
207
    {
208 3
        if (empty($indexes) === true) {
209 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
210
        }
211
212
        assert(call_user_func(function () use ($indexes) {
213 2
            $allOk = true;
214
215 2
            foreach ($indexes as $index) {
216 2
                $allOk = ($allOk === true && (is_string($index) === true || is_int($index) === true));
217
            }
218
219 2
            return $allOk;
220 2
        }) === true);
221
222 2
        $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
223 2
        $this->withFilters([
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...RATION_IN => $indexes)) is of type array<string,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...
224
            $pkName => [
225 2
                FilterParameterInterface::OPERATION_IN => $indexes,
226
            ],
227
        ]);
228
229 2
        return $this;
230
    }
231
232
    /**
233
     * @inheritdoc
234
     */
235 7
    public function withRelationshipFilters(string $name, iterable $filters): CrudInterface
236
    {
237 7
        assert($this->getModelSchemas()->hasRelationship($this->getModelClass(), $name) === true);
238
239 7
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__FILTERS] = $filters;
240
241 7
        return $this;
242
    }
243
244
    /**
245
     * @inheritdoc
246
     */
247 1
    public function withRelationshipSorts(string $name, iterable $sorts): CrudInterface
248
    {
249 1
        assert($this->getModelSchemas()->hasRelationship($this->getModelClass(), $name) === true);
250
251 1
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__SORTS] = $sorts;
252
253 1
        return $this;
254
    }
255
256
    /**
257
     * @inheritdoc
258
     */
259 16
    public function combineWithAnd(): CrudInterface
260
    {
261 16
        $this->areFiltersWithAnd = true;
262
263 16
        return $this;
264
    }
265
266
    /**
267
     * @inheritdoc
268
     */
269 2
    public function combineWithOr(): CrudInterface
270
    {
271 2
        $this->areFiltersWithAnd = false;
272
273 2
        return $this;
274
    }
275
276
    /**
277
     * @return bool
278
     */
279 38
    private function hasColumnMapper(): bool
280
    {
281 38
        return $this->columnMapper !== null;
282
    }
283
284
    /**
285
     * @return Closure
286
     */
287 1
    private function getColumnMapper(): Closure
288
    {
289 1
        return $this->columnMapper;
290
    }
291
292
    /**
293
     * @return bool
294
     */
295 47
    private function hasFilters(): bool
296
    {
297 47
        return empty($this->filterParameters) === false;
298
    }
299
300
    /**
301
     * @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...
302
     */
303 36
    private function getFilters(): iterable
304
    {
305 36
        return $this->filterParameters;
306
    }
307
308
    /**
309
     * @return bool
310
     */
311 36
    private function areFiltersWithAnd(): bool
312
    {
313 36
        return $this->areFiltersWithAnd;
314
    }
315
316
    /**
317
     * @inheritdoc
318
     */
319 17
    public function withSorts(iterable $sortingParameters): CrudInterface
320
    {
321 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...
322
323 17
        return $this;
324
    }
325
326
    /**
327
     * @return bool
328
     */
329 38
    private function hasSorts(): bool
330
    {
331 38
        return empty($this->sortingParameters) === false;
332
    }
333
334
    /**
335
     * @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...
336
     */
337 11
    private function getSorts(): ?iterable
338
    {
339 11
        return $this->sortingParameters;
340
    }
341
342
    /**
343
     * @inheritdoc
344
     */
345 21
    public function withIncludes(iterable $includePaths): CrudInterface
346
    {
347 21
        $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...
348
349 21
        return $this;
350
    }
351
352
    /**
353
     * @return bool
354
     */
355 36
    private function hasIncludes(): bool
356
    {
357 36
        return empty($this->includePaths) === false;
358
    }
359
360
    /**
361
     * @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...
362
     */
363 21
    private function getIncludes(): iterable
364
    {
365 21
        return $this->includePaths;
366
    }
367
368
    /**
369
     * @inheritdoc
370
     */
371 20
    public function withPaging(int $offset, int $limit): CrudInterface
372
    {
373 20
        $this->pagingOffset = $offset;
374 20
        $this->pagingLimit  = $limit;
375
376 20
        return $this;
377
    }
378
379
    /**
380
     * @inheritdoc
381
     */
382 1
    public function withoutPaging(): CrudInterface
383
    {
384 1
        $this->pagingOffset = null;
385 1
        $this->pagingLimit  = null;
386
387 1
        return $this;
388
    }
389
390
    /**
391
     * @return self
392
     */
393 61
    public function shouldBeTyped(): self
394
    {
395 61
        $this->isFetchTyped = true;
396
397 61
        return $this;
398
    }
399
400
    /**
401
     * @return self
402
     */
403 4
    public function shouldBeUntyped(): self
404
    {
405 4
        $this->isFetchTyped = false;
406
407 4
        return $this;
408
    }
409
410
    /**
411
     * @return bool
412
     */
413 45
    private function hasPaging(): bool
414
    {
415 45
        return $this->pagingOffset !== null && $this->pagingLimit !== null;
416
    }
417
418
    /**
419
     * @return int
420
     */
421 18
    private function getPagingOffset(): int
422
    {
423 18
        return $this->pagingOffset;
424
    }
425
426
    /**
427
     * @return int
428
     */
429 18
    private function getPagingLimit(): int
430
    {
431 18
        return $this->pagingLimit;
432
    }
433
434
    /**
435
     * @return bool
436
     */
437 43
    private function isFetchTyped(): bool
438
    {
439 43
        return $this->isFetchTyped;
440
    }
441
442
    /**
443
     * @return Connection
444
     */
445 52
    protected function getConnection(): Connection
446
    {
447 52
        return $this->connection;
448
    }
449
450
    /**
451
     * @param string $modelClass
452
     *
453
     * @return ModelQueryBuilder
454
     */
455 50
    protected function createBuilder(string $modelClass): ModelQueryBuilder
456
    {
457 50
        return $this->createBuilderFromConnection($this->getConnection(), $modelClass);
458
    }
459
460
    /**
461
     * @param Connection $connection
462
     * @param string     $modelClass
463
     *
464
     * @return ModelQueryBuilder
465
     */
466 52
    private function createBuilderFromConnection(Connection $connection, string $modelClass): ModelQueryBuilder
467
    {
468 52
        return $this->getFactory()->createModelQueryBuilder($connection, $modelClass, $this->getModelSchemas());
469
    }
470
471
    /**
472
     * @param ModelQueryBuilder $builder
473
     *
474
     * @return Crud
475
     */
476 38
    protected function applyColumnMapper(ModelQueryBuilder $builder): self
477
    {
478 38
        if ($this->hasColumnMapper() === true) {
479 1
            $builder->setColumnToDatabaseMapper($this->getColumnMapper());
480
        }
481
482 38
        return $this;
483
    }
484
485
    /**
486
     * @param ModelQueryBuilder $builder
487
     *
488
     * @return Crud
489
     *
490
     * @throws DBALException
491
     */
492 38
    protected function applyAliasFilters(ModelQueryBuilder $builder): self
493
    {
494 38
        if ($this->hasFilters() === true) {
495 27
            $filters = $this->getFilters();
496 27
            $this->areFiltersWithAnd() === true ?
497 27
                $builder->addFiltersWithAndToAlias($filters) : $builder->addFiltersWithOrToAlias($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 495 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 495 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...
498
        }
499
500 38
        return $this;
501
    }
502
503
    /**
504
     * @param ModelQueryBuilder $builder
505
     *
506
     * @return self
507
     *
508
     * @throws DBALException
509
     */
510 4
    protected function applyTableFilters(ModelQueryBuilder $builder): self
511
    {
512 4
        if ($this->hasFilters() === true) {
513 4
            $filters = $this->getFilters();
514 4
            $this->areFiltersWithAnd() === true ?
515 4
                $builder->addFiltersWithAndToTable($filters) : $builder->addFiltersWithOrToTable($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 513 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 513 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...
516
        }
517
518 4
        return $this;
519
    }
520
521
    /**
522
     * @param ModelQueryBuilder $builder
523
     *
524
     * @return self
525
     *
526
     * @throws DBALException
527
     */
528 38
    protected function applyRelationshipFiltersAndSorts(ModelQueryBuilder $builder): self
529
    {
530
        // While joining tables we select distinct rows. This flag used to apply `distinct` no more than once.
531 38
        $distinctApplied = false;
532
533 38
        foreach ($this->relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
534 7
            assert(is_string($relationshipName) === true && is_array($filtersAndSorts) === true);
535 7
            $builder->addRelationshipFiltersAndSorts(
536 7
                $relationshipName,
537 7
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__FILTERS] ?? [],
538 7
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__SORTS] ?? []
539
            );
540
541 7
            if ($distinctApplied === false) {
542 7
                $builder->distinct();
543 7
                $distinctApplied = true;
544
            }
545
        }
546
547 38
        return $this;
548
    }
549
550
    /**
551
     * @param ModelQueryBuilder $builder
552
     *
553
     * @return self
554
     *
555
     * @throws DBALException
556
     */
557 38
    protected function applySorts(ModelQueryBuilder $builder): self
558
    {
559 38
        if ($this->hasSorts() === true) {
560 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...
561
        }
562
563 38
        return $this;
564
    }
565
566
    /**
567
     * @param ModelQueryBuilder $builder
568
     *
569
     * @return self
570
     */
571 45
    protected function applyPaging(ModelQueryBuilder $builder): self
572
    {
573 45
        if ($this->hasPaging() === true) {
574 18
            $builder->setFirstResult($this->getPagingOffset());
575 18
            $builder->setMaxResults($this->getPagingLimit() + 1);
576
        }
577
578 45
        return $this;
579
    }
580
581
    /**
582
     * @return self
583
     */
584 61
    protected function clearBuilderParameters(): self
585
    {
586 61
        $this->columnMapper       = null;
587 61
        $this->filterParameters   = null;
588 61
        $this->areFiltersWithAnd  = true;
589 61
        $this->sortingParameters  = null;
590 61
        $this->pagingOffset       = null;
591 61
        $this->pagingLimit        = null;
592 61
        $this->relFiltersAndSorts = [];
593
594 61
        return $this;
595
    }
596
597
    /**
598
     * @return self
599
     */
600 61
    private function clearFetchParameters(): self
601
    {
602 61
        $this->includePaths = null;
603 61
        $this->shouldBeTyped();
604
605 61
        return $this;
606
    }
607
608
    /**
609
     * @param ModelQueryBuilder $builder
610
     *
611
     * @return ModelQueryBuilder
612
     */
613 2
    protected function builderOnCount(ModelQueryBuilder $builder): ModelQueryBuilder
614
    {
615 2
        return $builder;
616
    }
617
618
    /**
619
     * @param ModelQueryBuilder $builder
620
     *
621
     * @return ModelQueryBuilder
622
     */
623 38
    protected function builderOnIndex(ModelQueryBuilder $builder): ModelQueryBuilder
624
    {
625 38
        return $builder;
626
    }
627
628
    /**
629
     * @param ModelQueryBuilder $builder
630
     *
631
     * @return ModelQueryBuilder
632
     */
633 7
    protected function builderOnReadRelationship(ModelQueryBuilder $builder): ModelQueryBuilder
634
    {
635 7
        return $builder;
636
    }
637
638
    /**
639
     * @param ModelQueryBuilder $builder
640
     *
641
     * @return ModelQueryBuilder
642
     */
643 5
    protected function builderSaveResourceOnCreate(ModelQueryBuilder $builder): ModelQueryBuilder
644
    {
645 5
        return $builder;
646
    }
647
648
    /**
649
     * @param ModelQueryBuilder $builder
650
     *
651
     * @return ModelQueryBuilder
652
     */
653 5
    protected function builderSaveResourceOnUpdate(ModelQueryBuilder $builder): ModelQueryBuilder
654
    {
655 5
        return $builder;
656
    }
657
658
    /**
659
     * @param string            $relationshipName
660
     * @param ModelQueryBuilder $builder
661
     *
662
     * @return ModelQueryBuilder
663
     *
664
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
665
     */
666 2
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
667
        $relationshipName,
668
        ModelQueryBuilder $builder
669
    ): ModelQueryBuilder {
670 2
        return $builder;
671
    }
672
673
    /**
674
     * @param string            $relationshipName
675
     * @param ModelQueryBuilder $builder
676
     *
677
     * @return ModelQueryBuilder
678
     *
679
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
680
     */
681 2
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
682
        $relationshipName,
683
        ModelQueryBuilder $builder
684
    ): ModelQueryBuilder {
685 2
        return $builder;
686
    }
687
688
    /**
689
     * @param string            $relationshipName
690
     * @param ModelQueryBuilder $builder
691
     *
692
     * @return ModelQueryBuilder
693
     *
694
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
695
     */
696 1
    protected function builderOnCreateInBelongsToManyRelationship(/** @noinspection PhpUnusedParameterInspection */
697
        $relationshipName,
698
        ModelQueryBuilder $builder
699
    ): ModelQueryBuilder {
700 1
        return $builder;
701
    }
702
703
    /**
704
     * @param string            $relationshipName
705
     * @param ModelQueryBuilder $builder
706
     *
707
     * @return ModelQueryBuilder
708
     *
709
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
710
     */
711 1
    protected function builderOnRemoveInBelongsToManyRelationship(/** @noinspection PhpUnusedParameterInspection */
712
        $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...
713
        ModelQueryBuilder $builder
714
    ): ModelQueryBuilder {
715 1
        return $builder;
716
    }
717
718
    /**
719
     * @param string            $relationshipName
720
     * @param ModelQueryBuilder $builder
721
     *
722
     * @return ModelQueryBuilder
723
     *
724
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
725
     */
726 2
    protected function builderCleanRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
727
        $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...
728
        ModelQueryBuilder $builder
729
    ): ModelQueryBuilder {
730 2
        return $builder;
731
    }
732
733
    /**
734
     * @param ModelQueryBuilder $builder
735
     *
736
     * @return ModelQueryBuilder
737
     */
738 4
    protected function builderOnDelete(ModelQueryBuilder $builder): ModelQueryBuilder
739
    {
740 4
        return $builder;
741
    }
742
743
    /**
744
     * @param PaginatedDataInterface|mixed|null $data
745
     *
746
     * @return void
747
     *
748
     * @SuppressWarnings(PHPMD.ElseExpression)
749
     *
750
     * @throws DBALException
751
     */
752 21
    private function loadRelationships($data): void
753
    {
754 21
        $isPaginated = $data instanceof PaginatedDataInterface;
755 21
        $hasData     = ($isPaginated === true && empty($data->getData()) === false) ||
756 21
            ($isPaginated === false && $data !== null);
757
758 21
        if ($hasData === true && $this->hasIncludes() === true) {
759 21
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemas());
760 21
            $modelsAtPath = $this->getFactory()->createTagStorage();
761
762
            // we gonna send these objects via function params so it is an equivalent for &array
763 21
            $classAtPath = new ArrayObject();
764 21
            $idsAtPath   = new ArrayObject();
765
766
            $registerModelAtRoot = function ($model) use ($modelStorage, $modelsAtPath, $idsAtPath): void {
767 21
                self::registerModelAtPath(
768 21
                    $model,
769 21
                    static::ROOT_PATH,
770 21
                    $this->getModelSchemas(),
771 21
                    $modelStorage,
772 21
                    $modelsAtPath,
773 21
                    $idsAtPath
774
                );
775 21
            };
776
777 21
            $model = null;
778 21
            if ($isPaginated === true) {
779 13
                foreach ($data->getData() as $model) {
780 13
                    $registerModelAtRoot($model);
781
                }
782
            } else {
783 8
                $model = $data;
784 8
                $registerModelAtRoot($model);
785
            }
786 21
            assert($model !== null);
787 21
            $classAtPath[static::ROOT_PATH] = get_class($model);
788
789 21
            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...
790 10
                $this->loadRelationshipsLayer(
791 10
                    $modelsAtPath,
792 10
                    $classAtPath,
793 10
                    $idsAtPath,
794 10
                    $modelStorage,
795 10
                    $parentPath,
796 10
                    $childPaths
797
                );
798
            }
799
        }
800
    }
801
802
    /**
803
     * A helper to remember all model related data. Helps to ensure we consistently handle models in CRUD.
804
     *
805
     * @param mixed                    $model
806
     * @param string                   $path
807
     * @param ModelSchemaInfoInterface $modelSchemas
808
     * @param ModelStorageInterface    $modelStorage
809
     * @param TagStorageInterface      $modelsAtPath
810
     * @param ArrayObject              $idsAtPath
811
     *
812
     * @return mixed
813
     */
814 21
    private static function registerModelAtPath(
815
        $model,
816
        string $path,
817
        ModelSchemaInfoInterface $modelSchemas,
818
        ModelStorageInterface $modelStorage,
819
        TagStorageInterface $modelsAtPath,
820
        ArrayObject $idsAtPath
821
    ) {
822 21
        $uniqueModel = $modelStorage->register($model);
823 21
        if ($uniqueModel !== null) {
824 21
            $modelsAtPath->register($uniqueModel, $path);
825 21
            $pkName             = $modelSchemas->getPrimaryKey(get_class($uniqueModel));
826 21
            $modelId            = $uniqueModel->{$pkName};
827 21
            $idsAtPath[$path][] = $modelId;
828
        }
829
830 21
        return $uniqueModel;
831
    }
832
833
    /**
834
     * @param iterable $paths (string[])
835
     *
836
     * @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...
837
     */
838 21
    private static function getPaths(iterable $paths): iterable
839
    {
840
        // The idea is to normalize paths. It means build all intermediate paths.
841
        // e.g. if only `a.b.c` path it given it will be normalized to `a`, `a.b` and `a.b.c`.
842
        // Path depths store depth of each path (e.g. 0 for root, 1 for `a`, 2 for `a.b` and etc).
843
        // It is needed for yielding them in correct order (from top level to bottom).
844 21
        $normalizedPaths = [];
845 21
        $pathsDepths     = [];
846 21
        foreach ($paths as $path) {
847 10
            assert(is_array($path) || $path instanceof Traversable);
848 10
            $parentDepth = 0;
849 10
            $tmpPath     = static::ROOT_PATH;
850 10
            foreach ($path as $pathPiece) {
851 10
                assert(is_string($pathPiece));
852 10
                $parent                    = $tmpPath;
853 10
                $tmpPath                   = empty($tmpPath) === true ?
854 10
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
855 10
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
856 10
                $pathsDepths[$parent]      = $parentDepth++;
857
            }
858
        }
859
860
        // Here we collect paths in form of parent => [list of children]
861
        // e.g. '' => ['a', 'c', 'b'], 'b' => ['bb', 'aa'] and etc
862 21
        $parentWithChildren = [];
863 21
        foreach ($normalizedPaths as $path => list ($parent, $childPath)) {
864 10
            $parentWithChildren[$parent][] = $childPath;
865
        }
866
867
        // And finally sort by path depth and yield parent with its children. Top level paths first then deeper ones.
868 21
        asort($pathsDepths, SORT_NUMERIC);
869 21
        foreach ($pathsDepths as $parent => $depth) {
870 10
            assert($depth !== null); // suppress unused
871 10
            $childPaths = $parentWithChildren[$parent];
872 10
            yield [$parent, $childPaths];
873
        }
874
    }
875
876
    /**
877
     * @inheritdoc
878
     *
879
     * @throws DBALException
880
     */
881 3
    public function createIndexBuilder(iterable $columns = null): QueryBuilder
882
    {
883 3
        return $this->createIndexModelBuilder($columns);
884
    }
885
886
    /**
887
     * @inheritdoc
888
     *
889
     * @throws DBALException
890
     */
891 4
    public function createDeleteBuilder(): QueryBuilder
892
    {
893 4
        return $this->createDeleteModelBuilder();
894
    }
895
896
    /**
897
     * @param iterable|null $columns
898
     *
899
     * @return ModelQueryBuilder
900
     *
901
     * @throws DBALException
902
     */
903 38
    protected function createIndexModelBuilder(iterable $columns = null): ModelQueryBuilder
904
    {
905 38
        $builder = $this->createBuilder($this->getModelClass());
906
907
        $this
908 38
            ->applyColumnMapper($builder);
909
910
        $builder
911 38
            ->selectModelColumns($columns)
912 38
            ->fromModelTable();
913
914
        $this
915 38
            ->applyAliasFilters($builder)
916 38
            ->applySorts($builder)
917 38
            ->applyRelationshipFiltersAndSorts($builder)
918 38
            ->applyPaging($builder);
919
920 38
        $result = $this->builderOnIndex($builder);
921
922 38
        $this->clearBuilderParameters();
923
924 38
        return $result;
925
    }
926
927
    /**
928
     * @return ModelQueryBuilder
929
     *
930
     * @throws DBALException
931
     */
932 4
    protected function createDeleteModelBuilder(): ModelQueryBuilder
933
    {
934
        $builder = $this
935 4
            ->createBuilder($this->getModelClass())
936 4
            ->deleteModels();
937
938 4
        $this->applyTableFilters($builder);
939
940 4
        $result = $this->builderOnDelete($builder);
941
942 4
        $this->clearBuilderParameters();
943
944 4
        return $result;
945
    }
946
947
    /**
948
     * @inheritdoc
949
     *
950
     * @throws DBALException
951
     */
952 17
    public function index(): PaginatedDataInterface
953
    {
954 17
        $builder = $this->createIndexModelBuilder();
955 17
        $data    = $this->fetchResources($builder, $builder->getModelClass());
956
957 17
        return $data;
958
    }
959
960
    /**
961
     * @inheritdoc
962
     *
963
     * @throws DBALException
964
     */
965 4
    public function indexIdentities(): array
966
    {
967 4
        $pkName  = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
968 4
        $builder = $this->createIndexModelBuilder([$pkName]);
0 ignored issues
show
Documentation introduced by
array($pkName) is of type array<integer,string,{"0":"string"}>, 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...
969
        /** @var Generator $data */
970 4
        $data   = $this->fetchColumn($builder, $builder->getModelClass(), $pkName);
971 4
        $result = iterator_to_array($data);
972
973 4
        return $result;
974
    }
975
976
    /**
977
     * @inheritdoc
978
     *
979
     * @throws DBALException
980
     */
981 13
    public function read(string $index)
982
    {
983 13
        $this->withIndexFilter($index);
984
985 13
        $builder = $this->createIndexModelBuilder();
986 13
        $data    = $this->fetchResource($builder, $builder->getModelClass());
987
988 13
        return $data;
989
    }
990
991
    /**
992
     * @inheritdoc
993
     *
994
     * @throws DBALException
995
     */
996 2
    public function count(): ?int
997
    {
998 2
        $result = $this->builderOnCount(
999 2
            $this->createCountBuilderFromBuilder($this->createIndexModelBuilder())
1000 2
        )->execute()->fetchColumn();
1001
1002 2
        return $result === false ? null : (int)$result;
1003
    }
1004
1005
    /**
1006
     * @param string        $relationshipName
1007
     * @param iterable|null $relationshipFilters
1008
     * @param iterable|null $relationshipSorts
1009
     * @param iterable|null $columns
1010
     *
1011
     * @return ModelQueryBuilder
1012
     *
1013
     * @throws DBALException
1014
     */
1015 7
    public function createReadRelationshipBuilder(
1016
        string $relationshipName,
1017
        iterable $relationshipFilters = null,
1018
        iterable $relationshipSorts = null,
1019
        iterable $columns = null
1020
    ): ModelQueryBuilder {
1021 7
        assert(
1022 7
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $relationshipName),
1023 7
            "Relationship `$relationshipName` do not exist in model `" . $this->getModelClass() . '`'
1024
        );
1025
1026
        // as we read data from a relationship our main table and model would be the table/model in the relationship
1027
        // so 'root' model(s) will be located in the reverse relationship.
1028
1029
        list ($targetModelClass, $reverseRelName) =
1030 7
            $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $relationshipName);
1031
1032
        $builder = $this
1033 7
            ->createBuilder($targetModelClass)
1034 7
            ->selectModelColumns($columns)
1035 7
            ->fromModelTable();
1036
1037
        // 'root' filters would be applied to the data in the reverse relationship ...
1038 7
        if ($this->hasFilters() === true) {
1039 7
            $filters = $this->getFilters();
1040 7
            $sorts   = $this->getSorts();
1041 7
            $joinCondition = $this->areFiltersWithAnd() === true ? ModelQueryBuilder::AND : ModelQueryBuilder::OR;
1042 7
            $builder->addRelationshipFiltersAndSorts($reverseRelName, $filters, $sorts, $joinCondition);
1043
        }
1044
        // ... and the input filters to actual data we select
1045 7
        if ($relationshipFilters !== null) {
1046 4
            $builder->addFiltersWithAndToAlias($relationshipFilters);
1047
        }
1048 7
        if ($relationshipSorts !== null) {
1049 3
            $builder->addSorts($relationshipSorts);
1050
        }
1051
1052 7
        $this->applyPaging($builder);
1053
1054
        // While joining tables we select distinct rows.
1055 7
        $builder->distinct();
1056
1057 7
        return $this->builderOnReadRelationship($builder);
1058
    }
1059
1060
    /**
1061
     * @inheritdoc
1062
     *
1063
     * @throws DBALException
1064
     */
1065 6
    public function indexRelationship(
1066
        string $name,
1067
        iterable $relationshipFilters = null,
1068
        iterable $relationshipSorts = null
1069
    ) {
1070 6
        assert(
1071 6
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $name),
1072 6
            "Relationship `$name` do not exist in model `" . $this->getModelClass() . '`'
1073
        );
1074
1075
        // depending on the relationship type we expect the result to be either single resource or a collection
1076 6
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1077 6
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
1078 6
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
1079
1080 6
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts);
1081
1082 6
        $modelClass = $builder->getModelClass();
1083 6
        $data       = $isExpectMany === true ?
1084 6
            $this->fetchResources($builder, $modelClass) : $this->fetchResource($builder, $modelClass);
1085
1086 6
        return $data;
1087
    }
1088
1089
    /**
1090
     * @inheritdoc
1091
     *
1092
     * @throws DBALException
1093
     */
1094 2
    public function indexRelationshipIdentities(
1095
        string $name,
1096
        iterable $relationshipFilters = null,
1097
        iterable $relationshipSorts = null
1098
    ): array {
1099 2
        assert(
1100 2
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $name),
1101 2
            "Relationship `$name` do not exist in model `" . $this->getModelClass() . '`'
1102
        );
1103
1104
        // depending on the relationship type we expect the result to be either single resource or a collection
1105 2
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1106 2
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
1107 2
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
1108 2
        if ($isExpectMany === false) {
1109 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1110
        }
1111
1112 1
        list ($targetModelClass) = $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $name);
1113 1
        $targetPk = $this->getModelSchemas()->getPrimaryKey($targetModelClass);
1114
1115 1
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts, [$targetPk]);
0 ignored issues
show
Documentation introduced by
array($targetPk) is of type array<integer,string,{"0":"string"}>, 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...
1116
1117 1
        $modelClass = $builder->getModelClass();
1118
        /** @var Generator $data */
1119 1
        $data   = $this->fetchColumn($builder, $modelClass, $targetPk);
1120 1
        $result = iterator_to_array($data);
1121
1122 1
        return $result;
1123
    }
1124
1125
    /**
1126
     * @inheritdoc
1127
     */
1128 3
    public function readRelationship(
1129
        string $index,
1130
        string $name,
1131
        iterable $relationshipFilters = null,
1132
        iterable $relationshipSorts = null
1133
    ) {
1134 3
        return $this->withIndexFilter($index)->indexRelationship($name, $relationshipFilters, $relationshipSorts);
1135
    }
1136
1137
    /**
1138
     * @inheritdoc
1139
     */
1140 1
    public function hasInRelationship(string $parentId, string $name, string $childId): bool
1141
    {
1142 1
        $parentPkName  = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1143 1
        $parentFilters = [$parentPkName => [FilterParameterInterface::OPERATION_EQUALS => [$parentId]]];
1144 1
        list($childClass) = $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $name);
1145 1
        $childPkName  = $this->getModelSchemas()->getPrimaryKey($childClass);
1146 1
        $childFilters = [$childPkName => [FilterParameterInterface::OPERATION_EQUALS => [$childId]]];
1147
1148
        $data = $this
1149 1
            ->clearBuilderParameters()
1150 1
            ->clearFetchParameters()
1151 1
            ->withFilters($parentFilters)
0 ignored issues
show
Documentation introduced by
$parentFilters is of type array<string,array<strin...tring,{"0":"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...
1152 1
            ->indexRelationship($name, $childFilters);
0 ignored issues
show
Documentation introduced by
$childFilters is of type array<string,array<strin...tring,{"0":"string"}>>>, 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...
1153
1154 1
        $has = empty($data->getData()) === false;
1155
1156 1
        return $has;
1157
    }
1158
1159
    /**
1160
     * @inheritdoc
1161
     *
1162
     * @throws DBALException
1163
     */
1164 1
    public function delete(): int
1165
    {
1166 1
        $deleted = $this->createDeleteBuilder()->execute();
1167
1168 1
        $this->clearFetchParameters();
1169
1170 1
        return (int)$deleted;
1171
    }
1172
1173
    /**
1174
     * @inheritdoc
1175
     *
1176
     * @throws DBALException
1177
     */
1178 4
    public function remove(string $index): bool
1179
    {
1180 4
        $this->withIndexFilter($index);
1181
1182 4
        $deleted = $this->createDeleteBuilder()->execute();
1183
1184 3
        $this->clearFetchParameters();
1185
1186 3
        return (int)$deleted > 0;
1187
    }
1188
1189
    /**
1190
     * @inheritdoc
1191
     *
1192
     * @throws DBALException
1193
     *
1194
     * @SuppressWarnings(PHPMD.StaticAccess)
1195
     */
1196 5
    public function create(?string $index, array $attributes, array $toMany): string
1197
    {
1198 5
        $allowedChanges = $this->filterAttributesOnCreate($index, $attributes);
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...
1199
        $saveMain       = $this
1200 5
            ->createBuilder($this->getModelClass())
1201 5
            ->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...
1202 5
        $saveMain       = $this->builderSaveResourceOnCreate($saveMain);
1203 5
        $saveMain->getSQL(); // prepare
1204
1205 5
        $this->clearBuilderParameters()->clearFetchParameters();
1206
1207
        $this->inTransaction(function () use ($saveMain, $toMany, &$index) {
1208 5
            $saveMain->execute();
1209
1210
            // if no index given will use last insert ID as index
1211 4
            $connection = $saveMain->getConnection();
1212 4
            $index !== null ?: $index = $connection->lastInsertId();
1213
1214 4
            $builderHook = Closure::fromCallable([$this, 'builderSaveRelationshipOnCreate']);
1215 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1216 2
                $this->addInToManyRelationship($connection, $index, $relationshipName, $secondaryIds, $builderHook);
1217
            }
1218 5
        });
1219
1220 4
        return $index;
1221
    }
1222
1223
    /**
1224
     * @inheritdoc
1225
     *
1226
     * @throws DBALException
1227
     *
1228
     * @SuppressWarnings(PHPMD.StaticAccess)
1229
     */
1230 5
    public function update(string $index, array $attributes, array $toMany): int
1231
    {
1232 5
        $updated        = 0;
1233 5
        $pkName         = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1234
        $filters        = [
1235
            $pkName => [
1236 5
                FilterParameterInterface::OPERATION_EQUALS => [$index],
1237
            ],
1238
        ];
1239 5
        $allowedChanges = $this->filterAttributesOnUpdate($attributes);
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...
1240
        $saveMain       = $this
1241 5
            ->createBuilder($this->getModelClass())
1242 5
            ->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...
1243 5
            ->addFiltersWithAndToTable($filters);
0 ignored issues
show
Documentation introduced by
$filters is of type array<string,array<strin...tring,{"0":"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...
1244 5
        $saveMain       = $this->builderSaveResourceOnUpdate($saveMain);
1245 5
        $saveMain->getSQL(); // prepare
1246
1247 5
        $this->clearBuilderParameters()->clearFetchParameters();
1248
1249
        $this->inTransaction(function () use ($saveMain, $toMany, $index, &$updated) {
1250 5
            $updated = $saveMain->execute();
1251
1252 4
            $builderHook = Closure::fromCallable([$this, 'builderSaveRelationshipOnUpdate']);
1253 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1254 2
                $connection = $saveMain->getConnection();
1255
1256
                // clear existing
1257 2
                $this->builderCleanRelationshipOnUpdate(
1258 2
                    $relationshipName,
1259
                    $this
1260 2
                        ->createBuilderFromConnection($this->getConnection(), $this->getModelClass())
1261 2
                        ->clearToManyRelationship($relationshipName, $index)
1262 2
                )->execute();
1263
1264
                // add new ones
1265 2
                $updated   += $this->addInToManyRelationship(
1266 2
                    $connection,
1267 2
                    $index,
1268 2
                    $relationshipName,
1269 2
                    $secondaryIds,
1270 2
                    $builderHook
1271
                );
1272
            }
1273 5
        });
1274
1275 4
        return (int)$updated;
1276
    }
1277
1278
    /**
1279
     * @param string   $parentId
1280
     * @param string   $name
1281
     * @param iterable $childIds
1282
     *
1283
     * @return int
1284
     *
1285
     * @throws DBALException
1286
     *
1287
     * @SuppressWarnings(PHPMD.StaticAccess)
1288
     */
1289 1
    public function createInBelongsToManyRelationship(string $parentId, string $name, iterable $childIds): int
1290
    {
1291
        // Check that relationship is `BelongsToMany`
1292
        assert(call_user_func(function () use ($name): bool {
1293 1
            $relType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1294 1
            $errMsg  = "Relationship `$name` of class `" . $this->getModelClass() .
1295 1
                '` either is not `belongsToMany` or do not exist in the class.';
1296 1
            $isOk = $relType === RelationshipTypes::BELONGS_TO_MANY;
1297
1298 1
            assert($isOk, $errMsg);
1299
1300 1
            return $isOk;
1301 1
        }));
1302
1303 1
        $builderHook = Closure::fromCallable([$this, 'builderOnCreateInBelongsToManyRelationship']);
1304
1305 1
        return $this->addInToManyRelationship($this->getConnection(), $parentId, $name, $childIds, $builderHook);
1306
    }
1307
1308
    /**
1309
     * @inheritdoc
1310
     *
1311
     * @throws DBALException
1312
     */
1313 1
    public function removeInBelongsToManyRelationship(string $parentId, string $name, iterable $childIds): int
1314
    {
1315
        // Check that relationship is `BelongsToMany`
1316
        assert(call_user_func(function () use ($name): bool {
1317 1
            $relType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1318 1
            $errMsg  = "Relationship `$name` of class `" . $this->getModelClass() .
1319 1
                '` either is not `belongsToMany` or do not exist in the class.';
1320 1
            $isOk = $relType === RelationshipTypes::BELONGS_TO_MANY;
1321
1322 1
            assert($isOk, $errMsg);
1323
1324 1
            return $isOk;
1325 1
        }));
1326
1327 1
        return $this->removeInToManyRelationship($this->getConnection(), $parentId, $name, $childIds);
1328
    }
1329
1330
    /**
1331
     * @return FactoryInterface
1332
     */
1333 52
    protected function getFactory(): FactoryInterface
1334
    {
1335 52
        return $this->factory;
1336
    }
1337
1338
    /**
1339
     * @return string
1340
     */
1341 53
    protected function getModelClass(): string
1342
    {
1343 53
        return $this->modelClass;
1344
    }
1345
1346
    /**
1347
     * @return ModelSchemaInfoInterface
1348
     */
1349 53
    protected function getModelSchemas(): ModelSchemaInfoInterface
1350
    {
1351 53
        return $this->modelSchemas;
1352
    }
1353
1354
    /**
1355
     * @return RelationshipPaginationStrategyInterface
1356
     */
1357 8
    protected function getRelationshipPagingStrategy(): RelationshipPaginationStrategyInterface
1358
    {
1359 8
        return $this->relPagingStrategy;
1360
    }
1361
1362
    /**
1363
     * @param Closure $closure
1364
     *
1365
     * @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...
1366
     *
1367
     * @throws DBALException
1368
     */
1369 10
    public function inTransaction(Closure $closure): void
1370
    {
1371 10
        $connection = $this->getConnection();
1372 10
        $connection->beginTransaction();
1373
        try {
1374 10
            $isOk = ($closure() === false ? null : true);
1375 8
        } finally {
1376 10
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
1377
        }
1378
    }
1379
1380
    /**
1381
     * @inheritdoc
1382
     *
1383
     * @throws DBALException
1384
     */
1385 22
    public function fetchResources(QueryBuilder $builder, string $modelClass): PaginatedDataInterface
1386
    {
1387 22
        $data = $this->fetchPaginatedResourcesWithoutRelationships($builder, $modelClass);
1388
1389 22
        if ($this->hasIncludes() === true) {
1390 13
            $this->loadRelationships($data);
1391 13
            $this->clearFetchParameters();
1392
        }
1393
1394 22
        return $data;
1395
    }
1396
1397
    /**
1398
     * @inheritdoc
1399
     *
1400
     * @throws DBALException
1401
     */
1402 14
    public function fetchResource(QueryBuilder $builder, string $modelClass)
1403
    {
1404 14
        $data = $this->fetchResourceWithoutRelationships($builder, $modelClass);
1405
1406 14
        if ($this->hasIncludes() === true) {
1407 8
            $this->loadRelationships($data);
1408 8
            $this->clearFetchParameters();
1409
        }
1410
1411 14
        return $data;
1412
    }
1413
1414
    /**
1415
     * @inheritdoc
1416
     *
1417
     * @throws DBALException
1418
     *
1419
     * @SuppressWarnings(PHPMD.ElseExpression)
1420
     */
1421 2
    public function fetchRow(QueryBuilder $builder, string $modelClass): ?array
1422
    {
1423 2
        $model = null;
1424
1425 2
        $statement = $builder->execute();
1426 2
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1427
1428 2
        if (($attributes = $statement->fetch()) !== false) {
1429 2
            if ($this->isFetchTyped() === true) {
1430 1
                $platform  = $builder->getConnection()->getDatabasePlatform();
1431 1
                $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1432 1
                $model     = $this->readRowFromAssoc($attributes, $typeNames, $platform);
1433
            } else {
1434 1
                $model = $attributes;
1435
            }
1436
        }
1437
1438 2
        $this->clearFetchParameters();
1439
1440 2
        return $model;
1441
    }
1442
1443
    /**
1444
     * @inheritdoc
1445
     *
1446
     * @throws DBALException
1447
     *
1448
     * @SuppressWarnings(PHPMD.StaticAccess)
1449
     * @SuppressWarnings(PHPMD.ElseExpression)
1450
     */
1451 5
    public function fetchColumn(QueryBuilder $builder, string $modelClass, string $columnName): iterable
1452
    {
1453 5
        $statement = $builder->execute();
1454 5
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1455
1456 5
        if ($this->isFetchTyped() === true) {
1457 4
            $platform = $builder->getConnection()->getDatabasePlatform();
1458 4
            $typeName = $this->getModelSchemas()->getAttributeTypes($modelClass)[$columnName];
1459 4
            $type     = Type::getType($typeName);
1460 4
            while (($attributes = $statement->fetch()) !== false) {
1461 4
                $value     = $attributes[$columnName];
1462 4
                $converted = $type->convertToPHPValue($value, $platform);
1463
1464 4
                yield $converted;
1465
            }
1466
        } else {
1467 1
            while (($attributes = $statement->fetch()) !== false) {
1468 1
                $value = $attributes[$columnName];
1469
1470 1
                yield $value;
1471
            }
1472
        }
1473
1474 5
        $this->clearFetchParameters();
1475
    }
1476
1477
    /**
1478
     * @param QueryBuilder $builder
1479
     *
1480
     * @return ModelQueryBuilder
1481
     */
1482 2
    protected function createCountBuilderFromBuilder(QueryBuilder $builder): ModelQueryBuilder
1483
    {
1484 2
        $countBuilder = $this->createBuilder($this->getModelClass());
1485 2
        $countBuilder->setParameters($builder->getParameters());
1486 2
        $countBuilder->select('COUNT(*)')->from('(' . $builder->getSQL() . ') AS RESULT');
1487
1488 2
        return $countBuilder;
1489
    }
1490
1491
    /**
1492
     * @param Connection $connection
1493
     * @param string     $primaryIdentity
1494
     * @param string     $name
1495
     * @param iterable   $secondaryIdentities
1496
     * @param Closure    $builderHook
1497
     *
1498
     * @return int
1499
     *
1500
     * @throws DBALException
1501
     */
1502 5
    private function addInToManyRelationship(
1503
        Connection $connection,
1504
        string $primaryIdentity,
1505
        string $name,
1506
        iterable $secondaryIdentities,
1507
        Closure $builderHook
1508
    ): int {
1509 5
        $inserted = 0;
1510
1511 5
        $secondaryIdBindName = ':secondaryId';
1512
        $saveToMany          = $this
1513 5
            ->createBuilderFromConnection($connection, $this->getModelClass())
1514 5
            ->prepareCreateInToManyRelationship($name, $primaryIdentity, $secondaryIdBindName);
1515
1516 5
        $saveToMany = call_user_func($builderHook, $name, $saveToMany);
1517
1518 5
        foreach ($secondaryIdentities as $secondaryId) {
1519
            try {
1520 5
                $inserted += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1521 1
            } /** @noinspection PhpRedundantCatchClauseInspection */ catch (UcvException $exception) {
1522
                // Spec: If all of the specified resources can be added to, or are already present in,
1523
                // the relationship then the server MUST return a successful response.
1524
                //
1525
                // Currently DBAL cannot do insert or update in the same request.
1526
                // https://github.com/doctrine/dbal/issues/1320
1527 5
                continue;
1528
            }
1529
        }
1530
1531 5
        return $inserted;
1532
    }
1533
1534
    /**
1535
     * @param Connection $connection
1536
     * @param string     $primaryIdentity
1537
     * @param string     $name
1538
     * @param iterable   $secondaryIdentities
1539
     *
1540
     * @return int
1541
     *
1542
     * @throws DBALException
1543
     */
1544 1
    private function removeInToManyRelationship(
1545
        Connection $connection,
1546
        string $primaryIdentity,
1547
        string $name,
1548
        iterable $secondaryIdentities
1549
    ): int {
1550 1
        $removeToMany = $this->builderOnRemoveInBelongsToManyRelationship(
1551 1
            $name,
1552
            $this
1553 1
                ->createBuilderFromConnection($connection, $this->getModelClass())
1554 1
                ->prepareDeleteInToManyRelationship($name, $primaryIdentity, $secondaryIdentities)
1555
        );
1556 1
        $removed = $removeToMany->execute();
1557
1558 1
        return $removed;
1559
    }
1560
1561
    /**
1562
     * @param QueryBuilder $builder
1563
     * @param string       $modelClass
1564
     *
1565
     * @return mixed|null
1566
     *
1567
     * @throws DBALException
1568
     *
1569
     * @SuppressWarnings(PHPMD.ElseExpression)
1570
     */
1571 14
    private function fetchResourceWithoutRelationships(QueryBuilder $builder, string $modelClass)
1572
    {
1573 14
        $model     = null;
1574 14
        $statement = $builder->execute();
1575
1576 14
        if ($this->isFetchTyped() === true) {
1577 12
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1578 12
            if (($attributes = $statement->fetch()) !== false) {
1579 12
                $platform  = $builder->getConnection()->getDatabasePlatform();
1580 12
                $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1581 12
                $model     = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1582
            }
1583
        } else {
1584 2
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1585 2
            if (($fetched = $statement->fetch()) !== false) {
1586 2
                $model = $fetched;
1587
            }
1588
        }
1589
1590 14
        return $model;
1591
    }
1592
1593
    /**
1594
     * @param QueryBuilder $builder
1595
     * @param string       $modelClass
1596
     * @param string       $keyColumnName
1597
     *
1598
     * @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...
1599
     *
1600
     * @throws DBALException
1601
     *
1602
     * @SuppressWarnings(PHPMD.ElseExpression)
1603
     */
1604 9
    private function fetchResourcesWithoutRelationships(
1605
        QueryBuilder $builder,
1606
        string $modelClass,
1607
        string $keyColumnName
1608
    ): iterable {
1609 9
        $statement = $builder->execute();
1610
1611 9
        if ($this->isFetchTyped() === true) {
1612 8
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1613 8
            $platform  = $builder->getConnection()->getDatabasePlatform();
1614 8
            $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1615 8
            while (($attributes = $statement->fetch()) !== false) {
1616 6
                $model = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1617 6
                yield $model->{$keyColumnName} => $model;
1618
            }
1619
        } else {
1620 1
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1621 1
            while (($model = $statement->fetch()) !== false) {
1622 1
                yield $model->{$keyColumnName} => $model;
1623
            }
1624
        }
1625
    }
1626
1627
    /**
1628
     * @param QueryBuilder $builder
1629
     * @param string       $modelClass
1630
     *
1631
     * @return PaginatedDataInterface
1632
     *
1633
     * @throws DBALException
1634
     */
1635 27
    private function fetchPaginatedResourcesWithoutRelationships(
1636
        QueryBuilder $builder,
1637
        string $modelClass
1638
    ): PaginatedDataInterface {
1639 27
        list($models, $hasMore, $limit, $offset) = $this->fetchResourceCollection($builder, $modelClass);
1640
1641 27
        $data = $this->getFactory()
1642 27
            ->createPaginatedData($models)
1643 27
            ->markAsCollection()
1644 27
            ->setOffset($offset)
1645 27
            ->setLimit($limit);
1646
1647 27
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
1648
1649 27
        return $data;
1650
    }
1651
1652
    /**
1653
     * @param QueryBuilder $builder
1654
     * @param string       $modelClass
1655
     *
1656
     * @return array
1657
     *
1658
     * @throws DBALException
1659
     *
1660
     * @SuppressWarnings(PHPMD.ElseExpression)
1661
     */
1662 27
    private function fetchResourceCollection(QueryBuilder $builder, string $modelClass): array
1663
    {
1664 27
        $statement = $builder->execute();
1665
1666 27
        $models           = [];
1667 27
        $counter          = 0;
1668 27
        $hasMoreThanLimit = false;
1669 27
        $limit            = $builder->getMaxResults() !== null ? $builder->getMaxResults() - 1 : null;
1670
1671 27
        if ($this->isFetchTyped() === true) {
1672 26
            $platform  = $builder->getConnection()->getDatabasePlatform();
1673 26
            $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1674 26
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1675 26
            while (($attributes = $statement->fetch()) !== false) {
1676 25
                $counter++;
1677 25
                if ($limit !== null && $counter > $limit) {
1678 6
                    $hasMoreThanLimit = true;
1679 6
                    break;
1680
                }
1681 25
                $models[] = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1682
            }
1683
        } else {
1684 1
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1685 1
            while (($fetched = $statement->fetch()) !== false) {
1686 1
                $counter++;
1687 1
                if ($limit !== null && $counter > $limit) {
1688 1
                    $hasMoreThanLimit = true;
1689 1
                    break;
1690
                }
1691 1
                $models[] = $fetched;
1692
            }
1693
        }
1694
1695 27
        return [$models, $hasMoreThanLimit, $limit, $builder->getFirstResult()];
1696
    }
1697
1698
    /**
1699
     * @param null|string $index
1700
     * @param iterable    $attributes
1701
     *
1702
     * @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...
1703
     */
1704 5
    protected function filterAttributesOnCreate(?string $index, iterable $attributes): iterable
1705
    {
1706 5
        if ($index !== null) {
1707 1
            $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1708 1
            yield $pkName => $index;
1709
        }
1710
1711 5
        $knownAttrAndTypes = $this->getModelSchemas()->getAttributeTypes($this->getModelClass());
1712 5
        foreach ($attributes as $attribute => $value) {
1713 5
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1714 5
                yield $attribute => $value;
1715
            }
1716
        }
1717
    }
1718
1719
    /**
1720
     * @param iterable $attributes
1721
     *
1722
     * @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...
1723
     */
1724 5
    protected function filterAttributesOnUpdate(iterable $attributes): iterable
1725
    {
1726 5
        $knownAttrAndTypes = $this->getModelSchemas()->getAttributeTypes($this->getModelClass());
1727 5
        foreach ($attributes as $attribute => $value) {
1728 5
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1729 5
                yield $attribute => $value;
1730
            }
1731
        }
1732
    }
1733
1734
    /**
1735
     * @param TagStorageInterface   $modelsAtPath
1736
     * @param ArrayObject           $classAtPath
1737
     * @param ArrayObject           $idsAtPath
1738
     * @param ModelStorageInterface $deDup
1739
     * @param string                $parentsPath
1740
     * @param array                 $childRelationships
1741
     *
1742
     * @return void
1743
     *
1744
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1745
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
1746
     *
1747
     * @throws DBALException
1748
     */
1749 10
    private function loadRelationshipsLayer(
1750
        TagStorageInterface $modelsAtPath,
1751
        ArrayObject $classAtPath,
1752
        ArrayObject $idsAtPath,
1753
        ModelStorageInterface $deDup,
1754
        string $parentsPath,
1755
        array $childRelationships
1756
    ): void {
1757 10
        $rootClass   = $classAtPath[static::ROOT_PATH];
1758 10
        $parentClass = $classAtPath[$parentsPath];
1759 10
        $parents     = $modelsAtPath->get($parentsPath);
1760
1761
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
1762
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
1763
        // $models.
1764
1765 10
        $pkName = $this->getModelSchemas()->getPrimaryKey($parentClass);
1766
1767
        $registerModelAtPath = function ($model, string $path) use ($deDup, $modelsAtPath, $idsAtPath) {
1768 8
            return self::registerModelAtPath(
1769 8
                $model,
1770 8
                $path,
1771 8
                $this->getModelSchemas(),
1772 8
                $deDup,
1773 8
                $modelsAtPath,
1774 8
                $idsAtPath
1775
            );
1776 10
        };
1777
1778 10
        foreach ($childRelationships as $name) {
1779 10
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
1780
1781 10
            $relationshipType = $this->getModelSchemas()->getRelationshipType($parentClass, $name);
1782
            list ($targetModelClass, $reverseRelName) =
1783 10
                $this->getModelSchemas()->getReverseRelationship($parentClass, $name);
1784
1785
            $builder = $this
1786 10
                ->createBuilder($targetModelClass)
1787 10
                ->selectModelColumns()
1788 10
                ->fromModelTable();
1789
1790 10
            $classAtPath[$childrenPath] = $targetModelClass;
1791
1792
            switch ($relationshipType) {
1793 10
                case RelationshipTypes::BELONGS_TO:
1794
                    // some paths might not have any records in the database
1795 9
                    $areParentsLoaded = $idsAtPath->offsetExists($parentsPath);
1796 9
                    if ($areParentsLoaded === false) {
1797 1
                        break;
1798
                    }
1799
                    // for 'belongsTo' relationship all resources could be read at once.
1800 9
                    $parentIds            = $idsAtPath[$parentsPath];
1801 9
                    $clonedBuilder        = (clone $builder)->addRelationshipFiltersAndSorts(
1802 9
                        $reverseRelName,
1803 9
                        [$pkName => [FilterParameterInterface::OPERATION_IN => $parentIds]],
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...TION_IN => $parentIds)) is of type array<string,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...
1804 9
                        null
1805
                    );
1806 9
                    $unregisteredChildren = $this->fetchResourcesWithoutRelationships(
1807 9
                        $clonedBuilder,
1808 9
                        $clonedBuilder->getModelClass(),
1809 9
                        $this->getModelSchemas()->getPrimaryKey($clonedBuilder->getModelClass())
1810
                    );
1811 9
                    $children             = [];
1812 9
                    foreach ($unregisteredChildren as $index => $unregisteredChild) {
1813 7
                        $children[$index] = $registerModelAtPath($unregisteredChild, $childrenPath);
1814
                    }
1815 9
                    $fkNameToChild = $this->getModelSchemas()->getForeignKey($parentClass, $name);
1816 9
                    foreach ($parents as $parent) {
1817 9
                        $fkToChild       = $parent->{$fkNameToChild};
1818 9
                        $parent->{$name} = $children[$fkToChild] ?? null;
1819
                    }
1820 9
                    break;
1821 8
                case RelationshipTypes::HAS_MANY:
1822 6
                case RelationshipTypes::BELONGS_TO_MANY:
1823
                    // unfortunately we have paging limits for 'many' relationship thus we have read such
1824
                    // relationships for each 'parent' individually
1825 8
                    list ($queryOffset, $queryLimit) = $this->getRelationshipPagingStrategy()
1826 8
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
1827 8
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
1828 8
                    foreach ($parents as $parent) {
1829 8
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSorts(
1830 8
                            $reverseRelName,
1831 8
                            [$pkName => [FilterParameterInterface::OPERATION_EQUALS => [$parent->{$pkName}]]],
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...y($parent->{$pkName}))) is of type array<string,array<strin...<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...
1832 8
                            []
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...
1833
                        );
1834 8
                        $children      = $this->fetchPaginatedResourcesWithoutRelationships(
1835 8
                            $clonedBuilder,
1836 8
                            $clonedBuilder->getModelClass()
1837
                        );
1838
1839 8
                        $deDupedChildren = [];
1840 8
                        foreach ($children->getData() as $child) {
1841 7
                            $deDupedChildren[] = $registerModelAtPath($child, $childrenPath);
1842
                        }
1843
1844 8
                        $paginated = $this->getFactory()
1845 8
                            ->createPaginatedData($deDupedChildren)
1846 8
                            ->markAsCollection()
1847 8
                            ->setOffset($children->getOffset())
1848 8
                            ->setLimit($children->getLimit());
1849 8
                        $children->hasMoreItems() === true ?
1850 8
                            $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
1851
1852 8
                        $parent->{$name} = $paginated;
1853
                    }
1854 10
                    break;
1855
            }
1856
        }
1857
    }
1858
1859
    /**
1860
     * @param string $message
1861
     *
1862
     * @return string
1863
     *
1864
     * @throws ContainerExceptionInterface
1865
     * @throws NotFoundExceptionInterface
1866
     */
1867 2
    private function getMessage(string $message): string
1868
    {
1869
        /** @var FormatterFactoryInterface $factory */
1870 2
        $factory   = $this->getContainer()->get(FormatterFactoryInterface::class);
1871 2
        $formatter = $factory->createFormatter(Messages::NAMESPACE_NAME);
1872 2
        $result    = $formatter->formatMessage($message);
1873
1874 2
        return $result;
1875
    }
1876
1877
    /**
1878
     * @param string           $class
1879
     * @param array            $attributes
1880
     * @param Type[]           $typeNames
1881
     * @param AbstractPlatform $platform
1882
     *
1883
     * @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...
1884
     *
1885
     * @SuppressWarnings(PHPMD.StaticAccess)
1886
     *
1887
     * @throws DBALException
1888
     */
1889 34
    private function readResourceFromAssoc(
1890
        string $class,
1891
        array $attributes,
1892
        array $typeNames,
1893
        AbstractPlatform $platform
1894
    ) {
1895 34
        $instance = new $class();
1896 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...
1897 34
            $instance->{$name} = $value;
1898
        }
1899
1900 34
        return $instance;
1901
    }
1902
1903
    /**
1904
     * @param array            $attributes
1905
     * @param Type[]           $typeNames
1906
     * @param AbstractPlatform $platform
1907
     *
1908
     * @return array
1909
     *
1910
     * @SuppressWarnings(PHPMD.StaticAccess)
1911
     *
1912
     * @throws DBALException
1913
     */
1914 1
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
1915
    {
1916 1
        $row = [];
1917 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...
1918 1
            $row[$name] = $value;
1919
        }
1920
1921 1
        return $row;
1922
    }
1923
1924
    /**
1925
     * @param iterable         $attributes
1926
     * @param array            $typeNames
1927
     * @param AbstractPlatform $platform
1928
     *
1929
     * @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...
1930
     *
1931
     * @throws DBALException
1932
     */
1933 35
    private function readTypedAttributes(iterable $attributes, array $typeNames, AbstractPlatform $platform): iterable
1934
    {
1935 35
        foreach ($attributes as $name => $value) {
1936 35
            yield $name => (array_key_exists($name, $typeNames) === true ?
1937 35
                Type::getType($typeNames[$name])->convertToPHPValue($value, $platform) : $value);
1938
        }
1939
    }
1940
}
1941