Completed
Pull Request — master (#6392)
by Alessandro
13:46
created

getChildDiscriminatorsFromClassMetadata()   D

Complexity

Conditions 9
Paths 11

Size

Total Lines 42
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 42
ccs 23
cts 23
cp 1
rs 4.909
c 0
b 0
f 0
cc 9
eloc 23
nc 11
nop 2
crap 9
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\PersisterHelper;
29
30
/**
31
 * The SqlWalker is a TreeWalker that walks over a DQL AST and constructs
32
 * the corresponding SQL.
33
 *
34
 * @author Guilherme Blanco <[email protected]>
35
 * @author Roman Borschel <[email protected]>
36
 * @author Benjamin Eberlei <[email protected]>
37
 * @author Alexander <[email protected]>
38
 * @author Fabio B. Silva <[email protected]>
39
 * @since  2.0
40
 */
41
class SqlWalker implements TreeWalker
42
{
43
    /**
44
     * @var string
45
     */
46
    const HINT_DISTINCT = 'doctrine.distinct';
47
48
    /**
49
     * @var ResultSetMapping
50
     */
51
    private $rsm;
52
53
    /**
54
     * Counter for generating unique column aliases.
55
     *
56
     * @var integer
57
     */
58
    private $aliasCounter = 0;
59
60
    /**
61
     * Counter for generating unique table aliases.
62
     *
63
     * @var integer
64
     */
65
    private $tableAliasCounter = 0;
66
67
    /**
68
     * Counter for generating unique scalar result.
69
     *
70
     * @var integer
71
     */
72
    private $scalarResultCounter = 1;
73
74
    /**
75
     * Counter for generating unique parameter indexes.
76
     *
77
     * @var integer
78
     */
79
    private $sqlParamIndex = 0;
80
81
    /**
82
     * Counter for generating indexes.
83
     *
84
     * @var integer
85
     */
86
    private $newObjectCounter = 0;
87
88
    /**
89
     * @var ParserResult
90
     */
91
    private $parserResult;
92
93
    /**
94
     * @var \Doctrine\ORM\EntityManager
95
     */
96
    private $em;
97
98
    /**
99
     * @var \Doctrine\DBAL\Connection
100
     */
101
    private $conn;
102
103
    /**
104
     * @var \Doctrine\ORM\AbstractQuery
105
     */
106
    private $query;
107
108
    /**
109
     * @var array
110
     */
111
    private $tableAliasMap = [];
112
113
    /**
114
     * Map from result variable names to their SQL column alias names.
115
     *
116
     * @var array
117
     */
118
    private $scalarResultAliasMap = [];
119
120
    /**
121
     * Map from Table-Alias + Column-Name to OrderBy-Direction.
122
     *
123
     * @var array
124
     */
125
    private $orderedColumnsMap = [];
126
127
    /**
128
     * Map from DQL-Alias + Field-Name to SQL Column Alias.
129
     *
130
     * @var array
131
     */
132
    private $scalarFields = [];
133
134
    /**
135
     * Map of all components/classes that appear in the DQL query.
136
     *
137
     * @var array
138
     */
139
    private $queryComponents;
140
141
    /**
142
     * A list of classes that appear in non-scalar SelectExpressions.
143
     *
144
     * @var array
145
     */
146
    private $selectedClasses = [];
147
148
    /**
149
     * The DQL alias of the root class of the currently traversed query.
150
     *
151
     * @var array
152
     */
153
    private $rootAliases = [];
154
155
    /**
156
     * Flag that indicates whether to generate SQL table aliases in the SQL.
157
     * These should only be generated for SELECT queries, not for UPDATE/DELETE.
158
     *
159
     * @var boolean
160
     */
161
    private $useSqlTableAliases = true;
162
163
    /**
164
     * The database platform abstraction.
165
     *
166
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
167
     */
168
    private $platform;
169
170
    /**
171
     * The quote strategy.
172
     *
173
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
174
     */
175
    private $quoteStrategy;
176
177
    /**
178
     * {@inheritDoc}
179
     */
180 683
    public function __construct($query, $parserResult, array $queryComponents)
181
    {
182 683
        $this->query            = $query;
183 683
        $this->parserResult     = $parserResult;
184 683
        $this->queryComponents  = $queryComponents;
185 683
        $this->rsm              = $parserResult->getResultSetMapping();
186 683
        $this->em               = $query->getEntityManager();
187 683
        $this->conn             = $this->em->getConnection();
188 683
        $this->platform         = $this->conn->getDatabasePlatform();
189 683
        $this->quoteStrategy    = $this->em->getConfiguration()->getQuoteStrategy();
190 683
    }
191
192
    /**
193
     * Gets the Query instance used by the walker.
194
     *
195
     * @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...
196
     */
197
    public function getQuery()
198
    {
199
        return $this->query;
200
    }
201
202
    /**
203
     * Gets the Connection used by the walker.
204
     *
205
     * @return \Doctrine\DBAL\Connection
206
     */
207 35
    public function getConnection()
208
    {
209 35
        return $this->conn;
210
    }
211
212
    /**
213
     * Gets the EntityManager used by the walker.
214
     *
215
     * @return \Doctrine\ORM\EntityManager
216
     */
217 21
    public function getEntityManager()
218
    {
219 21
        return $this->em;
220
    }
221
222
    /**
223
     * Gets the information about a single query component.
224
     *
225
     * @param string $dqlAlias The DQL alias.
226
     *
227
     * @return array
228
     */
229 17
    public function getQueryComponent($dqlAlias)
230
    {
231 17
        return $this->queryComponents[$dqlAlias];
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237
    public function getQueryComponents()
238
    {
239
        return $this->queryComponents;
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245 1
    public function setQueryComponent($dqlAlias, array $queryComponent)
246
    {
247 1
        $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token'];
248
249 1
        if (array_diff($requiredKeys, array_keys($queryComponent))) {
250 1
            throw QueryException::invalidQueryComponent($dqlAlias);
251
        }
252
253
        $this->queryComponents[$dqlAlias] = $queryComponent;
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 677
    public function getExecutor($AST)
260
    {
261
        switch (true) {
262 677
            case ($AST instanceof AST\DeleteStatement):
263 38
                $primaryClass = $this->em->getClassMetadata($AST->deleteClause->abstractSchemaName);
264
265 38
                return ($primaryClass->isInheritanceTypeJoined())
266 2
                    ? new Exec\MultiTableDeleteExecutor($AST, $this)
267 38
                    : new Exec\SingleTableDeleteUpdateExecutor($AST, $this);
268
269 619
            case ($AST instanceof AST\UpdateStatement):
270 27
                $primaryClass = $this->em->getClassMetadata($AST->updateClause->abstractSchemaName);
271
272 27
                return ($primaryClass->isInheritanceTypeJoined())
273 3
                    ? new Exec\MultiTableUpdateExecutor($AST, $this)
274 27
                    : new Exec\SingleTableDeleteUpdateExecutor($AST, $this);
275
276
            default:
277 619
                return new Exec\SingleSelectExecutor($AST, $this);
278
        }
279
    }
280
281
    /**
282
     * Generates a unique, short SQL table alias.
283
     *
284
     * @param string $tableName Table name
285
     * @param string $dqlAlias  The DQL alias.
286
     *
287
     * @return string Generated table alias.
288
     */
289 629
    public function getSQLTableAlias($tableName, $dqlAlias = '')
290
    {
291 629
        $tableName .= ($dqlAlias) ? '@[' . $dqlAlias . ']' : '';
292
293 629
        if ( ! isset($this->tableAliasMap[$tableName])) {
294 629
            $this->tableAliasMap[$tableName] = (preg_match('/[a-z]/i', $tableName[0]) ? strtolower($tableName[0]) : 't')
295 629
                . $this->tableAliasCounter++ . '_';
296
        }
297
298 629
        return $this->tableAliasMap[$tableName];
299
    }
300
301
    /**
302
     * Forces the SqlWalker to use a specific alias for a table name, rather than
303
     * generating an alias on its own.
304
     *
305
     * @param string $tableName
306
     * @param string $alias
307
     * @param string $dqlAlias
308
     *
309
     * @return string
310
     */
311 63
    public function setSQLTableAlias($tableName, $alias, $dqlAlias = '')
312
    {
313 63
        $tableName .= ($dqlAlias) ? '@[' . $dqlAlias . ']' : '';
314
315 63
        $this->tableAliasMap[$tableName] = $alias;
316
317 63
        return $alias;
318
    }
319
320
    /**
321
     * Gets an SQL column alias for a column name.
322
     *
323
     * @param string $columnName
324
     *
325
     * @return string
326
     */
327 619
    public function getSQLColumnAlias($columnName)
328
    {
329 619
        return $this->quoteStrategy->getColumnAlias($columnName, $this->aliasCounter++, $this->platform);
330
    }
331
332
    /**
333
     * Generates the SQL JOINs that are necessary for Class Table Inheritance
334
     * for the given class.
335
     *
336
     * @param ClassMetadata $class    The class for which to generate the joins.
337
     * @param string        $dqlAlias The DQL alias of the class.
338
     *
339
     * @return string The SQL.
340
     */
341 93
    private function _generateClassTableInheritanceJoins($class, $dqlAlias)
342
    {
343 93
        $sql = '';
344
345 93
        $baseTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
346
347
        // INNER JOIN parent class tables
348 93
        foreach ($class->parentClasses as $parentClassName) {
349 64
            $parentClass = $this->em->getClassMetadata($parentClassName);
350 64
            $tableAlias  = $this->getSQLTableAlias($parentClass->getTableName(), $dqlAlias);
351
352
            // If this is a joined association we must use left joins to preserve the correct result.
353 64
            $sql .= isset($this->queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER ';
354 64
            $sql .= 'JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON ';
355
356 64
            $sqlParts = [];
357
358 64
            foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) {
359 64
                $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName;
360
            }
361
362
            // Add filters on the root class
363 64
            if ($filterSql = $this->generateFilterConditionSQL($parentClass, $tableAlias)) {
364 1
                $sqlParts[] = $filterSql;
365
            }
366
367 64
            $sql .= implode(' AND ', $sqlParts);
368
        }
369
370
        // Ignore subclassing inclusion if partial objects is disallowed
371 93
        if ($this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
372 21
            return $sql;
373
        }
374
375
        // LEFT JOIN child class tables
376 72
        foreach ($class->subClasses as $subClassName) {
377 34
            $subClass   = $this->em->getClassMetadata($subClassName);
378 34
            $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
379
380 34
            $sql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON ';
381
382 34
            $sqlParts = [];
383
384 34
            foreach ($this->quoteStrategy->getIdentifierColumnNames($subClass, $this->platform) as $columnName) {
385 34
                $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName;
386
            }
387
388 34
            $sql .= implode(' AND ', $sqlParts);
389
        }
390
391 72
        return $sql;
392
    }
393
394
    /**
395
     * @return string
396
     */
397 612
    private function _generateOrderedCollectionOrderByItems()
398
    {
399 612
        $orderedColumns = [];
400
401 612
        foreach ($this->selectedClasses as $selectedClass) {
402 476
            $dqlAlias  = $selectedClass['dqlAlias'];
403 476
            $qComp     = $this->queryComponents[$dqlAlias];
404
405 476
            if ( ! isset($qComp['relation']['orderBy'])) {
406 476
                continue;
407
            }
408
409 6
            $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name);
410
411 6
            foreach ($qComp['relation']['orderBy'] as $fieldName => $orientation) {
412 6
                $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform);
413 6
                $tableName  = ($qComp['metadata']->isInheritanceTypeJoined())
414 1
                    ? $persister->getOwningTable($fieldName)
415 6
                    : $qComp['metadata']->getTableName();
416
417 6
                $orderedColumn = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName;
418
419
                // OrderByClause should replace an ordered relation. see - DDC-2475
420 6
                if (isset($this->orderedColumnsMap[$orderedColumn])) {
421 1
                    continue;
422
                }
423
424 6
                $this->orderedColumnsMap[$orderedColumn] = $orientation;
425 6
                $orderedColumns[] = $orderedColumn . ' ' . $orientation;
426
            }
427
        }
428
429 612
        return implode(', ', $orderedColumns);
430
    }
431
432
    /**
433
     * Generates a discriminator column SQL condition for the class with the given DQL alias.
434
     *
435
     * @param array $dqlAliases List of root DQL aliases to inspect for discriminator restrictions.
436
     *
437
     * @return string
438
     */
439 671
    private function _generateDiscriminatorColumnConditionSQL(array $dqlAliases)
440
    {
441 671
        $sqlParts = [];
442
443 671
        foreach ($dqlAliases as $dqlAlias) {
444 671
            $class = $this->queryComponents[$dqlAlias]['metadata'];
445
446 671
            if ( ! $class->isInheritanceTypeSingleTable()) continue;
447
448 39
            $conn   = $this->em->getConnection();
449 39
            $values = [];
450
451 39
            if ($class->discriminatorValue !== null) { // discriminators can be 0
452 20
                $values[] = $conn->quote($class->discriminatorValue);
453
            }
454
455 39
            foreach ($class->subClasses as $subclassName) {
456 29
                $values[] = $conn->quote($this->em->getClassMetadata($subclassName)->discriminatorValue);
457
            }
458
459 39
            $sqlTableAlias = ($this->useSqlTableAliases)
460 34
                ? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.'
461 39
                : '';
462
463 39
            $sqlParts[] = $sqlTableAlias . $class->discriminatorColumn['name'] . ' IN (' . implode(', ', $values) . ')';
464
        }
465
466 671
        $sql = implode(' AND ', $sqlParts);
467
468 671
        return (count($sqlParts) > 1) ? '(' . $sql . ')' : $sql;
469
    }
470
471
    /**
472
     * Generates the filter SQL for a given entity and table alias.
473
     *
474
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
475
     * @param string        $targetTableAlias The table alias of the joined/selected table.
476
     *
477
     * @return string The SQL query part to add to a query.
478
     */
479 315
    private function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
480
    {
481 315
        if (!$this->em->hasFilters()) {
482 277
            return '';
483
        }
484
485 43
        switch($targetEntity->inheritanceType) {
486 43
            case ClassMetadata::INHERITANCE_TYPE_NONE:
487 33
                break;
488 10
            case ClassMetadata::INHERITANCE_TYPE_JOINED:
489
                // The classes in the inheritance will be added to the query one by one,
490
                // but only the root node is getting filtered
491 6
                if ($targetEntity->name !== $targetEntity->rootEntityName) {
492 4
                    return '';
493
                }
494 6
                break;
495 4
            case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE:
496
                // With STI the table will only be queried once, make sure that the filters
497
                // are added to the root entity
498 4
                $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName);
499 4
                break;
500
            default:
501
                //@todo: throw exception?
502
                return '';
503
        }
504
505 43
        $filterClauses = [];
506 43
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
507 10
            if ('' !== $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
508 10
                $filterClauses[] = '(' . $filterExpr . ')';
509
            }
510
        }
511
512 43
        return implode(' AND ', $filterClauses);
513
    }
514
515
    /**
516
     * {@inheritdoc}
517
     */
518 619
    public function walkSelectStatement(AST\SelectStatement $AST)
519
    {
520 619
        $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...
521 619
        $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...
522 619
        $lockMode = $this->query->getHint(Query::HINT_LOCK_MODE);
523 619
        $sql      = $this->walkSelectClause($AST->selectClause)
524 619
            . $this->walkFromClause($AST->fromClause)
525 616
            . $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...
526
527 613
        if ($AST->groupByClause) {
528 23
            $sql .= $this->walkGroupByClause($AST->groupByClause);
529
        }
530
531 613
        if ($AST->havingClause) {
532 14
            $sql .= $this->walkHavingClause($AST->havingClause);
533
        }
534
535 613
        if ($AST->orderByClause) {
536 142
            $sql .= $this->walkOrderByClause($AST->orderByClause);
537
        }
538
539 612
        if ( ! $AST->orderByClause && ($orderBySql = $this->_generateOrderedCollectionOrderByItems())) {
540 6
            $sql .= ' ORDER BY ' . $orderBySql;
541
        }
542
543 612
        if ($limit !== null || $offset !== null) {
544 39
            $sql = $this->platform->modifyLimitQuery($sql, $limit, $offset);
545
        }
546
547 612
        if ($lockMode === null || $lockMode === false || $lockMode === LockMode::NONE) {
548 607
            return $sql;
549
        }
550
551 5
        if ($lockMode === LockMode::PESSIMISTIC_READ) {
552 3
            return $sql . ' ' . $this->platform->getReadLockSQL();
553
        }
554
555 2
        if ($lockMode === LockMode::PESSIMISTIC_WRITE) {
556 1
            return $sql . ' ' . $this->platform->getWriteLockSQL();
557
        }
558
559 1
        if ($lockMode !== LockMode::OPTIMISTIC) {
560
            throw QueryException::invalidLockMode();
561
        }
562
563 1
        foreach ($this->selectedClasses as $selectedClass) {
564 1
            if ( ! $selectedClass['class']->isVersioned) {
565 1
                throw OptimisticLockException::lockFailed($selectedClass['class']->name);
566
            }
567
        }
568
569
        return $sql;
570
    }
571
572
    /**
573
     * {@inheritdoc}
574
     */
575 24
    public function walkUpdateStatement(AST\UpdateStatement $AST)
576
    {
577 24
        $this->useSqlTableAliases = false;
578 24
        $this->rsm->isSelect      = false;
579
580 24
        return $this->walkUpdateClause($AST->updateClause)
581 24
            . $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...
582
    }
583
584
    /**
585
     * {@inheritdoc}
586
     */
587 36
    public function walkDeleteStatement(AST\DeleteStatement $AST)
588
    {
589 36
        $this->useSqlTableAliases = false;
590 36
        $this->rsm->isSelect      = false;
591
592 36
        return $this->walkDeleteClause($AST->deleteClause)
593 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...
594
    }
595
596
    /**
597
     * Walks down an IdentificationVariable AST node, thereby generating the appropriate SQL.
598
     * This one differs of ->walkIdentificationVariable() because it generates the entity identifiers.
599
     *
600
     * @param string $identVariable
601
     *
602
     * @return string
603
     */
604 2
    public function walkEntityIdentificationVariable($identVariable)
605
    {
606 2
        $class      = $this->queryComponents[$identVariable]['metadata'];
607 2
        $tableAlias = $this->getSQLTableAlias($class->getTableName(), $identVariable);
608 2
        $sqlParts   = [];
609
610 2
        foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) {
611 2
            $sqlParts[] = $tableAlias . '.' . $columnName;
612
        }
613
614 2
        return implode(', ', $sqlParts);
615
    }
