Completed
Push — develop ( 8087c5...db9bd8 )
by Neomerx
04:46 queued 03:09
created

Crud::applyAliasFilters()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 1
crap 3
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\Driver\PDOConnection;
23
use Doctrine\DBAL\Platforms\AbstractPlatform;
24
use Doctrine\DBAL\Query\QueryBuilder;
25
use Doctrine\DBAL\Types\Type;
26
use Generator;
27
use Limoncello\Container\Traits\HasContainerTrait;
28
use Limoncello\Contracts\Data\ModelSchemeInfoInterface;
29
use Limoncello\Contracts\Data\RelationshipTypes;
30
use Limoncello\Contracts\L10n\FormatterFactoryInterface;
31
use Limoncello\Flute\Adapters\ModelQueryBuilder;
32
use Limoncello\Flute\Contracts\Adapters\PaginationStrategyInterface;
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 Neomerx\JsonApi\Contracts\Document\DocumentInterface;
42
use Psr\Container\ContainerInterface;
43
use Traversable;
44
45
/**
46
 * @package Limoncello\Flute
47
 *
48
 * @SuppressWarnings(PHPMD.TooManyMethods)
49
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
50
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
51
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
52
 */
53
class Crud implements CrudInterface
54
{
55
    use HasContainerTrait;
56
57
    /** Internal constant. Path constant. */
58
    protected const ROOT_PATH = '';
59
60
    /** Internal constant. Path constant. */
61
    protected const PATH_SEPARATOR = DocumentInterface::PATH_SEPARATOR;
62
63
    /**
64
     * @var FactoryInterface
65
     */
66
    private $factory;
67
68
    /**
69
     * @var string
70
     */
71
    private $modelClass;
72
73
    /**
74
     * @var ModelSchemeInfoInterface
75
     */
76
    private $modelSchemes;
77
78
    /**
79
     * @var PaginationStrategyInterface
80
     */
81
    private $paginationStrategy;
82
83
    /**
84
     * @var Connection
85
     */
86
    private $connection;
87
88
    /**
89
     * @var iterable|null
90
     */
91
    private $filterParameters = null;
92
93
    /**
94
     * @var bool
95
     */
96
    private $areFiltersWithAnd = true;
97
98
    /**
99
     * @var iterable|null
100
     */
101
    private $sortingParameters = null;
102
103
    /**
104
     * @var array
105
     */
106
    private $relFiltersAndSorts = [];
107
108
    /**
109
     * @var iterable|null
110
     */
111
    private $includePaths = null;
112
113
    /**
114
     * @var int|null
115
     */
116
    private $pagingOffset = null;
117
118
    /**
119
     * @var Closure|null
120
     */
121
    private $columnMapper = null;
122
123
    /**
124
     * @var bool
125
     */
126
    private $isFetchTyped;
127
128
    /**
129
     * @var int|null
130
     */
131
    private $pagingLimit = null;
132
133
    /** internal constant */
134
    private const REL_FILTERS_AND_SORTS__FILTERS = 0;
135
136
    /** internal constant */
137
    private const REL_FILTERS_AND_SORTS__SORTS = 1;
138
139
    /**
140
     * @param ContainerInterface $container
141
     * @param string             $modelClass
142
     */
143 50
    public function __construct(ContainerInterface $container, string $modelClass)
144
    {
145 50
        $this->setContainer($container);
146
147 50
        $this->modelClass         = $modelClass;
148 50
        $this->factory            = $this->getContainer()->get(FactoryInterface::class);
149 50
        $this->modelSchemes       = $this->getContainer()->get(ModelSchemeInfoInterface::class);
150 50
        $this->paginationStrategy = $this->getContainer()->get(PaginationStrategyInterface::class);
151 50
        $this->connection         = $this->getContainer()->get(Connection::class);
152
153 50
        $this->clearBuilderParameters()->clearFetchParameters();
154
    }
155
156
    /**
157
     * @param Closure $mapper
158
     *
159
     * @return self
160
     */
161 1
    public function withColumnMapper(Closure $mapper): self
162
    {
163 1
        $this->columnMapper = $mapper;
164
165 1
        return $this;
166
    }
167
168
    /**
169
     * @inheritdoc
170
     */
171 42
    public function withFilters(iterable $filterParameters): CrudInterface
172
    {
173 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...
174
175 42
        return $this;
176
    }
177
178
    /**
179
     * @inheritdoc
180
     */
181 23
    public function withIndexFilter($index): CrudInterface
182
    {
183 23
        if (is_int($index) === false && is_string($index) === false) {
184 3
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
185
        }
186
187 20
        $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
188 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...
189
            $pkName => [
190 20
                FilterParameterInterface::OPERATION_EQUALS => [$index],
191
            ],
192
        ]);
193
194 20
        return $this;
195
    }
196
197
    /**
198
     * @inheritdoc
199
     */
200 4
    public function withRelationshipFilters(string $name, iterable $filters): CrudInterface
201
    {
202 4
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__FILTERS] = $filters;
203
204 4
        return $this;
205
    }
206
207
    /**
208
     * @inheritdoc
209
     */
210 1
    public function withRelationshipSorts(string $name, iterable $sorts): CrudInterface
211
    {
212 1
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__SORTS] = $sorts;
213
214 1
        return $this;
215
    }
216
217
    /**
218
     * @inheritdoc
219
     */
220 15
    public function combineWithAnd(): CrudInterface
221
    {
222 15
        $this->areFiltersWithAnd = true;
223
224 15
        return $this;
225
    }
226
227
    /**
228
     * @inheritdoc
229
     */
230 1
    public function combineWithOr(): CrudInterface
231
    {
232 1
        $this->areFiltersWithAnd = false;
233
234 1
        return $this;
235
    }
236
237
    /**
238
     * @return bool
239
     */
240 32
    private function hasColumnMapper(): bool
241
    {
242 32
        return $this->columnMapper !== null;
243
    }
244
245
    /**
246
     * @return Closure
247
     */
