Issues (197)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Api/Crud.php (38 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
978
        /** @var Generator $data */
979
        $data   = $this->fetchColumn($builder, $builder->getModelClass(), $pkName);
980
        $result = iterator_to_array($data);
981 13
982
        return $result;
983 13
    }
984
985 13
    /**
986 13
     * @inheritdoc
987
     *
988 13
     * @throws DBALException
989
     */
990
    public function read(string $index)
991
    {
992
        $this->withIndexFilter($index);
993
994
        $builder = $this->createIndexModelBuilder();
995
        $data    = $this->fetchResource($builder, $builder->getModelClass());
996 2
997
        return $data;
998 2
    }
999 2
1000 2
    /**
1001
     * @inheritdoc
1002 2
     *
1003
     * @throws DBALException
1004
     */
1005
    public function count(): ?int
1006
    {
1007
        $result = $this->builderOnCount(
1008
            $this->createCountBuilderFromBuilder($this->createIndexModelBuilder())
1009
        )->execute()->fetchColumn();
1010
1011
        return $result === false ? null : (int)$result;
1012
    }
1013
1014
    /**
1015 7
     * @param string        $relationshipName
1016
     * @param iterable|null $relationshipFilters
1017
     * @param iterable|null $relationshipSorts
1018
     * @param iterable|null $columns
1019
     *
1020
     * @return ModelQueryBuilder
1021 7
     *
1022 7
     * @throws DBALException
1023 7
     */
1024
    public function createReadRelationshipBuilder(
1025
        string $relationshipName,
1026
        iterable $relationshipFilters = null,
1027
        iterable $relationshipSorts = null,
1028
        iterable $columns = null
1029
    ): ModelQueryBuilder {
1030 7
        assert(
1031
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $relationshipName),
1032
            "Relationship `$relationshipName` do not exist in model `" . $this->getModelClass() . '`'
1033 7
        );
1034 7
1035 7
        // as we read data from a relationship our main table and model would be the table/model in the relationship
1036
        // so 'root' model(s) will be located in the reverse relationship.
1037
1038 7
        list ($targetModelClass, $reverseRelName) =
1039 7
            $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $relationshipName);
1040 7
1041 7
        $builder = $this
1042 7
            ->createBuilder($targetModelClass)
1043
            ->selectModelColumns($columns)
1044
            ->fromModelTable();
1045 7
1046 4
        // 'root' filters would be applied to the data in the reverse relationship ...
1047
        if ($this->hasFilters() === true) {
1048 7
            $filters = $this->getFilters();
1049 3
            $sorts   = $this->getSorts();
1050
            $joinCondition = $this->areFiltersWithAnd() === true ? ModelQueryBuilder::AND : ModelQueryBuilder::OR;
1051
            $builder->addRelationshipFiltersAndSorts($reverseRelName, $filters, $sorts, $joinCondition);
1052 7
        }
1053
        // ... and the input filters to actual data we select
1054
        if ($relationshipFilters !== null) {
1055 7
            $builder->addFiltersWithAndToAlias($relationshipFilters);
1056
        }
1057 7
        if ($relationshipSorts !== null) {
1058
            $builder->addSorts($relationshipSorts);
1059
        }
1060
1061
        $this->applyPaging($builder);
1062
1063
        // While joining tables we select distinct rows.
1064
        $builder->distinct();
1065 6
1066
        return $this->builderOnReadRelationship($builder);
1067
    }
1068
1069
    /**
1070 6
     * @inheritdoc
1071 6
     *
1072 6
     * @throws DBALException
1073
     */
1074
    public function indexRelationship(
1075
        string $name,
1076 6
        iterable $relationshipFilters = null,
1077 6
        iterable $relationshipSorts = null
1078 6
    ) {
1079
        assert(
1080 6
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $name),
1081
            "Relationship `$name` do not exist in model `" . $this->getModelClass() . '`'
1082 6
        );
1083 6
1084 6
        // depending on the relationship type we expect the result to be either single resource or a collection
1085
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1086 6
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
1087
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
1088
1089
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts);
1090
1091
        $modelClass = $builder->getModelClass();
1092
        $data       = $isExpectMany === true ?
1093
            $this->fetchResources($builder, $modelClass) : $this->fetchResource($builder, $modelClass);
1094 2
1095
        return $data;
1096
    }
1097
1098
    /**
1099 2
     * @inheritdoc
1100 2
     *
1101 2
     * @throws DBALException
1102
     */