616
617
    /**
618
     * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL.
619
     *
620
     * @param string $identificationVariable
621
     * @param string $fieldName
622
     *
623
     * @return string The SQL.
624
     */
625 419
    public function walkIdentificationVariable($identificationVariable, $fieldName = null)
626
    {
627 419
        $class = $this->queryComponents[$identificationVariable]['metadata'];
628
629
        if (
630 419
            $fieldName !== null && $class->isInheritanceTypeJoined() &&
631 419
            isset($class->fieldMappings[$fieldName]['inherited'])
632
        ) {
633 37
            $class = $this->em->getClassMetadata($class->fieldMappings[$fieldName]['inherited']);
634
        }
635
636 419
        return $this->getSQLTableAlias($class->getTableName(), $identificationVariable);
637
    }
638
639
    /**
640
     * {@inheritdoc}
641
     */
642 486
    public function walkPathExpression($pathExpr)
643
    {
644 486
        $sql = '';
645
646 486
        switch ($pathExpr->type) {
647 486
            case AST\PathExpression::TYPE_STATE_FIELD:
648 466
                $fieldName = $pathExpr->field;
649 466
                $dqlAlias = $pathExpr->identificationVariable;
650 466
                $class = $this->queryComponents[$dqlAlias]['metadata'];
651
652 466
                if ($this->useSqlTableAliases) {
653 419
                    $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.';
654
                }
655
656 466
                $sql .= $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
657 466
                break;
658
659 61
            case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION:
660
                // 1- the owning side:
661
                //    Just use the foreign key, i.e. u.group_id
662 61
                $fieldName = $pathExpr->field;
663 61
                $dqlAlias = $pathExpr->identificationVariable;
664 61
                $class = $this->queryComponents[$dqlAlias]['metadata'];
665
666 61
                if (isset($class->associationMappings[$fieldName]['inherited'])) {
667 2
                    $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]['inherited']);
668
                }
669
670 61
                $assoc = $class->associationMappings[$fieldName];
671
672 61
                if ( ! $assoc['isOwningSide']) {
673 2
                    throw QueryException::associationPathInverseSideNotSupported();
674
                }
675
676
                // COMPOSITE KEYS NOT (YET?) SUPPORTED
677 59
                if (count($assoc['sourceToTargetKeyColumns']) > 1) {
678 1
                    throw QueryException::associationPathCompositeKeyNotSupported();
679
                }
680
681 58
                if ($this->useSqlTableAliases) {
682 55
                    $sql .= $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.';
683
                }
684
685 58
                $sql .= reset($assoc['targetToSourceKeyColumns']);
686 58
                break;
687
688
            default:
