Completed
Push — master ( 6173d9...210649 )
by Neomerx
04:26
created

Crud::update()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 51

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 51
ccs 30
cts 30
cp 1
rs 9.069
c 0
b 0
f 0
cc 4
nc 2
nop 3
crap 4

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Document\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 63
    public function __construct(ContainerInterface $container, string $modelClass)
153
    {
154 63
        $this->setContainer($container);
155
156 63
        $this->modelClass        = $modelClass;
157 63
        $this->factory           = $this->getContainer()->get(FactoryInterface::class);
158 63
        $this->modelSchemas      = $this->getContainer()->get(ModelSchemaInfoInterface::class);
159 63
        $this->relPagingStrategy = $this->getContainer()->get(RelationshipPaginationStrategyInterface::class);
160 63
        $this->connection        = $this->getContainer()->get(Connection::class);
161
162 63
        $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 2
    public function withIndexesFilter(array $indexes): CrudInterface
210
    {
211
        assert(call_user_func(function () use ($indexes) {
212 2
            $allOk = true;
213
214 2
            foreach ($indexes as $index) {
215 2
                $allOk = ($allOk === true && (is_string($index) === true || is_int($index) === true));
216
            }
217
218 2
            return $allOk;
219 2
        }) === true);
220
221 2
        $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
222 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...
223
            $pkName => [
224 2
                FilterParameterInterface::OPERATION_IN => $indexes,
225
            ],
226
        ]);
227
228 2
        return $this;
229
    }
230
231
    /**
232
     * @inheritdoc
233
     */
234 7
    public function withRelationshipFilters(string $name, iterable $filters): CrudInterface
235
    {
236 7
        assert($this->getModelSchemas()->hasRelationship($this->getModelClass(), $name) === true);
237
238 7
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__FILTERS] = $filters;
239
240 7
        return $this;
241
    }
242
243
    /**
244
     * @inheritdoc
245
     */
246 1
    public function withRelationshipSorts(string $name, iterable $sorts): CrudInterface
247
    {
248 1
        assert($this->getModelSchemas()->hasRelationship($this->getModelClass(), $name) === true);
249
250 1
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__SORTS] = $sorts;
251
252 1
        return $this;
253
    }
254
255
    /**
256
     * @inheritdoc
257
     */
258 16
    public function combineWithAnd(): CrudInterface
259
    {
260 16
        $this->areFiltersWithAnd = true;
261
262 16
        return $this;
263
    }
264
265
    /**
266
     * @inheritdoc
267
     */
268 2
    public function combineWithOr(): CrudInterface
269
    {
270 2
        $this->areFiltersWithAnd = false;
271
272 2
        return $this;
273
    }
274
275
    /**
276
     * @return bool
277
     */
278 38
    private function hasColumnMapper(): bool
279
    {
280 38
        return $this->columnMapper !== null;
281
    }
282
283
    /**
284
     * @return Closure
285
     */
286 1
    private function getColumnMapper(): Closure
287
    {
288 1
        return $this->columnMapper;
289
    }
290
291
    /**
292
     * @return bool
293
     */
294 47
    private function hasFilters(): bool
295
    {
296 47
        return empty($this->filterParameters) === false;
297
    }
298
299
    /**
300
     * @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...
301
     */
302 36
    private function getFilters(): iterable
303
    {
304 36
        return $this->filterParameters;
305
    }
306
307
    /**
308
     * @return bool
309
     */
310 36
    private function areFiltersWithAnd(): bool
311
    {
312 36
        return $this->areFiltersWithAnd;
313
    }
314
315
    /**
316
     * @inheritdoc
317
     */
318 17
    public function withSorts(iterable $sortingParameters): CrudInterface
319
    {
320 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...
321
322 17
        return $this;
323
    }
324
325
    /**
326
     * @return bool
327
     */
328 38
    private function hasSorts(): bool
329
    {
330 38
        return empty($this->sortingParameters) === false;
331
    }
332
333
    /**
334
     * @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...
335
     */
336 11
    private function getSorts(): ?iterable
337
    {
338 11
        return $this->sortingParameters;
339
    }
340
341
    /**
342
     * @inheritdoc
343
     */
344 21
    public function withIncludes(iterable $includePaths): CrudInterface
345
    {
346 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...
347
348 21
        return $this;
349
    }
350
351
    /**
352
     * @return bool
353
     */
354 36
    private function hasIncludes(): bool
355
    {
356 36
        return empty($this->includePaths) === false;
357
    }
358
359
    /**
360
     * @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...
361
     */
362 21
    private function getIncludes(): iterable
363
    {
364 21
        return $this->includePaths;
365
    }
366
367
    /**
368
     * @inheritdoc
369
     */
370 20
    public function withPaging(int $offset, int $limit): CrudInterface
371
    {
372 20
        $this->pagingOffset = $offset;
373 20
        $this->pagingLimit  = $limit;
374
375 20
        return $this;
376
    }
377
378
    /**
379
     * @inheritdoc
380
     */
381 1
    public function withoutPaging(): CrudInterface
382
    {
383 1
        $this->pagingOffset = null;
384 1
        $this->pagingLimit  = null;
385
386 1
        return $this;
387
    }
388
389
    /**
390
     * @return self
391
     */
392 63
    public function shouldBeTyped(): self
393
    {
394 63
        $this->isFetchTyped = true;
395
396 63
        return $this;
397
    }
398
399
    /**
400
     * @return self
401
     */
402 4
    public function shouldBeUntyped(): self
403
    {
404 4
        $this->isFetchTyped = false;
405
406 4
        return $this;
407
    }
408
409
    /**
410
     * @return bool
411
     */
412 45
    private function hasPaging(): bool
413
    {
414 45
        return $this->pagingOffset !== null && $this->pagingLimit !== null;
415
    }
416
417
    /**
418
     * @return int
419
     */
420 18
    private function getPagingOffset(): int
421
    {
422 18
        return $this->pagingOffset;
423
    }
424
425
    /**
426
     * @return int
427
     */
428 18
    private function getPagingLimit(): int
429
    {
430 18
        return $this->pagingLimit;
431
    }
432
433
    /**
434
     * @return bool
435
     */
436 43
    private function isFetchTyped(): bool
437
    {
438 43
        return $this->isFetchTyped;
439
    }
440
441
    /**
442
     * @return Connection
443
     */
444 50
    protected function getConnection(): Connection
445
    {
446 50
        return $this->connection;
447
    }
448
449
    /**
450
     * @param string $modelClass
451
     *
452
     * @return ModelQueryBuilder
453
     */
454 48
    protected function createBuilder(string $modelClass): ModelQueryBuilder
455
    {
456 48
        return $this->createBuilderFromConnection($this->getConnection(), $modelClass);
457
    }
458
459
    /**
460
     * @param Connection $connection
461
     * @param string     $modelClass
462
     *
463
     * @return ModelQueryBuilder
464
     */
465 50
    private function createBuilderFromConnection(Connection $connection, string $modelClass): ModelQueryBuilder
466
    {
467 50
        return $this->getFactory()->createModelQueryBuilder($connection, $modelClass, $this->getModelSchemas());
468
    }
469
470
    /**
471
     * @param ModelQueryBuilder $builder
472
     *
473
     * @return Crud
474
     */
