Failed Conditions
Pull Request — master (#6709)
by Sergey
15:18
created

SqlWalker::walkGroupByClause()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 10
c 0
b 0
f 0
ccs 5
cts 5
cp 1
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
crap 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Query;
21
22
use Doctrine\DBAL\LockMode;
23
use Doctrine\DBAL\Types\Type;
24
use Doctrine\ORM\Mapping\ClassMetadata;
25
use Doctrine\ORM\Mapping\ClassMetadataInfo;
26
use Doctrine\ORM\OptimisticLockException;
27
use Doctrine\ORM\Query;
28
use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver;
29
use Doctrine\ORM\Utility\PersisterHelper;
30
31
/**
32
 * The SqlWalker is a TreeWalker that walks over a DQL AST and constructs
33
 * the corresponding SQL.
34
 *
35
 * @author Guilherme Blanco <[email protected]>
36
 * @author Roman Borschel <[email protected]>
37
 * @author Benjamin Eberlei <[email protected]>
38
 * @author Alexander <[email protected]>
39
 * @author Fabio B. Silva <[email protected]>
40
 * @since  2.0
41
 */
42
class SqlWalker implements TreeWalker
43
{
44
    /**
45
     * @var string
46
     */
47
    const HINT_DISTINCT = 'doctrine.distinct';
48
49
    /**
50
     * @var ResultSetMapping
51
     */
52
    private $rsm;
53
54
    /**
55
     * Counter for generating unique column aliases.
56
     *
57
     * @var integer
58
     */
59
    private $aliasCounter = 0;
60
61
    /**
62
     * Counter for generating unique table aliases.
63
     *
64
     * @var integer
65
     */
66
    private $tableAliasCounter = 0;
67
68
    /**
69
     * Counter for generating unique scalar result.
70
     *
71
     * @var integer
72
     */
73
    private $scalarResultCounter = 1;
74
75
    /**
76
     * Counter for generating unique parameter indexes.
77
     *
78
     * @var integer
79
     */
80
    private $sqlParamIndex = 0;
81
82
    /**
83
     * Counter for generating indexes.
84
     *
85
     * @var integer
86
     */
87
    private $newObjectCounter = 0;
88
89
    /**
90
     * Contains nesting levels of new objects arguments
91
     *
92
     * @var array of newObject indexes
93
     */
94
    private $newObjectStack = [];
95
96
    /**
97
     * @var ParserResult
98
     */
99
    private $parserResult;
100
101
    /**
102
     * @var \Doctrine\ORM\EntityManager
103
     */
104
    private $em;
105
106
    /**
107
     * @var \Doctrine\DBAL\Connection
108
     */
109
    private $conn;
110
111
    /**
112
     * @var \Doctrine\ORM\AbstractQuery
113
     */
114
    private $query;
115
116
    /**
117
     * @var array
118
     */
119
    private $tableAliasMap = [];
120
121
    /**
122
     * Map from result variable names to their SQL column alias names.
123
     *
124
     * @var array
125
     */
126
    private $scalarResultAliasMap = [];
127
128
    /**
129
     * Map from Table-Alias + Column-Name to OrderBy-Direction.
130
     *
131
     * @var array
132
     */
133
    private $orderedColumnsMap = [];
134
135
    /**
136
     * Map from DQL-Alias + Field-Name to SQL Column Alias.
137
     *
138
     * @var array
139
     */
140
    private $scalarFields = [];
141
142
    /**
143
     * Map of all components/classes that appear in the DQL query.
144
     *
145
     * @var array
146
     */
147
    private $queryComponents;
148
149
    /**
150
     * A list of classes that appear in non-scalar SelectExpressions.
151
     *
152
     * @var array
153
     */
154
    private $selectedClasses = [];
155
156
    /**
157
     * The DQL alias of the root class of the currently traversed query.
158
     *
159
     * @var array
160
     */
161
    private $rootAliases = [];
162
163
    /**
164
     * Flag that indicates whether to generate SQL table aliases in the SQL.
165
     * These should only be generated for SELECT queries, not for UPDATE/DELETE.
166
     *
167
     * @var boolean
168
     */
169
    private $useSqlTableAliases = true;
170
171
    /**
172
     * The database platform abstraction.
173
     *
174
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
175
     */
176
    private $platform;
177
178
    /**
179
     * The quote strategy.
180
     *
181
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
182
     */
183
    private $quoteStrategy;
184
185
    /**
186
     * {@inheritDoc}
187
     */
188 700
    public function __construct($query, $parserResult, array $queryComponents)
189
    {
190 700
        $this->query            = $query;
191 700
        $this->parserResult     = $parserResult;
192 700
        $this->queryComponents  = $queryComponents;
193 700
        $this->rsm              = $parserResult->getResultSetMapping();
194 700
        $this->em               = $query->getEntityManager();
195 700
        $this->conn             = $this->em->getConnection();
196 700
        $this->platform         = $this->conn->getDatabasePlatform();
197 700
        $this->quoteStrategy    = $this->em->getConfiguration()->getQuoteStrategy();
198 700
    }
199
200
    /**
201
     * Gets the Query instance used by the walker.
202
     *
203
     * @return Query.
0 ignored issues
show
Documentation introduced by
The doc-type Query. could not be parsed: Unknown type name "Query." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
204
     */
205
    public function getQuery()
206
    {
207
        return $this->query;
208
    }
209
210
    /**
211
     * Gets the Connection used by the walker.
212
     *
213
     * @return \Doctrine\DBAL\Connection
214
     */
215 35
    public function getConnection()
216
    {
217 35
        return $this->conn;
218
    }
219
220
    /**
221
     * Gets the EntityManager used by the walker.
222
     *
223
     * @return \Doctrine\ORM\EntityManager
224
     */
225 22
    public function getEntityManager()
226
    {
227 22
        return $this->em;
228
    }
229
230
    /**
231
     * Gets the information about a single query component.
232
     *
233
     * @param string $dqlAlias The DQL alias.
234
     *
235
     * @return array
236
     */
237 17
    public function getQueryComponent($dqlAlias)
238
    {
239 17
        return $this->queryComponents[$dqlAlias];
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245
    public function getQueryComponents()
246
    {
247
        return $this->queryComponents;
248
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253 1
    public function setQueryComponent($dqlAlias, array $queryComponent)
254
    {
255 1
        $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token'];
256
257 1
        if (array_diff($requiredKeys, array_keys($queryComponent))) {
258 1
            throw QueryException::invalidQueryComponent($dqlAlias);
259
        }
260
261
        $this->queryComponents[$dqlAlias] = $queryComponent;
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267 694
    public function getExecutor($AST)
268
    {
269
        switch (true) {
270 694
            case ($AST instanceof AST\DeleteStatement):
271 38
                $primaryClass = $this->em->getClassMetadata($AST->deleteClause->abstractSchemaName);
272
273 38
                return ($primaryClass->isInheritanceTypeJoined())
274 2
                    ? new Exec\MultiTableDeleteExecutor($AST, $this)
275 38
                    : new Exec\SingleTableDeleteUpdateExecutor($AST, $this);
276
277 660
            case ($AST instanceof AST\UpdateStatement):
278 29
                $primaryClass = $this->em->getClassMetadata($AST->updateClause->abstractSchemaName);
279
280 29
                return ($primaryClass->isInheritanceTypeJoined())
281 4
                    ? new Exec\MultiTableUpdateExecutor($AST, $this)
282 29
                    : new Exec\SingleTableDeleteUpdateExecutor($AST, $this);
283
284
            default:
285 635
                return new Exec\SingleSelectExecutor($AST, $this);
286
        }
287
    }
288
289
    /**
290
     * Generates a unique, short SQL table alias.
291
     *
292
     * @param string $tableName Table name
293
     * @param string $dqlAlias  The DQL alias.
294
     *
295
     * @return string Generated table alias.
296
     */
297 646
    public function getSQLTableAlias($tableName, $dqlAlias = '')
298
    {
299 646
        $tableName .= ($dqlAlias) ? '@[' . $dqlAlias . ']' : '';
300
301 646
        if ( ! isset($this->tableAliasMap[$tableName])) {
302 646
            $this->tableAliasMap[$tableName] = (preg_match('/[a-z]/i', $tableName[0]) ? strtolower($tableName[0]) : 't')
303 646
                . $this->tableAliasCounter++ . '_';
304
        }
305
306 646
        return $this->tableAliasMap[$tableName];
307
    }
308
309
    /**
310
     * Forces the SqlWalker to use a specific alias for a table name, rather than
311
     * generating an alias on its own.
312
     *
313
     * @param string $tableName
314
     * @param string $alias
315
     * @param string $dqlAlias
316
     *
317
     * @return string
318
     */
319 65
    public function setSQLTableAlias($tableName, $alias, $dqlAlias = '')
320
    {
321 65
        $tableName .= ($dqlAlias) ? '@[' . $dqlAlias . ']' : '';
322
323 65
        $this->tableAliasMap[$tableName] = $alias;
324
325 65
        return $alias;
326
    }
327
328
    /**
329
     * Gets an SQL column alias for a column name.
330
     *
331
     * @param string $columnName
332
     *
333
     * @return string
334
     */
335 635
    public function getSQLColumnAlias($columnName)
336
    {
337 635
        return $this->quoteStrategy->getColumnAlias($columnName, $this->aliasCounter++, $this->platform);
338
    }
339
340
    /**
341
     * Generates the SQL JOINs that are necessary for Class Table Inheritance
342
     * for the given class.
343
     *
344
     * @param ClassMetadata $class    The class for which to generate the joins.
345
     * @param string        $dqlAlias The DQL alias of the class.
346
     *
347
     * @return string The SQL.
348
     */
349 100
    private function _generateClassTableInheritanceJoins($class, $dqlAlias)
350
    {
351 100
        $sql = '';
352
353 100
        $baseTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
354
355
        // INNER JOIN parent class tables
356 100
        foreach ($class->parentClasses as $parentClassName) {
357 67
            $parentClass = $this->em->getClassMetadata($parentClassName);
358 67
            $tableAlias  = $this->getSQLTableAlias($parentClass->getTableName(), $dqlAlias);
359
360
            // If this is a joined association we must use left joins to preserve the correct result.
361 67
            $sql .= isset($this->queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER ';
362 67
            $sql .= 'JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON ';
363
364 67
            $sqlParts = [];
365
366 67
            foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) {
367 67
                $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName;
368
            }
369
370
            // Add filters on the root class
371 67
            if ($filterSql = $this->generateFilterConditionSQL($parentClass, $tableAlias)) {
372 1
                $sqlParts[] = $filterSql;
373
            }
374
375 67
            $sql .= implode(' AND ', $sqlParts);
376
        }
377
378
        // Ignore subclassing inclusion if partial objects is disallowed
379 100
        if ($this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
380 21
            return $sql;
381
        }
382
383
        // LEFT JOIN child class tables
384 79
        foreach ($class->subClasses as $subClassName) {
385 39
            $subClass   = $this->em->getClassMetadata($subClassName);
386 39
            $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
387
388 39
            $sql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON ';
389
390 39
            $sqlParts = [];
391
392 39
            foreach ($this->quoteStrategy->getIdentifierColumnNames($subClass, $this->platform) as $columnName) {
393 39
                $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName;
394
            }
395
396 39
            $sql .= implode(' AND ', $sqlParts);
397
        }
398
399 79
        return $sql;
400
    }
401
402
    /**
403
     * @return string
404
     */
405 629
    private function _generateOrderedCollectionOrderByItems()
406
    {
407 629
        $orderedColumns = [];
408
409 629
        foreach ($this->selectedClasses as $selectedClass) {
410 490
            $dqlAlias  = $selectedClass['dqlAlias'];
411 490
            $qComp     = $this->queryComponents[$dqlAlias];
412
413 490
            if ( ! isset($qComp['relation']['orderBy'])) {
414 490
                continue;
415
            }
416
417 6
            $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name);
418
419 6
            foreach ($qComp['relation']['orderBy'] as $fieldName => $orientation) {
420 6
                $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform);
421 6
                $tableName  = ($qComp['metadata']->isInheritanceTypeJoined())
422 1
                    ? $persister->getOwningTable($fieldName)
423 6
                    : $qComp['metadata']->getTableName();
424
425 6
                $orderedColumn = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName;
426
427
                // OrderByClause should replace an ordered relation. see - DDC-2475
428 6
                if (isset($this->orderedColumnsMap[$orderedColumn])) {
429 1
                    continue;
430
                }
431
432 6
                $this->orderedColumnsMap[$orderedColumn] = $orientation;
433 6
                $orderedColumns[] = $orderedColumn . ' ' . $orientation;
434
            }
435
        }
436
437 629
        return implode(', ', $orderedColumns);
438
    }
439
440
    /**
441
     * Generates a discriminator column SQL condition for the class with the given DQL alias.
442
     *
443
     * @param array $dqlAliases List of root DQL aliases to inspect for discriminator restrictions.
444
     *
445
     * @return string
446
     */
447 689
    private function _generateDiscriminatorColumnConditionSQL(array $dqlAliases)
448
    {
449 689
        $sqlParts = [];
450
451 689
        foreach ($dqlAliases as $dqlAlias) {
452 689
            $class = $this->queryComponents[$dqlAlias]['metadata'];
453
454 689
            if ( ! $class->isInheritanceTypeSingleTable()) continue;
455
456 40
            $conn   = $this->em->getConnection();
457 40
            $values = [];
458
459 40
            if ($class->discriminatorValue !== null) { // discriminators can be 0
460 21
                $values[] = $conn->quote($class->discriminatorValue);
461
            }
462
463 40
            foreach ($class->subClasses as $subclassName) {
464 29
                $values[] = $conn->quote($this->em->getClassMetadata($subclassName)->discriminatorValue);
465
            }
466
467 40
            $sqlTableAlias = ($this->useSqlTableAliases)
468 35
                ? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.'
469 40
                : '';
470
471 40
            $sqlParts[] = $sqlTableAlias . $class->discriminatorColumn['name'] . ' IN (' . implode(', ', $values) . ')';
472
        }
473
474 689
        $sql = implode(' AND ', $sqlParts);
475
476 689
        return (count($sqlParts) > 1) ? '(' . $sql . ')' : $sql;
477
    }
478
479
    /**
480
     * Generates the filter SQL for a given entity and table alias.
481
     *
482
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
483
     * @param string        $targetTableAlias The table alias of the joined/selected table.
484
     *
485
     * @return string The SQL query part to add to a query.
486
     */
487 323
    private function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
488
    {
489 323
        if (!$this->em->hasFilters()) {
490 285
            return '';
491
        }
492
493 43
        switch($targetEntity->inheritanceType) {
494 43
            case ClassMetadata::INHERITANCE_TYPE_NONE:
495 33
                break;
496 10
            case ClassMetadata::INHERITANCE_TYPE_JOINED:
497
                // The classes in the inheritance will be added to the query one by one,
498
                // but only the root node is getting filtered
499 6
                if ($targetEntity->name !== $targetEntity->rootEntityName) {
500 4
                    return '';
501
                }
502 6
                break;
503 4
            case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE:
504
                // With STI the table will only be queried once, make sure that the filters
505
                // are added to the root entity
506 4
                $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName);
507 4
                break;
508
            default:
509
                //@todo: throw exception?
510
                return '';
511
        }
512
513 43
        $filterClauses = [];
514 43
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
515 10
            if ('' !== $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
516 10
                $filterClauses[] = '(' . $filterExpr . ')';
517
            }
518
        }
519
520 43
        return implode(' AND ', $filterClauses);
521
    }
522
523
    /**
524
     * {@inheritdoc}
525
     */
526 635
    public function walkSelectStatement(AST\SelectStatement $AST)
527
    {
528 635
        $limit    = $this->query->getMaxResults();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\ORM\AbstractQuery as the method getMaxResults() does only exist in the following sub-classes of Doctrine\ORM\AbstractQuery: Doctrine\ORM\Query. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
529 635
        $offset   = $this->query->getFirstResult();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Doctrine\ORM\AbstractQuery as the method getFirstResult() does only exist in the following sub-classes of Doctrine\ORM\AbstractQuery: Doctrine\ORM\Query. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
530 635
        $lockMode = $this->query->getHint(Query::HINT_LOCK_MODE);
531 635
        $sql      = $this->walkSelectClause($AST->selectClause)
532 635
            . $this->walkFromClause($AST->fromClause)
533 633
            . $this->walkWhereClause($AST->whereClause);
0 ignored issues
show
Bug introduced by
It seems like $AST->whereClause can be null; however, walkWhereClause() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
534
535 630
        if ($AST->groupByClause) {
536 24
            $sql .= $this->walkGroupByClause($AST->groupByClause);
537
        }
538
539 630
        if ($AST->havingClause) {
540 14
            $sql .= $this->walkHavingClause($AST->havingClause);
541
        }
542
543 630
        if ($AST->orderByClause) {
544 143
            $sql .= $this->walkOrderByClause($AST->orderByClause);
545
        }
546
547 629
        if ( ! $AST->orderByClause && ($orderBySql = $this->_generateOrderedCollectionOrderByItems())) {
548 6
            $sql .= ' ORDER BY ' . $orderBySql;
549
        }
550
551 629
        if ($limit !== null || $offset !== null) {
552 39
            $sql = $this->platform->modifyLimitQuery($sql, $limit, $offset);
553
        }
554
555 629
        if ($lockMode === null || $lockMode === false || $lockMode === LockMode::NONE) {
556 624
            return $sql;
557
        }
558
559 5
        if ($lockMode === LockMode::PESSIMISTIC_READ) {
560 3
            return $sql . ' ' . $this->platform->getReadLockSQL();
561
        }
562
563 2
        if ($lockMode === LockMode::PESSIMISTIC_WRITE) {
564 1
            return $sql . ' ' . $this->platform->getWriteLockSQL();
565
        }
566
567 1
        if ($lockMode !== LockMode::OPTIMISTIC) {
568
            throw QueryException::invalidLockMode();
569
        }
570
571 1
        foreach ($this->selectedClasses as $selectedClass) {
572 1
            if ( ! $selectedClass['class']->isVersioned) {
573 1
                throw OptimisticLockException::lockFailed($selectedClass['class']->name);
574
            }
575
        }
576
577
        return $sql;
578
    }
579
580
    /**
581
     * {@inheritdoc}
582
     */
583 25
    public function walkUpdateStatement(AST\UpdateStatement $AST)
584
    {
585 25
        $this->useSqlTableAliases = false;
586 25
        $this->rsm->isSelect      = false;
587
588 25
        return $this->walkUpdateClause($AST->updateClause)
589 25
            . $this->walkWhereClause($AST->whereClause);
0 ignored issues
show
Bug introduced by
It seems like $AST->whereClause can be null; however, walkWhereClause() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
590
    }
591
592
    /**
593
     * {@inheritdoc}
594
     */
595 36
    public function walkDeleteStatement(AST\DeleteStatement $AST)
596
    {
597 36
        $this->useSqlTableAliases = false;
598 36
        $this->rsm->isSelect      = false;
599
600 36
        return $this->walkDeleteClause($AST->deleteClause)
601 36
            . $this->walkWhereClause($AST->whereClause);
0 ignored issues
show
Bug introduced by
It seems like $AST->whereClause can be null; however, walkWhereClause() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
602
    }
603
604
    /**
605
     * Walks down an IdentificationVariable AST node, thereby generating the appropriate SQL.
606
     * This one differs of ->walkIdentificationVariable() because it generates the entity identifiers.
607
     *
608
     * @param string $identVariable
609
     *
610
     * @return string
611
     */
612 2
    public function walkEntityIdentificationVariable($identVariable)
613
    {
614 2
        $class      = $this->queryComponents[$identVariable]['metadata'];
615 2
        $tableAlias = $this->getSQLTableAlias($class->getTableName(), $identVariable);
616 2
        $sqlParts   = [];
617
618 2
        foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) {
619 2
            $sqlParts[] = $tableAlias . '.' . $columnName;
620
        }
621
622 2
        return implode(', ', $sqlParts);
623
    }
