ModelQueryBuilder   F
last analyzed

Complexity

Total Complexity 104

Size/Duplication

Total Lines 969
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 104
lcom 1
cbo 8
dl 0
loc 969
ccs 318
cts 318
cp 1
rs 1.631
c 0
b 0
f 0

43 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 1
A getModelClass() 0 4 1
B getModelColumns() 0 25 6
A selectModelColumns() 0 15 3
A distinct() 0 8 1
A setColumnToDatabaseMapper() 0 6 1
A fromModelTable() 0 9 1
A createModel() 0 12 2
A updateModels() 0 10 2
A bindAttributes() 0 16 3
A deleteModels() 0 6 1
A prepareCreateInToManyRelationship() 0 17 1
A prepareDeleteInToManyRelationship() 0 22 2
A clearToManyRelationship() 0 15 2
A addFiltersWithAndToTable() 0 8 2
A addFiltersWithOrToTable() 0 8 2
A addFiltersWithAndToAlias() 0 8 2
A addFiltersWithOrToAlias() 0 8 2
C addRelationshipFiltersAndSorts() 0 62 16
A addSorts() 0 4 1
A getQuotedMainTableColumn() 0 4 1
A getQuotedMainAliasColumn() 0 4 1
A createRelationshipAlias() 0 49 4
A getAlias() 0 4 1
A applyFilters() 0 20 5
A applySorts() 0 10 4
A quoteSingleIdentifier() 0 4 1
A quoteDoubleIdentifier() 0 6 1
A createSingleValueNamedParameter() 0 6 1
A createArrayValuesNamedParameter() 0 10 2
A createAlias() 0 7 1
A innerJoinOneTable() 0 19 1
A innerJoinTwoSequentialTables() 0 14 1
A getDbalType() 0 7 1
A getTableName() 0 4 1
A getModelSchemas() 0 4 1
C createFilterExpression() 0 55 13
A firstValue() 0 9 2
A getColumnToDatabaseMapper() 0 4 1
A getPdoValue() 0 4 2
A convertDataTimeToDatabaseFormat() 0 7 1
A getPdoType() 0 20 4
A getDateTimeType() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like ModelQueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ModelQueryBuilder, and based on these observations, apply Extract Interface, too.

1
<?php declare (strict_types = 1);
2
3
namespace Limoncello\Flute\Adapters;
4
5
/**
6
 * Copyright 2015-2019 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use Closure;
22
use DateTimeInterface;
23
use Doctrine\DBAL\Connection;
24
use Doctrine\DBAL\DBALException;
25
use Doctrine\DBAL\Query\Expression\CompositeExpression;
26
use Doctrine\DBAL\Query\QueryBuilder;
27
use Doctrine\DBAL\Types\DateTimeType;
28
use Doctrine\DBAL\Types\Type;
29
use Limoncello\Contracts\Data\ModelSchemaInfoInterface;
30
use Limoncello\Contracts\Data\RelationshipTypes;
31
use Limoncello\Flute\Contracts\Http\Query\FilterParameterInterface;
32
use Limoncello\Flute\Exceptions\InvalidArgumentException;
33
use PDO;
34
use function assert;
35
use function call_user_func;
36
use function is_array;
37
use function is_bool;
38
use function is_callable;
39
use function is_int;
40
use function is_iterable;
41
use function is_string;
42
43
/**
44
 * @package Limoncello\Flute
45
 *
46
 * @SuppressWarnings(PHPMD.TooManyMethods)
47
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
48
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
49
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
50
 */
