Completed
Push — develop ( 3822de...d029a9 )
by Neomerx
06:40
created

Crud::builderOnRemoveInBelongsToManyRelationship()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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 66
    public function __construct(ContainerInterface $container, string $modelClass)
153
    {
154 66
        $this->setContainer($container);
155
156 66
        $this->modelClass        = $modelClass;
157 66
        $this->factory           = $this->getContainer()->get(FactoryInterface::class);
158 66
        $this->modelSchemas      = $this->getContainer()->get(ModelSchemaInfoInterface::class);
159 66
        $this->relPagingStrategy = $this->getContainer()->get(RelationshipPaginationStrategyInterface::class);
160 66
        $this->connection        = $this->getContainer()->get(Connection::class);
161
162 66
        $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 43
    public function withFilters(iterable $filterParameters): CrudInterface
181
    {
182 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...
183
184 43
        return $this;
185
    }
186
187
    /**
188
     * @inheritdoc
189
     */
190 24
    public function withIndexFilter($index): CrudInterface
191
    {
192 24
        if (is_int($index) === false && is_string($index) === false) {
193 3
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
194
        }
195
196 21
        $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
197 21
        $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 21
                FilterParameterInterface::OPERATION_EQUALS => [$index],
200
            ],
201
        ]);
202
203 21
        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 7
    public function withRelationshipFilters(string $name, iterable $filters): CrudInterface
239
    {
240 7
        assert($this->getModelSchemas()->hasRelationship($this->getModelClass(), $name) === true);
241
242 7
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__FILTERS] = $filters;
243
244 7
        return $this;
245
    }
246
247
    /**
248
     * @inheritdoc
249
     */
250 1
    public function withRelationshipSorts(string $name, iterable $sorts): CrudInterface
251
    {
252 1
        assert($this->getModelSchemas()->hasRelationship($this->getModelClass(), $name) === true);
253
254 1
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__SORTS] = $sorts;
255
256 1
        return $this;
257
    }
258
259
    /**
260
     * @inheritdoc
261
     */
262 16
    public function combineWithAnd(): CrudInterface
263
    {
264 16
        $this->areFiltersWithAnd = true;
265
266 16
        return $this;
267
    }
268
269
    /**
270
     * @inheritdoc
271
     */
272 2
    public function combineWithOr(): CrudInterface
273
    {
274 2
        $this->areFiltersWithAnd = false;
275
276 2
        return $this;
277
    }
278
279
    /**
280
     * @return bool
281
     */
282 38
    private function hasColumnMapper(): bool
