Completed
Push — develop ( 0fd0fb...9d9666 )
by Neomerx
02:16
created

Crud::createIndexModelBuilder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 11
cts 11
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 13
nc 1
nop 1
crap 1
1
<?php namespace Limoncello\Flute\Api;
2
3
/**
4
 * Copyright 2015-2017 [email protected]
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use ArrayObject;
20
use Closure;
21
use Doctrine\DBAL\Connection;
22
use Doctrine\DBAL\Driver\PDOConnection;
23
use Doctrine\DBAL\Platforms\AbstractPlatform;
24
use Doctrine\DBAL\Query\QueryBuilder;
25
use Doctrine\DBAL\Types\Type;
26
use Generator;
27
use Limoncello\Container\Traits\HasContainerTrait;
28
use Limoncello\Contracts\Data\ModelSchemeInfoInterface;
29
use Limoncello\Contracts\Data\RelationshipTypes;
30
use Limoncello\Contracts\L10n\FormatterFactoryInterface;
31
use Limoncello\Flute\Adapters\ModelQueryBuilder;
32
use Limoncello\Flute\Contracts\Adapters\PaginationStrategyInterface;
33
use Limoncello\Flute\Contracts\Api\CrudInterface;
34
use Limoncello\Flute\Contracts\FactoryInterface;
35
use Limoncello\Flute\Contracts\Http\Query\FilterParameterInterface;
36
use Limoncello\Flute\Contracts\Models\ModelStorageInterface;
37
use Limoncello\Flute\Contracts\Models\PaginatedDataInterface;
38
use Limoncello\Flute\Contracts\Models\TagStorageInterface;
39
use Limoncello\Flute\Exceptions\InvalidArgumentException;
40
use Limoncello\Flute\L10n\Messages;
41
use Neomerx\JsonApi\Contracts\Document\DocumentInterface;
42
use Psr\Container\ContainerInterface;
43
use Traversable;
44
45
/**
46
 * @package Limoncello\Flute
47
 *
48
 * @SuppressWarnings(PHPMD.TooManyMethods)
49
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
50
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
51
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
52
 */
53
class Crud implements CrudInterface
54
{
55
    use HasContainerTrait;
56
57
    /** Internal constant. Path constant. */
58
    protected const ROOT_PATH = '';
59
60
    /** Internal constant. Path constant. */
61
    protected const PATH_SEPARATOR = DocumentInterface::PATH_SEPARATOR;
62
63
    /**
64
     * @var FactoryInterface
65
     */
66
    private $factory;
67
68
    /**
69
     * @var string
70
     */
71
    private $modelClass;
72
73
    /**
74
     * @var ModelSchemeInfoInterface
75
     */
76
    private $modelSchemes;
77
78
    /**
79
     * @var PaginationStrategyInterface
80
     */
81
    private $paginationStrategy;
82
83
    /**
84
     * @var Connection
85
     */
86
    private $connection;
87
88
    /**
89
     * @var iterable|null
90
     */
91
    private $filterParameters = null;
92
93
    /**
94
     * @var bool
95
     */
96
    private $areFiltersWithAnd = true;
97
98
    /**
99
     * @var iterable|null
100
     */
101
    private $sortingParameters = null;
102
103
    /**
104
     * @var array
105
     */
106
    private $relFiltersAndSorts = [];
107
108
    /**
109
     * @var iterable|null
110
     */
111
    private $includePaths = null;
112
113
    /**
114
     * @var int|null
115
     */
116
    private $pagingOffset = null;
117
118
    /**
119
     * @var int|null
120
     */
121
    private $pagingLimit = null;
122
123
    /** internal constant */
124
    private const REL_FILTERS_AND_SORTS__FILTERS = 0;
125
126
    /** internal constant */
127
    private const REL_FILTERS_AND_SORTS__SORTS = 1;
128
129
    /**
130
     * @param ContainerInterface $container
131
     * @param string             $modelClass
132
     */
133 46
    public function __construct(ContainerInterface $container, string $modelClass)
134
    {
135 46
        $this->setContainer($container);
136
137 46
        $this->modelClass         = $modelClass;
138 46
        $this->factory            = $this->getContainer()->get(FactoryInterface::class);
139 46
        $this->modelSchemes       = $this->getContainer()->get(ModelSchemeInfoInterface::class);
140 46
        $this->paginationStrategy = $this->getContainer()->get(PaginationStrategyInterface::class);
141 46
        $this->connection         = $this->getContainer()->get(Connection::class);
142
143 46
        $this->clearBuilderParameters()->clearFetchParameters();
144
    }
145
146
    /**
147
     * @inheritdoc
148
     */
149 38
    public function withFilters(iterable $filterParameters): CrudInterface
150
    {
151 38
        $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...
152
153 38
        return $this;
154
    }
155
156
    /**
157
     * @inheritdoc
158
     */
159 20
    public function withIndexFilter($index): CrudInterface
160
    {
161 20
        if (is_int($index) === false && is_string($index) === false) {
162 3
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
163
        }
164
165 17
        $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
166 17
        $this->withFilters([
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...UALS => array($index))) is of type array<string,array<strin...0":"integer|string"}>>>, but the function expects a object<Limoncello\Flute\Contracts\Api\iterable>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
167
            $pkName => [
168 17
                FilterParameterInterface::OPERATION_EQUALS => [$index],
169
            ],
170
        ]);
171
172 17
        return $this;
173
    }
174
175
    /**
176
     * @inheritdoc
177
     */
178 4
    public function withRelationshipFilters(string $name, iterable $filters): CrudInterface
179
    {
180 4
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__FILTERS] = $filters;
181
182 4
        return $this;
183
    }
184
185
    /**
186
     * @inheritdoc
187
     */
188 1
    public function withRelationshipSorts(string $name, iterable $sorts): CrudInterface
189
    {
190 1
        $this->relFiltersAndSorts[$name][self::REL_FILTERS_AND_SORTS__SORTS] = $sorts;
191
192 1
        return $this;
193
    }
194
195
    /**
196
     * @inheritdoc
197
     */
198 15
    public function combineWithAnd(): CrudInterface
199
    {
200 15
        $this->areFiltersWithAnd = true;
201
202 15
        return $this;
203
    }
204
205
    /**
206
     * @inheritdoc
207
     */
208 1
    public function combineWithOr(): CrudInterface
209
    {
210 1
        $this->areFiltersWithAnd = false;
211
212 1
        return $this;
213
    }
214
215
    /**
216
     * @return bool
217
     */
218 39
    private function hasFilters(): bool
219
    {
220 39
        return empty($this->filterParameters) === false;
221
    }
