Completed
Push — develop ( ec64f2...9148fa )
by Neomerx
02:26
created

Crud::fetchRow()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 13
cts 13
cp 1
rs 8.7972
c 0
b 0
f 0
cc 4
eloc 16
nc 4
nop 2
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 Limoncello\Container\Traits\HasContainerTrait;
27
use Limoncello\Contracts\Data\ModelSchemeInfoInterface;
28
use Limoncello\Contracts\Data\RelationshipTypes;
29
use Limoncello\Contracts\L10n\FormatterFactoryInterface;
30
use Limoncello\Flute\Adapters\ModelQueryBuilder;
31
use Limoncello\Flute\Contracts\Adapters\PaginationStrategyInterface;
32
use Limoncello\Flute\Contracts\Api\CrudInterface;
33
use Limoncello\Flute\Contracts\FactoryInterface;
34
use Limoncello\Flute\Contracts\Http\Query\FilterParameterInterface;
35
use Limoncello\Flute\Contracts\Models\ModelStorageInterface;
36
use Limoncello\Flute\Contracts\Models\PaginatedDataInterface;
37
use Limoncello\Flute\Contracts\Models\TagStorageInterface;
38
use Limoncello\Flute\Exceptions\InvalidArgumentException;
39
use Limoncello\Flute\L10n\Messages;
40
use Neomerx\JsonApi\Contracts\Document\DocumentInterface;
41
use Psr\Container\ContainerInterface;
42
use Traversable;
43
44
/**
45
 * @package Limoncello\Flute
46
 *
47
 * @SuppressWarnings(PHPMD.TooManyMethods)
48
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
49
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
50
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
51
 */
52
class Crud implements CrudInterface
53
{
54
    use HasContainerTrait;
55
56
    /** Internal constant. Path constant. */
57
    protected const ROOT_PATH = '';
58
59
    /** Internal constant. Path constant. */
60
    protected const PATH_SEPARATOR = DocumentInterface::PATH_SEPARATOR;
61
62
    /**
63
     * @var FactoryInterface
64
     */
65
    private $factory;
66
67
    /**
68
     * @var string
69
     */
70
    private $modelClass;
71
72
    /**
73
     * @var ModelSchemeInfoInterface
74
     */
75
    private $modelSchemes;
76
77
    /**
78
     * @var PaginationStrategyInterface
79
     */
80
    private $paginationStrategy;
81
82
    /**
83
     * @var Connection
84
     */
85
    private $connection;
86
87
    /**
88
     * @var iterable|null
89
     */
90
    private $filterParameters = null;
91
92
    /**
93
     * @var bool
94
     */
95
    private $areFiltersWithAnd = true;
96
97
    /**
98
     * @var iterable|null
99
     */
100
    private $sortingParameters = null;
101
102
    /**
103
     * @var array
104
     */
105
    private $relFiltersAndSorts = [];
106
107
    /**
108
     * @var iterable|null
109
     */
110
    private $includePaths = null;
111
112
    /**
113
     * @var int|null
114
     */
115
    private $pagingOffset = null;
116
117
    /**
118
     * @var int|null
119
     */
120
    private $pagingLimit = null;
121
122
    /** internal constant */
123
    private const REL_FILTERS_AND_SORTS__FILTERS = 0;
124
125
    /** internal constant */
126
    private const REL_FILTERS_AND_SORTS__SORTS = 1;
127
128
    /**
129
     * @param ContainerInterface $container
130
     * @param string             $modelClass
131
     */
132 44
    public function __construct(ContainerInterface $container, string $modelClass)
133
    {
134 44
        $this->setContainer($container);
135
136 44
        $this->modelClass         = $modelClass;
137 44
        $this->factory            = $this->getContainer()->get(FactoryInterface::class);
138 44
        $this->modelSchemes       = $this->getContainer()->get(ModelSchemeInfoInterface::class);
139 44
        $this->paginationStrategy = $this->getContainer()->get(PaginationStrategyInterface::class);
140 44
        $this->connection         = $this->getContainer()->get(Connection::class);
141
142 44
        $this->clearBuilderParameters()->clearFetchParameters();
143
    }
144
145
    /**
146
     * @inheritdoc
147
     */
148 35
    public function withFilters(iterable $filterParameters): CrudInterface
149
    {
150 35
        $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...
151
152 35
        return $this;
153
    }
154
155
    /**
156
     * @inheritdoc
157
     */
158 17
    public function withIndexFilter($index): CrudInterface
159
    {
160 17
        if (is_int($index) === false && is_string($index) === false) {
161 3
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
162
        }
163
164 14
        $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
165 14
        $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...
166
            $pkName => [
167 14
                FilterParameterInterface::OPERATION_EQUALS => [$index],
168
            ],
169
        ]);
170
171 14
        return $this;
172
    }
173
174
    /**
175
     * @inheritdoc
176
     */
177 5
    public function withRelationshipFilters(string $name, iterable $filters): CrudInterface
178
    {
179 5
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__FILTERS] = $filters;
180
181 5
        return $this;
182
    }
183
184
    /**
185
     * @inheritdoc
186
     */
187 1
    public function withRelationshipSorts(string $name, iterable $sorts): CrudInterface
188
    {
189 1
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__SORTS] = $sorts;
190
191 1
        return $this;
192
    }
193
194
    /**
195
     * @inheritdoc
196
     */