475 38
    protected function applyColumnMapper(ModelQueryBuilder $builder): self
476
    {
477 38
        if ($this->hasColumnMapper() === true) {
478 1
            $builder->setColumnToDatabaseMapper($this->getColumnMapper());
479
        }
480
481 38
        return $this;
482
    }
483
484
    /**
485
     * @param ModelQueryBuilder $builder
486
     *
487
     * @return Crud
488
     *
489
     * @throws DBALException
490
     */
491 38
    protected function applyAliasFilters(ModelQueryBuilder $builder): self
492
    {
493 38
        if ($this->hasFilters() === true) {
494 27
            $filters = $this->getFilters();
495 27
            $this->areFiltersWithAnd() === true ?
496 27
                $builder->addFiltersWithAndToAlias($filters) : $builder->addFiltersWithOrToAlias($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 494 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 494 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...
497
        }
498
499 38
        return $this;
500
    }
501
502
    /**
503
     * @param ModelQueryBuilder $builder
504
     *
505
     * @return self
506
     *
507
     * @throws DBALException
508
     */
509 4
    protected function applyTableFilters(ModelQueryBuilder $builder): self
510
    {
511 4
        if ($this->hasFilters() === true) {
512 4
            $filters = $this->getFilters();
513 4
            $this->areFiltersWithAnd() === true ?
514 4
                $builder->addFiltersWithAndToTable($filters) : $builder->addFiltersWithOrToTable($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 512 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 512 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...
515
        }
516
517 4
        return $this;
518
    }
519
520
    /**
521
     * @param ModelQueryBuilder $builder
522
     *
523
     * @return self
524
     *
525
     * @throws DBALException
526
     */
527 38
    protected function applyRelationshipFiltersAndSorts(ModelQueryBuilder $builder): self
528
    {
529
        // While joining tables we select distinct rows. This flag used to apply `distinct` no more than once.
530 38
        $distinctApplied = false;
531
532 38
        foreach ($this->relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
533 7
            assert(is_string($relationshipName) === true && is_array($filtersAndSorts) === true);
534 7
            $builder->addRelationshipFiltersAndSortsWithAnd(
535 7
                $relationshipName,
536 7
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__FILTERS] ?? [],
537 7
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__SORTS] ?? []
538
            );
539
540 7
            if ($distinctApplied === false) {
541 7
                $builder->distinct();
542 7
                $distinctApplied = true;
543
            }
544
        }
545
546 38
        return $this;
547
    }
548
549
    /**
550
     * @param ModelQueryBuilder $builder
551
     *
552
     * @return self
553
     */
554 38
    protected function applySorts(ModelQueryBuilder $builder): self
555
    {
556 38
        if ($this->hasSorts() === true) {
557 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...
558
        }
559
560 38
        return $this;
561
    }
562
563
    /**
564
     * @param ModelQueryBuilder $builder
565
     *
566
     * @return self
567
     */
568 45
    protected function applyPaging(ModelQueryBuilder $builder): self
569
    {
570 45
        if ($this->hasPaging() === true) {
571 18
            $builder->setFirstResult($this->getPagingOffset());
572 18
            $builder->setMaxResults($this->getPagingLimit() + 1);
573
        }
574
575 45
        return $this;
576
    }
577
578
    /**
579
     * @return self
580
     */
581 63
    protected function clearBuilderParameters(): self
582
    {
583 63
        $this->columnMapper       = null;
584 63
        $this->filterParameters   = null;
585 63
        $this->areFiltersWithAnd  = true;
586 63
        $this->sortingParameters  = null;
587 63
        $this->pagingOffset       = null;
588 63
        $this->pagingLimit        = null;
589 63
        $this->relFiltersAndSorts = [];
590
591 63
        return $this;
592
    }
593
594
    /**
595
     * @return self
596
     */
597 63
    private function clearFetchParameters(): self
598
    {
599 63
        $this->includePaths = null;
600 63
        $this->shouldBeTyped();
601
602 63
        return $this;
603
    }
604
605
    /**
606
     * @param ModelQueryBuilder $builder
607
     *
608
     * @return ModelQueryBuilder
609
     */
610 2
    protected function builderOnCount(ModelQueryBuilder $builder): ModelQueryBuilder
611
    {
612 2
        return $builder;
613
    }
614
615
    /**
616
     * @param ModelQueryBuilder $builder
617
     *
618
     * @return ModelQueryBuilder
619
     */
620 38
    protected function builderOnIndex(ModelQueryBuilder $builder): ModelQueryBuilder
621
    {
622 38
        return $builder;
623
    }
624
625
    /**
626
     * @param ModelQueryBuilder $builder
627
     *
628
     * @return ModelQueryBuilder
629
     */
630 7
    protected function builderOnReadRelationship(ModelQueryBuilder $builder): ModelQueryBuilder
631
    {
632 7
        return $builder;
633
    }
634
635
    /**
636
     * @param ModelQueryBuilder $builder
637
     *
638
     * @return ModelQueryBuilder
639
     */
640 4
    protected function builderSaveResourceOnCreate(ModelQueryBuilder $builder): ModelQueryBuilder
641
    {
642 4
        return $builder;
643
    }
644
645
    /**
646
     * @param ModelQueryBuilder $builder
647
     *
648
     * @return ModelQueryBuilder
649
     */
650 4
    protected function builderSaveResourceOnUpdate(ModelQueryBuilder $builder): ModelQueryBuilder
651
    {
652 4
        return $builder;
653
    }
654
655
    /**
656
     * @param string            $relationshipName
657
     * @param ModelQueryBuilder $builder
658
     *
659
     * @return ModelQueryBuilder
660
     *
661
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
662
     */
663 2
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
664
        $relationshipName,
665
        ModelQueryBuilder $builder
666
    ): ModelQueryBuilder {
667 2
        return $builder;
668
    }
669
670
    /**
671
     * @param string            $relationshipName
672
     * @param ModelQueryBuilder $builder
673
     *
674
     * @return ModelQueryBuilder
675
     *
676
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
677
     */
678 2
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
679
        $relationshipName,
680
        ModelQueryBuilder $builder
681
    ): ModelQueryBuilder {
682 2
        return $builder;
683
    }
684
685
    /**
686
     * @param string            $relationshipName
687
     * @param ModelQueryBuilder $builder
688
     *
689
     * @return ModelQueryBuilder
690
     *
691
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
692
     */
693 1
    protected function builderOnCreateInBelongsToManyRelationship(/** @noinspection PhpUnusedParameterInspection */
694
        $relationshipName,
695
        ModelQueryBuilder $builder
696
    ): ModelQueryBuilder {
697 1
        return $builder;
698
    }
699
700
    /**
701
     * @param string            $relationshipName
702
     * @param ModelQueryBuilder $builder
703
     *
704
     * @return ModelQueryBuilder
705
     *
706
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
707
     */
708 1
    protected function builderOnRemoveInBelongsToManyRelationship(/** @noinspection PhpUnusedParameterInspection */
709
        $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...
710
        ModelQueryBuilder $builder
711
    ): ModelQueryBuilder {
712 1
        return $builder;
713
    }
714
715
    /**
716
     * @param string            $relationshipName
717
     * @param ModelQueryBuilder $builder
718
     *
719
     * @return ModelQueryBuilder
720
     *
721
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
722
     */