222
223
    /**
224
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be iterable|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
225
     */
226 30
    private function getFilters(): iterable
227
    {
228 30
        return $this->filterParameters;
229
    }
230
231
    /**
232
     * @return bool
233
     */
234 30
    private function areFiltersWithAnd(): bool
235
    {
236 30
        return $this->areFiltersWithAnd;
237
    }
238
239
    /**
240
     * @inheritdoc
241
     */
242 17
    public function withSorts(iterable $sortingParameters): CrudInterface
243
    {
244 17
        $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...
245
246 17
        return $this;
247
    }
248
249
    /**
250
     * @return bool
251
     */
252 28
    private function hasSorts(): bool
253
    {
254 28
        return empty($this->sortingParameters) === false;
255
    }
256
257
    /**
258
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be iterable|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
259
     */
260 12
    private function getSorts(): ?iterable
261
    {
262 12
        return $this->sortingParameters;
263
    }
264
265
    /**
266
     * @inheritdoc
267
     */
268 20
    public function withIncludes(iterable $includePaths): CrudInterface
269
    {
270 20
        $this->includePaths = $includePaths;
0 ignored issues
show
Documentation Bug introduced by
It seems like $includePaths of type object<Limoncello\Flute\Contracts\Api\iterable> is incompatible with the declared type object<Limoncello\Flute\Api\iterable>|null of property $includePaths.

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

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

Loading history...
271
272 20
        return $this;
273
    }
274
275
    /**
276
     * @return bool
277
     */
278 33
    private function hasIncludes(): bool
279
    {
280 33
        return empty($this->includePaths) === false;
281
    }
282
283
    /**
284
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be iterable|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
285
     */
286 20
    private function getIncludes(): iterable
287
    {
288 20
        return $this->includePaths;
289
    }
290
291
    /**
292
     * @inheritdoc
293
     */
294 19
    public function withPaging(int $offset, int $limit): CrudInterface
295
    {
296 19
        $this->pagingOffset = $offset;
297 19
        $this->pagingLimit  = $limit;
298
299 19
        return $this;
300
    }
301
302
    /**
303
     * @return bool
304
     */
305 36
    private function hasPaging(): bool
306
    {
307 36
        return $this->pagingOffset !== null && $this->pagingLimit !== null;
308
    }
309
310
    /**
311
     * @return int
312
     */
313 19
    private function getPagingOffset(): int
314
    {
315 19
        return $this->pagingOffset;
316
    }
317
318
    /**
319
     * @return int
320
     */
321 19
    private function getPagingLimit(): int
322
    {
323 19
        return $this->pagingLimit;
324
    }
325
326
    /**
327
     * @return Connection
328
     */
329 39
    protected function getConnection(): Connection
330
    {
331 39
        return $this->connection;
332
    }
333
334
    /**
335
     * @param string $modelClass
336
     *
337
     * @return ModelQueryBuilder
338
     */
339 39
    protected function createBuilder(string $modelClass): ModelQueryBuilder
340
    {
341 39
        return $this->createBuilderFromConnection($this->getConnection(), $modelClass);
342
    }
343
344
    /**
345
     * @param Connection $connection
346
     * @param string     $modelClass
347
     *
348
     * @return ModelQueryBuilder
349
     */
350 39
    private function createBuilderFromConnection(Connection $connection, string $modelClass): ModelQueryBuilder
351
    {
352 39
        return $this->getFactory()->createModelQueryBuilder($connection, $modelClass, $this->getModelSchemes());
353
    }
354
355
    /**
356
     * @param ModelQueryBuilder $builder
357
     *
358
     * @return Crud
359
     */
360 29
    private function applyAliasFilters(ModelQueryBuilder $builder): self
361
    {
362 29
        if ($this->hasFilters() === true) {
363 20
            $filters = $this->getFilters();
364 20
            $this->areFiltersWithAnd() === true ?
365 20
                $builder->addFiltersWithAndToAlias($filters) : $builder->addFiltersWithOrToAlias($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 363 can also be of type null; however, Limoncello\Flute\Adapter...FiltersWithAndToAlias() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 363 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...
366
        }
367
368 29
        return $this;
369
    }
370
371
    /**
372
     * @param ModelQueryBuilder $builder
373
     *
374
     * @return self
375
     */
376 5
    private function applyTableFilters(ModelQueryBuilder $builder): self
377
    {
378 5
        if ($this->hasFilters() === true) {
379 5
            $filters = $this->getFilters();
380 5
            $this->areFiltersWithAnd() === true ?
381 5
                $builder->addFiltersWithAndToTable($filters) : $builder->addFiltersWithOrToTable($filters);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 379 can also be of type null; however, Limoncello\Flute\Adapter...FiltersWithAndToTable() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 379 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...
382
        }
383
384 5
        return $this;
385
    }
386
387
    /**
388
     * @param ModelQueryBuilder $builder
389
     *
390
     * @return self
391
     */
392 28
    private function applyRelationshipFiltersAndSorts(ModelQueryBuilder $builder): self
393
    {
394
        // While joining tables we select distinct rows. This flag used to apply `distinct` no more than once.
395 28
        $distinctApplied = false;
396
397 28
        foreach ($this->relFiltersAndSorts as $relationshipName => $filtersAndSorts) {
398 4
            assert(is_string($relationshipName) === true && is_array($filtersAndSorts) === true);
399 4
            $builder->addRelationshipFiltersAndSortsWithAnd(
400 4
                $relationshipName,
401 4
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__FILTERS] ?? [],
402 4
                $filtersAndSorts[self::REL_FILTERS_AND_SORTS__SORTS] ?? []
403
            );
404
405 4
            if ($distinctApplied === false) {
406 4
                $builder->distinct();
407 4
                $distinctApplied = true;
408
            }
409
        }
410
411 28
        return $this;
412
    }
413
414
    /**
415
     * @param ModelQueryBuilder $builder
416
     *
417
     * @return self
418
     */
419 28
    private function applySorts(ModelQueryBuilder $builder): self
420
    {
421 28
        if ($this->hasSorts() === true) {
422 3
            $builder->addSorts($this->getSorts());
0 ignored issues
show
Bug introduced by
It seems like $this->getSorts() targeting Limoncello\Flute\Api\Crud::getSorts() can also be of type null; however, Limoncello\Flute\Adapter...ueryBuilder::addSorts() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
423
        }
424
425 28
        return $this;
426
    }
427
428
    /**
429
     * @param ModelQueryBuilder $builder
430
     *
431
     * @return self
432
     */
433 36
    private function applyPaging(ModelQueryBuilder $builder): self
