Completed
Push — develop ( d107a0...92ed96 )
by Neomerx
04:17 queued 02:15
created

ModelQueryBuilder   D

Complexity

Total Complexity 82

Size/Duplication

Total Lines 837
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 82
lcom 1
cbo 7
dl 0
loc 837
ccs 305
cts 305
cp 1
rs 4.4444
c 0
b 0
f 0

40 Methods

Rating   Name   Duplication   Size   Complexity  
B addRelationshipFiltersAndSorts() 0 40 4
A getMainTableName() 0 4 1
A getModelSchemes() 0 4 1
A getMainAlias() 0 4 1
A __construct() 0 14 1
A getModelClass() 0 4 1
A selectModelColumns() 0 15 3
A setColumnToDatabaseMapper() 0 6 1
A fromModelTable() 0 9 1
A createModel() 0 12 2
A updateModels() 0 10 2
A bindAttributes() 0 17 3
A deleteModels() 0 6 1
A prepareCreateInToManyRelationship() 0 17 1
A clearToManyRelationship() 0 12 1
A addFiltersWithAndToTable() 0 4 1
A addFiltersWithOrToTable() 0 4 1
A addFiltersWithAndToAlias() 0 4 1
A addFiltersWithOrToAlias() 0 4 1
A addRelationshipFiltersAndSortsWithAnd() 0 14 1
A distinct() 0 8 1
A addRelationshipFiltersAndSortsWithOr() 0 14 1
A addSorts() 0 11 4
A addFilters() 0 12 3
B addBelongsToFiltersAndSorts() 0 25 2
B addHasManyFiltersAndSorts() 0 25 2
B addBelongsToManyFiltersAndSorts() 0 35 2
C innerJoinOneTable() 0 37 8
B innerJoinTwoSequentialTables() 0 36 1
A createAlias() 0 7 1
A quoteTableName() 0 4 1
A quoteColumnName() 0 4 1
A buildColumnName() 0 4 1
A getQuotedMainTableColumn() 0 4 1
A getQuotedMainAliasColumn() 0 4 1
C applyFilter() 0 55 15
A createSingleNamedParameter() 0 11 2
A createNamedParameterArray() 0 10 2
A getColumnToDatabaseMapper() 0 4 1
A getPdoType() 0 18 3

How to fix   Complexity   

Complex Class

Complex classes like ModelQueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ModelQueryBuilder, and based on these observations, apply Extract Interface, too.

1
<?php namespace Limoncello\Flute\Adapters;
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 Closure;
20
use Doctrine\DBAL\Connection;
21
use Doctrine\DBAL\Query\Expression\CompositeExpression;
22
use Doctrine\DBAL\Query\QueryBuilder;
23
use Doctrine\DBAL\Types\Type;
24
use Generator;
25
use Limoncello\Contracts\Data\ModelSchemeInfoInterface;
26
use Limoncello\Contracts\Data\RelationshipTypes;
27
use Limoncello\Flute\Contracts\Http\Query\FilterParameterInterface;
28
use Limoncello\Flute\Exceptions\InvalidArgumentException;
29
use PDO;
30
31
/**
32
 * @package Limoncello\Flute
33
 *
34
 * @SuppressWarnings(PHPMD.TooManyMethods)
35
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
36
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
37
 */