248 1
    private function getColumnMapper(): Closure
249
    {
250 1
        return $this->columnMapper;
251
    }
252
253
    /**
254
     * @return bool
255
     */
256 43
    private function hasFilters(): bool
257
    {
258 43
        return empty($this->filterParameters) === false;
259
    }
260
261
    /**
262
     * @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...
263
     */
264 34
    private function getFilters(): iterable
265
    {
266 34
        return $this->filterParameters;
267
    }
268
269
    /**
270
     * @return bool
271
     */
272 34
    private function areFiltersWithAnd(): bool
273
    {
274 34
        return $this->areFiltersWithAnd;
275
    }
276
277
    /**
278
     * @inheritdoc
279
     */
280 18
    public function withSorts(iterable $sortingParameters): CrudInterface
281
    {
282 18
        $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...
283
284 18
        return $this;
285
    }
286
287
    /**
288
     * @return bool
289
     */
290 32
    private function hasSorts(): bool
291
    {
292 32
        return empty($this->sortingParameters) === false;
293
    }
294
295
    /**
296
     * @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...
297
     */
298 13
    private function getSorts(): ?iterable
299
    {
300 13
        return $this->sortingParameters;
301
    }
302
303
    /**
304
     * @inheritdoc
305
     */
306 21
    public function withIncludes(iterable $includePaths): CrudInterface
307
    {
308 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...
309
310 21
        return $this;
311
    }
312
313
    /**
314
     * @return bool
315
     */
316 35
    private function hasIncludes(): bool
317
    {
318 35
        return empty($this->includePaths) === false;
319
    }
320
321
    /**
322
     * @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...
323
     */
324 21
    private function getIncludes(): iterable
325
    {
326 21
        return $this->includePaths;
327
    }
328
329
    /**
330
     * @inheritdoc
331
     */
332 19
    public function withPaging(int $offset, int $limit): CrudInterface
333
    {
334 19
        $this->pagingOffset = $offset;
335 19
        $this->pagingLimit  = $limit;
336
337 19
        return $this;
338
    }
339
340
    /**
341
     * @return self
342
     */
343 50
    public function shouldBeTyped(): self
344
    {
345 50
        $this->isFetchTyped = true;
346
347 50
        return $this;
348
    }
349
350
    /**
351
     * @return self
352
     */
353 4
    public function shouldBeUntyped(): self
354
    {
355 4
        $this->isFetchTyped = false;
356
357 4
        return $this;
358
    }
359
360
    /**
361
     * @return bool
362
     */
363 40
    private function hasPaging(): bool
364
    {
365 40
        return $this->pagingOffset !== null && $this->pagingLimit !== null;
366
    }
367
368
    /**
369
     * @return int
370
     */
371 19
    private function getPagingOffset(): int
372
    {
373 19
        return $this->pagingOffset;
374
    }
375
376
    /**
377
     * @return int
378
     */
379 19
    private function getPagingLimit(): int
380
    {
381 19
        return $this->pagingLimit;
382
    }
383
384
    /**
385
     * @return bool
386
     */
387 40
    private function isFetchTyped(): bool
388
    {
389 40
        return $this->isFetchTyped;
390
    }
391
392
    /**
393
     * @return Connection
394
     */
395 43
    protected function getConnection(): Connection
396
    {
397 43
        return $this->connection;
398
    }
399
400
    /**
401
     * @param string $modelClass
402
     *
403
     * @return ModelQueryBuilder
404
     */
405 43
    protected function createBuilder(string $modelClass): ModelQueryBuilder
406
    {
407 43
        return $this->createBuilderFromConnection($this->getConnection(), $modelClass);
408
    }
409
410
    /**
411
     * @param Connection $connection
412
     * @param string     $modelClass
413
     *
414
     * @return ModelQueryBuilder
415
     */
416 43
    private function createBuilderFromConnection(Connection $connection, string $modelClass): ModelQueryBuilder
417
    {
418 43
        return $this->getFactory()->createModelQueryBuilder($connection, $modelClass, $this->getModelSchemes());
419
    }
420
421
    /**
422
     * @param ModelQueryBuilder $builder
423
     *
424
     * @return Crud
425
     */
426 32
    protected function applyColumnMapper(ModelQueryBuilder $builder): self
427
    {
428 32
        if ($this->hasColumnMapper() === true) {
429 1
            $builder->setColumnToDatabaseMapper($this->getColumnMapper());
430
        }
431
432 32
        return $this;
433
    }
434
435
    /**
436
     * @param ModelQueryBuilder $builder
437
     *
438
     * @return Crud
439
     */
440 33
    protected function applyAliasFilters(ModelQueryBuilder $builder): self
441
    {
442 33
        if ($this->hasFilters() === true) {
443 24
            $filters = $this->getFilters();
444 24
            $this->areFiltersWithAnd() === true ?
445 24
                $builder->addFiltersWithAndToAlias($filters) : $builder->addFiltersWithOrToAlias($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 443 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 443 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...
446
        }
447
448 33
        return $this;
449
    }
450
451
    /**
452
     * @param ModelQueryBuilder $builder
453
     *
454
     * @return self
455
     */
456 5
    protected function applyTableFilters(ModelQueryBuilder $builder): self
457
    {
458 5
        if ($this->hasFilters() === true) {
459 5
            $filters = $this->getFilters();
460 5
            $this->areFiltersWithAnd() === true ?
461 5
                $builder->addFiltersWithAndToTable($filters) : $builder->addFiltersWithOrToTable($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 459 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 459 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...
462
        }
463
464 5
        return $this;
465
    }
466
467
    /**
468
     * @param ModelQueryBuilder $builder
469
     *
470
     * @return self
471
     */
472 32
    protected function applyRelationshipFiltersAndSorts(ModelQueryBuilder $builder): self
473
    {
474
        // While joining tables we select distinct rows. This flag used to apply `distinct` no more than once.
475 32
        $distinctApplied = false;
476
477 32
        foreach ($this->relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
478 4
            assert(is_string($relationshipName) === true && is_array($filtersAndSorts) === true);
479 4
            $builder->addRelationshipFiltersAndSortsWithAnd(
480 4
                $relationshipName,
481 4
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__FILTERS] ?? [],
482 4
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__SORTS] ?? []
483
            );
484
485 4
            if ($distinctApplied === false) {
486 4
                $builder->distinct();
487 4
                $distinctApplied = true;
488
            }
489
        }
490
491 32
        return $this;
492
    }
