Completed
Push — develop ( 323919...323bfc )
by Neomerx
15:34 queued 11:51
created

ModelQueryBuilder::getModelColumns()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 16
cts 16
cp 1
rs 8.8977
c 0
b 0
f 0
cc 6
nc 4
nop 2
crap 6
1
<?php declare (strict_types = 1);
2
3
namespace Limoncello\Flute\Adapters;
4
5
/**
6
 * Copyright 2015-2019 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use Closure;
22
use DateTimeInterface;
23
use Doctrine\DBAL\Connection;
24
use Doctrine\DBAL\DBALException;
25
use Doctrine\DBAL\Query\Expression\CompositeExpression;
26
use Doctrine\DBAL\Query\QueryBuilder;
27
use Doctrine\DBAL\Types\DateTimeType;
28
use Doctrine\DBAL\Types\Type;
29
use Limoncello\Contracts\Data\ModelSchemaInfoInterface;
30
use Limoncello\Contracts\Data\RelationshipTypes;
31
use Limoncello\Flute\Contracts\Http\Query\FilterParameterInterface;
32
use Limoncello\Flute\Exceptions\InvalidArgumentException;
33
use PDO;
34
use function assert;
35
use function call_user_func;
36
use function is_array;
37
use function is_bool;
38
use function is_callable;
39
use function is_int;
40
use function is_iterable;
41
use function is_string;
42
43
/**
44
 * @package Limoncello\Flute
45
 *
46
 * @SuppressWarnings(PHPMD.TooManyMethods)
47
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
48
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
49
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
50
 */