38
class ModelQueryBuilder extends QueryBuilder
39
{
40
    /**
41
     * @var string
42
     */
43
    private $modelClass;
44
45
    /**
46
     * @var string
47
     */
48
    private $mainTableName;
49
50
    /**
51
     * @var string
52
     */
53
    private $mainAlias;
54
55
    /**
56
     * @var Closure
57
     */
58
    private $columnMapper;
59
60
    /**
61
     * @var ModelSchemeInfoInterface
62
     */
63
    private $modelSchemes;
64
65
    /**
66
     * @var int
67
     */
68
    private $aliasIdCounter = 0;
69
70
    /**
71
     * @var array
72
     */
73
    private $knownAliases = [];
74
75
    /**
76
     * @param Connection               $connection
77
     * @param string                   $modelClass
78
     * @param ModelSchemeInfoInterface $modelSchemes
79
     *
80
     * @SuppressWarnings(PHPMD.StaticAccess)
81
     */
82 58
    public function __construct(Connection $connection, string $modelClass, ModelSchemeInfoInterface $modelSchemes)
83
    {
84 58
        assert(!empty($modelClass));
85
86 58
        parent::__construct($connection);
87
88 58
        $this->modelSchemes = $modelSchemes;
89 58
        $this->modelClass   = $modelClass;
90
91 58
        $this->mainTableName = $this->getModelSchemes()->getTable($this->getModelClass());
92 58
        $this->mainAlias     = $this->createAlias($this->getMainTableName());
93
94 58
        $this->setColumnToDatabaseMapper(Closure::fromCallable([$this, 'getQuotedMainAliasColumn']));
95
    }
96
97
    /**
98
     * @return string
99
     */
100 58
    public function getModelClass(): string
101
    {
102 58
        return $this->modelClass;
103
    }
104
105
    /**
106
     * Select all fields associated with model.
107
     *
108
     * @param iterable|null $columns
109
     *
110
     * @return self
111
     */
112 52
    public function selectModelColumns(iterable $columns = null): self
113
    {
114
        $selectedColumns =
115 52
            $columns === null ? $this->getModelSchemes()->getAttributes($this->getModelClass()) : $columns;
116
117 52
        $quotedColumns = [];
118 52
        $columnMapper  = $this->getColumnToDatabaseMapper();
119 52
        foreach ($selectedColumns as $column) {
120 52
            $quotedColumns[] = call_user_func($columnMapper, $column, $this);
121
        }
122
123 52
        $this->select($quotedColumns);
124
125 52
        return $this;
126
    }
127
128
    /**
129
     * @param Closure $columnMapper
130
     *
131
     * @return self
132
     */
133 58
    public function setColumnToDatabaseMapper(Closure $columnMapper): self
134
    {
135 58
        $this->columnMapper = $columnMapper;
136
137 58
        return $this;
138
    }
139
140
    /**
141
     * @return self
142
     */
143 53
    public function fromModelTable(): self
144
    {
145 53
        $this->from(
146 53
            $this->quoteTableName($this->getMainTableName()),
147 53
            $this->quoteTableName($this->getMainAlias())
148
        );
149
150 53
        return $this;
151
    }
152
153
    /**
154
     * @param iterable $attributes
155
     *
156
     * @return self
157
     *
158
     * @SuppressWarnings(PHPMD.StaticAccess)
159
     */
160 4
    public function createModel(iterable $attributes): self
161
    {
162 4
        $this->insert($this->quoteTableName($this->getMainTableName()));
163
164 4
        $valuesAsParams = [];
165 4
        foreach ($this->bindAttributes($this->getModelClass(), $attributes) as $quotedColumn => $parameterName) {
166 4
            $valuesAsParams[$quotedColumn] = $parameterName;
167
        }
168 4
        $this->values($valuesAsParams);
169
170 4
        return $this;
171
    }
172
173
    /**
174
     * @param iterable $attributes
175
     *
176
     * @return self
177
     *
178
     * @SuppressWarnings(PHPMD.StaticAccess)
179
     */
180 5
    public function updateModels(iterable $attributes): self
181
    {
182 5
        $this->update($this->quoteTableName($this->getMainTableName()));
183
184 5
        foreach ($this->bindAttributes($this->getModelClass(), $attributes) as $quotedColumn => $parameterName) {
185 5
            $this->set($quotedColumn, $parameterName);
186
        }
187
188 5
        return $this;
189
    }
190
191
    /**
192
     * @param string   $modelClass
193
     * @param iterable $attributes
194
     *
195
     * @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...
196
     *
197
     * @SuppressWarnings(PHPMD.StaticAccess)
198
     */
199 9
    public function bindAttributes(string $modelClass, iterable $attributes): iterable
200
    {
201 9
        $dbPlatform = $this->getConnection()->getDatabasePlatform();
202 9
        $types      = $this->getModelSchemes()->getAttributeTypes($modelClass);
203
204 9
        foreach ($attributes as $column => $value) {
205 9
            assert(is_string($column) && $this->getModelSchemes()->hasAttributeType($this->getModelClass(), $column));
206
207 9
            $quotedColumn  = $this->quoteColumnName($column);
208
209 9
            $type          = Type::getType($types[$column]);
210 9
            $pdoValue      = $type->convertToDatabaseValue($value, $dbPlatform);
211 9
            $parameterName = $this->createNamedParameter($pdoValue, $type->getBindingType());
212
213 9
            yield $quotedColumn => $parameterName;
214
        }
215
    }
216
217
    /**
218
     * @return self
219
     */
220 5
    public function deleteModels(): self
221
    {
222 5
        $this->delete($this->quoteTableName($this->getMainTableName()));
223
224 5
        return $this;
225
    }
226
227
    /**
228
     * @inheritdoc
229
     */
230 4
    public function prepareCreateInToManyRelationship(
231
        string $relationshipName,
232
        string $identity,
233
        string $secondaryIdBindName
234
    ): self {
235
        list ($intermediateTable, $primaryKey, $secondaryKey) =
236 4
            $this->getModelSchemes()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName);
237
238
        $this
239 4
            ->insert($this->quoteTableName($intermediateTable))
240 4
            ->values([
241 4
                $this->quoteColumnName($primaryKey)   => $this->createNamedParameter($identity),
242 4
                $this->quoteColumnName($secondaryKey) => $secondaryIdBindName,
243
            ]);
244
245 4
        return $this;
246
    }
