Completed
Push — master ( 2062de...fe7948 )
by Neomerx
11:49
created

ModelQueryBuilder::addSorts()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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