51
class ModelQueryBuilder extends QueryBuilder
52
{
53
    /**
54
     * Condition joining method.
55
     */
56
    public const AND = 0;
57
58
    /**
59
     * Condition joining method.
60
     */
61
    public const OR = self::AND + 1;
62
63
    /**
64
     * @var string
65
     */
66
    private $modelClass;
67
68
    /**
69
     * @var string
70
     */
71
    private $mainTableName;
72
73
    /**
74
     * @var string
75
     */
76
    private $mainAlias;
77
78
    /**
79
     * @var Closure
80
     */
81
    private $columnMapper;
82
83
    /**
84
     * @var ModelSchemaInfoInterface
85
     */
86
    private $modelSchemas;
87
88
    /**
89
     * @var int
90
     */
91
    private $aliasIdCounter = 0;
92
93
    /**
94
     * @var array
95
     */
96
    private $knownAliases = [];
97
98
    /**
99
     * @var Type|null
100
     */
101
    private $dateTimeType;
102 66
103
    /**
104 66
     * @param Connection               $connection
105
     * @param string                   $modelClass
106 66
     * @param ModelSchemaInfoInterface $modelSchemas
107
     *
108 66
     * @SuppressWarnings(PHPMD.StaticAccess)
109 66
     */
110
    public function __construct(Connection $connection, string $modelClass, ModelSchemaInfoInterface $modelSchemas)
111 66
    {
112 66
        assert(!empty($modelClass));
113
114 66
        parent::__construct($connection);
115
116
        $this->modelSchemas = $modelSchemas;
117
        $this->modelClass   = $modelClass;
118
119
        $this->mainTableName = $this->getModelSchemas()->getTable($this->getModelClass());
120 66
        $this->mainAlias     = $this->createAlias($this->getTableName());
121
122 66
        $this->setColumnToDatabaseMapper(Closure::fromCallable([$this, 'quoteDoubleIdentifier']));
123
    }
124
125
    /**
126
     * @return string
127
     */
128
    public function getModelClass(): string
129
    {
130
        return $this->modelClass;
131 52
    }
132
133 52
    /**
134 52
     * @param string|null $tableAlias
135
     * @param string|null $modelClass
136 52
     *
137
     * @return array
138 52
     *
139 52
     * @throws DBALException
140 52
     */
141 52
    public function getModelColumns(string $tableAlias = null, string $modelClass = null): array
142
    {
143
        $modelClass = $modelClass ?? $this->getModelClass();
144 52
        $tableAlias = $tableAlias ?? $this->getAlias();
145 52
146 1
        $quotedColumns = [];
147 1
148
        $columnMapper    = $this->getColumnToDatabaseMapper();
149
        $selectedColumns = $this->getModelSchemas()->getAttributes($modelClass);
150 52
        foreach ($selectedColumns as $column) {
151
            $quotedColumns[] = call_user_func($columnMapper, $tableAlias, $column, $this);
152
        }
153
154
        $rawColumns = $this->getModelSchemas()->getRawAttributes($modelClass);
155
        if (empty($rawColumns) === false) {
156
            $platform = $this->getConnection()->getDatabasePlatform();
157
            foreach ($rawColumns as $columnOrCallable) {
158
                assert(is_string($columnOrCallable) === true || is_callable($columnOrCallable) === true);
159
                $quotedColumns[] = is_callable($columnOrCallable) === true ?
160
                    call_user_func($columnOrCallable, $tableAlias, $platform) : $columnOrCallable;
161
            }
162
        }
163
164 57
        return $quotedColumns;
165
    }
166 57
167 5
    /**
168 5
     * Select all fields associated with model.
169 5
     *
170
     * @param iterable|null $columns
171
     *
172 52
     * @return self
173
     *
174
     * @SuppressWarnings(PHPMD.ElseExpression)
175 57
     *
176
     * @throws DBALException
177 57
     */
178
    public function selectModelColumns(iterable $columns = null): self
179
    {
180
        if ($columns !== null) {
181
            $quotedColumns = [];
182
            foreach ($columns as $column) {
183
                $quotedColumns[] = $this->quoteDoubleIdentifier($this->getAlias(), $column);
184
            }
185 14
        } else {
186
            $quotedColumns = $this->getModelColumns();
187
        }
188 14
189 14
        $this->select($quotedColumns);
190
191 14
        return $this;
192
    }
193
194
    /**
195
     * @return self
196
     *
197
     * @throws DBALException
198
     */
199 66
    public function distinct(): self
200
    {
201 66
        // emulate SELECT DISTINCT with grouping by primary key
202
        $primaryColumn = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
203 66
        $this->addGroupBy($this->getQuotedMainAliasColumn($primaryColumn));
204
205
        return $this;
206
    }
207
208
    /**
209
     * @param Closure $columnMapper
210
     *
211 57
     * @return self
212
     */
213 57
    public function setColumnToDatabaseMapper(Closure $columnMapper): self
214 57
    {
215 57
        $this->columnMapper = $columnMapper;
216
217
        return $this;
218 57
    }
219
220
    /**
221
     * @return self
222
     *
223
     * @throws DBALException
224
     */
225
    public function fromModelTable(): self
226
    {
227
        $this->from(
228
            $this->quoteSingleIdentifier($this->getTableName()),
229
            $this->quoteSingleIdentifier($this->getAlias())
230 5
        );
231
232 5
        return $this;
233
    }
234 5
235 5
    /**
236 5
     * @param iterable $attributes
237
     *
238 5
     * @return self
239
     *
240 5
     * @throws DBALException
241
     *
242
     * @SuppressWarnings(PHPMD.StaticAccess)
243
     */
244
    public function createModel(iterable $attributes): self
245
    {
246
        $this->insert($this->quoteSingleIdentifier($this->getTableName()));
247
248
        $valuesAsParams = [];
249
        foreach ($this->bindAttributes($this->getModelClass(), $attributes) as $quotedColumn => $parameterName) {
250
            $valuesAsParams[$quotedColumn] = $parameterName;
251
        }
252 6
        $this->values($valuesAsParams);
253
254 6
        return $this;
255
    }
256 6
257 6
    /**
258
     * @param iterable $attributes
259
     *
260 6
     * @return self
261
     *
262
     * @throws DBALException
263
     *
264
     * @SuppressWarnings(PHPMD.StaticAccess)
265
     */
266
    public function updateModels(iterable $attributes): self
267
    {
268
        $this->update($this->quoteSingleIdentifier($this->getTableName()));
269
270
        foreach ($this->bindAttributes($this->getModelClass(), $attributes) as $quotedColumn => $parameterName) {
271
            $this->set($quotedColumn, $parameterName);
272
        }
273 11
274
        return $this;
275 11
    }
276 11
277
    /**
278 11
     * @param string   $modelClass
279 11
     * @param iterable $attributes
280
     *
281 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...
282 11
     *
283 11
     * @SuppressWarnings(PHPMD.StaticAccess)
284 11
     *
285
     * @throws DBALException
286 11
     */
287
    public function bindAttributes(string $modelClass, iterable $attributes): iterable
288
    {
289
        $dbPlatform = $this->getConnection()->getDatabasePlatform();
290
        $types      = $this->getModelSchemas()->getAttributeTypes($modelClass);
291
292
        foreach ($attributes as $column => $value) {
293
            assert(is_string($column) && $this->getModelSchemas()->hasAttributeType($this->getModelClass(), $column));
294
295 4
            $quotedColumn  = $this->quoteSingleIdentifier($column);
296
            $type          = $this->getDbalType($types[$column]);
297 4
            $pdoValue      = $type->convertToDatabaseValue($value, $dbPlatform);
298
            $parameterName = $this->createNamedParameter($pdoValue, $type->getBindingType());
299 4
300
            yield $quotedColumn => $parameterName;
301
        }
302
    }
303
304
    /**
305
     * @return self
306
     *
307
     * @throws DBALException
308
     */
309
    public function deleteModels(): self
310
    {
311 5
        $this->delete($this->quoteSingleIdentifier($this->getTableName()));
312
313
        return $this;
314
    }
315
316
    /**
317 5
     * @param string $relationshipName
318
     * @param string $identity
319
     * @param string $secondaryIdBindName
320 5
     *
321 5
     * @return self
322 5
     *
323 5
     * @throws DBALException
324
     */
325
    public function prepareCreateInToManyRelationship(
326 5
        string $relationshipName,
327
        string $identity,
328
        string $secondaryIdBindName
329
    ): self {
330
        list ($intermediateTable, $primaryKey, $secondaryKey) =
331
            $this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName);
332
333
        $this
334
            ->insert($this->quoteSingleIdentifier($intermediateTable))
335
            ->values([
336
                $this->quoteSingleIdentifier($primaryKey)   => $this->createNamedParameter($identity),
337
                $this->quoteSingleIdentifier($secondaryKey) => $secondaryIdBindName,
338 1
            ]);
339
340
        return $this;
341
    }