247
248
    /**
249
     * @inheritdoc
250
     */
251 2
    public function clearToManyRelationship(string $relationshipName, string $identity): self
252
    {
253
        list ($intermediateTable, $primaryKey) =
254 2
            $this->getModelSchemes()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName);
255
256 2
        $filters = [$primaryKey => [FilterParameterInterface::OPERATION_EQUALS => [$identity]]];
257
        $this
258 2
            ->delete($this->quoteTableName($intermediateTable))
259 2
            ->addFilters($intermediateTable, $this->expr()->andX(), $filters);
0 ignored issues
show
Documentation introduced by
$filters is of type array<?,array<string|int...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...
260
261 2
        return $this;
262
    }
263
264
    /**
265
     * @param iterable $filters
266
     *
267
     * @return self
268
     */
269 9
    public function addFiltersWithAndToTable(iterable $filters): self
270
    {
271 9
        return $this->addFilters($this->getMainTableName(), $this->expr()->andX(), $filters);
272
    }
273
274
    /**
275
     * @param iterable $filters
276
     *
277
     * @return self
278
     */
279 1
    public function addFiltersWithOrToTable(iterable $filters): self
280
    {
281 1
        return $this->addFilters($this->getMainTableName(), $this->expr()->orX(), $filters);
282
    }
283
284
    /**
285
     * @param iterable $filters
286
     *
287
     * @return self
288
     */
289 38
    public function addFiltersWithAndToAlias(iterable $filters): self
290
    {
291 38
        return $this->addFilters($this->getMainAlias(), $this->expr()->andX(), $filters);
292
    }
293
294
    /**
295
     * @param iterable $filters
296
     *
297
     * @return self
298
     */
299 2
    public function addFiltersWithOrToAlias(iterable $filters): self
300
    {
301 2
        return $this->addFilters($this->getMainAlias(), $this->expr()->orX(), $filters);
302
    }
303
304
    /**
305
     * @param string        $relationshipName
306
     * @param iterable      $relationshipFilters
307
     * @param iterable|null $relationshipSorts
308
     *
309
     * @return self
310
     */
311 26
    public function addRelationshipFiltersAndSortsWithAnd(
312
        string $relationshipName,
313
        iterable $relationshipFilters,
314
        ?iterable $relationshipSorts
315
    ): self {
316 26
        $joinWith = $this->expr()->andX();
317
318 26
        return $this->addRelationshipFiltersAndSorts(
319 26
            $relationshipName,
320 26
            $joinWith,
321 26
            $relationshipFilters,
322 26
            $relationshipSorts
323
        );
324
    }
325
326
    /**
327
     * @return self
328
     */
