Completed
Push — develop ( 26b032...524257 )
by Neomerx
18:05 queued 09:45
created

Crud::withIncludes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php namespace Limoncello\Flute\Api;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use ArrayObject;
20
use Closure;
21
use Doctrine\DBAL\Connection;
22
use Doctrine\DBAL\DBALException;
23
use Doctrine\DBAL\Driver\PDOConnection;
24
use Doctrine\DBAL\Platforms\AbstractPlatform;
25
use Doctrine\DBAL\Query\QueryBuilder;
26
use Doctrine\DBAL\Types\Type;
27
use Generator;
28
use Limoncello\Container\Traits\HasContainerTrait;
29
use Limoncello\Contracts\Data\ModelSchemeInfoInterface;
30
use Limoncello\Contracts\Data\RelationshipTypes;
31
use Limoncello\Flute\Adapters\ModelQueryBuilder;
32
use Limoncello\Flute\Contracts\Adapters\RelationshipPaginationStrategyInterface;
33
use Limoncello\Flute\Contracts\Api\CrudInterface;
34
use Limoncello\Flute\Contracts\FactoryInterface;
35
use Limoncello\Flute\Contracts\Http\Query\FilterParameterInterface;
36
use Limoncello\Flute\Contracts\Models\ModelStorageInterface;
37
use Limoncello\Flute\Contracts\Models\PaginatedDataInterface;
38
use Limoncello\Flute\Contracts\Models\TagStorageInterface;
39
use Limoncello\Flute\Exceptions\InvalidArgumentException;
40
use Limoncello\Flute\L10n\Messages;
41
use Limoncello\Flute\Validation\Traits\HasValidationFormatterTrait;
42
use Neomerx\JsonApi\Contracts\Document\DocumentInterface;
43
use Psr\Container\ContainerExceptionInterface;
44
use Psr\Container\ContainerInterface;
45
use Psr\Container\NotFoundExceptionInterface;
46
use Traversable;
47
48
/**
49
 * @package Limoncello\Flute
50
 *
51
 * @SuppressWarnings(PHPMD.TooManyMethods)
52
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
53
 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
54
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
55
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
56
 */
57
class Crud implements CrudInterface
58
{
59
    use HasContainerTrait, HasValidationFormatterTrait;
60
61
    /** Internal constant. Path constant. */
62
    protected const ROOT_PATH = '';
63
64
    /** Internal constant. Path constant. */
65
    protected const PATH_SEPARATOR = DocumentInterface::PATH_SEPARATOR;
66
67
    /**
68
     * @var FactoryInterface
69
     */
70
    private $factory;
71
72
    /**
73
     * @var string
74
     */
75
    private $modelClass;
76
77
    /**
78
     * @var ModelSchemeInfoInterface
79
     */
80
    private $modelSchemes;
81
82
    /**
83
     * @var RelationshipPaginationStrategyInterface
84
     */
85
    private $relPagingStrategy;
86
87
    /**
88
     * @var Connection
89
     */
90
    private $connection;
91
92
    /**
93
     * @var iterable|null
94
     */
95
    private $filterParameters = null;
96
97
    /**
98
     * @var bool
99
     */
100
    private $areFiltersWithAnd = true;
101
102
    /**
103
     * @var iterable|null
104
     */
105
    private $sortingParameters = null;
106
107
    /**
108
     * @var array
109
     */
110
    private $relFiltersAndSorts = [];
111
112
    /**
113
     * @var iterable|null
114
     */
115
    private $includePaths = null;
116
117
    /**
118
     * @var int|null
119
     */
120
    private $pagingOffset = null;
121
122
    /**
123
     * @var Closure|null
124
     */
125
    private $columnMapper = null;
126
127
    /**
128
     * @var bool
129
     */
130
    private $isFetchTyped;
131
132
    /**
133
     * @var int|null
134
     */
135
    private $pagingLimit = null;
136
137
    /** internal constant */
138
    private const REL_FILTERS_AND_SORTS__FILTERS = 0;
139
140
    /** internal constant */
141
    private const REL_FILTERS_AND_SORTS__SORTS = 1;
142
143
    /**
144
     * @param ContainerInterface $container
145
     * @param string             $modelClass
146
     *
147
     * @throws ContainerExceptionInterface
148
     * @throws NotFoundExceptionInterface
149
     */
150 59
    public function __construct(ContainerInterface $container, string $modelClass)
151
    {
152 59
        $this->setContainer($container);
153
154 59
        $this->modelClass        = $modelClass;
155 59
        $this->factory           = $this->getContainer()->get(FactoryInterface::class);
156 59
        $this->modelSchemes      = $this->getContainer()->get(ModelSchemeInfoInterface::class);
157 59
        $this->relPagingStrategy = $this->getContainer()->get(RelationshipPaginationStrategyInterface::class);
158 59
        $this->connection        = $this->getContainer()->get(Connection::class);
159
160 59
        $this->clearBuilderParameters()->clearFetchParameters();
161
    }
162
163
    /**
164
     * @param Closure $mapper
165
     *
166
     * @return self
167
     */
168 1
    public function withColumnMapper(Closure $mapper): self
169
    {
170 1
        $this->columnMapper = $mapper;
171
172 1
        return $this;
173
    }
174
175
    /**
176
     * @inheritdoc
177
     */
178 42
    public function withFilters(iterable $filterParameters): CrudInterface
179
    {
180 42
        $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...
181
182 42
        return $this;
183
    }
184
185
    /**
186
     * @inheritdoc
187
     */
188 23
    public function withIndexFilter($index): CrudInterface
189
    {
190 23
        if (is_int($index) === false && is_string($index) === false) {
191 3
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
192
        }
193
194 20
        $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
195 20
        $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...
196
            $pkName => [
197 20
                FilterParameterInterface::OPERATION_EQUALS => [$index],
198
            ],
199
        ]);
200
201 20
        return $this;
202
    }
203
204
    /**
205
     * @inheritdoc
206
     */
207 7
    public function withRelationshipFilters(string $name, iterable $filters): CrudInterface
208
    {
209 7
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__FILTERS] = $filters;
210
211 7
        return $this;
212
    }
213
214
    /**
215
     * @inheritdoc
216
     */
217 1
    public function withRelationshipSorts(string $name, iterable $sorts): CrudInterface
218
    {
219 1
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__SORTS] = $sorts;
220
221 1
        return $this;
222
    }
223
224
    /**
225
     * @inheritdoc
226
     */
227 15
    public function combineWithAnd(): CrudInterface
228
    {
229 15
        $this->areFiltersWithAnd = true;
230
231 15
        return $this;
232
    }
233
234
    /**
235
     * @inheritdoc
236
     */
237 2
    public function combineWithOr(): CrudInterface
238
    {
239 2
        $this->areFiltersWithAnd = false;
240
241 2
        return $this;
242
    }
243
244
    /**
245
     * @return bool
246
     */
247 34
    private function hasColumnMapper(): bool