624
625
    /**
626
     * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL.
627
     *
628
     * @param string $identificationVariable
629
     * @param string $fieldName
630
     *
631
     * @return string The SQL.
632
     */
633 428
    public function walkIdentificationVariable($identificationVariable, $fieldName = null)
634
    {
635 428
        $class = $this->queryComponents[$identificationVariable]['metadata'];
636
637
        if (
638 428
            $fieldName !== null && $class->isInheritanceTypeJoined() &&
639 428
            isset($class->fieldMappings[$fieldName]['inherited'])
640
        ) {
641 38
            $class = $this->em->getClassMetadata($class->fieldMappings[$fieldName]['inherited']);
642
        }
643
644 428
        return $this->getSQLTableAlias($class->getTableName(), $identificationVariable);
645
    }
646
647
    /**
648
     * {@inheritdoc}
649
     */
650 497
    public function walkPathExpression($pathExpr)
651
    {
652 497
        $sql = '';
653
654
        /* @var $pathExpr Query\AST\PathExpression */
655 497
        switch ($pathExpr->type) {
656 497
            case AST\PathExpression::TYPE_STATE_FIELD:
657 476
                $fieldName = $pathExpr->field;
658 476
                $dqlAlias = $pathExpr->identificationVariable;
659 476
                $class = $this->queryComponents[$dqlAlias]['metadata'];
660
661 476
                if ($this->useSqlTableAliases) {
662 428
                    $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.';
663
                }
664
665 476
                $sql .= $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
666 476
                break;
667
668 62
            case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION:
669
                // 1- the owning side:
670
                //    Just use the foreign key, i.e. u.group_id
671 62
                $fieldName = $pathExpr->field;
672 62
                $dqlAlias = $pathExpr->identificationVariable;
673 62
                $class = $this->queryComponents[$dqlAlias]['metadata'];
674
675 62
                if (isset($class->associationMappings[$fieldName]['inherited'])) {
676 2
                    $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]['inherited']);
677
                }
678
679 62
                $assoc = $class->associationMappings[$fieldName];
680
681 62
                if ( ! $assoc['isOwningSide']) {
682 2
                    throw QueryException::associationPathInverseSideNotSupported($pathExpr);
683
                }