51
class ModelQueryBuilder extends QueryBuilder
52
{
53
    /**
54
     * Condition joining method.
55
     */
56
    public const AND = 0;
57
58
    /**
59
     * Condition joining method.
60
     */
61
    public const OR = self::AND + 1;
62
63
    /**
64
     * @var string
65
     */
66
    private $modelClass;
67
68
    /**
69
     * @var string
70
     */
71
    private $mainTableName;
72
73
    /**
74
     * @var string
75
     */
76
    private $mainAlias;
77
78
    /**
79
     * @var Closure
80
     */
81
    private $columnMapper;
82
83
    /**
84
     * @var ModelSchemaInfoInterface
85
     */
86
    private $modelSchemas;
87
88
    /**
89
     * @var int
90
     */
91
    private $aliasIdCounter = 0;
92
93
    /**
94
     * @var array
95
     */
96
    private $knownAliases = [];
97
98
    /**
99
     * @var Type|null
100
     */
101
    private $dateTimeType;
102
103
    /**
104
     * @param Connection               $connection
105
     * @param string                   $modelClass
106
     * @param ModelSchemaInfoInterface $modelSchemas
107
     *
108
     * @SuppressWarnings(PHPMD.StaticAccess)
109
     */
110 66
    public function __construct(Connection $connection, string $modelClass, ModelSchemaInfoInterface $modelSchemas)
111
    {
112 66
        assert(!empty($modelClass));
113
114 66
        parent::__construct($connection);
115
116 66
        $this->modelSchemas = $modelSchemas;
117 66
        $this->modelClass   = $modelClass;
118
119 66
        $this->mainTableName = $this->getModelSchemas()->getTable($this->getModelClass());
120 66
        $this->mainAlias     = $this->createAlias($this->getTableName());
121
122 66
        $this->setColumnToDatabaseMapper(Closure::fromCallable([$this, 'quoteDoubleIdentifier']));
123
    }
124
125
    /**
126
     * @return string
127
     */
128 66
    public function getModelClass(): string
129
    {
130 66
        return $this->modelClass;
131
    }
132
133
    /**
134
     * @param string|null $tableAlias
135
     * @param string|null $modelClass
136
     *
137
     * @return array
138
     *
139
     * @throws DBALException
140
     */
141 52
    public function getModelColumns(string $tableAlias = null, string $modelClass = null): array
142
    {
143 52
        $modelClass = $modelClass ?? $this->getModelClass();
144 52
        $tableAlias = $tableAlias ?? $this->getAlias();
145
146 52
        $quotedColumns = [];
147
148 52
        $columnMapper    = $this->getColumnToDatabaseMapper();
149 52
        $selectedColumns = $this->getModelSchemas()->getAttributes($modelClass);
150 52
        foreach ($selectedColumns as $column) {
151 52
            $quotedColumns[] = call_user_func($columnMapper, $tableAlias, $column, $this);
152
        }
153
154 52
        $rawColumns = $this->getModelSchemas()->getRawAttributes($modelClass);
155 52
        if (empty($rawColumns) === false) {
156 1
            $platform = $this->getConnection()->getDatabasePlatform();
157 1
            foreach ($rawColumns as $columnOrCallable) {
158 1
                assert(is_string($columnOrCallable) === true || is_callable($columnOrCallable) === true);
159 1
                $quotedColumns[] = is_callable($columnOrCallable) === true ?
160 1
                    call_user_func($columnOrCallable, $tableAlias, $platform) : $columnOrCallable;
161
            }
162
        }
163
164 52
        return $quotedColumns;
165
    }
166
167
    /**
168
     * Select all fields associated with model.
169
     *
170
     * @param iterable|null $columns
171
     *
172
     * @return self
173
     *
174
     * @SuppressWarnings(PHPMD.ElseExpression)
175
     *
176
     * @throws DBALException
177
     */
178 57
    public function selectModelColumns(iterable $columns = null): self
179
    {
180 57
        if ($columns !== null) {
181 5
            $quotedColumns = [];
182 5
            foreach ($columns as $column) {
183 5
                $quotedColumns[] = $this->quoteDoubleIdentifier($this->getAlias(), $column);
184
            }
185
        } else {
186 52
            $quotedColumns = $this->getModelColumns();
187
        }
188
189 57
        $this->select($quotedColumns);
190
191 57
        return $this;
192
    }
193
194
    /**
195
     * @return self
196
     *
197
     * @throws DBALException
198
     */
199 14
    public function distinct(): self
200
    {
201
        // emulate SELECT DISTINCT with grouping by primary key
202 14
        $primaryColumn = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
203 14
        $this->addGroupBy($this->getQuotedMainAliasColumn($primaryColumn));
204
205 14
        return $this;
206
    }
207
208
    /**
209
     * @param Closure $columnMapper
210
     *
211
     * @return self
212
     */
213 66
    public function setColumnToDatabaseMapper(Closure $columnMapper): self
214
    {
215 66
        $this->columnMapper = $columnMapper;
216
217 66
        return $this;
218
    }
219
220
    /**
221
     * @return self
222
     *
223
     * @throws DBALException
224
     */
225 57
    public function fromModelTable(): self
226
    {
227 57
        $this->from(
228 57
            $this->quoteSingleIdentifier($this->getTableName()),
229 57
            $this->quoteSingleIdentifier($this->getAlias())
230
        );
231
232 57
        return $this;
233
    }
234
235
    /**
236
     * @param iterable $attributes
237
     *
238
     * @return self
239
     *
240
     * @throws DBALException
241
     *
242
     * @SuppressWarnings(PHPMD.StaticAccess)
243
     */
244 5
    public function createModel(iterable $attributes): self
245
    {
246 5
        $this->insert($this->quoteSingleIdentifier($this->getTableName()));
247
248 5
        $valuesAsParams = [];
249 5
        foreach ($this->bindAttributes($this->getModelClass(), $attributes) as $quotedColumn => $parameterName) {
250 5
            $valuesAsParams[$quotedColumn] = $parameterName;
251
        }
252 5
        $this->values($valuesAsParams);
253
254 5
        return $this;
255
    }
256
257
    /**
258
     * @param iterable $attributes
259
     *
260
     * @return self
261
     *
262
     * @throws DBALException
263
     *
264
     * @SuppressWarnings(PHPMD.StaticAccess)
265
     */
266 6
    public function updateModels(iterable $attributes): self
267
    {
268 6
        $this->update($this->quoteSingleIdentifier($this->getTableName()));
269
270 6
        foreach ($this->bindAttributes($this->getModelClass(), $attributes) as $quotedColumn => $parameterName) {
271 6
            $this->set($quotedColumn, $parameterName);
272
        }
273
274 6
        return $this;
275
    }
276
277
    /**
278
     * @param string   $modelClass
279
     * @param iterable $attributes
280
     *
281
     * @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...
282
     *
283
     * @SuppressWarnings(PHPMD.StaticAccess)
284
     *
285
     * @throws DBALException
286
     */
287 11
    public function bindAttributes(string $modelClass, iterable $attributes): iterable
288
    {
289 11
        $dbPlatform = $this->getConnection()->getDatabasePlatform();
290 11
        $types      = $this->getModelSchemas()->getAttributeTypes($modelClass);
291
292 11
        foreach ($attributes as $column => $value) {
293 11
            assert(is_string($column) && $this->getModelSchemas()->hasAttributeType($this->getModelClass(), $column));
294
295 11
            $quotedColumn  = $this->quoteSingleIdentifier($column);
296 11
            $type          = $this->getDbalType($types[$column]);
297 11
            $pdoValue      = $type->convertToDatabaseValue($value, $dbPlatform);
298 11
            $parameterName = $this->createNamedParameter($pdoValue, $type->getBindingType());
299
300 11
            yield $quotedColumn => $parameterName;
301
        }
302
    }
303
304
    /**
305
     * @return self
306
     *
307
     * @throws DBALException
308
     */
309 4
    public function deleteModels(): self
310
    {
311 4
        $this->delete($this->quoteSingleIdentifier($this->getTableName()));
312
313 4
        return $this;
314
    }
315
316
    /**
317
     * @param string $relationshipName
318
     * @param string $identity
319
     * @param string $secondaryIdBindName
320
     *
321
     * @return self
322
     *
323
     * @throws DBALException
324
     */
325 5
    public function prepareCreateInToManyRelationship(
326
        string $relationshipName,
327
        string $identity,
328
        string $secondaryIdBindName
329
    ): self {
330
        list ($intermediateTable, $primaryKey, $secondaryKey) =
331 5
            $this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName);
332
333
        $this
334 5
            ->insert($this->quoteSingleIdentifier($intermediateTable))
335 5
            ->values([
336 5
                $this->quoteSingleIdentifier($primaryKey)   => $this->createNamedParameter($identity),
337 5
                $this->quoteSingleIdentifier($secondaryKey) => $secondaryIdBindName,
338
            ]);
339
340 5
        return $this;
341
    }