493
494
    /**
495
     * @param ModelQueryBuilder $builder
496
     *
497
     * @return self
498
     */
499 32
    protected function applySorts(ModelQueryBuilder $builder): self
500
    {
501 32
        if ($this->hasSorts() === true) {
502 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...
503
        }
504
505 32
        return $this;
506
    }
507
508
    /**
509
     * @param ModelQueryBuilder $builder
510
     *
511
     * @return self
512
     */
513 40
    protected function applyPaging(ModelQueryBuilder $builder): self
514
    {
515 40
        if ($this->hasPaging() === true) {
516 19
            $builder->setFirstResult($this->getPagingOffset());
517 19
            $builder->setMaxResults($this->getPagingLimit() + 1);
518
        }
519
520 40
        return $this;
521
    }
522
523
    /**
524
     * @return self
525
     */
526 50
    protected function clearBuilderParameters(): self
527
    {
528 50
        $this->columnMapper       = null;
529 50
        $this->filterParameters   = null;
530 50
        $this->areFiltersWithAnd  = true;
531 50
        $this->sortingParameters  = null;
532 50
        $this->pagingOffset       = null;
533 50
        $this->pagingLimit        = null;
534 50
        $this->relFiltersAndSorts = [];
535
536 50
        return $this;
537
    }
538
539
    /**
540
     * @return self
541
     */
542 50
    private function clearFetchParameters(): self
543
    {
544 50
        $this->includePaths = null;
545 50
        $this->shouldBeTyped();
546
547 50
        return $this;
548
    }
549
550
    /**
551
     * @param ModelQueryBuilder $builder
552
     *
553
     * @return ModelQueryBuilder
554
     */
555 1
    protected function builderOnCount(ModelQueryBuilder $builder): ModelQueryBuilder
556
    {
557 1
        return $builder;
558
    }
559
560
    /**
561
     * @param ModelQueryBuilder $builder
562
     *
563
     * @return ModelQueryBuilder
564
     */
565 32
    protected function builderOnIndex(ModelQueryBuilder $builder): ModelQueryBuilder
566
    {
567 32
        return $builder;
568
    }
569
570
    /**
571
     * @param ModelQueryBuilder $builder
572
     *
573
     * @return ModelQueryBuilder
574
     */
575 9
    protected function builderOnReadRelationship(ModelQueryBuilder $builder): ModelQueryBuilder
576
    {
577 9
        return $builder;
578
    }
579
580
    /**
581
     * @param ModelQueryBuilder $builder
582
     *
583
     * @return ModelQueryBuilder
584
     */
585 4
    protected function builderSaveResourceOnCreate(ModelQueryBuilder $builder): ModelQueryBuilder
586
    {
587 4
        return $builder;
588
    }
589
590
    /**
591
     * @param ModelQueryBuilder $builder
592
     *
593
     * @return ModelQueryBuilder
594
     */
595 3
    protected function builderSaveResourceOnUpdate(ModelQueryBuilder $builder): ModelQueryBuilder
596
    {
597 3
        return $builder;
598
    }
599
600
    /**
601
     * @param string            $relationshipName
602
     * @param ModelQueryBuilder $builder
603
     *
604
     * @return ModelQueryBuilder
605
     *
606
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
607
     */
608 2
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
609
        $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...
610
        ModelQueryBuilder $builder
611
    ): ModelQueryBuilder {
612 2
        return $builder;
613
    }
614
615
    /**
616
     * @param string            $relationshipName
617
     * @param ModelQueryBuilder $builder
618
     *
619
     * @return ModelQueryBuilder
620
     *
621
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
622
     */
623 2
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
624
        $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...
625
        ModelQueryBuilder $builder
626
    ): ModelQueryBuilder {
627 2
        return $builder;
628
    }
629
630
    /**
631
     * @param string            $relationshipName
632
     * @param ModelQueryBuilder $builder
633
     *
634
     * @return ModelQueryBuilder
635
     *
636
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
637
     */
638 2
    protected function builderCleanRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
639
        $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...
640
        ModelQueryBuilder $builder
641
    ): ModelQueryBuilder {
642 2
        return $builder;
643
    }
644
645
    /**
646
     * @param ModelQueryBuilder $builder
647
     *
648
     * @return ModelQueryBuilder
649
     */
650 5
    protected function builderOnDelete(ModelQueryBuilder $builder): ModelQueryBuilder
651
    {
652 5
        return $builder;
653
    }
654
655
    /**
656
     * @param PaginatedDataInterface|mixed|null $data
657
     *
658
     * @return void
659
     *
660
     * @SuppressWarnings(PHPMD.ElseExpression)
661
     */
662 21
    private function loadRelationships($data): void
663
    {
664 21
        $isPaginated = $data instanceof PaginatedDataInterface;
665 21
        $hasData     = ($isPaginated === true && empty($data->getData()) === false) ||
666 21
            ($isPaginated === false && $data !== null);
667
668 21
        if ($hasData === true && $this->hasIncludes() === true) {
669 21
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemes());
670 21
            $modelsAtPath = $this->getFactory()->createTagStorage();
671
672
            // we gonna send this storage via function params so it is an equivalent for &array
673 21
            $classAtPath = new ArrayObject();
674
675 21
            $model = null;
676 21
            if ($isPaginated === true) {
677 14
                foreach ($data->getData() as $model) {
678 14
                    $uniqueModel = $modelStorage->register($model);
679 14
                    if ($uniqueModel !== null) {
680 14
                        $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
681
                    }
682
                }
683
            } else {
684 7
                $model       = $data;
685 7
                $uniqueModel = $modelStorage->register($model);
686 7
                if ($uniqueModel !== null) {
687 7
                    $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
688
                }
689
            }
690 21
            $classAtPath[static::ROOT_PATH] = get_class($model);
691
692 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...
693 9
                $this->loadRelationshipsLayer(
694 9
                    $modelsAtPath,
695 9
                    $classAtPath,
696 9
                    $modelStorage,
697 9
                    $parentPath,
698 9
                    $childPaths
699
                );
700
            }