248
    {
249 34
        return $this->columnMapper !== null;
250
    }
251
252
    /**
253
     * @return Closure
254
     */
255 1
    private function getColumnMapper(): Closure
256
    {
257 1
        return $this->columnMapper;
258
    }
259
260
    /**
261
     * @return bool
262
     */
263 45
    private function hasFilters(): bool
264
    {
265 45
        return empty($this->filterParameters) === false;
266
    }
267
268
    /**
269
     * @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...
270
     */
271 35
    private function getFilters(): iterable
272
    {
273 35
        return $this->filterParameters;
274
    }
275
276
    /**
277
     * @return bool
278
     */
279 35
    private function areFiltersWithAnd(): bool
280
    {
281 35
        return $this->areFiltersWithAnd;
282
    }
283
284
    /**
285
     * @inheritdoc
286
     */
287 17
    public function withSorts(iterable $sortingParameters): CrudInterface
288
    {
289 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...
290
291 17
        return $this;
292
    }
293
294
    /**
295
     * @return bool
296
     */
297 34
    private function hasSorts(): bool
298
    {
299 34
        return empty($this->sortingParameters) === false;
300
    }
301
302
    /**
303
     * @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...
304
     */
305 14
    private function getSorts(): ?iterable
306
    {
307 14
        return $this->sortingParameters;
308
    }
309
310
    /**
311
     * @inheritdoc
312
     */
313 20
    public function withIncludes(iterable $includePaths): CrudInterface
314
    {
315 20
        $this->includePaths = $includePaths;
0 ignored issues
show
Documentation Bug introduced by
It seems like $includePaths of type object<Limoncello\Flute\Contracts\Api\iterable> is incompatible with the declared type object<Limoncello\Flute\Api\iterable>|null of property $includePaths.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
316
317 20
        return $this;
318
    }
319
320
    /**
321
     * @return bool
322
     */
323 36
    private function hasIncludes(): bool
324
    {
325 36
        return empty($this->includePaths) === false;
326
    }
327
328
    /**
329
     * @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...
330
     */
331 20
    private function getIncludes(): iterable
332
    {
333 20
        return $this->includePaths;
334
    }
335
336
    /**
337
     * @inheritdoc
338
     */
339 19
    public function withPaging(int $offset, int $limit): CrudInterface
340
    {
341 19
        $this->pagingOffset = $offset;
342 19
        $this->pagingLimit  = $limit;
343
344 19
        return $this;
345
    }
346
347
    /**
348
     * @return self
349
     */
350 59
    public function shouldBeTyped(): self
351
    {
352 59
        $this->isFetchTyped = true;
353
354 59
        return $this;
355
    }
356
357
    /**
358
     * @return self
359
     */
360 4
    public function shouldBeUntyped(): self
361
    {
362 4
        $this->isFetchTyped = false;
363
364 4
        return $this;
365
    }
366
367
    /**
368
     * @return bool
369
     */
370 43
    private function hasPaging(): bool
371
    {
372 43
        return $this->pagingOffset !== null && $this->pagingLimit !== null;
373
    }
374
375
    /**
376
     * @return int
377
     */
378 18
    private function getPagingOffset(): int
379
    {
380 18
        return $this->pagingOffset;
381
    }
382
383
    /**
384
     * @return int
385
     */
386 18
    private function getPagingLimit(): int
387
    {
388 18
        return $this->pagingLimit;
389
    }
390
391
    /**
392
     * @return bool
393
     */
394 41
    private function isFetchTyped(): bool
395
    {
396 41
        return $this->isFetchTyped;
397
    }
398
399
    /**
400
     * @return Connection
401
     */
402 46
    protected function getConnection(): Connection
403
    {
404 46
        return $this->connection;
405
    }
406
407
    /**
408
     * @param string $modelClass
409
     *
410
     * @return ModelQueryBuilder
411
     */
412 46
    protected function createBuilder(string $modelClass): ModelQueryBuilder
413
    {
414 46
        return $this->createBuilderFromConnection($this->getConnection(), $modelClass);
415
    }
416
417
    /**
418
     * @param Connection $connection
419
     * @param string     $modelClass
420
     *
421
     * @return ModelQueryBuilder
422
     */
423 46
    private function createBuilderFromConnection(Connection $connection, string $modelClass): ModelQueryBuilder
424
    {
425 46
        return $this->getFactory()->createModelQueryBuilder($connection, $modelClass, $this->getModelSchemes());
426
    }
427
428
    /**
429
     * @param ModelQueryBuilder $builder
430
     *
431
     * @return Crud
432
     */
433 34
    protected function applyColumnMapper(ModelQueryBuilder $builder): self
434
    {
435 34
        if ($this->hasColumnMapper() === true) {
436 1
            $builder->setColumnToDatabaseMapper($this->getColumnMapper());
437
        }
438
439 34
        return $this;
440
    }
441
442
    /**
443
     * @param ModelQueryBuilder $builder
444
     *
445
     * @return Crud
446
     *
447
     * @throws DBALException
448
     */
449 34
    protected function applyAliasFilters(ModelQueryBuilder $builder): self
450
    {
451 34
        if ($this->hasFilters() === true) {
452 24
            $filters = $this->getFilters();
453 24
            $this->areFiltersWithAnd() === true ?
454 24
                $builder->addFiltersWithAndToAlias($filters) : $builder->addFiltersWithOrToAlias($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 452 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 452 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...
455
        }
456
457 34
        return $this;
458
    }
459
460
    /**
461
     * @param ModelQueryBuilder $builder
462
     *
463
     * @return self
464
     *
465
     * @throws DBALException
466
     */
467 5
    protected function applyTableFilters(ModelQueryBuilder $builder): self
468
    {
469 5
        if ($this->hasFilters() === true) {
470 5
            $filters = $this->getFilters();
471 5
            $this->areFiltersWithAnd() === true ?
472 5
                $builder->addFiltersWithAndToTable($filters) : $builder->addFiltersWithOrToTable($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 470 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 470 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...
473
        }
474
475 5
        return $this;
476
    }
477
478
    /**
479
     * @param ModelQueryBuilder $builder
480
     *
481
     * @return self
482
     *
483
     * @throws DBALException
484
     */
485 34
    protected function applyRelationshipFiltersAndSorts(ModelQueryBuilder $builder): self
486
    {
487
        // While joining tables we select distinct rows. This flag used to apply `distinct` no more than once.
488 34
        $distinctApplied = false;
489
490 34
        foreach ($this->relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
491 7
            assert(is_string($relationshipName) === true && is_array($filtersAndSorts) === true);
492 7
            $builder->addRelationshipFiltersAndSortsWithAnd(
493 7
                $relationshipName,
494 7
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__FILTERS] ?? [],
495 7
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__SORTS] ?? []
496
            );
497
498 7
            if ($distinctApplied === false) {
499 7
                $builder->distinct();
500 7
                $distinctApplied = true;
501
            }
502
        }
503
504 34
        return $this;
505
    }