723 2
    protected function builderCleanRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
724
        $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...
725
        ModelQueryBuilder $builder
726
    ): ModelQueryBuilder {
727 2
        return $builder;
728
    }
729
730
    /**
731
     * @param ModelQueryBuilder $builder
732
     *
733
     * @return ModelQueryBuilder
734
     */
735 4
    protected function builderOnDelete(ModelQueryBuilder $builder): ModelQueryBuilder
736
    {
737 4
        return $builder;
738
    }
739
740
    /**
741
     * @param PaginatedDataInterface|mixed|null $data
742
     *
743
     * @return void
744
     *
745
     * @SuppressWarnings(PHPMD.ElseExpression)
746
     *
747
     * @throws DBALException
748
     */
749 21
    private function loadRelationships($data): void
750
    {
751 21
        $isPaginated = $data instanceof PaginatedDataInterface;
752 21
        $hasData     = ($isPaginated === true && empty($data->getData()) === false) ||
753 21
            ($isPaginated === false && $data !== null);
754
755 21
        if ($hasData === true && $this->hasIncludes() === true) {
756 21
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemas());
757 21
            $modelsAtPath = $this->getFactory()->createTagStorage();
758
759
            // we gonna send these objects via function params so it is an equivalent for &array
760 21
            $classAtPath = new ArrayObject();
761 21
            $idsAtPath   = new ArrayObject();
762
763
            $registerModelAtRoot = function ($model) use ($modelStorage, $modelsAtPath, $idsAtPath): void {
764 21
                self::registerModelAtPath(
765 21
                    $model,
766 21
                    static::ROOT_PATH,
767 21
                    $this->getModelSchemas(),
768 21
                    $modelStorage,
769 21
                    $modelsAtPath,
770 21
                    $idsAtPath
771
                );
772 21
            };
773
774 21
            $model = null;
775 21
            if ($isPaginated === true) {
776 13
                foreach ($data->getData() as $model) {
777 13
                    $registerModelAtRoot($model);
778
                }
779
            } else {
780 8
                $model = $data;
781 8
                $registerModelAtRoot($model);
782
            }
783 21
            assert($model !== null);
784 21
            $classAtPath[static::ROOT_PATH] = get_class($model);
785
786 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...
787 10
                $this->loadRelationshipsLayer(
788 10
                    $modelsAtPath,
789 10
                    $classAtPath,
790 10
                    $idsAtPath,
791 10
                    $modelStorage,
792 10
                    $parentPath,
793 10
                    $childPaths
794
                );
795
            }
796
        }
797
    }
798
799
    /**
800
     * A helper to remember all model related data. Helps to ensure we consistently handle models in CRUD.
801
     *
802
     * @param mixed                    $model
803
     * @param string                   $path
804
     * @param ModelSchemaInfoInterface $modelSchemas
805
     * @param ModelStorageInterface    $modelStorage
806
     * @param TagStorageInterface      $modelsAtPath
807
     * @param ArrayObject              $idsAtPath
808
     *
809
     * @return mixed
810
     */
811 21
    private static function registerModelAtPath(
812
        $model,
813
        string $path,
814
        ModelSchemaInfoInterface $modelSchemas,
815
        ModelStorageInterface $modelStorage,
816
        TagStorageInterface $modelsAtPath,
817
        ArrayObject $idsAtPath
818
    ) {
819 21
        $uniqueModel = $modelStorage->register($model);
820 21
        if ($uniqueModel !== null) {
821 21
            $modelsAtPath->register($uniqueModel, $path);
822 21
            $pkName             = $modelSchemas->getPrimaryKey(get_class($uniqueModel));
823 21
            $modelId            = $uniqueModel->{$pkName};
824 21
            $idsAtPath[$path][] = $modelId;
825
        }
826
827 21
        return $uniqueModel;
828
    }
829
830
    /**
831
     * @param iterable $paths (string[])
832
     *
833
     * @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...
834
     */
835 21
    private static function getPaths(iterable $paths): iterable
836
    {
837
        // The idea is to normalize paths. It means build all intermediate paths.
838
        // e.g. if only `a.b.c` path it given it will be normalized to `a`, `a.b` and `a.b.c`.
839
        // Path depths store depth of each path (e.g. 0 for root, 1 for `a`, 2 for `a.b` and etc).
840
        // It is needed for yielding them in correct order (from top level to bottom).
841 21
        $normalizedPaths = [];
842 21
        $pathsDepths     = [];
843 21
        foreach ($paths as $path) {
844 10
            assert(is_array($path) || $path instanceof Traversable);
845 10
            $parentDepth = 0;
846 10
            $tmpPath     = static::ROOT_PATH;
847 10
            foreach ($path as $pathPiece) {
848 10
                assert(is_string($pathPiece));
849 10
                $parent                    = $tmpPath;
850 10
                $tmpPath                   = empty($tmpPath) === true ?
851 10
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
852 10
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
853 10
                $pathsDepths[$parent]      = $parentDepth++;
854
            }
855
        }
856
857
        // Here we collect paths in form of parent => [list of children]
858
        // e.g. '' => ['a', 'c', 'b'], 'b' => ['bb', 'aa'] and etc
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
859 21
        $parentWithChildren = [];
860 21
        foreach ($normalizedPaths as $path => list ($parent, $childPath)) {
861 10
            $parentWithChildren[$parent][] = $childPath;
862
        }
863
864
        // And finally sort by path depth and yield parent with its children. Top level paths first then deeper ones.
865 21
        asort($pathsDepths, SORT_NUMERIC);
866 21
        foreach ($pathsDepths as $parent => $depth) {
867 10
            assert($depth !== null); // suppress unused
868 10
            $childPaths = $parentWithChildren[$parent];
869 10
            yield [$parent, $childPaths];
870
        }
871
    }
872
873
    /**
874
     * @inheritdoc
875
     *
876
     * @throws DBALException
877
     */
878 3
    public function createIndexBuilder(iterable $columns = null): QueryBuilder
879
    {
880 3
        return $this->createIndexModelBuilder($columns);
881
    }
882
883
    /**
884
     * @inheritdoc
885
     *
886
     * @throws DBALException
887
     */
888 4
    public function createDeleteBuilder(): QueryBuilder
889
    {
890 4
        return $this->createDeleteModelBuilder();
891
    }
892
893
    /**
894
     * @param iterable|null $columns
895
     *
896
     * @return ModelQueryBuilder
897
     *
898
     * @throws DBALException
899
     */
900 38
    protected function createIndexModelBuilder(iterable $columns = null): ModelQueryBuilder
901
    {
902 38
        $builder = $this->createBuilder($this->getModelClass());
903
904
        $this
905 38
            ->applyColumnMapper($builder);
906
907
        $builder
908 38
            ->selectModelColumns($columns)
909 38
            ->fromModelTable();
910
911
        $this
912 38
            ->applyAliasFilters($builder)
913 38
            ->applySorts($builder)
914 38
            ->applyRelationshipFiltersAndSorts($builder)
915 38
            ->applyPaging($builder);
916
917 38
        $result = $this->builderOnIndex($builder);
918
919 38
        $this->clearBuilderParameters();
920
921 38
        return $result;
922
    }