197 15
    public function combineWithAnd(): CrudInterface
198
    {
199 15
        $this->areFiltersWithAnd = true;
200
201 15
        return $this;
202
    }
203
204
    /**
205
     * @inheritdoc
206
     */
207 1
    public function combineWithOr(): CrudInterface
208
    {
209 1
        $this->areFiltersWithAnd = false;
210
211 1
        return $this;
212
    }
213
214
    /**
215
     * @return bool
216
     */
217 36
    private function hasFilters(): bool
218
    {
219 36
        return empty($this->filterParameters) === false;
220
    }
221
222
    /**
223
     * @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...
224
     */
225 28
    private function getFilters(): iterable
226
    {
227 28
        return $this->filterParameters;
228
    }
229
230
    /**
231
     * @return bool
232
     */
233 28
    private function areFiltersWithAnd(): bool
234
    {
235 28
        return $this->areFiltersWithAnd;
236
    }
237
238
    /**
239
     * @inheritdoc
240
     */
241 15
    public function withSorts(iterable $sortingParameters): CrudInterface
242
    {
243 15
        $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...
244
245 15
        return $this;
246
    }
247
248
    /**
249
     * @return bool
250
     */
251 26
    private function hasSorts(): bool
252
    {
253 26
        return empty($this->sortingParameters) === false;
254
    }
255
256
    /**
257
     * @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...
258
     */
259 10
    private function getSorts(): ?iterable
260
    {
261 10
        return $this->sortingParameters;
262
    }
263
264
    /**
265
     * @inheritdoc
266
     */
267 19
    public function withIncludes(iterable $includePaths): CrudInterface
268
    {
269 19
        $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...
270
271 19
        return $this;
272
    }
273
274
    /**
275
     * @return bool
276
     */
277 32
    private function hasIncludes(): bool
278
    {
279 32
        return empty($this->includePaths) === false;
280
    }
281
282
    /**
283
     * @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...
284
     */
285 19
    private function getIncludes(): iterable
286
    {
287 19
        return $this->includePaths;
288
    }
289
290
    /**
291
     * @inheritdoc
292
     */
293 18
    public function withPaging(int $offset, int $limit): CrudInterface
294
    {
295 18
        $this->pagingOffset = $offset;
296 18
        $this->pagingLimit  = $limit;
297
298 18
        return $this;
299
    }
300
301
    /**
302
     * @return bool
303
     */
304 33
    private function hasPaging(): bool
305
    {
306 33
        return $this->pagingOffset !== null && $this->pagingLimit !== null;
307
    }
308
309
    /**
310
     * @return int
311
     */
312 17
    private function getPagingOffset(): int
313
    {
314 17
        return $this->pagingOffset;
315
    }
316
317
    /**
318
     * @return int
319
     */
320 17
    private function getPagingLimit(): int
321
    {
322 17
        return $this->pagingLimit;
323
    }
324
325
    /**
326
     * @return Connection
327
     */
328 36
    private function getConnection(): Connection
329
    {
330 36
        return $this->connection;
331
    }
332
333
    /**
334
     * @param string $modelClass
335
     *
336
     * @return ModelQueryBuilder
337
     */
338 36
    private function createBuilder(string $modelClass): ModelQueryBuilder
339
    {
340 36
        return $this->createBuilderFromConnection($this->getConnection(), $modelClass);
341
    }
342
343
    /**
344
     * @param Connection $connection
345
     * @param string     $modelClass
346
     *
347
     * @return ModelQueryBuilder
348
     */
349 36
    private function createBuilderFromConnection(Connection $connection, string $modelClass): ModelQueryBuilder
350
    {
351 36
        return $this->getFactory()->createModelQueryBuilder($connection, $modelClass, $this->getModelSchemes());
352
    }
353
354
    /**
355
     * @param ModelQueryBuilder $builder
356
     *
357
     * @return Crud
358
     */
359 27
    private function applyAliasFilters(ModelQueryBuilder $builder): self
360
    {
361 27
        if ($this->hasFilters() === true) {
362 19
            $filters = $this->getFilters();
363 19
            $this->areFiltersWithAnd() === true ?
364 19
                $builder->addFiltersWithAndToAlias($filters) : $builder->addFiltersWithOrToAlias($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 362 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 362 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...
365
        }
366
367 27
        return $this;
368
    }
369
370
    /**
371
     * @param ModelQueryBuilder $builder
372
     *
373
     * @return self
374
     */
375 5
    private function applyTableFilters(ModelQueryBuilder $builder): self
376
    {
377 5
        if ($this->hasFilters() === true) {
378 5
            $filters = $this->getFilters();
379 5
            $this->areFiltersWithAnd() === true ?
380 5
                $builder->addFiltersWithAndToTable($filters) : $builder->addFiltersWithOrToTable($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 378 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 378 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...
381
        }
382
383 5
        return $this;
384
    }
385
386
    /**
387
     * @param ModelQueryBuilder $builder
388
     *
389
     * @return self
390
     */
391 26
    private function applyRelationshipFiltersAndSorts(ModelQueryBuilder $builder): self
392
    {
393
        // While joining tables we select distinct rows. This flag used to apply `distinct` no more than once.
394 26
        $distinctApplied = false;
395
396 26
        foreach ($this->relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
397 5
            assert(is_string($relationshipName) === true && is_array($filtersAndSorts) === true);
398 5
            $builder->addRelationshipFiltersAndSortsWithAnd(
399 5
                $relationshipName,
400 5
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__FILTERS] ?? [],
401 5
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__SORTS] ?? []
402
            );
403
404 5
            if ($distinctApplied === false) {
405 5
                $builder->distinct();
406 5
                $distinctApplied = true;
407
            }
408
        }
409
410 26
        return $this;
411
    }