684
685
                // COMPOSITE KEYS NOT (YET?) SUPPORTED
686 60
                if (count($assoc['sourceToTargetKeyColumns']) > 1) {
687 1
                    throw QueryException::associationPathCompositeKeyNotSupported();
688
                }
689
690 59
                if ($this->useSqlTableAliases) {
691 56
                    $sql .= $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.';
692
                }
693
694 59
                $sql .= reset($assoc['targetToSourceKeyColumns']);
695 59
                break;
696
697
            default:
698
                throw QueryException::invalidPathExpression($pathExpr);
699
        }
700
701 494
        return $sql;
702
    }
703
704
    /**
705
     * {@inheritdoc}
706
     */
707 635
    public function walkSelectClause($selectClause)
708
    {
709 635
        $sql = 'SELECT ' . (($selectClause->isDistinct) ? 'DISTINCT ' : '');
710 635
        $sqlSelectExpressions = array_filter(array_map([$this, 'walkSelectExpression'], $selectClause->selectExpressions));
711
712 635
        if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) == true && $selectClause->isDistinct) {
713 1
            $this->query->setHint(self::HINT_DISTINCT, true);
714
        }
715
716 635
        $addMetaColumns = ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD) &&
717 472
            $this->query->getHydrationMode() == Query::HYDRATE_OBJECT
718
            ||
719 290
            $this->query->getHydrationMode() != Query::HYDRATE_OBJECT &&
720 635
            $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS);
721
722 635
        foreach ($this->selectedClasses as $selectedClass) {
723 496
            $class       = $selectedClass['class'];
724 496
            $dqlAlias    = $selectedClass['dqlAlias'];
725 496
            $resultAlias = $selectedClass['resultAlias'];
726
727
            // Register as entity or joined entity result
728 496
            if ($this->queryComponents[$dqlAlias]['relation'] === null) {
729 496
                $this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias);
730
            } else {
731 159
                $this->rsm->addJoinedEntityResult(
732 159
                    $class->name,
733 159
                    $dqlAlias,
734 159
                    $this->queryComponents[$dqlAlias]['parent'],
735 159
                    $this->queryComponents[$dqlAlias]['relation']['fieldName']
736
                );
737
            }
738
739 496
            if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) {
740
                // Add discriminator columns to SQL
741 97
                $rootClass   = $this->em->getClassMetadata($class->rootEntityName);
742 97
                $tblAlias    = $this->getSQLTableAlias($rootClass->getTableName(), $dqlAlias);
743 97
                $discrColumn = $rootClass->discriminatorColumn;
744 97
                $columnAlias = $this->getSQLColumnAlias($discrColumn['name']);
745
746 97
                $sqlSelectExpressions[] = $tblAlias . '.' . $discrColumn['name'] . ' AS ' . $columnAlias;
747
748 97
                $this->rsm->setDiscriminatorColumn($dqlAlias, $columnAlias);
749 97
                $this->rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn['fieldName'], false, $discrColumn['type']);
750
            }
751
752
            // Add foreign key columns to SQL, if necessary
753 496
            if ( ! $addMetaColumns && ! $class->containsForeignIdentifier) {
754 181
                continue;
755
            }
756
757
            // Add foreign key columns of class and also parent classes
758 365
            foreach ($class->associationMappings as $assoc) {
759 319
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
760 268
                    continue;
761 286
                } else if ( !$addMetaColumns && !isset($assoc['id'])) {
762
                    continue;
763
                }
764
765 286
                $targetClass   = $this->em->getClassMetadata($assoc['targetEntity']);
766 286
                $isIdentifier  = (isset($assoc['id']) && $assoc['id'] === true);
767 286
                $owningClass   = (isset($assoc['inherited'])) ? $this->em->getClassMetadata($assoc['inherited']) : $class;
768 286
                $sqlTableAlias = $this->getSQLTableAlias($owningClass->getTableName(), $dqlAlias);
769
770 286
                foreach ($assoc['joinColumns'] as $joinColumn) {
771 286
                    $columnName  = $joinColumn['name'];
772 286
                    $columnAlias = $this->getSQLColumnAlias($columnName);
773 286
                    $columnType  = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
774
775 286
                    $quotedColumnName       = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
776 286
                    $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias;
777
778 286
                    $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $isIdentifier, $columnType);
779
                }
780
            }
781
782
            // Add foreign key columns to SQL, if necessary
783 365
            if ( ! $addMetaColumns) {
784 8
                continue;
785
            }
786
787
            // Add foreign key columns of subclasses
788 360
            foreach ($class->subClasses as $subClassName) {
789 37
                $subClass      = $this->em->getClassMetadata($subClassName);
790 37
                $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
791
792 37
                foreach ($subClass->associationMappings as $assoc) {
793
                    // Skip if association is inherited
794 27
                    if (isset($assoc['inherited'])) continue;
795
796 16
                    if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
797 14
                        $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
798
799 14
                        foreach ($assoc['joinColumns'] as $joinColumn) {
800 14
                            $columnName  = $joinColumn['name'];
801 14
                            $columnAlias = $this->getSQLColumnAlias($columnName);
802 14
                            $columnType  = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
803
804 14
                            $quotedColumnName       = $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform);
805 14
                            $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias;
806
807 360
                            $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $subClass->isIdentifier($columnName), $columnType);
808
                        }
809
                    }
810
                }
811
            }
812
        }
813
814 635
        $sql .= implode(', ', $sqlSelectExpressions);
815
816 635
        return $sql;
817
    }
818
819
    /**
820
     * {@inheritdoc}
821
     */
822 637
    public function walkFromClause($fromClause)
823
    {
824 637
        $identificationVarDecls = $fromClause->identificationVariableDeclarations;
825 637
        $sqlParts = [];
826
827 637
        foreach ($identificationVarDecls as $identificationVariableDecl) {
828 637
            $sqlParts[] = $this->walkIdentificationVariableDeclaration($identificationVariableDecl);
829
        }
830
831 635
        return ' FROM ' . implode(', ', $sqlParts);
832
    }
833
834
    /**
835
     * Walks down a IdentificationVariableDeclaration AST node, thereby generating the appropriate SQL.
836
     *
837
     * @param AST\IdentificationVariableDeclaration $identificationVariableDecl
838
     *
839
     * @return string
840
     */
841 638
    public function walkIdentificationVariableDeclaration($identificationVariableDecl)
842
    {
843 638
        $sql = $this->walkRangeVariableDeclaration($identificationVariableDecl->rangeVariableDeclaration);
0 ignored issues
show
Bug introduced by
It seems like $identificationVariableD...angeVariableDeclaration can be null; however, walkRangeVariableDeclaration() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
844
845 638
        if ($identificationVariableDecl->indexBy) {
846 6
            $this->walkIndexBy($identificationVariableDecl->indexBy);
847
        }
848
849 638
        foreach ($identificationVariableDecl->joins as $join) {
850 248
            $sql .= $this->walkJoin($join);
851
        }
852
853 636
        return $sql;
854
    }
855
856
    /**
857
     * Walks down a IndexBy AST node.
858
     *
859
     * @param AST\IndexBy $indexBy
860
     *
861
     * @return void
862
     */
863 9
    public function walkIndexBy($indexBy)
864
    {
865 9
        $pathExpression = $indexBy->simpleStateFieldPathExpression;
866 9
        $alias          = $pathExpression->identificationVariable;
867 9
        $field          = $pathExpression->field;
868
869 9
        if (isset($this->scalarFields[$alias][$field])) {
870
            $this->rsm->addIndexByScalar($this->scalarFields[$alias][$field]);
871
872
            return;
873
        }
874
875 9
        $this->rsm->addIndexBy($alias, $field);
876 9
    }
877
878
    /**
879
     * Walks down a RangeVariableDeclaration AST node, thereby generating the appropriate SQL.
880
     *
881
     * @param AST\RangeVariableDeclaration $rangeVariableDeclaration
882
     *
883
     * @return string
884
     */
885 638
    public function walkRangeVariableDeclaration($rangeVariableDeclaration)
886
    {
887 638
        return $this->generateRangeVariableDeclarationSQL($rangeVariableDeclaration, false);
888
    }
889
890
    /**
891
     * Generate appropriate SQL for RangeVariableDeclaration AST node
892
     *
893
     * @param AST\RangeVariableDeclaration $rangeVariableDeclaration
894
     * @param bool $buildNestedJoins
895
     *
896
     * @return string
897
     */
898 638
    private function generateRangeVariableDeclarationSQL($rangeVariableDeclaration, bool $buildNestedJoins) : string
899
    {
900 638
        $class    = $this->em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName);
901 638
        $dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable;
902
903 638
        if ($rangeVariableDeclaration->isRoot) {
904 638
            $this->rootAliases[] = $dqlAlias;
905
        }
906
907 638
        $sql = $this->platform->appendLockHint(
908 638
            $this->quoteStrategy->getTableName($class, $this->platform) . ' ' .
909 638
            $this->getSQLTableAlias($class->getTableName(), $dqlAlias),
910 638
            $this->query->getHint(Query::HINT_LOCK_MODE)
911
        );
912
913 638
        if ($class->isInheritanceTypeJoined()) {
914 96
            if ($buildNestedJoins) {
915 3
                $sql = '(' . $sql . $this->_generateClassTableInheritanceJoins($class, $dqlAlias) . ')';
916
            } else {
917 94
                $sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias);
918
            }
919
        }
920
921 638
        return $sql;
922
    }
923
924
    /**
925
     * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL.
926
     *
927
     * @param AST\JoinAssociationDeclaration $joinAssociationDeclaration
928
     * @param int                            $joinType
929
     * @param AST\ConditionalExpression      $condExpr
930
     *
931
     * @return string
932
     *
933
     * @throws QueryException
934
     */
935 230
    public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joinType = AST\Join::JOIN_TYPE_INNER, $condExpr = null)
936
    {
937 230
        $sql = '';
938
939 230
        $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression;
940 230
        $joinedDqlAlias            = $joinAssociationDeclaration->aliasIdentificationVariable;
941 230
        $indexBy                   = $joinAssociationDeclaration->indexBy;
942
943 230
        $relation        = $this->queryComponents[$joinedDqlAlias]['relation'];
944 230
        $targetClass     = $this->em->getClassMetadata($relation['targetEntity']);
945 230
        $sourceClass     = $this->em->getClassMetadata($relation['sourceEntity']);
946 230
        $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
947
948 230
        $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias);
949 230
        $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable);
950
951
        // Ensure we got the owning side, since it has all mapping info
952 230
        $assoc = ( ! $relation['isOwningSide']) ? $targetClass->associationMappings[$relation['mappedBy']] : $relation;
953
954 230
        if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) == true && (!$this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) {
955 3
            if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) {
956 2
                throw QueryException::iterateWithFetchJoinNotAllowed($assoc);
957
            }
958
        }
959
960 228
        $targetTableJoin = null;
0 ignored issues
show
Unused Code introduced by
$targetTableJoin is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
961
962
        // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot
963
        // be the owning side and previously we ensured that $assoc is always the owning side of the associations.
964
        // The owning side is necessary at this point because only it contains the JoinColumn information.