434
    {
435 36
        if ($this->hasPaging() === true) {
436 19
            $builder->setFirstResult($this->getPagingOffset());
437 19
            $builder->setMaxResults($this->getPagingLimit() + 1);
438
        }
439
440 36
        return $this;
441
    }
442
443
    /**
444
     * @return self
445
     */
446 46
    private function clearBuilderParameters(): self
447
    {
448 46
        $this->filterParameters   = null;
449 46
        $this->areFiltersWithAnd  = true;
450 46
        $this->sortingParameters  = null;
451 46
        $this->pagingOffset       = null;
452 46
        $this->pagingLimit        = null;
453 46
        $this->relFiltersAndSorts = [];
454
455 46
        return $this;
456
    }
457
458
    /**
459
     * @return self
460
     */
461 46
    private function clearFetchParameters(): self
462
    {
463 46
        $this->includePaths = null;
464
465 46
        return $this;
466
    }
467
468
    /**
469
     * @param ModelQueryBuilder $builder
470
     *
471
     * @return ModelQueryBuilder
472
     */
473 1
    protected function builderOnCount(ModelQueryBuilder $builder): ModelQueryBuilder
474
    {
475 1
        return $builder;
476
    }
477
478
    /**
479
     * @param ModelQueryBuilder $builder
480
     *
481
     * @return ModelQueryBuilder
482
     */
483 28
    protected function builderOnIndex(ModelQueryBuilder $builder): ModelQueryBuilder
484
    {
485 28
        return $builder;
486
    }
487
488
    /**
489
     * @param ModelQueryBuilder $builder
490
     *
491
     * @return ModelQueryBuilder
492
     */
493 9
    protected function builderOnReadRelationship(ModelQueryBuilder $builder): ModelQueryBuilder
494
    {
495 9
        return $builder;
496
    }
497
498
    /**
499
     * @param ModelQueryBuilder $builder
500
     *
501
     * @return ModelQueryBuilder
502
     */
503 4
    protected function builderSaveResourceOnCreate(ModelQueryBuilder $builder): ModelQueryBuilder
504
    {
505 4
        return $builder;
506
    }
507
508
    /**
509
     * @param ModelQueryBuilder $builder
510
     *
511
     * @return ModelQueryBuilder
512
     */
513 3
    protected function builderSaveResourceOnUpdate(ModelQueryBuilder $builder): ModelQueryBuilder
514
    {
515 3
        return $builder;
516
    }
517
518
    /**
519
     * @param string            $relationshipName
520
     * @param ModelQueryBuilder $builder
521
     *
522
     * @return ModelQueryBuilder
523
     *
524
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
525
     */
526 2
    protected function builderSaveRelationshipOnCreate(/** @noinspection PhpUnusedParameterInspection */
527
        $relationshipName,
0 ignored issues
show
Unused Code introduced by
The parameter $relationshipName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
528
        ModelQueryBuilder $builder
529
    ): ModelQueryBuilder {
530 2
        return $builder;
531
    }
532
533
    /**
534
     * @param string            $relationshipName
535
     * @param ModelQueryBuilder $builder
536
     *
537
     * @return ModelQueryBuilder
538
     *
539
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
540
     */
541 2
    protected function builderSaveRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
542
        $relationshipName,
0 ignored issues
show
Unused Code introduced by
The parameter $relationshipName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
543
        ModelQueryBuilder $builder
544
    ): ModelQueryBuilder {
545 2
        return $builder;
546
    }
547
548
    /**
549
     * @param string            $relationshipName
550
     * @param ModelQueryBuilder $builder
551
     *
552
     * @return ModelQueryBuilder
553
     *
554
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
555
     */
556 2
    protected function builderCleanRelationshipOnUpdate(/** @noinspection PhpUnusedParameterInspection */
557
        $relationshipName,
0 ignored issues
show
Unused Code introduced by
The parameter $relationshipName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
558
        ModelQueryBuilder $builder
559
    ): ModelQueryBuilder {
560 2
        return $builder;
561
    }
562
563
    /**
564
     * @param ModelQueryBuilder $builder
565
     *
566
     * @return ModelQueryBuilder
567
     */
568 5
    protected function builderOnDelete(ModelQueryBuilder $builder): ModelQueryBuilder
569
    {
570 5
        return $builder;
571
    }
572
573
    /**
574
     * @param PaginatedDataInterface $data
575
     *
576
     * @return void
577
     *
578
     * @SuppressWarnings(PHPMD.ElseExpression)
579
     */
580 20
    private function loadRelationships(PaginatedDataInterface $data): void
581
    {
582 20
        if (empty($data->getData()) === false && $this->hasIncludes() === true) {
583 20
            $modelStorage = $this->getFactory()->createModelStorage($this->getModelSchemes());
584 20
            $modelsAtPath = $this->getFactory()->createTagStorage();
585
586
            // we gonna send this storage via function params so it is an equivalent for &array
587 20
            $classAtPath = new ArrayObject();
588
589 20
            $model = null;
590 20
            if ($data->isCollection() === true) {
591 14
                foreach ($data->getData() as $model) {
592 14
                    $uniqueModel = $modelStorage->register($model);
593 14
                    if ($uniqueModel !== null) {
594 14
                        $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
595
                    }
596
                }
597
            } else {
598 6
                $model       = $data->getData();
599 6
                $uniqueModel = $modelStorage->register($model);
600 6
                if ($uniqueModel !== null) {
601 6
                    $modelsAtPath->register($uniqueModel, static::ROOT_PATH);
602
                }
603
            }
604 20
            $classAtPath[static::ROOT_PATH] = get_class($model);
605
606 20
            foreach ($this->getPaths($this->getIncludes()) as list ($parentPath, $childPaths)) {
0 ignored issues
show
Bug introduced by
It seems like $this->getIncludes() can be null; however, getPaths() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
607 8
                $this->loadRelationshipsLayer(
608 8
                    $modelsAtPath,
609 8
                    $classAtPath,
610 8
                    $modelStorage,
611 8
                    $parentPath,
612 8
                    $childPaths
613
                );
614
            }
615
        }
616
    }
617
618
    /**
619
     * @param iterable $paths (string[])
620
     *
621
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
622
     */
623 20
    private static function getPaths(iterable $paths): iterable