701
        }
702
    }
703
704
    /**
705
     * @param iterable $paths (string[])
706
     *
707
     * @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...
708
     */
709 21
    private static function getPaths(iterable $paths): iterable
710
    {
711
        // The idea is to normalize paths. It means build all intermediate paths.
712
        // e.g. if only `a.b.c` path it given it will be normalized to `a`, `a.b` and `a.b.c`.
713
        // Path depths store depth of each path (e.g. 0 for root, 1 for `a`, 2 for `a.b` and etc).
714
        // It is needed for yielding them in correct order (from top level to bottom).
715 21
        $normalizedPaths = [];
716 21
        $pathsDepths     = [];
717 21
        foreach ($paths as $path) {
718 9
            assert(is_array($path) || $path instanceof Traversable);
719 9
            $parentDepth = 0;
720 9
            $tmpPath     = static::ROOT_PATH;
721 9
            foreach ($path as $pathPiece) {
722 9
                assert(is_string($pathPiece));
723 9
                $parent                    = $tmpPath;
724 9
                $tmpPath                   = empty($tmpPath) === true ?
725 9
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
726 9
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
727 9
                $pathsDepths[$parent]      = $parentDepth++;
728
            }
729
        }
730
731
        // Here we collect paths in form of parent => [list of children]
732
        // 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...
733 21
        $parentWithChildren = [];
734 21
        foreach ($normalizedPaths as $path => list ($parent, $childPath)) {
735 9
            $parentWithChildren[$parent][] = $childPath;
736
        }
737
738
        // And finally sort by path depth and yield parent with its children. Top level paths first then deeper ones.
739 21
        asort($pathsDepths, SORT_NUMERIC);
740 21
        foreach ($pathsDepths as $parent => $depth) {
741 9
            assert($depth !== null); // suppress unused
742 9
            $childPaths = $parentWithChildren[$parent];
743 9
            yield [$parent, $childPaths];
744
        }
745
    }
746
747
    /**
748
     * @inheritdoc
749
     */
750 2
    public function createIndexBuilder(iterable $columns = null): QueryBuilder
751
    {
752 2
        return $this->createIndexModelBuilder($columns);
753
    }
754
755
    /**
756
     * @inheritdoc
757
     */
758
    public function createDeleteBuilder(): QueryBuilder
759
    {
760
        return $this->createDeleteModelBuilder();
761
    }
762
763
    /**
764
     * @param iterable|null $columns
765
     *
766
     * @return ModelQueryBuilder
767
     */
768 32
    protected function createIndexModelBuilder(iterable $columns = null): ModelQueryBuilder
769
    {
770 32
        $builder = $this->createBuilder($this->getModelClass());
771
772
        $this
773 32
            ->applyColumnMapper($builder);
774
775
        $builder
776 32
            ->selectModelColumns($columns)
777 32
            ->fromModelTable();
778
779
        $this
780 32
            ->applyAliasFilters($builder)
781 32
            ->applySorts($builder)
782 32
            ->applyRelationshipFiltersAndSorts($builder)
783 32
            ->applyPaging($builder);
784
785 32
        $result = $this->builderOnIndex($builder);
786
787 32
        $this->clearBuilderParameters();
788
789 32
        return $result;
790
    }
791
792
    /**
793
     * @return ModelQueryBuilder
794
     */
795 5
    protected function createDeleteModelBuilder(): ModelQueryBuilder
796
    {
797
        $builder = $this
798 5
            ->createBuilder($this->getModelClass())
799 5
            ->deleteModels();
800
801 5
        $this->applyTableFilters($builder);
802
803 5
        $result = $this->builderOnDelete($builder);
804
805 5
        $this->clearBuilderParameters();
806
807 5
        return $result;
808
    }
809
810
    /**
811
     * @inheritdoc
812
     */
813 16
    public function index(): PaginatedDataInterface
814
    {
815 16
        $builder = $this->createIndexModelBuilder();
816 16
        $data    = $this->fetchResources($builder, $builder->getModelClass());
817
818 16
        return $data;
819
    }
820
821
    /**
822
     * @inheritdoc
823
     */
824 2
    public function indexIdentities(): array
825
    {
826 2
        $pkName  = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
827 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...
828
        /** @var Generator $data */
829 2
        $data   = $this->fetchColumn($builder, $builder->getModelClass(), $pkName);
830 2
        $result = iterator_to_array($data);
831
832 2
        return $result;
833
    }
834
835
    /**
836
     * @inheritdoc
837
     */
838 14
    public function read($index)
839
    {
840 14
        $this->withIndexFilter($index);
841
842 12
        $builder = $this->createIndexModelBuilder();
843 12
        $data    = $this->fetchResource($builder, $builder->getModelClass());
844
845 12
        return $data;
846
    }
847
848
    /**
849
     * @inheritdoc
850
     */
851 1
    public function count(): ?int
852
    {
853
        $builder = $this
854 1
            ->createBuilder($this->getModelClass())
855 1
            ->select('COUNT(*)')
856 1
            ->fromModelTable();
857
858 1
        $this->applyAliasFilters($builder);
859
860 1
        $this->clearBuilderParameters()->clearFetchParameters();
861
862 1
        $result = $this->builderOnCount($builder)->execute()->fetchColumn();
863
864 1
        return $result === false ? null : $result;
865
    }
