Completed
Push — develop ( 9d9666...8087c5 )
by Neomerx
02:02
created

Crud::fetchColumn()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4

Importance

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