329 14
    public function distinct(): self
330
    {
331
        // emulate SELECT DISTINCT with grouping by primary key
332 14
        $primaryColumn = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
333 14
        $this->addGroupBy($this->getQuotedMainAliasColumn($primaryColumn));
334
335 14
        return $this;
336
    }
337
338
    /**
339
     * @param string        $relationshipName
340
     * @param iterable      $relationshipFilters
341
     * @param iterable|null $relationshipSorts
342
     *
343
     * @return self
344
     */
345 2
    public function addRelationshipFiltersAndSortsWithOr(
346
        string $relationshipName,
347
        iterable $relationshipFilters,
348
        ?iterable $relationshipSorts
349
    ): self {
350 2
        $joinWith = $this->expr()->orX();
351
352 2
        return $this->addRelationshipFiltersAndSorts(
353 2
            $relationshipName,
354 2
            $joinWith,
355 2
            $relationshipFilters,
356 2
            $relationshipSorts
357
        );
358
    }
359
360
    /**
361
     * @param iterable $sortParameters
362
     *
363
     * @return self
364
     */
365 8
    public function addSorts(iterable $sortParameters): self
366
    {
367 8
        foreach ($sortParameters as $columnName => $isAsc) {
368 8
            assert(is_string($columnName) === true && is_bool($isAsc) === true);
369 8
            $fullColumnName = $this->getQuotedMainAliasColumn($columnName);
370 8
            assert($this->getModelSchemes()->hasAttributeType($this->getModelClass(), $columnName));
371 8
            $this->addOrderBy($fullColumnName, $isAsc === true ? 'ASC' : 'DESC');
372
        }
373
374 8
        return $this;
375
    }
376
377
    /**
378
     * @param string              $tableOrAlias
379
     * @param CompositeExpression $filterLink
380
     * @param iterable            $filters
381
     *
382
     * @return self
383
     */
384 43
    private function addFilters(string $tableOrAlias, CompositeExpression $filterLink, iterable $filters): self
385
    {
386 43
        foreach ($filters as $columnName => $operationsWithArgs) {
387 43
            $fullColumnName = $this->buildColumnName($tableOrAlias, $columnName);
388 43
            $this->applyFilter($filterLink, $fullColumnName, $operationsWithArgs);
389
        }
390 42
        if ($filterLink->count() > 0) {
391 42
            $this->andWhere($filterLink);
392
        }
393
394 42
        return $this;
395
    }
396
397
    /**
398
     * @param string              $relationshipName
399
     * @param CompositeExpression $filterLink
400
     * @param iterable            $relationshipFilters
401
     * @param iterable|null       $relationshipSorts
402
     *
403
     * @return self
404
     */
405 28
    private function addRelationshipFiltersAndSorts(
406
        string $relationshipName,
407
        CompositeExpression $filterLink,
408
        iterable $relationshipFilters,
409
        ?iterable $relationshipSorts
410
    ): self {
411 28
        $relationshipType = $this->getModelSchemes()->getRelationshipType($this->getModelClass(), $relationshipName);
412
        switch ($relationshipType) {
413 28
            case RelationshipTypes::BELONGS_TO:
414 17
                $builder = $this->addBelongsToFiltersAndSorts(
415 17
                    $relationshipName,
416 17
                    $filterLink,
417 17
                    $relationshipFilters,
418 17
                    $relationshipSorts
419
                );
420 17
                break;
421
422 16
            case RelationshipTypes::HAS_MANY:
423 12
                $builder = $this->addHasManyFiltersAndSorts(
424 12
                    $relationshipName,
425 12
                    $filterLink,
426 12
                    $relationshipFilters,
427 12
                    $relationshipSorts
428
                );
429 12
                break;
430
431 9
            case RelationshipTypes::BELONGS_TO_MANY:
432
            default:
433 9
                assert($relationshipType === RelationshipTypes::BELONGS_TO_MANY);
434 9
                $builder = $this->addBelongsToManyFiltersAndSorts(
435 9
                    $relationshipName,
436 9
                    $filterLink,
437 9
                    $relationshipFilters,
438 9
                    $relationshipSorts
439
                );
440 9
                break;
441
        }
442
443 28
        return $builder;
444
    }