624
    {
625
        // The idea is to normalize paths. It means build all intermediate paths.
626
        // e.g. if only `a.b.c` path it given it will be normalized to `a`, `a.b` and `a.b.c`.
627
        // Path depths store depth of each path (e.g. 0 for root, 1 for `a`, 2 for `a.b` and etc).
628
        // It is needed for yielding them in correct order (from top level to bottom).
629 20
        $normalizedPaths = [];
630 20
        $pathsDepths     = [];
631 20
        foreach ($paths as $path) {
632 8
            assert(is_array($path) || $path instanceof Traversable);
633 8
            $parentDepth = 0;
634 8
            $tmpPath     = static::ROOT_PATH;
635 8
            foreach ($path as $pathPiece) {
636 8
                assert(is_string($pathPiece));
637 8
                $parent                    = $tmpPath;
638 8
                $tmpPath                   = empty($tmpPath) === true ?
639 8
                    $pathPiece : $tmpPath . static::PATH_SEPARATOR . $pathPiece;
640 8
                $normalizedPaths[$tmpPath] = [$parent, $pathPiece];
641 8
                $pathsDepths[$parent]      = $parentDepth++;
642
            }
643
        }
644
645
        // Here we collect paths in form of parent => [list of children]
646
        // e.g. '' => ['a', 'c', 'b'], 'b' => ['bb', 'aa'] and etc
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
647 20
        $parentWithChildren = [];
648 20
        foreach ($normalizedPaths as $path => list ($parent, $childPath)) {
649 8
            $parentWithChildren[$parent][] = $childPath;
650
        }
651
652
        // And finally sort by path depth and yield parent with its children. Top level paths first then deeper ones.
653 20
        asort($pathsDepths, SORT_NUMERIC);
654 20
        foreach ($pathsDepths as $parent => $depth) {
655 8
            assert($depth !== null); // suppress unused
656 8
            $childPaths = $parentWithChildren[$parent];
657 8
            yield [$parent, $childPaths];
658
        }
659
    }
660
661
    /**
662
     * @inheritdoc
663
     */
664 1
    public function createIndexBuilder(iterable $columns = null): QueryBuilder
665
    {
666 1
        return $this->createIndexModelBuilder($columns);
667
    }
668
669
    /**
670
     * @inheritdoc
671
     */
672
    public function createDeleteBuilder(): QueryBuilder
673
    {
674
        return $this->createDeleteModelBuilder();
675
    }
676
677
    /**
678
     * @param iterable|null $columns
679
     *
680
     * @return ModelQueryBuilder
681
     */
682 28
    protected function createIndexModelBuilder(iterable $columns = null): ModelQueryBuilder
683
    {
684
        $builder = $this
685 28
            ->createBuilder($this->getModelClass())
686 28
            ->selectModelColumns($columns)
687 28
            ->fromModelTable();
688
689
        $this
690 28
            ->applyAliasFilters($builder)
691 28
            ->applySorts($builder)
692 28
            ->applyRelationshipFiltersAndSorts($builder)
693 28
            ->applyPaging($builder);
694
695 28
        $result = $this->builderOnIndex($builder);
696
697 28
        $this->clearBuilderParameters();
698
699 28
        return $result;
700
    }
701
702
    /**
703
     * @return ModelQueryBuilder
704
     */
705 5
    protected function createDeleteModelBuilder(): ModelQueryBuilder
706
    {
707
        $builder = $this
708 5
            ->createBuilder($this->getModelClass())
709 5
            ->deleteModels();
710
711 5
        $this->applyTableFilters($builder);
712
713 5
        $result = $this->builderOnDelete($builder);
714
715 5
        $this->clearBuilderParameters();
716
717 5
        return $result;
718
    }
719
720
    /**
721
     * @inheritdoc
722
     */
723 16
    public function index(): PaginatedDataInterface
724
    {
725 16
        $builder = $this->createIndexModelBuilder();
726 16
        $data    = $this->fetchResources($builder, $builder->getModelClass());
727
728 16
        return $data;
729
    }
730
731
    /**
732
     * @inheritdoc
733
     */
734 1
    public function indexIdentities(): array
735
    {
736 1
        $pkName  = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
737 1
        $builder = $this->createIndexModelBuilder([$pkName]);
0 ignored issues
show
Documentation introduced by
array($pkName) is of type array<integer,string,{"0":"string"}>, but the function expects a object<Limoncello\Flute\Api\iterable>|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
738
        /** @var Generator $data */
739 1
        $data   = $this->fetchColumn($builder, $builder->getModelClass(), $pkName);
740 1
        $result = iterator_to_array($data);
741
742 1
        return $result;
743
    }
744
745
    /**
746
     * @inheritdoc
747
     */
748 12
    public function read($index): PaginatedDataInterface
749
    {
750 12
        $this->withIndexFilter($index);
751
752 10
        $builder = $this->createIndexModelBuilder();
753 10
        $data    = $this->fetchResource($builder, $builder->getModelClass());
754
755 10
        return $data;
756
    }
757
758
    /**
759
     * @inheritdoc
760
     */
761 1
    public function count(): ?int
762
    {
763
        $builder = $this
764 1
            ->createBuilder($this->getModelClass())
765 1
            ->select('COUNT(*)')
766 1
            ->fromModelTable();
767
768 1
        $this->applyAliasFilters($builder);
769
770 1
        $this->clearBuilderParameters()->clearFetchParameters();
771
772 1
        $result = $this->builderOnCount($builder)->execute()->fetchColumn();
773
774 1
        return $result === false ? null : $result;
775
    }
776
777
    /**
778
     * @param string        $relationshipName
779
     * @param iterable|null $relationshipFilters
780
     * @param iterable|null $relationshipSorts
781
     * @param iterable|null $columns
782
     *
783
     * @return ModelQueryBuilder
784
     */
785 9
    public function createReadRelationshipBuilder(
786
        string $relationshipName,
787
        iterable $relationshipFilters = null,
788
        iterable $relationshipSorts = null,
789
        iterable $columns = null
790
    ): ModelQueryBuilder {
791
        // as we read data from a relationship our main table and model would be the table/model in the relationship
792
        // so 'root' model(s) will be located in the reverse relationship.
793
794
        list ($targetModelClass, $reverseRelName) =
795 9
            $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $relationshipName);
796
797
        $builder = $this
798 9
            ->createBuilder($targetModelClass)
799 9
            ->selectModelColumns($columns)
800 9
            ->fromModelTable();
801
802
        // 'root' filters would be applied to the data in the reverse relationship ...