506
507
    /**
508
     * @param ModelQueryBuilder $builder
509
     *
510
     * @return self
511
     */
512 34
    protected function applySorts(ModelQueryBuilder $builder): self
513
    {
514 34
        if ($this->hasSorts() === true) {
515 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...
516
        }
517
518 34
        return $this;
519
    }
520
521
    /**
522
     * @param ModelQueryBuilder $builder
523
     *
524
     * @return self
525
     */
526 43
    protected function applyPaging(ModelQueryBuilder $builder): self
527
    {
528 43
        if ($this->hasPaging() === true) {
529 18
            $builder->setFirstResult($this->getPagingOffset());
530 18
            $builder->setMaxResults($this->getPagingLimit() + 1);
531
        }
532
533 43
        return $this;
534
    }
535
536
    /**
537
     * @return self
538
     */
539 59
    protected function clearBuilderParameters(): self
540
    {
541 59
        $this->columnMapper       = null;
542 59
        $this->filterParameters   = null;
543 59
        $this->areFiltersWithAnd  = true;
544 59
        $this->sortingParameters  = null;
545 59
        $this->pagingOffset       = null;
546 59
        $this->pagingLimit        = null;
547 59
        $this->relFiltersAndSorts = [];
548
549 59
        return $this;
550
    }
551
552
    /**
553
     * @return self
554
     */
555 59
    private function clearFetchParameters(): self
556
    {
557 59
        $this->includePaths = null;
558 59
        $this->shouldBeTyped();
559
560 59
        return $this;
561
    }
562
563
    /**
564
     * @param QueryBuilder $builder
565
     *
566
     * @return QueryBuilder
567
     */
568 2
    protected function builderOnCount(QueryBuilder $builder): QueryBuilder
569
    {
570 2
        return $builder;
571
    }
572
573
    /**
574
     * @param ModelQueryBuilder $builder
575
     *
576
     * @return ModelQueryBuilder
577
     */
578 34
    protected function builderOnIndex(ModelQueryBuilder $builder): ModelQueryBuilder
579
    {
580 34
        return $builder;
581
    }
582
583
    /**
584
     * @param ModelQueryBuilder $builder
585
     *
586
     * @return ModelQueryBuilder
587
     */
588 10
    protected function builderOnReadRelationship(ModelQueryBuilder $builder): ModelQueryBuilder
589
    {
590 10
        return $builder;
591
    }
592
593
    /**
594
     * @param ModelQueryBuilder $builder
595
     *
596
     * @return ModelQueryBuilder
597
     */
598 4
    protected function builderSaveResourceOnCreate(ModelQueryBuilder $builder): ModelQueryBuilder
599
    {
600 4
        return $builder;
601
    }
602
603
    /**
604
     * @param ModelQueryBuilder $builder
605
     *
606
     * @return ModelQueryBuilder
607
     */
608 4
    protected function builderSaveResourceOnUpdate(ModelQueryBuilder $builder): ModelQueryBuilder
609
    {
610 4
        return $builder;
611
    }
612
613
    /**
614
     * @param string            $relationshipName
615
     * @param ModelQueryBuilder $builder
616
     *
617
     * @return ModelQueryBuilder
618
     *
619
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
620
     */
621 2
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
622
        $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...
623
        ModelQueryBuilder $builder
624
    ): ModelQueryBuilder {
625 2
        return $builder;
626
    }
627
628
    /**
629
     * @param string            $relationshipName
630
     * @param ModelQueryBuilder $builder
631
     *
632
     * @return ModelQueryBuilder
633
     *
634
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
635
     */
636 2
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
637
        $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...
638
        ModelQueryBuilder $builder
639
    ): ModelQueryBuilder {
640 2
        return $builder;
641
    }
642
643
    /**
644
     * @param string            $relationshipName
645
     * @param ModelQueryBuilder $builder
646
     *
647
     * @return ModelQueryBuilder
648
     *
649
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
650
     */
651 2
    protected function builderCleanRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
652
        $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...
653
        ModelQueryBuilder $builder
654
    ): ModelQueryBuilder {
655 2
        return $builder;
656
    }
657
658
    /**
659
     * @param ModelQueryBuilder $builder
660
     *
661
     * @return ModelQueryBuilder
662
     */
663 5
    protected function builderOnDelete(ModelQueryBuilder $builder): ModelQueryBuilder
664
    {
665 5
        return $builder;
666
    }
667
668
    /**
669
     * @param PaginatedDataInterface|mixed|null $data
670
     *
671
     * @return void
672
     *
673
     * @SuppressWarnings(PHPMD.ElseExpression)
674
     *
675
     * @throws DBALException
676
     */
677 20
    private function loadRelationships($data): void
678
    {
679 20
        $isPaginated = $data instanceof PaginatedDataInterface;
680 20
        $hasData     = ($isPaginated === true && empty($data->getData()) === false) ||
681 20
            ($isPaginated === false && $data !== null);
682
683 20
        if ($hasData === true && $this->hasIncludes() === true) {
684 20
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemes());
685 20
            $modelsAtPath = $this->getFactory()->createTagStorage();
686
687
            // we gonna send these objects via function params so it is an equivalent for &array
688 20
            $classAtPath = new ArrayObject();
689 20
            $idsAtPath   = new ArrayObject();
690
691 20
            $registerModelAtRoot = function ($model) use ($modelStorage, $modelsAtPath, $idsAtPath): void {
692 20
                self::registerModelAtPath(
693 20
                    $model,
694 20
                    static::ROOT_PATH,
695 20
                    $this->getModelSchemes(),
696 20
                    $modelStorage,
697 20
                    $modelsAtPath,
698 20
                    $idsAtPath
699
                );
700 20
            };
701
702 20
            $model = null;
703 20
            if ($isPaginated === true) {
704 13
                foreach ($data->getData() as $model) {
705 13
                    $registerModelAtRoot($model);
706
                }
707
            } else {
708 7
                $model = $data;
709 7
                $registerModelAtRoot($model);
710
            }
711 20
            assert($model !== null);
712 20
            $classAtPath[static::ROOT_PATH] = get_class($model);
713
714 20
            foreach ($this->getPaths($this->getIncludes()) as list ($parentPath, $childPaths)) {
0 ignored issues
show
Bug introduced by
It seems like $this->getIncludes() can be null; however, getPaths() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
715 9
                $this->loadRelationshipsLayer(
716 9
                    $modelsAtPath,
717 9
                    $classAtPath,
718 9
                    $idsAtPath,
719 9
                    $modelStorage,
720 9
                    $parentPath,
721 9
                    $childPaths
722
                );
723
            }
724
        }
725
    }