445
446
    /**
447
     * @return string
448
     */
449 58
    private function getMainTableName(): string
450
    {
451 58
        return $this->mainTableName;
452
    }
453
454
    /**
455
     * @return ModelSchemeInfoInterface
456
     */
457 58
    private function getModelSchemes(): ModelSchemeInfoInterface
458
    {
459 58
        return $this->modelSchemes;
460
    }
461
462
    /**
463
     * @return string
464
     */
465 54
    private function getMainAlias(): string
466
    {
467 54
        return $this->mainAlias;
468
    }
469
470
    /**
471
     * @param string              $relationshipName
472
     * @param CompositeExpression $filterLink
473
     * @param iterable            $relationshipFilters
474
     * @param iterable|null       $relationshipSorts
475
     *
476
     * @return self
477
     */
478 17
    private function addBelongsToFiltersAndSorts(
479
        string $relationshipName,
480
        CompositeExpression $filterLink,
481
        iterable $relationshipFilters,
482
        ?iterable $relationshipSorts
483
    ): self {
484 17
        $foreignKey = $this->getModelSchemes()->getForeignKey($this->getModelClass(), $relationshipName);
485
        list($onePrimaryKey, $oneTable) =
486 17
            $this->getModelSchemes()->getReversePrimaryKey($this->getModelClass(), $relationshipName);
487
488 17
        $this->innerJoinOneTable(
489 17
            $this->getMainAlias(),
490 17
            $foreignKey,
491 17
            $oneTable,
492 17
            $onePrimaryKey,
493 17
            $filterLink,
494 17
            $relationshipFilters,
495 17
            $relationshipSorts
496
        );
497 17
        if ($filterLink->count() > 0) {
498 16
            $this->andWhere($filterLink);
499
        }
500
501 17
        return $this;
502
    }
503
504
    /**
505
     * @param string              $relationshipName
506
     * @param CompositeExpression $filterLink
507
     * @param iterable            $relationshipFilters
508
     * @param iterable|null       $relationshipSorts
509
     *
510
     * @return self
511
     */
512 12
    private function addHasManyFiltersAndSorts(
513
        string $relationshipName,
514
        CompositeExpression $filterLink,
515
        iterable $relationshipFilters,
516
        ?iterable $relationshipSorts
517
    ): self {
518 12
        $primaryKey = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
519
        list($manyForeignKey, $manyTable) =
520 12
            $this->getModelSchemes()->getReverseForeignKey($this->getModelClass(), $relationshipName);
521
522 12
        $this->innerJoinOneTable(
523 12
            $this->getMainAlias(),
524 12
            $primaryKey,
525 12
            $manyTable,
526 12
            $manyForeignKey,
527 12
            $filterLink,
528 12
            $relationshipFilters,
529 12
            $relationshipSorts
530
        );
531 12
        if ($filterLink->count() > 0) {
532 12
            $this->andWhere($filterLink);
533
        }
534
535 12
        return $this;
536
    }
537
538
    /**
539
     * @param string              $relationshipName
540
     * @param CompositeExpression $targetFilterLink
541
     * @param iterable            $relationshipFilters
542
     * @param iterable|null       $relationshipSorts
543
     *
544
     * @return self
545
     */
546 9
    private function addBelongsToManyFiltersAndSorts(
547
        string $relationshipName,
548
        CompositeExpression $targetFilterLink,
549
        iterable $relationshipFilters,
550
        ?iterable $relationshipSorts
551
    ): self {
552 9
        $primaryKey = $this->getModelSchemes()->getPrimaryKey($this->getModelClass());
553
        list ($intermediateTable, $intermediatePk, $intermediateFk) =
554 9
            $this->getModelSchemes()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName);
555
        list($targetPrimaryKey, $targetTable) =
556 9
            $this->getModelSchemes()->getReversePrimaryKey($this->getModelClass(), $relationshipName);
557
558
        // no filters for intermediate table
559 9
        $intFilterLink = null;
560 9
        $intFilters    = null;