866
867
    /**
868
     * @param string        $relationshipName
869
     * @param iterable|null $relationshipFilters
870
     * @param iterable|null $relationshipSorts
871
     * @param iterable|null $columns
872
     *
873
     * @return ModelQueryBuilder
874
     */
875 9
    public function createReadRelationshipBuilder(
876
        string $relationshipName,
877
        iterable $relationshipFilters = null,
878
        iterable $relationshipSorts = null,
879
        iterable $columns = null
880
    ): ModelQueryBuilder {
881
        // as we read data from a relationship our main table and model would be the table/model in the relationship
882
        // so 'root' model(s) will be located in the reverse relationship.
883
884
        list ($targetModelClass, $reverseRelName) =
885 9
            $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $relationshipName);
886
887
        $builder = $this
888 9
            ->createBuilder($targetModelClass)
889 9
            ->selectModelColumns($columns)
890 9
            ->fromModelTable();
891
892
        // 'root' filters would be applied to the data in the reverse relationship ...
893 9
        if ($this->hasFilters() === true) {
894 9
            $filters = $this->getFilters();
895 9
            $sorts   = $this->getSorts();
896 9
            $this->areFiltersWithAnd() ?
897 9
                $builder->addRelationshipFiltersAndSortsWithAnd($reverseRelName, $filters, $sorts) :
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 894 can also be of type null; however, Limoncello\Flute\Adapter...iltersAndSortsWithAnd() 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...
898
                $builder->addRelationshipFiltersAndSortsWithOr($reverseRelName, $filters, $sorts);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 894 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...
899
        }
900
        // ... and the input filters to actual data we select
901 9
        if ($relationshipFilters !== null) {
902 6
            $builder->addFiltersWithAndToAlias($relationshipFilters);
903
        }
904 9
        if ($relationshipSorts !== null) {
905 2
            $builder->addSorts($relationshipSorts);
906
        }
907
908 9
        $this->applyPaging($builder);
909
910
        // While joining tables we select distinct rows.
911 9
        $builder->distinct();
912
913 9
        return $this->builderOnReadRelationship($builder);
914
    }
915
916
    /**
917
     * @inheritdoc
918
     */
919 8
    public function indexRelationship(
920
        string $name,
921
        iterable $relationshipFilters = null,
922
        iterable $relationshipSorts = null
923
    ) {
924
        // depending on the relationship type we expect the result to be either single resource or a collection
925 8
        $relationshipType = $this->getModelSchemes()->getRelationshipType($this->getModelClass(), $name);
926 8
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
927 8
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
928
929 8
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts);
930
931 8
        $modelClass = $builder->getModelClass();
932 8
        $data       = $isExpectMany === true ?
933 8
            $this->fetchResources($builder, $modelClass) : $this->fetchResource($builder, $modelClass);
934
935 8
        return $data;
936
    }
937
938
    /**
939
     * @inheritdoc
940
     */
941 1
    public function indexRelationshipIdentities(
942
        string $name,
943
        iterable $relationshipFilters = null,
944
        iterable $relationshipSorts = null
945
    ): array {
946
        // depending on the relationship type we expect the result to be either single resource or a collection
947 1
        $relationshipType = $this->getModelSchemes()->getRelationshipType($this->getModelClass(), $name);
948 1
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
949 1
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
950 1
        if ($isExpectMany === false) {
951
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
952
        }
953
954 1
        list ($targetModelClass) = $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $name);
955 1
        $targetPk = $this->getModelSchemes()->getPrimaryKey($targetModelClass);
956
957 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...
958
959 1
        $modelClass = $builder->getModelClass();
960
        /** @var Generator $data */
961 1
        $data   = $this->fetchColumn($builder, $modelClass, $targetPk);
962 1
        $result = iterator_to_array($data);
963
964 1
        return $result;
965
    }
966
967
    /**
968
     * @inheritdoc
969
     */
970 3
    public function readRelationship(
971
        $index,
972
        string $name,
973
        iterable $relationshipFilters = null,
974
        iterable $relationshipSorts = null
975
    ) {
976 3
        return $this->withIndexFilter($index)->indexRelationship($name, $relationshipFilters, $relationshipSorts);
977
    }
978
979
    /**
980
     * @inheritdoc
981
     */
982 6
    public function hasInRelationship($parentId, string $name, $childId): bool
983
    {
984 6
        if ($parentId !== null && is_scalar($parentId) === false) {
985 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
986
        }
987 5
        if ($childId !== null && is_scalar($childId) === false) {
988 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
989
        }
990
991 4
        $parentPkName  = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
992 4
        $parentFilters = [$parentPkName => [FilterParameterInterface::OPERATION_EQUALS => [$parentId]]];
993 4
        list($childClass) = $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $name);
994 4
        $childPkName  = $this->getModelSchemes()->getPrimaryKey($childClass);
995 4
        $childFilters = [$childPkName => [FilterParameterInterface::OPERATION_EQUALS => [$childId]]];
996
997
        $data = $this
998 4
            ->clearBuilderParameters()
999 4
            ->clearFetchParameters()
1000 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...
1001 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...
1002
1003 4
        $has = empty($data->getData()) === false;
1004
1005 4
        return $has;
1006
    }
1007
1008
    /**
1009
     * @inheritdoc
1010
     */
1011 5
    public function delete(): int
1012
    {
1013 5
        $deleted = $this->createDeleteModelBuilder()->execute();
1014
1015 4
        $this->clearFetchParameters();
1016
1017 4
        return (int)$deleted;
1018
    }
1019
1020
    /**
1021
     * @inheritdoc
1022
     */
1023 6
    public function remove($index): bool
1024
    {
1025 6
        return $this->withIndexFilter($index)->delete() > 0;
1026
    }
1027
1028
    /**
1029
     * @inheritdoc
1030
     */
1031 5
    public function create($index, iterable $attributes, iterable $toMany): string