412
413
    /**
414
     * @param ModelQueryBuilder $builder
415
     *
416
     * @return self
417
     */
418 26
    private function applySorts(ModelQueryBuilder $builder): self
419
    {
420 26
        if ($this->hasSorts() === true) {
421 2
            $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...
422
        }
423
424 26
        return $this;
425
    }
426
427
    /**
428
     * @param ModelQueryBuilder $builder
429
     *
430
     * @return self
431
     */
432 33
    private function applyPaging(ModelQueryBuilder $builder): self
433
    {
434 33
        if ($this->hasPaging() === true) {
435 17
            $builder->setFirstResult($this->getPagingOffset());
436 17
            $builder->setMaxResults($this->getPagingLimit() + 1);
437
        }
438
439 33
        return $this;
440
    }
441
442
    /**
443
     * @return self
444
     */
445 44
    private function clearBuilderParameters(): self
446
    {
447 44
        $this->filterParameters   = null;
448 44
        $this->areFiltersWithAnd  = true;
449 44
        $this->sortingParameters  = null;
450 44
        $this->pagingOffset       = null;
451 44
        $this->pagingLimit        = null;
452 44
        $this->relFiltersAndSorts = [];
453
454 44
        return $this;
455
    }
456
457
    /**
458
     * @return self
459
     */
460 44
    private function clearFetchParameters(): self
461
    {
462 44
        $this->includePaths = null;
463
464 44
        return $this;
465
    }
466
467
    /**
468
     * @param ModelQueryBuilder $builder
469
     *
470
     * @return ModelQueryBuilder
471
     */
472 1
    protected function builderOnCount(ModelQueryBuilder $builder): ModelQueryBuilder
473
    {
474 1
        return $builder;
475
    }
476
477
    /**
478
     * @param ModelQueryBuilder $builder
479
     *
480
     * @return ModelQueryBuilder
481
     */
482 26
    protected function builderOnIndex(ModelQueryBuilder $builder): ModelQueryBuilder
483
    {
484 26
        return $builder;
485
    }
486
487
    /**
488
     * @param ModelQueryBuilder $builder
489
     *
490
     * @return ModelQueryBuilder
491
     */
492 8
    protected function builderOnReadRelationship(ModelQueryBuilder $builder): ModelQueryBuilder
493
    {
494 8
        return $builder;
495
    }
496
497
    /**
498
     * @param ModelQueryBuilder $builder
499
     *
500
     * @return ModelQueryBuilder
501
     */
502 4
    protected function builderSaveResourceOnCreate(ModelQueryBuilder $builder): ModelQueryBuilder
503
    {
504 4
        return $builder;
505
    }
506
507
    /**
508
     * @param ModelQueryBuilder $builder
509
     *
510
     * @return ModelQueryBuilder
511
     */
512 3
    protected function builderSaveResourceOnUpdate(ModelQueryBuilder $builder): ModelQueryBuilder
513
    {
514 3
        return $builder;
515
    }
516
517
    /**
518
     * @param string            $relationshipName
519
     * @param ModelQueryBuilder $builder
520
     *
521
     * @return ModelQueryBuilder
522
     *
523
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
524
     */
525 2
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
526
        $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...
527
        ModelQueryBuilder $builder
528
    ): ModelQueryBuilder {
529 2
        return $builder;
530
    }
531
532
    /**
533
     * @param string            $relationshipName
534
     * @param ModelQueryBuilder $builder
535
     *
536
     * @return ModelQueryBuilder
537
     *
538
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
539
     */
540 2
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
541
        $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...
542
        ModelQueryBuilder $builder
543
    ): ModelQueryBuilder {
544 2
        return $builder;
545
    }
546
547
    /**
548
     * @param string            $relationshipName
549
     * @param ModelQueryBuilder $builder
550
     *
551
     * @return ModelQueryBuilder
552
     *
553
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
554
     */
555 2
    protected function builderCleanRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
556
        $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...
557
        ModelQueryBuilder $builder
558
    ): ModelQueryBuilder {
559 2
        return $builder;
560
    }
561
562
    /**
563
     * @param ModelQueryBuilder $builder
564
     *
565
     * @return ModelQueryBuilder
566
     */
567 5
    protected function builderOnDelete(ModelQueryBuilder $builder): ModelQueryBuilder
568
    {
569 5
        return $builder;
570
    }
571
572
    /**
573
     * @param PaginatedDataInterface $data
574
     *
575
     * @return void
576
     *
577
     * @SuppressWarnings(PHPMD.ElseExpression)
578
     */
579 19
    private function loadRelationships(PaginatedDataInterface $data): void