342
343
    /**
344 1
     * @param string   $relationshipName
345
     * @param string   $identity
346
     * @param iterable $secondaryIds
347 1
     *
348 1
     * @return ModelQueryBuilder
349
     *
350
     * @throws DBALException
351 1
     */
352
    public function prepareDeleteInToManyRelationship(
353 1
        string $relationshipName,
354 1
        string $identity,
355
        iterable $secondaryIds
356 1
    ): self {
357
        list ($intermediateTable, $primaryKey, $secondaryKey) =
358 1
            $this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName);
359
360
        $filters = [
361
            $primaryKey   => [FilterParameterInterface::OPERATION_EQUALS => [$identity]],
362
            $secondaryKey => [FilterParameterInterface::OPERATION_IN     => $secondaryIds],
363
        ];
364
365
        $addWith = $this->expr()->andX();
366
        $this
367
            ->delete($this->quoteSingleIdentifier($intermediateTable))
368
            ->applyFilters($addWith, $intermediateTable, $filters);
0 ignored issues
show
Documentation introduced by
$filters is of type array<?,array<string|int...te\Adapters\iterable>>>, but the function expects a object<Limoncello\Flute\Adapters\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
369 2
370
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
371
372 2
        return $this;
373
    }
374 2
375 2
    /**
376
     * @param string $relationshipName
377 2
     * @param string $identity
378 2
     *
379
     * @return self
380 2
     *
381
     * @throws DBALException
382 2
     */