726
727
    /**
728
     * A helper to remember all model related data. Helps to ensure we consistently handle models in CRUD.
729
     *
730
     * @param mixed                    $model
731
     * @param string                   $path
732
     * @param ModelSchemeInfoInterface $modelSchemes
733
     * @param ModelStorageInterface    $modelStorage
734
     * @param TagStorageInterface      $modelsAtPath
735
     * @param ArrayObject              $idsAtPath
736
     *
737
     * @return mixed
738
     */
739 20
    private static function registerModelAtPath(
740
        $model,
741
        string $path,
742
        ModelSchemeInfoInterface $modelSchemes,
743
        ModelStorageInterface $modelStorage,
744
        TagStorageInterface $modelsAtPath,
745
        ArrayObject $idsAtPath
746
    ) {
747 20
        $uniqueModel = $modelStorage->register($model);
748 20
        if ($uniqueModel !== null) {
749 20
            $modelsAtPath->register($uniqueModel, $path);
750 20
            $pkName             = $modelSchemes->getPrimaryKey(get_class($uniqueModel));
751 20
            $modelId            = $uniqueModel->{$pkName};
752 20
            $idsAtPath[$path][] = $modelId;
753
        }
754
755 20
        return $uniqueModel;
756
    }
757
758
    /**
759
     * @param iterable $paths (string[])
760
     *
761
     * @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...
762
     */
763 20
    private static function getPaths(iterable $paths): iterable
764
    {
765
        // The idea is to normalize paths. It means build all intermediate paths.
766
        // e.g. if only `a.b.c` path it given it will be normalized to `a`, `a.b` and `a.b.c`.
767
        // Path depths store depth of each path (e.g. 0 for root, 1 for `a`, 2 for `a.b` and etc).
768
        // It is needed for yielding them in correct order (from top level to bottom).
769 20
        $normalizedPaths = [];
770 20
        $pathsDepths     = [];
771 20
        foreach ($paths as $path) {
772 9
            assert(is_array($path) || $path instanceof Traversable);
773 9
            $parentDepth = 0;
774 9
            $tmpPath     = static::ROOT_PATH;
775 9
            foreach ($path as $pathPiece) {
776 9
                assert(is_string($pathPiece));
777 9
                $parent                    = $tmpPath;
778 9
                $tmpPath                   = empty($tmpPath) === true ?
779 9
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
780 9
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
781 9
                $pathsDepths[$parent]      = $parentDepth++;
782
            }
783
        }
784
785
        // Here we collect paths in form of parent => [list of children]
786
        // 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...
787 20
        $parentWithChildren = [];
788 20
        foreach ($normalizedPaths as $path => list ($parent, $childPath)) {
789 9
            $parentWithChildren[$parent][] = $childPath;
790
        }
791
792
        // And finally sort by path depth and yield parent with its children. Top level paths first then deeper ones.
793 20
        asort($pathsDepths, SORT_NUMERIC);
794 20
        foreach ($pathsDepths as $parent => $depth) {
795 9
            assert($depth !== null); // suppress unused
796 9
            $childPaths = $parentWithChildren[$parent];
797 9
            yield [$parent, $childPaths];
798
        }
799
    }
800
801
    /**
802
     * @inheritdoc
803
     */
804 2
    public function createIndexBuilder(iterable $columns = null): QueryBuilder
805
    {
806 2
        return $this->createIndexModelBuilder($columns);
807
    }
808
809
    /**
810
     * @inheritdoc
811
     */
812 5
    public function createDeleteBuilder(): QueryBuilder
813
    {
814 5
        return $this->createDeleteModelBuilder();
815
    }
816
817
    /**
818
     * @param iterable|null $columns
819
     *
820
     * @return ModelQueryBuilder
821
     *
822
     * @throws DBALException
823
     */
824 34
    protected function createIndexModelBuilder(iterable $columns = null): ModelQueryBuilder
825
    {
826 34
        $builder = $this->createBuilder($this->getModelClass());
827
828
        $this
829 34
            ->applyColumnMapper($builder);
830
831
        $builder
832 34
            ->selectModelColumns($columns)
833 34
            ->fromModelTable();
834
835
        $this
836 34
            ->applyAliasFilters($builder)
837 34
            ->applySorts($builder)
838 34
            ->applyRelationshipFiltersAndSorts($builder)
839 34
            ->applyPaging($builder);
840
841 34
        $result = $this->builderOnIndex($builder);
842
843 34
        $this->clearBuilderParameters();
844
845 34
        return $result;
846
    }
847
848
    /**
849
     * @return ModelQueryBuilder
850
     *
851
     * @throws DBALException
852
     */
853 5
    protected function createDeleteModelBuilder(): ModelQueryBuilder
854
    {
855
        $builder = $this
856 5
            ->createBuilder($this->getModelClass())
857 5
            ->deleteModels();
858
859 5
        $this->applyTableFilters($builder);
860
861 5
        $result = $this->builderOnDelete($builder);
862
863 5
        $this->clearBuilderParameters();
864
865 5
        return $result;
866
    }
867
868
    /**
869
     * @inheritdoc
870
     */
871 16
    public function index(): PaginatedDataInterface
872
    {
873 16
        $builder = $this->createIndexModelBuilder();
874 16
        $data    = $this->fetchResources($builder, $builder->getModelClass());
875
876 16
        return $data;
877
    }
878
879
    /**
880
     * @inheritdoc
881
     */
882 2
    public function indexIdentities(): array
883
    {
884 2
        $pkName  = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
885 2
        $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...
886
        /** @var Generator $data */
887 2
        $data   = $this->fetchColumn($builder, $builder->getModelClass(), $pkName);
888 2
        $result = iterator_to_array($data);
889
890 2
        return $result;
891
    }
892
893
    /**
894
     * @inheritdoc
895
     */
896 14
    public function read($index)
897
    {
898 14
        $this->withIndexFilter($index);
899
900 12
        $builder = $this->createIndexModelBuilder();
901 12
        $data    = $this->fetchResource($builder, $builder->getModelClass());
902
903 12
        return $data;
904
    }
905
906
    /**
907
     * @inheritdoc
908
     */
909 2
    public function count(): ?int
910
    {
911 2
        $result = $this->builderOnCount(
912 2
            $this->createCountBuilderFromBuilder($this->createIndexModelBuilder())
913 2
        )->execute()->fetchColumn();
914
915 2
        return $result === false ? null : $result;
916
    }
917
918
    /**
919
     * @param string        $relationshipName
920
     * @param iterable|null $relationshipFilters
921
     * @param iterable|null $relationshipSorts
922
     * @param iterable|null $columns
923
     *
924
     * @return ModelQueryBuilder
925
     *
926
     * @throws DBALException
927
     */
928 10
    public function createReadRelationshipBuilder(
929
        string $relationshipName,
930
        iterable $relationshipFilters = null,
931
        iterable $relationshipSorts = null,
932
        iterable $columns = null
933
    ): ModelQueryBuilder {
934 10
        assert(
935 10
            $this->getModelSchemes()->hasRelationship($this->getModelClass(), $relationshipName),
936 10
            "Relationship `$relationshipName` do not exist in model `" . $this->getModelClass() . '`'
937
        );
938
939
        // as we read data from a relationship our main table and model would be the table/model in the relationship
940
        // so 'root' model(s) will be located in the reverse relationship.
941
942
        list ($targetModelClass, $reverseRelName) =
943 10
            $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $relationshipName);