803 9
        if ($this->hasFilters() === true) {
804 9
            $filters = $this->getFilters();
805 9
            $sorts   = $this->getSorts();
806 9
            $this->areFiltersWithAnd() ?
807 9
                $builder->addRelationshipFiltersAndSortsWithAnd($reverseRelName, $filters, $sorts) :
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 804 can also be of type null; however, Limoncello\Flute\Adapter...iltersAndSortsWithAnd() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
808
                $builder->addRelationshipFiltersAndSortsWithOr($reverseRelName, $filters, $sorts);
0 ignored issues
show
Bug introduced by
It seems like $filters defined by $this->getFilters() on line 804 can also be of type null; however, Limoncello\Flute\Adapter...FiltersAndSortsWithOr() does only seem to accept object<Limoncello\Flute\Adapters\iterable>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
809
        }
810
        // ... and the input filters to actual data we select
811 9
        if ($relationshipFilters !== null) {
812 6
            $builder->addFiltersWithAndToAlias($relationshipFilters);
813
        }
814 9
        if ($relationshipSorts !== null) {
815 2
            $builder->addSorts($relationshipSorts);
816
        }
817
818 9
        $this->applyPaging($builder);
819
820
        // While joining tables we select distinct rows.
821 9
        $builder->distinct();
822
823 9
        return $this->builderOnReadRelationship($builder);
824
    }
825
826
    /**
827
     * @inheritdoc
828
     */
829 8
    public function indexRelationship(
830
        string $name,
831
        iterable $relationshipFilters = null,
832
        iterable $relationshipSorts = null
833
    ): PaginatedDataInterface {
834
        // depending on the relationship type we expect the result to be either single resource or a collection
835 8
        $relationshipType = $this->getModelSchemes()->getRelationshipType($this->getModelClass(), $name);
836 8
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
837 8
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
838
839 8
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts);
840
841 8
        $modelClass = $builder->getModelClass();
842 8
        $data       = $isExpectMany === true ?
843 8
            $this->fetchResources($builder, $modelClass) : $this->fetchResource($builder, $modelClass);
844
845 8
        return $data;
846
    }
847
848
    /**
849
     * @inheritdoc
850
     */
851 1
    public function indexRelationshipIdentities(
852
        string $name,
853
        iterable $relationshipFilters = null,
854
        iterable $relationshipSorts = null
855
    ): array {
856
        // depending on the relationship type we expect the result to be either single resource or a collection
857 1
        $relationshipType = $this->getModelSchemes()->getRelationshipType($this->getModelClass(), $name);
858 1
        $isExpectMany     = $relationshipType === RelationshipTypes::HAS_MANY ||
859 1
            $relationshipType === RelationshipTypes::BELONGS_TO_MANY;
860 1
        if ($isExpectMany === false) {
861
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
862
        }
863
864 1
        list ($targetModelClass) = $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $name);
865 1
        $targetPk = $this->getModelSchemes()->getPrimaryKey($targetModelClass);
866
867 1
        $builder = $this->createReadRelationshipBuilder($name, $relationshipFilters, $relationshipSorts, [$targetPk]);
0 ignored issues
show
Documentation introduced by
array($targetPk) is of type array<integer,string,{"0":"string"}>, but the function expects a object<Limoncello\Flute\Api\iterable>|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
868
869 1
        $modelClass = $builder->getModelClass();
870
        /** @var Generator $data */
871 1
        $data   = $this->fetchColumn($builder, $modelClass, $targetPk);
872 1
        $result = iterator_to_array($data);
873
874 1
        return $result;
875
    }
876
877
    /**
878
     * @inheritdoc
879
     */
880 3
    public function readRelationship(
881
        $index,
882
        string $name,
883
        iterable $relationshipFilters = null,
884
        iterable $relationshipSorts = null
885
    ): PaginatedDataInterface {
886 3
        return $this->withIndexFilter($index)->indexRelationship($name, $relationshipFilters, $relationshipSorts);
887
    }
888
889
    /**
890
     * @inheritdoc
891
     */
892 6
    public function hasInRelationship($parentId, string $name, $childId): bool
893
    {
894 6
        if ($parentId !== null && is_scalar($parentId) === false) {
895 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
896
        }
897 5
        if ($childId !== null && is_scalar($childId) === false) {
898 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
899
        }
900
901 4
        $parentPkName  = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
902 4
        $parentFilters = [$parentPkName => [FilterParameterInterface::OPERATION_EQUALS => [$parentId]]];
903 4
        list($childClass) = $this->getModelSchemes()->getReverseRelationship($this->getModelClass(), $name);
904 4
        $childPkName  = $this->getModelSchemes()->getPrimaryKey($childClass);
905 4
        $childFilters = [$childPkName => [FilterParameterInterface::OPERATION_EQUALS => [$childId]]];
906
907
        $data = $this
908 4
            ->clearBuilderParameters()
909 4
            ->clearFetchParameters()
910 4
            ->withFilters($parentFilters)
0 ignored issues
show
Documentation introduced by
$parentFilters is of type array<string,array<strin...ble|string|boolean"}>>>, but the function expects a object<Limoncello\Flute\Contracts\Api\iterable>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
911 4
            ->indexRelationship($name, $childFilters);
0 ignored issues
show
Documentation introduced by
$childFilters is of type array<string,array<strin...ble|string|boolean"}>>>, but the function expects a object<Limoncello\Flute\...acts\Api\iterable>|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
912
913 4
        $has = empty($data->getData()) === false;
914
915 4
        return $has;
916
    }
917
918
    /**
919
     * @inheritdoc
920
     */
921 5
    public function delete(): int
922
    {
923 5
        $deleted = $this->createDeleteModelBuilder()->execute();
924
925 4
        $this->clearFetchParameters();
926
927 4
        return (int)$deleted;
928
    }
929
930
    /**
931
     * @inheritdoc
932
     */
933 6
    public function remove($index): bool
934
    {
935 6
        return $this->withIndexFilter($index)->delete() > 0;
936
    }
937
938
    /**
939
     * @inheritdoc
940
     */
941 5
    public function create($index, iterable $attributes, iterable $toMany): string
942
    {
943 5
        if ($index !== null && is_int($index) === false && is_string($index) === false) {
944 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
945
        }
946
947 4
        $allowedChanges = $this->filterAttributesOnCreate($index, $attributes);
948
        $saveMain       = $this
949 4
            ->createBuilder($this->getModelClass())
950 4
            ->createModel($allowedChanges);
0 ignored issues
show
Documentation introduced by
$allowedChanges is of type object<Generator>, but the function expects a object<Limoncello\Flute\Adapters\iterable>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
951 4
        $saveMain       = $this->builderSaveResourceOnCreate($saveMain);
952 4
        $saveMain->getSQL(); // prepare
953
954 4
        $this->clearBuilderParameters()->clearFetchParameters();
955
956 4
        $this->inTransaction(function () use ($saveMain, $toMany, &$index) {
957 4
            $saveMain->execute();
958
959
            // if no index given will use last insert ID as index
960 4
            $index !== null ?: $index = $saveMain->getConnection()->lastInsertId();
961
962 4
            $inserted = 0;
963 4
            foreach ($toMany as $relationshipName => $secondaryIds) {
964 2
                $secondaryIdBindName = ':secondaryId';
965 2
                $saveToMany          = $this->builderSaveRelationshipOnCreate(
966 2
                    $relationshipName,
967
                    $this
968 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
969 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
970
                );
971 2
                foreach ($secondaryIds as $secondaryId) {
972 2
                    $inserted += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
973
                }
974
            }
975 4
        });
976
977 4
        return $index;
978
    }