383
    public function clearToManyRelationship(string $relationshipName, string $identity): self
384
    {
385
        list ($intermediateTable, $primaryKey) =
386
            $this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $relationshipName);
387
388
        $filters = [$primaryKey => [FilterParameterInterface::OPERATION_EQUALS => [$identity]]];
389
        $addWith = $this->expr()->andX();
390
        $this
391
            ->delete($this->quoteSingleIdentifier($intermediateTable))
392 9
            ->applyFilters($addWith, $intermediateTable, $filters);
0 ignored issues
show
Documentation introduced by
$filters is of type array<?,array<string|int...tring,{"0":"string"}>>>, but the function expects a object<Limoncello\Flute\Adapters\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
393
394 9
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
395 9
396 9
        return $this;
397
    }
398 9
399
    /**
400
     * @param iterable $filters
401
     *
402
     * @return self
403
     *
404
     * @throws DBALException
405
     */
406
    public function addFiltersWithAndToTable(iterable $filters): self
407
    {
408 1
        $addWith = $this->expr()->andX();
409
        $this->applyFilters($addWith, $this->getTableName(), $filters);
410 1
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
411 1
412 1
        return $this;
413
    }
414 1
415
    /**
416
     * @param iterable $filters
417
     *
418
     * @return self
419
     *
420
     * @throws DBALException
421
     */
422
    public function addFiltersWithOrToTable(iterable $filters): self
423
    {
424 39
        $addWith = $this->expr()->orX();
425
        $this->applyFilters($addWith, $this->getTableName(), $filters);
426 39
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
427 39
428 38
        return $this;
429
    }
430 38
431
    /**
432
     * @param iterable $filters
433
     *
434
     * @return self
435
     *
436
     * @throws DBALException
437
     */
438
    public function addFiltersWithAndToAlias(iterable $filters): self
439
    {
440 2
        $addWith = $this->expr()->andX();
441
        $this->applyFilters($addWith, $this->getAlias(), $filters);
442 2
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
443 2
444 2
        return $this;
445
    }
446 2
447
    /**
448
     * @param iterable $filters
449
     *
450
     * @return self
451
     *
452
     * @throws DBALException
453
     */
454
    public function addFiltersWithOrToAlias(iterable $filters): self
455
    {
456
        $addWith = $this->expr()->orX();
457
        $this->applyFilters($addWith, $this->getAlias(), $filters);
458
        $addWith->count() <= 0 ?: $this->andWhere($addWith);
459
460
        return $this;
461
    }
462
463
    /**
464 29
     * @param string        $relationshipName
465
     * @param iterable|null $relationshipFilters
466
     * @param iterable|null $relationshipSorts
467
     * @param int           $joinIndividuals
468
     * @param int           $joinRelationship
469
     *
470
     * @return self
471 29
     *
472
     * @throws DBALException
473 29
     *
474 29
     * @SuppressWarnings(PHPMD.ElseExpression)
475 29
     * @SuppressWarnings(PHPMD.NPathComplexity)
476
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
477
     */