923
924
    /**
925
     * @return ModelQueryBuilder
926
     *
927
     * @throws DBALException
928
     */
929 4
    protected function createDeleteModelBuilder(): ModelQueryBuilder
930
    {
931
        $builder = $this
932 4
            ->createBuilder($this->getModelClass())
933 4
            ->deleteModels();
934
935 4
        $this->applyTableFilters($builder);
936
937 4
        $result = $this->builderOnDelete($builder);
938
939 4
        $this->clearBuilderParameters();
940
941 4
        return $result;
942
    }
943
944
    /**
945
     * @inheritdoc
946
     *
947
     * @throws DBALException
948
     */
949 17
    public function index(): PaginatedDataInterface
950
    {
951 17
        $builder = $this->createIndexModelBuilder();
952 17
        $data    = $this->fetchResources($builder, $builder->getModelClass());
953
954 17
        return $data;
955
    }
956
957
    /**
958
     * @inheritdoc
959
     *
960
     * @throws DBALException
961
     */
962 4
    public function indexIdentities(): array
963
    {
964 4
        $pkName  = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
965 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...
966
        /** @var Generator $data */
967 4
        $data   = $this->fetchColumn($builder, $builder->getModelClass(), $pkName);
968 4
        $result = iterator_to_array($data);
969
970 4
        return $result;
971
    }
972
973
    /**
974
     * @inheritdoc
975
     *
976
     * @throws DBALException
977
     */
978 15
    public function read($index)
979
    {
980 15
        $this->withIndexFilter($index);
981
982 13
        $builder = $this->createIndexModelBuilder();
983 13
        $data    = $this->fetchResource($builder, $builder->getModelClass());
984
985 13
        return $data;
986
    }
987
988
    /**
989
     * @inheritdoc
990
     *
991
     * @throws DBALException
992
     */
993 2
    public function count(): ?int
994
    {
995 2
        $result = $this->builderOnCount(
996 2
            $this->createCountBuilderFromBuilder($this->createIndexModelBuilder())
997 2
        )->execute()->fetchColumn();
998
999 2
        return $result === false ? null : $result;
1000
    }
1001
1002
    /**
1003
     * @param string        $relationshipName
1004
     * @param iterable|null $relationshipFilters
1005
     * @param iterable|null $relationshipSorts
1006
     * @param iterable|null $columns
1007
     *
1008
     * @return ModelQueryBuilder
1009
     *
1010
     * @throws DBALException
1011
     */
1012 7
    public function createReadRelationshipBuilder(
1013
        string $relationshipName,
1014
        iterable $relationshipFilters = null,
1015
        iterable $relationshipSorts = null,
1016
        iterable $columns = null
1017
    ): ModelQueryBuilder {
1018 7
        assert(
1019 7
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $relationshipName),
1020 7
            "Relationship `$relationshipName` do not exist in model `" . $this->getModelClass() . '`'
1021
        );
1022
1023
        // as we read data from a relationship our main table and model would be the table/model in the relationship
1024
        // so 'root' model(s) will be located in the reverse relationship.
1025
1026
        list ($targetModelClass, $reverseRelName) =
1027 7
            $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $relationshipName);
1028
1029
        $builder = $this
1030 7
            ->createBuilder($targetModelClass)
1031 7
            ->selectModelColumns($columns)
1032 7
            ->fromModelTable();
1033
1034
        // 'root' filters would be applied to the data in the reverse relationship ...
1035 7
        if ($this->hasFilters() === true) {
1036 7
            $filters = $this->getFilters();
1037 7
            $sorts   = $this->getSorts();
1038 7
            $this->areFiltersWithAnd() ?
1039 6
                $builder->addRelationshipFiltersAndSortsWithAnd($reverseRelName, $filters, $sorts) :
1040 1
                $builder->addRelationshipFiltersAndSortsWithOr($reverseRelName, $filters, $sorts);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 1036 can also be of type null; however, Limoncello\Flute\Adapter...FiltersAndSortsWithOr() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1041
        }
1042
        // ... and the input filters to actual data we select
1043 7
        if ($relationshipFilters !== null) {
1044 4
            $builder->addFiltersWithAndToAlias($relationshipFilters);
1045
        }
1046 7
        if ($relationshipSorts !== null) {
1047 3
            $builder->addSorts($relationshipSorts);
1048
        }
1049
1050 7
        $this->applyPaging($builder);
1051
1052
        // While joining tables we select distinct rows.
1053 7
        $builder->distinct();
1054
1055 7
        return $this->builderOnReadRelationship($builder);
1056
    }
1057
1058
    /**
1059
     * @inheritdoc
1060
     *
1061
     * @throws DBALException
1062
     */
1063 6
    public function indexRelationship(
1064
        string $name,
1065
        iterable $relationshipFilters = null,
1066
        iterable $relationshipSorts = null
1067
    ) {
1068 6
        assert(
1069 6
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $name),
1070 6
            "Relationship `$name` do not exist in model `" . $this->getModelClass() . '`'
1071
        );
1072
1073
        // depending on the relationship type we expect the result to be either single resource or a collection
1074 6
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1075 6
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
1076 6
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
1077
1078 6
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts);
1079
1080 6
        $modelClass = $builder->getModelClass();
1081 6
        $data       = $isExpectMany === true ?
1082 6
            $this->fetchResources($builder, $modelClass) : $this->fetchResource($builder, $modelClass);
1083
1084 6
        return $data;
1085
    }
1086
1087
    /**
1088
     * @inheritdoc
1089
     *
1090
     * @throws DBALException
1091
     */
1092 2
    public function indexRelationshipIdentities(
1093
        string $name,
1094
        iterable $relationshipFilters = null,
1095
        iterable $relationshipSorts = null
1096
    ): array {
1097 2
        assert(
1098 2
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $name),
1099 2
            "Relationship `$name` do not exist in model `" . $this->getModelClass() . '`'
1100
        );
1101
1102
        // depending on the relationship type we expect the result to be either single resource or a collection
1103 2
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1104 2
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
1105 2
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
1106 2
        if ($isExpectMany === false) {
1107 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1108
        }
1109
1110 1
        list ($targetModelClass) = $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $name);
1111 1
        $targetPk = $this->getModelSchemas()->getPrimaryKey($targetModelClass);
1112
1113 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...
1114
1115 1
        $modelClass = $builder->getModelClass();
1116
        /** @var Generator $data */
1117 1
        $data   = $this->fetchColumn($builder, $modelClass, $targetPk);
1118 1
        $result = iterator_to_array($data);
1119
1120 1
        return $result;
1121
    }
1122
1123
    /**
1124
     * @inheritdoc
1125
     */
1126 3
    public function readRelationship(
1127
        $index,
1128
        string $name,
1129
        iterable $relationshipFilters = null,
1130
        iterable $relationshipSorts = null
1131
    ) {
1132 3
        return $this->withIndexFilter($index)->indexRelationship($name, $relationshipFilters, $relationshipSorts);
1133
    }
1134
1135
    /**
1136
     * @inheritdoc
1137
     */
1138 3
    public function hasInRelationship($parentId, string $name, $childId): bool