689
                throw QueryException::invalidPathExpression($pathExpr);
690
        }
691
692 483
        return $sql;
693
    }
694
695
    /**
696
     * {@inheritdoc}
697
     */
698 619
    public function walkSelectClause($selectClause)
699
    {
700 619
        $sql = 'SELECT ' . (($selectClause->isDistinct) ? 'DISTINCT ' : '');
701 619
        $sqlSelectExpressions = array_filter(array_map([$this, 'walkSelectExpression'], $selectClause->selectExpressions));
702
703 619
        if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) == true && $selectClause->isDistinct) {
704
            $this->query->setHint(self::HINT_DISTINCT, true);
705
        }
706
707 619
        $addMetaColumns = ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD) &&
708 456
            $this->query->getHydrationMode() == Query::HYDRATE_OBJECT
709
            ||
710 288
            $this->query->getHydrationMode() != Query::HYDRATE_OBJECT &&
711 619
            $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS);
712
713 619
        foreach ($this->selectedClasses as $selectedClass) {
714 483
            $class       = $selectedClass['class'];
715 483
            $dqlAlias    = $selectedClass['dqlAlias'];
716 483
            $resultAlias = $selectedClass['resultAlias'];
717
718
            // Register as entity or joined entity result
719 483
            if ($this->queryComponents[$dqlAlias]['relation'] === null) {
720 483
                $this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias);
721
            } else {
722 157
                $this->rsm->addJoinedEntityResult(
723 157
                    $class->name,
724
                    $dqlAlias,
725 157
                    $this->queryComponents[$dqlAlias]['parent'],
726 157
                    $this->queryComponents[$dqlAlias]['relation']['fieldName']
727
                );
728
            }
729
730 483
            if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) {
731
                // Add discriminator columns to SQL
732 92
                $rootClass   = $this->em->getClassMetadata($class->rootEntityName);
733 92
                $tblAlias    = $this->getSQLTableAlias($rootClass->getTableName(), $dqlAlias);
734 92
                $discrColumn = $rootClass->discriminatorColumn;
735 92
                $columnAlias = $this->getSQLColumnAlias($discrColumn['name']);
736
737 92
                $sqlSelectExpressions[] = $tblAlias . '.' . $discrColumn['name'] . ' AS ' . $columnAlias;
738
739 92
                $this->rsm->setDiscriminatorColumn($dqlAlias, $columnAlias);
740 92
                $this->rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn['fieldName'], false, $discrColumn['type']);
741
            }
742
743
            // Add foreign key columns to SQL, if necessary
744 483
            if ( ! $addMetaColumns && ! $class->containsForeignIdentifier) {
745 180
                continue;
746
            }
747
748
            // Add foreign key columns of class and also parent classes
749 353
            foreach ($class->associationMappings as $assoc) {
750 314
                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
751 263
                    continue;
752 281
                } else if ( !$addMetaColumns && !isset($assoc['id'])) {
753
                    continue;
754
                }
755
756 281
                $targetClass   = $this->em->getClassMetadata($assoc['targetEntity']);
757 281
                $isIdentifier  = (isset($assoc['id']) && $assoc['id'] === true);
758 281
                $owningClass   = (isset($assoc['inherited'])) ? $this->em->getClassMetadata($assoc['inherited']) : $class;
759 281
                $sqlTableAlias = $this->getSQLTableAlias($owningClass->getTableName(), $dqlAlias);
760
761 281
                foreach ($assoc['joinColumns'] as $joinColumn) {
762 281
                    $columnName  = $joinColumn['name'];
763 281
                    $columnAlias = $this->getSQLColumnAlias($columnName);
764 281
                    $columnType  = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
765
766 281
                    $quotedColumnName       = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
767 281
                    $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias;
768
769 281
                    $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $isIdentifier, $columnType);
770
                }
771
            }
772
773
            // Add foreign key columns to SQL, if necessary
774 353
            if ( ! $addMetaColumns) {
775 8
                continue;
776
            }
777
778
            // Add foreign key columns of subclasses
779 348
            foreach ($class->subClasses as $subClassName) {
780 33
                $subClass      = $this->em->getClassMetadata($subClassName);
781 33
                $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
782
783 33
                foreach ($subClass->associationMappings as $assoc) {
784
                    // Skip if association is inherited
785 27
                    if (isset($assoc['inherited'])) continue;
786
787 16
                    if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
788 14
                        $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
789
790 14
                        foreach ($assoc['joinColumns'] as $joinColumn) {
791 14
                            $columnName  = $joinColumn['name'];
792 14
                            $columnAlias = $this->getSQLColumnAlias($columnName);
793 14
                            $columnType  = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
794
795 14
                            $quotedColumnName       = $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform);
796 14
                            $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias;
797
798 348
                            $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $subClass->isIdentifier($columnName), $columnType);
799
                        }
800
                    }
801
                }
802
            }
803
        }
804
805 619
        $sql .= implode(', ', $sqlSelectExpressions);
806
807 619
        return $sql;
808
    }
809
810
    /**
811
     * {@inheritdoc}
812
     */
813 620
    public function walkFromClause($fromClause)
814
    {
815 620
        $identificationVarDecls = $fromClause->identificationVariableDeclarations;
816 620
        $sqlParts = [];
817
818 620
        foreach ($identificationVarDecls as $identificationVariableDecl) {
819 620
            $sqlParts[] = $this->walkIdentificationVariableDeclaration($identificationVariableDecl);
820
        }
821
822 617
        return ' FROM ' . implode(', ', $sqlParts);
823
    }
824
825
    /**
826
     * Walks down a IdentificationVariableDeclaration AST node, thereby generating the appropriate SQL.
827
     *
828
     * @param AST\IdentificationVariableDeclaration $identificationVariableDecl
829
     *
830
     * @return string
831
     */
832 621
    public function walkIdentificationVariableDeclaration($identificationVariableDecl)
833
    {
834 621
        $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...
835
836 621
        if ($identificationVariableDecl->indexBy) {
837 5
            $this->walkIndexBy($identificationVariableDecl->indexBy);
838
        }
839
840 621
        foreach ($identificationVariableDecl->joins as $join) {
841 243
            $sql .= $this->walkJoin($join);
842
        }
843
844 618
        return $sql;
845
    }
846
847
    /**
848
     * Walks down a IndexBy AST node.
849
     *
850
     * @param AST\IndexBy $indexBy
851
     *
852
     * @return void
853
     */
854 8
    public function walkIndexBy($indexBy)
855
    {
856 8
        $pathExpression = $indexBy->simpleStateFieldPathExpression;
857 8
        $alias          = $pathExpression->identificationVariable;
858 8
        $field          = $pathExpression->field;
859
860 8
        if (isset($this->scalarFields[$alias][$field])) {
861
            $this->rsm->addIndexByScalar($this->scalarFields[$alias][$field]);
862
863
            return;
864
        }
865
866 8
        $this->rsm->addIndexBy($alias, $field);
867 8
    }
868
869
    /**
870
     * Walks down a RangeVariableDeclaration AST node, thereby generating the appropriate SQL.
871
     *
872
     * @param AST\RangeVariableDeclaration $rangeVariableDeclaration
873
     *
874
     * @return string
875
     */
876 621
    public function walkRangeVariableDeclaration($rangeVariableDeclaration)
877
    {
878 621
        $class    = $this->em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName);
879 621
        $dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable;
880
881 621
        if ($rangeVariableDeclaration->isRoot) {
882 621
            $this->rootAliases[] = $dqlAlias;
883
        }
884
885 621
        $sql = $this->platform->appendLockHint(
886 621
            $this->quoteStrategy->getTableName($class, $this->platform) . ' ' .
887 621
            $this->getSQLTableAlias($class->getTableName(), $dqlAlias),
888 621
            $this->query->getHint(Query::HINT_LOCK_MODE)
889
        );
890
891 621
        if ($class->isInheritanceTypeJoined()) {
892 89
            $sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias);
893
        }
894
895 621
        return $sql;
896
    }
897
898
    /**
899
     * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL.
900
     *
901
     * @param AST\JoinAssociationDeclaration $joinAssociationDeclaration
902
     * @param int                            $joinType
903
     * @param AST\ConditionalExpression      $condExpr
904
     *
905
     * @return string
906
     *
907
     * @throws QueryException
908
     */
909 226
    public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joinType = AST\Join::JOIN_TYPE_INNER, $condExpr = null)
910
    {
911 226
        $sql = '';
912
913 226
        $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression;
914 226
        $joinedDqlAlias            = $joinAssociationDeclaration->aliasIdentificationVariable;
915 226
        $indexBy                   = $joinAssociationDeclaration->indexBy;
916
917 226
        $relation        = $this->queryComponents[$joinedDqlAlias]['relation'];
918 226
        $targetClass     = $this->em->getClassMetadata($relation['targetEntity']);
919 226
        $sourceClass     = $this->em->getClassMetadata($relation['sourceEntity']);
920 226
        $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
921
922 226
        $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias);
923 226
        $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable);
924
925
        // Ensure we got the owning side, since it has all mapping info
926 226
        $assoc = ( ! $relation['isOwningSide']) ? $targetClass->associationMappings[$relation['mappedBy']] : $relation;
927
928 226
        if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) == true && (!$this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) {
929 4
            if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) {
930 3
                throw QueryException::iterateWithFetchJoinNotAllowed($assoc);
931
            }
932
        }
933
934 223
        $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...
935
936
        // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot
937
        // be the owning side and previously we ensured that $assoc is always the owning side of the associations.
938
        // The owning side is necessary at this point because only it contains the JoinColumn information.
939
        switch (true) {
940 223
            case ($assoc['type'] & ClassMetadata::TO_ONE):
941 176
                $conditions = [];
942
943 176
                foreach ($assoc['joinColumns'] as $joinColumn) {
944 176
                    $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
945 176
                    $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
946
947 176
                    if ($relation['isOwningSide']) {
948 103
                        $conditions[] = $sourceTableAlias . '.' . $quotedSourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn;
949
950 103
                        continue;
951
                    }
952
953 106
                    $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $quotedSourceColumn;
954
                }
955
956
                // Apply remaining inheritance restrictions
957 176
                $discrSql = $this->_generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]);
958
959 176
                if ($discrSql) {
960 3
                    $conditions[] = $discrSql;
961
                }
962
963
                // Apply the filters
964 176
                $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias);
965
966 176
                if ($filterExpr) {
967 1
                    $conditions[] = $filterExpr;
968
                }
969
970
                $targetTableJoin = [
971 176
                    'table' => $targetTableName . ' ' . $targetTableAlias,
972 176
                    'condition' => implode(' AND ', $conditions),
973
                ];
974 176
                break;
