Issues (197)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Adapters/ModelQueryBuilder.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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
$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
$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