1032
    {
1033 5
        if ($index !== null && is_int($index) === false && is_string($index) === false) {
1034 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1035
        }
1036
1037 4
        $allowedChanges = $this->filterAttributesOnCreate($index, $attributes);
1038
        $saveMain       = $this
1039 4
            ->createBuilder($this->getModelClass())
1040 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...
1041 4
        $saveMain       = $this->builderSaveResourceOnCreate($saveMain);
1042 4
        $saveMain->getSQL(); // prepare
1043
1044 4
        $this->clearBuilderParameters()->clearFetchParameters();
1045
1046 4
        $this->inTransaction(function () use ($saveMain, $toMany, &$index) {
1047 4
            $saveMain->execute();
1048
1049
            // if no index given will use last insert ID as index
1050 4
            $index !== null ?: $index = $saveMain->getConnection()->lastInsertId();
1051
1052 4
            $inserted = 0;
1053 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
1054 2
                $secondaryIdBindName = ':secondaryId';
1055 2
                $saveToMany          = $this->builderSaveRelationshipOnCreate(
1056 2
                    $relationshipName,
1057
                    $this
1058 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1059 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
1060
                );
1061 2
                foreach ($secondaryIds as $secondaryId) {
1062 2
                    $inserted += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1063
                }
1064
            }
1065 4
        });
1066
1067 4
        return $index;
1068
    }
1069
1070
    /**
1071
     * @inheritdoc
1072
     */
1073 4
    public function update($index, iterable $attributes, iterable $toMany): int
1074
    {
1075 4
        if (is_int($index) === false && is_string($index) === false) {
1076 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1077
        }
1078
1079 3
        $updated        = 0;
1080 3
        $pkName         = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
1081
        $filters        = [
1082
            $pkName => [
1083 3
                FilterParameterInterface::OPERATION_EQUALS => [$index],
1084
            ],
1085
        ];
1086 3
        $allowedChanges = $this->filterAttributesOnUpdate($attributes);
1087
        $saveMain       = $this
1088 3
            ->createBuilder($this->getModelClass())
1089 3
            ->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...
1090 3
            ->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...
1091 3
        $saveMain       = $this->builderSaveResourceOnUpdate($saveMain);
1092 3
        $saveMain->getSQL(); // prepare
1093
1094 3
        $this->clearBuilderParameters()->clearFetchParameters();
1095
1096 3
        $this->inTransaction(function () use ($saveMain, $toMany, $index, &$updated) {
1097 3
            $updated = $saveMain->execute();
1098
1099 3
            foreach ($toMany as $relationshipName => $secondaryIds) {
1100 2
                $cleanToMany = $this->builderCleanRelationshipOnUpdate(
1101 2
                    $relationshipName,
1102
                    $this
1103 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1104 2
                        ->clearToManyRelationship($relationshipName, $index)
1105
                );
1106 2
                $cleanToMany->execute();
1107
1108 2
                $secondaryIdBindName = ':secondaryId';
1109 2
                $saveToMany          = $this->builderSaveRelationshipOnUpdate(
1110 2
                    $relationshipName,
1111
                    $this
1112 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1113 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
1114
                );
1115 2
                foreach ($secondaryIds as $secondaryId) {
1116 2
                    $updated += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1117
                }
1118
            }
1119 3
        });
1120
1121 3
        return (int)$updated;
1122
    }
1123
1124
    /**
1125
     * @return FactoryInterface
1126
     */
1127 43
    protected function getFactory(): FactoryInterface
1128
    {
1129 43
        return $this->factory;
1130
    }
1131
1132
    /**
1133
     * @return string
1134
     */
1135 43
    protected function getModelClass(): string
1136
    {
1137 43
        return $this->modelClass;
1138
    }
1139
1140
    /**
1141
     * @return ModelSchemeInfoInterface
1142
     */
1143 43
    protected function getModelSchemes(): ModelSchemeInfoInterface
1144
    {
1145 43
        return $this->modelSchemes;
1146
    }
1147
1148
    /**
1149
     * @return PaginationStrategyInterface
1150
     */
1151 7
    protected function getPaginationStrategy(): PaginationStrategyInterface
1152
    {
1153 7
        return $this->paginationStrategy;
1154
    }
1155
1156
    /**
1157
     * @param Closure $closure
1158
     *
1159
     * @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...
1160
     */
1161 7
    public function inTransaction(Closure $closure): void
1162
    {
1163 7
        $connection = $this->getConnection();
1164 7
        $connection->beginTransaction();
1165
        try {
1166 7
            $isOk = ($closure() === false ? null : true);
1167 7
        } finally {
1168 7
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
1169
        }
1170
    }
1171
1172
    /**
1173
     * @inheritdoc
1174
     */
1175 23
    public function fetchResources(QueryBuilder $builder, string $modelClass): PaginatedDataInterface
1176
    {
1177 23
        $data = $this->fetchPaginatedResourcesWithoutRelationships($builder, $modelClass);
1178
1179 23
        if ($this->hasIncludes() === true) {
1180 14
            $this->loadRelationships($data);
1181 14
            $this->clearFetchParameters();
1182
        }
1183
1184 23
        return $data;
1185
    }
1186
1187
    /**
1188
     * @inheritdoc
1189
     */
1190 13
    public function fetchResource(QueryBuilder $builder, string $modelClass)
1191
    {
1192 13
        $data = $this->fetchResourceWithoutRelationships($builder, $modelClass);
1193
1194 13
        if ($this->hasIncludes() === true) {
1195 7
            $this->loadRelationships($data);
1196 7
            $this->clearFetchParameters();
1197
        }
1198
1199 13
        return $data;
1200
    }
1201
1202
    /**
1203
     * @inheritdoc
1204
     */
1205 2
    public function fetchRow(QueryBuilder $builder, string $modelClass): ?array