979
980
    /**
981
     * @inheritdoc
982
     */
983 4
    public function update($index, iterable $attributes, iterable $toMany): int
984
    {
985 4
        if (is_int($index) === false && is_string($index) === false) {
986 1
            throw new InvalidArgumentException($this->getMessage(Messages::MSG_ERR_INVALID_ARGUMENT));
987
        }
988
989 3
        $updated        = 0;
990 3
        $pkName         = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
991
        $filters        = [
992
            $pkName => [
993 3
                FilterParameterInterface::OPERATION_EQUALS => [$index],
994
            ],
995
        ];
996 3
        $allowedChanges = $this->filterAttributesOnUpdate($attributes);
997
        $saveMain       = $this
998 3
            ->createBuilder($this->getModelClass())
999 3
            ->updateModels($allowedChanges)
0 ignored issues
show
Documentation introduced by
$allowedChanges is of type object<Generator>, but the function expects a object<Limoncello\Flute\Adapters\iterable>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1000 3
            ->addFiltersWithAndToTable($filters);
0 ignored issues
show
Documentation introduced by
$filters is of type array<string,array<strin...0":"integer|string"}>>>, but the function expects a object<Limoncello\Flute\Adapters\iterable>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1001 3
        $saveMain       = $this->builderSaveResourceOnUpdate($saveMain);
1002 3
        $saveMain->getSQL(); // prepare
1003
1004 3
        $this->clearBuilderParameters()->clearFetchParameters();
1005
1006 3
        $this->inTransaction(function () use ($saveMain, $toMany, $index, &$updated) {
1007 3
            $updated = $saveMain->execute();
1008
1009 3
            foreach ($toMany as $relationshipName => $secondaryIds) {
1010 2
                $cleanToMany = $this->builderCleanRelationshipOnUpdate(
1011 2
                    $relationshipName,
1012
                    $this
1013 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1014 2
                        ->clearToManyRelationship($relationshipName, $index)
1015
                );
1016 2
                $cleanToMany->execute();
1017
1018 2
                $secondaryIdBindName = ':secondaryId';
1019 2
                $saveToMany          = $this->builderSaveRelationshipOnUpdate(
1020 2
                    $relationshipName,
1021
                    $this
1022 2
                        ->createBuilderFromConnection($saveMain->getConnection(), $this->getModelClass())
1023 2
                        ->prepareCreateInToManyRelationship($relationshipName, $index, $secondaryIdBindName)
1024
                );
1025 2
                foreach ($secondaryIds as $secondaryId) {
1026 2
                    $updated += (int)$saveToMany->setParameter($secondaryIdBindName, $secondaryId)->execute();
1027
                }
1028
            }
1029 3
        });
1030
1031 3
        return (int)$updated;
1032
    }
1033
1034
    /**
1035
     * @return FactoryInterface
1036
     */
1037 39
    protected function getFactory(): FactoryInterface
1038
    {
1039 39
        return $this->factory;
1040
    }
1041
1042
    /**
1043
     * @return string
1044
     */
1045 39
    protected function getModelClass(): string
1046
    {
1047 39
        return $this->modelClass;
1048
    }
1049
1050
    /**
1051
     * @return ModelSchemeInfoInterface
1052
     */
1053 39
    protected function getModelSchemes(): ModelSchemeInfoInterface
1054
    {
1055 39
        return $this->modelSchemes;
1056
    }
1057
1058
    /**
1059
     * @return PaginationStrategyInterface
1060
     */
1061 6
    protected function getPaginationStrategy(): PaginationStrategyInterface
1062
    {
1063 6
        return $this->paginationStrategy;
1064
    }
1065
1066
    /**
1067
     * @param Closure $closure
1068
     *
1069
     * @return void
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use NoType.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1070
     */
1071 7
    public function inTransaction(Closure $closure): void
1072
    {
1073 7
        $connection = $this->getConnection();
1074 7
        $connection->beginTransaction();
1075
        try {
1076 7
            $isOk = ($closure() === false ? null : true);
1077 7
        } finally {
1078 7
            isset($isOk) === true ? $connection->commit() : $connection->rollBack();
1079
        }
1080
    }
1081
1082
    /**
1083
     * @inheritdoc
1084
     */
1085 23
    public function fetchResources(QueryBuilder $builder, string $modelClass): PaginatedDataInterface
1086
    {
1087 23
        $data = $this->fetchPaginatedResourcesWithoutRelationships($builder, $modelClass);
1088
1089 23
        if ($this->hasIncludes() === true) {
1090 14
            $this->loadRelationships($data);
1091 14
            $this->clearFetchParameters();
1092
        }
1093
1094 23
        return $data;
1095
    }
1096
1097
    /**
1098
     * @inheritdoc
1099
     */
1100 11
    public function fetchResource(QueryBuilder $builder, string $modelClass): PaginatedDataInterface
1101
    {
1102 11
        $data = $this->getFactory()->createPaginatedData(
1103 11
            $this->fetchResourceWithoutRelationships($builder, $modelClass)
1104 11
        )->markAsSingleItem();
1105
1106 11
        if ($this->hasIncludes() === true) {
1107 6
            $this->loadRelationships($data);
1108 6
            $this->clearFetchParameters();
1109
        }
1110
1111 11
        return $data;
1112
    }
1113
1114
    /**
1115
     * @inheritdoc
1116
     */
1117 1
    public function fetchRow(QueryBuilder $builder, string $modelClass): ?array
1118
    {
1119 1
        $statement = $builder->execute();
1120 1
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1121 1
        $platform  = $builder->getConnection()->getDatabasePlatform();
1122 1
        $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1123
1124 1
        $model = null;
1125 1
        if (($attributes = $statement->fetch()) !== false) {
1126 1
            $model = $this->readRowFromAssoc($attributes, $typeNames, $platform);
1127
        }
1128
1129 1
        $this->clearFetchParameters();
1130
1131 1
        return $model;
1132
    }