342
343
    /**
344
     * @param string   $relationshipName
345
     * @param string   $identity
346
     * @param iterable $secondaryIds
347
     *
348
     * @return ModelQueryBuilder
349
     *
350
     * @throws DBALException
351
     */
352 1
    public function prepareDeleteInToManyRelationship(
353
        string $relationshipName,
354
        string $identity,
355
        iterable $secondaryIds
356
    ): self {
357
        list ($intermediateTable, $primaryKey, $secondaryKey) =
358 1
            $this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName);
359
360
        $filters = [
361 1
            $primaryKey   => [FilterParameterInterface::OPERATION_EQUALS => [$identity]],
362 1
            $secondaryKey => [FilterParameterInterface::OPERATION_IN     => $secondaryIds],
363
        ];
364
365 1
        $addWith = $this->expr()->andX();
366
        $this
367 1
            ->delete($this->quoteSingleIdentifier($intermediateTable))
368 1
            ->applyFilters($addWith, $intermediateTable, $filters);
0 ignored issues
show
Documentation introduced by
$filters is of type array<?,array<string|int...te\Adapters\iterable>>>, 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...
369
370 1
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
371
372 1
        return $this;
373
    }
374
375
    /**
376
     * @param string $relationshipName
377
     * @param string $identity
378
     *
379
     * @return self
380
     *
381
     * @throws DBALException
382
     */
383 2
    public function clearToManyRelationship(string $relationshipName, string $identity): self
384
    {
385
        list ($intermediateTable, $primaryKey) =
386 2
            $this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName);
387
388 2
        $filters = [$primaryKey => [FilterParameterInterface::OPERATION_EQUALS => [$identity]]];
389 2
        $addWith = $this->expr()->andX();
390
        $this
391 2
            ->delete($this->quoteSingleIdentifier($intermediateTable))
392 2
            ->applyFilters($addWith, $intermediateTable, $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...
393
394 2
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
395
396 2
        return $this;
397
    }
398
399
    /**
400
     * @param iterable $filters
401
     *
402
     * @return self
403
     *
404
     * @throws DBALException
405
     */
406 9
    public function addFiltersWithAndToTable(iterable $filters): self