1103
    public function indexRelationshipIdentities(
1104
        string $name,
1105 2
        iterable $relationshipFilters = null,
1106 2
        iterable $relationshipSorts = null
1107 2
    ): array {
1108 2
        assert(
1109 1
            $this->getModelSchemas()->hasRelationship($this->getModelClass(), $name),
1110
            "Relationship `$name` do not exist in model `" . $this->getModelClass() . '`'
1111
        );
1112 1
1113 1
        // depending on the relationship type we expect the result to be either single resource or a collection
1114
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1115 1
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
1116
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
1117 1
        if ($isExpectMany === false) {
1118
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
1119 1
        }
1120 1
1121
        list ($targetModelClass) = $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $name);
1122 1
        $targetPk = $this->getModelSchemas()->getPrimaryKey($targetModelClass);
1123
1124
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts, [$targetPk]);
0 ignored issues
show
array($targetPk) is of type array<integer,string,{"0":"string"}>, but the function expects a object<Limoncello\Flute\Api\iterable>|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1125
1126
        $modelClass = $builder->getModelClass();
1127
        /** @var Generator $data */
1128 3
        $data   = $this->fetchColumn($builder, $modelClass, $targetPk);
1129
        $result = iterator_to_array($data);
1130
1131
        return $result;
1132
    }
1133
1134 3
    /**
1135
     * @inheritdoc
1136
     */
1137
    public function readRelationship(
1138
        string $index,
1139
        string $name,
1140 1
        iterable $relationshipFilters = null,
1141
        iterable $relationshipSorts = null
1142 1
    ) {
1143 1
        return $this->withIndexFilter($index)->indexRelationship($name, $relationshipFilters, $relationshipSorts);
1144 1
    }
1145 1
1146 1
    /**
1147
     * @inheritdoc
1148
     */
1149 1
    public function hasInRelationship(string $parentId, string $name, string $childId): bool