944
945
        $builder = $this
946 10
            ->createBuilder($targetModelClass)
947 10
            ->selectModelColumns($columns)
948 10
            ->fromModelTable();
949
950
        // 'root' filters would be applied to the data in the reverse relationship ...
951 10
        if ($this->hasFilters() === true) {
952 10
            $filters = $this->getFilters();
953 10
            $sorts   = $this->getSorts();
954 10
            $this->areFiltersWithAnd() ?
955 9
                $builder->addRelationshipFiltersAndSortsWithAnd($reverseRelName, $filters, $sorts) :
956 1
                $builder->addRelationshipFiltersAndSortsWithOr($reverseRelName, $filters, $sorts);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 952 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...
957
        }
958
        // ... and the input filters to actual data we select
959 10
        if ($relationshipFilters !== null) {
960 7
            $builder->addFiltersWithAndToAlias($relationshipFilters);
961
        }
962 10
        if ($relationshipSorts !== null) {
963 3
            $builder->addSorts($relationshipSorts);
964
        }
965
966 10
        $this->applyPaging($builder);
967
968
        // While joining tables we select distinct rows.
969 10
        $builder->distinct();
970
971 10
        return $this->builderOnReadRelationship($builder);
972
    }
973
974
    /**
975
     * @inheritdoc
976
     */
977 9
    public function indexRelationship(
978
        string $name,
979
        iterable $relationshipFilters = null,
980
        iterable $relationshipSorts = null
981
    ) {
982 9
        assert(
983 9
            $this->getModelSchemes()->hasRelationship($this->getModelClass(), $name),
984 9
            "Relationship `$name` do not exist in model `" . $this->getModelClass() . '`'
985
        );
986
987
        // depending on the relationship type we expect the result to be either single resource or a collection
988 9
        $relationshipType = $this->getModelSchemes()->getRelationshipType($this->getModelClass(), $name);
989 9
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
990 9
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
991
992 9
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts);
993
994 9
        $modelClass = $builder->getModelClass();
995 9
        $data       = $isExpectMany === true ?
996 9
            $this->fetchResources($builder, $modelClass) : $this->fetchResource($builder, $modelClass);
997
998 9
        return $data;
999
    }
1000
1001
    /**
1002
     * @inheritdoc
1003
     */
1004 2
    public function indexRelationshipIdentities(
1005
        string $name,
1006
        iterable $relationshipFilters = null,
1007
        iterable $relationshipSorts = null
1008
    ): array {
1009 2
        assert(
1010 2
            $this->getModelSchemes()->hasRelationship($this->getModelClass(), $name),
1011 2
            "Relationship `$name` do not exist in model `" . $this->getModelClass() . '`'
1012
        );
1013
1014
        // depending on the relationship type we expect the result to be either single resource or a collection
1015 2
        $relationshipType = $this->getModelSchemes()->getRelationshipType($this->getModelClass(), $name);
1016 2
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
1017 2
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
1018 2
        if ($isExpectMany === false) {
1019 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1020
        }
1021
1022 1
        list ($targetModelClass) = $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $name);
1023 1
        $targetPk = $this->getModelSchemes()->getPrimaryKey($targetModelClass);
1024
1025 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...
1026
1027 1
        $modelClass = $builder->getModelClass();
1028
        /** @var Generator $data */
1029 1
        $data   = $this->fetchColumn($builder, $modelClass, $targetPk);
1030 1
        $result = iterator_to_array($data);
1031
1032 1
        return $result;
1033
    }
1034
1035
    /**
1036
     * @inheritdoc
1037
     */
1038 3
    public function readRelationship(
1039
        $index,
1040
        string $name,
1041
        iterable $relationshipFilters = null,
1042
        iterable $relationshipSorts = null
1043
    ) {
1044 3
        return $this->withIndexFilter($index)->indexRelationship($name, $relationshipFilters, $relationshipSorts);
1045
    }
1046
1047
    /**
1048
     * @inheritdoc
1049
     */
1050 6
    public function hasInRelationship($parentId, string $name, $childId): bool
1051
    {
1052 6
        if ($parentId !== null && is_scalar($parentId) === false) {
1053 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1054
        }
1055 5
        if ($childId !== null && is_scalar($childId) === false) {
1056 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1057
        }
1058
1059 4
        $parentPkName  = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
1060 4
        $parentFilters = [$parentPkName => [FilterParameterInterface::OPERATION_EQUALS => [$parentId]]];
1061 4
        list($childClass) = $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $name);
1062 4
        $childPkName  = $this->getModelSchemes()->getPrimaryKey($childClass);
1063 4
        $childFilters = [$childPkName => [FilterParameterInterface::OPERATION_EQUALS => [$childId]]];
1064
1065
        $data = $this
1066 4
            ->clearBuilderParameters()
1067 4
            ->clearFetchParameters()
1068 4
            ->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...
1069 4
            ->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...
1070
1071 4
        $has = empty($data->getData()) === false;
1072
1073 4
        return $has;
1074
    }
1075
1076
    /**
1077
     * @inheritdoc
1078
     */
1079 1
    public function delete(): int
1080
    {
1081 1
        $deleted = $this->createDeleteBuilder()->execute();
1082
1083 1
        $this->clearFetchParameters();
1084
1085 1
        return (int)$deleted;
1086
    }
1087
1088
    /**
1089
     * @inheritdoc
1090
     */
1091 6
    public function remove($index): bool
1092
    {
1093 6
        $this->withIndexFilter($index);
1094
1095 5
        $deleted = $this->createDeleteBuilder()->execute();
1096
1097 4
        $this->clearFetchParameters();
1098
1099 4
        return (int)$deleted > 0;
1100
    }
1101
1102
    /**
1103
     * @inheritdoc
1104
     */
1105 5
    public function create($index, iterable $attributes, iterable $toMany): string
1106
    {
1107 5
        if ($index !== null && is_int($index) === false && is_string($index) === false) {
1108 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1109
        }
1110
1111 4
        $allowedChanges = $this->filterAttributesOnCreate($index, $attributes);
1112
        $saveMain       = $this
1113 4
            ->createBuilder($this->getModelClass())
1114 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...
1115 4
        $saveMain       = $this->builderSaveResourceOnCreate($saveMain);
1116 4
        $saveMain->getSQL(); // prepare
1117
1118 4
        $this->clearBuilderParameters()->clearFetchParameters();
1119
1120 4
        $this->inTransaction(function () use ($saveMain, $toMany, &$index) {
1121 4
            $saveMain->execute();
1122
1123
            // if no index given will use last insert ID as index
1124 4
            $index !== null ?: $index = $saveMain->getConnection()->lastInsertId();
1125
1126 4
            $inserted = 0;
1127 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1128 2
                $secondaryIdBindName = ':secondaryId';
1129 2
                $saveToMany          = $this->builderSaveRelationshipOnCreate(
1130 2
                    $relationshipName,
1131
                    $this
1132 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1133 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
1134
                );
1135 2
                foreach ($secondaryIds as $secondaryId) {
1136 2
                    $inserted += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1137
                }
1138
            }
1139 4
        });