407
    {
408 9
        $addWith = $this->expr()->andX();
409 9
        $this->applyFilters($addWith, $this->getTableName(), $filters);
410 9
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
411
412 9
        return $this;
413
    }
414
415
    /**
416
     * @param iterable $filters
417
     *
418
     * @return self
419
     *
420
     * @throws DBALException
421
     */
422 1
    public function addFiltersWithOrToTable(iterable $filters): self
423
    {
424 1
        $addWith = $this->expr()->orX();
425 1
        $this->applyFilters($addWith, $this->getTableName(), $filters);
426 1
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
427
428 1
        return $this;
429
    }
430
431
    /**
432
     * @param iterable $filters
433
     *
434
     * @return self
435
     *
436
     * @throws DBALException
437
     */
438 39
    public function addFiltersWithAndToAlias(iterable $filters): self
439
    {
440 39
        $addWith = $this->expr()->andX();
441 39
        $this->applyFilters($addWith, $this->getAlias(), $filters);
442 38
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
443
444 38
        return $this;
445
    }
446
447
    /**
448
     * @param iterable $filters
449
     *
450
     * @return self
451
     *
452
     * @throws DBALException
453
     */
454 2
    public function addFiltersWithOrToAlias(iterable $filters): self
455
    {
456 2
        $addWith = $this->expr()->orX();
457 2
        $this->applyFilters($addWith, $this->getAlias(), $filters);
458 2
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
459
460 2
        return $this;
461
    }
462
463
    /**
464
     * @param string        $relationshipName
465
     * @param iterable|null $relationshipFilters
466
     * @param iterable|null $relationshipSorts
467
     * @param int           $joinIndividuals
468
     * @param int           $joinRelationship
469
     *
470
     * @return self
471
     *
472
     * @throws DBALException
473
     *
474
     * @SuppressWarnings(PHPMD.ElseExpression)
475
     * @SuppressWarnings(PHPMD.NPathComplexity)
476
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
477
     */
478 29
    public function addRelationshipFiltersAndSorts(
479
        string $relationshipName,
480
        ?iterable $relationshipFilters,
481
        ?iterable $relationshipSorts,
482
        int $joinIndividuals = self::AND,
483
        int $joinRelationship = self::AND
484
    ): self {
485 29
        $targetAlias = null;
486
487 29
        if ($relationshipFilters !== null) {
488 29
            $isBelongsTo = $this->getModelSchemas()
489 29
                    ->getRelationshipType($this->getModelClass(), $relationshipName) === RelationshipTypes::BELONGS_TO;
490
491
            // it will have non-null value only in a `belongsTo` relationship
492 29
            $reversePk = $isBelongsTo === true ?
493 29
                $this->getModelSchemas()->getReversePrimaryKey($this->getModelClass(), $relationshipName)[0] : null;
494
495 29
            $addWith = $joinIndividuals === self::AND ? $this->expr()->andX() : $this->expr()->orX();
496
497 29
            foreach ($relationshipFilters as $columnName => $operationsWithArgs) {
498 28
                if ($columnName === $reversePk) {
499
                    // We are applying a filter to a primary key in `belongsTo` relationship
500
                    // It could be replaced with a filter to a value in main table. Why might we need it?
501
                    // Filter could be 'IS NULL' so joining a table will not work because there are no
502
                    // related records with 'NULL` key. For plain values it will produce shorter SQL.
503
                    $fkName         =
504 15
                        $this->getModelSchemas()->getForeignKey($this->getModelClass(), $relationshipName);
505 15
                    $fullColumnName = $this->getQuotedMainAliasColumn($fkName);
506
                } else {
507
                    // Will apply filters to a joined table.
508 19
                    $targetAlias    = $targetAlias ?: $this->createRelationshipAlias($relationshipName);
509 19
                    $fullColumnName = $this->quoteDoubleIdentifier($targetAlias, $columnName);
510
                }
511
512 28
                foreach ($operationsWithArgs as $operation => $arguments) {
513 28
                    assert(
514 28
                        is_iterable($arguments) === true || is_array($arguments) === true,
515 28
                        "Operation arguments are missing for `$columnName` column. " .
516 28
                        'Use an empty array as an empty argument list.'
517
                    );
518 28
                    $addWith->add($this->createFilterExpression($fullColumnName, $operation, $arguments));
519
                }
520
521 28
                if ($addWith->count() > 0) {
522 28
                    $joinRelationship === self::AND ? $this->andWhere($addWith) : $this->orWhere($addWith);
523
                }
524
            }
525
        }
526
527 29
        if ($relationshipSorts !== null) {
528 23
            foreach ($relationshipSorts as $columnName => $isAsc) {
529
                // we join the table only once and only if we have at least one 'sort' or non-belongsToPK filter.
530 8
                $targetAlias = $targetAlias ?: $this->createRelationshipAlias($relationshipName);
531
532 8
                assert(is_string($columnName) === true && is_bool($isAsc) === true);
533 8
                $fullColumnName = $this->quoteDoubleIdentifier($targetAlias, $columnName);
534 8
                $this->addOrderBy($fullColumnName, $isAsc === true ? 'ASC' : 'DESC');
535
            }
536
        }
537
538 29
        return $this;
539
    }