975
976 57
            case ($assoc['type'] == ClassMetadata::MANY_TO_MANY):
977
                // Join relation table
978 57
                $joinTable      = $assoc['joinTable'];
979 57
                $joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias);
980 57
                $joinTableName  = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->platform);
981
982 57
                $conditions      = [];
983 57
                $relationColumns = ($relation['isOwningSide'])
984 48
                    ? $assoc['joinTable']['joinColumns']
985 57
                    : $assoc['joinTable']['inverseJoinColumns'];
986
987 57
                foreach ($relationColumns as $joinColumn) {
988 57
                    $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
989 57
                    $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
990
991 57
                    $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn;
992
                }
993
994 57
                $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions);
995
996
                // Join target table
997 57
                $sql .= ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) ? ' LEFT JOIN ' : ' INNER JOIN ';
998
999 57
                $conditions      = [];
1000 57
                $relationColumns = ($relation['isOwningSide'])
1001 48
                    ? $assoc['joinTable']['inverseJoinColumns']
1002 57
                    : $assoc['joinTable']['joinColumns'];
1003
1004 57
                foreach ($relationColumns as $joinColumn) {
1005 57
                    $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
1006 57
                    $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
1007
1008 57
                    $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn;
1009
                }
1010
1011
                // Apply remaining inheritance restrictions
1012 57
                $discrSql = $this->_generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]);
1013
1014 57
                if ($discrSql) {
1015 1
                    $conditions[] = $discrSql;
1016
                }
1017
1018
                // Apply the filters
1019 57
                $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias);
1020
1021 57
                if ($filterExpr) {
1022 1
                    $conditions[] = $filterExpr;
1023
                }
1024
1025
                $targetTableJoin = [
1026 57
                    'table' => $targetTableName . ' ' . $targetTableAlias,
1027 57
                    'condition' => implode(' AND ', $conditions),
1028
                ];
1029 57
                break;
1030
1031
            default:
1032
                throw new \BadMethodCallException('Type of association must be one of *_TO_ONE or MANY_TO_MANY');
1033
        }
1034
1035
        // Handle WITH clause
1036 223
        $withCondition = (null === $condExpr) ? '' : ('(' . $this->walkConditionalExpression($condExpr) . ')');
1037
1038 223
        if ($targetClass->isInheritanceTypeJoined()) {
1039 9
            $ctiJoins = $this->_generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias);
1040
            // If we have WITH condition, we need to build nested joins for target class table and cti joins
1041 9
            if ($withCondition) {
1042 1
                $sql .= '(' . $targetTableJoin['table'] . $ctiJoins . ') ON ' . $targetTableJoin['condition'];
1043
            } else {
1044 9
                $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'] . $ctiJoins;
1045
            }
1046
        } else {
1047 214
            $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'];
1048
        }
1049
1050 223
        if ($withCondition) {
1051 4
            $sql .= ' AND ' . $withCondition;
1052
        }
1053
1054
        // Apply the indexes
1055 223
        if ($indexBy) {
1056
            // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently.
1057 5
            $this->walkIndexBy($indexBy);
1058 218
        } else if (isset($relation['indexBy'])) {
1059 2
            $this->rsm->addIndexBy($joinedDqlAlias, $relation['indexBy']);
1060
        }
1061
1062 223
        return $sql;
1063
    }
1064
1065
    /**
1066
     * {@inheritdoc}
1067
     */
1068 63
    public function walkFunction($function)
1069
    {
1070 63
        return $function->getSql($this);
1071
    }
1072
1073
    /**
1074
     * {@inheritdoc}
1075
     */
1076 153
    public function walkOrderByClause($orderByClause)
1077
    {
1078 153
        $orderByItems = array_map([$this, 'walkOrderByItem'], $orderByClause->orderByItems);
1079
1080 152
        if (($collectionOrderByItems = $this->_generateOrderedCollectionOrderByItems()) !== '') {
1081
            $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems);
1082
        }
1083
1084 152
        return ' ORDER BY ' . implode(', ', $orderByItems);
1085
    }
1086
1087
    /**
1088
     * {@inheritdoc}
1089
     */
1090 171
    public function walkOrderByItem($orderByItem)
1091
    {
1092 171
        $type = strtoupper($orderByItem->type);
1093 171
        $expr = $orderByItem->expression;
1094 171
        $sql  = ($expr instanceof AST\Node)
1095 164
            ? $expr->dispatch($this)
1096 170
            : $this->walkResultVariable($this->queryComponents[$expr]['token']['value']);
1097
1098 170
        $this->orderedColumnsMap[$sql] = $type;
1099
1100 170
        if ($expr instanceof AST\Subselect) {
1101 2
            return '(' . $sql . ') ' . $type;
1102
        }
1103
1104 168
        return $sql . ' ' . $type;
1105
    }
1106
1107
    /**
1108
     * {@inheritdoc}
1109
     */
1110 14
    public function walkHavingClause($havingClause)
1111
    {
1112 14
        return ' HAVING ' . $this->walkConditionalExpression($havingClause->conditionalExpression);
1113
    }
1114
1115
    /**
1116
     * {@inheritdoc}
1117
     */
1118 243
    public function walkJoin($join)
1119
    {
1120 243
        $joinType        = $join->joinType;
1121 243
        $joinDeclaration = $join->joinAssociationDeclaration;
1122
1123 243
        $sql = ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER)
1124 55
            ? ' LEFT JOIN '
1125 243
            : ' INNER JOIN ';
1126
1127
        switch (true) {
1128 243
            case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\RangeVariableDeclaration):
1129 17
                $class      = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName);
1130 17
                $dqlAlias   = $joinDeclaration->aliasIdentificationVariable;
1131 17
                $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias);
1132 17
                $conditions = [];
1133
1134 17
                if ($join->conditionalExpression) {
1135 15
                    $conditions[] = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')';
1136
                }
1137
1138 17
                $condExprConjunction = ($class->isInheritanceTypeJoined() && $joinType != AST\Join::JOIN_TYPE_LEFT && $joinType != AST\Join::JOIN_TYPE_LEFTOUTER)
1139 3
                    ? ' AND '
1140 17
                    : ' ON ';
1141
1142 17
                $sql .= $this->walkRangeVariableDeclaration($joinDeclaration);
1143
1144
                // Apply remaining inheritance restrictions
1145 17
                $discrSql = $this->_generateDiscriminatorColumnConditionSQL([$dqlAlias]);
1146
1147 17
                if ($discrSql) {
1148 3
                    $conditions[] = $discrSql;
1149
                }
1150
1151
                // Apply the filters
1152 17
                $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias);
1153
1154 17
                if ($filterExpr) {
1155
                    $conditions[] = $filterExpr;
1156
                }
1157
1158 17
                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...
1159 15
                    $sql .= $condExprConjunction . implode(' AND ', $conditions);
1160
                }
1161
1162 17
                break;
1163
1164
            case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\JoinAssociationDeclaration):
1165 226
                $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType, $join->conditionalExpression);
1166 223
                break;
1167
        }
1168
1169 240
        return $sql;
1170
    }
1171
1172
    /**
1173
     * Walks down a CoalesceExpression AST node and generates the corresponding SQL.
1174
     *
1175
     * @param AST\CoalesceExpression $coalesceExpression
1176
     *
1177
     * @return string The SQL.
1178
     */
1179 2
    public function walkCoalesceExpression($coalesceExpression)
1180
    {
1181 2
        $sql = 'COALESCE(';
1182
1183 2
        $scalarExpressions = [];
1184
1185 2
        foreach ($coalesceExpression->scalarExpressions as $scalarExpression) {
1186 2
            $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression);
1187
        }
1188
1189 2
        $sql .= implode(', ', $scalarExpressions) . ')';
1190
1191 2
        return $sql;
1192
    }
1193
1194
    /**
1195
     * Walks down a NullIfExpression AST node and generates the corresponding SQL.
1196
     *
1197
     * @param AST\NullIfExpression $nullIfExpression
1198
     *
1199
     * @return string The SQL.
1200
     */
1201 3
    public function walkNullIfExpression($nullIfExpression)
1202
    {
1203 3
        $firstExpression = is_string($nullIfExpression->firstExpression)
1204
            ? $this->conn->quote($nullIfExpression->firstExpression)
1205 3
            : $this->walkSimpleArithmeticExpression($nullIfExpression->firstExpression);
1206
1207 3
        $secondExpression = is_string($nullIfExpression->secondExpression)
1208
            ? $this->conn->quote($nullIfExpression->secondExpression)
1209 3
            : $this->walkSimpleArithmeticExpression($nullIfExpression->secondExpression);
1210
1211 3
        return 'NULLIF(' . $firstExpression . ', ' . $secondExpression . ')';
1212
    }
1213
1214
    /**
1215
     * Walks down a GeneralCaseExpression AST node and generates the corresponding SQL.
1216
     *
1217
     * @param AST\GeneralCaseExpression $generalCaseExpression
1218
     *
1219
     * @return string The SQL.
1220
     */
1221 9
    public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression)
1222
    {
1223 9
        $sql = 'CASE';
1224
1225 9
        foreach ($generalCaseExpression->whenClauses as $whenClause) {
1226 9
            $sql .= ' WHEN ' . $this->walkConditionalExpression($whenClause->caseConditionExpression);
1227 9
            $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($whenClause->thenScalarExpression);
1228
        }
1229
1230 9
        $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression) . ' END';
1231
1232 9
        return $sql;
1233
    }
1234
1235
    /**
1236
     * Walks down a SimpleCaseExpression AST node and generates the corresponding SQL.
1237
     *
1238
     * @param AST\SimpleCaseExpression $simpleCaseExpression
1239
     *
1240
     * @return string The SQL.
1241
     */
1242 5
    public function walkSimpleCaseExpression($simpleCaseExpression)
1243
    {
1244 5
        $sql = 'CASE ' . $this->walkStateFieldPathExpression($simpleCaseExpression->caseOperand);
1245
1246 5
        foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) {
1247 5
            $sql .= ' WHEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression);
1248 5
            $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression);
1249
        }
1250
1251 5
        $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression) . ' END';
1252
1253 5
        return $sql;
1254
    }
1255
1256
    /**
1257
     * {@inheritdoc}
1258
     */
1259 619
    public function walkSelectExpression($selectExpression)