561 9
        $this->innerJoinTwoSequentialTables(
562 9
            $this->getMainAlias(),
563 9
            $primaryKey,
564 9
            $intermediateTable,
565 9
            $intermediatePk,
566 9
            $intermediateFk,
567 9
            $targetTable,
568 9
            $targetPrimaryKey,
569 9
            $intFilterLink,
570 9
            $intFilters,
571 9
            $targetFilterLink,
572 9
            $relationshipFilters,
573 9
            $relationshipSorts
574
        );
575 9
        if ($targetFilterLink->count() > 0) {
576 9
            $this->andWhere($targetFilterLink);
577
        }
578
579 9
        return $this;
580
    }
581
582
    /**
583
     * @param string                   $fromAlias
584
     * @param string                   $fromColumn
585
     * @param string                   $targetTable
586
     * @param string                   $targetColumn
587
     * @param CompositeExpression|null $targetFilterLink
588
     * @param iterable|null            $targetFilterParams
589
     * @param iterable|null            $relationshipSorts
590
     *
591
     * @return string
592
     */
593 28
    private function innerJoinOneTable(
594
        string $fromAlias,
595
        string $fromColumn,
596
        string $targetTable,
597
        string $targetColumn,
598
        ?CompositeExpression $targetFilterLink,
599
        ?iterable $targetFilterParams,
600
        ?iterable $relationshipSorts
601
    ): string {
602 28
        $targetAlias   = $this->createAlias($targetTable);
603 28
        $joinCondition = $this->buildColumnName($fromAlias, $fromColumn) . '=' .
604 28
            $this->buildColumnName($targetAlias, $targetColumn);
605
606 28
        $this->innerJoin(
607 28
            $this->quoteTableName($fromAlias),
608 28
            $this->quoteTableName($targetTable),
609 28
            $this->quoteTableName($targetAlias),
610 28
            $joinCondition
611
        );
612
613 28
        if ($targetFilterLink !== null && $targetFilterParams !== null) {
614 28
            foreach ($targetFilterParams as $columnName => $operationsWithArgs) {
615 27
                assert(is_string($columnName) === true);
616 27
                $fullColumnName = $this->buildColumnName($targetAlias, $columnName);
617 27
                $this->applyFilter($targetFilterLink, $fullColumnName, $operationsWithArgs);
618
            }
619
        }
620 28
        if ($relationshipSorts !== null) {
621 20
            foreach ($relationshipSorts as $columnName => $isAsc) {
622 6
                assert(is_string($columnName) === true && is_bool($isAsc) === true);
623 6
                $fullColumnName = $this->buildColumnName($targetAlias, $columnName);
624 6
                $this->addOrderBy($fullColumnName, $isAsc === true ? 'ASC' : 'DESC');
625
            }
626
        }
627
628 28
        return $targetAlias;
629
    }
630
631
    /** @noinspection PhpTooManyParametersInspection
632
     * @param string                   $fromAlias
633
     * @param string                   $fromColumn
634
     * @param string                   $intTable
635
     * @param string                   $intToFromColumn
636
     * @param string                   $intToTargetColumn
637
     * @param string                   $targetTable
638
     * @param string                   $targetColumn
639
     * @param CompositeExpression|null $intFilterLink
640
     * @param iterable|null            $intFilterParams
641
     * @param CompositeExpression|null $targetFilterLink
642
     * @param iterable|null            $targetFilterParams
643
     * @param iterable|null            $targetSortParams
644
     *
645
     * @return string
646
     *
647
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
648
     */
