Completed
Push — master ( 697864...c1ef57 )
by Neomerx
09:24
created

ModelQueryBuilder::getTableName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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