1260
    {
1261 619
        $sql    = '';
1262 619
        $expr   = $selectExpression->expression;
1263 619
        $hidden = $selectExpression->hiddenAliasResultVariable;
1264
1265
        switch (true) {
1266 619
            case ($expr instanceof AST\PathExpression):
1267 101
                if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) {
1268
                    throw QueryException::invalidPathExpression($expr);
1269
                }
1270
1271 101
                $fieldName = $expr->field;
1272 101
                $dqlAlias  = $expr->identificationVariable;
1273 101
                $qComp     = $this->queryComponents[$dqlAlias];
1274 101
                $class     = $qComp['metadata'];
1275
1276 101
                $resultAlias = $selectExpression->fieldIdentificationVariable ?: $fieldName;
1277 101
                $tableName   = ($class->isInheritanceTypeJoined())
1278 11
                    ? $this->em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName)
1279 101
                    : $class->getTableName();
1280
1281 101
                $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
1282 101
                $fieldMapping  = $class->fieldMappings[$fieldName];
1283 101
                $columnName    = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
1284 101
                $columnAlias   = $this->getSQLColumnAlias($fieldMapping['columnName']);
1285 101
                $col           = $sqlTableAlias . '.' . $columnName;
1286
1287 101
                if (isset($fieldMapping['requireSQLConversion'])) {
1288 2
                    $type = Type::getType($fieldMapping['type']);
1289 2
                    $col  = $type->convertToPHPValueSQL($col, $this->conn->getDatabasePlatform());
1290
                }
1291
1292 101
                $sql .= $col . ' AS ' . $columnAlias;
1293
1294 101
                $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1295
1296 101
                if ( ! $hidden) {
1297 101
                    $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldMapping['type']);
1298 101
                    $this->scalarFields[$dqlAlias][$fieldName] = $columnAlias;
1299
                }
1300
1301 101
                break;
1302
1303
            case ($expr instanceof AST\AggregateExpression):
1304
            case ($expr instanceof AST\Functions\FunctionNode):
1305 10
            case ($expr instanceof AST\SimpleArithmeticExpression):
1306 22
            case ($expr instanceof AST\ArithmeticTerm):
1307 33
            case ($expr instanceof AST\ArithmeticFactor):
1308 28
            case ($expr instanceof AST\ParenthesisExpression):
1309
            case ($expr instanceof AST\Literal):
1310 37
            case ($expr instanceof AST\NullIfExpression):
1311 37
            case ($expr instanceof AST\CoalesceExpression):
1312 9
            case ($expr instanceof AST\GeneralCaseExpression):
1313 43
            case ($expr instanceof AST\SimpleCaseExpression):
1314 106
                $columnAlias = $this->getSQLColumnAlias('sclr');
1315 106
                $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1316
1317 106
                $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias;
1318
1319 106
                $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1320
1321 106
                if ( ! $hidden) {
1322
                    // We cannot resolve field type here; assume 'string'.
1323 106
                    $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string');
1324
                }
1325 106
                break;
1326
1327
            case ($expr instanceof AST\Subselect):
1328 15
                $columnAlias = $this->getSQLColumnAlias('sclr');
1329 15
                $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1330
1331 15
                $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias;
1332
1333 15
                $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1334
1335 15
                if ( ! $hidden) {
1336
                    // We cannot resolve field type here; assume 'string'.
1337 13
                    $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string');
1338
                }
1339 15
                break;
1340
1341 483
            case ($expr instanceof AST\NewObjectExpression):
1342 22
                $sql .= $this->walkNewObject($expr,$selectExpression->fieldIdentificationVariable);
1343 22
                break;
1344
1345
            default:
1346
                // IdentificationVariable or PartialObjectExpression
1347 483
                if ($expr instanceof AST\PartialObjectExpression) {
1348 16
                    $dqlAlias = $expr->identificationVariable;
1349 16
                    $partialFieldSet = $expr->partialFieldSet;
1350
                } else {
1351 478
                    $dqlAlias = $expr;
1352 478
                    $partialFieldSet = [];
1353
                }
1354
1355 483
                $queryComp   = $this->queryComponents[$dqlAlias];
1356 483
                $class       = $queryComp['metadata'];
1357 483
                $resultAlias = $selectExpression->fieldIdentificationVariable ?: null;
1358
1359 483
                if ( ! isset($this->selectedClasses[$dqlAlias])) {
1360 483
                    $this->selectedClasses[$dqlAlias] = [
1361 483
                        'class'       => $class,
1362 483
                        'dqlAlias'    => $dqlAlias,
1363 483
                        'resultAlias' => $resultAlias
1364
                    ];
1365
                }
1366
1367 483
                $sqlParts = [];
1368
1369
                // Select all fields from the queried class
1370 483
                foreach ($class->fieldMappings as $fieldName => $mapping) {
1371 482
                    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...
1372 14
                        continue;
1373
                    }
1374
1375 481
                    $tableName = (isset($mapping['inherited']))
1376 51
                        ? $this->em->getClassMetadata($mapping['inherited'])->getTableName()
1377 481
                        : $class->getTableName();
1378
1379 481
                    $sqlTableAlias    = $this->getSQLTableAlias($tableName, $dqlAlias);
1380 481
                    $columnAlias      = $this->getSQLColumnAlias($mapping['columnName']);
1381 481
                    $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
1382
1383 481
                    $col = $sqlTableAlias . '.' . $quotedColumnName;
1384
1385 481
                    if (isset($mapping['requireSQLConversion'])) {
1386 5
                        $type = Type::getType($mapping['type']);
1387 5
                        $col = $type->convertToPHPValueSQL($col, $this->platform);
1388
                    }
1389
1390 481
                    $sqlParts[] = $col . ' AS '. $columnAlias;
1391
1392 481
                    $this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
1393
1394 481
                    $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);
1395
                }
1396
1397
                // Add any additional fields of subclasses (excluding inherited fields)
1398
                // 1) on Single Table Inheritance: always, since its marginal overhead
1399
                // 2) on Class Table Inheritance only if partial objects are disallowed,
1400
                //    since it requires outer joining subtables.
1401 483
                if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
1402 390
                    foreach ($class->subClasses as $subClassName) {
1403 44
                        $subClass      = $this->em->getClassMetadata($subClassName);
1404 44
                        $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
1405
1406 44
                        foreach ($subClass->fieldMappings as $fieldName => $mapping) {
1407 44
                            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...
1408 44
                                continue;
1409
                            }
1410
1411 36
                            $columnAlias      = $this->getSQLColumnAlias($mapping['columnName']);
1412 36
                            $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
1413
1414 36
                            $col = $sqlTableAlias . '.' . $quotedColumnName;
1415
1416 36
                            if (isset($mapping['requireSQLConversion'])) {
1417
                                $type = Type::getType($mapping['type']);
1418
                                $col = $type->convertToPHPValueSQL($col, $this->platform);
1419
                            }
1420
1421 36
                            $sqlParts[] = $col . ' AS ' . $columnAlias;
1422
1423 36
                            $this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
1424
1425 44
                            $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
1426
                        }
1427
                    }
1428
                }
1429
1430 483
                $sql .= implode(', ', $sqlParts);
1431
        }
1432
1433 619
        return $sql;
1434
    }
1435
1436
    /**
1437
     * {@inheritdoc}
1438
     */
1439
    public function walkQuantifiedExpression($qExpr)
1440
    {
1441
        return ' ' . strtoupper($qExpr->type) . '(' . $this->walkSubselect($qExpr->subselect) . ')';
1442
    }
1443
1444
    /**
1445
     * {@inheritdoc}
1446
     */
1447 33
    public function walkSubselect($subselect)
1448
    {
1449 33
        $useAliasesBefore  = $this->useSqlTableAliases;
1450 33
        $rootAliasesBefore = $this->rootAliases;
1451
1452 33
        $this->rootAliases = []; // reset the rootAliases for the subselect
1453 33
        $this->useSqlTableAliases = true;
1454
1455 33
        $sql  = $this->walkSimpleSelectClause($subselect->simpleSelectClause);
1456 33
        $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause);
1457 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...
1458
1459 33
        $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : '';
1460 33
        $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : '';
1461 33
        $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : '';
1462
1463 33
        $this->rootAliases        = $rootAliasesBefore; // put the main aliases back
1464 33
        $this->useSqlTableAliases = $useAliasesBefore;
1465
1466 33
        return $sql;
1467
    }
1468
1469
    /**
1470
     * {@inheritdoc}
1471
     */
1472 33
    public function walkSubselectFromClause($subselectFromClause)
1473
    {
1474 33
        $identificationVarDecls = $subselectFromClause->identificationVariableDeclarations;
1475 33
        $sqlParts               = [];
1476
1477 33
        foreach ($identificationVarDecls as $subselectIdVarDecl) {
1478 33
            $sqlParts[] = $this->walkIdentificationVariableDeclaration($subselectIdVarDecl);
1479
        }
1480
1481 33
        return ' FROM ' . implode(', ', $sqlParts);
1482
    }
1483
1484
    /**
1485
     * {@inheritdoc}
1486
     */
1487 33
    public function walkSimpleSelectClause($simpleSelectClause)
1488
    {
1489 33
        return 'SELECT' . ($simpleSelectClause->isDistinct ? ' DISTINCT' : '')
1490 33
            . $this->walkSimpleSelectExpression($simpleSelectClause->simpleSelectExpression);
1491
    }
1492
1493
    /**
1494
     * @param \Doctrine\ORM\Query\AST\ParenthesisExpression $parenthesisExpression
1495
     *
1496
     * @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...
1497
     */
1498 22
    public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesisExpression)
1499
    {
1500 22
        return sprintf('(%s)', $parenthesisExpression->expression->dispatch($this));
1501
    }
1502
1503
    /**
1504
     * @param AST\NewObjectExpression $newObjectExpression
1505
     *
1506
     * @return string The SQL.
1507
     */
1508 22
    public function walkNewObject($newObjectExpression, $newObjectResultAlias=null)
1509
    {
1510 22
        $sqlSelectExpressions = [];
1511 22
        $objIndex             = $newObjectResultAlias?:$this->newObjectCounter++;
1512
1513 22
        foreach ($newObjectExpression->args as $argIndex => $e) {
1514 22
            $resultAlias = $this->scalarResultCounter++;
1515 22
            $columnAlias = $this->getSQLColumnAlias('sclr');
1516 22
            $fieldType   = 'string';
1517
1518
            switch (true) {
1519 22
                case ($e instanceof AST\NewObjectExpression):
1520
                    $sqlSelectExpressions[] = $e->dispatch($this);
1521
                    break;
1522
1523
                case ($e instanceof AST\Subselect):
1524 1
                    $sqlSelectExpressions[] = '(' . $e->dispatch($this) . ') AS ' . $columnAlias;
1525 1
                    break;
1526
1527
                case ($e instanceof AST\PathExpression):
1528 22
                    $dqlAlias  = $e->identificationVariable;
1529 22
                    $qComp     = $this->queryComponents[$dqlAlias];
1530 22
                    $class     = $qComp['metadata'];
1531 22
                    $fieldType = $class->fieldMappings[$e->field]['type'];
1532
1533 22
                    $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
1534 22
                    break;
1535
1536 5
                case ($e instanceof AST\Literal):
1537 1
                    switch ($e->type) {
1538 1
                        case AST\Literal::BOOLEAN:
1539 1
                            $fieldType = 'boolean';
1540 1
                            break;
1541
1542 1
                        case AST\Literal::NUMERIC:
1543 1
                            $fieldType = is_float($e->value) ? 'float' : 'integer';
1544 1
                            break;
1545
                    }
1546
1547 1
                    $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
1548 1
                    break;
1549
1550
                default:
1551 5
                    $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
1552 5
                    break;
1553
            }
1554
1555 22
            $this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1556 22
            $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType);
1557
1558 22
            $this->rsm->newObjectMappings[$columnAlias] = [
1559 22
                'className' => $newObjectExpression->className,
1560 22
                'objIndex'  => $objIndex,
1561 22
                'argIndex'  => $argIndex
1562
            ];
1563
        }