283
    {
284 38
        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 47
    private function hasFilters(): bool
299
    {
300 47
        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 36
    private function getFilters(): iterable
307
    {
308 36
        return $this->filterParameters;
309
    }
310
311
    /**
312
     * @return bool
313
     */
314 36
    private function areFiltersWithAnd(): bool
315
    {
316 36
        return $this->areFiltersWithAnd;
317
    }
318
319
    /**
320
     * @inheritdoc
321
     */
322 17
    public function withSorts(iterable $sortingParameters): CrudInterface
323
    {
324 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...
325
326 17
        return $this;
327
    }
328
329
    /**
330
     * @return bool
331
     */
332 38
    private function hasSorts(): bool
333
    {
334 38
        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 11
    private function getSorts(): ?iterable
341
    {
342 11
        return $this->sortingParameters;
343
    }
344
345
    /**
346
     * @inheritdoc
347
     */
348 21
    public function withIncludes(iterable $includePaths): CrudInterface
349
    {
350 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...
351
352 21
        return $this;
353
    }
354
355
    /**
356
     * @return bool
357
     */
358 36
    private function hasIncludes(): bool
359
    {
360 36
        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 21
    private function getIncludes(): iterable
367
    {
368 21
        return $this->includePaths;
369
    }
370
371
    /**
372
     * @inheritdoc
373
     */
374 20
    public function withPaging(int $offset, int $limit): CrudInterface
375
    {
376 20
        $this->pagingOffset = $offset;
377 20
        $this->pagingLimit  = $limit;
378
379 20
        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 66
    public function shouldBeTyped(): self
397
    {
398 66
        $this->isFetchTyped = true;
399
400 66
        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 45
    private function hasPaging(): bool
417
    {
418 45
        return $this->pagingOffset !== null && $this->pagingLimit !== null;
419
    }
420
421
    /**
422
     * @return int
423
     */
424 18
    private function getPagingOffset(): int
425
    {
426 18
        return $this->pagingOffset;
427
    }
428
429
    /**
430
     * @return int
431
     */
432 18
    private function getPagingLimit(): int
433
    {
434 18
        return $this->pagingLimit;
435
    }
436
437
    /**
438
     * @return bool
439
     */
440 43
    private function isFetchTyped(): bool
441
    {
442 43
        return $this->isFetchTyped;
443
    }
444
445
    /**
446
     * @return Connection
447
     */
448 52
    protected function getConnection(): Connection
449
    {
450 52
        return $this->connection;
451
    }
452
453
    /**
454
     * @param string $modelClass
455
     *
456
     * @return ModelQueryBuilder
457
     */
458 50
    protected function createBuilder(string $modelClass): ModelQueryBuilder
459
    {
460 50
        return $this->createBuilderFromConnection($this->getConnection(), $modelClass);
461
    }
462
463
    /**
464
     * @param Connection $connection
465
     * @param string     $modelClass
466
     *
467
     * @return ModelQueryBuilder
468
     */
469 52
    private function createBuilderFromConnection(Connection $connection, string $modelClass): ModelQueryBuilder
470
    {
471 52
        return $this->getFactory()->createModelQueryBuilder($connection, $modelClass, $this->getModelSchemas());
472
    }
473
474
    /**
475
     * @param ModelQueryBuilder $builder
476
     *
477
     * @return Crud
478
     */
479 38
    protected function applyColumnMapper(ModelQueryBuilder $builder): self
480
    {
481 38
        if ($this->hasColumnMapper() === true) {
482 1
            $builder->setColumnToDatabaseMapper($this->getColumnMapper());
483
        }
484
485 38
        return $this;
486
    }
487
488
    /**
489
     * @param ModelQueryBuilder $builder
490
     *
491
     * @return Crud
492
     *
493
     * @throws DBALException
494
     */
495 38
    protected function applyAliasFilters(ModelQueryBuilder $builder): self
496
    {
497 38
        if ($this->hasFilters() === true) {
498 27
            $filters = $this->getFilters();
499 27
            $this->areFiltersWithAnd() === true ?
500 27
                $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 38
        return $this;
504
    }
505
506
    /**
507
     * @param ModelQueryBuilder $builder
508
     *
509
     * @return self
510
     *
511
     * @throws DBALException
512
     */
513 4
    protected function applyTableFilters(ModelQueryBuilder $builder): self
514
    {
515 4
        if ($this->hasFilters() === true) {
516 4
            $filters = $this->getFilters();
517 4
            $this->areFiltersWithAnd() === true ?
518 4
                $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 4
        return $this;
522
    }
523
524
    /**
525
     * @param ModelQueryBuilder $builder
526
     *
527
     * @return self
528
     *
529
     * @throws DBALException
530
     */
531 38
    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 38
        $distinctApplied = false;
535
536 38
        foreach ($this->relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
537 7
            assert(is_string($relationshipName) === true && is_array($filtersAndSorts) === true);
538 7
            $builder->addRelationshipFiltersAndSorts(
539 7
                $relationshipName,
540 7
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__FILTERS] ?? [],
541 7
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__SORTS] ?? []
542
            );
543
544 7
            if ($distinctApplied === false) {
545 7
                $builder->distinct();
546 7
                $distinctApplied = true;
547
            }
548
        }
549
550 38
        return $this;
551
    }
552
553
    /**
554
     * @param ModelQueryBuilder $builder
555
     *
556
     * @return self
557
     */
558 38
    protected function applySorts(ModelQueryBuilder $builder): self
559
    {
560 38
        if ($this->hasSorts() === true) {
561 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...
562
        }
563
564 38
        return $this;
565
    }
566
567
    /**
568
     * @param ModelQueryBuilder $builder
569
     *
570
     * @return self
571
     */
572 45
    protected function applyPaging(ModelQueryBuilder $builder): self
573
    {
574 45
        if ($this->hasPaging() === true) {
575 18
            $builder->setFirstResult($this->getPagingOffset());
576 18
            $builder->setMaxResults($this->getPagingLimit() + 1);
577
        }
578
579 45
        return $this;
580
    }
581
582
    /**
583
     * @return self
584
     */
585 66
    protected function clearBuilderParameters(): self
586
    {
587 66
        $this->columnMapper       = null;
588 66
        $this->filterParameters   = null;
589 66
        $this->areFiltersWithAnd  = true;
590 66
        $this->sortingParameters  = null;
591 66
        $this->pagingOffset       = null;
592 66
        $this->pagingLimit        = null;
593 66
        $this->relFiltersAndSorts = [];
594
595 66
        return $this;
596
    }
597
598
    /**
599
     * @return self
600
     */
601 66
    private function clearFetchParameters(): self
602
    {
603 66
        $this->includePaths = null;
604 66
        $this->shouldBeTyped();
605
606 66
        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 38
    protected function builderOnIndex(ModelQueryBuilder $builder): ModelQueryBuilder
625
    {
626 38
        return $builder;
627
    }
628
629
    /**
630
     * @param ModelQueryBuilder $builder
631
     *
632
     * @return ModelQueryBuilder
633
     */
634 7
    protected function builderOnReadRelationship(ModelQueryBuilder $builder): ModelQueryBuilder
635
    {
636 7
        return $builder;
637
    }
638
639
    /**
640
     * @param ModelQueryBuilder $builder
641
     *
642
     * @return ModelQueryBuilder
643
     */
644 5
    protected function builderSaveResourceOnCreate(ModelQueryBuilder $builder): ModelQueryBuilder
645
    {
646 5
        return $builder;
647
    }
648
649
    /**
650
     * @param ModelQueryBuilder $builder
651
     *
652
     * @return ModelQueryBuilder
653
     */
654 5
    protected function builderSaveResourceOnUpdate(ModelQueryBuilder $builder): ModelQueryBuilder
655
    {
656 5
        return $builder;
657
    }
658
659
    /**
660
     * @param string            $relationshipName
661
     * @param ModelQueryBuilder $builder
662
     *
663
     * @return ModelQueryBuilder
664
     *
665
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
666
     */
667 2
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
668
        $relationshipName,
669
        ModelQueryBuilder $builder
670
    ): ModelQueryBuilder {
671 2
        return $builder;
672
    }
673
674
    /**
675
     * @param string            $relationshipName
676
     * @param ModelQueryBuilder $builder
677
     *
678
     * @return ModelQueryBuilder
679
     *
680
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
681
     */
682 2
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
683
        $relationshipName,
684
        ModelQueryBuilder $builder
685
    ): ModelQueryBuilder {
686 2
        return $builder;
687
    }
688
689
    /**
690
     * @param string            $relationshipName
691
     * @param ModelQueryBuilder $builder
692
     *
693
     * @return ModelQueryBuilder
694
     *
695
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
696
     */
697 1
    protected function builderOnCreateInBelongsToManyRelationship(/** @noinspection PhpUnusedParameterInspection */
698
        $relationshipName,
699
        ModelQueryBuilder $builder
700
    ): ModelQueryBuilder {
701 1
        return $builder;
702
    }
703
704
    /**
705
     * @param string            $relationshipName
706
     * @param ModelQueryBuilder $builder
707
     *
708
     * @return ModelQueryBuilder
709
     *
710
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
711
     */
712 1
    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 1
        return $builder;
717
    }
718
719
    /**
720
     * @param string            $relationshipName
721
     * @param ModelQueryBuilder $builder
722
     *
723
     * @return ModelQueryBuilder
724
     *
725
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
726
     */
727 2
    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 2
        return $builder;
732
    }
733
734
    /**
735
     * @param ModelQueryBuilder $builder
736
     *
737
     * @return ModelQueryBuilder
738
     */
739 4
    protected function builderOnDelete(ModelQueryBuilder $builder): ModelQueryBuilder
740
    {
741 4
        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 21
    private function loadRelationships($data): void
754
    {
755 21
        $isPaginated = $data instanceof PaginatedDataInterface;
756 21
        $hasData     = ($isPaginated === true && empty($data->getData()) === false) ||
757 21
            ($isPaginated === false && $data !== null);
758
759 21
        if ($hasData === true && $this->hasIncludes() === true) {
760 21
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemas());
761 21
            $modelsAtPath = $this->getFactory()->createTagStorage();
762
763
            // we gonna send these objects via function params so it is an equivalent for &array
764 21
            $classAtPath = new ArrayObject();
765 21
            $idsAtPath   = new ArrayObject();
766
767
            $registerModelAtRoot = function ($model) use ($modelStorage, $modelsAtPath, $idsAtPath): void {
768 21
                self::registerModelAtPath(
769 21
                    $model,
770 21
                    static::ROOT_PATH,
771 21
                    $this->getModelSchemas(),
772 21
                    $modelStorage,
773 21
                    $modelsAtPath,
774 21
                    $idsAtPath
775
                );
776 21
            };
777
778 21
            $model = null;
779 21
            if ($isPaginated === true) {
780 13
                foreach ($data->getData() as $model) {
781 13
                    $registerModelAtRoot($model);
782
                }
783
            } else {
784 8
                $model = $data;
785 8
                $registerModelAtRoot($model);
786
            }
787 21
            assert($model !== null);
788 21
            $classAtPath[static::ROOT_PATH] = get_class($model);
789
790 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...
791 10
                $this->loadRelationshipsLayer(
792 10
                    $modelsAtPath,
793 10
                    $classAtPath,
794 10
                    $idsAtPath,
795 10
                    $modelStorage,
796 10
                    $parentPath,
797 10
                    $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 21
    private static function registerModelAtPath(
816
        $model,
817
        string $path,
818
        ModelSchemaInfoInterface $modelSchemas,
819
        ModelStorageInterface $modelStorage,
820
        TagStorageInterface $modelsAtPath,
821
        ArrayObject $idsAtPath
822
    ) {
823 21
        $uniqueModel = $modelStorage->register($model);
824 21
        if ($uniqueModel !== null) {
825 21
            $modelsAtPath->register($uniqueModel, $path);
826 21
            $pkName             = $modelSchemas->getPrimaryKey(get_class($uniqueModel));
827 21
            $modelId            = $uniqueModel->{$pkName};
828 21
            $idsAtPath[$path][] = $modelId;
829
        }
830
831 21
        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 21
    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 21
        $normalizedPaths = [];
846 21
        $pathsDepths     = [];
847 21
        foreach ($paths as $path) {
848 10
            assert(is_array($path) || $path instanceof Traversable);
849 10
            $parentDepth = 0;
850 10
            $tmpPath     = static::ROOT_PATH;
851 10
            foreach ($path as $pathPiece) {
852 10
                assert(is_string($pathPiece));
853 10
                $parent                    = $tmpPath;
854 10
                $tmpPath                   = empty($tmpPath) === true ?
855 10
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
856 10
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
857 10
                $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 21
        $parentWithChildren = [];
864 21
        foreach ($normalizedPaths as $path => list ($parent, $childPath)) {
865 10
            $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 21
        asort($pathsDepths, SORT_NUMERIC);
870 21
        foreach ($pathsDepths as $parent => $depth) {
871 10
            assert($depth !== null); // suppress unused
872 10
            $childPaths = $parentWithChildren[$parent];
873 10
            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 4
    public function createDeleteBuilder(): QueryBuilder
893
    {
894 4
        return $this->createDeleteModelBuilder();
895
    }
896
897
    /**
898
     * @param iterable|null $columns
899
     *
900
     * @return ModelQueryBuilder
901
     *
902
     * @throws DBALException
903
     */
904 38
    protected function createIndexModelBuilder(iterable $columns = null): ModelQueryBuilder
905
    {
906 38
        $builder = $this->createBuilder($this->getModelClass());
907
908
        $this
909 38
            ->applyColumnMapper($builder);
910
911
        $builder
912 38
            ->selectModelColumns($columns)
913 38
            ->fromModelTable();
914
915
        $this
916 38
            ->applyAliasFilters($builder)
917 38
            ->applySorts($builder)
918 38
            ->applyRelationshipFiltersAndSorts($builder)
919 38
            ->applyPaging($builder);
920
921 38
        $result = $this->builderOnIndex($builder);
922
923 38
        $this->clearBuilderParameters();
924
925 38
        return $result;
926
    }
927
928
    /**
929
     * @return ModelQueryBuilder
930
     *
931
     * @throws DBALException
932
     */
933 4
    protected function createDeleteModelBuilder(): ModelQueryBuilder
934
    {
935
        $builder = $this
936 4
            ->createBuilder($this->getModelClass())
937 4
            ->deleteModels();
938
939 4
        $this->applyTableFilters($builder);
940
941 4
        $result = $this->builderOnDelete($builder);
942
943 4
        $this->clearBuilderParameters();
944
945 4
        return $result;
946
    }
947
948
    /**
949
     * @inheritdoc
950
     *
951
     * @throws DBALException
952
     */
953 17
    public function index(): PaginatedDataInterface
954
    {
955 17
        $builder = $this->createIndexModelBuilder();
956 17
        $data    = $this->fetchResources($builder, $builder->getModelClass());
957
958 17
        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 15
    public function read($index)
983
    {
984 15
        $this->withIndexFilter($index);
985
986 13
        $builder = $this->createIndexModelBuilder();
987 13
        $data    = $this->fetchResource($builder, $builder->getModelClass());
988
989 13
        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 7
    public function createReadRelationshipBuilder(
1017
        string $relationshipName,
1018
        iterable $relationshipFilters = null,
1019
        iterable $relationshipSorts = null,
1020
        iterable $columns = null
1021
    ): ModelQueryBuilder {
1022 7
        assert(
1023 7
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $relationshipName),
1024 7
            "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 7
            $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $relationshipName);
1032
1033
        $builder = $this
1034 7
            ->createBuilder($targetModelClass)
1035 7
            ->selectModelColumns($columns)
1036 7
            ->fromModelTable();
1037
1038
        // 'root' filters would be applied to the data in the reverse relationship ...
1039 7
        if ($this->hasFilters() === true) {
1040 7
            $filters = $this->getFilters();
1041 7
            $sorts   = $this->getSorts();
1042 7
            $joinCondition = $this->areFiltersWithAnd() === true ? ModelQueryBuilder::AND : ModelQueryBuilder::OR;
1043 7
            $builder->addRelationshipFiltersAndSorts($reverseRelName, $filters, $sorts, $joinCondition);
1044
        }
1045
        // ... and the input filters to actual data we select
1046 7
        if ($relationshipFilters !== null) {
1047 4
            $builder->addFiltersWithAndToAlias($relationshipFilters);
1048
        }
1049 7
        if ($relationshipSorts !== null) {
1050 3
            $builder->addSorts($relationshipSorts);
1051
        }
1052
1053 7
        $this->applyPaging($builder);
1054
1055
        // While joining tables we select distinct rows.
1056 7
        $builder->distinct();
1057
1058 7
        return $this->builderOnReadRelationship($builder);
1059
    }
1060
1061
    /**
1062
     * @inheritdoc
1063
     *
1064
     * @throws DBALException
1065
     */
1066 6
    public function indexRelationship(
1067
        string $name,
1068
        iterable $relationshipFilters = null,
1069
        iterable $relationshipSorts = null
1070
    ) {
1071 6
        assert(
1072 6
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $name),
1073 6
            "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 6
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1078 6
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
1079 6
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
1080
1081 6
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts);
1082
1083 6
        $modelClass = $builder->getModelClass();
1084 6
        $data       = $isExpectMany === true ?
1085 6
            $this->fetchResources($builder, $modelClass) : $this->fetchResource($builder, $modelClass);
1086
1087 6
        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 3
    public function readRelationship(
1130
        $index,
1131
        string $name,
1132
        iterable $relationshipFilters = null,
1133
        iterable $relationshipSorts = null
1134
    ) {
1135 3
        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 5
    public function remove($index): bool
1187
    {
1188 5
        $this->withIndexFilter($index);
1189
1190 4
        $deleted = $this->createDeleteBuilder()->execute();
1191
1192 3
        $this->clearFetchParameters();
1193
1194 3
        return (int)$deleted > 0;
1195
    }
1196
1197
    /**
1198
     * @inheritdoc
1199
     *
1200
     * @throws DBALException
1201
     *
1202
     * @SuppressWarnings(PHPMD.StaticAccess)
1203
     */
1204 6
    public function create($index, array $attributes, array $toMany): string
1205
    {
1206 6
        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 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...
1211
        $saveMain       = $this
1212 5
            ->createBuilder($this->getModelClass())
1213 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...
1214 5
        $saveMain       = $this->builderSaveResourceOnCreate($saveMain);
1215 5
        $saveMain->getSQL(); // prepare
1216
1217 5
        $this->clearBuilderParameters()->clearFetchParameters();
1218
1219
        $this->inTransaction(function () use ($saveMain, $toMany, &$index) {
1220 5
            $saveMain->execute();
1221
1222
            // if no index given will use last insert ID as index
1223 4
            $connection = $saveMain->getConnection();
1224 4
            $index !== null ?: $index = $connection->lastInsertId();
1225
1226 4
            $builderHook = Closure::fromCallable([$this, 'builderSaveRelationshipOnCreate']);
1227 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1228 2
                $this->addInToManyRelationship($connection, $index, $relationshipName, $secondaryIds, $builderHook);
1229
            }
1230 5
        });
1231
1232 4
        return $index;
1233
    }
1234
1235
    /**
1236
     * @inheritdoc
1237
     *
1238
     * @throws DBALException
1239
     *
1240
     * @SuppressWarnings(PHPMD.StaticAccess)
1241
     */
1242 6
    public function update($index, array $attributes, array $toMany): int
1243
    {
1244 6
        if (is_int($index) === false && is_string($index) === false) {
1245 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1246
        }
1247
1248 5
        $updated        = 0;
1249 5
        $pkName         = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1250
        $filters        = [
1251
            $pkName => [
1252 5
                FilterParameterInterface::OPERATION_EQUALS => [$index],
1253
            ],
1254
        ];
1255 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...
1256
        $saveMain       = $this
1257 5
            ->createBuilder($this->getModelClass())
1258 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...
1259 5
            ->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 5
        $saveMain       = $this->builderSaveResourceOnUpdate($saveMain);
1261 5
        $saveMain->getSQL(); // prepare
1262
1263 5
        $this->clearBuilderParameters()->clearFetchParameters();
1264
1265
        $this->inTransaction(function () use ($saveMain, $toMany, $index, &$updated) {
1266 5
            $updated = $saveMain->execute();
1267
1268 4
            $builderHook = Closure::fromCallable([$this, 'builderSaveRelationshipOnUpdate']);
1269 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1270 2
                $connection = $saveMain->getConnection();
1271
1272
                // clear existing
1273 2
                $this->builderCleanRelationshipOnUpdate(
1274 2
                    $relationshipName,
1275
                    $this
1276 2
                        ->createBuilderFromConnection($this->getConnection(), $this->getModelClass())
1277 2
                        ->clearToManyRelationship($relationshipName, $index)
1278 2
                )->execute();
1279
1280
                // add new ones
1281 2
                $updated   += $this->addInToManyRelationship(
1282 2
                    $connection,
1283 2
                    $index,
1284 2
                    $relationshipName,
1285 2
                    $secondaryIds,
1286 2
                    $builderHook
1287
                );
1288
            }
1289 5
        });
1290
1291 4
        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 1
    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 1
            $relType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1308 1
            $errMsg  = "Relationship `$name` of class `" . $this->getModelClass() .
1309 1
                '` either is not `belongsToMany` or do not exist in the class.';
1310 1
            $isOk = $relType === RelationshipTypes::BELONGS_TO_MANY;
1311
1312 1
            assert($isOk, $errMsg);
1313
1314 1
            return $isOk;
1315 1
        }));
1316
1317 1
        $builderHook = Closure::fromCallable([$this, 'builderOnCreateInBelongsToManyRelationship']);
1318
1319 1
        return $this->addInToManyRelationship($this->getConnection(), $parentId, $name, $childIds, $builderHook);
1320
    }
1321
1322
    /**
1323
     * @inheritdoc
1324
     *
1325
     * @throws DBALException
1326
     */
1327 1
    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 1
            $relType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1332 1
            $errMsg  = "Relationship `$name` of class `" . $this->getModelClass() .
1333 1
                '` either is not `belongsToMany` or do not exist in the class.';
1334 1
            $isOk = $relType === RelationshipTypes::BELONGS_TO_MANY;
1335
1336 1
            assert($isOk, $errMsg);
1337
1338 1
            return $isOk;
1339 1
        }));
1340
1341 1
        return $this->removeInToManyRelationship($this->getConnection(), $parentId, $name, $childIds);
1342
    }
1343
1344
    /**
1345
     * @return FactoryInterface
1346
     */
1347 52
    protected function getFactory(): FactoryInterface
1348
    {
1349 52
        return $this->factory;
1350
    }
1351
1352
    /**
1353
     * @return string
1354
     */
1355 53
    protected function getModelClass(): string
1356
    {
1357 53
        return $this->modelClass;
1358
    }
1359
1360
    /**
1361
     * @return ModelSchemaInfoInterface
1362
     */
1363 53
    protected function getModelSchemas(): ModelSchemaInfoInterface
1364
    {
1365 53
        return $this->modelSchemas;
1366
    }
1367
1368
    /**
1369
     * @return RelationshipPaginationStrategyInterface
1370
     */
1371 8
    protected function getRelationshipPagingStrategy(): RelationshipPaginationStrategyInterface
1372
    {
1373 8
        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 10
    public function inTransaction(Closure $closure): void
1384
    {
1385 10
        $connection = $this->getConnection();
1386 10
        $connection->beginTransaction();
1387
        try {
1388 10
            $isOk = ($closure() === false ? null : true);
1389 8
        } finally {
1390 10
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
1391
        }
1392
    }
1393
1394
    /**
1395
     * @inheritdoc
1396
     *
1397
     * @throws DBALException
1398
     */
1399 22
    public function fetchResources(QueryBuilder $builder, string $modelClass): PaginatedDataInterface
1400
    {
1401 22
        $data = $this->fetchPaginatedResourcesWithoutRelationships($builder, $modelClass);
1402
1403 22
        if ($this->hasIncludes() === true) {
1404 13
            $this->loadRelationships($data);
1405 13
            $this->clearFetchParameters();
1406
        }
1407
1408 22
        return $data;
1409
    }
1410
1411
    /**
1412
     * @inheritdoc
1413
     *
1414
     * @throws DBALException
1415
     */
1416 14
    public function fetchResource(QueryBuilder $builder, string $modelClass)
1417
    {
1418 14
        $data = $this->fetchResourceWithoutRelationships($builder, $modelClass);
1419
1420 14
        if ($this->hasIncludes() === true) {
1421 8
            $this->loadRelationships($data);
1422 8
            $this->clearFetchParameters();
1423
        }
1424
1425 14
        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 5
    private function addInToManyRelationship(
1515
        Connection $connection,
1516
        string $primaryIdentity,
1517
        string $name,
1518
        iterable $secondaryIdentities,
1519
        Closure $builderHook
1520
    ): int {
1521 5
        $inserted = 0;
1522
1523 5
        $secondaryIdBindName = ':secondaryId';
1524
        $saveToMany          = $this
1525 5
            ->createBuilderFromConnection($connection, $this->getModelClass())
1526 5
            ->prepareCreateInToManyRelationship($name, $primaryIdentity, $secondaryIdBindName);
1527
1528 5
        $saveToMany = call_user_func($builderHook, $name, $saveToMany);
1529
1530 5
        foreach ($secondaryIdentities as $secondaryId) {
1531
            try {
1532 5
                $inserted += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1533 1
            } /** @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 5
                continue;
1540
            }
1541
        }
1542
1543 5
        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 1
    private function removeInToManyRelationship(
1557
        Connection $connection,
1558
        string $primaryIdentity,
1559
        string $name,
1560
        iterable $secondaryIdentities
1561
    ): int {
1562 1
        $removeToMany = $this->builderOnRemoveInBelongsToManyRelationship(
1563 1
            $name,
1564
            $this
1565 1
                ->createBuilderFromConnection($connection, $this->getModelClass())
1566 1
                ->prepareDeleteInToManyRelationship($name, $primaryIdentity, $secondaryIdentities)
1567
        );
1568 1
        $removed = $removeToMany->execute();
1569
1570 1
        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 14
    private function fetchResourceWithoutRelationships(QueryBuilder $builder, string $modelClass)
1584
    {
1585 14
        $model     = null;
1586 14
        $statement = $builder->execute();
1587
1588 14
        if ($this->isFetchTyped() === true) {
1589 12
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1590 12
            if (($attributes = $statement->fetch()) !== false) {
1591 12
                $platform  = $builder->getConnection()->getDatabasePlatform();
1592 12
                $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1593 12
                $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 14
        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 9
    private function fetchResourcesWithoutRelationships(
1617
        QueryBuilder $builder,
1618
        string $modelClass,
1619
        string $keyColumnName
1620
    ): iterable {
1621 9
        $statement = $builder->execute();
1622
1623 9
        if ($this->isFetchTyped() === true) {
1624 8
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1625 8
            $platform  = $builder->getConnection()->getDatabasePlatform();
1626 8
            $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1627 8
            while (($attributes = $statement->fetch()) !== false) {
1628 6
                $model = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1629 6
                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 27
    private function fetchPaginatedResourcesWithoutRelationships(
1648
        QueryBuilder $builder,
1649
        string $modelClass
1650
    ): PaginatedDataInterface {
1651 27
        list($models, $hasMore, $limit, $offset) = $this->fetchResourceCollection($builder, $modelClass);
1652
1653 27
        $data = $this->getFactory()
1654 27
            ->createPaginatedData($models)
1655 27
            ->markAsCollection()
1656 27
            ->setOffset($offset)
1657 27
            ->setLimit($limit);
1658
1659 27
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
1660
1661 27
        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 27
    private function fetchResourceCollection(QueryBuilder $builder, string $modelClass): array
1675
    {
1676 27
        $statement = $builder->execute();
1677
1678 27
        $models           = [];
1679 27
        $counter          = 0;
1680 27
        $hasMoreThanLimit = false;
1681 27
        $limit            = $builder->getMaxResults() !== null ? $builder->getMaxResults() - 1 : null;
1682
1683 27
        if ($this->isFetchTyped() === true) {
1684 26
            $platform  = $builder->getConnection()->getDatabasePlatform();
1685 26
            $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1686 26
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1687 26
            while (($attributes = $statement->fetch()) !== false) {
1688 25
                $counter++;
1689 25
                if ($limit !== null && $counter > $limit) {
1690 6
                    $hasMoreThanLimit = true;
1691 6
                    break;
1692
                }
1693 25
                $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 27
        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 5
    protected function filterAttributesOnCreate(?string $index, iterable $attributes): iterable
1717
    {
1718 5
        if ($index !== null) {
1719 1
            $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1720 1
            yield $pkName => $index;
1721
        }
1722
1723 5
        $knownAttrAndTypes = $this->getModelSchemas()->getAttributeTypes($this->getModelClass());
1724 5
        foreach ($attributes as $attribute => $value) {
1725 5
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1726 5
                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 5
    protected function filterAttributesOnUpdate(iterable $attributes): iterable
1737
    {
1738 5
        $knownAttrAndTypes = $this->getModelSchemas()->getAttributeTypes($this->getModelClass());
1739 5
        foreach ($attributes as $attribute => $value) {
1740 5
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1741 5
                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 10
    private function loadRelationshipsLayer(
1762
        TagStorageInterface $modelsAtPath,
1763
        ArrayObject $classAtPath,
1764
        ArrayObject $idsAtPath,
1765
        ModelStorageInterface $deDup,
1766
        string $parentsPath,
1767
        array $childRelationships
1768
    ): void {
1769 10
        $rootClass   = $classAtPath[static::ROOT_PATH];
1770 10
        $parentClass = $classAtPath[$parentsPath];
1771 10
        $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 10
        $pkName = $this->getModelSchemas()->getPrimaryKey($parentClass);
1778
1779
        $registerModelAtPath = function ($model, string $path) use ($deDup, $modelsAtPath, $idsAtPath) {
1780 8
            return self::registerModelAtPath(
1781 8
                $model,
1782 8
                $path,
1783 8
                $this->getModelSchemas(),
1784 8
                $deDup,
1785 8
                $modelsAtPath,
1786 8
                $idsAtPath
1787
            );
1788 10
        };
1789
1790 10
        foreach ($childRelationships as $name) {
1791 10
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
1792
1793 10
            $relationshipType = $this->getModelSchemas()->getRelationshipType($parentClass, $name);
1794
            list ($targetModelClass, $reverseRelName) =
1795 10
                $this->getModelSchemas()->getReverseRelationship($parentClass, $name);
1796
1797
            $builder = $this
1798 10
                ->createBuilder($targetModelClass)
1799 10
                ->selectModelColumns()
1800 10
                ->fromModelTable();
1801
1802 10
            $classAtPath[$childrenPath] = $targetModelClass;
1803
1804
            switch ($relationshipType) {
1805 10
                case RelationshipTypes::BELONGS_TO:
1806
                    // some paths might not have any records in the database
1807 9
                    $areParentsLoaded = $idsAtPath->offsetExists($parentsPath);
1808 9
                    if ($areParentsLoaded === false) {
1809 1
                        break;
1810
                    }
1811
                    // for 'belongsTo' relationship all resources could be read at once.
1812 9
                    $parentIds            = $idsAtPath[$parentsPath];
1813 9
                    $clonedBuilder        = (clone $builder)->addRelationshipFiltersAndSorts(
1814 9
                        $reverseRelName,
1815 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...
1816 9
                        null
1817
                    );
1818 9
                    $unregisteredChildren = $this->fetchResourcesWithoutRelationships(
1819 9
                        $clonedBuilder,
1820 9
                        $clonedBuilder->getModelClass(),
1821 9
                        $this->getModelSchemas()->getPrimaryKey($clonedBuilder->getModelClass())
1822
                    );
1823 9
                    $children             = [];
1824 9
                    foreach ($unregisteredChildren as $index => $unregisteredChild) {
1825 7
                        $children[$index] = $registerModelAtPath($unregisteredChild, $childrenPath);
1826
                    }
1827 9
                    $fkNameToChild = $this->getModelSchemas()->getForeignKey($parentClass, $name);
1828 9
                    foreach ($parents as $parent) {
1829 9
                        $fkToChild       = $parent->{$fkNameToChild};
1830 9
                        $parent->{$name} = $children[$fkToChild] ?? null;
1831
                    }
1832 9
                    break;
1833 8
                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 8
                    list ($queryOffset, $queryLimit) = $this->getRelationshipPagingStrategy()
1838 8
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
1839 8
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
1840 8
                    foreach ($parents as $parent) {
1841 8
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSorts(
1842 8
                            $reverseRelName,
1843 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...
1844 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...
1845
                        );
1846 8
                        $children      = $this->fetchPaginatedResourcesWithoutRelationships(
1847 8
                            $clonedBuilder,
1848 8
                            $clonedBuilder->getModelClass()
1849
                        );
1850
1851 8
                        $deDupedChildren = [];
1852 8
                        foreach ($children->getData() as $child) {
1853 7
                            $deDupedChildren[] = $registerModelAtPath($child, $childrenPath);
1854
                        }
1855
1856 8
                        $paginated = $this->getFactory()
1857 8
                            ->createPaginatedData($deDupedChildren)
1858 8
                            ->markAsCollection()
1859 8
                            ->setOffset($children->getOffset())
1860 8
                            ->setLimit($children->getLimit());
1861 8
                        $children->hasMoreItems() === true ?
1862 8
                            $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
1863
1864 8
                        $parent->{$name} = $paginated;
1865
                    }
1866 10
                    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 34
    private function readResourceFromAssoc(
1902
        string $class,
1903
        array $attributes,
1904
        array $typeNames,
1905
        AbstractPlatform $platform
1906
    ) {
1907 34
        $instance = new $class();
1908 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...
1909 34
            $instance->{$name} = $value;
1910
        }
1911
1912 34
        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 35
    private function readTypedAttributes(iterable $attributes, array $typeNames, AbstractPlatform $platform): iterable
1946
    {
1947 35
        foreach ($attributes as $name => $value) {
1948 35
            yield $name => (array_key_exists($name, $typeNames) === true ?
1949 35
                Type::getType($typeNames[$name])->convertToPHPValue($value, $platform) : $value);
1950
        }
1951
    }
1952
}
1953