Completed
Pull Request — master (#6392)
by Alessandro
11:39
created

SqlWalker::walkNullIfExpression()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.1406

Importance

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