1206
    {
1207 2
        $model = null;
1208
1209 2
        $statement = $builder->execute();
1210 2
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1211
1212 2
        if (($attributes = $statement->fetch()) !== false) {
1213 2
            if ($this->isFetchTyped() === true) {
1214 1
                $platform  = $builder->getConnection()->getDatabasePlatform();
1215 1
                $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1216 1
                $model     = $this->readRowFromAssoc($attributes, $typeNames, $platform);
1217
            } else {
1218 1
                $model = $attributes;
1219
            }
1220
        }
1221
1222 2
        $this->clearFetchParameters();
1223
1224 2
        return $model;
1225
    }
1226
1227
    /**
1228
     * @inheritdoc
1229
     */
1230 3
    public function fetchColumn(QueryBuilder $builder, string $modelClass, string $columnName): iterable
1231
    {
1232 3
        $statement = $builder->execute();
1233 3
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1234
1235 3
        if ($this->isFetchTyped() === true) {
1236 2
            $platform = $builder->getConnection()->getDatabasePlatform();
1237 2
            $typeName = $this->getModelSchemes()->getAttributeTypes($modelClass)[$columnName];
1238 2
            $type     = Type::getType($typeName);
1239 2
            while (($attributes = $statement->fetch()) !== false) {
1240 2
                $value     = $attributes[$columnName];
1241 2
                $converted = $type->convertToPHPValue($value, $platform);
1242
1243 2
                yield $converted;
1244
            }
1245
        } else {
1246 1
            while (($attributes = $statement->fetch()) !== false) {
1247 1
                $value = $attributes[$columnName];
1248
1249 1
                yield $value;
1250
            }
1251
        }
1252
1253 3
        $this->clearFetchParameters();
1254
    }
1255
1256
    /**
1257
     * @param QueryBuilder $builder
1258
     * @param string       $modelClass
1259
     *
1260
     * @return mixed|null
1261
     */
1262 16
    private function fetchResourceWithoutRelationships(QueryBuilder $builder, string $modelClass)
1263
    {
1264 16
        $model     = null;
1265 16
        $statement = $builder->execute();
1266
1267 16
        if ($this->isFetchTyped() === true) {
1268 14
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1269 14
            if (($attributes = $statement->fetch()) !== false) {
1270 14
                $platform  = $builder->getConnection()->getDatabasePlatform();
1271 14
                $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1272 14
                $model     = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1273
            }
1274
        } else {
1275 2
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1276 2
            if (($fetched = $statement->fetch()) !== false) {
1277 2
                $model = $fetched;
1278
            }
1279
        }
1280
1281 16
        return $model;
1282
    }
1283
1284
    /**
1285
     * @param QueryBuilder $builder
1286
     * @param string       $modelClass
1287
     *
1288
     * @return PaginatedDataInterface
1289
     */
1290 27
    private function fetchPaginatedResourcesWithoutRelationships(
1291
        QueryBuilder $builder,
1292
        string $modelClass
1293
    ): PaginatedDataInterface {
1294 27
        list($models, $hasMore, $limit, $offset) = $this->fetchResourceCollection($builder, $modelClass);
1295
1296 27
        $data = $this->getFactory()
1297 27
            ->createPaginatedData($models)
1298 27
            ->markAsCollection()
1299 27
            ->setOffset($offset)
1300 27
            ->setLimit($limit);
1301
1302 27
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
1303
1304 27
        return $data;
1305
    }
1306
1307
    /**
1308
     * @param QueryBuilder $builder
1309
     * @param string       $modelClass
1310
     *
1311
     * @return array
1312
     */
1313 27
    private function fetchResourceCollection(QueryBuilder $builder, string $modelClass): array
1314
    {
1315 27
        $statement = $builder->execute();
1316
1317 27
        $models           = [];
1318 27
        $counter          = 0;
1319 27
        $hasMoreThanLimit = false;
1320 27
        $limit            = $builder->getMaxResults() !== null ? $builder->getMaxResults() - 1 : null;
1321
1322 27
        if ($this->isFetchTyped() === true) {
1323 26
            $platform  = $builder->getConnection()->getDatabasePlatform();
1324 26
            $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1325 26
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1326 26
            while (($attributes = $statement->fetch()) !== false) {
1327 25
                $counter++;
1328 25
                if ($limit !== null && $counter > $limit) {
1329 6
                    $hasMoreThanLimit = true;
1330 6
                    break;
1331
                }
1332 25
                $models[] = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1333
            }
1334
        } else {
1335 1
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1336 1
            while (($fetched = $statement->fetch()) !== false) {
1337 1
                $counter++;
1338 1
                if ($limit !== null && $counter > $limit) {
1339 1
                    $hasMoreThanLimit = true;
1340 1
                    break;
1341
                }
1342 1
                $models[] = $fetched;
1343
            }
1344
        }
1345
1346 27
        return [$models, $hasMoreThanLimit, $limit, $builder->getFirstResult()];
1347
    }
1348
1349
    /**
1350
     * @param null|string $index
1351
     * @param iterable    $attributes
1352
     *
1353
     * @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...
1354
     */
1355 4
    protected function filterAttributesOnCreate(?string $index, iterable $attributes): iterable
1356
    {
1357 4
        if ($index !== null) {
1358 1
            $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
1359 1
            yield $pkName => $index;
1360
        }
1361
1362 4
        $knownAttrAndTypes = $this->getModelSchemes()->getAttributeTypes($this->getModelClass());
1363 4
        foreach ($attributes as $attribute => $value) {
1364 4
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1365 4
                yield $attribute => $value;
1366
            }
1367
        }
1368
    }
1369
1370
    /**
1371
     * @param iterable $attributes
1372
     *
1373
     * @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...
1374
     */
1375 3
    protected function filterAttributesOnUpdate(iterable $attributes): iterable
1376
    {
1377 3
        $knownAttrAndTypes = $this->getModelSchemes()->getAttributeTypes($this->getModelClass());
1378 3
        foreach ($attributes as $attribute => $value) {
1379 3
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1380 3
                yield $attribute => $value;
1381
            }
1382
        }
1383
    }