540
541
    /**
542
     * @param iterable $sortParameters
543
     *
544
     * @return self
545
     *
546
     * @throws DBALException
547
     */
548 15
    public function addSorts(iterable $sortParameters): self
549
    {
550 15
        return $this->applySorts($this->getAlias(), $sortParameters);
551
    }
552
553
    /**
554
     * @param string $column
555
     *
556
     * @return string
557
     *
558
     * @throws DBALException
559
     */
560 1
    public function getQuotedMainTableColumn(string $column): string
561
    {
562 1
        return $this->quoteDoubleIdentifier($this->getTableName(), $column);
563
    }
564
565
    /**
566
     * @param string $column
567
     *
568
     * @return string
569
     *
570
     * @throws DBALException
571
     */
572 23
    public function getQuotedMainAliasColumn(string $column): string
573
    {
574 23
        return $this->quoteDoubleIdentifier($this->getAlias(), $column);
575
    }
576
577
    /**
578
     * @param string $name
579
     *
580
     * @return string Table alias.
581
     *
582
     * @throws DBALException
583
     */
584 21
    public function createRelationshipAlias(string $name): string
585
    {
586 21
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
587
        switch ($relationshipType) {
588 21
            case RelationshipTypes::BELONGS_TO:
589
                list($targetColumn, $targetTable) =
590 4
                    $this->getModelSchemas()->getReversePrimaryKey($this->getModelClass(), $name);
591 4
                $targetAlias = $this->innerJoinOneTable(
592 4
                    $this->getAlias(),
593 4
                    $this->getModelSchemas()->getForeignKey($this->getModelClass(), $name),
594 4
                    $targetTable,
595 4
                    $targetColumn
596
                );
597 4
                break;
598
599 18
            case RelationshipTypes::HAS_MANY:
600
                list($targetColumn, $targetTable) =
601 13
                    $this->getModelSchemas()->getReverseForeignKey($this->getModelClass(), $name);
602 13
                $targetAlias = $this->innerJoinOneTable(
603 13
                    $this->getAlias(),
604 13
                    $this->getModelSchemas()->getPrimaryKey($this->getModelClass()),
605 13
                    $targetTable,
606 13
                    $targetColumn
607
                );
608 13
                break;
609
610 10
            case RelationshipTypes::BELONGS_TO_MANY:
611
            default:
612 10
                assert($relationshipType === RelationshipTypes::BELONGS_TO_MANY);
613 10
                $primaryKey = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
614
                list ($intermediateTable, $intermediatePk, $intermediateFk) =
615 10
                    $this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $name);
616
                list($targetPrimaryKey, $targetTable) =
617 10
                    $this->getModelSchemas()->getReversePrimaryKey($this->getModelClass(), $name);
618
619 10
                $targetAlias = $this->innerJoinTwoSequentialTables(
620 10
                    $this->getAlias(),
621 10
                    $primaryKey,
622 10
                    $intermediateTable,
623 10
                    $intermediatePk,
624 10
                    $intermediateFk,
625 10
                    $targetTable,
626 10
                    $targetPrimaryKey
627
                );
628 10
                break;
629
        }
630
631 21
        return $targetAlias;
632
    }
633
634
    /**
635
     * @return string
636
     */
637 58
    public function getAlias(): string
638
    {
639 58
        return $this->mainAlias;
640
    }
