Completed
Push — develop ( fe7948...dcfc04 )
by Neomerx
04:48 queued 02:36
created

ModelQueryBuilder::setColumnToDatabaseMapper()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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