1140
1141 4
        return $index;
1142
    }
1143
1144
    /**
1145
     * @inheritdoc
1146
     */
1147 5
    public function update($index, iterable $attributes, iterable $toMany): int
1148
    {
1149 5
        if (is_int($index) === false && is_string($index) === false) {
1150 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1151
        }
1152
1153 4
        $updated        = 0;
1154 4
        $pkName         = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
1155
        $filters        = [
1156
            $pkName => [
1157 4
                FilterParameterInterface::OPERATION_EQUALS => [$index],
1158
            ],
1159
        ];
1160 4
        $allowedChanges = $this->filterAttributesOnUpdate($attributes);
1161
        $saveMain       = $this
1162 4
            ->createBuilder($this->getModelClass())
1163 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...
1164 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...
1165 4
        $saveMain       = $this->builderSaveResourceOnUpdate($saveMain);
1166 4
        $saveMain->getSQL(); // prepare
1167
1168 4
        $this->clearBuilderParameters()->clearFetchParameters();
1169
1170 4
        $this->inTransaction(function () use ($saveMain, $toMany, $index, &$updated) {
1171 4
            $updated = $saveMain->execute();
1172
1173 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1174 2
                $cleanToMany = $this->builderCleanRelationshipOnUpdate(
1175 2
                    $relationshipName,
1176
                    $this
1177 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1178 2
                        ->clearToManyRelationship($relationshipName, $index)
1179
                );
1180 2
                $cleanToMany->execute();
1181
1182 2
                $secondaryIdBindName = ':secondaryId';
1183 2
                $saveToMany          = $this->builderSaveRelationshipOnUpdate(
1184 2
                    $relationshipName,
1185
                    $this
1186 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1187 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
1188
                );
1189 2
                foreach ($secondaryIds as $secondaryId) {
1190 2
                    $updated += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1191
                }
1192
            }
1193 4
        });
1194
1195 4
        return (int)$updated;
1196
    }
1197
1198
    /**
1199
     * @return FactoryInterface
1200
     */
1201 46
    protected function getFactory(): FactoryInterface
1202
    {
1203 46
        return $this->factory;
1204
    }
1205
1206
    /**
1207
     * @return string
1208
     */
1209 47
    protected function getModelClass(): string
1210
    {
1211 47
        return $this->modelClass;
1212
    }
1213
1214
    /**
1215
     * @return ModelSchemeInfoInterface
1216
     */
1217 47
    protected function getModelSchemes(): ModelSchemeInfoInterface
1218
    {
1219 47
        return $this->modelSchemes;
1220
    }
1221
1222
    /**
1223
     * @return RelationshipPaginationStrategyInterface
1224
     */
1225 7
    protected function getRelationshipPagingStrategy(): RelationshipPaginationStrategyInterface
1226
    {
1227 7
        return $this->relPagingStrategy;
1228
    }
1229
1230
    /**
1231
     * @param Closure $closure
1232
     *
1233
     * @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...
1234
     *
1235
     * @throws DBALException
1236
     */
1237 8
    public function inTransaction(Closure $closure): void
1238
    {
1239 8
        $connection = $this->getConnection();
1240 8
        $connection->beginTransaction();
1241
        try {
1242 8
            $isOk = ($closure() === false ? null : true);
1243 8
        } finally {
1244 8
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
1245
        }
1246
    }
1247
1248
    /**
1249
     * @inheritdoc
1250
     */
1251 24
    public function fetchResources(QueryBuilder $builder, string $modelClass): PaginatedDataInterface
1252
    {
1253 24
        $data = $this->fetchPaginatedResourcesWithoutRelationships($builder, $modelClass);
1254
1255 24
        if ($this->hasIncludes() === true) {
1256 13
            $this->loadRelationships($data);
1257 13
            $this->clearFetchParameters();
1258
        }
1259
1260 24
        return $data;
1261
    }
1262
1263
    /**
1264
     * @inheritdoc
1265
     */
1266 13
    public function fetchResource(QueryBuilder $builder, string $modelClass)
1267
    {
1268 13
        $data = $this->fetchResourceWithoutRelationships($builder, $modelClass);
1269
1270 13
        if ($this->hasIncludes() === true) {
1271 7
            $this->loadRelationships($data);
1272 7
            $this->clearFetchParameters();
1273
        }
1274
1275 13
        return $data;
1276
    }
1277
1278
    /**
1279
     * @inheritdoc
1280
     *
1281
     * @SuppressWarnings(PHPMD.ElseExpression)
1282
     */
1283 2
    public function fetchRow(QueryBuilder $builder, string $modelClass): ?array
1284
    {
1285 2
        $model = null;
1286
1287 2
        $statement = $builder->execute();
1288 2
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1289
1290 2
        if (($attributes = $statement->fetch()) !== false) {
1291 2
            if ($this->isFetchTyped() === true) {
1292 1
                $platform  = $builder->getConnection()->getDatabasePlatform();
1293 1
                $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1294 1
                $model     = $this->readRowFromAssoc($attributes, $typeNames, $platform);
1295
            } else {
1296 1
                $model = $attributes;
1297
            }
1298
        }
1299
1300 2
        $this->clearFetchParameters();
1301
1302 2
        return $model;
1303
    }
1304
1305
    /**
1306
     * @inheritdoc
1307
     *
1308
     * @SuppressWarnings(PHPMD.StaticAccess)
1309
     * @SuppressWarnings(PHPMD.ElseExpression)
1310
     */
1311 3
    public function fetchColumn(QueryBuilder $builder, string $modelClass, string $columnName): iterable
1312
    {
1313 3
        $statement = $builder->execute();
1314 3
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1315
1316 3
        if ($this->isFetchTyped() === true) {
1317 2
            $platform = $builder->getConnection()->getDatabasePlatform();
1318 2
            $typeName = $this->getModelSchemes()->getAttributeTypes($modelClass)[$columnName];
1319 2
            $type     = Type::getType($typeName);
1320 2
            while (($attributes = $statement->fetch()) !== false) {
1321 2
                $value     = $attributes[$columnName];
1322 2
                $converted = $type->convertToPHPValue($value, $platform);
1323
1324 2
                yield $converted;
1325
            }
1326
        } else {
1327 1
            while (($attributes = $statement->fetch()) !== false) {
1328 1
                $value = $attributes[$columnName];
1329
1330 1
                yield $value;
1331
            }
1332
        }
1333
1334 3
        $this->clearFetchParameters();
1335
    }