1564
1565 22
        return implode(', ', $sqlSelectExpressions);
1566
    }
1567
1568
    /**
1569
     * {@inheritdoc}
1570
     */
1571 33
    public function walkSimpleSelectExpression($simpleSelectExpression)
1572
    {
1573 33
        $expr = $simpleSelectExpression->expression;
1574 33
        $sql  = ' ';
1575
1576
        switch (true) {
1577 33
            case ($expr instanceof AST\PathExpression):
1578 9
                $sql .= $this->walkPathExpression($expr);
1579 9
                break;
1580
1581
            case ($expr instanceof AST\AggregateExpression):
1582 14
                $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1583
1584 14
                $sql .= $this->walkAggregateExpression($expr) . ' AS dctrn__' . $alias;
1585 14
                break;
1586
1587
            case ($expr instanceof AST\Subselect):
1588
                $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1589
1590
                $columnAlias = 'sclr' . $this->aliasCounter++;
1591
                $this->scalarResultAliasMap[$alias] = $columnAlias;
1592
1593
                $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias;
1594
                break;
1595
1596
            case ($expr instanceof AST\Functions\FunctionNode):
1597
            case ($expr instanceof AST\SimpleArithmeticExpression):
1598
            case ($expr instanceof AST\ArithmeticTerm):
1599
            case ($expr instanceof AST\ArithmeticFactor):
1600
            case ($expr instanceof AST\Literal):
1601
            case ($expr instanceof AST\NullIfExpression):
1602
            case ($expr instanceof AST\CoalesceExpression):
1603
            case ($expr instanceof AST\GeneralCaseExpression):
1604
            case ($expr instanceof AST\SimpleCaseExpression):
1605 8
                $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++;
1606
1607 8
                $columnAlias = $this->getSQLColumnAlias('sclr');
1608 8
                $this->scalarResultAliasMap[$alias] = $columnAlias;
1609
1610 8
                $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias;
1611 8
                break;
1612
1613 2
            case ($expr instanceof AST\ParenthesisExpression):
1614 1
                $sql .= $this->walkParenthesisExpression($expr);
1615 1
                break;
1616
1617
            default: // IdentificationVariable
1618 2
                $sql .= $this->walkEntityIdentificationVariable($expr);
1619 2
                break;
1620
        }
1621
1622 33
        return $sql;
1623
    }
1624
1625
    /**
1626
     * {@inheritdoc}
1627
     */
1628 75
    public function walkAggregateExpression($aggExpression)
1629
    {
1630 75
        return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '')
1631 75
            . $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...
1632
    }
1633
1634
    /**
1635
     * {@inheritdoc}
1636
     */
1637 23
    public function walkGroupByClause($groupByClause)
1638
    {
1639 23
        $sqlParts = [];
1640
1641 23
        foreach ($groupByClause->groupByItems as $groupByItem) {
1642 23
            $sqlParts[] = $this->walkGroupByItem($groupByItem);
1643
        }
1644
1645 23
        return ' GROUP BY ' . implode(', ', $sqlParts);
1646
    }
1647
1648
    /**
1649
     * {@inheritdoc}
1650
     */
1651 23
    public function walkGroupByItem($groupByItem)
1652
    {
1653
        // StateFieldPathExpression
1654 23
        if ( ! is_string($groupByItem)) {
1655 10
            return $this->walkPathExpression($groupByItem);
1656
        }
1657
1658
        // ResultVariable
1659 13
        if (isset($this->queryComponents[$groupByItem]['resultVariable'])) {
1660 2
            $resultVariable = $this->queryComponents[$groupByItem]['resultVariable'];
1661
1662 2
            if ($resultVariable instanceof AST\PathExpression) {
1663 1
                return $this->walkPathExpression($resultVariable);
1664
            }
1665
1666 1
            if (isset($resultVariable->pathExpression)) {
1667
                return $this->walkPathExpression($resultVariable->pathExpression);
1668
            }
1669
1670 1
            return $this->walkResultVariable($groupByItem);
1671
        }
1672
1673
        // IdentificationVariable
1674 11
        $sqlParts = [];
1675
1676 11
        foreach ($this->queryComponents[$groupByItem]['metadata']->fieldNames as $field) {
1677 11
            $item       = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $field);
1678 11
            $item->type = AST\PathExpression::TYPE_STATE_FIELD;
1679
1680 11
            $sqlParts[] = $this->walkPathExpression($item);
1681
        }
1682
1683 11
        foreach ($this->queryComponents[$groupByItem]['metadata']->associationMappings as $mapping) {
1684 11
            if ($mapping['isOwningSide'] && $mapping['type'] & ClassMetadataInfo::TO_ONE) {
1685 7
                $item       = new AST\PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $groupByItem, $mapping['fieldName']);
1686 7
                $item->type = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION;
1687
1688 11
                $sqlParts[] = $this->walkPathExpression($item);
1689
            }
1690
        }
1691
1692 11
        return implode(', ', $sqlParts);
1693
    }
1694
1695
    /**
1696
     * {@inheritdoc}
1697
     */
1698 36
    public function walkDeleteClause(AST\DeleteClause $deleteClause)
1699
    {
1700 36
        $class     = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1701 36
        $tableName = $class->getTableName();
1702 36
        $sql       = 'DELETE FROM ' . $this->quoteStrategy->getTableName($class, $this->platform);
1703
1704 36
        $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable);
1705 36
        $this->rootAliases[] = $deleteClause->aliasIdentificationVariable;
1706
1707 36
        return $sql;
1708
    }
1709
1710
    /**
1711
     * {@inheritdoc}
1712
     */
1713 24
    public function walkUpdateClause($updateClause)
1714
    {
1715 24
        $class     = $this->em->getClassMetadata($updateClause->abstractSchemaName);
1716 24
        $tableName = $class->getTableName();
1717 24
        $sql       = 'UPDATE ' . $this->quoteStrategy->getTableName($class, $this->platform);
1718
1719 24
        $this->setSQLTableAlias($tableName, $tableName, $updateClause->aliasIdentificationVariable);
1720 24
        $this->rootAliases[] = $updateClause->aliasIdentificationVariable;
1721
1722 24
        $sql .= ' SET ' . implode(', ', array_map([$this, 'walkUpdateItem'], $updateClause->updateItems));
1723
1724 24
        return $sql;
1725
    }
1726
1727
    /**
1728
     * {@inheritdoc}
1729
     */
1730 27
    public function walkUpdateItem($updateItem)
1731
    {
1732 27
        $useTableAliasesBefore = $this->useSqlTableAliases;
1733 27
        $this->useSqlTableAliases = false;
1734
1735 27
        $sql      = $this->walkPathExpression($updateItem->pathExpression) . ' = ';
1736 27
        $newValue = $updateItem->newValue;
1737
1738
        switch (true) {
1739 27
            case ($newValue instanceof AST\Node):
1740 26
                $sql .= $newValue->dispatch($this);
1741 26
                break;
1742
1743 1
            case ($newValue === null):
1744 1
                $sql .= 'NULL';
1745 1
                break;
1746
1747
            default:
1748
                $sql .= $this->conn->quote($newValue);
1749
                break;
1750
        }
1751
1752 27
        $this->useSqlTableAliases = $useTableAliasesBefore;
1753
1754 27
        return $sql;
1755
    }
1756
1757
    /**
1758
     * {@inheritdoc}
1759
     */
1760 674
    public function walkWhereClause($whereClause)
1761
    {
1762 674
        $condSql  = null !== $whereClause ? $this->walkConditionalExpression($whereClause->conditionalExpression) : '';
1763 671
        $discrSql = $this->_generateDiscriminatorColumnConditionSql($this->rootAliases);
1764
1765 671
        if ($this->em->hasFilters()) {
1766 43
            $filterClauses = [];
1767 43
            foreach ($this->rootAliases as $dqlAlias) {
1768 43
                $class = $this->queryComponents[$dqlAlias]['metadata'];
1769 43
                $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias);
1770
1771 43
                if ($filterExpr = $this->generateFilterConditionSQL($class, $tableAlias)) {
1772 43
                    $filterClauses[] = $filterExpr;
1773
                }
1774
            }
1775
1776 43
            if (count($filterClauses)) {
1777 6
                if ($condSql) {
1778 2
                    $condSql = '(' . $condSql . ') AND ';
1779
                }
1780
1781 6
                $condSql .= implode(' AND ', $filterClauses);
1782
            }
1783
        }
1784
1785 671
        if ($condSql) {
1786 331
            return ' WHERE ' . (( ! $discrSql) ? $condSql : '(' . $condSql . ') AND ' . $discrSql);
1787
        }
1788
1789 415
        if ($discrSql) {
1790 22
            return ' WHERE ' . $discrSql;
1791
        }
1792
1793 395
        return '';
1794
    }
1795
1796
    /**
1797
     * {@inheritdoc}
1798
     */
1799 361
    public function walkConditionalExpression($condExpr)
1800
    {
1801
        // Phase 2 AST optimization: Skip processing of ConditionalExpression
1802
        // if only one ConditionalTerm is defined
1803 361
        if ( ! ($condExpr instanceof AST\ConditionalExpression)) {
1804 306
            return $this->walkConditionalTerm($condExpr);
1805
        }
1806
1807 72
        return implode(' OR ', array_map([$this, 'walkConditionalTerm'], $condExpr->conditionalTerms));
1808
    }
1809
1810
    /**
1811
     * {@inheritdoc}
1812
     */
1813 361
    public function walkConditionalTerm($condTerm)