965
        switch (true) {
966 228
            case ($assoc['type'] & ClassMetadata::TO_ONE):
967 181
                $conditions = [];
968
969 181
                foreach ($assoc['joinColumns'] as $joinColumn) {
970 181
                    $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
971 181
                    $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
972
973 181
                    if ($relation['isOwningSide']) {
974 105
                        $conditions[] = $sourceTableAlias . '.' . $quotedSourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn;
975
976 105
                        continue;
977
                    }
978
979 111
                    $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $quotedSourceColumn;
980
                }
981
982
                // Apply remaining inheritance restrictions
983 181
                $discrSql = $this->_generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]);
984
985 181
                if ($discrSql) {
986 3
                    $conditions[] = $discrSql;
987
                }
988
989
                // Apply the filters
990 181
                $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias);
991
992 181
                if ($filterExpr) {
993 1
                    $conditions[] = $filterExpr;
994
                }
995
996
                $targetTableJoin = [
997 181
                    'table' => $targetTableName . ' ' . $targetTableAlias,
998 181
                    'condition' => implode(' AND ', $conditions),
999
                ];
1000 181
                break;
1001
1002 57
            case ($assoc['type'] == ClassMetadata::MANY_TO_MANY):
1003
                // Join relation table
1004 57
                $joinTable      = $assoc['joinTable'];
1005 57
                $joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias);
1006 57
                $joinTableName  = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->platform);
1007
1008 57
                $conditions      = [];
1009 57
                $relationColumns = ($relation['isOwningSide'])
1010 48
                    ? $assoc['joinTable']['joinColumns']
1011 57
                    : $assoc['joinTable']['inverseJoinColumns'];
1012
1013 57
                foreach ($relationColumns as $joinColumn) {
1014 57
                    $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
1015 57
                    $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
1016
1017 57
                    $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn;
1018
                }
1019
1020 57
                $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions);
1021
1022
                // Join target table
1023 57
                $sql .= ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) ? ' LEFT JOIN ' : ' INNER JOIN ';
1024
1025 57
                $conditions      = [];
1026 57
                $relationColumns = ($relation['isOwningSide'])
1027 48
                    ? $assoc['joinTable']['inverseJoinColumns']
1028 57
                    : $assoc['joinTable']['joinColumns'];
1029
1030 57
                foreach ($relationColumns as $joinColumn) {
1031 57
                    $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
1032 57
                    $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
1033
1034 57
                    $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn;
1035
                }
1036
1037
                // Apply remaining inheritance restrictions
1038 57
                $discrSql = $this->_generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]);
1039
1040 57
                if ($discrSql) {
1041 1
                    $conditions[] = $discrSql;
1042
                }
1043
1044
                // Apply the filters
1045 57
                $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias);
1046
1047 57
                if ($filterExpr) {
1048 1
                    $conditions[] = $filterExpr;
1049
                }
1050
1051
                $targetTableJoin = [
1052 57
                    'table' => $targetTableName . ' ' . $targetTableAlias,
1053 57
                    'condition' => implode(' AND ', $conditions),
1054
                ];
1055 57
                break;
1056
1057
            default:
1058
                throw new \BadMethodCallException('Type of association must be one of *_TO_ONE or MANY_TO_MANY');
1059
        }
1060
1061
        // Handle WITH clause
1062 228
        $withCondition = (null === $condExpr) ? '' : ('(' . $this->walkConditionalExpression($condExpr) . ')');
1063
1064 228
        if ($targetClass->isInheritanceTypeJoined()) {
1065 9
            $ctiJoins = $this->_generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias);
1066
            // If we have WITH condition, we need to build nested joins for target class table and cti joins
1067 9
            if ($withCondition) {
1068 1
                $sql .= '(' . $targetTableJoin['table'] . $ctiJoins . ') ON ' . $targetTableJoin['condition'];
1069
            } else {
1070 9
                $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'] . $ctiJoins;
1071
            }
1072
        } else {
1073 219
            $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'];
1074
        }
1075
1076 228
        if ($withCondition) {
1077 5
            $sql .= ' AND ' . $withCondition;
1078
        }
1079
1080
        // Apply the indexes
1081 228
        if ($indexBy) {
1082
            // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently.
1083 6
            $this->walkIndexBy($indexBy);
1084 223
        } else if (isset($relation['indexBy'])) {
1085 3
            $this->rsm->addIndexBy($joinedDqlAlias, $relation['indexBy']);
1086
        }
1087
1088 228
        return $sql;
1089
    }
1090
1091
    /**
1092
     * {@inheritdoc}
1093
     */
1094 118
    public function walkFunction($function)
1095
    {
1096 118
        return $function->getSql($this);
1097
    }
1098
1099
    /**
1100
     * {@inheritdoc}
1101
     */
1102 154
    public function walkOrderByClause($orderByClause)
1103
    {
1104 154
        $orderByItems = array_map([$this, 'walkOrderByItem'], $orderByClause->orderByItems);
1105
1106 153
        if (($collectionOrderByItems = $this->_generateOrderedCollectionOrderByItems()) !== '') {
1107
            $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems);
1108
        }
1109
1110 153
        return ' ORDER BY ' . implode(', ', $orderByItems);
1111
    }
1112
1113
    /**
1114
     * {@inheritdoc}
1115
     */
1116 172
    public function walkOrderByItem($orderByItem)
1117
    {
1118 172
        $type = strtoupper($orderByItem->type);
1119 172
        $expr = $orderByItem->expression;
1120 172
        $sql  = ($expr instanceof AST\Node)
1121 165
            ? $expr->dispatch($this)
1122 171
            : $this->walkResultVariable($this->queryComponents[$expr]['token']['value']);
1123
1124 171
        $this->orderedColumnsMap[$sql] = $type;
1125
1126 171
        if ($expr instanceof AST\Subselect) {
1127 2
            return '(' . $sql . ') ' . $type;
1128
        }
1129
1130 169
        return $sql . ' ' . $type;
1131
    }
1132
1133
    /**
1134
     * {@inheritdoc}
1135
     */
1136 14
    public function walkHavingClause($havingClause)
1137
    {
1138 14
        return ' HAVING ' . $this->walkConditionalExpression($havingClause->conditionalExpression);
1139
    }
1140
1141
    /**
1142
     * {@inheritdoc}
1143
     */
1144 248
    public function walkJoin($join)
1145
    {
1146 248
        $joinType        = $join->joinType;
1147 248
        $joinDeclaration = $join->joinAssociationDeclaration;
1148
1149 248
        $sql = ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER)
1150 57
            ? ' LEFT JOIN '
1151 248
            : ' INNER JOIN ';
1152
1153
        switch (true) {
1154 248
            case ($joinDeclaration instanceof AST\RangeVariableDeclaration):
1155 18
                $class      = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName);
1156 18
                $dqlAlias   = $joinDeclaration->aliasIdentificationVariable;
1157 18
                $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias);
1158 18
                $conditions = [];
1159
1160 18
                if ($join->conditionalExpression) {
1161 16
                    $conditions[] = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')';
1162
                }
1163
1164 18
                $isUnconditionalJoin = empty($conditions);
1165 18
                $condExprConjunction = ($class->isInheritanceTypeJoined() && $joinType != AST\Join::JOIN_TYPE_LEFT && $joinType != AST\Join::JOIN_TYPE_LEFTOUTER && $isUnconditionalJoin)
1166 2
                    ? ' AND '
1167 18
                    : ' ON ';
1168
1169 18
                $sql .= $this->generateRangeVariableDeclarationSQL($joinDeclaration, !$isUnconditionalJoin);
1170
1171
                // Apply remaining inheritance restrictions
1172 18
                $discrSql = $this->_generateDiscriminatorColumnConditionSQL([$dqlAlias]);
1173
1174 18
                if ($discrSql) {
1175 3
                    $conditions[] = $discrSql;
1176
                }
1177
1178
                // Apply the filters
1179 18
                $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias);
1180
1181 18
                if ($filterExpr) {
1182
                    $conditions[] = $filterExpr;
1183
                }
1184
1185 18
                if ($conditions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $conditions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1186 16
                    $sql .= $condExprConjunction . implode(' AND ', $conditions);
1187
                }
1188
1189 18
                break;
1190
1191 230
            case ($joinDeclaration instanceof AST\JoinAssociationDeclaration):
1192 230
                $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType, $join->conditionalExpression);
1193 228
                break;
1194
        }
1195
1196 246
        return $sql;
1197
    }
1198
1199
    /**
1200
     * Walks down a CoalesceExpression AST node and generates the corresponding SQL.
1201
     *
1202
     * @param AST\CoalesceExpression $coalesceExpression
1203
     *
1204
     * @return string The SQL.
1205
     */
1206 2
    public function walkCoalesceExpression($coalesceExpression)
1207
    {
1208 2
        $sql = 'COALESCE(';
1209
1210 2
        $scalarExpressions = [];
1211
1212 2
        foreach ($coalesceExpression->scalarExpressions as $scalarExpression) {
1213 2
            $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression);
1214
        }
1215
1216 2
        $sql .= implode(', ', $scalarExpressions) . ')';
1217
1218 2
        return $sql;
1219
    }
1220
1221
    /**
1222
     * Walks down a NullIfExpression AST node and generates the corresponding SQL.
1223
     *
1224
     * @param AST\NullIfExpression $nullIfExpression
1225
     *
1226
     * @return string The SQL.
1227
     */
1228 3
    public function walkNullIfExpression($nullIfExpression)
1229
    {
1230 3
        $firstExpression = is_string($nullIfExpression->firstExpression)
1231
            ? $this->conn->quote($nullIfExpression->firstExpression)
1232 3
            : $this->walkSimpleArithmeticExpression($nullIfExpression->firstExpression);
1233
1234 3
        $secondExpression = is_string($nullIfExpression->secondExpression)
1235
            ? $this->conn->quote($nullIfExpression->secondExpression)
1236 3
            : $this->walkSimpleArithmeticExpression($nullIfExpression->secondExpression);
1237
1238 3
        return 'NULLIF(' . $firstExpression . ', ' . $secondExpression . ')';
1239
    }
1240
1241
    /**
1242
     * Walks down a GeneralCaseExpression AST node and generates the corresponding SQL.
1243
     *
1244
     * @param AST\GeneralCaseExpression $generalCaseExpression
1245
     *
1246
     * @return string The SQL.
1247
     */
1248 9
    public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression)
1249
    {
1250 9
        $sql = 'CASE';
1251
1252 9
        foreach ($generalCaseExpression->whenClauses as $whenClause) {
1253 9
            $sql .= ' WHEN ' . $this->walkConditionalExpression($whenClause->caseConditionExpression);
1254 9
            $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($whenClause->thenScalarExpression);
1255
        }
1256
1257 9
        $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression) . ' END';
1258
1259 9
        return $sql;
1260
    }
1261
1262
    /**
1263
     * Walks down a SimpleCaseExpression AST node and generates the corresponding SQL.
1264
     *
1265
     * @param AST\SimpleCaseExpression $simpleCaseExpression
1266
     *
1267
     * @return string The SQL.
1268
     */
1269 5
    public function walkSimpleCaseExpression($simpleCaseExpression)
