Completed
Push — develop ( dcfc04...3d10a6 )
by Neomerx
04:33
created

Crud::addInToManyRelationship()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

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