1336
1337
    /**
1338
     * @param QueryBuilder $builder
1339
     *
1340
     * @return QueryBuilder
1341
     */
1342 2
    protected function createCountBuilderFromBuilder(QueryBuilder $builder): QueryBuilder
1343
    {
1344 2
        $countBuilder = $this->getConnection()->createQueryBuilder();
1345 2
        $countBuilder->setParameters($builder->getParameters());
1346 2
        $countBuilder->select('COUNT(*)')->from('(' . $builder->getSQL() . ') AS RESULT');
1347
1348 2
        return $countBuilder;
1349
    }
1350
1351
    /**
1352
     * @param QueryBuilder $builder
1353
     * @param string       $modelClass
1354
     *
1355
     * @return mixed|null
1356
     *
1357
     * @throws DBALException
1358
     *
1359
     * @SuppressWarnings(PHPMD.ElseExpression)
1360
     */
1361 13
    private function fetchResourceWithoutRelationships(QueryBuilder $builder, string $modelClass)
1362
    {
1363 13
        $model     = null;
1364 13
        $statement = $builder->execute();
1365
1366 13
        if ($this->isFetchTyped() === true) {
1367 11
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1368 11
            if (($attributes = $statement->fetch()) !== false) {
1369 11
                $platform  = $builder->getConnection()->getDatabasePlatform();
1370 11
                $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1371 11
                $model     = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1372
            }
1373
        } else {
1374 2
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1375 2
            if (($fetched = $statement->fetch()) !== false) {
1376 2
                $model = $fetched;
1377
            }
1378
        }
1379
1380 13
        return $model;
1381
    }
1382
1383
    /**
1384
     * @param QueryBuilder $builder
1385
     * @param string       $modelClass
1386
     * @param string       $keyColumnName
1387
     *
1388
     * @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...
1389
     *
1390
     * @throws DBALException
1391
     *
1392
     * @SuppressWarnings(PHPMD.ElseExpression)
1393
     */
1394 8
    private function fetchResourcesWithoutRelationships(
1395
        QueryBuilder $builder,
1396
        string $modelClass,
1397
        string $keyColumnName
1398
    ): iterable {
1399 8
        $statement = $builder->execute();
1400
1401 8
        if ($this->isFetchTyped() === true) {
1402 7
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1403 7
            $platform  = $builder->getConnection()->getDatabasePlatform();
1404 7
            $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1405 7
            while (($attributes = $statement->fetch()) !== false) {
1406 6
                $model = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1407 6
                yield $model->{$keyColumnName} => $model;
1408
            }
1409
        } else {
1410 1
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1411 1
            while (($model = $statement->fetch()) !== false) {
1412 1
                yield $model->{$keyColumnName} => $model;
1413
            }
1414
        }
1415
    }
1416
1417
    /**
1418
     * @param QueryBuilder $builder
1419
     * @param string       $modelClass
1420
     *
1421
     * @return PaginatedDataInterface
1422
     *
1423
     * @throws DBALException
1424
     */
1425 28
    private function fetchPaginatedResourcesWithoutRelationships(
1426
        QueryBuilder $builder,
1427
        string $modelClass
1428
    ): PaginatedDataInterface {
1429 28
        list($models, $hasMore, $limit, $offset) = $this->fetchResourceCollection($builder, $modelClass);
1430
1431 28
        $data = $this->getFactory()
1432 28
            ->createPaginatedData($models)
1433 28
            ->markAsCollection()
1434 28
            ->setOffset($offset)
1435 28
            ->setLimit($limit);
1436
1437 28
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
1438
1439 28
        return $data;
1440
    }
1441
1442
    /**
1443
     * @param QueryBuilder $builder
1444
     * @param string       $modelClass
1445
     *
1446
     * @return array
1447
     *
1448
     * @throws DBALException
1449
     *
1450
     * @SuppressWarnings(PHPMD.ElseExpression)
1451
     */
1452 28
    private function fetchResourceCollection(QueryBuilder $builder, string $modelClass): array
1453
    {
1454 28
        $statement = $builder->execute();
1455
1456 28
        $models           = [];
1457 28
        $counter          = 0;
1458 28
        $hasMoreThanLimit = false;
1459 28
        $limit            = $builder->getMaxResults() !== null ? $builder->getMaxResults() - 1 : null;
1460
1461 28
        if ($this->isFetchTyped() === true) {
1462 27
            $platform  = $builder->getConnection()->getDatabasePlatform();
1463 27
            $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1464 27
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1465 27
            while (($attributes = $statement->fetch()) !== false) {
1466 26
                $counter++;
1467 26
                if ($limit !== null && $counter > $limit) {
1468 6
                    $hasMoreThanLimit = true;
1469 6
                    break;
1470
                }
1471 26
                $models[] = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1472
            }
1473
        } else {
1474 1
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1475 1
            while (($fetched = $statement->fetch()) !== false) {
1476 1
                $counter++;
1477 1
                if ($limit !== null && $counter > $limit) {
1478 1
                    $hasMoreThanLimit = true;
1479 1
                    break;
1480
                }
1481 1
                $models[] = $fetched;
1482
            }
1483
        }
1484
1485 28
        return [$models, $hasMoreThanLimit, $limit, $builder->getFirstResult()];
1486
    }
1487
1488
    /**
1489
     * @param null|string $index
1490
     * @param iterable    $attributes
1491
     *
1492
     * @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...
1493
     */
1494 4
    protected function filterAttributesOnCreate(?string $index, iterable $attributes): iterable
1495
    {
1496 4
        if ($index !== null) {
1497 1
            $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
1498 1
            yield $pkName => $index;
1499
        }
1500
1501 4
        $knownAttrAndTypes = $this->getModelSchemes()->getAttributeTypes($this->getModelClass());
1502 4
        foreach ($attributes as $attribute => $value) {
1503 4
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1504 4
                yield $attribute => $value;
1505
            }
1506
        }
1507
    }
1508
1509
    /**
1510
     * @param iterable $attributes
1511
     *
1512
     * @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...
1513
     */
1514 4
    protected function filterAttributesOnUpdate(iterable $attributes): iterable
1515
    {
1516 4
        $knownAttrAndTypes = $this->getModelSchemes()->getAttributeTypes($this->getModelClass());
1517 4
        foreach ($attributes as $attribute => $value) {
1518 4
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1519 4
                yield $attribute => $value;
1520
            }
1521
        }
1522
    }
1523
1524
    /**
1525
     * @param TagStorageInterface   $modelsAtPath
1526
     * @param ArrayObject           $classAtPath
1527
     * @param ArrayObject           $idsAtPath
1528
     * @param ModelStorageInterface $deDup
1529
     * @param string                $parentsPath
1530
     * @param array                 $childRelationships
1531
     *
1532
     * @return void
1533
     *
1534
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1535
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
1536
     *
1537
     * @throws DBALException
1538
     */