641
642
    /**
643
     * @param CompositeExpression $expression
644
     * @param string              $tableOrAlias
645
     * @param iterable            $filters
646
     *
647
     * @return self
648
     *
649
     * @throws DBALException
650
     * @throws InvalidArgumentException
651
     */
652 46
    public function applyFilters(CompositeExpression $expression, string $tableOrAlias, iterable $filters): self
653
    {
654 46
        foreach ($filters as $columnName => $operationsWithArgs) {
655 46
            assert(
656 46
                is_string($columnName) === true && empty($columnName) === false,
657 46
                "Haven't you forgotten to specify a column name in a relationship that joins `$tableOrAlias` table?"
658
            );
659 46
            $fullColumnName = $this->quoteDoubleIdentifier($tableOrAlias, $columnName);
660 46
            foreach ($operationsWithArgs as $operation => $arguments) {
661 46
                assert(
662 46
                    is_iterable($arguments) === true || is_array($arguments) === true,
663 46
                    "Operation arguments are missing for `$columnName` column. " .
664 46
                    'Use an empty array as an empty argument list.'
665
                );
666 46
                $expression->add($this->createFilterExpression($fullColumnName, $operation, $arguments));
667
            }
668
        }
669
670 45
        return $this;
671
    }
672
673
    /**
674
     * @param string   $tableOrAlias
675
     * @param iterable $sorts
676
     *
677
     * @return self
678
     *
679
     * @throws DBALException
680
     */
681 15
    public function applySorts(string $tableOrAlias, iterable $sorts): self
682
    {
683 15
        foreach ($sorts as $columnName => $isAsc) {
684 15
            assert(is_string($columnName) === true && is_bool($isAsc) === true);
685 15
            $fullColumnName = $this->quoteDoubleIdentifier($tableOrAlias, $columnName);
686 15
            $this->addOrderBy($fullColumnName, $isAsc === true ? 'ASC' : 'DESC');
687
        }
688
689 15
        return $this;
690
    }
691
692
    /**
693
     * @param string $tableOrColumn
694
     *
695
     * @return string
696
     *
697
     * @throws DBALException
698
     */
699 65
    public function quoteSingleIdentifier(string $tableOrColumn): string
700
    {
701 65
        return $this->getConnection()->getDatabasePlatform()->quoteSingleIdentifier($tableOrColumn);
702
    }
703
704
    /**
705
     * @param string $tableOrAlias
706
     * @param string $column
707
     *
708
     * @return string
709
     *
710
     * @throws DBALException
711
     */
712 64
    public function quoteDoubleIdentifier(string $tableOrAlias, string $column): string
713
    {
714 64
        $platform = $this->getConnection()->getDatabasePlatform();
715
716 64
        return $platform->quoteSingleIdentifier($tableOrAlias) . '.' . $platform->quoteSingleIdentifier($column);
717
    }
718
719
    /**
720
     * @param $value
721
     *
722
     * @return string
723
     *
724
     * @throws DBALException
725
     */
726 58
    public function createSingleValueNamedParameter($value): string
727
    {
728 58
        $paramName = $this->createNamedParameter($this->getPdoValue($value), $this->getPdoType($value));
729
730 58
        return $paramName;
731
    }
732
733
    /**
734
     * @param iterable $values
735
     *
736
     * @return array
737
     *
738
     * @throws DBALException
739
     */
740 18
    public function createArrayValuesNamedParameter(iterable $values): array
741
    {
742 18
        $names = [];
743
744 18
        foreach ($values as $value) {
745 18
            $names[] = $this->createSingleValueNamedParameter($value);
746
        }
747
748 18
        return $names;
749
    }
750
751
    /**
752
     * @param string $tableName
753
     *
754
     * @return string
755
     */
756 66
    public function createAlias(string $tableName): string
757
    {
758 66
        $alias                          = $tableName . (++$this->aliasIdCounter);
759 66
        $this->knownAliases[$tableName] = $alias;
760
761 66
        return $alias;
762
    }
763
764
    /**
765
     * @param string $fromAlias
766
     * @param string $fromColumn
767
     * @param string $targetTable
768
     * @param string $targetColumn
769
     *
770
     * @return string
771
     *
772
     * @throws DBALException
773
     */