1139
    {
1140 3
        if ($parentId !== null && is_scalar($parentId) === false) {
1141 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1142
        }
1143 2
        if ($childId !== null && is_scalar($childId) === false) {
1144 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1145
        }
1146
1147 1
        $parentPkName  = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1148 1
        $parentFilters = [$parentPkName => [FilterParameterInterface::OPERATION_EQUALS => [$parentId]]];
1149 1
        list($childClass) = $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $name);
1150 1
        $childPkName  = $this->getModelSchemas()->getPrimaryKey($childClass);
1151 1
        $childFilters = [$childPkName => [FilterParameterInterface::OPERATION_EQUALS => [$childId]]];
1152
1153
        $data = $this
1154 1
            ->clearBuilderParameters()
1155 1
            ->clearFetchParameters()
1156 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...
1157 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...
1158
1159 1
        $has = empty($data->getData()) === false;
1160
1161 1
        return $has;
1162
    }
1163
1164
    /**
1165
     * @inheritdoc
1166
     *
1167
     * @throws DBALException
1168
     */
1169 1
    public function delete(): int
1170
    {
1171 1
        $deleted = $this->createDeleteBuilder()->execute();
1172
1173 1
        $this->clearFetchParameters();
1174
1175 1
        return (int)$deleted;
1176
    }
1177
1178
    /**
1179
     * @inheritdoc
1180
     *
1181
     * @throws DBALException
1182
     */
1183 5
    public function remove($index): bool
1184
    {
1185 5
        $this->withIndexFilter($index);
1186
1187 4
        $deleted = $this->createDeleteBuilder()->execute();
1188
1189 3
        $this->clearFetchParameters();
1190
1191 3
        return (int)$deleted > 0;
1192
    }
1193
1194
    /**
1195
     * @inheritdoc
1196
     *
1197
     * @throws DBALException
1198
     *
1199
     * @SuppressWarnings(PHPMD.StaticAccess)
1200
     */
1201 5
    public function create($index, iterable $attributes, iterable $toMany): string
1202
    {
1203 5
        if ($index !== null && is_int($index) === false && is_string($index) === false) {
1204 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1205
        }
1206
1207 4
        $allowedChanges = $this->filterAttributesOnCreate($index, $attributes);
1208
        $saveMain       = $this
1209 4
            ->createBuilder($this->getModelClass())
1210 4
            ->createModel($allowedChanges);
0 ignored issues
show
Documentation introduced by
$allowedChanges is of type object<Generator>, but the function expects a object<Limoncello\Flute\Adapters\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1211 4
        $saveMain       = $this->builderSaveResourceOnCreate($saveMain);
1212 4
        $saveMain->getSQL(); // prepare
1213
1214 4
        $this->clearBuilderParameters()->clearFetchParameters();
1215
1216
        $this->inTransaction(function () use ($saveMain, $toMany, &$index) {
1217 4
            $saveMain->execute();
1218
1219
            // if no index given will use last insert ID as index
1220 4
            $connection = $saveMain->getConnection();
1221 4
            $index !== null ?: $index = $connection->lastInsertId();
1222
1223 4
            $builderHook = Closure::fromCallable([$this, 'builderSaveRelationshipOnCreate']);
1224 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1225 2
                $this->addInToManyRelationship($connection, $index, $relationshipName, $secondaryIds, $builderHook);
1226
            }
1227 4
        });
1228
1229 4
        return $index;
1230
    }
1231
1232
    /**
1233
     * @inheritdoc
1234
     *
1235
     * @throws DBALException
1236
     *
1237
     * @SuppressWarnings(PHPMD.StaticAccess)
1238
     */
1239 5
    public function update($index, iterable $attributes, iterable $toMany): int
1240
    {
1241 5
        if (is_int($index) === false && is_string($index) === false) {
1242 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1243
        }
1244
1245 4
        $updated        = 0;
1246 4
        $pkName         = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1247
        $filters        = [
1248
            $pkName => [
1249 4
                FilterParameterInterface::OPERATION_EQUALS => [$index],
1250
            ],
1251
        ];
1252 4
        $allowedChanges = $this->filterAttributesOnUpdate($attributes);
1253
        $saveMain       = $this
1254 4
            ->createBuilder($this->getModelClass())
1255 4
            ->updateModels($allowedChanges)
0 ignored issues
show
Documentation introduced by
$allowedChanges is of type object<Generator>, but the function expects a object<Limoncello\Flute\Adapters\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1256 4
            ->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...
1257 4
        $saveMain       = $this->builderSaveResourceOnUpdate($saveMain);
1258 4
        $saveMain->getSQL(); // prepare
1259
1260 4
        $this->clearBuilderParameters()->clearFetchParameters();
1261
1262
        $this->inTransaction(function () use ($saveMain, $toMany, $index, &$updated) {
1263 4
            $updated = $saveMain->execute();
1264
1265 4
            $builderHook = Closure::fromCallable([$this, 'builderSaveRelationshipOnUpdate']);
1266 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1267 2
                $connection = $saveMain->getConnection();
1268
1269
                // clear existing
1270 2
                $this->builderCleanRelationshipOnUpdate(
1271 2
                    $relationshipName,
1272
                    $this
1273 2
                        ->createBuilderFromConnection($this->getConnection(), $this->getModelClass())
1274 2
                        ->clearToManyRelationship($relationshipName, $index)
1275 2
                )->execute();
1276
1277
                // add new ones
1278 2
                $updated   += $this->addInToManyRelationship(
1279 2
                    $connection,
1280 2
                    $index,
1281 2
                    $relationshipName,
1282 2
                    $secondaryIds,
1283 2
                    $builderHook
1284
                );
1285
            }
1286 4
        });
1287
1288 4
        return (int)$updated;
1289
    }
1290
1291
    /**
1292
     * @param string   $parentId
1293
     * @param string   $name
1294
     * @param iterable $childIds
1295
     *
1296
     * @return int
1297
     *
1298
     * @SuppressWarnings(PHPMD.StaticAccess)
1299
     */
1300 1
    public function createInBelongsToManyRelationship(string $parentId, string $name, iterable $childIds): int
1301
    {
1302
        // Check that relationship is `BelongsToMany`
1303
        assert(call_user_func(function () use ($name): bool {
1304 1
            $relType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1305 1
            $errMsg  = "Relationship `$name` of class `" . $this->getModelClass() .
1306 1
                '` either is not `belongsToMany` or do not exist in the class.';
1307 1
            $isOk = $relType === RelationshipTypes::BELONGS_TO_MANY;
1308
1309 1
            assert($isOk, $errMsg);
1310
1311 1
            return $isOk;
1312 1
        }));
1313
1314 1
        $builderHook = Closure::fromCallable([$this, 'builderOnCreateInBelongsToManyRelationship']);
1315
1316 1
        return $this->addInToManyRelationship($this->getConnection(), $parentId, $name, $childIds, $builderHook);
1317
    }
1318
1319
    /**
1320
     * @inheritdoc
1321
     *
1322
     * @throws DBALException
1323
     */
1324 1
    public function removeInBelongsToManyRelationship(string $parentId, string $name, iterable $childIds): int