1539 9
    private function loadRelationshipsLayer(
1540
        TagStorageInterface $modelsAtPath,
1541
        ArrayObject $classAtPath,
1542
        ArrayObject $idsAtPath,
1543
        ModelStorageInterface $deDup,
1544
        string $parentsPath,
1545
        array $childRelationships
1546
    ): void {
1547 9
        $rootClass   = $classAtPath[static::ROOT_PATH];
1548 9
        $parentClass = $classAtPath[$parentsPath];
1549 9
        $parents     = $modelsAtPath->get($parentsPath);
1550
1551
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
1552
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
1553
        // $models.
1554
1555 9
        $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
1556
1557 9
        $registerModelAtPath = function ($model, string $path) use ($deDup, $modelsAtPath, $idsAtPath) {
1558 8
            return self::registerModelAtPath(
1559 8
                $model,
1560 8
                $path,
1561 8
                $this->getModelSchemes(),
1562 8
                $deDup,
1563 8
                $modelsAtPath,
1564 8
                $idsAtPath
1565
            );
1566 9
        };
1567
1568 9
        foreach ($childRelationships as $name) {
1569 9
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
1570
1571 9
            $relationshipType = $this->getModelSchemes()->getRelationshipType($parentClass, $name);
1572
            list ($targetModelClass, $reverseRelName) =
1573 9
                $this->getModelSchemes()->getReverseRelationship($parentClass, $name);
1574
1575
            $builder = $this
1576 9
                ->createBuilder($targetModelClass)
1577 9
                ->selectModelColumns()
1578 9
                ->fromModelTable();
1579
1580 9
            $classAtPath[$childrenPath] = $targetModelClass;
1581
1582
            switch ($relationshipType) {
1583 9
                case RelationshipTypes::BELONGS_TO:
1584
                    // for 'belongsTo' relationship all resources could be read at once.
1585 8
                    $parentIds            = $idsAtPath[$parentsPath];
1586 8
                    $clonedBuilder        = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1587 8
                        $reverseRelName,
1588 8
                        [$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...
1589 8
                        null
1590
                    );
1591 8
                    $unregisteredChildren = $this->fetchResourcesWithoutRelationships(
1592 8
                        $clonedBuilder,
1593 8
                        $clonedBuilder->getModelClass(),
1594 8
                        $this->getModelSchemes()->getPrimaryKey($clonedBuilder->getModelClass())
1595
                    );
1596 8
                    $children             = [];
1597 8
                    foreach ($unregisteredChildren as $index => $unregisteredChild) {
1598 7
                        $children[$index] = $registerModelAtPath($unregisteredChild, $childrenPath);
1599
                    }
1600 8
                    $fkNameToChild = $this->getModelSchemes()->getForeignKey($parentClass, $name);
1601 8
                    foreach ($parents as $parent) {
1602 8
                        $fkToChild       = $parent->{$fkNameToChild};
1603 8
                        $parent->{$name} = $children[$fkToChild] ?? null;
1604
                    }
1605 8
                    break;
1606 7
                case RelationshipTypes::HAS_MANY:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

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

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

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

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

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

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

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

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

Loading history...
1608
                    // unfortunately we have paging limits for 'many' relationship thus we have read such
1609
                    // relationships for each 'parent' individually
1610 7
                    list ($queryOffset, $queryLimit) = $this->getRelationshipPagingStrategy()
1611 7
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
1612 7
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
1613 7
                    foreach ($parents as $parent) {
1614 7
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1615 7
                            $reverseRelName,
1616 7
                            [$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...
1617 7
                            []
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<Limoncello\Flute\Adapters\iterable>|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1618
                        );
1619 7
                        $children      = $this->fetchPaginatedResourcesWithoutRelationships(
1620 7
                            $clonedBuilder,
1621 7
                            $clonedBuilder->getModelClass()
1622
                        );
1623
1624 7
                        $deDupedChildren = [];
1625 7
                        foreach ($children->getData() as $child) {
1626 7
                            $deDupedChildren[] = $registerModelAtPath($child, $childrenPath);
1627
                        }
1628
1629 7
                        $paginated = $this->getFactory()
1630 7
                            ->createPaginatedData($deDupedChildren)
1631 7
                            ->markAsCollection()
1632 7
                            ->setOffset($children->getOffset())
1633 7
                            ->setLimit($children->getLimit());
1634 7
                        $children->hasMoreItems() === true ?
1635 7
                            $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
1636
1637 7
                        $parent->{$name} = $paginated;
1638
                    }
1639 9
                    break;
1640
            }
1641
        }
1642
    }
1643
1644
    /**
1645
     * @param string $message
1646
     *
1647
     * @return string
1648
     *
1649
     * @throws ContainerExceptionInterface
1650
     * @throws NotFoundExceptionInterface
1651
     */
1652 8
    private function getMessage(string $message): string
1653
    {
1654 8
        $formatter = $this->createValidationFormatter();
1655 8
        $result    = $formatter->formatMessage($message);
1656
1657 8
        return $result;
1658
    }
1659
1660
    /**
1661
     * @param string           $class
1662
     * @param array            $attributes
1663
     * @param Type[]           $typeNames
1664
     * @param AbstractPlatform $platform
1665
     *
1666
     * @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...
1667
     *
1668
     * @SuppressWarnings(PHPMD.StaticAccess)
1669
     *
1670
     * @throws DBALException
1671
     */
1672 33
    private function readResourceFromAssoc(
1673
        string $class,
1674
        array $attributes,
1675
        array $typeNames,
1676
        AbstractPlatform $platform
1677
    ) {
1678 33
        $instance = new $class();
1679 33
        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...
1680 33
            $instance->{$name} = $value;
1681
        }
1682
1683 33
        return $instance;
1684
    }
1685
1686
    /**
1687
     * @param array            $attributes
1688
     * @param Type[]           $typeNames
1689
     * @param AbstractPlatform $platform
1690
     *
1691
     * @return array
1692
     *
1693
     * @SuppressWarnings(PHPMD.StaticAccess)
1694
     *
1695
     * @throws DBALException
1696
     */
1697 1
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
1698
    {
1699 1
        $row = [];
1700 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...
1701 1
            $row[$name] = $value;
1702
        }
1703
1704 1
        return $row;
1705
    }
1706
1707
    /**
1708
     * @param iterable         $attributes
1709
     * @param array            $typeNames
1710
     * @param AbstractPlatform $platform
1711
     *
1712
     * @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...
1713
     *
1714
     * @throws DBALException
1715
     */
1716 34
    private function readTypedAttributes(iterable $attributes, array $typeNames, AbstractPlatform $platform): iterable
1717
    {
1718 34
        foreach ($attributes as $name => $value) {
1719 34
            yield $name => (array_key_exists($name, $typeNames) === true ?
1720 34
                Type::getType($typeNames[$name])->convertToPHPValue($value, $platform) : $value);
1721
        }
1722
    }
1723
}
1724