Completed
Push — master ( 6173d9...210649 )
by Neomerx
04:26
created

prepareDeleteInToManyRelationship()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

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