649 9
    private function innerJoinTwoSequentialTables(
650
        string $fromAlias,
651
        string $fromColumn,
652
        string $intTable,
653
        string $intToFromColumn,
654
        string $intToTargetColumn,
655
        string $targetTable,
656
        string $targetColumn,
657
        ?CompositeExpression $intFilterLink,
658
        ?iterable $intFilterParams,
659
        ?CompositeExpression $targetFilterLink,
660
        ?iterable $targetFilterParams,
661
        ?iterable $targetSortParams
662
    ): string {
663 9
        $intNoSorting = null;
664 9
        $intAlias     = $this->innerJoinOneTable(
665 9
            $fromAlias,
666 9
            $fromColumn,
667 9
            $intTable,
668 9
            $intToFromColumn,
669 9
            $intFilterLink,
670 9
            $intFilterParams,
671 9
            $intNoSorting
672
        );
673 9
        $targetAlias  = $this->innerJoinOneTable(
674 9
            $intAlias,
675 9
            $intToTargetColumn,
676 9
            $targetTable,
677 9
            $targetColumn,
678 9
            $targetFilterLink,
679 9
            $targetFilterParams,
680 9
            $targetSortParams
681
        );
682
683 9
        return $targetAlias;
684
    }
685
686
    /**
687
     * @param string $tableName
688
     *
689
     * @return string
690
     */
691 58
    private function createAlias(string $tableName): string
692
    {
693 58
        $alias                          = $tableName . (++$this->aliasIdCounter);
694 58
        $this->knownAliases[$tableName] = $alias;
695
696 58
        return $alias;
697
    }
698
699
    /**
700
     * @inheritdoc
701
     */
702 57
    private function quoteTableName(string $tableName): string
703
    {
704 57
        return "`$tableName`";
705
    }
706
707
    /**
708
     * @inheritdoc
709
     */
710 9
    private function quoteColumnName(string $columnName): string
711
    {
712 9
        return "`$columnName`";
713
    }
714
715
    /**
716
     * @inheritdoc
717
     */
718 58
    private function buildColumnName(string $table, string $column): string
719
    {
720 58
        return "`$table`.`$column`";
721
    }
722
723
    /**
724
     * @param string $column
725
     *
726
     * @return string
727
     */
728 1
    public function getQuotedMainTableColumn(string $column): string
729
    {
730 1
        return $this->buildColumnName($this->getMainTableName(), $column);
731
    }
732
733
    /**
734
     * @param string $column
735
     *
736
     * @return string
737
     */
738 53
    public function getQuotedMainAliasColumn(string $column): string
739
    {
740 53
        return $this->buildColumnName($this->getMainAlias(), $column);
741
    }
742
743
    /**
744
     * @param CompositeExpression $filterLink
745
     * @param string              $fullColumnName
746
     * @param iterable            $operationsWithArgs
747
     *
748
     * @return void
749
     *
750
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
751
     */