1150 1
    {
1151 1
        $parentPkName  = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1152 1
        $parentFilters = [$parentPkName => [FilterParameterInterface::OPERATION_EQUALS => [$parentId]]];
1153
        list($childClass) = $this->getModelSchemas()->getReverseRelationship($this->getModelClass(), $name);
1154 1
        $childPkName  = $this->getModelSchemas()->getPrimaryKey($childClass);
1155
        $childFilters = [$childPkName => [FilterParameterInterface::OPERATION_EQUALS => [$childId]]];
1156 1
1157
        $data = $this
1158
            ->clearBuilderParameters()
1159
            ->clearFetchParameters()
1160
            ->withFilters($parentFilters)
0 ignored issues
show
$parentFilters is of type array<string,array<strin...tring,{"0":"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...
1161
            ->indexRelationship($name, $childFilters);
0 ignored issues
show
$childFilters is of type array<string,array<strin...tring,{"0":"string"}>>>, 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...
1162
1163
        $has = empty($data->getData()) === false;
1164 1
1165
        return $has;
1166 1
    }
1167
1168 1
    /**
1169
     * @inheritdoc
1170 1
     *
1171
     * @throws DBALException
1172
     */
1173
    public function delete(): int
1174
    {
1175
        $deleted = $this->createDeleteBuilder()->execute();
1176
1177
        $this->clearFetchParameters();
1178 4
1179
        return (int)$deleted;
1180 4
    }
1181
1182 4
    /**
1183
     * @inheritdoc
1184 3
     *
1185
     * @throws DBALException
1186 3
     */
1187
    public function remove(string $index): bool
1188
    {
1189
        $this->withIndexFilter($index);
1190
1191
        $deleted = $this->createDeleteBuilder()->execute();
1192
1193
        $this->clearFetchParameters();
1194
1195
        return (int)$deleted > 0;
1196 5
    }
1197
1198 5
    /**
1199
     * @inheritdoc
1200 5
     *
1201 5
     * @throws DBALException
1202 5
     *
1203 5
     * @SuppressWarnings(PHPMD.StaticAccess)
1204
     */
1205 5
    public function create(?string $index, array $attributes, array $toMany): string
1206
    {
1207
        $allowedChanges = $this->filterAttributesOnCreate($index, $attributes);
0 ignored issues
show
$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...
1208 5
        $saveMain       = $this
1209
            ->createBuilder($this->getModelClass())
1210
            ->createModel($allowedChanges);
0 ignored issues
show
$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...
1211 4
        $saveMain       = $this->builderSaveResourceOnCreate($saveMain);
1212 4
        $saveMain->getSQL(); // prepare
1213
1214 4
        $this->clearBuilderParameters()->clearFetchParameters();
1215 4
1216 2
        $this->inTransaction(function () use ($saveMain, $toMany, &$index) {
1217
            $saveMain->execute();
1218 5
1219
            // if no index given will use last insert ID as index
1220 4
            $connection = $saveMain->getConnection();
1221
            $index !== null ?: $index = $connection->lastInsertId();
1222
1223
            $builderHook = Closure::fromCallable([$this, 'builderSaveRelationshipOnCreate']);
1224
            foreach ($toMany as $relationshipName => $secondaryIds) {
1225
                $this->addInToManyRelationship($connection, $index, $relationshipName, $secondaryIds, $builderHook);
1226
            }
1227
        });
1228
1229
        return $index;
1230 5
    }
1231
1232 5
    /**
1233 5
     * @inheritdoc
1234
     *
1235
     * @throws DBALException
1236 5
     *
1237
     * @SuppressWarnings(PHPMD.StaticAccess)
1238
     */
1239 5
    public function update(string $index, array $attributes, array $toMany): int
1240
    {
1241 5
        $updated        = 0;
1242 5
        $pkName         = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1243 5
        $filters        = [
1244 5
            $pkName => [
1245 5
                FilterParameterInterface::OPERATION_EQUALS => [$index],
1246
            ],
1247 5
        ];
1248
        $allowedChanges = $this->filterAttributesOnUpdate($attributes);
0 ignored issues
show
$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...
1249
        $saveMain       = $this
1250 5
            ->createBuilder($this->getModelClass())
1251
            ->updateModels($allowedChanges)
0 ignored issues
show
$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...
1252 4
            ->addFiltersWithAndToTable($filters);
0 ignored issues
show
$filters is of type array<string,array<strin...tring,{"0":"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...
1253 4
        $saveMain       = $this->builderSaveResourceOnUpdate($saveMain);
1254 2
        $saveMain->getSQL(); // prepare
1255
1256
        $this->clearBuilderParameters()->clearFetchParameters();
1257 2
1258 2
        $this->inTransaction(function () use ($saveMain, $toMany, $index, &$updated) {
1259
            $updated = $saveMain->execute();
1260 2
1261 2
            $builderHook = Closure::fromCallable([$this, 'builderSaveRelationshipOnUpdate']);
1262 2
            foreach ($toMany as $relationshipName => $secondaryIds) {
1263
                $connection = $saveMain->getConnection();
1264
1265 2
                // clear existing
1266 2
                $this->builderCleanRelationshipOnUpdate(
1267 2
                    $relationshipName,
1268 2
                    $this
1269 2
                        ->createBuilderFromConnection($this->getConnection(), $this->getModelClass())
1270 2
                        ->clearToManyRelationship($relationshipName, $index)
1271
                )->execute();
1272
1273 5
                // add new ones
1274
                $updated   += $this->addInToManyRelationship(
1275 4
                    $connection,
1276
                    $index,
1277
                    $relationshipName,
1278
                    $secondaryIds,
1279
                    $builderHook
1280
                );
1281
            }
1282
        });
1283
1284
        return (int)$updated;
1285
    }
1286
1287
    /**
1288
     * @param string   $parentId
1289 1
     * @param string   $name
1290
     * @param iterable $childIds
1291
     *
1292
     * @return int
1293 1
     *
1294 1
     * @throws DBALException
1295 1
     *
1296 1
     * @SuppressWarnings(PHPMD.StaticAccess)
1297
     */
1298 1
    public function createInBelongsToManyRelationship(string $parentId, string $name, iterable $childIds): int
1299
    {
1300 1
        // Check that relationship is `BelongsToMany`
1301 1
        assert(call_user_func(function () use ($name): bool {
1302
            $relType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1303 1
            $errMsg  = "Relationship `$name` of class `" . $this->getModelClass() .
1304
                '` either is not `belongsToMany` or do not exist in the class.';
1305 1
            $isOk = $relType === RelationshipTypes::BELONGS_TO_MANY;
1306
1307
            assert($isOk, $errMsg);
1308
1309
            return $isOk;
1310
        }));
1311
1312
        $builderHook = Closure::fromCallable([$this, 'builderOnCreateInBelongsToManyRelationship']);
1313 1
1314
        return $this->addInToManyRelationship($this->getConnection(), $parentId, $name, $childIds, $builderHook);
1315
    }
1316
1317 1
    /**
1318 1
     * @inheritdoc
1319 1
     *
1320 1
     * @throws DBALException
1321
     */
1322 1
    public function removeInBelongsToManyRelationship(string $parentId, string $name, iterable $childIds): int
1323
    {
1324 1
        // Check that relationship is `BelongsToMany`
1325 1
        assert(call_user_func(function () use ($name): bool {
1326
            $relType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
1327 1
            $errMsg  = "Relationship `$name` of class `" . $this->getModelClass() .
1328
                '` either is not `belongsToMany` or do not exist in the class.';
1329
            $isOk = $relType === RelationshipTypes::BELONGS_TO_MANY;
1330
1331
            assert($isOk, $errMsg);
1332
1333 52
            return $isOk;
1334
        }));
1335 52
1336
        return $this->removeInToManyRelationship($this->getConnection(), $parentId, $name, $childIds);
1337
    }
1338
1339
    /**
1340
     * @return FactoryInterface
1341 53
     */
1342
    protected function getFactory(): FactoryInterface
1343 53
    {
1344
        return $this->factory;
1345
    }
1346
1347
    /**
1348
     * @return string
1349 53
     */
1350
    protected function getModelClass(): string
1351 53
    {
1352
        return $this->modelClass;
1353
    }
1354
1355
    /**
1356
     * @return ModelSchemaInfoInterface
1357 8
     */
1358
    protected function getModelSchemas(): ModelSchemaInfoInterface
1359 8
    {
1360
        return $this->modelSchemas;
1361
    }
1362
1363
    /**
1364
     * @return RelationshipPaginationStrategyInterface
1365
     */
1366
    protected function getRelationshipPagingStrategy(): RelationshipPaginationStrategyInterface
1367
    {
1368
        return $this->relPagingStrategy;
1369 10
    }
1370
1371 10
    /**
1372 10
     * @param Closure $closure
1373
     *
1374 10
     * @return void
0 ignored issues
show
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...
1375 8
     *
1376 10
     * @throws DBALException
1377
     */
1378
    public function inTransaction(Closure $closure): void
1379
    {
1380
        $connection = $this->getConnection();
1381
        $connection->beginTransaction();
1382
        try {
1383
            $isOk = ($closure() === false ? null : true);
1384
        } finally {
1385 22
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
1386
        }
1387 22
    }
1388
1389 22
    /**
1390 13
     * @inheritdoc
1391 13
     *
1392
     * @throws DBALException
1393
     */
1394 22
    public function fetchResources(QueryBuilder $builder, string $modelClass): PaginatedDataInterface
1395
    {
1396
        $data = $this->fetchPaginatedResourcesWithoutRelationships($builder, $modelClass);
1397
1398
        if ($this->hasIncludes() === true) {
1399
            $this->loadRelationships($data);
1400
            $this->clearFetchParameters();
1401
        }
1402 14
1403
        return $data;
1404 14
    }
1405
1406 14
    /**
1407 8
     * @inheritdoc
1408 8
     *
1409
     * @throws DBALException
1410
     */
1411 14
    public function fetchResource(QueryBuilder $builder, string $modelClass)
1412
    {
1413
        $data = $this->fetchResourceWithoutRelationships($builder, $modelClass);
1414
1415
        if ($this->hasIncludes() === true) {
1416
            $this->loadRelationships($data);
1417
            $this->clearFetchParameters();
1418
        }
1419
1420
        return $data;
1421 2
    }
1422
1423 2
    /**
1424
     * @inheritdoc
1425 2
     *
1426 2
     * @throws DBALException
1427
     *
1428 2
     * @SuppressWarnings(PHPMD.ElseExpression)
1429 2
     */
1430 1
    public function fetchRow(QueryBuilder $builder, string $modelClass): ?array
1431 1
    {
1432 1
        $model = null;
1433
1434 1
        $statement = $builder->execute();
1435
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1436
1437
        if (($attributes = $statement->fetch()) !== false) {
1438 2
            if ($this->isFetchTyped() === true) {
1439
                $platform  = $builder->getConnection()->getDatabasePlatform();
1440 2
                $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1441
                $model     = $this->readRowFromAssoc($attributes, $typeNames, $platform);
1442
            } else {
1443
                $model = $attributes;
1444
            }
1445
        }
1446
1447
        $this->clearFetchParameters();
1448
1449
        return $model;
1450
    }
1451 5
1452
    /**
1453 5
     * @inheritdoc
1454 5
     *
1455
     * @throws DBALException
1456 5
     *
1457 4
     * @SuppressWarnings(PHPMD.StaticAccess)
1458 4
     * @SuppressWarnings(PHPMD.ElseExpression)
1459 4
     */
1460 4
    public function fetchColumn(QueryBuilder $builder, string $modelClass, string $columnName): iterable
1461 4
    {
1462 4
        $statement = $builder->execute();
1463
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1464 4
1465
        if ($this->isFetchTyped() === true) {
1466
            $platform = $builder->getConnection()->getDatabasePlatform();
1467 1
            $typeName = $this->getModelSchemas()->getAttributeTypes($modelClass)[$columnName];
1468 1
            $type     = Type::getType($typeName);
1469
            while (($attributes = $statement->fetch()) !== false) {
1470 1
                $value     = $attributes[$columnName];
1471
                $converted = $type->convertToPHPValue($value, $platform);
1472
1473
                yield $converted;
1474 5
            }
1475
        } else {
1476
            while (($attributes = $statement->fetch()) !== false) {
1477
                $value = $attributes[$columnName];
1478
1479
                yield $value;
1480
            }
1481
        }
1482 2
1483
        $this->clearFetchParameters();
1484 2
    }
1485 2
1486 2
    /**
1487
     * @param QueryBuilder $builder
1488 2
     *
1489
     * @return ModelQueryBuilder
1490
     */
1491
    protected function createCountBuilderFromBuilder(QueryBuilder $builder): ModelQueryBuilder
1492
    {
1493
        $countBuilder = $this->createBuilder($this->getModelClass());
1494
        $countBuilder->setParameters($builder->getParameters());
1495
        $countBuilder->select('COUNT(*)')->from('(' . $builder->getSQL() . ') AS RESULT');
1496
1497
        return $countBuilder;
1498
    }
1499
1500
    /**
1501
     * @param Connection $connection
1502 5
     * @param string     $primaryIdentity
1503
     * @param string     $name
1504
     * @param iterable   $secondaryIdentities
1505
     * @param Closure    $builderHook
1506
     *
1507
     * @return int
1508
     *
1509 5
     * @throws DBALException
1510
     */
1511 5
    private function addInToManyRelationship(
1512
        Connection $connection,
1513 5
        string $primaryIdentity,
1514 5
        string $name,
1515
        iterable $secondaryIdentities,
1516 5
        Closure $builderHook
1517
    ): int {
1518 5
        $inserted = 0;
1519
1520 5
        $secondaryIdBindName = ':secondaryId';
1521 1
        $saveToMany          = $this
1522
            ->createBuilderFromConnection($connection, $this->getModelClass())
1523
            ->prepareCreateInToManyRelationship($name, $primaryIdentity, $secondaryIdBindName);
1524
1525
        $saveToMany = call_user_func($builderHook, $name, $saveToMany);
1526
1527 5
        foreach ($secondaryIdentities as $secondaryId) {
1528
            try {
1529
                $inserted += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1530
            } /** @noinspection PhpRedundantCatchClauseInspection */ catch (UcvException $exception) {
1531 5
                // Spec: If all of the specified resources can be added to, or are already present in,
1532
                // the relationship then the server MUST return a successful response.
1533
                //
1534
                // Currently DBAL cannot do insert or update in the same request.
1535
                // https://github.com/doctrine/dbal/issues/1320
1536
                continue;
1537
            }
1538
        }
1539
1540
        return $inserted;
1541
    }
1542
1543
    /**
1544 1
     * @param Connection $connection
1545
     * @param string     $primaryIdentity
1546
     * @param string     $name
1547
     * @param iterable   $secondaryIdentities
1548
     *
1549
     * @return int
1550 1
     *
1551 1
     * @throws DBALException
1552
     */
1553 1
    private function removeInToManyRelationship(
1554 1
        Connection $connection,
1555
        string $primaryIdentity,
1556 1
        string $name,
1557
        iterable $secondaryIdentities
1558 1
    ): int {
1559
        $removeToMany = $this->builderOnRemoveInBelongsToManyRelationship(
1560
            $name,
1561
            $this
1562
                ->createBuilderFromConnection($connection, $this->getModelClass())
1563
                ->prepareDeleteInToManyRelationship($name, $primaryIdentity, $secondaryIdentities)
1564
        );
1565
        $removed = $removeToMany->execute();
1566
1567
        return $removed;
1568
    }
1569
1570
    /**
1571 14
     * @param QueryBuilder $builder
1572
     * @param string       $modelClass
1573 14
     *
1574 14
     * @return mixed|null
1575
     *
1576 14
     * @throws DBALException
1577 12
     *
1578 12
     * @SuppressWarnings(PHPMD.ElseExpression)
1579 12
     */
1580 12
    private function fetchResourceWithoutRelationships(QueryBuilder $builder, string $modelClass)
1581 12
    {
1582
        $model     = null;
1583
        $statement = $builder->execute();
1584 2
1585 2
        if ($this->isFetchTyped() === true) {
1586 2
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1587
            if (($attributes = $statement->fetch()) !== false) {
1588
                $platform  = $builder->getConnection()->getDatabasePlatform();
1589
                $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1590 14
                $model     = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1591
            }
1592
        } else {
1593
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1594
            if (($fetched = $statement->fetch()) !== false) {
1595
                $model = $fetched;
1596
            }
1597
        }
1598
1599
        return $model;
1600
    }
1601
1602
    /**
1603
     * @param QueryBuilder $builder
1604 9
     * @param string       $modelClass
1605
     * @param string       $keyColumnName
1606
     *
1607
     * @return iterable
0 ignored issues
show
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...
1608
     *
1609 9
     * @throws DBALException
1610
     *
1611 9
     * @SuppressWarnings(PHPMD.ElseExpression)
1612 8
     */
1613 8
    private function fetchResourcesWithoutRelationships(
1614 8
        QueryBuilder $builder,
1615 8
        string $modelClass,
1616 6
        string $keyColumnName
1617 6
    ): iterable {
1618
        $statement = $builder->execute();
1619
1620 1
        if ($this->isFetchTyped() === true) {
1621 1
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1622 1
            $platform  = $builder->getConnection()->getDatabasePlatform();
1623
            $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1624
            while (($attributes = $statement->fetch()) !== false) {
1625
                $model = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1626
                yield $model->{$keyColumnName} => $model;
1627
            }
1628
        } else {
1629
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1630
            while (($model = $statement->fetch()) !== false) {
1631
                yield $model->{$keyColumnName} => $model;
1632
            }
1633
        }
1634
    }
1635 27
1636
    /**
1637
     * @param QueryBuilder $builder
1638
     * @param string       $modelClass
1639 27
     *
1640
     * @return PaginatedDataInterface
1641 27
     *
1642 27
     * @throws DBALException
1643 27
     */
1644 27
    private function fetchPaginatedResourcesWithoutRelationships(
1645 27
        QueryBuilder $builder,
1646
        string $modelClass
1647 27
    ): PaginatedDataInterface {
1648
        list($models, $hasMore, $limit, $offset) = $this->fetchResourceCollection($builder, $modelClass);
1649 27
1650
        $data = $this->getFactory()
1651
            ->createPaginatedData($models)
1652
            ->markAsCollection()
1653
            ->setOffset($offset)
1654
            ->setLimit($limit);
1655
1656
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
1657
1658
        return $data;
1659
    }
1660
1661
    /**
1662 27
     * @param QueryBuilder $builder
1663
     * @param string       $modelClass
1664 27
     *
1665
     * @return array
1666 27
     *
1667 27
     * @throws DBALException
1668 27
     *
1669 27
     * @SuppressWarnings(PHPMD.ElseExpression)
1670
     */
1671 27
    private function fetchResourceCollection(QueryBuilder $builder, string $modelClass): array
1672 26
    {
1673 26
        $statement = $builder->execute();
1674 26
1675 26
        $models           = [];
1676 25
        $counter          = 0;
1677 25
        $hasMoreThanLimit = false;
1678 6
        $limit            = $builder->getMaxResults() !== null ? $builder->getMaxResults() - 1 : null;
1679 6
1680
        if ($this->isFetchTyped() === true) {
1681 25
            $platform  = $builder->getConnection()->getDatabasePlatform();
1682
            $typeNames = $this->getModelSchemas()->getAttributeTypes($modelClass);
1683
            $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1684 1
            while (($attributes = $statement->fetch()) !== false) {
1685 1
                $counter++;
1686 1
                if ($limit !== null && $counter > $limit) {
1687 1
                    $hasMoreThanLimit = true;
1688 1
                    break;
1689 1
                }
1690
                $models[] = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1691 1
            }
1692
        } else {
1693
            $statement->setFetchMode(PDOConnection::FETCH_CLASS, $modelClass);
1694
            while (($fetched = $statement->fetch()) !== false) {
1695 27
                $counter++;
1696
                if ($limit !== null && $counter > $limit) {
1697
                    $hasMoreThanLimit = true;
1698
                    break;
1699
                }
1700
                $models[] = $fetched;
1701
            }
1702
        }
1703
1704 5
        return [$models, $hasMoreThanLimit, $limit, $builder->getFirstResult()];
1705
    }
1706 5
1707 1
    /**
1708 1
     * @param null|string $index
1709
     * @param iterable    $attributes
1710
     *
1711 5
     * @return iterable
0 ignored issues
show
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...
1712 5
     */
1713 5
    protected function filterAttributesOnCreate(?string $index, iterable $attributes): iterable
1714 5
    {
1715
        if ($index !== null) {
1716
            $pkName = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
1717
            yield $pkName => $index;
1718
        }
1719
1720
        $knownAttrAndTypes = $this->getModelSchemas()->getAttributeTypes($this->getModelClass());
1721
        foreach ($attributes as $attribute => $value) {
1722
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1723
                yield $attribute => $value;
1724 5
            }
1725
        }
1726 5
    }
1727 5
1728 5
    /**
1729 5
     * @param iterable $attributes
1730
     *
1731
     * @return iterable
0 ignored issues
show
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...
1732
     */
1733
    protected function filterAttributesOnUpdate(iterable $attributes): iterable
1734
    {
1735
        $knownAttrAndTypes = $this->getModelSchemas()->getAttributeTypes($this->getModelClass());
1736
        foreach ($attributes as $attribute => $value) {
1737
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1738
                yield $attribute => $value;
1739
            }
1740
        }
1741
    }