1133
1134
    /**
1135
     * @inheritdoc
1136
     */
1137 2
    public function fetchColumn(QueryBuilder $builder, string $modelClass, string $columnName): iterable
1138
    {
1139 2
        $statement = $builder->execute();
1140 2
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1141 2
        $platform = $builder->getConnection()->getDatabasePlatform();
1142 2
        $typeName = $this->getModelSchemes()->getAttributeTypes($modelClass)[$columnName];
1143 2
        $type     = Type::getType($typeName);
1144
1145 2
        while (($attributes = $statement->fetch()) !== false) {
1146 2
            $value     = $attributes[$columnName];
1147 2
            $converted = $type->convertToPHPValue($value, $platform);
1148
1149 2
            yield $converted;
1150
        }
1151
1152 2
        $this->clearFetchParameters();
1153
    }
1154
1155
    /**
1156
     * @param QueryBuilder $builder
1157
     * @param string       $modelClass
1158
     *
1159
     * @return PaginatedDataInterface
1160
     */
1161 26
    private function fetchPaginatedResourcesWithoutRelationships(
1162
        QueryBuilder $builder,
1163
        string $modelClass
1164
    ): PaginatedDataInterface {
1165 26
        list($models, $hasMore, $limit, $offset) = $this->fetchResourceCollection($builder, $modelClass);
1166
1167 26
        $data = $this->getFactory()
1168 26
            ->createPaginatedData($models)
1169 26
            ->markAsCollection()
1170 26
            ->setOffset($offset)
1171 26
            ->setLimit($limit);
1172
1173 26
        $hasMore === true ? $data->markHasMoreItems() : $data->markHasNoMoreItems();
1174
1175 26
        return $data;
1176
    }
1177
1178
    /**
1179
     * @param QueryBuilder $builder
1180
     * @param string       $modelClass
1181
     *
1182
     * @return mixed|null
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use object|null.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1183
     */
1184 14
    protected function fetchResourceWithoutRelationships(QueryBuilder $builder, string $modelClass)
1185
    {
1186 14
        $statement = $builder->execute();
1187 14
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1188 14
        $platform  = $builder->getConnection()->getDatabasePlatform();
1189 14
        $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1190
1191 14
        $model = null;
1192 14
        if (($attributes = $statement->fetch()) !== false) {
1193 14
            $model = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1194
        }
1195
1196 14
        return $model;
1197
    }
1198
1199
    /**
1200
     * @param QueryBuilder $builder
1201
     * @param string       $modelClass
1202
     *
1203
     * @return array
1204
     */
1205 26
    protected function fetchResourceCollection(QueryBuilder $builder, string $modelClass): array
1206
    {
1207 26
        $platform  = $builder->getConnection()->getDatabasePlatform();
1208 26
        $typeNames = $this->getModelSchemes()->getAttributeTypes($modelClass);
1209
1210 26
        $statement = $builder->execute();
1211 26
        $statement->setFetchMode(PDOConnection::FETCH_ASSOC);
1212
1213 26
        $models           = [];
1214 26
        $counter          = 0;
1215 26
        $hasMoreThanLimit = false;
1216 26
        $limit            = $builder->getMaxResults() !== null ? $builder->getMaxResults() - 1 : null;
1217 26
        while (($attributes = $statement->fetch()) !== false) {
1218 25
            $counter++;
1219 25
            if ($limit !== null && $counter > $limit) {
1220 6
                $hasMoreThanLimit = true;
1221 6
                break;
1222
            }
1223 25
            $models[] = $this->readResourceFromAssoc($modelClass, $attributes, $typeNames, $platform);
1224
        }
1225
1226 26
        return [$models, $hasMoreThanLimit, $limit, $builder->getFirstResult()];
1227
    }
1228
1229
    /**
1230
     * @param null|string $index
1231
     * @param iterable    $attributes
1232
     *
1233
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1234
     */
1235 4
    protected function filterAttributesOnCreate(?string $index, iterable $attributes): iterable
1236
    {
1237 4
        if ($index !== null) {
1238 1
            $pkName = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
1239 1
            yield $pkName => $index;
1240
        }
1241
1242 4
        $knownAttrAndTypes = $this->getModelSchemes()->getAttributeTypes($this->getModelClass());
1243 4
        foreach ($attributes as $attribute => $value) {
1244 4
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1245 4
                yield $attribute => $value;
1246
            }
1247
        }
1248
    }
1249
1250
    /**
1251
     * @param iterable $attributes
1252
     *
1253
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1254
     */
1255 3
    protected function filterAttributesOnUpdate(iterable $attributes): iterable
1256
    {
1257 3
        $knownAttrAndTypes = $this->getModelSchemes()->getAttributeTypes($this->getModelClass());
1258 3
        foreach ($attributes as $attribute => $value) {
1259 3
            if (array_key_exists($attribute, $knownAttrAndTypes) === true) {
1260 3
                yield $attribute => $value;
1261
            }
1262
        }
1263
    }
1264
1265
    /**
1266
     * @param TagStorageInterface   $modelsAtPath
1267
     * @param ArrayObject           $classAtPath
1268
     * @param ModelStorageInterface $deDup
1269
     * @param string                $parentsPath
1270
     * @param array                 $childRelationships
1271
     *
1272
     * @return void
1273
     *
1274
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1275
     */
1276 8
    private function loadRelationshipsLayer(
1277
        TagStorageInterface $modelsAtPath,
1278
        ArrayObject $classAtPath,
1279
        ModelStorageInterface $deDup,
1280
        string $parentsPath,
1281
        array $childRelationships
1282
    ): void {
1283 8
        $rootClass   = $classAtPath[static::ROOT_PATH];
1284 8
        $parentClass = $classAtPath[$parentsPath];
1285 8
        $parents     = $modelsAtPath->get($parentsPath);
1286
1287
        // What should we do? We have do find all child resources for $parents at paths $childRelationships (1 level
1288
        // child paths) and add them to $relationships. While doing it we have to deduplicate resources with
1289
        // $models.
1290
1291 8
        $pkName = $this->getModelSchemes()->getPrimaryKey($parentClass);
1292
1293 8
        foreach ($childRelationships as $name) {
1294 8
            $childrenPath = $parentsPath !== static::ROOT_PATH ? $parentsPath . static::PATH_SEPARATOR . $name : $name;
1295
1296 8
            $relationshipType = $this->getModelSchemes()->getRelationshipType($parentClass, $name);
1297
            list ($targetModelClass, $reverseRelName) =
1298 8
                $this->getModelSchemes()->getReverseRelationship($parentClass, $name);
1299
1300
            $builder = $this
1301 8
                ->createBuilder($targetModelClass)
1302 8
                ->selectModelColumns()
1303 8
                ->fromModelTable();
1304
1305 8
            $classAtPath[$childrenPath] = $targetModelClass;
1306
1307
            switch ($relationshipType) {
1308 8
                case RelationshipTypes::BELONGS_TO:
1309 7
                    foreach ($parents as $parent) {
1310 7
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1311 7
                            $reverseRelName,
1312 7
                            [$pkName => [FilterParameterInterface::OPERATION_EQUALS => [$parent->{$pkName}]]],
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...y($parent->{$pkName}))) is of type array<string,array<strin...<integer,?,{"0":"?"}>>>, but the function expects a object<Limoncello\Flute\Adapters\iterable>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1313 7
                            []
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<Limoncello\Flute\Adapters\iterable>|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1314
                        );
1315 7
                        $child         = $deDup->register($this->fetchResourceWithoutRelationships(
1316 7
                            $clonedBuilder,
1317 7
                            $clonedBuilder->getModelClass()
1318
                        ));
1319 7
                        if ($child !== null) {
1320 6
                            $modelsAtPath->register($child, $childrenPath);
1321
                        }
1322 7
                        $parent->{$name} = $child;
1323
                    }