1325
    {
1326
        // Check that relationship is `BelongsToMany`
1327
        assert(call_user_func(function () use ($name): bool {
1328 1
            $relType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1329 1
            $errMsg  = "Relationship `$name` of class `" . $this->getModelClass() .
1330 1
                '` either is not `belongsToMany` or do not exist in the class.';
1331 1
            $isOk = $relType === RelationshipTypes::BELONGS_TO_MANY;
1332
1333 1
            assert($isOk, $errMsg);
1334
1335 1
            return $isOk;
1336 1
        }));
1337
1338 1
        return $this->removeInToManyRelationship($this->getConnection(), $parentId, $name, $childIds);
1339
    }
1340
1341
    /**
1342
     * @return FactoryInterface
1343
     */
1344 50
    protected function getFactory(): FactoryInterface
1345
    {
1346 50
        return $this->factory;
1347
    }
1348
1349
    /**
1350
     * @return string
1351
     */
1352 51
    protected function getModelClass(): string
1353
    {
1354 51
        return $this->modelClass;
1355
    }
1356
1357
    /**
1358
     * @return ModelSchemaInfoInterface
1359
     */
1360 51
    protected function getModelSchemas(): ModelSchemaInfoInterface
1361
    {
1362 51
        return $this->modelSchemas;
1363
    }
1364
1365
    /**
1366
     * @return RelationshipPaginationStrategyInterface
1367
     */
1368 8
    protected function getRelationshipPagingStrategy(): RelationshipPaginationStrategyInterface
1369
    {
1370 8
        return $this->relPagingStrategy;
1371
    }
1372
1373
    /**
1374
     * @param Closure $closure
1375
     *
1376
     * @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...
1377
     *
1378
     * @throws DBALException
1379
     */
1380 8
    public function inTransaction(Closure $closure): void
1381
    {
1382 8
        $connection = $this->getConnection();
1383 8
        $connection->beginTransaction();
1384
        try {
1385 8
            $isOk = ($closure() === false ? null : true);
1386 8
        } finally {
1387 8
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
1388
        }
1389
    }
1390
1391
    /**
1392
     * @inheritdoc
1393
     *
1394
     * @throws DBALException
1395
     */
1396 22
    public function fetchResources(QueryBuilder $builder, string $modelClass): PaginatedDataInterface
1397
    {
1398 22
        $data = $this->fetchPaginatedResourcesWithoutRelationships($builder, $modelClass);
1399
1400 22
        if ($this->hasIncludes() === true) {
1401 13
            $this->loadRelationships($data);
1402 13
            $this->clearFetchParameters();
1403
        }
1404
1405 22
        return $data;
1406
    }
1407
1408
    /**
1409
     * @inheritdoc
1410
     *
1411
     * @throws DBALException
1412
     */
1413 14
    public function fetchResource(QueryBuilder $builder, string $modelClass)
1414
    {
1415 14
        $data = $this->fetchResourceWithoutRelationships($builder, $modelClass);
1416
1417 14
        if ($this->hasIncludes() === true) {
1418 8
            $this->loadRelationships($data);
1419 8
            $this->clearFetchParameters();
1420
        }
1421
1422 14
        return $data;
1423
    }
1424
1425
    /**
1426
     * @inheritdoc
1427
     *
1428
     * @throws DBALException
1429
     *
1430
     * @SuppressWarnings(PHPMD.ElseExpression)
1431
     */
1432 2
    public function fetchRow(QueryBuilder $builder, string $modelClass): ?array
1433
    {
1434 2
        $model = null;
1435
1436 2
        $statement = $builder->execute();
1437 2
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1438
1439 2
        if (($attributes = $statement->fetch()) !== false) {
1440 2
            if ($this->isFetchTyped() === true) {
1441 1
                $platform  = $builder->getConnection()->getDatabasePlatform();
1442 1
                $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1443 1
                $model     = $this->readRowFromAssoc($attributes, $typeNames, $platform);
1444
            } else {
1445 1
                $model = $attributes;
1446
            }
1447
        }
1448
1449 2
        $this->clearFetchParameters();
1450
1451 2
        return $model;
1452
    }
1453
1454
    /**
1455
     * @inheritdoc
1456
     *
1457
     * @throws DBALException
1458
     *
1459
     * @SuppressWarnings(PHPMD.StaticAccess)
1460
     * @SuppressWarnings(PHPMD.ElseExpression)
1461
     */
1462 5
    public function fetchColumn(QueryBuilder $builder, string $modelClass, string $columnName): iterable
1463
    {
1464 5
        $statement = $builder->execute();
1465 5
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1466
1467 5
        if ($this->isFetchTyped() === true) {
1468 4
            $platform = $builder->getConnection()->getDatabasePlatform();
1469 4
            $typeName = $this->getModelSchemas()->getAttributeTypes($modelClass)[$columnName];
1470 4
            $type     = Type::getType($typeName);
1471 4
            while (($attributes = $statement->fetch()) !== false) {
1472 4
                $value     = $attributes[$columnName];
1473 4
                $converted = $type->convertToPHPValue($value, $platform);
1474
1475 4
                yield $converted;
1476
            }
1477
        } else {
1478 1
            while (($attributes = $statement->fetch()) !== false) {
1479 1
                $value = $attributes[$columnName];
1480
1481 1
                yield $value;
1482
            }
1483
        }
1484
1485 5
        $this->clearFetchParameters();
1486
    }
1487
1488
    /**
1489
     * @param QueryBuilder $builder
1490
     *
1491
     * @return ModelQueryBuilder
1492
     */
1493 2
    protected function createCountBuilderFromBuilder(QueryBuilder $builder): ModelQueryBuilder
1494
    {
1495 2
        $countBuilder = $this->createBuilder($this->getModelClass());
1496 2
        $countBuilder->setParameters($builder->getParameters());
1497 2
        $countBuilder->select('COUNT(*)')->from('(' . $builder->getSQL() . ') AS RESULT');
1498
1499 2
        return $countBuilder;
1500
    }
1501
1502
    /**
1503
     * @param Connection $connection
1504
     * @param string     $primaryIdentity
1505
     * @param string     $name
1506
     * @param iterable   $secondaryIdentities
1507
     * @param Closure    $builderHook
1508
     *
1509
     * @return int
1510
     */
1511 5
    private function addInToManyRelationship(
1512
        Connection $connection,
1513
        string $primaryIdentity,
1514
        string $name,
1515
        iterable $secondaryIdentities,
1516
        Closure $builderHook
1517
    ): int {
1518 5
        $inserted = 0;
1519
1520 5
        $secondaryIdBindName = ':secondaryId';
1521
        $saveToMany          = $this
1522 5
            ->createBuilderFromConnection($connection, $this->getModelClass())
1523 5
            ->prepareCreateInToManyRelationship($name, $primaryIdentity, $secondaryIdBindName);
1524
1525 5
        $saveToMany = call_user_func($builderHook, $name, $saveToMany);
1526
1527 5
        foreach ($secondaryIdentities as $secondaryId) {
1528
            try {
1529 5
                $inserted += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1530 1
            } /** @noinspection PhpRedundantCatchClauseInspection */ catch (UcvException $exception) {
1531
                // Spec: If all of the specified resources can be added to, or are already present in,
1532
                // the relationship then the server MUST return a successful response.
1533
                //
1534
                // Currently DBAL cannot do insert or update in the same request.
1535
                // https://github.com/doctrine/dbal/issues/1320
1536 5
                continue;
1537
            }
1538
        }
1539
1540 5
        return $inserted;
1541
    }