1742
1743
    /**
1744
     * @param TagStorageInterface   $modelsAtPath
1745
     * @param ArrayObject           $classAtPath
1746
     * @param ArrayObject           $idsAtPath
1747
     * @param ModelStorageInterface $deDup
1748
     * @param string                $parentsPath
1749 10
     * @param array                 $childRelationships
1750
     *
1751
     * @return void
1752
     *
1753
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1754
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
1755
     *
1756
     * @throws DBALException
1757 10
     */
1758 10
    private function loadRelationshipsLayer(
1759 10
        TagStorageInterface $modelsAtPath,
1760
        ArrayObject $classAtPath,
1761
        ArrayObject $idsAtPath,
1762
        ModelStorageInterface $deDup,
1763
        string $parentsPath,
1764
        array $childRelationships
1765 10
    ): void {
1766
        $rootClass   = $classAtPath[static::ROOT_PATH];
1767
        $parentClass = $classAtPath[$parentsPath];
1768 8
        $parents     = $modelsAtPath->get($parentsPath);
1769 8
1770 8
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
1771 8
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
1772 8
        // $models.
1773 8
1774 8
        $pkName = $this->getModelSchemas()->getPrimaryKey($parentClass);
1775
1776 10
        $registerModelAtPath = function ($model, string $path) use ($deDup, $modelsAtPath, $idsAtPath) {
1777
            return self::registerModelAtPath(
1778 10
                $model,
1779 10
                $path,
1780
                $this->getModelSchemas(),
1781 10
                $deDup,
1782
                $modelsAtPath,
1783 10
                $idsAtPath
1784
            );
1785
        };
1786 10
1787 10
        foreach ($childRelationships as $name) {
1788 10
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
1789
1790 10
            $relationshipType = $this->getModelSchemas()->getRelationshipType($parentClass, $name);
1791
            list ($targetModelClass, $reverseRelName) =
1792
                $this->getModelSchemas()->getReverseRelationship($parentClass, $name);
1793 10
1794
            $builder = $this
1795 9
                ->createBuilder($targetModelClass)
1796 9
                ->selectModelColumns()
1797 1
                ->fromModelTable();
1798
1799
            $classAtPath[$childrenPath] = $targetModelClass;
1800 9
1801 9
            switch ($relationshipType) {
1802 9
                case RelationshipTypes::BELONGS_TO:
1803 9
                    // some paths might not have any records in the database
1804 9
                    $areParentsLoaded = $idsAtPath->offsetExists($parentsPath);
1805
                    if ($areParentsLoaded === false) {
1806 9
                        break;
1807 9
                    }
1808 9
                    // for 'belongsTo' relationship all resources could be read at once.
1809 9
                    $parentIds            = $idsAtPath[$parentsPath];
1810
                    $clonedBuilder        = (clone $builder)->addRelationshipFiltersAndSorts(
1811 9
                        $reverseRelName,
1812 9
                        [$pkName => [FilterParameterInterface::OPERATION_IN => $parentIds]],
0 ignored issues
show
array($pkName => array(\...TION_IN => $parentIds)) is of type array<string,array>, but the function expects a object<Limoncello\Flute\Adapters\iterable>|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1813 7
                        null
1814
                    );
1815 9
                    $unregisteredChildren = $this->fetchResourcesWithoutRelationships(
1816 9
                        $clonedBuilder,
1817 9
                        $clonedBuilder->getModelClass(),
1818 9
                        $this->getModelSchemas()->getPrimaryKey($clonedBuilder->getModelClass())
1819
                    );
1820 9
                    $children             = [];
1821 8
                    foreach ($unregisteredChildren as $index => $unregisteredChild) {
1822 6
                        $children[$index] = $registerModelAtPath($unregisteredChild, $childrenPath);
1823
                    }
1824
                    $fkNameToChild = $this->getModelSchemas()->getForeignKey($parentClass, $name);
1825 8
                    foreach ($parents as $parent) {
1826 8
                        $fkToChild       = $parent->{$fkNameToChild};
1827 8
                        $parent->{$name} = $children[$fkToChild] ?? null;
1828 8
                    }
1829 8
                    break;
1830 8
                case RelationshipTypes::HAS_MANY:
1831 8
                case RelationshipTypes::BELONGS_TO_MANY:
1832 8
                    // unfortunately we have paging limits for 'many' relationship thus we have read such
1833
                    // relationships for each 'parent' individually
1834 8
                    list ($queryOffset, $queryLimit) = $this->getRelationshipPagingStrategy()
1835 8
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
1836 8
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
1837
                    // pagination requires predictable data order from the database so we are adding sorting by PK asc
1838
                    $targetPkName = $this->getModelSchemas()->getPrimaryKey($targetModelClass);
1839 8
                    $builder->addSorts([$targetPkName => true]);
0 ignored issues
show
array($targetPkName => true) is of type array<string,boolean>, 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...
1840 8
                    foreach ($parents as $parent) {
1841 7
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSorts(
1842
                            $reverseRelName,
1843
                            [$pkName => [FilterParameterInterface::OPERATION_EQUALS => [$parent->{$pkName}]]],
0 ignored issues
show
array($pkName => array(\...y($parent->{$pkName}))) is of type array<string,array<strin...<integer,?,{"0":"?"}>>>, but the function expects a object<Limoncello\Flute\Adapters\iterable>|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1844 8
                            []
0 ignored issues
show
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...
1845 8
                        );
1846 8
                        $children      = $this->fetchPaginatedResourcesWithoutRelationships(
1847 8
                            $clonedBuilder,
1848 8
                            $clonedBuilder->getModelClass()
1849 8
                        );
1850 8
1851
                        $deDupedChildren = [];
1852 8
                        foreach ($children->getData() as $child) {
1853
                            $deDupedChildren[] = $registerModelAtPath($child, $childrenPath);
1854 10
                        }
1855
1856
                        $paginated = $this->getFactory()
1857
                            ->createPaginatedData($deDupedChildren)
1858
                            ->markAsCollection()
1859
                            ->setOffset($children->getOffset())
1860
                            ->setLimit($children->getLimit());
1861
                        $children->hasMoreItems() === true ?
1862
                            $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
1863
1864
                        $parent->{$name} = $paginated;
1865
                    }
1866
                    break;
1867 2
            }
1868
        }
1869
    }