1270
    {
1271 5
        $sql = 'CASE ' . $this->walkStateFieldPathExpression($simpleCaseExpression->caseOperand);
1272
1273 5
        foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) {
1274 5
            $sql .= ' WHEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression);
1275 5
            $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression);
1276
        }
1277
1278 5
        $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression) . ' END';
1279
1280 5
        return $sql;
1281
    }
1282
1283
    /**
1284
     * {@inheritdoc}
1285
     */
1286 635
    public function walkSelectExpression($selectExpression)
1287
    {
1288 635
        $sql    = '';
1289 635
        $expr   = $selectExpression->expression;
1290 635
        $hidden = $selectExpression->hiddenAliasResultVariable;
1291
1292
        switch (true) {
1293 635
            case ($expr instanceof AST\PathExpression):
1294 103
                if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) {
1295
                    throw QueryException::invalidPathExpression($expr);
1296
                }
1297
1298 103
                $fieldName = $expr->field;
1299 103
                $dqlAlias  = $expr->identificationVariable;
1300 103
                $qComp     = $this->queryComponents[$dqlAlias];
1301 103
                $class     = $qComp['metadata'];
1302
1303 103
                $resultAlias = $selectExpression->fieldIdentificationVariable ?: $fieldName;
1304 103
                $tableName   = ($class->isInheritanceTypeJoined())
1305 11
                    ? $this->em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName)
1306 103
                    : $class->getTableName();
1307
1308 103
                $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
1309 103
                $fieldMapping  = $class->fieldMappings[$fieldName];
1310 103
                $columnName    = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
1311 103
                $columnAlias   = $this->getSQLColumnAlias($fieldMapping['columnName']);
1312 103
                $col           = $sqlTableAlias . '.' . $columnName;
1313
1314 103
                if (isset($fieldMapping['requireSQLConversion'])) {
1315 2
                    $type = Type::getType($fieldMapping['type']);
1316 2
                    $col  = $type->convertToPHPValueSQL($col, $this->conn->getDatabasePlatform());
1317
                }
1318
1319 103
                $sql .= $col . ' AS ' . $columnAlias;
1320
1321 103
                $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1322
1323 103
                if ( ! $hidden) {
1324 103
                    $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldMapping['type']);
1325 103
                    $this->scalarFields[$dqlAlias][$fieldName] = $columnAlias;
1326
                }
1327
1328 103
                break;
1329
1330 584
            case ($expr instanceof AST\AggregateExpression):
1331 575
            case ($expr instanceof AST\Functions\FunctionNode):
1332 535
            case ($expr instanceof AST\SimpleArithmeticExpression):
1333 535
            case ($expr instanceof AST\ArithmeticTerm):
1334 533
            case ($expr instanceof AST\ArithmeticFactor):
1335 532
            case ($expr instanceof AST\ParenthesisExpression):
1336 531
            case ($expr instanceof AST\Literal):
1337 530
            case ($expr instanceof AST\NullIfExpression):
1338 529
            case ($expr instanceof AST\CoalesceExpression):
1339 528
            case ($expr instanceof AST\GeneralCaseExpression):
1340 524
            case ($expr instanceof AST\SimpleCaseExpression):
1341 107
                $columnAlias = $this->getSQLColumnAlias('sclr');
1342 107
                $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1343
1344 107
                $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias;
1345
1346 107
                $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1347
1348 107
                if ( ! $hidden) {
1349
                    // We cannot resolve field type here; assume 'string'.
1350 107
                    $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string');
1351
                }
1352 107
                break;
1353
1354 523
            case ($expr instanceof AST\Subselect):
1355 15
                $columnAlias = $this->getSQLColumnAlias('sclr');
1356 15
                $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1357
1358 15
                $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias;
1359
1360 15
                $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1361
1362 15
                if ( ! $hidden) {
1363
                    // We cannot resolve field type here; assume 'string'.
1364 13
                    $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string');
1365
                }
1366 15
                break;
1367
1368 519
            case ($expr instanceof AST\NewObjectExpression):
1369 23
                $sql .= $this->walkNewObject($expr,$selectExpression->fieldIdentificationVariable);
1370 23
                break;
1371
1372
            default:
1373
                // IdentificationVariable or PartialObjectExpression
1374 496
                if ($expr instanceof AST\PartialObjectExpression) {
1375 16
                    $dqlAlias = $expr->identificationVariable;
1376 16
                    $partialFieldSet = $expr->partialFieldSet;
1377
                } else {
1378 491
                    $dqlAlias = $expr;
1379 491
                    $partialFieldSet = [];
1380
                }
1381
1382 496
                $queryComp   = $this->queryComponents[$dqlAlias];
1383 496
                $class       = $queryComp['metadata'];
1384 496
                $resultAlias = $selectExpression->fieldIdentificationVariable ?: null;
1385
1386 496
                if ( ! isset($this->selectedClasses[$dqlAlias])) {
1387 496
                    $this->selectedClasses[$dqlAlias] = [
1388 496
                        'class'       => $class,
1389 496
                        'dqlAlias'    => $dqlAlias,
1390 496
                        'resultAlias' => $resultAlias
1391
                    ];
1392
                }
1393
1394 496
                $sqlParts = [];
1395
1396
                // Select all fields from the queried class
1397 496
                foreach ($class->fieldMappings as $fieldName => $mapping) {
1398 495
                    if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $partialFieldSet of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1399 14
                        continue;
1400
                    }
1401
1402 494
                    $tableName = (isset($mapping['inherited']))
1403 52
                        ? $this->em->getClassMetadata($mapping['inherited'])->getTableName()
1404 494
                        : $class->getTableName();
1405
1406 494
                    $sqlTableAlias    = $this->getSQLTableAlias($tableName, $dqlAlias);
1407 494
                    $columnAlias      = $this->getSQLColumnAlias($mapping['columnName']);
1408 494
                    $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
1409
1410 494
                    $col = $sqlTableAlias . '.' . $quotedColumnName;
1411
1412 494
                    if (isset($mapping['requireSQLConversion'])) {
1413 5
                        $type = Type::getType($mapping['type']);
1414 5
                        $col = $type->convertToPHPValueSQL($col, $this->platform);
1415
                    }
1416
1417 494
                    $sqlParts[] = $col . ' AS '. $columnAlias;
1418
1419 494
                    $this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
1420
1421 494
                    $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);
1422
                }
1423
1424
                // Add any additional fields of subclasses (excluding inherited fields)
1425
                // 1) on Single Table Inheritance: always, since its marginal overhead
1426
                // 2) on Class Table Inheritance only if partial objects are disallowed,
1427
                //    since it requires outer joining subtables.
1428 496
                if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
1429 403
                    foreach ($class->subClasses as $subClassName) {
1430 48
                        $subClass      = $this->em->getClassMetadata($subClassName);
1431 48
                        $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
1432
1433 48
                        foreach ($subClass->fieldMappings as $fieldName => $mapping) {
1434 48
                            if (isset($mapping['inherited']) || ($partialFieldSet && !in_array($fieldName, $partialFieldSet))) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $partialFieldSet of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1435 48
                                continue;
1436
                            }
1437
1438 35
                            $columnAlias      = $this->getSQLColumnAlias($mapping['columnName']);
1439 35
                            $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
1440
1441 35
                            $col = $sqlTableAlias . '.' . $quotedColumnName;
1442
1443 35
                            if (isset($mapping['requireSQLConversion'])) {
1444
                                $type = Type::getType($mapping['type']);
1445
                                $col = $type->convertToPHPValueSQL($col, $this->platform);
1446
                            }
1447
1448 35
                            $sqlParts[] = $col . ' AS ' . $columnAlias;
1449
1450 35
                            $this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
1451
1452 48
                            $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
1453
                        }
1454
                    }
1455
                }
1456
1457 496
                $sql .= implode(', ', $sqlParts);
1458
        }
1459
1460 635
        return $sql;
1461
    }
1462
1463
    /**
1464
     * {@inheritdoc}
1465
     */
1466
    public function walkQuantifiedExpression($qExpr)
1467
    {
1468
        return ' ' . strtoupper($qExpr->type) . '(' . $this->walkSubselect($qExpr->subselect) . ')';
1469
    }
1470
1471
    /**
1472
     * {@inheritdoc}
1473
     */
1474 33
    public function walkSubselect($subselect)
1475
    {
1476 33
        $useAliasesBefore  = $this->useSqlTableAliases;
1477 33
        $rootAliasesBefore = $this->rootAliases;
1478
1479 33
        $this->rootAliases = []; // reset the rootAliases for the subselect
1480 33
        $this->useSqlTableAliases = true;
1481
1482 33
        $sql  = $this->walkSimpleSelectClause($subselect->simpleSelectClause);
1483 33
        $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause);
1484 33
        $sql .= $this->walkWhereClause($subselect->whereClause);
0 ignored issues
show
Bug introduced by
It seems like $subselect->whereClause can be null; however, walkWhereClause() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1485
1486 33
        $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : '';
1487 33
        $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : '';
1488 33
        $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : '';
1489
1490 33
        $this->rootAliases        = $rootAliasesBefore; // put the main aliases back
1491 33
        $this->useSqlTableAliases = $useAliasesBefore;
1492
1493 33
        return $sql;
1494
    }
1495
1496
    /**
1497
     * {@inheritdoc}
1498
     */
1499 33
    public function walkSubselectFromClause($subselectFromClause)
1500
    {
1501 33
        $identificationVarDecls = $subselectFromClause->identificationVariableDeclarations;
1502 33
        $sqlParts               = [];
1503
1504 33
        foreach ($identificationVarDecls as $subselectIdVarDecl) {
1505 33
            $sqlParts[] = $this->walkIdentificationVariableDeclaration($subselectIdVarDecl);
1506
        }
1507
1508 33
        return ' FROM ' . implode(', ', $sqlParts);
1509
    }
1510
1511
    /**
1512
     * {@inheritdoc}
1513
     */
1514 33
    public function walkSimpleSelectClause($simpleSelectClause)
1515
    {
1516 33
        return 'SELECT' . ($simpleSelectClause->isDistinct ? ' DISTINCT' : '')
1517 33
            . $this->walkSimpleSelectExpression($simpleSelectClause->simpleSelectExpression);
1518
    }
1519
1520
    /**
1521
     * @param \Doctrine\ORM\Query\AST\ParenthesisExpression $parenthesisExpression
1522
     *
1523
     * @return string.
0 ignored issues
show
Documentation introduced by
The doc-type string. could not be parsed: Unknown type name "string." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1524
     */
1525 22
    public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesisExpression)
1526
    {
1527 22
        return sprintf('(%s)', $parenthesisExpression->expression->dispatch($this));
1528
    }
1529
1530
    /**
1531
     * @param AST\NewObjectExpression   $newObjectExpression
1532
     * @param null|string               $newObjectResultAlias
1533
     * @return string The SQL.
1534
     */
1535 23
    public function walkNewObject($newObjectExpression, $newObjectResultAlias=null)