580
    {
581 19
        if (empty($data->getData()) === false && $this->hasIncludes() === true) {
582 19
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemes());
583 19
            $modelsAtPath = $this->getFactory()->createTagStorage();
584
585
            // we gonna send this storage via function params so it is an equivalent for &array
586 19
            $classAtPath = new ArrayObject();
587
588 19
            $model = null;
589 19
            if ($data->isCollection() === true) {
590 13
                foreach ($data->getData() as $model) {
591 13
                    $uniqueModel = $modelStorage->register($model);
592 13
                    if ($uniqueModel !== null) {
593 13
                        $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
594
                    }
595
                }
596
            } else {
597 6
                $model       = $data->getData();
598 6
                $uniqueModel = $modelStorage->register($model);
599 6
                if ($uniqueModel !== null) {
600 6
                    $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
601
                }
602
            }
603 19
            $classAtPath[static::ROOT_PATH] = get_class($model);
604
605 19
            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...
606 8
                $this->loadRelationshipsLayer(
607 8
                    $modelsAtPath,
608 8
                    $classAtPath,
609 8
                    $modelStorage,
610 8
                    $parentPath,
611 8
                    $childPaths
612
                );
613
            }
614
        }
615
    }
616
617
    /**
618
     * @param iterable $paths (string[])
619
     *
620
     * @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...
621
     */
622 19
    private static function getPaths(iterable $paths): iterable
623
    {
624
        // The idea is to normalize paths. It means build all intermediate paths.
625
        // e.g. if only `a.b.c` path it given it will be normalized to `a`, `a.b` and `a.b.c`.
626
        // Path depths store depth of each path (e.g. 0 for root, 1 for `a`, 2 for `a.b` and etc).
627
        // It is needed for yielding them in correct order (from top level to bottom).
628 19
        $normalizedPaths = [];
629 19
        $pathsDepths     = [];
630 19
        foreach ($paths as $path) {
631 8
            assert(is_array($path) || $path instanceof Traversable);
632 8
            $parentDepth = 0;
633 8
            $tmpPath     = static::ROOT_PATH;
634 8
            foreach ($path as $pathPiece) {
635 8
                assert(is_string($pathPiece));
636 8
                $parent                    = $tmpPath;
637 8
                $tmpPath                   = empty($tmpPath) === true ?
638 8
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
639 8
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
640 8
                $pathsDepths[$parent]      = $parentDepth++;
641
            }
642
        }
643
644
        // Here we collect paths in form of parent => [list of children]
645
        // 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...
646 19
        $parentWithChildren = [];
647 19
        foreach ($normalizedPaths as $path => list ($parent, $childPath)) {
648 8
            $parentWithChildren[$parent][] = $childPath;
649
        }
650
651
        // And finally sort by path depth and yield parent with its children. Top level paths first then deeper ones.
652 19
        asort($pathsDepths, SORT_NUMERIC);
653 19
        foreach ($pathsDepths as $parent => $depth) {
654 8
            assert($depth !== null); // suppress unused
655 8
            $childPaths = $parentWithChildren[$parent];
656 8
            yield [$parent, $childPaths];
657
        }
658
    }
659
660
    /**
661
     * @inheritdoc
662
     */
663
    public function getIndexBuilder(): QueryBuilder
664
    {
665
        return $this->getIndexModelBuilder();
666
    }
667
668
    /**
669
     * @inheritdoc
670
     */
671
    public function getDeleteBuilder(): QueryBuilder
672
    {
673
        return $this->getDeleteModelBuilder();
674
    }
675
676
    /**
677
     * @return ModelQueryBuilder
678
     */
679 26
    protected function getIndexModelBuilder(): ModelQueryBuilder
680
    {
681
        $builder = $this
682 26
            ->createBuilder($this->getModelClass())
683 26
            ->selectModelFields()
684 26
            ->fromModelTable();
685
686
        $this
687 26
            ->applyAliasFilters($builder)
688 26
            ->applySorts($builder)
689 26
            ->applyRelationshipFiltersAndSorts($builder)
690 26
            ->applyPaging($builder);
691
692 26
        $result = $this->builderOnIndex($builder);
693
694 26
        $this->clearBuilderParameters();
695
696 26
        return $result;
697
    }
698
699
    /**
700
     * @return ModelQueryBuilder
701
     */
702 5
    protected function getDeleteModelBuilder(): ModelQueryBuilder
703
    {
704
        $builder = $this
705 5
            ->createBuilder($this->getModelClass())
706 5
            ->deleteModels();
707
708 5
        $this->applyTableFilters($builder);
709
710 5
        $result = $this->builderOnDelete($builder);
711
712 5
        $this->clearBuilderParameters();
713
714 5
        return $result;
715
    }
716
717
    /**
718
     * @inheritdoc
719
     */
720 15
    public function index(): PaginatedDataInterface
721
    {
722 15
        $builder = $this->getIndexModelBuilder();
723 15
        $data    = $this->fetchResources($builder, $builder->getModelClass());
724
725 15
        return $data;
726
    }
727
728
    /**
729
     * @inheritdoc
730
     */
731 1
    public function count(): ?int
732
    {
733
        $builder = $this
734 1
            ->createBuilder($this->getModelClass())
735 1
            ->select('COUNT(*)')
736 1
            ->fromModelTable();
737
738 1
        $this->applyAliasFilters($builder);
739
740 1
        $this->clearBuilderParameters()->clearFetchParameters();
741
742 1
        $result = $this->builderOnCount($builder)->execute()->fetchColumn();
743
744 1
        return $result === false ? null : $result;
745
    }