1814
    {
1815
        // Phase 2 AST optimization: Skip processing of ConditionalTerm
1816
        // if only one ConditionalFactor is defined
1817 361
        if ( ! ($condTerm instanceof AST\ConditionalTerm)) {
1818 295
            return $this->walkConditionalFactor($condTerm);
1819
        }
1820
1821 89
        return implode(' AND ', array_map([$this, 'walkConditionalFactor'], $condTerm->conditionalFactors));
1822
    }
1823
1824
    /**
1825
     * {@inheritdoc}
1826
     */
1827 361
    public function walkConditionalFactor($factor)
1828
    {
1829
        // Phase 2 AST optimization: Skip processing of ConditionalFactor
1830
        // if only one ConditionalPrimary is defined
1831 361
        return ( ! ($factor instanceof AST\ConditionalFactor))
1832 358
            ? $this->walkConditionalPrimary($factor)
1833 358
            : ($factor->not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary);
1834
    }
1835
1836
    /**
1837
     * {@inheritdoc}
1838
     */
1839 361
    public function walkConditionalPrimary($primary)
1840
    {
1841 361
        if ($primary->isSimpleConditionalExpression()) {
1842 361
            return $primary->simpleConditionalExpression->dispatch($this);
1843
        }
1844
1845 25
        if ($primary->isConditionalExpression()) {
1846 25
            $condExpr = $primary->conditionalExpression;
1847
1848 25
            return '(' . $this->walkConditionalExpression($condExpr) . ')';
0 ignored issues
show
Bug introduced by
It seems like $condExpr defined by $primary->conditionalExpression on line 1846 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...
1849
        }
1850
    }
1851
1852
    /**
1853
     * {@inheritdoc}
1854
     */
1855 5
    public function walkExistsExpression($existsExpr)
1856
    {
1857 5
        $sql = ($existsExpr->not) ? 'NOT ' : '';
1858
1859 5
        $sql .= 'EXISTS (' . $this->walkSubselect($existsExpr->subselect) . ')';
1860
1861 5
        return $sql;
1862
    }
1863
1864
    /**
1865
     * {@inheritdoc}
1866
     */
1867 6
    public function walkCollectionMemberExpression($collMemberExpr)
1868
    {
1869 6
        $sql = $collMemberExpr->not ? 'NOT ' : '';
1870 6
        $sql .= 'EXISTS (SELECT 1 FROM ';
1871
1872 6
        $entityExpr   = $collMemberExpr->entityExpression;
1873 6
        $collPathExpr = $collMemberExpr->collectionValuedPathExpression;
1874
1875 6
        $fieldName = $collPathExpr->field;
1876 6
        $dqlAlias  = $collPathExpr->identificationVariable;
1877
1878 6
        $class = $this->queryComponents[$dqlAlias]['metadata'];
1879
1880
        switch (true) {
1881
            // InputParameter
1882 6
            case ($entityExpr instanceof AST\InputParameter):
1883 4
                $dqlParamKey = $entityExpr->name;
1884 4
                $entitySql   = '?';
1885 4
                break;
1886
1887
            // SingleValuedAssociationPathExpression | IdentificationVariable
1888
            case ($entityExpr instanceof AST\PathExpression):
1889 2
                $entitySql = $this->walkPathExpression($entityExpr);
1890 2
                break;
1891
1892
            default:
1893
                throw new \BadMethodCallException("Not implemented");
1894
        }
1895
1896 6
        $assoc = $class->associationMappings[$fieldName];
1897
1898 6
        if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) {
1899 1
            $targetClass      = $this->em->getClassMetadata($assoc['targetEntity']);
1900 1
            $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName());
1901 1
            $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
1902
1903 1
            $sql .= $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' ' . $targetTableAlias . ' WHERE ';
1904
1905 1
            $owningAssoc = $targetClass->associationMappings[$assoc['mappedBy']];
1906 1
            $sqlParts    = [];
1907
1908 1
            foreach ($owningAssoc['targetToSourceKeyColumns'] as $targetColumn => $sourceColumn) {
1909 1
                $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $this->platform);
1910
1911 1
                $sqlParts[] = $sourceTableAlias . '.' . $targetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn;
1912
            }
1913
1914 1
            foreach ($this->quoteStrategy->getIdentifierColumnNames($targetClass, $this->platform) as $targetColumnName) {
1915 1
                if (isset($dqlParamKey)) {
1916 1
                    $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++);
1917
                }
1918
1919 1
                $sqlParts[] = $targetTableAlias . '.'  . $targetColumnName . ' = ' . $entitySql;
1920
            }
1921
1922 1
            $sql .= implode(' AND ', $sqlParts);
1923
        } else { // many-to-many
1924 5
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1925
1926 5
            $owningAssoc = $assoc['isOwningSide'] ? $assoc : $targetClass->associationMappings[$assoc['mappedBy']];
1927 5
            $joinTable = $owningAssoc['joinTable'];
1928
1929
            // SQL table aliases
1930 5
            $joinTableAlias   = $this->getSQLTableAlias($joinTable['name']);
1931 5
            $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName());
1932 5
            $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias);
1933
1934
            // join to target table
1935 5
            $sql .= $this->quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $this->platform) . ' ' . $joinTableAlias
1936 5
                . ' INNER JOIN ' . $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' ' . $targetTableAlias . ' ON ';
1937
1938
            // join conditions
1939 5
            $joinColumns  = $assoc['isOwningSide'] ? $joinTable['inverseJoinColumns'] : $joinTable['joinColumns'];
1940 5
            $joinSqlParts = [];
1941
1942 5
            foreach ($joinColumns as $joinColumn) {
1943 5
                $targetColumn = $this->quoteStrategy->getColumnName($targetClass->fieldNames[$joinColumn['referencedColumnName']], $targetClass, $this->platform);
1944
1945 5
                $joinSqlParts[] = $joinTableAlias . '.' . $joinColumn['name'] . ' = ' . $targetTableAlias . '.' . $targetColumn;
1946
            }
1947
1948 5
            $sql .= implode(' AND ', $joinSqlParts);
1949 5
            $sql .= ' WHERE ';
1950
1951 5
            $joinColumns = $assoc['isOwningSide'] ? $joinTable['joinColumns'] : $joinTable['inverseJoinColumns'];
1952 5
            $sqlParts    = [];
1953
1954 5
            foreach ($joinColumns as $joinColumn) {
1955 5
                $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$joinColumn['referencedColumnName']], $class, $this->platform);
1956
1957 5
                $sqlParts[] = $joinTableAlias . '.' . $joinColumn['name'] . ' = ' . $sourceTableAlias . '.' . $targetColumn;
1958
            }
1959
1960 5
            foreach ($this->quoteStrategy->getIdentifierColumnNames($targetClass, $this->platform) as $targetColumnName) {
1961 5
                if (isset($dqlParamKey)) {
1962 3
                    $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++);
1963
                }
1964
1965 5
                $sqlParts[] = $targetTableAlias . '.' . $targetColumnName . ' IN (' . $entitySql . ')';
1966
            }
1967
1968 5
            $sql .= implode(' AND ', $sqlParts);
1969
        }
1970
1971 6
        return $sql . ')';
1972
    }
1973
1974
    /**
1975
     * {@inheritdoc}
1976
     */
1977 3
    public function walkEmptyCollectionComparisonExpression($emptyCollCompExpr)
1978
    {
1979 3
        $sizeFunc = new AST\Functions\SizeFunction('size');
1980 3
        $sizeFunc->collectionPathExpression = $emptyCollCompExpr->expression;
1981
1982 3
        return $sizeFunc->getSql($this) . ($emptyCollCompExpr->not ? ' > 0' : ' = 0');
1983
    }
1984
1985
    /**
1986
     * {@inheritdoc}
1987
     */
1988 11
    public function walkNullComparisonExpression($nullCompExpr)
1989
    {
1990 11
        $expression = $nullCompExpr->expression;
1991 11
        $comparison = ' IS' . ($nullCompExpr->not ? ' NOT' : '') . ' NULL';
1992
1993
        // Handle ResultVariable
1994 11
        if (is_string($expression) && isset($this->queryComponents[$expression]['resultVariable'])) {
1995 2
            return $this->walkResultVariable($expression) . $comparison;
1996
        }
1997
1998
        // Handle InputParameter mapping inclusion to ParserResult
1999 9
        if ($expression instanceof AST\InputParameter) {
2000
            return $this->walkInputParameter($expression) . $comparison;
2001
        }
2002
2003 9
        return $expression->dispatch($this) . $comparison;
2004
    }
2005
2006
    /**
2007
     * {@inheritdoc}
2008
     */
2009 85
    public function walkInExpression($inExpr)
2010
    {
2011 85
        $sql = $this->walkArithmeticExpression($inExpr->expression) . ($inExpr->not ? ' NOT' : '') . ' IN (';
2012
2013 84
        $sql .= ($inExpr->subselect)
2014 7
            ? $this->walkSubselect($inExpr->subselect)
2015 84
            : implode(', ', array_map([$this, 'walkInParameter'], $inExpr->literals));
2016
2017 84
        $sql .= ')';
2018
2019 84
        return $sql;
2020
    }
2021
2022
    /**
2023
     * {@inheritdoc}
2024
     * @throws \Doctrine\ORM\Query\QueryException
2025
     */
2026 11
    public function walkInstanceOfExpression($instanceOfExpr)
2027
    {
2028 11
        $sql = '';
2029
2030 11
        $dqlAlias = $instanceOfExpr->identificationVariable;
2031 11
        $discrClass = $class = $this->queryComponents[$dqlAlias]['metadata'];
2032
2033 11
        if ($class->discriminatorColumn) {
2034 11
            $discrClass = $this->em->getClassMetadata($class->rootEntityName);
2035
        }
2036
2037 11
        if ($this->useSqlTableAliases) {
2038 11
            $sql .= $this->getSQLTableAlias($discrClass->getTableName(), $dqlAlias) . '.';
2039
        }
2040
2041 11
        $sql .= $class->discriminatorColumn['name'] . ($instanceOfExpr->not ? ' NOT IN ' : ' IN ');
2042 11
        $sql .= $this->getChildDiscriminatorsFromClassMetadata($discrClass, $instanceOfExpr);
2043
2044 10
        return $sql;
2045
    }
2046
2047
    /**
2048
     * {@inheritdoc}
2049
     */
2050 77
    public function walkInParameter($inParam)
2051
    {
2052 77
        return $inParam instanceof AST\InputParameter
2053 68
            ? $this->walkInputParameter($inParam)
2054 77
            : $this->walkLiteral($inParam);
2055
    }
2056
2057
    /**
2058
     * {@inheritdoc}
2059
     */
2060 148
    public function walkLiteral($literal)
