Completed
Push — develop ( 9148fa...0fd0fb )
by Neomerx
01:54
created

Crud::filterAttributesOnCreate()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 8
cts 8
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 8
nc 6
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 36
    public function withFilters(iterable $filterParameters): CrudInterface
149
    {
150 36
        $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 36
        return $this;
153
    }
154
155
    /**
156
     * @inheritdoc
157
     */
158 20
    public function withIndexFilter($index): CrudInterface
159
    {
160 20
        if (is_int($index) === false && is_string($index) === false) {
161 3
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
162
        }
163
164 17
        $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
165 17
        $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 17
                FilterParameterInterface::OPERATION_EQUALS => [$index],
168
            ],
169
        ]);
170
171 17
        return $this;
172
    }
173
174
    /**
175
     * @inheritdoc
176
     */
177 4
    public function withRelationshipFilters(string $name, iterable $filters): CrudInterface
178
    {
179 4
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__FILTERS] = $filters;
180
181 4
        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 37
    private function hasFilters(): bool
218
    {
219 37
        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 16
    public function withSorts(iterable $sortingParameters): CrudInterface
242
    {
243 16
        $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 16
        return $this;
246
    }
247
248
    /**
249
     * @return bool
250
     */
251 27
    private function hasSorts(): bool
252
    {
253 27
        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 20
    public function withIncludes(iterable $includePaths): CrudInterface
268
    {
269 20
        $this->includePaths = $includePaths;
0 ignored issues
show
Documentation Bug introduced by
It seems like $includePaths of type object<Limoncello\Flute\Contracts\Api\iterable> is incompatible with the declared type object<Limoncello\Flute\Api\iterable>|null of property $includePaths.

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

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

Loading history...
270
271 20
        return $this;
272
    }
273
274
    /**
275
     * @return bool
276
     */
277 33
    private function hasIncludes(): bool
278
    {
279 33
        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 20
    private function getIncludes(): iterable
286
    {
287 20
        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 34
    private function hasPaging(): bool
305
    {
306 34
        return $this->pagingOffset !== null && $this->pagingLimit !== null;
307
    }
308
309
    /**
310
     * @return int
311
     */
312 18
    private function getPagingOffset(): int
313
    {
314 18
        return $this->pagingOffset;
315
    }
316
317
    /**
318
     * @return int
319
     */
320 18
    private function getPagingLimit(): int
321
    {
322 18
        return $this->pagingLimit;
323
    }
324
325
    /**
326
     * @return Connection
327
     */
328 37
    private function getConnection(): Connection
329
    {
330 37
        return $this->connection;
331
    }
332
333
    /**
334
     * @param string $modelClass
335
     *
336
     * @return ModelQueryBuilder
337
     */
338 37
    private function createBuilder(string $modelClass): ModelQueryBuilder
339
    {
340 37
        return $this->createBuilderFromConnection($this->getConnection(), $modelClass);
341
    }
342
343
    /**
344
     * @param Connection $connection
345
     * @param string     $modelClass
346
     *
347
     * @return ModelQueryBuilder
348
     */
349 37
    private function createBuilderFromConnection(Connection $connection, string $modelClass): ModelQueryBuilder
350
    {
351 37
        return $this->getFactory()->createModelQueryBuilder($connection, $modelClass, $this->getModelSchemes());
352
    }
353
354
    /**
355
     * @param ModelQueryBuilder $builder
356
     *
357
     * @return Crud
358
     */
359 28
    private function applyAliasFilters(ModelQueryBuilder $builder): self
360
    {
361 28
        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 28
        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 27
    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 27
        $distinctApplied = false;
395
396 27
        foreach ($this->relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
397 4
            assert(is_string($relationshipName) === true && is_array($filtersAndSorts) === true);
398 4
            $builder->addRelationshipFiltersAndSortsWithAnd(
399 4
                $relationshipName,
400 4
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__FILTERS] ?? [],
401 4
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__SORTS] ?? []
402
            );
403
404 4
            if ($distinctApplied === false) {
405 4
                $builder->distinct();
406 4
                $distinctApplied = true;
407
            }
408
        }
409
410 27
        return $this;
411
    }
412
413
    /**
414
     * @param ModelQueryBuilder $builder
415
     *
416
     * @return self
417
     */
418 27
    private function applySorts(ModelQueryBuilder $builder): self
419
    {
420 27
        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 27
        return $this;
425
    }
426
427
    /**
428
     * @param ModelQueryBuilder $builder
429
     *
430
     * @return self
431
     */
432 34
    private function applyPaging(ModelQueryBuilder $builder): self
433
    {
434 34
        if ($this->hasPaging() === true) {
435 18
            $builder->setFirstResult($this->getPagingOffset());
436 18
            $builder->setMaxResults($this->getPagingLimit() + 1);
437
        }
438
439 34
        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 27
    protected function builderOnIndex(ModelQueryBuilder $builder): ModelQueryBuilder
483
    {
484 27
        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 20
    private function loadRelationships(PaginatedDataInterface $data): void
580
    {
581 20
        if (empty($data->getData()) === false && $this->hasIncludes() === true) {
582 20
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemes());
583 20
            $modelsAtPath = $this->getFactory()->createTagStorage();
584
585
            // we gonna send this storage via function params so it is an equivalent for &array
586 20
            $classAtPath = new ArrayObject();
587
588 20
            $model = null;
589 20
            if ($data->isCollection() === true) {
590 14
                foreach ($data->getData() as $model) {
591 14
                    $uniqueModel = $modelStorage->register($model);
592 14
                    if ($uniqueModel !== null) {
593 14
                        $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 20
            $classAtPath[static::ROOT_PATH] = get_class($model);
604
605 20
            foreach ($this->getPaths($this->getIncludes()) as list ($parentPath, $childPaths)) {
0 ignored issues
show
Bug introduced by
It seems like $this->getIncludes() can be null; however, getPaths() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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 20
    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 20
        $normalizedPaths = [];
629 20
        $pathsDepths     = [];
630 20
        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 20
        $parentWithChildren = [];
647 20
        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 20
        asort($pathsDepths, SORT_NUMERIC);
653 20
        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 27
    protected function getIndexModelBuilder(): ModelQueryBuilder
680
    {
681
        $builder = $this
682 27
            ->createBuilder($this->getModelClass())
683 27
            ->selectModelFields()
684 27
            ->fromModelTable();
685
686
        $this
687 27
            ->applyAliasFilters($builder)
688 27
            ->applySorts($builder)
689 27
            ->applyRelationshipFiltersAndSorts($builder)
690 27
            ->applyPaging($builder);
691
692 27
        $result = $this->builderOnIndex($builder);
693
694 27
        $this->clearBuilderParameters();
695
696 27
        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 16
    public function index(): PaginatedDataInterface
721
    {
722 16
        $builder = $this->getIndexModelBuilder();
723 16
        $data    = $this->fetchResources($builder, $builder->getModelClass());
724
725 16
        return $data;
726
    }
727
728
    /**
729
     * @inheritdoc
730
     */
731 12
    public function read($index): PaginatedDataInterface
732
    {
733 12
        $this->withIndexFilter($index);
734
735 10
        $builder = $this->getIndexModelBuilder();
736 10
        $data    = $this->fetchResource($builder, $builder->getModelClass());
737
738 10
        return $data;
739
    }
740
741
    /**
742
     * @inheritdoc
743
     */
744 1
    public function count(): ?int
745
    {
746
        $builder = $this
747 1
            ->createBuilder($this->getModelClass())
748 1
            ->select('COUNT(*)')
749 1
            ->fromModelTable();
750
751 1
        $this->applyAliasFilters($builder);
752
753 1
        $this->clearBuilderParameters()->clearFetchParameters();
754
755 1
        $result = $this->builderOnCount($builder)->execute()->fetchColumn();
756
757 1
        return $result === false ? null : $result;
758
    }
759
760
    /**
761
     * @param string        $relationshipName
762
     * @param iterable|null $relationshipFilters
763
     * @param iterable|null $relationshipSorts
764
     *
765
     * @return ModelQueryBuilder
766
     */
767 8
    public function createReadRelationshipBuilder(
768
        string $relationshipName,
769
        iterable $relationshipFilters = null,
770
        iterable $relationshipSorts = null
771
    ): ModelQueryBuilder {
772
        // as we read data from a relationship our main table and model would be the table/model in the relationship
773
        // so 'root' model(s) will be located in the reverse relationship.
774
775
        list ($targetModelClass, $reverseRelName) =
776 8
            $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $relationshipName);
777
778
        $builder = $this
779 8
            ->createBuilder($targetModelClass)
780 8
            ->selectModelFields()
781 8
            ->fromModelTable();
782
783
        // 'root' filters would be applied to the data in the reverse relationship ...
784 8
        if ($this->hasFilters() === true) {
785 8
            $filters = $this->getFilters();
786 8
            $sorts   = $this->getSorts();
787 8
            $this->areFiltersWithAnd() ?
788 8
                $builder->addRelationshipFiltersAndSortsWithAnd($reverseRelName, $filters, $sorts) :
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 785 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...
789
                $builder->addRelationshipFiltersAndSortsWithOr($reverseRelName, $filters, $sorts);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 785 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...
790
        }
791
        // ... and the input filters to actual data we select
792 8
        if ($relationshipFilters !== null) {
793 5
            $builder->addFiltersWithAndToAlias($relationshipFilters);
794
        }
795 8
        if ($relationshipSorts !== null) {
796 1
            $builder->addSorts($relationshipSorts);
797
        }
798
799 8
        $this->applyPaging($builder);
800
801
        // While joining tables we select distinct rows.
802 8
        $builder->distinct();
803
804 8
        return $this->builderOnReadRelationship($builder);
805
    }
806
807
    /**
808
     * @inheritdoc
809
     */
810 8
    public function indexRelationship(
811
        string $name,
812
        iterable $relationshipFilters = null,
813
        iterable $relationshipSorts = null
814
    ): PaginatedDataInterface {
815
        // depending on the relationship type we expect the result to be either single resource or a collection
816 8
        $relationshipType = $this->getModelSchemes()->getRelationshipType($this->getModelClass(), $name);
817 8
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
818 8
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
819
820 8
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts);
821
822 8
        $modelClass = $builder->getModelClass();
823 8
        $data       = $isExpectMany === true ?
824 8
            $this->fetchResources($builder, $modelClass) : $this->fetchResource($builder, $modelClass);
825
826 8
        return $data;
827
    }
828
829
    /**
830
     * @inheritdoc
831
     */
832 3
    public function readRelationship(
833
        $index,
834
        string $name,
835
        iterable $relationshipFilters = null,
836
        iterable $relationshipSorts = null
837
    ): PaginatedDataInterface {
838 3
        return $this->withIndexFilter($index)->indexRelationship($name, $relationshipFilters, $relationshipSorts);
839
    }
840
841
    /**
842
     * @inheritdoc
843
     */
844 6
    public function hasInRelationship($parentId, string $name, $childId): bool
845
    {
846 6
        if ($parentId !== null && is_scalar($parentId) === false) {
847 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
848
        }
849 5
        if ($childId !== null && is_scalar($childId) === false) {
850 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
851
        }
852
853 4
        $parentPkName  = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
854 4
        $parentFilters = [$parentPkName => [FilterParameterInterface::OPERATION_EQUALS => [$parentId]]];
855 4
        list($childClass) = $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $name);
856 4
        $childPkName  = $this->getModelSchemes()->getPrimaryKey($childClass);
857 4
        $childFilters = [$childPkName => [FilterParameterInterface::OPERATION_EQUALS => [$childId]]];
858
859
        $data = $this
860 4
            ->clearBuilderParameters()
861 4
            ->clearFetchParameters()
862 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...
863 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...
864
865 4
        $has = empty($data->getData()) === false;
866
867 4
        return $has;
868
    }
869
870
    /**
871
     * @inheritdoc
872
     */
873 5
    public function delete(): int
874
    {
875 5
        $deleted = $this->getDeleteModelBuilder()->execute();
876
877 4
        $this->clearFetchParameters();
878
879 4
        return (int)$deleted;
880
    }
881
882
    /**
883
     * @inheritdoc
884
     */
885 6
    public function remove($index): bool
886
    {
887 6
        return $this->withIndexFilter($index)->delete() > 0;
888
    }
889
890
    /**
891
     * @inheritdoc
892
     */
893 5
    public function create($index, iterable $attributes, iterable $toMany): string
894
    {
895 5
        if ($index !== null && is_int($index) === false && is_string($index) === false) {
896 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
897
        }
898
899 4
        $allowedChanges = $this->filterAttributesOnCreate($index, $attributes);
900
        $saveMain       = $this
901 4
            ->createBuilder($this->getModelClass())
902 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...
903 4
        $saveMain       = $this->builderSaveResourceOnCreate($saveMain);
904 4
        $saveMain->getSQL(); // prepare
905
906 4
        $this->clearBuilderParameters()->clearFetchParameters();
907
908 4
        $this->inTransaction($saveMain->getConnection(), function () use ($saveMain, $toMany, &$index) {
909 4
            $saveMain->execute();
910
911
            // if no index given will use last insert ID as index
912 4
            $index !== null ?: $index = $saveMain->getConnection()->lastInsertId();
913
914 4
            $inserted = 0;
915 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
916 2
                $secondaryIdBindName = ':secondaryId';
917 2
                $saveToMany          = $this->builderSaveRelationshipOnCreate(
918 2
                    $relationshipName,
919
                    $this
920 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
921 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
922
                );
923 2
                foreach ($secondaryIds as $secondaryId) {
924 2
                    $inserted += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
925
                }
926
            }
927 4
        });
928
929 4
        return $index;
930
    }
931
932
    /**
933
     * @inheritdoc
934
     */
935 4
    public function update($index, iterable $attributes, iterable $toMany): int
936
    {
937 4
        if (is_int($index) === false && is_string($index) === false) {
938 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
939
        }
940
941 3
        $updated        = 0;
942 3
        $pkName         = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
943
        $filters        = [
944
            $pkName => [
945 3
                FilterParameterInterface::OPERATION_EQUALS => [$index],
946
            ],
947
        ];
948 3
        $allowedChanges = $this->filterAttributesOnUpdate($attributes);
949
        $saveMain       = $this
950 3
            ->createBuilder($this->getModelClass())
951 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...
952 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...
953 3
        $saveMain       = $this->builderSaveResourceOnUpdate($saveMain);
954 3
        $saveMain->getSQL(); // prepare
955
956 3
        $this->clearBuilderParameters()->clearFetchParameters();
957
958 3
        $this->inTransaction($saveMain->getConnection(), function () use ($saveMain, $toMany, $index, &$updated) {
959 3
            $updated = $saveMain->execute();
960
961 3
            foreach ($toMany as $relationshipName => $secondaryIds) {
962 2
                $cleanToMany = $this->builderCleanRelationshipOnUpdate(
963 2
                    $relationshipName,
964
                    $this
965 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
966 2
                        ->clearToManyRelationship($relationshipName, $index)
967
                );
968 2
                $cleanToMany->execute();
969
970 2
                $secondaryIdBindName = ':secondaryId';
971 2
                $saveToMany          = $this->builderSaveRelationshipOnUpdate(
972 2
                    $relationshipName,
973
                    $this
974 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
975 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
976
                );
977 2
                foreach ($secondaryIds as $secondaryId) {
978 2
                    $updated += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
979
                }
980
            }
981 3
        });
982
983 3
        return (int)$updated;
984
    }
985
986
    /**
987
     * @return FactoryInterface
988
     */
989 37
    protected function getFactory(): FactoryInterface
990
    {
991 37
        return $this->factory;
992
    }
993
994
    /**
995
     * @return string
996
     */
997 37
    protected function getModelClass(): string
998
    {
999 37
        return $this->modelClass;
1000
    }
1001
1002
    /**
1003
     * @return ModelSchemeInfoInterface
1004
     */
1005 37
    protected function getModelSchemes(): ModelSchemeInfoInterface
1006
    {
1007 37
        return $this->modelSchemes;
1008
    }
1009
1010
    /**
1011
     * @return PaginationStrategyInterface
1012
     */
1013 6
    protected function getPaginationStrategy(): PaginationStrategyInterface
1014
    {
1015 6
        return $this->paginationStrategy;
1016
    }
1017
1018
    /**
1019
     * @param Connection $connection
1020
     * @param Closure    $closure
1021
     *
1022
     * @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...
1023
     */
1024 7
    protected function inTransaction(Connection $connection, Closure $closure): void
1025
    {
1026 7
        $connection->beginTransaction();
1027
        try {
1028 7
            $isOk = ($closure() === false ? null : true);
1029 7
        } finally {
1030 7
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
1031
        }
1032
    }
1033
1034
    /**
1035
     * @inheritdoc
1036
     */
1037 23
    public function fetchResources(
1038
        QueryBuilder $builder = null,
1039
        string $modelClass = null
1040
    ): PaginatedDataInterface {
1041 23
        if ($builder === null && $modelClass === null) {
1042
            $builder    = $this->getIndexModelBuilder();
1043
            $modelClass = $builder->getModelClass();
1044
        }
1045
1046 23
        $data = $this->fetchPaginatedResourcesWithoutRelationships($builder, $modelClass);
0 ignored issues
show
Bug introduced by
It seems like $builder defined by parameter $builder on line 1038 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...
1047
1048 23
        if ($this->hasIncludes() === true) {
1049 14
            $this->loadRelationships($data);
1050 14
            $this->clearFetchParameters();
1051
        }
1052
1053 23
        return $data;
1054
    }
1055
1056
    /**
1057
     * @inheritdoc
1058
     */
1059 11
    public function fetchResource(
1060
        QueryBuilder $builder = null,
1061
        string $modelClass = null
1062
    ): PaginatedDataInterface {
1063 11
        if ($builder === null && $modelClass === null) {
1064
            $builder    = $this->getIndexModelBuilder();
1065
            $modelClass = $builder->getModelClass();
1066
        }
1067
1068 11
        $data = $this->getFactory()->createPaginatedData(
1069 11
            $this->fetchResourceWithoutRelationships($builder, $modelClass)
0 ignored issues
show
Bug introduced by
It seems like $builder defined by parameter $builder on line 1060 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...
1070 11
        )->markAsSingleItem();
1071
1072 11
        if ($this->hasIncludes() === true) {
1073 6
            $this->loadRelationships($data);
1074 6
            $this->clearFetchParameters();
1075
        }
1076
1077 11
        return $data;
1078
    }
1079
1080
    /**
1081
     * @inheritdoc
1082
     */
1083 1
    public function fetchRow(
1084
        QueryBuilder $builder = null,
1085
        string $modelClass = null
1086
    ): ?array {
1087 1
        if ($builder === null && $modelClass === null) {
1088 1
            $builder    = $this->getIndexModelBuilder();
1089 1
            $modelClass = $builder->getModelClass();
1090
        }
1091
1092 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...
1093 1
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1094 1
        $platform  = $builder->getConnection()->getDatabasePlatform();
1095 1
        $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1096
1097 1
        $model = null;
1098 1
        if (($attributes = $statement->fetch()) !== false) {
1099 1
            $model = $this->readRowFromAssoc($attributes, $typeNames, $platform);
1100
        }
1101
1102 1
        $this->clearFetchParameters();
1103
1104 1
        return $model;
1105
    }
1106
1107
    /**
1108
     * @param QueryBuilder $builder
1109
     * @param string       $modelClass
1110
     *
1111
     * @return PaginatedDataInterface
1112
     */
1113 26
    private function fetchPaginatedResourcesWithoutRelationships(
1114
        QueryBuilder $builder,
1115
        string $modelClass
1116
    ): PaginatedDataInterface {
1117 26
        list($models, $hasMore, $limit, $offset) = $this->fetchResourceCollection($builder, $modelClass);
1118
1119 26
        $data = $this->getFactory()
1120 26
            ->createPaginatedData($models)
1121 26
            ->markAsCollection()
1122 26
            ->setOffset($offset)
1123 26
            ->setLimit($limit);
1124
1125 26
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
1126
1127 26
        return $data;
1128
    }
1129
1130
    /**
1131
     * @param QueryBuilder $builder
1132
     * @param string       $modelClass
1133
     *
1134
     * @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...
1135
     */
1136 14
    protected function fetchResourceWithoutRelationships(QueryBuilder $builder, string $modelClass)
1137
    {
1138 14
        $statement = $builder->execute();
1139 14
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1140 14
        $platform  = $builder->getConnection()->getDatabasePlatform();
1141 14
        $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1142
1143 14
        $model = null;
1144 14
        if (($attributes = $statement->fetch()) !== false) {
1145 14
            $model = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1146
        }
1147
1148 14
        return $model;
1149
    }
1150
1151
    /**
1152
     * @param QueryBuilder $builder
1153
     * @param string       $modelClass
1154
     *
1155
     * @return array
1156
     */
1157 26
    protected function fetchResourceCollection(QueryBuilder $builder, string $modelClass): array
1158
    {
1159 26
        $platform  = $builder->getConnection()->getDatabasePlatform();
1160 26
        $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1161
1162 26
        $statement = $builder->execute();
1163 26
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1164
1165 26
        $models           = [];
1166 26
        $counter          = 0;
1167 26
        $hasMoreThanLimit = false;
1168 26
        $limit            = $builder->getMaxResults() !== null ? $builder->getMaxResults() - 1 : null;
1169 26
        while (($attributes = $statement->fetch()) !== false) {
1170 25
            $counter++;
1171 25
            if ($limit !== null && $counter > $limit) {
1172 6
                $hasMoreThanLimit = true;
1173 6
                break;
1174
            }
1175 25
            $models[] = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1176
        }
1177
1178 26
        return [$models, $hasMoreThanLimit, $limit, $builder->getFirstResult()];
1179
    }
1180
1181
    /**
1182
     * @param null|string $index
1183
     * @param iterable    $attributes
1184
     *
1185
     * @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...
1186
     */
1187 4
    protected function filterAttributesOnCreate(?string $index, iterable $attributes): iterable
1188
    {
1189 4
        if ($index !== null) {
1190 1
            $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
1191 1
            yield $pkName => $index;
1192
        }
1193
1194 4
        $knownAttrAndTypes = $this->getModelSchemes()->getAttributeTypes($this->getModelClass());
1195 4
        foreach ($attributes as $attribute => $value) {
1196 4
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1197 4
                yield $attribute => $value;
1198
            }
1199
        }
1200
    }
1201
1202
    /**
1203
     * @param iterable $attributes
1204
     *
1205
     * @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...
1206
     */
1207 3
    protected function filterAttributesOnUpdate(iterable $attributes): iterable
1208
    {
1209 3
        $knownAttrAndTypes = $this->getModelSchemes()->getAttributeTypes($this->getModelClass());
1210 3
        foreach ($attributes as $attribute => $value) {
1211 3
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1212 3
                yield $attribute => $value;
1213
            }
1214
        }
1215
    }
1216
1217
    /**
1218
     * @param TagStorageInterface   $modelsAtPath
1219
     * @param ArrayObject           $classAtPath
1220
     * @param ModelStorageInterface $deDup
1221
     * @param string                $parentsPath
1222
     * @param array                 $childRelationships
1223
     *
1224
     * @return void
1225
     *
1226
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1227
     */
1228 8
    private function loadRelationshipsLayer(
1229
        TagStorageInterface $modelsAtPath,
1230
        ArrayObject $classAtPath,
1231
        ModelStorageInterface $deDup,
1232
        string $parentsPath,
1233
        array $childRelationships
1234
    ): void {
1235 8
        $rootClass   = $classAtPath[static::ROOT_PATH];
1236 8
        $parentClass = $classAtPath[$parentsPath];
1237 8
        $parents     = $modelsAtPath->get($parentsPath);
1238
1239
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
1240
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
1241
        // $models.
1242
1243 8
        $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
1244
1245 8
        foreach ($childRelationships as $name) {
1246 8
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
1247
1248 8
            $relationshipType = $this->getModelSchemes()->getRelationshipType($parentClass, $name);
1249
            list ($targetModelClass, $reverseRelName) =
1250 8
                $this->getModelSchemes()->getReverseRelationship($parentClass, $name);
1251
1252
            $builder = $this
1253 8
                ->createBuilder($targetModelClass)
1254 8
                ->selectModelFields()
1255 8
                ->fromModelTable();
1256
1257 8
            $classAtPath[$childrenPath] = $targetModelClass;
1258
1259
            switch ($relationshipType) {
1260 8
                case RelationshipTypes::BELONGS_TO:
1261 7
                    foreach ($parents as $parent) {
1262 7
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1263 7
                            $reverseRelName,
1264 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...
1265 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...
1266
                        );
1267 7
                        $child         = $deDup->register($this->fetchResourceWithoutRelationships(
1268 7
                            $clonedBuilder,
1269 7
                            $clonedBuilder->getModelClass()
1270
                        ));
1271 7
                        if ($child !== null) {
1272 6
                            $modelsAtPath->register($child, $childrenPath);
1273
                        }
1274 7
                        $parent->{$name} = $child;
1275
                    }
1276 7
                    break;
1277 6
                case RelationshipTypes::HAS_MANY:
1278 4
                case RelationshipTypes::BELONGS_TO_MANY:
1279 6
                    list ($queryOffset, $queryLimit) = $this->getPaginationStrategy()
1280 6
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
1281 6
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
1282 6
                    foreach ($parents as $parent) {
1283 6
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1284 6
                            $reverseRelName,
1285 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...
1286 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...
1287
                        );
1288 6
                        $children      = $this->fetchPaginatedResourcesWithoutRelationships(
1289 6
                            $clonedBuilder,
1290 6
                            $clonedBuilder->getModelClass()
1291
                        );
1292
1293 6
                        $deDupedChildren = [];
1294 6
                        foreach ($children->getData() as $child) {
1295 6
                            $child = $deDup->register($child);
1296 6
                            $modelsAtPath->register($child, $childrenPath);
1297 6
                            if ($child !== null) {
1298 6
                                $deDupedChildren[] = $child;
1299
                            }
1300
                        }
1301
1302 6
                        $paginated = $this->getFactory()
1303 6
                            ->createPaginatedData($deDupedChildren)
1304 6
                            ->markAsCollection()
1305 6
                            ->setOffset($children->getOffset())
1306 6
                            ->setLimit($children->getLimit());
1307 6
                        $children->hasMoreItems() === true ?
1308 6
                            $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
1309
1310 6
                        $parent->{$name} = $paginated;
1311
                    }
1312 8
                    break;
1313
            }
1314
        }
1315
    }
1316
1317
    /**
1318
     * @param string $message
1319
     *
1320
     * @return string
1321
     */
1322 7
    private function getMessage(string $message): string
1323
    {
1324
        /** @var FormatterFactoryInterface $factory */
1325 7
        $factory   = $this->getContainer()->get(FormatterFactoryInterface::class);
1326 7
        $formatter = $factory->createFormatter(Messages::RESOURCES_NAMESPACE);
1327 7
        $result    = $formatter->formatMessage($message);
1328
1329 7
        return $result;
1330
    }
1331
1332
    /**
1333
     * @param string           $class
1334
     * @param array            $attributes
1335
     * @param Type[]           $typeNames
1336
     * @param AbstractPlatform $platform
1337
     *
1338
     * @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...
1339
     *
1340
     * @SuppressWarnings(PHPMD.StaticAccess)
1341
     */
1342 32
    private function readResourceFromAssoc(
1343
        string $class,
1344
        array $attributes,
1345
        array $typeNames,
1346
        AbstractPlatform $platform
1347
    ) {
1348 32
        $instance = new $class();
1349 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...
1350 32
            $instance->{$name} = $value;
1351
        }
1352
1353 32
        return $instance;
1354
    }
1355
1356
    /**
1357
     * @param array            $attributes
1358
     * @param Type[]           $typeNames
1359
     * @param AbstractPlatform $platform
1360
     *
1361
     * @return array
1362
     *
1363
     * @SuppressWarnings(PHPMD.StaticAccess)
1364
     */
1365 1
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
1366
    {
1367 1
        $row = [];
1368 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...
1369 1
            $row[$name] = $value;
1370
        }
1371
1372 1
        return $row;
1373
    }
1374
1375
    /**
1376
     * @param iterable         $attributes
1377
     * @param array            $typeNames
1378
     * @param AbstractPlatform $platform
1379
     *
1380
     * @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...
1381
     */
1382 33
    private function readTypedAttributes(iterable $attributes, array $typeNames, AbstractPlatform $platform): iterable
1383
    {
1384 33
        foreach ($attributes as $name => $value) {
1385 33
            yield $name => (array_key_exists($name, $typeNames) === true ?
1386 33
                Type::getType($typeNames[$name])->convertToPHPValue($value, $platform) : $value);
1387
        }
1388
    }
1389
}
1390