752 54
    private function applyFilter(
753
        CompositeExpression $filterLink,
754
        string $fullColumnName,
755
        iterable $operationsWithArgs
756
    ): void {
757 54
        foreach ($operationsWithArgs as $operation => $arguments) {
758 54
            assert(is_int($operation));
759 54
            assert(
760 54
                is_array($arguments) || $arguments instanceof Generator,
761 54
                "Filter argument(s) for $fullColumnName must be iterable (an array or Generator)."
762
            );
763
            switch ($operation) {
764 54
                case FilterParameterInterface::OPERATION_EQUALS:
765 45
                    $expression = $this->expr()->eq($fullColumnName, $this->createSingleNamedParameter($arguments));
0 ignored issues
show
Documentation introduced by
$arguments is of type array|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...
766 44
                    break;
767 19
                case FilterParameterInterface::OPERATION_NOT_EQUALS:
768 1
                    $expression = $this->expr()->neq($fullColumnName, $this->createSingleNamedParameter($arguments));
0 ignored issues
show
Documentation introduced by
$arguments is of type array|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...
769 1
                    break;
770 19
                case FilterParameterInterface::OPERATION_LESS_THAN:
771 6
                    $expression = $this->expr()->lt($fullColumnName, $this->createSingleNamedParameter($arguments));
0 ignored issues
show
Documentation introduced by
$arguments is of type array|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...
772 6
                    break;
773 19
                case FilterParameterInterface::OPERATION_LESS_OR_EQUALS:
774 7
                    $expression = $this->expr()->lte($fullColumnName, $this->createSingleNamedParameter($arguments));
0 ignored issues
show
Documentation introduced by
$arguments is of type array|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...
775 7
                    break;
776 18
                case FilterParameterInterface::OPERATION_GREATER_THAN:
777 2
                    $expression = $this->expr()->gt($fullColumnName, $this->createSingleNamedParameter($arguments));
0 ignored issues
show
Documentation introduced by
$arguments is of type array|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...
778 2
                    break;
779 17
                case FilterParameterInterface::OPERATION_GREATER_OR_EQUALS:
780 6
                    $expression = $this->expr()->gte($fullColumnName, $this->createSingleNamedParameter($arguments));
0 ignored issues
show
Documentation introduced by
$arguments is of type array|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...
781 6
                    break;
782 12
                case FilterParameterInterface::OPERATION_LIKE:
783 9
                    $expression = $this->expr()->like($fullColumnName, $this->createSingleNamedParameter($arguments));
0 ignored issues
show
Documentation introduced by
$arguments is of type array|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...
784 9
                    break;
785 6
                case FilterParameterInterface::OPERATION_NOT_LIKE:
786 1
                    $parameter  = $this->createSingleNamedParameter($arguments);
0 ignored issues
show
Documentation introduced by
$arguments is of type array|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...
787 1
                    $expression = $this->expr()->notLike($fullColumnName, $parameter);
788 1
                    break;
789 6
                case FilterParameterInterface::OPERATION_IN:
790 6
                    $expression = $this->expr()->in($fullColumnName, $this->createNamedParameterArray($arguments));
0 ignored issues
show
Documentation introduced by
$arguments is of type array|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...
791 6
                    break;
792 1
                case FilterParameterInterface::OPERATION_NOT_IN:
793 1
                    $expression = $this->expr()->notIn($fullColumnName, $this->createNamedParameterArray($arguments));
0 ignored issues
show
Documentation introduced by
$arguments is of type array|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...
794 1
                    break;
795 1
                case FilterParameterInterface::OPERATION_IS_NULL:
796 1
                    $expression = $this->expr()->isNull($fullColumnName);
797 1
                    break;
798 1
                case FilterParameterInterface::OPERATION_IS_NOT_NULL:
799
                default:
800 1
                    $expression = $this->expr()->isNotNull($fullColumnName);
801 1
                    break;
802
            }
803
804 53
            $filterLink->add($expression);
805
        }
806
    }
807
808
    /**
809
     * @param iterable $arguments
810
     *
811
     * @return string
812
     */
813 52
    private function createSingleNamedParameter(iterable $arguments): string
814
    {
815 52
        foreach ($arguments as $argument) {
816 51
            $paramName = $this->createNamedParameter($argument, $this->getPdoType($argument));
817
818 51
            return $paramName;
819
        }
820
821
        // arguments are empty
822 1
        throw new InvalidArgumentException();
823
    }
824
825
    /**
826
     * @param iterable $arguments
827
     *
828
     * @return string[]
829
     */
830 6
    private function createNamedParameterArray(iterable $arguments): array
831
    {
832 6
        $names = [];
833
834 6
        foreach ($arguments as $argument) {
835 6
            $names[] = $this->createNamedParameter($argument, $this->getPdoType($argument));
836
        }
837
838 6
        return $names;
839
    }
840
841
    /**
842
     * @return Closure
843
     */
844 52
    private function getColumnToDatabaseMapper(): Closure
845
    {
846 52
        return $this->columnMapper;
847
    }
848
849
    /**
850
     * @param mixed $value
851
     *
852
     * @return int
853
     *
854
     * @SuppressWarnings(PHPMD.ElseExpression)
855
     */
856 53
    private function getPdoType($value): int
857
    {
858 53
        if (is_int($value) === true) {
859 41
            $type = PDO::PARAM_INT;
860 28
        } elseif (is_bool($value)) {
861 1
            $type = PDO::PARAM_BOOL;
862
        } else {
863 27
            assert(
864 27
                $value !== null,
865
                'It seems you are trying to use `null` with =, >, <, or etc operator. ' .
866 27
                'Use `is null` or `not null` instead.'
867
            );
868 27
            assert(is_string($value), "Only strings, booleans and integers are supported.");
869 27
            $type = PDO::PARAM_STR;
870
        }
871
872 53
        return $type;
873
    }
874
}
875