1324 7
                    break;
1325 6
                case RelationshipTypes::HAS_MANY:
1326 4
                case RelationshipTypes::BELONGS_TO_MANY:
1327 6
                    list ($queryOffset, $queryLimit) = $this->getPaginationStrategy()
1328 6
                        ->getParameters($rootClass, $parentClass, $parentsPath, $name);
1329 6
                    $builder->setFirstResult($queryOffset)->setMaxResults($queryLimit + 1);
1330 6
                    foreach ($parents as $parent) {
1331 6
                        $clonedBuilder = (clone $builder)->addRelationshipFiltersAndSortsWithAnd(
1332 6
                            $reverseRelName,
1333 6
                            [$pkName => [FilterParameterInterface::OPERATION_EQUALS => [$parent->{$pkName}]]],
0 ignored issues
show
Documentation introduced by
array($pkName => array(\...y($parent->{$pkName}))) is of type array<string,array<strin...<integer,?,{"0":"?"}>>>, but the function expects a object<Limoncello\Flute\Adapters\iterable>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1334 6
                            []
0 ignored issues
show
Documentation introduced by
array() is of type array, but the function expects a object<Limoncello\Flute\Adapters\iterable>|null.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1335
                        );
1336 6
                        $children      = $this->fetchPaginatedResourcesWithoutRelationships(
1337 6
                            $clonedBuilder,
1338 6
                            $clonedBuilder->getModelClass()
1339
                        );
1340
1341 6
                        $deDupedChildren = [];
1342 6
                        foreach ($children->getData() as $child) {
1343 6
                            $child = $deDup->register($child);
1344 6
                            $modelsAtPath->register($child, $childrenPath);
1345 6
                            if ($child !== null) {
1346 6
                                $deDupedChildren[] = $child;
1347
                            }
1348
                        }
1349
1350 6
                        $paginated = $this->getFactory()
1351 6
                            ->createPaginatedData($deDupedChildren)
1352 6
                            ->markAsCollection()
1353 6
                            ->setOffset($children->getOffset())
1354 6
                            ->setLimit($children->getLimit());
1355 6
                        $children->hasMoreItems() === true ?
1356 6
                            $paginated->markHasMoreItems() : $paginated->markHasNoMoreItems();
1357
1358 6
                        $parent->{$name} = $paginated;
1359
                    }
1360 8
                    break;
1361
            }
1362
        }
1363
    }
1364
1365
    /**
1366
     * @param string $message
1367
     *
1368
     * @return string
1369
     */
1370 7
    private function getMessage(string $message): string
1371
    {
1372
        /** @var FormatterFactoryInterface $factory */
1373 7
        $factory   = $this->getContainer()->get(FormatterFactoryInterface::class);
1374 7
        $formatter = $factory->createFormatter(Messages::RESOURCES_NAMESPACE);
1375 7
        $result    = $formatter->formatMessage($message);
1376
1377 7
        return $result;
1378
    }
1379
1380
    /**
1381
     * @param string           $class
1382
     * @param array            $attributes
1383
     * @param Type[]           $typeNames
1384
     * @param AbstractPlatform $platform
1385
     *
1386
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use object.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
1387
     *
1388
     * @SuppressWarnings(PHPMD.StaticAccess)
1389
     */
1390 32
    private function readResourceFromAssoc(
1391
        string $class,
1392
        array $attributes,
1393
        array $typeNames,
1394
        AbstractPlatform $platform
1395
    ) {
1396 32
        $instance = new $class();
1397 32
        foreach ($this->readTypedAttributes($attributes, $typeNames, $platform) as $name => $value) {
0 ignored issues
show
Documentation introduced by
$attributes is of type array, but the function expects a object<Limoncello\Flute\Api\iterable>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1398 32
            $instance->{$name} = $value;
1399
        }
1400
1401 32
        return $instance;
1402
    }
1403
1404
    /**
1405
     * @param array            $attributes
1406
     * @param Type[]           $typeNames
1407
     * @param AbstractPlatform $platform
1408
     *
1409
     * @return array
1410
     *
1411
     * @SuppressWarnings(PHPMD.StaticAccess)
1412
     */
1413 1
    private function readRowFromAssoc(array $attributes, array $typeNames, AbstractPlatform $platform): array
1414
    {
1415 1
        $row = [];
1416 1
        foreach ($this->readTypedAttributes($attributes, $typeNames, $platform) as $name => $value) {
0 ignored issues
show
Documentation introduced by
$attributes is of type array, but the function expects a object<Limoncello\Flute\Api\iterable>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1417 1
            $row[$name] = $value;
1418
        }
1419
1420 1
        return $row;
1421
    }
1422
1423
    /**
1424
     * @param iterable         $attributes
1425
     * @param array            $typeNames
1426
     * @param AbstractPlatform $platform
1427
     *
1428
     * @return iterable
0 ignored issues
show
Documentation introduced by
Should the return type not be Generator?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1429
     */
1430 33
    private function readTypedAttributes(iterable $attributes, array $typeNames, AbstractPlatform $platform): iterable
1431
    {
1432 33
        foreach ($attributes as $name => $value) {
1433 33
            yield $name => (array_key_exists($name, $typeNames) === true ?
1434 33
                Type::getType($typeNames[$name])->convertToPHPValue($value, $platform) : $value);
1435
        }
1436
    }
1437
}
1438