2061
    {
2062 148
        switch ($literal->type) {
2063 148
            case AST\Literal::STRING:
2064 50
                return $this->conn->quote($literal->value);
2065
2066 111
            case AST\Literal::BOOLEAN:
2067 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...
2068
2069 104
            case AST\Literal::NUMERIC:
2070 104
                return $literal->value;
2071
2072
            default:
2073
                throw QueryException::invalidLiteral($literal);
2074
        }
2075
    }
2076
2077
    /**
2078
     * {@inheritdoc}
2079
     */
2080 6
    public function walkBetweenExpression($betweenExpr)
2081
    {
2082 6
        $sql = $this->walkArithmeticExpression($betweenExpr->expression);
2083
2084 6
        if ($betweenExpr->not) {
2085 2
            $sql .= ' NOT';
2086
        }
2087
2088 6
        $sql .= ' BETWEEN ' . $this->walkArithmeticExpression($betweenExpr->leftBetweenExpression)
2089 6
            . ' AND ' . $this->walkArithmeticExpression($betweenExpr->rightBetweenExpression);
2090
2091 6
        return $sql;
2092
    }
2093
2094
    /**
2095
     * {@inheritdoc}
2096
     */
2097 9
    public function walkLikeExpression($likeExpr)
2098
    {
2099 9
        $stringExpr = $likeExpr->stringExpression;
2100 9
        $leftExpr   = (is_string($stringExpr) && isset($this->queryComponents[$stringExpr]['resultVariable']))
2101 1
            ? $this->walkResultVariable($stringExpr)
2102 9
            : $stringExpr->dispatch($this);
2103
2104 9
        $sql = $leftExpr . ($likeExpr->not ? ' NOT' : '') . ' LIKE ';
2105
2106 9
        if ($likeExpr->stringPattern instanceof AST\InputParameter) {
2107 4
            $sql .= $this->walkInputParameter($likeExpr->stringPattern);
2108 6
        } elseif ($likeExpr->stringPattern instanceof AST\Functions\FunctionNode) {
2109 2
            $sql .= $this->walkFunction($likeExpr->stringPattern);
2110 6
        } elseif ($likeExpr->stringPattern instanceof AST\PathExpression) {
2111 2
            $sql .= $this->walkPathExpression($likeExpr->stringPattern);
2112
        } else {
2113 6
            $sql .= $this->walkLiteral($likeExpr->stringPattern);
2114
        }
2115
2116 9
        if ($likeExpr->escapeChar) {
2117 1
            $sql .= ' ESCAPE ' . $this->walkLiteral($likeExpr->escapeChar);
2118
        }
2119
2120 9
        return $sql;
2121
    }
2122
2123
    /**
2124
     * {@inheritdoc}
2125
     */
2126 5
    public function walkStateFieldPathExpression($stateFieldPathExpression)
2127
    {
2128 5
        return $this->walkPathExpression($stateFieldPathExpression);
2129
    }
2130
2131
    /**
2132
     * {@inheritdoc}
2133
     */
2134 256
    public function walkComparisonExpression($compExpr)
2135
    {
2136 256
        $leftExpr  = $compExpr->leftExpression;
2137 256
        $rightExpr = $compExpr->rightExpression;
2138 256
        $sql       = '';
2139
2140 256
        $sql .= ($leftExpr instanceof AST\Node)
2141 256
            ? $leftExpr->dispatch($this)
2142 255
            : (is_numeric($leftExpr) ? $leftExpr : $this->conn->quote($leftExpr));
2143
2144 255
        $sql .= ' ' . $compExpr->operator . ' ';
2145
2146 255
        $sql .= ($rightExpr instanceof AST\Node)
2147 253
            ? $rightExpr->dispatch($this)
2148 255
            : (is_numeric($rightExpr) ? $rightExpr : $this->conn->quote($rightExpr));
2149
2150 255
        return $sql;
2151
    }
2152
2153
    /**
2154
     * {@inheritdoc}
2155
     */
2156 211
    public function walkInputParameter($inputParam)
2157
    {
2158 211
        $this->parserResult->addParameterMapping($inputParam->name, $this->sqlParamIndex++);
2159
2160 211
        $parameter = $this->query->getParameter($inputParam->name);
2161
2162 211
        if ($parameter && Type::hasType($type = $parameter->getType())) {
2163 53
            return Type::getType($type)->convertToDatabaseValueSQL('?', $this->platform);
2164
        }
2165
2166 163
        return '?';
2167
    }
2168
2169
    /**
2170
     * {@inheritdoc}
2171
     */
2172 322
    public function walkArithmeticExpression($arithmeticExpr)
2173
    {
2174 322
        return ($arithmeticExpr->isSimpleArithmeticExpression())
2175 322
            ? $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...
2176 320
            : '(' . $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...
2177
    }
2178
2179
    /**
2180
     * {@inheritdoc}
2181
     */
2182 384
    public function walkSimpleArithmeticExpression($simpleArithmeticExpr)
2183
    {
2184 384
        if ( ! ($simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression)) {
2185 334
            return $this->walkArithmeticTerm($simpleArithmeticExpr);
2186
        }
2187
2188 75
        return implode(' ', array_map([$this, 'walkArithmeticTerm'], $simpleArithmeticExpr->arithmeticTerms));
2189
    }
2190
2191
    /**
2192
     * {@inheritdoc}
2193
     */
2194 405
    public function walkArithmeticTerm($term)
2195
    {
2196 405
        if (is_string($term)) {
2197 21
            return (isset($this->queryComponents[$term]))
2198 6
                ? $this->walkResultVariable($this->queryComponents[$term]['token']['value'])
2199 21
                : $term;
2200
        }
2201
2202
        // Phase 2 AST optimization: Skip processing of ArithmeticTerm
2203
        // if only one ArithmeticFactor is defined
2204 404
        if ( ! ($term instanceof AST\ArithmeticTerm)) {
2205 382
            return $this->walkArithmeticFactor($term);
2206
        }
2207
2208 47
        return implode(' ', array_map([$this, 'walkArithmeticFactor'], $term->arithmeticFactors));
2209
    }
2210
2211
    /**
2212
     * {@inheritdoc}
2213
     */
2214 405
    public function walkArithmeticFactor($factor)
2215
    {
2216 405
        if (is_string($factor)) {
2217 47
            return (isset($this->queryComponents[$factor]))
2218 2
                ? $this->walkResultVariable($this->queryComponents[$factor]['token']['value'])
2219 47
                : $factor;
2220
        }
2221
2222
        // Phase 2 AST optimization: Skip processing of ArithmeticFactor
2223
        // if only one ArithmeticPrimary is defined
2224 405
        if ( ! ($factor instanceof AST\ArithmeticFactor)) {
2225 404
            return $this->walkArithmeticPrimary($factor);
2226
        }
2227
2228 3
        $sign = $factor->isNegativeSigned() ? '-' : ($factor->isPositiveSigned() ? '+' : '');
2229
2230 3
        return $sign . $this->walkArithmeticPrimary($factor->arithmeticPrimary);
2231
    }
2232
2233
    /**
2234
     * Walks down an ArithmeticPrimary that represents an AST node, thereby generating the appropriate SQL.
2235
     *
2236
     * @param mixed $primary
2237
     *
2238
     * @return string The SQL.
2239
     */
2240 405
    public function walkArithmeticPrimary($primary)
2241
    {
2242 405
        if ($primary instanceof AST\SimpleArithmeticExpression) {
2243
            return '(' . $this->walkSimpleArithmeticExpression($primary) . ')';
2244
        }
2245
2246 405
        if ($primary instanceof AST\Node) {
2247 405
            return $primary->dispatch($this);
2248
        }
2249
2250
        return $this->walkEntityIdentificationVariable($primary);
2251
    }
2252
2253
    /**
2254
     * {@inheritdoc}
2255
     */
2256 18
    public function walkStringPrimary($stringPrimary)
2257
    {
2258 18
        return (is_string($stringPrimary))
2259
            ? $this->conn->quote($stringPrimary)
2260 18
            : $stringPrimary->dispatch($this);
2261
    }
2262
2263
    /**
2264
     * {@inheritdoc}
2265
     */
2266 30
    public function walkResultVariable($resultVariable)
2267
    {
2268 30
        $resultAlias = $this->scalarResultAliasMap[$resultVariable];
2269
2270 30
        if (is_array($resultAlias)) {
2271 1
            return implode(', ', $resultAlias);
2272
        }
2273
2274 29
        return $resultAlias;
2275
    }
2276
2277
    /**
2278
     * @param ClassMetadataInfo $discrClass
2279
     * @param AST\InstanceOfExpression $instanceOfExpr
2280
     * @return string The list in parentheses of valid child discriminators from the given class
2281
     * @throws QueryException
2282
     */
2283 11
    private function getChildDiscriminatorsFromClassMetadata(ClassMetadataInfo $discrClass, AST\InstanceOfExpression $instanceOfExpr)
2284
    {
2285 11
        $knownSubclasses = array_flip($discrClass->subClasses);
2286 11
        $sqlParameterList = [];
2287 11
        $discriminators = [];
2288 11
        foreach ($instanceOfExpr->value as $parameter) {
2289 11
            if ($parameter instanceof AST\InputParameter) {
2290 3
                $this->rsm->addMetadataParameterMapping($parameter->name, 'discriminatorValue');
2291
2292 3
                $sqlParameterList[] = $this->walkInputParameter($parameter);
2293
2294 3
                continue;
2295
            }
2296
2297
            // Trim first backslash
2298 8
            $parameter = ltrim($parameter, '\\');
2299
2300 8
            if ($parameter !== $discrClass->name && ! array_key_exists($parameter, $knownSubclasses)) {
2301 1
                throw QueryException::instanceOfUnrelatedClass($parameter, $discrClass->name);
2302
            }
2303
2304
            // Include discriminators for parameter class and its subclass
2305 7
            $metadata = $this->em->getClassMetadata($parameter);
2306 7
            $hierarchyClasses = $metadata->subClasses;
2307 7
            $hierarchyClasses[] = $metadata->name;
2308
2309 7
            foreach ($hierarchyClasses as $class) {
2310 7
                $currentMetadata = $this->em->getClassMetadata($class);
2311 7
                $currentDiscriminator = $currentMetadata->discriminatorValue;
2312
2313 7
                if (is_string($currentDiscriminator) && ! array_key_exists($currentDiscriminator, $discriminators)) {
2314 7
                    $discriminators[$currentDiscriminator] = true;
2315
                }
2316
            }
2317
        }
2318
2319 10
        foreach (array_keys($discriminators) as $dis) {
2320 7
            $sqlParameterList[] = $this->conn->quote($dis);
2321
        }
2322
2323 10
        return '(' . implode(', ', $sqlParameterList) . ')';
2324
    }
2325
}
2326