1536
    {
1537 23
        $sqlSelectExpressions = [];
1538
1539 23
        $objOwner = $objOwnerIdx = null;
1540 23
        if (!empty($this->newObjectStack)) {
1541 1
            [$objOwner, $objOwnerIdx] = end($this->newObjectStack);
1542 1
            $objIndex = "$objOwner:$objOwnerIdx";
1543
        } else {
1544 23
            $objIndex = $newObjectResultAlias?:$this->newObjectCounter++;
1545
        }
1546
1547 23
        foreach ($newObjectExpression->args as $argIndex => $e) {
1548 23
            $resultAlias = $this->scalarResultCounter++;
1549 23
            $columnAlias = $this->getSQLColumnAlias('sclr');
1550 23
            $fieldType   = 'string';
1551
1552
            switch (true) {
1553 23
                case ($e instanceof AST\NewObjectExpression):
1554 1
                    array_push($this->newObjectStack, [$objIndex, $argIndex]);
1555 1
                    $sqlSelectExpressions[] = $e->dispatch($this);
1556 1
                    array_pop($this->newObjectStack);
1557
1558 1
                    break;
1559
1560 23
                case ($e instanceof AST\Subselect):
1561 1
                    $sqlSelectExpressions[] = '(' . $e->dispatch($this) . ') AS ' . $columnAlias;
1562 1
                    break;
1563
1564 23
                case ($e instanceof AST\PathExpression):
1565 23
                    $dqlAlias  = $e->identificationVariable;
1566 23
                    $qComp     = $this->queryComponents[$dqlAlias];
1567 23
                    $class     = $qComp['metadata'];
1568 23
                    $fieldType = $class->fieldMappings[$e->field]['type'];
1569
1570 23
                    $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
1571 23
                    break;
1572
1573 6
                case ($e instanceof AST\Literal):
1574 1
                    switch ($e->type) {
1575 1
                        case AST\Literal::BOOLEAN:
1576 1
                            $fieldType = 'boolean';
1577 1
                            break;
1578
1579 1
                        case AST\Literal::NUMERIC:
1580 1
                            $fieldType = is_float($e->value) ? 'float' : 'integer';
1581 1
                            break;
1582
                    }
1583
1584 1
                    $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
1585 1
                    break;
1586
1587
                default:
1588 5
                    $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
1589 5
                    break;
1590
            }
1591
1592 23
            $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1593 23
            $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType);
1594
1595 23
            $this->rsm->newObjectMappings[$columnAlias] = [
1596 23
                'className'      => $newObjectExpression->className,
1597 23
                'objIndex'       => $objIndex,
1598 23
                'argIndex'       => $argIndex,
1599
            ];
1600
1601 23
            if ($objOwner !== null) {
1602 23
                $this->rsm->addNewObjectAsArgument($objIndex, $objOwner, $objOwnerIdx);
1603
            }
1604
        }
1605
1606 23
        return implode(', ', $sqlSelectExpressions);
1607
    }
1608
1609
    /**
1610
     * {@inheritdoc}
1611
     */
1612 33
    public function walkSimpleSelectExpression($simpleSelectExpression)
1613
    {
1614 33
        $expr = $simpleSelectExpression->expression;
1615 33
        $sql  = ' ';
1616
1617
        switch (true) {
1618 33
            case ($expr instanceof AST\PathExpression):
1619 9
                $sql .= $this->walkPathExpression($expr);
1620 9
                break;
1621
1622 24
            case ($expr instanceof AST\Subselect):
1623
                $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1624
1625
                $columnAlias = 'sclr' . $this->aliasCounter++;
1626
                $this->scalarResultAliasMap[$alias] = $columnAlias;
1627
1628
                $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias;
1629
                break;
1630
1631 24
            case ($expr instanceof AST\Functions\FunctionNode):
1632 11
            case ($expr instanceof AST\SimpleArithmeticExpression):
1633 10
            case ($expr instanceof AST\ArithmeticTerm):
1634 9
            case ($expr instanceof AST\ArithmeticFactor):
1635 9
            case ($expr instanceof AST\Literal):
1636 7
            case ($expr instanceof AST\NullIfExpression):
1637 7
            case ($expr instanceof AST\CoalesceExpression):
1638 7
            case ($expr instanceof AST\GeneralCaseExpression):
1639 5
            case ($expr instanceof AST\SimpleCaseExpression):
1640 21
                $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1641
1642 21
                $columnAlias = $this->getSQLColumnAlias('sclr');
1643 21
                $this->scalarResultAliasMap[$alias] = $columnAlias;
1644
1645 21
                $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias;
1646 21
                break;
1647
1648 3
            case ($expr instanceof AST\ParenthesisExpression):
1649 1
                $sql .= $this->walkParenthesisExpression($expr);
1650 1
                break;
1651
1652
            default: // IdentificationVariable
1653 2
                $sql .= $this->walkEntityIdentificationVariable($expr);
1654 2
                break;
1655
        }
1656
1657 33
        return $sql;
1658
    }
1659
1660
    /**
1661
     * {@inheritdoc}
1662
     */
1663 76
    public function walkAggregateExpression($aggExpression)
1664
    {
1665 76
        return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '')
1666 76
            . $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) . ')';
0 ignored issues
show
Bug introduced by
It seems like $aggExpression->pathExpression can also be of type object<Doctrine\ORM\Query\AST\PathExpression>; however, Doctrine\ORM\Query\SqlWa...eArithmeticExpression() does only seem to accept object<Doctrine\ORM\Quer...leArithmeticExpression>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1667
    }
1668
1669
    /**
1670
     * {@inheritdoc}
1671
     */
1672 24
    public function walkGroupByClause($groupByClause)
1673
    {
1674 24
        $sqlParts = [];
1675
1676 24
        foreach ($groupByClause->groupByItems as $groupByItem) {
1677 24
            $sqlParts[] = $this->walkGroupByItem($groupByItem);
1678
        }
1679
1680 24
        return ' GROUP BY ' . implode(', ', $sqlParts);
1681
    }
1682
1683
    /**
1684
     * {@inheritdoc}
1685
     */
1686 24
    public function walkGroupByItem($groupByItem)
1687
    {
1688
        // StateFieldPathExpression
1689 24
        if ( ! is_string($groupByItem)) {
1690 11
            return $this->walkPathExpression($groupByItem);
1691
        }
1692
1693
        // ResultVariable
1694 13
        if (isset($this->queryComponents[$groupByItem]['resultVariable'])) {
1695 2
            $resultVariable = $this->queryComponents[$groupByItem]['resultVariable'];
1696
1697 2
            if ($resultVariable instanceof AST\PathExpression) {
1698 1
                return $this->walkPathExpression($resultVariable);
1699
            }
1700
1701 1
            if (isset($resultVariable->pathExpression)) {
1702
                return $this->walkPathExpression($resultVariable->pathExpression);
1703
            }
1704
1705 1
            return $this->walkResultVariable($groupByItem);
1706
        }
1707
1708
        // IdentificationVariable
1709 11
        $sqlParts = [];
1710
1711 11
        foreach ($this->queryComponents[$groupByItem]['metadata']->fieldNames as $field) {
1712 11
            $item       = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $field);
1713 11
            $item->type = AST\PathExpression::TYPE_STATE_FIELD;
1714
1715 11
            $sqlParts[] = $this->walkPathExpression($item);
1716
        }
1717
1718 11
        foreach ($this->queryComponents[$groupByItem]['metadata']->associationMappings as $mapping) {
1719 11
            if ($mapping['isOwningSide'] && $mapping['type'] & ClassMetadataInfo::TO_ONE) {
1720 7
                $item       = new AST\PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $groupByItem, $mapping['fieldName']);
1721 7
                $item->type = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
1722
1723 11
                $sqlParts[] = $this->walkPathExpression($item);
1724
            }
1725
        }
1726
1727 11
        return implode(', ', $sqlParts);
1728
    }
1729
1730
    /**
1731
     * {@inheritdoc}
1732
     */
1733 36
    public function walkDeleteClause(AST\DeleteClause $deleteClause)
1734
    {
1735 36
        $class     = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1736 36
        $tableName = $class->getTableName();
1737 36
        $sql       = 'DELETE FROM ' . $this->quoteStrategy->getTableName($class, $this->platform);
1738
1739 36
        $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable);
1740 36
        $this->rootAliases[] = $deleteClause->aliasIdentificationVariable;
1741
1742 36
        return $sql;
1743
    }
1744
1745
    /**
1746
     * {@inheritdoc}
1747
     */
1748 25
    public function walkUpdateClause($updateClause)
1749
    {
1750 25
        $class     = $this->em->getClassMetadata($updateClause->abstractSchemaName);
1751 25
        $tableName = $class->getTableName();
1752 25
        $sql       = 'UPDATE ' . $this->quoteStrategy->getTableName($class, $this->platform);
1753
1754 25
        $this->setSQLTableAlias($tableName, $tableName, $updateClause->aliasIdentificationVariable);
1755 25
        $this->rootAliases[] = $updateClause->aliasIdentificationVariable;
1756
1757 25
        $sql .= ' SET ' . implode(', ', array_map([$this, 'walkUpdateItem'], $updateClause->updateItems));
1758
1759 25
        return $sql;
1760
    }
1761
1762
    /**
1763
     * {@inheritdoc}
1764
     */
1765 29
    public function walkUpdateItem($updateItem)
1766
    {
1767 29
        $useTableAliasesBefore = $this->useSqlTableAliases;
1768 29
        $this->useSqlTableAliases = false;
1769
1770 29
        $sql      = $this->walkPathExpression($updateItem->pathExpression) . ' = ';
1771 29
        $newValue = $updateItem->newValue;
1772
1773
        switch (true) {
1774 29
            case ($newValue instanceof AST\Node):
1775 28
                $sql .= $newValue->dispatch($this);
1776 28
                break;
1777
1778 1
            case ($newValue === null):
1779 1
                $sql .= 'NULL';
1780 1
                break;
1781
1782
            default:
1783
                $sql .= $this->conn->quote($newValue);
1784
                break;
1785
        }
1786
1787 29
        $this->useSqlTableAliases = $useTableAliasesBefore;
1788
1789 29
        return $sql;
1790
    }
1791
1792
    /**
1793
     * {@inheritdoc}
1794
     */
1795 692
    public function walkWhereClause($whereClause)
1796
    {
1797 692
        $condSql  = null !== $whereClause ? $this->walkConditionalExpression($whereClause->conditionalExpression) : '';
1798 689
        $discrSql = $this->_generateDiscriminatorColumnConditionSQL($this->rootAliases);
1799
1800 689
        if ($this->em->hasFilters()) {
1801 43
            $filterClauses = [];
1802 43
            foreach ($this->rootAliases as $dqlAlias) {
1803 43
                $class = $this->queryComponents[$dqlAlias]['metadata'];
1804 43
                $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias);
1805
1806 43
                if ($filterExpr = $this->generateFilterConditionSQL($class, $tableAlias)) {
1807 43
                    $filterClauses[] = $filterExpr;
1808
                }
1809
            }
1810
1811 43
            if (count($filterClauses)) {
1812 6
                if ($condSql) {
1813 2
                    $condSql = '(' . $condSql . ') AND ';
1814
                }
1815
1816 6
                $condSql .= implode(' AND ', $filterClauses);
1817
            }
1818
        }
1819
1820 689
        if ($condSql) {
1821 340
            return ' WHERE ' . (( ! $discrSql) ? $condSql : '(' . $condSql . ') AND ' . $discrSql);
1822
        }