478 29
    public function addRelationshipFiltersAndSorts(
479 29
        string $relationshipName,
480
        ?iterable $relationshipFilters,
481 29
        ?iterable $relationshipSorts,
482
        int $joinIndividuals = self::AND,
483 29
        int $joinRelationship = self::AND
484 28
    ): self {
485
        $targetAlias = null;
486
487
        if ($relationshipFilters !== null) {
488
            $isBelongsTo = $this->getModelSchemas()
489
                    ->getRelationshipType($this->getModelClass(), $relationshipName) === RelationshipTypes::BELONGS_TO;
490 15
491 15
            // it will have non-null value only in a `belongsTo` relationship
492
            $reversePk = $isBelongsTo === true ?
493
                $this->getModelSchemas()->getReversePrimaryKey($this->getModelClass(), $relationshipName)[0] : null;
494 19
495 19
            $addWith = $joinIndividuals === self::AND ? $this->expr()->andX() : $this->expr()->orX();
496
497
            foreach ($relationshipFilters as $columnName => $operationsWithArgs) {
498 28
                if ($columnName === $reversePk) {
499 28
                    // We are applying a filter to a primary key in `belongsTo` relationship
500 28
                    // It could be replaced with a filter to a value in main table. Why might we need it?
501 28
                    // Filter could be 'IS NULL' so joining a table will not work because there are no
502 28
                    // related records with 'NULL` key. For plain values it will produce shorter SQL.
503
                    $fkName         =
504 28
                        $this->getModelSchemas()->getForeignKey($this->getModelClass(), $relationshipName);
505
                    $fullColumnName = $this->getQuotedMainAliasColumn($fkName);
506
                } else {
507 28
                    // Will apply filters to a joined table.
508 28
                    $targetAlias    = $targetAlias ?: $this->createRelationshipAlias($relationshipName);
509
                    $fullColumnName = $this->quoteDoubleIdentifier($targetAlias, $columnName);
510
                }
511
512
                foreach ($operationsWithArgs as $operation => $arguments) {
513 29
                    assert(
514 23
                        is_iterable($arguments) === true || is_array($arguments) === true,
515
                        "Operation arguments are missing for `$columnName` column. " .
516 8
                        'Use an empty array as an empty argument list.'
517
                    );
518 8
                    $addWith->add($this->createFilterExpression($fullColumnName, $operation, $arguments));
519 8
                }
520 8
521
                if ($addWith->count() > 0) {
522
                    $joinRelationship === self::AND ? $this->andWhere($addWith) : $this->orWhere($addWith);
523
                }
524 29
            }
525
        }
526
527
        if ($relationshipSorts !== null) {
528
            foreach ($relationshipSorts as $columnName => $isAsc) {
529
                // we join the table only once and only if we have at least one 'sort' or non-belongsToPK filter.
530
                $targetAlias = $targetAlias ?: $this->createRelationshipAlias($relationshipName);
531
532
                assert(is_string($columnName) === true && is_bool($isAsc) === true);
533
                $fullColumnName = $this->quoteDoubleIdentifier($targetAlias, $columnName);
534 8
                $this->addOrderBy($fullColumnName, $isAsc === true ? 'ASC' : 'DESC');
535
            }
536 8
        }
537
538
        return $this;
539
    }
540
541
    /**
542
     * @param iterable $sortParameters
543
     *
544
     * @return self
545
     *
546 1
     * @throws DBALException
547
     */
548 1
    public function addSorts(iterable $sortParameters): self
549
    {
550
        return $this->applySorts($this->getAlias(), $sortParameters);
551
    }
552
553
    /**
554
     * @param string $column
555
     *
556
     * @return string
557
     *
558 23
     * @throws DBALException
559
     */
560 23
    public function getQuotedMainTableColumn(string $column): string
561
    {
562
        return $this->quoteDoubleIdentifier($this->getTableName(), $column);
563
    }
564
565
    /**
566
     * @param string $column
567
     *
568
     * @return string
569
     *
570 21
     * @throws DBALException
571
     */
572 21
    public function getQuotedMainAliasColumn(string $column): string
573
    {
574 21
        return $this->quoteDoubleIdentifier($this->getAlias(), $column);
575
    }
576 4
577 4
    /**
578 4
     * @param string $name
579 4
     *
580 4
     * @return string Table alias.
581 4
     *
582
     * @throws DBALException
583 4
     */
584
    public function createRelationshipAlias(string $name): string
585 18
    {
586
        $relationshipType = $this->getModelSchemas()->getRelationshipType($this->getModelClass(), $name);
587 13
        switch ($relationshipType) {
588 13
            case RelationshipTypes::BELONGS_TO:
589 13
                list($targetColumn, $targetTable) =
590 13
                    $this->getModelSchemas()->getReversePrimaryKey($this->getModelClass(), $name);
591 13
                $targetAlias = $this->innerJoinOneTable(
592 13
                    $this->getAlias(),
593
                    $this->getModelSchemas()->getForeignKey($this->getModelClass(), $name),
594 13
                    $targetTable,
595
                    $targetColumn
596 10
                );
597
                break;
598 10
599 10
            case RelationshipTypes::HAS_MANY:
600
                list($targetColumn, $targetTable) =
601 10
                    $this->getModelSchemas()->getReverseForeignKey($this->getModelClass(), $name);
602
                $targetAlias = $this->innerJoinOneTable(
603 10
                    $this->getAlias(),
604
                    $this->getModelSchemas()->getPrimaryKey($this->getModelClass()),
605 10
                    $targetTable,
606 10
                    $targetColumn
607 10
                );
608 10
                break;
609 10
610 10
            case RelationshipTypes::BELONGS_TO_MANY:
611 10
            default:
612 10
                assert($relationshipType === RelationshipTypes::BELONGS_TO_MANY);
613
                $primaryKey = $this->getModelSchemas()->getPrimaryKey($this->getModelClass());
614 10
                list ($intermediateTable, $intermediatePk, $intermediateFk) =
615
                    $this->getModelSchemas()->getBelongsToManyRelationship($this->getModelClass(), $name);
616
                list($targetPrimaryKey, $targetTable) =
617 21
                    $this->getModelSchemas()->getReversePrimaryKey($this->getModelClass(), $name);
618
619
                $targetAlias = $this->innerJoinTwoSequentialTables(
620
                    $this->getAlias(),
621
                    $primaryKey,
622
                    $intermediateTable,
623 58
                    $intermediatePk,
624
                    $intermediateFk,
625 58
                    $targetTable,
626
                    $targetPrimaryKey
627
                );
628
                break;
629
        }
630
631
        return $targetAlias;
632
    }