774 21
    public function innerJoinOneTable(
775
        string $fromAlias,
776
        string $fromColumn,
777
        string $targetTable,
778
        string $targetColumn
779
    ): string {
780 21
        $targetAlias   = $this->createAlias($targetTable);
781 21
        $joinCondition = $this->quoteDoubleIdentifier($fromAlias, $fromColumn) . '=' .
782 21
            $this->quoteDoubleIdentifier($targetAlias, $targetColumn);
783
784 21
        $this->innerJoin(
785 21
            $this->quoteSingleIdentifier($fromAlias),
786 21
            $this->quoteSingleIdentifier($targetTable),
787 21
            $this->quoteSingleIdentifier($targetAlias),
788 21
            $joinCondition
789
        );
790
791 21
        return $targetAlias;
792
    }
793
794
    /**
795
     * @param string $fromAlias
796
     * @param string $fromColumn
797
     * @param string $intTable
798
     * @param string $intToFromColumn
799
     * @param string $intToTargetColumn
800
     * @param string $targetTable
801
     * @param string $targetColumn
802
     *
803
     * @return string
804
     *
805
     * @throws DBALException
806
     */
807 10
    public function innerJoinTwoSequentialTables(
808
        string $fromAlias,
809
        string $fromColumn,
810
        string $intTable,
811
        string $intToFromColumn,
812
        string $intToTargetColumn,
813
        string $targetTable,
814
        string $targetColumn
815
    ): string {
816 10
        $intAlias    = $this->innerJoinOneTable($fromAlias, $fromColumn, $intTable, $intToFromColumn);
817 10
        $targetAlias = $this->innerJoinOneTable($intAlias, $intToTargetColumn, $targetTable, $targetColumn);
818
819 10
        return $targetAlias;
820
    }
821
822
    /**
823
     * @param string $name
824
     *
825
     * @return Type
826
     *
827
     * @throws DBALException
828
     *
829
     * @SuppressWarnings(PHPMD.StaticAccess)
830
     */
831 11
    protected function getDbalType(string $name): Type
832
    {
833 11
        assert(Type::hasType($name), "Type `$name` either do not exist or registered.");
834 11
        $type = Type::getType($name);
835
836 11
        return $type;
837
    }
838
839
    /**
840
     * @return string
841
     */
842 66
    private function getTableName(): string
843
    {
844 66
        return $this->mainTableName;
845
    }
846
847
    /**
848
     * @return ModelSchemaInfoInterface
849
     */
850 66
    private function getModelSchemas(): ModelSchemaInfoInterface
851
    {
852 66
        return $this->modelSchemas;
853
    }
854
855
    /**
856
     * @param string   $fullColumnName
857
     * @param int      $operation
858
     * @param iterable $arguments
859
     *
860
     * @return string
861
     *
862
     * @throws DBALException
863
     * @throws InvalidArgumentException
864
     *
865
     * @SuppressWarnings(PHPMD.StaticAccess)
866
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
867
     */
868 59
    private function createFilterExpression(string $fullColumnName, int $operation, iterable $arguments): string