1870 2
1871 2
    /**
1872 2
     * @param string $message
1873
     *
1874 2
     * @return string
1875
     *
1876
     * @throws ContainerExceptionInterface
1877
     * @throws NotFoundExceptionInterface
1878
     */
1879
    private function getMessage(string $message): string
1880
    {
1881
        /** @var FormatterFactoryInterface $factory */
1882
        $factory   = $this->getContainer()->get(FormatterFactoryInterface::class);
1883
        $formatter = $factory->createFormatter(Messages::NAMESPACE_NAME);
1884
        $result    = $formatter->formatMessage($message);
1885
1886
        return $result;
1887
    }
1888
1889 34
    /**
1890
     * @param string           $class
1891
     * @param array            $attributes
1892
     * @param Type[]           $typeNames
1893
     * @param AbstractPlatform $platform
1894
     *
1895 34
     * @return mixed
0 ignored issues
show
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...
1896 34
     *
1897 34
     * @SuppressWarnings(PHPMD.StaticAccess)
1898
     *
1899
     * @throws DBALException
1900 34
     */
1901
    private function readResourceFromAssoc(
1902
        string $class,
1903
        array $attributes,
1904
        array $typeNames,
1905
        AbstractPlatform $platform
1906
    ) {
1907
        $instance = new $class();
1908
        foreach ($this->readTypedAttributes($attributes, $typeNames, $platform) as $name => $value) {
0 ignored issues
show
$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...
1909
            $instance->{$name} = $value;
1910
        }
1911
1912
        return $instance;
1913
    }