1542
1543
    /**
1544
     * @param Connection $connection
1545
     * @param string     $primaryIdentity
1546
     * @param string     $name
1547
     * @param iterable   $secondaryIdentities
1548
     *
1549
     * @return int
1550
     *
1551
     * @throws DBALException
1552
     */
1553 1
    private function removeInToManyRelationship(
1554
        Connection $connection,
1555
        string $primaryIdentity,
1556
        string $name,
1557
        iterable $secondaryIdentities
1558
    ): int {
1559 1
        $removeToMany = $this->builderOnRemoveInBelongsToManyRelationship(
1560 1
            $name,
1561
            $this
1562 1
                ->createBuilderFromConnection($connection, $this->getModelClass())
1563 1
                ->prepareDeleteInToManyRelationship($name, $primaryIdentity, $secondaryIdentities)
1564
        );
1565 1
        $removed = $removeToMany->execute();
1566
1567 1
        return $removed;
1568
    }
1569
1570
    /**
1571
     * @param QueryBuilder $builder
1572
     * @param string       $modelClass
1573
     *
1574
     * @return mixed|null
1575
     *
1576
     * @throws DBALException
1577
     *
1578
     * @SuppressWarnings(PHPMD.ElseExpression)
1579
     */
1580 14
    private function fetchResourceWithoutRelationships(QueryBuilder $builder, string $modelClass)
1581
    {
1582 14
        $model     = null;
1583 14
        $statement = $builder->execute();
1584
1585 14
        if ($this->isFetchTyped() === true) {
1586 12
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1587 12
            if (($attributes = $statement->fetch()) !== false) {
1588 12
                $platform  = $builder->getConnection()->getDatabasePlatform();
1589 12
                $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1590 12
                $model     = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1591
            }
1592
        } else {
1593 2
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1594 2
            if (($fetched = $statement->fetch()) !== false) {
1595 2
                $model = $fetched;
1596
            }
1597
        }
1598
1599 14
        return $model;
1600
    }
1601
1602
    /**
1603
     * @param QueryBuilder $builder
1604
     * @param string       $modelClass
1605
     * @param string       $keyColumnName
1606
     *
1607
     * @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...
1608
     *
1609
     * @throws DBALException
1610
     *
1611
     * @SuppressWarnings(PHPMD.ElseExpression)
1612
     */
1613 9
    private function fetchResourcesWithoutRelationships(
1614
        QueryBuilder $builder,
1615
        string $modelClass,
1616
        string $keyColumnName
1617
    ): iterable {
1618 9
        $statement = $builder->execute();
1619
1620 9
        if ($this->isFetchTyped() === true) {
1621 8
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1622 8
            $platform  = $builder->getConnection()->getDatabasePlatform();
1623 8
            $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1624 8
            while (($attributes = $statement->fetch()) !== false) {
1625 6
                $model = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1626 6
                yield $model->{$keyColumnName} => $model;
1627
            }
1628
        } else {
1629 1
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1630 1
            while (($model = $statement->fetch()) !== false) {
1631 1
                yield $model->{$keyColumnName} => $model;
1632
            }
1633
        }
1634
    }
1635
1636
    /**
1637
     * @param QueryBuilder $builder
1638
     * @param string       $modelClass
1639
     *
1640
     * @return PaginatedDataInterface
1641
     *
1642
     * @throws DBALException
1643
     */
1644 27
    private function fetchPaginatedResourcesWithoutRelationships(
1645
        QueryBuilder $builder,
1646
        string $modelClass
1647
    ): PaginatedDataInterface {
1648 27
        list($models, $hasMore, $limit, $offset) = $this->fetchResourceCollection($builder, $modelClass);
1649
1650 27
        $data = $this->getFactory()
1651 27
            ->createPaginatedData($models)
1652 27
            ->markAsCollection()
1653 27
            ->setOffset($offset)
1654 27
            ->setLimit($limit);
1655
1656 27
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
1657
1658 27
        return $data;
1659
    }
1660
1661
    /**
1662
     * @param QueryBuilder $builder
1663
     * @param string       $modelClass
1664
     *
1665
     * @return array
1666
     *
1667
     * @throws DBALException
1668
     *
1669
     * @SuppressWarnings(PHPMD.ElseExpression)
1670
     */
1671 27
    private function fetchResourceCollection(QueryBuilder $builder, string $modelClass): array
1672
    {
1673 27
        $statement = $builder->execute();
1674
1675 27
        $models           = [];
1676 27
        $counter          = 0;
1677 27
        $hasMoreThanLimit = false;
1678 27
        $limit            = $builder->getMaxResults() !== null ? $builder->getMaxResults() - 1 : null;
1679
1680 27
        if ($this->isFetchTyped() === true) {
1681 26
            $platform  = $builder->getConnection()->getDatabasePlatform();
1682 26
            $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1683 26
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1684 26
            while (($attributes = $statement->fetch()) !== false) {
1685 25
                $counter++;
1686 25
                if ($limit !== null && $counter > $limit) {
1687 6
                    $hasMoreThanLimit = true;
1688 6
                    break;
1689
                }
1690 25
                $models[] = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1691
            }
1692
        } else {
1693 1
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1694 1
            while (($fetched = $statement->fetch()) !== false) {
1695 1
                $counter++;
1696 1
                if ($limit !== null && $counter > $limit) {
1697 1
                    $hasMoreThanLimit = true;
1698 1
                    break;
1699
                }
1700 1
                $models[] = $fetched;
1701
            }
1702
        }
1703
1704 27
        return [$models, $hasMoreThanLimit, $limit, $builder->getFirstResult()];
1705
    }
1706
1707
    /**
1708
     * @param null|string $index
1709
     * @param iterable    $attributes
1710
     *
1711
     * @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...
1712
     */
1713 4
    protected function filterAttributesOnCreate(?string $index, iterable $attributes): iterable
1714
    {
1715 4
        if ($index !== null) {
1716 1
            $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1717 1
            yield $pkName => $index;
1718
        }
1719
1720 4
        $knownAttrAndTypes = $this->getModelSchemas()->getAttributeTypes($this->getModelClass());
1721 4
        foreach ($attributes as $attribute => $value) {
1722 4
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1723 4
                yield $attribute => $value;
1724
            }
1725
        }
1726
    }
1727
1728
    /**
1729
     * @param iterable $attributes
1730
     *
1731
     * @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...
1732
     */
1733 4
    protected function filterAttributesOnUpdate(iterable $attributes): iterable
1734
    {
1735 4
        $knownAttrAndTypes = $this->getModelSchemas()->getAttributeTypes($this->getModelClass());
1736 4
        foreach ($attributes as $attribute => $value) {
1737 4
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1738 4
                yield $attribute => $value;
1739
            }
1740
        }
1741
    }
1742
1743
    /**
1744
     * @param TagStorageInterface   $modelsAtPath
1745
     * @param ArrayObject           $classAtPath
1746
     * @param ArrayObject           $idsAtPath
1747
     * @param ModelStorageInterface $deDup
1748
     * @param string                $parentsPath
1749
     * @param array                 $childRelationships
1750
     *
1751
     * @return void
1752
     *
1753
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1754
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
1755
     *
1756
     * @throws DBALException
1757
     */