746
747
    /**
748
     * @param string        $relationshipName
749
     * @param iterable|null $relationshipFilters
750
     * @param iterable|null $relationshipSorts
751
     *
752
     * @return ModelQueryBuilder
753
     */
754 8
    public function createReadRelationshipBuilder(
755
        string $relationshipName,
756
        iterable $relationshipFilters = null,
757
        iterable $relationshipSorts = null
758
    ): ModelQueryBuilder {
759
        // as we read data from a relationship our main table and model would be the table/model in the relationship
760
        // so 'root' model(s) will be located in the reverse relationship.
761
762
        list ($targetModelClass, $reverseRelName) =
763 8
            $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $relationshipName);
764
765
        $builder = $this
766 8
            ->createBuilder($targetModelClass)
767 8
            ->selectModelFields()
768 8
            ->fromModelTable();
769
770
        // 'root' filters would be applied to the data in the reverse relationship ...
771 8
        if ($this->hasFilters() === true) {
772 8
            $filters = $this->getFilters();
773 8
            $sorts   = $this->getSorts();
774 8
            $this->areFiltersWithAnd() ?
775 8
                $builder->addRelationshipFiltersAndSortsWithAnd($reverseRelName, $filters, $sorts) :
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 772 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...
776
                $builder->addRelationshipFiltersAndSortsWithOr($reverseRelName, $filters, $sorts);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 772 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...
777
        }
778
        // ... and the input filters to actual data we select
779 8
        if ($relationshipFilters !== null) {
780 5
            $builder->addFiltersWithAndToAlias($relationshipFilters);
781
        }
782 8
        if ($relationshipSorts !== null) {
783 1
            $builder->addSorts($relationshipSorts);
784
        }
785
786 8
        $this->applyPaging($builder);
787
788
        // While joining tables we select distinct rows.
789 8
        $builder->distinct();
790
791 8
        return $this->builderOnReadRelationship($builder);
792
    }
793
794
    /**
795
     * @inheritdoc
796
     */
797 8
    public function readRelationship(
798
        string $name,
799
        iterable $relationshipFilters = null,
800
        iterable $relationshipSorts = null
801
    ): PaginatedDataInterface {
802
        // depending on the relationship type we expect the result to be either single resource or a collection
803 8
        $relationshipType = $this->getModelSchemes()->getRelationshipType($this->getModelClass(), $name);
804 8
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
805 8
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
806
807 8
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts);
808
809 8
        $modelClass = $builder->getModelClass();
810 8
        $data       = $isExpectMany === true ?
811 8
            $this->fetchResources($builder, $modelClass) : $this->fetchResource($builder, $modelClass);
812
813 8
        return $data;
814
    }
815
816
    /**
817
     * @inheritdoc
818
     */
819 6
    public function hasInRelationship($parentId, string $name, $childId): bool
820
    {
821 6
        if ($parentId !== null && is_scalar($parentId) === false) {
822 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
823
        }
824 5
        if ($childId !== null && is_scalar($childId) === false) {
825 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
826
        }
827
828 4
        $parentPkName  = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
829 4
        $parentFilters = [$parentPkName => [FilterParameterInterface::OPERATION_EQUALS => [$parentId]]];
830 4
        list($childClass) = $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $name);
831 4
        $childPkName  = $this->getModelSchemes()->getPrimaryKey($childClass);
832 4
        $childFilters = [$childPkName => [FilterParameterInterface::OPERATION_EQUALS => [$childId]]];
833
834
        $data = $this
835 4
            ->clearBuilderParameters()
836 4
            ->clearFetchParameters()
837 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...
838 4
            ->readRelationship($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...
839
840 4
        $has = empty($data->getData()) === false;
841
842 4
        return $has;
843
    }
844
845
    /**
846
     * @inheritdoc
847
     */
848 5
    public function delete(): int
849
    {
850 5
        $deleted = $this->getDeleteModelBuilder()->execute();
851
852 4
        $this->clearFetchParameters();
853
854 4
        return (int)$deleted;
855
    }
856
857
    /**
858
     * @inheritdoc
859
     */
860 5
    public function create($index, iterable $attributes, iterable $toMany): string
861
    {
862 5
        if ($index !== null && is_int($index) === false && is_string($index) === false) {
863 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
864
        }
865
866 4
        $allowedChanges = $this->filterAttributesOnCreate($index, $attributes);
867
        $saveMain       = $this
868 4
            ->createBuilder($this->getModelClass())
869 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...
870 4
        $saveMain       = $this->builderSaveResourceOnCreate($saveMain);
871 4
        $saveMain->getSQL(); // prepare
872
873 4
        $this->clearBuilderParameters()->clearFetchParameters();
874
875 4
        $this->inTransaction($saveMain->getConnection(), function () use ($saveMain, $toMany, &$index) {
876 4
            $saveMain->execute();
877
878
            // if no index given will use last insert ID as index
879 4
            $index !== null ?: $index = $saveMain->getConnection()->lastInsertId();
880
881 4
            $inserted = 0;
882 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
883 2
                $secondaryIdBindName = ':secondaryId';
884 2
                $saveToMany          = $this->builderSaveRelationshipOnCreate(
885 2
                    $relationshipName,
886
                    $this
887 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
888 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
889
                );
890 2
                foreach ($secondaryIds as $secondaryId) {
891 2
                    $inserted += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
892
                }
893
            }
894 4
        });