1823
1824 425
        if ($discrSql) {
1825 23
            return ' WHERE ' . $discrSql;
1826
        }
1827
1828 405
        return '';
1829
    }
1830
1831
    /**
1832
     * {@inheritdoc}
1833
     */
1834 372
    public function walkConditionalExpression($condExpr)
1835
    {
1836
        // Phase 2 AST optimization: Skip processing of ConditionalExpression
1837
        // if only one ConditionalTerm is defined
1838 372
        if ( ! ($condExpr instanceof AST\ConditionalExpression)) {
1839 317
            return $this->walkConditionalTerm($condExpr);
1840
        }
1841
1842 72
        return implode(' OR ', array_map([$this, 'walkConditionalTerm'], $condExpr->conditionalTerms));
1843
    }
1844
1845
    /**
1846
     * {@inheritdoc}
1847
     */
1848 372
    public function walkConditionalTerm($condTerm)
1849
    {
1850
        // Phase 2 AST optimization: Skip processing of ConditionalTerm
1851
        // if only one ConditionalFactor is defined
1852 372
        if ( ! ($condTerm instanceof AST\ConditionalTerm)) {
1853 305
            return $this->walkConditionalFactor($condTerm);
1854
        }
1855
1856 90
        return implode(' AND ', array_map([$this, 'walkConditionalFactor'], $condTerm->conditionalFactors));
1857
    }
1858
1859
    /**
1860
     * {@inheritdoc}
1861
     */
1862 372
    public function walkConditionalFactor($factor)
1863
    {
1864
        // Phase 2 AST optimization: Skip processing of ConditionalFactor
1865
        // if only one ConditionalPrimary is defined
1866 372
        return ( ! ($factor instanceof AST\ConditionalFactor))
1867 369
            ? $this->walkConditionalPrimary($factor)
1868 369
            : ($factor->not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary);
1869
    }
1870
1871
    /**
1872
     * {@inheritdoc}
1873
     */
1874 372
    public function walkConditionalPrimary($primary)
1875
    {
1876 372
        if ($primary->isSimpleConditionalExpression()) {
1877 372
            return $primary->simpleConditionalExpression->dispatch($this);
1878
        }
1879
1880 25
        if ($primary->isConditionalExpression()) {
1881 25
            $condExpr = $primary->conditionalExpression;
1882
1883 25
            return '(' . $this->walkConditionalExpression($condExpr) . ')';
0 ignored issues
show
Bug introduced by
It seems like $condExpr defined by $primary->conditionalExpression on line 1881 can be null; however, Doctrine\ORM\Query\SqlWa...ConditionalExpression() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
1884
        }
1885
    }
1886
1887
    /**
1888
     * {@inheritdoc}
1889
     */
1890 5
    public function walkExistsExpression($existsExpr)
1891
    {
1892 5
        $sql = ($existsExpr->not) ? 'NOT ' : '';
1893
1894 5
        $sql .= 'EXISTS (' . $this->walkSubselect($existsExpr->subselect) . ')';
1895
1896 5
        return $sql;
1897
    }
1898
1899
    /**
1900
     * {@inheritdoc}
1901
     */
1902 6
    public function walkCollectionMemberExpression($collMemberExpr)
1903
    {
1904 6
        $sql = $collMemberExpr->not ? 'NOT ' : '';
1905 6
        $sql .= 'EXISTS (SELECT 1 FROM ';
1906
1907 6
        $entityExpr   = $collMemberExpr->entityExpression;
1908 6
        $collPathExpr = $collMemberExpr->collectionValuedPathExpression;
1909
1910 6
        $fieldName = $collPathExpr->field;
1911 6
        $dqlAlias  = $collPathExpr->identificationVariable;
1912
1913 6
        $class = $this->queryComponents[$dqlAlias]['metadata'];
1914
1915
        switch (true) {
1916
            // InputParameter
1917 6
            case ($entityExpr instanceof AST\InputParameter):
1918 4
                $dqlParamKey = $entityExpr->name;
1919 4
                $entitySql   = '?';
1920 4
                break;
1921
1922
            // SingleValuedAssociationPathExpression | IdentificationVariable
1923 2
            case ($entityExpr instanceof AST\PathExpression):
1924 2
                $entitySql = $this->walkPathExpression($entityExpr);
1925 2
                break;
1926
1927
            default:
1928
                throw new \BadMethodCallException("Not implemented");
1929
        }
1930
1931 6
        $assoc = $class->associationMappings[$fieldName];
1932
1933 6
        if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) {
1934 1
            $targetClass      = $this->em->getClassMetadata($assoc['targetEntity']);
1935 1
            $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName());
1936 1
            $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
1937
1938 1
            $sql .= $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' ' . $targetTableAlias . ' WHERE ';
1939
1940 1
            $owningAssoc = $targetClass->associationMappings[$assoc['mappedBy']];
1941 1
            $sqlParts    = [];
1942
1943 1
            foreach ($owningAssoc['targetToSourceKeyColumns'] as $targetColumn => $sourceColumn) {
1944 1
                $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $this->platform);
1945
1946 1
                $sqlParts[] = $sourceTableAlias . '.' . $targetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn;
1947
            }
1948
1949 1
            foreach ($this->quoteStrategy->getIdentifierColumnNames($targetClass, $this->platform) as $targetColumnName) {
1950 1
                if (isset($dqlParamKey)) {
1951 1
                    $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++);
1952
                }
1953
1954 1
                $sqlParts[] = $targetTableAlias . '.'  . $targetColumnName . ' = ' . $entitySql;
1955
            }
1956
1957 1
            $sql .= implode(' AND ', $sqlParts);
1958
        } else { // many-to-many
1959 5
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1960
1961 5
            $owningAssoc = $assoc['isOwningSide'] ? $assoc : $targetClass->associationMappings[$assoc['mappedBy']];
1962 5
            $joinTable = $owningAssoc['joinTable'];
1963
1964
            // SQL table aliases
1965 5
            $joinTableAlias   = $this->getSQLTableAlias($joinTable['name']);
1966 5
            $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName());
1967 5
            $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
1968
1969
            // join to target table
1970 5
            $sql .= $this->quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $this->platform) . ' ' . $joinTableAlias
1971 5
                . ' INNER JOIN ' . $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' ' . $targetTableAlias . ' ON ';
1972
1973
            // join conditions
1974 5
            $joinColumns  = $assoc['isOwningSide'] ? $joinTable['inverseJoinColumns'] : $joinTable['joinColumns'];
1975 5
            $joinSqlParts = [];
1976
1977 5
            foreach ($joinColumns as $joinColumn) {
1978 5
                $targetColumn = $this->quoteStrategy->getColumnName($targetClass->fieldNames[$joinColumn['referencedColumnName']], $targetClass, $this->platform);
1979
1980 5
                $joinSqlParts[] = $joinTableAlias . '.' . $joinColumn['name'] . ' = ' . $targetTableAlias . '.' . $targetColumn;
1981
            }
1982
1983 5
            $sql .= implode(' AND ', $joinSqlParts);
1984 5
            $sql .= ' WHERE ';
1985
1986 5
            $joinColumns = $assoc['isOwningSide'] ? $joinTable['joinColumns'] : $joinTable['inverseJoinColumns'];
1987 5
            $sqlParts    = [];
1988
1989 5
            foreach ($joinColumns as $joinColumn) {
1990 5
                $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$joinColumn['referencedColumnName']], $class, $this->platform);
1991
1992 5
                $sqlParts[] = $joinTableAlias . '.' . $joinColumn['name'] . ' = ' . $sourceTableAlias . '.' . $targetColumn;
1993
            }
1994
1995 5
            foreach ($this->quoteStrategy->getIdentifierColumnNames($targetClass, $this->platform) as $targetColumnName) {
1996 5
                if (isset($dqlParamKey)) {
1997 3
                    $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++);
1998
                }
1999
2000 5
                $sqlParts[] = $targetTableAlias . '.' . $targetColumnName . ' IN (' . $entitySql . ')';
2001
            }
2002
2003 5
            $sql .= implode(' AND ', $sqlParts);
2004
        }
2005
2006 6
        return $sql . ')';
2007
    }
2008
2009
    /**
2010
     * {@inheritdoc}
2011
     */
2012 3
    public function walkEmptyCollectionComparisonExpression($emptyCollCompExpr)
2013
    {
2014 3
        $sizeFunc = new AST\Functions\SizeFunction('size');
2015 3
        $sizeFunc->collectionPathExpression = $emptyCollCompExpr->expression;
2016
2017 3
        return $sizeFunc->getSql($this) . ($emptyCollCompExpr->not ? ' > 0' : ' = 0');
2018
    }
2019
2020
    /**
2021
     * {@inheritdoc}
2022
     */
2023 11
    public function walkNullComparisonExpression($nullCompExpr)
2024
    {
2025 11
        $expression = $nullCompExpr->expression;
2026 11
        $comparison = ' IS' . ($nullCompExpr->not ? ' NOT' : '') . ' NULL';
2027
2028
        // Handle ResultVariable
2029 11
        if (is_string($expression) && isset($this->queryComponents[$expression]['resultVariable'])) {
2030 2
            return $this->walkResultVariable($expression) . $comparison;
2031
        }
2032
2033
        // Handle InputParameter mapping inclusion to ParserResult
2034 9
        if ($expression instanceof AST\InputParameter) {
2035
            return $this->walkInputParameter($expression) . $comparison;
2036
        }
2037
2038 9
        return $expression->dispatch($this) . $comparison;
2039
    }
2040
2041
    /**
2042
     * {@inheritdoc}
2043
     */
2044 86
    public function walkInExpression($inExpr)
2045
    {
2046 86
        $sql = $this->walkArithmeticExpression($inExpr->expression) . ($inExpr->not ? ' NOT' : '') . ' IN (';
2047
2048 85
        $sql .= ($inExpr->subselect)
2049 7
            ? $this->walkSubselect($inExpr->subselect)
2050 85
            : implode(', ', array_map([$this, 'walkInParameter'], $inExpr->literals));
2051
2052 85
        $sql .= ')';
2053
2054 85
        return $sql;
2055
    }
2056
2057
    /**
2058
     * {@inheritdoc}
2059
     * @throws \Doctrine\ORM\Query\QueryException
2060
     */
2061 14
    public function walkInstanceOfExpression($instanceOfExpr)
2062
    {
2063 14
        $sql = '';
2064
2065 14
        $dqlAlias = $instanceOfExpr->identificationVariable;
2066 14
        $discrClass = $class = $this->queryComponents[$dqlAlias]['metadata'];
2067
2068 14
        if ($class->discriminatorColumn) {
2069 14
            $discrClass = $this->em->getClassMetadata($class->rootEntityName);
2070
        }
2071
2072 14
        if ($this->useSqlTableAliases) {
2073 14
            $sql .= $this->getSQLTableAlias($discrClass->getTableName(), $dqlAlias) . '.';
2074
        }
2075
2076 14
        $sql .= $class->discriminatorColumn['name'] . ($instanceOfExpr->not ? ' NOT IN ' : ' IN ');
2077 14
        $sql .= $this->getChildDiscriminatorsFromClassMetadata($discrClass, $instanceOfExpr);
2078
2079 13
        return $sql;
2080
    }