1758 10
    private function loadRelationshipsLayer(
1759
        TagStorageInterface $modelsAtPath,
1760
        ArrayObject $classAtPath,
1761
        ArrayObject $idsAtPath,
1762
        ModelStorageInterface $deDup,
1763
        string $parentsPath,
1764
        array $childRelationships
1765
    ): void {
1766 10
        $rootClass   = $classAtPath[static::ROOT_PATH];
1767 10
        $parentClass = $classAtPath[$parentsPath];
1768 10
        $parents     = $modelsAtPath->get($parentsPath);
1769
1770
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
1771
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
1772
        // $models.
1773
1774 10
        $pkName = $this->getModelSchemas()->getPrimaryKey($parentClass);
1775
1776
        $registerModelAtPath = function ($model, string $path) use ($deDup, $modelsAtPath, $idsAtPath) {
1777 8
            return self::registerModelAtPath(
1778 8
                $model,
1779 8
                $path,
1780 8
                $this->getModelSchemas(),
1781 8
                $deDup,
1782 8
                $modelsAtPath,
1783 8
                $idsAtPath
1784
            );
1785 10
        };
1786
1787 10
        foreach ($childRelationships as $name) {
1788 10
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
1789
1790 10
            $relationshipType = $this->getModelSchemas()->getRelationshipType($parentClass, $name);
1791
            list ($targetModelClass, $reverseRelName) =
1792 10
                $this->getModelSchemas()->getReverseRelationship($parentClass, $name);
1793
1794
            $builder = $this
1795 10
                ->createBuilder($targetModelClass)
1796 10
                ->selectModelColumns()
1797 10
                ->fromModelTable();
1798
1799 10
            $classAtPath[$childrenPath] = $targetModelClass;
1800
1801
            switch ($relationshipType) {
1802 10
                case RelationshipTypes::BELONGS_TO:
1803
                    // some paths might not have any records in the database
1804 9
                    $areParentsLoaded = $idsAtPath->offsetExists($parentsPath);
1805 9
                    if ($areParentsLoaded === false) {
1806 1
                        continue;
1807
                    }
1808
                    // for 'belongsTo' relationship all resources could be read at once.
1809 9
                    $parentIds            = $idsAtPath[$parentsPath];
1810 9
                    $clonedBuilder        = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1811 9
                        $reverseRelName,
1812 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...
1813 9
                        null
1814
                    );
1815 9
                    $unregisteredChildren = $this->fetchResourcesWithoutRelationships(
1816 9
                        $clonedBuilder,
1817 9
                        $clonedBuilder->getModelClass(),
1818 9
                        $this->getModelSchemas()->getPrimaryKey($clonedBuilder->getModelClass())
1819
                    );
1820 9
                    $children             = [];
1821 9
                    foreach ($unregisteredChildren as $index => $unregisteredChild) {
1822 7
                        $children[$index] = $registerModelAtPath($unregisteredChild, $childrenPath);
1823
                    }
1824 9
                    $fkNameToChild = $this->getModelSchemas()->getForeignKey($parentClass, $name);
1825 9
                    foreach ($parents as $parent) {
1826 9
                        $fkToChild       = $parent->{$fkNameToChild};
1827 9
                        $parent->{$name} = $children[$fkToChild] ?? null;
1828
                    }
1829 9
                    break;
1830 8
                case RelationshipTypes::HAS_MANY:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

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

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

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

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

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

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

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

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

Loading history...
1832
                    // unfortunately we have paging limits for 'many' relationship thus we have read such
1833
                    // relationships for each 'parent' individually
1834 8
                    list ($queryOffset, $queryLimit) = $this->getRelationshipPagingStrategy()
1835 8
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
1836 8
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
1837 8
                    foreach ($parents as $parent) {
1838 8
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1839 8
                            $reverseRelName,
1840 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...
1841 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...
1842
                        );
1843 8
                        $children      = $this->fetchPaginatedResourcesWithoutRelationships(
1844 8
                            $clonedBuilder,
1845 8
                            $clonedBuilder->getModelClass()
1846
                        );
1847
1848 8
                        $deDupedChildren = [];
1849 8
                        foreach ($children->getData() as $child) {
1850 7
                            $deDupedChildren[] = $registerModelAtPath($child, $childrenPath);
1851
                        }
1852
1853 8
                        $paginated = $this->getFactory()
1854 8
                            ->createPaginatedData($deDupedChildren)
1855 8
                            ->markAsCollection()
1856 8
                            ->setOffset($children->getOffset())
1857 8
                            ->setLimit($children->getLimit());
1858 8
                        $children->hasMoreItems() === true ?
1859 8
                            $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
1860
1861 8
                        $parent->{$name} = $paginated;
1862
                    }
1863 10
                    break;
1864
            }
1865
        }
1866
    }
1867
1868
    /**
1869
     * @param string $message
1870
     *
1871
     * @return string
1872
     *
1873
     * @throws ContainerExceptionInterface
1874
     * @throws NotFoundExceptionInterface
1875
     */
1876 8
    private function getMessage(string $message): string
1877
    {
1878
        /** @var FormatterFactoryInterface $factory */
1879 8
        $factory   = $this->getContainer()->get(FormatterFactoryInterface::class);
1880 8
        $formatter = $factory->createFormatter(FluteSettings::GENERIC_NAMESPACE);
1881 8
        $result    = $formatter->formatMessage($message);
1882
1883 8
        return $result;
1884
    }
1885
1886
    /**
1887
     * @param string           $class
1888
     * @param array            $attributes
1889
     * @param Type[]           $typeNames
1890
     * @param AbstractPlatform $platform
1891
     *
1892
     * @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...
1893
     *
1894
     * @SuppressWarnings(PHPMD.StaticAccess)
1895
     *
1896
     * @throws DBALException
1897
     */
1898 34
    private function readResourceFromAssoc(
1899
        string $class,
1900
        array $attributes,
1901
        array $typeNames,
1902
        AbstractPlatform $platform
1903
    ) {
1904 34
        $instance = new $class();
1905 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...
1906 34
            $instance->{$name} = $value;
1907
        }
1908
1909 34
        return $instance;
1910
    }
1911
1912
    /**
1913
     * @param array            $attributes
1914
     * @param Type[]           $typeNames
1915
     * @param AbstractPlatform $platform
1916
     *
1917
     * @return array
1918
     *
1919
     * @SuppressWarnings(PHPMD.StaticAccess)
1920
     *
1921
     * @throws DBALException
1922
     */
1923 1
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
1924
    {
1925 1
        $row = [];
1926 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...
1927 1
            $row[$name] = $value;
1928
        }
1929
1930 1
        return $row;
1931
    }
1932
1933
    /**
1934
     * @param iterable         $attributes
1935
     * @param array            $typeNames
1936
     * @param AbstractPlatform $platform
1937
     *
1938
     * @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...
1939
     *
1940
     * @throws DBALException
1941
     */
1942 35
    private function readTypedAttributes(iterable $attributes, array $typeNames, AbstractPlatform $platform): iterable
1943
    {
1944 35
        foreach ($attributes as $name => $value) {
1945 35
            yield $name => (array_key_exists($name, $typeNames) === true ?
1946 35
                Type::getType($typeNames[$name])->convertToPHPValue($value, $platform) : $value);
1947
        }
1948
    }
1949
}
1950