1384
1385
    /**
1386
     * @param TagStorageInterface   $modelsAtPath
1387
     * @param ArrayObject           $classAtPath
1388
     * @param ModelStorageInterface $deDup
1389
     * @param string                $parentsPath
1390
     * @param array                 $childRelationships
1391
     *
1392
     * @return void
1393
     *
1394
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1395
     */
1396 9
    private function loadRelationshipsLayer(
1397
        TagStorageInterface $modelsAtPath,
1398
        ArrayObject $classAtPath,
1399
        ModelStorageInterface $deDup,
1400
        string $parentsPath,
1401
        array $childRelationships
1402
    ): void {
1403 9
        $rootClass   = $classAtPath[static::ROOT_PATH];
1404 9
        $parentClass = $classAtPath[$parentsPath];
1405 9
        $parents     = $modelsAtPath->get($parentsPath);
1406
1407
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
1408
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
1409
        // $models.
1410
1411 9
        $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
1412
1413 9
        foreach ($childRelationships as $name) {
1414 9
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
1415
1416 9
            $relationshipType = $this->getModelSchemes()->getRelationshipType($parentClass, $name);
1417
            list ($targetModelClass, $reverseRelName) =
1418 9
                $this->getModelSchemes()->getReverseRelationship($parentClass, $name);
1419
1420
            $builder = $this
1421 9
                ->createBuilder($targetModelClass)
1422 9
                ->selectModelColumns()
1423 9
                ->fromModelTable();
1424
1425 9
            $classAtPath[$childrenPath] = $targetModelClass;
1426
1427
            switch ($relationshipType) {
1428 9
                case RelationshipTypes::BELONGS_TO:
1429 8
                    foreach ($parents as $parent) {
1430 8
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1431 8
                            $reverseRelName,
1432 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>.

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...
1433 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...
1434
                        );
1435 8
                        $child         = $deDup->register($this->fetchResourceWithoutRelationships(
1436 8
                            $clonedBuilder,
1437 8
                            $clonedBuilder->getModelClass()
1438
                        ));
1439 8
                        if ($child !== null) {
1440 7
                            $modelsAtPath->register($child, $childrenPath);
1441
                        }
1442 8
                        $parent->{$name} = $child;
1443
                    }
1444 8
                    break;
1445 7
                case RelationshipTypes::HAS_MANY:
1446 5
                case RelationshipTypes::BELONGS_TO_MANY:
1447 7
                    list ($queryOffset, $queryLimit) = $this->getPaginationStrategy()
1448 7
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
1449 7
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
1450 7
                    foreach ($parents as $parent) {
1451 7
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1452 7
                            $reverseRelName,
1453 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>.

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...
1454 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...
1455
                        );
1456 7
                        $children      = $this->fetchPaginatedResourcesWithoutRelationships(
1457 7
                            $clonedBuilder,
1458 7
                            $clonedBuilder->getModelClass()
1459
                        );
1460
1461 7
                        $deDupedChildren = [];
1462 7
                        foreach ($children->getData() as $child) {
1463 7
                            $child = $deDup->register($child);
1464 7
                            $modelsAtPath->register($child, $childrenPath);
1465 7
                            if ($child !== null) {
1466 7
                                $deDupedChildren[] = $child;
1467
                            }
1468
                        }
1469
1470 7
                        $paginated = $this->getFactory()
1471 7
                            ->createPaginatedData($deDupedChildren)
1472 7
                            ->markAsCollection()
1473 7
                            ->setOffset($children->getOffset())
1474 7
                            ->setLimit($children->getLimit());
1475 7
                        $children->hasMoreItems() === true ?
1476 7
                            $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
1477
1478 7
                        $parent->{$name} = $paginated;
1479
                    }
1480 9
                    break;
1481
            }
1482
        }
1483
    }
1484
1485
    /**
1486
     * @param string $message
1487
     *
1488
     * @return string
1489
     */
1490 7
    private function getMessage(string $message): string
1491
    {
1492
        /** @var FormatterFactoryInterface $factory */
1493 7
        $factory   = $this->getContainer()->get(FormatterFactoryInterface::class);
1494 7
        $formatter = $factory->createFormatter(Messages::RESOURCES_NAMESPACE);
1495 7
        $result    = $formatter->formatMessage($message);
1496
1497 7
        return $result;
1498
    }
1499
1500
    /**
1501
     * @param string           $class
1502
     * @param array            $attributes
1503
     * @param Type[]           $typeNames
1504
     * @param AbstractPlatform $platform
1505
     *
1506
     * @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...
1507
     *
1508
     * @SuppressWarnings(PHPMD.StaticAccess)
1509
     */
1510 32
    private function readResourceFromAssoc(
1511
        string $class,
1512
        array $attributes,
1513
        array $typeNames,
1514
        AbstractPlatform $platform
1515
    ) {
1516 32
        $instance = new $class();
1517 32
        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...
1518 32
            $instance->{$name} = $value;
1519
        }
1520
1521 32
        return $instance;
1522
    }
1523
1524
    /**
1525
     * @param array            $attributes
1526
     * @param Type[]           $typeNames
1527
     * @param AbstractPlatform $platform
1528
     *
1529
     * @return array
1530
     *
1531
     * @SuppressWarnings(PHPMD.StaticAccess)
1532
     */
1533 1
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
1534
    {
1535 1
        $row = [];
1536 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...
1537 1
            $row[$name] = $value;
1538
        }
1539
1540 1
        return $row;
1541
    }
1542
1543
    /**
1544
     * @param iterable         $attributes
1545
     * @param array            $typeNames
1546
     * @param AbstractPlatform $platform
1547
     *
1548
     * @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...
1549
     */
1550 33
    private function readTypedAttributes(iterable $attributes, array $typeNames, AbstractPlatform $platform): iterable
1551
    {
1552 33
        foreach ($attributes as $name => $value) {
1553 33
            yield $name => (array_key_exists($name, $typeNames) === true ?
1554 33
                Type::getType($typeNames[$name])->convertToPHPValue($value, $platform) : $value);
1555
        }
1556
    }
1557
}
1558