869
    {
870
        switch ($operation) {
871 59
            case FilterParameterInterface::OPERATION_EQUALS:
872 48
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
873 47
                $expression = $this->expr()->eq($fullColumnName, $parameter);
874 47
                break;
875 30
            case FilterParameterInterface::OPERATION_NOT_EQUALS:
876 1
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
877 1
                $expression = $this->expr()->neq($fullColumnName, $parameter);
878 1
                break;
879 30
            case FilterParameterInterface::OPERATION_LESS_THAN:
880 6
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
881 6
                $expression = $this->expr()->lt($fullColumnName, $parameter);
882 6
                break;
883 30
            case FilterParameterInterface::OPERATION_LESS_OR_EQUALS:
884 7
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
885 7
                $expression = $this->expr()->lte($fullColumnName, $parameter);
886 7
                break;
887 29
            case FilterParameterInterface::OPERATION_GREATER_THAN:
888 2
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
889 2
                $expression = $this->expr()->gt($fullColumnName, $parameter);
890 2
                break;
891 28
            case FilterParameterInterface::OPERATION_GREATER_OR_EQUALS:
892 6
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
893 6
                $expression = $this->expr()->gte($fullColumnName, $parameter);
894 6
                break;
895 23
            case FilterParameterInterface::OPERATION_LIKE:
896 9
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
897 9
                $expression = $this->expr()->like($fullColumnName, $parameter);
898 9
                break;
899 18
            case FilterParameterInterface::OPERATION_NOT_LIKE:
900 1
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
901 1
                $expression = $this->expr()->notLike($fullColumnName, $parameter);
902 1
                break;
903 18
            case FilterParameterInterface::OPERATION_IN:
904 18
                $parameters = $this->createArrayValuesNamedParameter($arguments);
905 18
                $expression = $this->expr()->in($fullColumnName, $parameters);
906 18
                break;
907 1
            case FilterParameterInterface::OPERATION_NOT_IN:
908 1
                $parameters = $this->createArrayValuesNamedParameter($arguments);
909 1
                $expression = $this->expr()->notIn($fullColumnName, $parameters);
910 1
                break;
911 1
            case FilterParameterInterface::OPERATION_IS_NULL:
912 1
                $expression = $this->expr()->isNull($fullColumnName);
913 1
                break;
914 1
            case FilterParameterInterface::OPERATION_IS_NOT_NULL:
915
            default:
916 1
                assert($operation === FilterParameterInterface::OPERATION_IS_NOT_NULL);
917 1
                $expression = $this->expr()->isNotNull($fullColumnName);
918 1
                break;
919
        }
920
921 58
        return $expression;
922
    }
923
924
    /**
925
     * @param iterable $arguments
926
     *
927
     * @return mixed
928
     *
929
     * @throws InvalidArgumentException
930
     */
931 55
    private function firstValue(iterable $arguments)
932
    {
933 55
        foreach ($arguments as $argument) {
934 54
            return $argument;
935
        }
936
937
        // arguments are empty
938 1
        throw new InvalidArgumentException();
939
    }
940
941
    /**
942
     * @return Closure
943
     */
944 52
    private function getColumnToDatabaseMapper(): Closure
945
    {
946 52
        return $this->columnMapper;
947
    }
948
949
    /**
950
     * @param mixed $value
951
     *
952
     * @return mixed
953
     *
954
     * @throws DBALException
955
     */
956 58
    private function getPdoValue($value)
957
    {
958 58
        return $value instanceof DateTimeInterface ? $this->convertDataTimeToDatabaseFormat($value) : $value;
959
    }
960
961
    /**
962
     * @param DateTimeInterface $dateTime
963
     *
964
     * @return string
965
     *
966
     * @throws DBALException
967
     */
968 1
    private function convertDataTimeToDatabaseFormat(DateTimeInterface $dateTime): string
969
    {
970 1
        return $this->getDateTimeType()->convertToDatabaseValue(
971 1
            $dateTime,
972 1
            $this->getConnection()->getDatabasePlatform()
973
        );
974
    }
975
976
    /**
977
     * @param mixed $value
978
     *
979
     * @return int
980
     *
981
     * @SuppressWarnings(PHPMD.ElseExpression)
982
     */
983 58
    private function getPdoType($value): int
984
    {
985 58
        if (is_int($value) === true) {
986 37
            $type = PDO::PARAM_INT;
987 38
        } elseif (is_bool($value)) {
988 1
            $type = PDO::PARAM_BOOL;
989 37
        } elseif ($value instanceof DateTimeInterface) {
990 1
            $type = PDO::PARAM_STR;
991
        } else {
992 36
            assert(
993 36
                $value !== null,
994
                'It seems you are trying to use `null` with =, >, <, or etc operator. ' .
995 36
                'Use `is null` or `not null` instead.'
996
            );
997 36
            assert(is_string($value), "Only strings, booleans and integers are supported.");
998 36
            $type = PDO::PARAM_STR;
999
        }
1000
1001 58
        return $type;
1002
    }
1003
1004
    /**
1005
     * @return Type
1006
     *
1007
     * @throws DBALException
1008
     *
1009
     * @SuppressWarnings(PHPMD.StaticAccess)
1010
     */
1011 1
    private function getDateTimeType(): Type
1012
    {
1013 1
        if ($this->dateTimeType === null) {
1014 1
            $this->dateTimeType = Type::getType(DateTimeType::DATETIME);
1015
        }
1016
1017 1
        return $this->dateTimeType;
1018
    }
1019
}
1020