633
634
    /**
635
     * @return string
636
     */
637
    public function getAlias(): string
638 46
    {
639
        return $this->mainAlias;
640 46
    }
641 46
642 46
    /**
643 46
     * @param CompositeExpression $expression
644
     * @param string              $tableOrAlias
645 46
     * @param iterable            $filters
646 46
     *
647 46
     * @return self
648 46
     *
649 46
     * @throws DBALException
650 46
     * @throws InvalidArgumentException
651
     */
652 46
    public function applyFilters(CompositeExpression $expression, string $tableOrAlias, iterable $filters): self
653
    {
654
        foreach ($filters as $columnName => $operationsWithArgs) {
655
            assert(
656 45
                is_string($columnName) === true && empty($columnName) === false,
657
                "Haven't you forgotten to specify a column name in a relationship that joins `$tableOrAlias` table?"
658
            );
659
            $fullColumnName = $this->quoteDoubleIdentifier($tableOrAlias, $columnName);
660
            foreach ($operationsWithArgs as $operation => $arguments) {
661
                assert(
662
                    is_iterable($arguments) === true || is_array($arguments) === true,
663
                    "Operation arguments are missing for `$columnName` column. " .
664
                    'Use an empty array as an empty argument list.'
665
                );
666
                $expression->add($this->createFilterExpression($fullColumnName, $operation, $arguments));
667 8
            }
668
        }
669 8
670 8
        return $this;
671 8
    }
672 8
673
    /**
674
     * @param string   $tableOrAlias
675 8
     * @param iterable $sorts
676
     *
677
     * @return self
678
     *
679
     * @throws DBALException
680
     */
681
    public function applySorts(string $tableOrAlias, iterable $sorts): self
682
    {
683
        foreach ($sorts as $columnName => $isAsc) {
684
            assert(is_string($columnName) === true && is_bool($isAsc) === true);
685 65
            $fullColumnName = $this->quoteDoubleIdentifier($tableOrAlias, $columnName);
686
            $this->addOrderBy($fullColumnName, $isAsc === true ? 'ASC' : 'DESC');
687 65
        }
688
689
        return $this;
690
    }
691
692
    /**
693
     * @param string $tableOrColumn
694
     *
695
     * @return string
696
     *
697
     * @throws DBALException
698 64
     */
699
    public function quoteSingleIdentifier(string $tableOrColumn): string
700 64
    {
701
        return $this->getConnection()->getDatabasePlatform()->quoteSingleIdentifier($tableOrColumn);
702 64
    }
703
704
    /**
705
     * @param string $tableOrAlias
706
     * @param string $column
707
     *
708
     * @return string
709
     *
710
     * @throws DBALException
711
     */
712 58
    public function quoteDoubleIdentifier(string $tableOrAlias, string $column): string
713
    {
714 58
        $platform = $this->getConnection()->getDatabasePlatform();
715
716 58
        return $platform->quoteSingleIdentifier($tableOrAlias) . '.' . $platform->quoteSingleIdentifier($column);
717
    }
718
719
    /**
720
     * @param $value
721
     *
722
     * @return string
723
     *
724
     * @throws DBALException
725
     */
726 18
    public function createSingleValueNamedParameter($value): string
727
    {
728 18
        $paramName = $this->createNamedParameter($this->getPdoValue($value), $this->getPdoType($value));
729
730 18
        return $paramName;
731 18
    }
732
733
    /**
734 18
     * @param iterable $values
735
     *
736
     * @return array
737
     *
738
     * @throws DBALException
739
     */
740
    public function createArrayValuesNamedParameter(iterable $values): array
741
    {
742 66
        $names = [];
743
744 66
        foreach ($values as $value) {
745 66
            $names[] = $this->createSingleValueNamedParameter($value);
746
        }
747 66
748
        return $names;
749
    }
750
751
    /**
752
     * @param string $tableName
753
     *
754
     * @return string
755
     */