2081
2082
    /**
2083
     * {@inheritdoc}
2084
     */
2085 82
    public function walkInParameter($inParam)
2086
    {
2087 82
        return $inParam instanceof AST\InputParameter
2088 73
            ? $this->walkInputParameter($inParam)
2089 82
            : $this->walkLiteral($inParam);
2090
    }
2091
2092
    /**
2093
     * {@inheritdoc}
2094
     */
2095 148
    public function walkLiteral($literal)
2096
    {
2097 148
        switch ($literal->type) {
2098 148
            case AST\Literal::STRING:
2099 50
                return $this->conn->quote($literal->value);
2100
2101 111
            case AST\Literal::BOOLEAN:
2102 8
                return $this->conn->getDatabasePlatform()->convertBooleans('true' === strtolower($literal->value));
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->conn->getD...ower($literal->value)); (object|integer|double|string|null|array) is incompatible with the return type declared by the interface Doctrine\ORM\Query\TreeWalker::walkLiteral of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2103
2104 104
            case AST\Literal::NUMERIC:
2105 104
                return $literal->value;
2106
2107
            default:
2108
                throw QueryException::invalidLiteral($literal);
2109
        }
2110
    }
2111
2112
    /**
2113
     * {@inheritdoc}
2114
     */
2115 6
    public function walkBetweenExpression($betweenExpr)
2116
    {
2117 6
        $sql = $this->walkArithmeticExpression($betweenExpr->expression);
2118
2119 6
        if ($betweenExpr->not) {
2120 2
            $sql .= ' NOT';
2121
        }
2122
2123 6
        $sql .= ' BETWEEN ' . $this->walkArithmeticExpression($betweenExpr->leftBetweenExpression)
2124 6
            . ' AND ' . $this->walkArithmeticExpression($betweenExpr->rightBetweenExpression);
2125
2126 6
        return $sql;
2127
    }
2128
2129
    /**
2130
     * {@inheritdoc}
2131
     */
2132 9
    public function walkLikeExpression($likeExpr)
2133
    {
2134 9
        $stringExpr = $likeExpr->stringExpression;
2135 9
        $leftExpr   = (is_string($stringExpr) && isset($this->queryComponents[$stringExpr]['resultVariable']))
2136 1
            ? $this->walkResultVariable($stringExpr)
2137 9
            : $stringExpr->dispatch($this);
2138
2139 9
        $sql = $leftExpr . ($likeExpr->not ? ' NOT' : '') . ' LIKE ';
2140
2141 9
        if ($likeExpr->stringPattern instanceof AST\InputParameter) {
2142 4
            $sql .= $this->walkInputParameter($likeExpr->stringPattern);
2143 6
        } elseif ($likeExpr->stringPattern instanceof AST\Functions\FunctionNode) {
2144 2
            $sql .= $this->walkFunction($likeExpr->stringPattern);
2145 6
        } elseif ($likeExpr->stringPattern instanceof AST\PathExpression) {
2146 2
            $sql .= $this->walkPathExpression($likeExpr->stringPattern);
2147
        } else {
2148 6
            $sql .= $this->walkLiteral($likeExpr->stringPattern);
2149
        }
2150
2151 9
        if ($likeExpr->escapeChar) {
2152 1
            $sql .= ' ESCAPE ' . $this->walkLiteral($likeExpr->escapeChar);
2153
        }
2154
2155 9
        return $sql;
2156
    }
2157
2158
    /**
2159
     * {@inheritdoc}
2160
     */
2161 5
    public function walkStateFieldPathExpression($stateFieldPathExpression)
2162
    {
2163 5
        return $this->walkPathExpression($stateFieldPathExpression);
2164
    }
2165
2166
    /**
2167
     * {@inheritdoc}
2168
     */
2169 263
    public function walkComparisonExpression($compExpr)
2170
    {
2171 263
        $leftExpr  = $compExpr->leftExpression;
2172 263
        $rightExpr = $compExpr->rightExpression;
2173 263
        $sql       = '';
2174
2175 263
        $sql .= ($leftExpr instanceof AST\Node)
2176 263
            ? $leftExpr->dispatch($this)
2177 262
            : (is_numeric($leftExpr) ? $leftExpr : $this->conn->quote($leftExpr));
2178
2179 262
        $sql .= ' ' . $compExpr->operator . ' ';
2180
2181 262
        $sql .= ($rightExpr instanceof AST\Node)
2182 260
            ? $rightExpr->dispatch($this)
2183 262
            : (is_numeric($rightExpr) ? $rightExpr : $this->conn->quote($rightExpr));
2184
2185 262
        return $sql;
2186
    }
2187
2188
    /**
2189
     * {@inheritdoc}
2190
     */
2191 219
    public function walkInputParameter($inputParam)
2192
    {
2193 219
        $this->parserResult->addParameterMapping($inputParam->name, $this->sqlParamIndex++);
2194
2195 219
        $parameter = $this->query->getParameter($inputParam->name);
2196
2197 219
        if ($parameter && Type::hasType($type = $parameter->getType())) {
2198 56
            return Type::getType($type)->convertToDatabaseValueSQL('?', $this->platform);
2199
        }
2200
2201 169
        return '?';
2202
    }
2203
2204
    /**
2205
     * {@inheritdoc}
2206
     */
2207 330
    public function walkArithmeticExpression($arithmeticExpr)
2208
    {
2209 330
        return ($arithmeticExpr->isSimpleArithmeticExpression())
2210 330
            ? $this->walkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression)
0 ignored issues
show
Bug introduced by
It seems like $arithmeticExpr->simpleArithmeticExpression can be null; however, walkSimpleArithmeticExpression() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2211 328
            : '(' . $this->walkSubselect($arithmeticExpr->subselect) . ')';
0 ignored issues
show
Bug introduced by
It seems like $arithmeticExpr->subselect can be null; however, walkSubselect() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2212
    }
2213
2214
    /**
2215
     * {@inheritdoc}
2216
     */
2217 393
    public function walkSimpleArithmeticExpression($simpleArithmeticExpr)
2218
    {
2219 393
        if ( ! ($simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression)) {
2220 343
            return $this->walkArithmeticTerm($simpleArithmeticExpr);
2221
        }
2222
2223 75
        return implode(' ', array_map([$this, 'walkArithmeticTerm'], $simpleArithmeticExpr->arithmeticTerms));
2224
    }
2225
2226
    /**
2227
     * {@inheritdoc}
2228
     */
2229 414
    public function walkArithmeticTerm($term)
2230
    {
2231 414
        if (is_string($term)) {
2232 21
            return (isset($this->queryComponents[$term]))
2233 6
                ? $this->walkResultVariable($this->queryComponents[$term]['token']['value'])
2234 21
                : $term;
2235
        }
2236
2237
        // Phase 2 AST optimization: Skip processing of ArithmeticTerm
2238
        // if only one ArithmeticFactor is defined
2239 413
        if ( ! ($term instanceof AST\ArithmeticTerm)) {
2240 391
            return $this->walkArithmeticFactor($term);
2241
        }
2242
2243 47
        return implode(' ', array_map([$this, 'walkArithmeticFactor'], $term->arithmeticFactors));
2244
    }
2245
2246
    /**
2247
     * {@inheritdoc}
2248
     */
2249 414
    public function walkArithmeticFactor($factor)
2250
    {
2251 414
        if (is_string($factor)) {
2252 47
            return (isset($this->queryComponents[$factor]))
2253 2
                ? $this->walkResultVariable($this->queryComponents[$factor]['token']['value'])
2254 47
                : $factor;
2255
        }
2256
2257
        // Phase 2 AST optimization: Skip processing of ArithmeticFactor
2258
        // if only one ArithmeticPrimary is defined
2259 414
        if ( ! ($factor instanceof AST\ArithmeticFactor)) {
2260 413
            return $this->walkArithmeticPrimary($factor);
2261
        }
2262
2263 3
        $sign = $factor->isNegativeSigned() ? '-' : ($factor->isPositiveSigned() ? '+' : '');
2264
2265 3
        return $sign . $this->walkArithmeticPrimary($factor->arithmeticPrimary);
2266
    }
2267
2268
    /**
2269
     * Walks down an ArithmeticPrimary that represents an AST node, thereby generating the appropriate SQL.
2270
     *
2271
     * @param mixed $primary
2272
     *
2273
     * @return string The SQL.
2274
     */
2275 414
    public function walkArithmeticPrimary($primary)
2276
    {
2277 414
        if ($primary instanceof AST\SimpleArithmeticExpression) {
2278
            return '(' . $this->walkSimpleArithmeticExpression($primary) . ')';
2279
        }
2280
2281 414
        if ($primary instanceof AST\Node) {
2282 414
            return $primary->dispatch($this);
2283
        }
2284
2285
        return $this->walkEntityIdentificationVariable($primary);
2286
    }
2287
2288
    /**
2289
     * {@inheritdoc}
2290
     */
2291 18
    public function walkStringPrimary($stringPrimary)
2292
    {
2293 18
        return (is_string($stringPrimary))
2294
            ? $this->conn->quote($stringPrimary)
2295 18
            : $stringPrimary->dispatch($this);
2296
    }
2297
2298
    /**
2299
     * {@inheritdoc}
2300
     */
2301 30
    public function walkResultVariable($resultVariable)
2302
    {
2303 30
        $resultAlias = $this->scalarResultAliasMap[$resultVariable];
2304
2305 30
        if (is_array($resultAlias)) {
2306 1
            return implode(', ', $resultAlias);
2307
        }
2308
2309 29
        return $resultAlias;
2310
    }
2311
2312
    /**
2313
     * @param ClassMetadataInfo $rootClass
2314
     * @param AST\InstanceOfExpression $instanceOfExpr
2315
     * @return string The list in parentheses of valid child discriminators from the given class
2316
     * @throws QueryException
2317
     */
2318 14
    private function getChildDiscriminatorsFromClassMetadata(ClassMetadataInfo $rootClass, AST\InstanceOfExpression $instanceOfExpr): string
2319
    {
2320 14
        $sqlParameterList = [];
2321 14
        $discriminators = [];
2322 14
        foreach ($instanceOfExpr->value as $parameter) {
2323 14
            if ($parameter instanceof AST\InputParameter) {
2324 4
                $this->rsm->discriminatorParameters[$parameter->name] = $parameter->name;
2325 4
                $sqlParameterList[] = $this->walkInParameter($parameter);
2326 4
                continue;
2327
            }
2328
2329 10
            $metadata = $this->em->getClassMetadata($parameter);
2330
2331 10
            if ($metadata->getName() !== $rootClass->name && ! $metadata->getReflectionClass()->isSubclassOf($rootClass->name)) {
2332 1
                throw QueryException::instanceOfUnrelatedClass($parameter, $rootClass->name);
2333
            }
2334
2335 9
            $discriminators += HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($metadata, $this->em);
2336
        }
2337
2338 13
        foreach (array_keys($discriminators) as $dis) {
2339 9
            $sqlParameterList[] = $this->conn->quote($dis);
2340
        }
2341
2342 13
        return '(' . implode(', ', $sqlParameterList) . ')';
2343
    }
2344
}
2345