895
896 4
        return $index;
897
    }
898
899
    /**
900
     * @inheritdoc
901
     */
902 4
    public function update($index, iterable $attributes, iterable $toMany): int
903
    {
904 4
        if (is_int($index) === false && is_string($index) === false) {
905 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
906
        }
907
908 3
        $updated        = 0;
909 3
        $pkName         = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
910
        $filters        = [
911
            $pkName => [
912 3
                FilterParameterInterface::OPERATION_EQUALS => [$index],
913
            ],
914
        ];
915 3
        $allowedChanges = $this->filterAttributesOnUpdate($attributes);
916
        $saveMain       = $this
917 3
            ->createBuilder($this->getModelClass())
918 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...
919 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...
920 3
        $saveMain       = $this->builderSaveResourceOnUpdate($saveMain);
921 3
        $saveMain->getSQL(); // prepare
922
923 3
        $this->clearBuilderParameters()->clearFetchParameters();
924
925 3
        $this->inTransaction($saveMain->getConnection(), function () use ($saveMain, $toMany, $index, &$updated) {
926 3
            $updated = $saveMain->execute();
927
928 3
            foreach ($toMany as $relationshipName => $secondaryIds) {
929 2
                $cleanToMany = $this->builderCleanRelationshipOnUpdate(
930 2
                    $relationshipName,
931
                    $this
932 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
933 2
                        ->clearToManyRelationship($relationshipName, $index)
934
                );
935 2
                $cleanToMany->execute();
936
937 2
                $secondaryIdBindName = ':secondaryId';
938 2
                $saveToMany          = $this->builderSaveRelationshipOnUpdate(
939 2
                    $relationshipName,
940
                    $this
941 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
942 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
943
                );
944 2
                foreach ($secondaryIds as $secondaryId) {
945 2
                    $updated += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
946
                }
947
            }
948 3
        });
949
950 3
        return (int)$updated;
951
    }
952
953
    /**
954
     * @return FactoryInterface
955
     */
956 36
    protected function getFactory(): FactoryInterface
957
    {
958 36
        return $this->factory;
959
    }
960
961
    /**
962
     * @return string
963
     */
964 36
    protected function getModelClass(): string
965
    {
966 36
        return $this->modelClass;
967
    }
968
969
    /**
970
     * @return ModelSchemeInfoInterface
971
     */
972 36
    protected function getModelSchemes(): ModelSchemeInfoInterface
973
    {
974 36
        return $this->modelSchemes;
975
    }
976
977
    /**
978
     * @return PaginationStrategyInterface
979
     */
980 6
    protected function getPaginationStrategy(): PaginationStrategyInterface
981
    {
982 6
        return $this->paginationStrategy;
983
    }
984
985
    /**
986
     * @param Connection $connection
987
     * @param Closure    $closure
988
     *
989
     * @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...
990
     */
991 7
    protected function inTransaction(Connection $connection, Closure $closure): void
992
    {
993 7
        $connection->beginTransaction();
994
        try {
995 7
            $isOk = ($closure() === false ? null : true);
996 7
        } finally {
997 7
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
998
        }
999
    }
1000
1001
    /**
1002
     * @inheritdoc
1003
     */
1004 22
    public function fetchResources(
1005
        QueryBuilder $builder = null,
1006
        string $modelClass = null
1007
    ): PaginatedDataInterface {
1008 22
        if ($builder === null && $modelClass === null) {
1009
            $builder    = $this->getIndexModelBuilder();
1010
            $modelClass = $builder->getModelClass();
1011
        }
1012
1013 22
        $data = $this->fetchPaginatedResourcesWithoutRelationships($builder, $modelClass);
0 ignored issues
show
Bug introduced by
It seems like $builder defined by parameter $builder on line 1005 can be null; however, Limoncello\Flute\Api\Cru...sWithoutRelationships() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
1014
1015 22
        if ($this->hasIncludes() === true) {
1016 13
            $this->loadRelationships($data);
1017 13
            $this->clearFetchParameters();
1018
        }
1019
1020 22
        return $data;
1021
    }
1022
1023
    /**
1024
     * @inheritdoc
1025
     */
1026 11
    public function fetchResource(
1027
        QueryBuilder $builder = null,
1028
        string $modelClass = null
1029
    ): PaginatedDataInterface {
1030 11
        if ($builder === null && $modelClass === null) {
1031 10
            $builder    = $this->getIndexModelBuilder();
1032 10
            $modelClass = $builder->getModelClass();
1033
        }
1034
1035 11
        $data = $this->getFactory()->createPaginatedData(
1036 11
            $this->fetchResourceWithoutRelationships($builder, $modelClass)
0 ignored issues
show
Bug introduced by
It seems like $builder defined by parameter $builder on line 1027 can be null; however, Limoncello\Flute\Api\Cru...eWithoutRelationships() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
1037 11
        )->markAsSingleItem();
1038
1039 11
        if ($this->hasIncludes() === true) {
1040 6
            $this->loadRelationships($data);
1041 6
            $this->clearFetchParameters();
1042
        }
1043
1044 11
        return $data;
1045
    }
1046
1047
    /**
1048
     * @inheritdoc
1049
     */