1914 1
1915
    /**
1916 1
     * @param array            $attributes
1917 1
     * @param Type[]           $typeNames
1918 1
     * @param AbstractPlatform $platform
1919
     *
1920
     * @return array
1921 1
     *
1922
     * @SuppressWarnings(PHPMD.StaticAccess)
1923
     *
1924
     * @throws DBALException
1925
     */
1926
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
1927
    {
1928
        $row = [];
1929
        foreach ($this->readTypedAttributes($attributes, $typeNames, $platform) as $name => $value) {
0 ignored issues
show
$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...
1930
            $row[$name] = $value;
1931
        }
1932
1933 35
        return $row;
1934
    }
1935 35
1936 35
    /**
1937 35
     * @param iterable         $attributes
1938
     * @param array            $typeNames
1939
     * @param AbstractPlatform $platform
1940
     *
1941
     * @return iterable
0 ignored issues
show
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...
1942
     *
1943
     * @throws DBALException
1944
     */
1945
    private function readTypedAttributes(iterable $attributes, array $typeNames, AbstractPlatform $platform): iterable
1946
    {
1947
        foreach ($attributes as $name => $value) {
1948
            yield $name => (array_key_exists($name, $typeNames) === true ?
1949
                Type::getType($typeNames[$name])->convertToPHPValue($value, $platform) : $value);
1950
        }
1951
    }
1952
}
1953