756
    public function createAlias(string $tableName): string
757
    {
758
        $alias                          = $tableName . (++$this->aliasIdCounter);
759
        $this->knownAliases[$tableName] = $alias;
760 21
761
        return $alias;
762
    }
763
764
    /**
765
     * @param string $fromAlias
766 21
     * @param string $fromColumn
767 21
     * @param string $targetTable
768 21
     * @param string $targetColumn
769
     *
770 21
     * @return string
771 21
     *
772 21
     * @throws DBALException
773 21
     */
774 21
    public function innerJoinOneTable(
775
        string $fromAlias,
776
        string $fromColumn,
777 21
        string $targetTable,
778
        string $targetColumn
779
    ): string {
780
        $targetAlias   = $this->createAlias($targetTable);
781
        $joinCondition = $this->quoteDoubleIdentifier($fromAlias, $fromColumn) . '=' .
782
            $this->quoteDoubleIdentifier($targetAlias, $targetColumn);
783
784
        $this->innerJoin(
785
            $this->quoteSingleIdentifier($fromAlias),
786
            $this->quoteSingleIdentifier($targetTable),
787
            $this->quoteSingleIdentifier($targetAlias),
788
            $joinCondition
789
        );
790
791
        return $targetAlias;
792
    }
793 10
794
    /**
795
     * @param string $fromAlias
796
     * @param string $fromColumn
797
     * @param string $intTable
798
     * @param string $intToFromColumn
799
     * @param string $intToTargetColumn
800
     * @param string $targetTable
801
     * @param string $targetColumn
802 10
     *
803 10
     * @return string
804
     *
805 10
     * @throws DBALException
806
     */
807
    public function innerJoinTwoSequentialTables(
808
        string $fromAlias,
809
        string $fromColumn,
810
        string $intTable,
811
        string $intToFromColumn,
812
        string $intToTargetColumn,
813
        string $targetTable,
814
        string $targetColumn
815
    ): string {
816
        $intAlias    = $this->innerJoinOneTable($fromAlias, $fromColumn, $intTable, $intToFromColumn);
817 11
        $targetAlias = $this->innerJoinOneTable($intAlias, $intToTargetColumn, $targetTable, $targetColumn);
818
819 11
        return $targetAlias;
820 11
    }
821
822 11
    /**
823
     * @param string $name
824
     *
825
     * @return Type
826
     *
827
     * @throws DBALException
828 66
     *
829
     * @SuppressWarnings(PHPMD.StaticAccess)
830 66
     */
831
    protected function getDbalType(string $name): Type
832
    {
833
        assert(Type::hasType($name), "Type `$name` either do not exist or registered.");
834
        $type = Type::getType($name);
835
836 66
        return $type;
837
    }
838 66
839
    /**
840
     * @return string
841
     */
842
    private function getTableName(): string
843
    {
844
        return $this->mainTableName;
845
    }
846
847
    /**
848
     * @return ModelSchemaInfoInterface
849
     */
850
    private function getModelSchemas(): ModelSchemaInfoInterface
851
    {
852
        return $this->modelSchemas;
853
    }
854 59
855
    /**
856
     * @param string   $fullColumnName
857 59
     * @param int      $operation
858 48
     * @param iterable $arguments
859 47
     *
860 47
     * @return string
861 30
     *
862 1
     * @throws DBALException
863 1
     * @throws InvalidArgumentException
864 1
     *
865 30
     * @SuppressWarnings(PHPMD.StaticAccess)
866 6
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
867 6
     */
868 6
    private function createFilterExpression(string $fullColumnName, int $operation, iterable $arguments): string