1050 1
    public function fetchRow(
1051
        QueryBuilder $builder = null,
1052
        string $modelClass = null
1053
    ): ?array {
1054 1
        if ($builder === null && $modelClass === null) {
1055 1
            $builder    = $this->getIndexModelBuilder();
1056 1
            $modelClass = $builder->getModelClass();
1057
        }
1058
1059 1
        $statement = $builder->execute();
0 ignored issues
show
Bug introduced by
It seems like $builder is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
1060 1
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1061 1
        $platform  = $builder->getConnection()->getDatabasePlatform();
1062 1
        $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1063
1064 1
        $model = null;
1065 1
        if (($attributes = $statement->fetch()) !== false) {
1066 1
            $model = $this->readRowFromAssoc($attributes, $typeNames, $platform);
1067
        }
1068
1069 1
        $this->clearFetchParameters();
1070
1071 1
        return $model;
1072
    }
1073
1074
    /**
1075
     * @param QueryBuilder $builder
1076
     * @param string       $modelClass
1077
     *
1078
     * @return PaginatedDataInterface
1079
     */
1080 25
    private function fetchPaginatedResourcesWithoutRelationships(
1081
        QueryBuilder $builder,
1082
        string $modelClass
1083
    ): PaginatedDataInterface {
1084 25
        list($models, $hasMore, $limit, $offset) = $this->fetchResourceCollection($builder, $modelClass);
1085
1086 25
        $data = $this->getFactory()
1087 25
            ->createPaginatedData($models)
1088 25
            ->markAsCollection()
1089 25
            ->setOffset($offset)
1090 25
            ->setLimit($limit);
1091
1092 25
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
1093
1094 25
        return $data;
1095
    }
1096
1097
    /**
1098
     * @param QueryBuilder $builder
1099
     * @param string       $modelClass
1100
     *
1101
     * @return mixed|null
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use object|null.

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...
1102
     */
1103 14
    protected function fetchResourceWithoutRelationships(QueryBuilder $builder, string $modelClass)
1104
    {
1105 14
        $statement = $builder->execute();
1106 14
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1107 14
        $platform  = $builder->getConnection()->getDatabasePlatform();
1108 14
        $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1109
1110 14
        $model = null;
1111 14
        if (($attributes = $statement->fetch()) !== false) {
1112 14
            $model = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1113
        }
1114
1115 14
        return $model;
1116
    }
1117
1118
    /**
1119
     * @param QueryBuilder $builder
1120
     * @param string       $modelClass
1121
     *
1122
     * @return array
1123
     */
1124 25
    protected function fetchResourceCollection(QueryBuilder $builder, string $modelClass): array
1125
    {
1126 25
        $platform  = $builder->getConnection()->getDatabasePlatform();
1127 25
        $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1128
1129 25
        $statement = $builder->execute();
1130 25
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1131
1132 25
        $models           = [];
1133 25
        $counter          = 0;
1134 25
        $hasMoreThanLimit = false;
1135 25
        $limit            = $builder->getMaxResults() !== null ? $builder->getMaxResults() - 1 : null;
1136 25
        while (($attributes = $statement->fetch()) !== false) {
1137 24
            $counter++;
1138 24
            if ($limit !== null && $counter > $limit) {
1139 6
                $hasMoreThanLimit = true;
1140 6
                break;
1141
            }
1142 24
            $models[] = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1143
        }
1144
1145 25
        return [$models, $hasMoreThanLimit, $limit, $builder->getFirstResult()];
1146
    }
1147
1148
    /**
1149
     * @param null|string $index
1150
     * @param iterable    $attributes
1151
     *
1152
     * @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...
1153
     */
1154 4
    protected function filterAttributesOnCreate(?string $index, iterable $attributes): iterable
1155
    {
1156 4
        if ($index !== null) {
1157 1
            $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
1158 1
            yield $pkName => $index;
1159
        }
1160
1161 4
        $knownAttrAndTypes = $this->getModelSchemes()->getAttributeTypes($this->getModelClass());
1162 4
        foreach ($attributes as $attribute => $value) {
1163 4
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1164 4
                yield $attribute => $value;
1165
            }
1166
        }
1167
    }
1168
1169
    /**
1170
     * @param iterable $attributes
1171
     *
1172
     * @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...
1173
     */
1174 3
    protected function filterAttributesOnUpdate(iterable $attributes): iterable
1175
    {
1176 3
        $knownAttrAndTypes = $this->getModelSchemes()->getAttributeTypes($this->getModelClass());
1177 3
        foreach ($attributes as $attribute => $value) {
1178 3
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1179 3
                yield $attribute => $value;
1180
            }
1181
        }
1182
    }
1183
1184
    /**
1185
     * @param TagStorageInterface   $modelsAtPath
1186
     * @param ArrayObject           $classAtPath
1187
     * @param ModelStorageInterface $deDup
1188
     * @param string                $parentsPath
1189
     * @param array                 $childRelationships
1190
     *
1191
     * @return void
1192
     *
1193
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1194
     */
1195 8
    private function loadRelationshipsLayer(
1196
        TagStorageInterface $modelsAtPath,
1197
        ArrayObject $classAtPath,
1198
        ModelStorageInterface $deDup,
1199
        string $parentsPath,
1200
        array $childRelationships
1201
    ): void {
1202 8
        $rootClass   = $classAtPath[static::ROOT_PATH];
1203 8
        $parentClass = $classAtPath[$parentsPath];
1204 8
        $parents     = $modelsAtPath->get($parentsPath);
1205
1206
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
1207
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
1208
        // $models.
1209
1210 8
        $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
1211
1212 8
        foreach ($childRelationships as $name) {
1213 8
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
1214
1215 8
            $relationshipType = $this->getModelSchemes()->getRelationshipType($parentClass, $name);
1216
            list ($targetModelClass, $reverseRelName) =
1217 8
                $this->getModelSchemes()->getReverseRelationship($parentClass, $name);
1218
1219
            $builder = $this
1220 8
                ->createBuilder($targetModelClass)
1221 8
                ->selectModelFields()
1222 8
                ->fromModelTable();
1223
1224 8
            $classAtPath[$childrenPath] = $targetModelClass;
1225
1226
            switch ($relationshipType) {
1227 8
                case RelationshipTypes::BELONGS_TO:
1228 7
                    foreach ($parents as $parent) {
1229 7
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1230 7
                            $reverseRelName,
1231 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...
1232 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...
1233
                        );
1234 7
                        $child         = $deDup->register($this->fetchResourceWithoutRelationships(
1235 7
                            $clonedBuilder,
1236 7
                            $clonedBuilder->getModelClass()
1237
                        ));
1238 7
                        if ($child !== null) {
1239 6
                            $modelsAtPath->register($child, $childrenPath);
1240
                        }
1241 7
                        $parent->{$name} = $child;
1242
                    }
1243 7
                    break;
1244 6
                case RelationshipTypes::HAS_MANY:
1245 4
                case RelationshipTypes::BELONGS_TO_MANY:
1246 6
                    list ($queryOffset, $queryLimit) = $this->getPaginationStrategy()
1247 6
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
1248 6
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
1249 6
                    foreach ($parents as $parent) {
1250 6
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1251 6
                            $reverseRelName,
1252 6
                            [$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...
1253 6
                            []
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...
1254
                        );
1255 6
                        $children      = $this->fetchPaginatedResourcesWithoutRelationships(
1256 6
                            $clonedBuilder,
1257 6
                            $clonedBuilder->getModelClass()
1258
                        );
1259
1260 6
                        $deDupedChildren = [];
1261 6
                        foreach ($children->getData() as $child) {
1262 6
                            $child = $deDup->register($child);
1263 6
                            $modelsAtPath->register($child, $childrenPath);
1264 6
                            if ($child !== null) {
1265 6
                                $deDupedChildren[] = $child;
1266
                            }
1267
                        }
1268
1269 6
                        $paginated = $this->getFactory()
1270 6
                            ->createPaginatedData($deDupedChildren)
1271 6
                            ->markAsCollection()
1272 6
                            ->setOffset($children->getOffset())
1273 6
                            ->setLimit($children->getLimit());
1274 6
                        $children->hasMoreItems() === true ?
1275 6
                            $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
1276
1277 6
                        $parent->{$name} = $paginated;
1278
                    }
1279 8
                    break;
1280
            }
1281
        }
1282
    }
1283
1284
    /**
1285
     * @param string $message
1286
     *
1287
     * @return string
1288
     */
1289 7
    private function getMessage(string $message): string
1290
    {
1291
        /** @var FormatterFactoryInterface $factory */
1292 7
        $factory   = $this->getContainer()->get(FormatterFactoryInterface::class);
1293 7
        $formatter = $factory->createFormatter(Messages::RESOURCES_NAMESPACE);
1294 7
        $result    = $formatter->formatMessage($message);
1295
1296 7
        return $result;
1297
    }
1298
1299
    /**
1300
     * @param string           $class
1301
     * @param array            $attributes
1302
     * @param Type[]           $typeNames
1303
     * @param AbstractPlatform $platform
1304
     *
1305
     * @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...
1306
     *
1307
     * @SuppressWarnings(PHPMD.StaticAccess)
1308
     */
1309 31
    private function readResourceFromAssoc(
1310
        string $class,
1311
        array $attributes,
1312
        array $typeNames,
1313
        AbstractPlatform $platform
1314
    ) {
1315 31
        $instance = new $class();
1316 31
        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...
1317 31
            $instance->{$name} = $value;
1318
        }
1319
1320 31
        return $instance;
1321
    }
1322
1323
    /**
1324
     * @param array            $attributes
1325
     * @param Type[]           $typeNames
1326
     * @param AbstractPlatform $platform
1327
     *
1328
     * @return array
1329
     *
1330
     * @SuppressWarnings(PHPMD.StaticAccess)
1331
     */
1332 1
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
1333
    {
1334 1
        $row = [];
1335 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...
1336 1
            $row[$name] = $value;
1337
        }
1338
1339 1
        return $row;
1340
    }
1341
1342
    /**
1343
     * @param iterable         $attributes
1344
     * @param array            $typeNames
1345
     * @param AbstractPlatform $platform
1346
     *
1347
     * @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...
1348
     */
1349 32
    private function readTypedAttributes(iterable $attributes, array $typeNames, AbstractPlatform $platform): iterable
1350
    {
1351 32
        foreach ($attributes as $name => $value) {
1352 32
            yield $name => (array_key_exists($name, $typeNames) === true ?
1353 32
                Type::getType($typeNames[$name])->convertToPHPValue($value, $platform) : $value);
1354
        }
1355
    }
1356
}
1357