869 30
    {
870 7
        switch ($operation) {
871 7
            case FilterParameterInterface::OPERATION_EQUALS:
872 7
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
873 29
                $expression = $this->expr()->eq($fullColumnName, $parameter);
874 2
                break;
875 2
            case FilterParameterInterface::OPERATION_NOT_EQUALS:
876 2
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
877 28
                $expression = $this->expr()->neq($fullColumnName, $parameter);
878 6
                break;
879 6
            case FilterParameterInterface::OPERATION_LESS_THAN:
880 6
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
881 23
                $expression = $this->expr()->lt($fullColumnName, $parameter);
882 9
                break;
883 9
            case FilterParameterInterface::OPERATION_LESS_OR_EQUALS:
884 9
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
885 18
                $expression = $this->expr()->lte($fullColumnName, $parameter);
886 1
                break;
887 1
            case FilterParameterInterface::OPERATION_GREATER_THAN:
888 1
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
889 18
                $expression = $this->expr()->gt($fullColumnName, $parameter);
890 18
                break;
891 18
            case FilterParameterInterface::OPERATION_GREATER_OR_EQUALS:
892 18
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
893 1
                $expression = $this->expr()->gte($fullColumnName, $parameter);
894 1
                break;
895 1
            case FilterParameterInterface::OPERATION_LIKE:
896 1
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
897 1
                $expression = $this->expr()->like($fullColumnName, $parameter);
898 1
                break;
899 1
            case FilterParameterInterface::OPERATION_NOT_LIKE:
900 1
                $parameter  = $this->createSingleValueNamedParameter($this->firstValue($arguments));
901
                $expression = $this->expr()->notLike($fullColumnName, $parameter);
902 1
                break;
903 1
            case FilterParameterInterface::OPERATION_IN:
904 1
                $parameters = $this->createArrayValuesNamedParameter($arguments);
905
                $expression = $this->expr()->in($fullColumnName, $parameters);
906
                break;
907 58
            case FilterParameterInterface::OPERATION_NOT_IN:
908
                $parameters = $this->createArrayValuesNamedParameter($arguments);
909
                $expression = $this->expr()->notIn($fullColumnName, $parameters);
910
                break;
911
            case FilterParameterInterface::OPERATION_IS_NULL:
912
                $expression = $this->expr()->isNull($fullColumnName);
913
                break;
914
            case FilterParameterInterface::OPERATION_IS_NOT_NULL:
915
            default:
916
                assert($operation === FilterParameterInterface::OPERATION_IS_NOT_NULL);
917 55
                $expression = $this->expr()->isNotNull($fullColumnName);
918
                break;
919 55
        }
920 54
921
        return $expression;
922
    }
923
924 1
    /**
925
     * @param iterable $arguments
926
     *
927
     * @return mixed
928
     *
929
     * @throws InvalidArgumentException
930 52
     */
931
    private function firstValue(iterable $arguments)
932 52
    {
933
        foreach ($arguments as $argument) {
934
            return $argument;
935
        }
936
937
        // arguments are empty
938
        throw new InvalidArgumentException();
939
    }
940
941
    /**
942 58
     * @return Closure
943
     */
944 58
    private function getColumnToDatabaseMapper(): Closure
945
    {
946
        return $this->columnMapper;
947
    }
948
949
    /**
950
     * @param mixed $value
951
     *
952
     * @return mixed
953
     *
954 1
     * @throws DBALException
955
     */
956 1
    private function getPdoValue($value)
957 1
    {
958 1
        return $value instanceof DateTimeInterface ? $this->convertDataTimeToDatabaseFormat($value) : $value;
959
    }
960
961
    /**
962
     * @param DateTimeInterface $dateTime
963
     *
964
     * @return string
965
     *
966
     * @throws DBALException
967
     */
968
    private function convertDataTimeToDatabaseFormat(DateTimeInterface $dateTime): string
969 58
    {
970
        return $this->getDateTimeType()->convertToDatabaseValue(
971 58
            $dateTime,
972 37
            $this->getConnection()->getDatabasePlatform()
973 38
        );
974 1
    }
975 37
976 1
    /**
977
     * @param mixed $value
978 36
     *
979 36
     * @return int
980
     *
981 36
     * @SuppressWarnings(PHPMD.ElseExpression)
982
     */
983 36
    private function getPdoType($value): int
984 36
    {
985
        if (is_int($value) === true) {
986
            $type = PDO::PARAM_INT;
987 58
        } elseif (is_bool($value)) {
988
            $type = PDO::PARAM_BOOL;
989
        } elseif ($value instanceof DateTimeInterface) {
990
            $type = PDO::PARAM_STR;
991
        } else {
992
            assert(
993
                $value !== null,
994
                'It seems you are trying to use `null` with =, >, <, or etc operator. ' .
995
                'Use `is null` or `not null` instead.'
996
            );
997 1
            assert(is_string($value), "Only strings, booleans and integers are supported.");
998
            $type = PDO::PARAM_STR;
999 1
        }
1000 1
1001
        return $type;
1002
    }
1003 1
1004
    /**
1005
     * @return Type
1006
     *
1007
     * @throws DBALException
1008
     *
1009
     * @SuppressWarnings(PHPMD.StaticAccess)
1010
     */
1011
    private function getDateTimeType(): Type
1012
    {
1013
        if ($this->dateTimeType === null) {
1014
            $this->dateTimeType = Type::getType(DateTimeType::DATETIME);
1015
        }
1016
1017
        return $this->dateTimeType;
1018
    }
1019
}
1020