Completed
Pull Request — master (#6365)
by Daniel Tome
10:44
created

Parser::UpdateItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 10
c 0
b 0
f 0
ccs 5
cts 5
cp 1
rs 9.4285
cc 1
eloc 5
nc 1
nop 0
crap 1
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\ORM\Mapping\ClassMetadata;
23
use Doctrine\ORM\Query;
24
use Doctrine\ORM\Query\AST\Functions;
25
26
/**
27
 * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
28
 * Parses a DQL query, reports any errors in it, and generates an AST.
29
 *
30
 * @since   2.0
31
 * @author  Guilherme Blanco <[email protected]>
32
 * @author  Jonathan Wage <[email protected]>
33
 * @author  Roman Borschel <[email protected]>
34
 * @author  Janne Vanhala <[email protected]>
35
 * @author  Fabio B. Silva <[email protected]>
36
 */
37
class Parser
38
{
39
    /**
40
     * READ-ONLY: Maps BUILT-IN string function names to AST class names.
41
     *
42
     * @var array
43
     */
44
    private static $_STRING_FUNCTIONS = [
45
        'concat'    => Functions\ConcatFunction::class,
46
        'substring' => Functions\SubstringFunction::class,
47
        'trim'      => Functions\TrimFunction::class,
48
        'lower'     => Functions\LowerFunction::class,
49
        'upper'     => Functions\UpperFunction::class,
50
        'identity'  => Functions\IdentityFunction::class,
51
    ];
52
53
    /**
54
     * READ-ONLY: Maps BUILT-IN numeric function names to AST class names.
55
     *
56
     * @var array
57
     */
58
    private static $_NUMERIC_FUNCTIONS = [
59
        'length'    => Functions\LengthFunction::class,
60
        'locate'    => Functions\LocateFunction::class,
61
        'abs'       => Functions\AbsFunction::class,
62
        'sqrt'      => Functions\SqrtFunction::class,
63
        'mod'       => Functions\ModFunction::class,
64
        'size'      => Functions\SizeFunction::class,
65
        'date_diff' => Functions\DateDiffFunction::class,
66
        'bit_and'   => Functions\BitAndFunction::class,
67
        'bit_or'    => Functions\BitOrFunction::class,
68
    ];
69
70
    /**
71
     * READ-ONLY: Maps BUILT-IN datetime function names to AST class names.
72
     *
73
     * @var array
74
     */
75
    private static $_DATETIME_FUNCTIONS = [
76
        'current_date'      => Functions\CurrentDateFunction::class,
77
        'current_time'      => Functions\CurrentTimeFunction::class,
78
        'current_timestamp' => Functions\CurrentTimestampFunction::class,
79
        'date_add'          => Functions\DateAddFunction::class,
80
        'date_sub'          => Functions\DateSubFunction::class,
81
    ];
82
83
    /*
84
     * Expressions that were encountered during parsing of identifiers and expressions
85
     * and still need to be validated.
86
     */
87
88
    /**
89
     * @var array
90
     */
91
    private $deferredIdentificationVariables = [];
92
93
    /**
94
     * @var array
95
     */
96
    private $deferredPartialObjectExpressions = [];
97
98
    /**
99
     * @var array
100
     */
101
    private $deferredPathExpressions = [];
102
103
    /**
104
     * @var array
105
     */
106
    private $deferredResultVariables = [];
107
108
    /**
109
     * @var array
110
     */
111
    private $deferredNewObjectExpressions = [];
112
113
    /**
114
     * The lexer.
115
     *
116
     * @var \Doctrine\ORM\Query\Lexer
117
     */
118
    private $lexer;
119
120
    /**
121
     * The parser result.
122
     *
123
     * @var \Doctrine\ORM\Query\ParserResult
124
     */
125
    private $parserResult;
126
127
    /**
128
     * The EntityManager.
129
     *
130
     * @var \Doctrine\ORM\EntityManager
131
     */
132
    private $em;
133
134
    /**
135
     * The Query to parse.
136
     *
137
     * @var Query
138
     */
139
    private $query;
140
141
    /**
142
     * Map of declared query components in the parsed query.
143
     *
144
     * @var array
145
     */
146
    private $queryComponents = [];
147
148
    /**
149
     * Keeps the nesting level of defined ResultVariables.
150
     *
151
     * @var integer
152
     */
153
    private $nestingLevel = 0;
154
155
    /**
156
     * Any additional custom tree walkers that modify the AST.
157
     *
158
     * @var array
159
     */
160
    private $customTreeWalkers = [];
161
162
    /**
163
     * The custom last tree walker, if any, that is responsible for producing the output.
164
     *
165
     * @var TreeWalker
166
     */
167
    private $customOutputWalker;
168
169
    /**
170
     * @var array
171
     */
172
    private $identVariableExpressions = [];
173
174
    /**
175
     * Checks if a function is internally defined. Used to prevent overwriting
176
     * of built-in functions through user-defined functions.
177
     *
178
     * @param string $functionName
179
     *
180
     * @return bool
181
     */
182 5
    static public function isInternalFunction($functionName)
183
    {
184 5
        $functionName = strtolower($functionName);
185
186 5
        return isset(self::$_STRING_FUNCTIONS[$functionName])
187 5
            || isset(self::$_DATETIME_FUNCTIONS[$functionName])
188 5
            || isset(self::$_NUMERIC_FUNCTIONS[$functionName]);
189
    }
190
191
    /**
192
     * Creates a new query parser object.
193
     *
194
     * @param Query $query The Query to parse.
195
     */
196 474
    public function __construct(Query $query)
197
    {
198 474
        $this->query        = $query;
199 474
        $this->em           = $query->getEntityManager();
200 474
        $this->lexer        = new Lexer($query->getDql());
201 474
        $this->parserResult = new ParserResult();
202 474
    }
203
204
    /**
205
     * Sets a custom tree walker that produces output.
206
     * This tree walker will be run last over the AST, after any other walkers.
207
     *
208
     * @param string $className
209
     *
210
     * @return void
211
     */
212 127
    public function setCustomOutputTreeWalker($className)
213
    {
214 127
        $this->customOutputWalker = $className;
0 ignored issues
show
Documentation Bug introduced by
It seems like $className of type string is incompatible with the declared type object<Doctrine\ORM\Query\TreeWalker> of property $customOutputWalker.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
215 127
    }
216
217
    /**
218
     * Adds a custom tree walker for modifying the AST.
219
     *
220
     * @param string $className
221
     *
222
     * @return void
223
     */
224
    public function addCustomTreeWalker($className)
225
    {
226
        $this->customTreeWalkers[] = $className;
227
    }
228
229
    /**
230
     * Gets the lexer used by the parser.
231
     *
232
     * @return \Doctrine\ORM\Query\Lexer
233
     */
234 21
    public function getLexer()
235
    {
236 21
        return $this->lexer;
237
    }
238
239
    /**
240
     * Gets the ParserResult that is being filled with information during parsing.
241
     *
242
     * @return \Doctrine\ORM\Query\ParserResult
243
     */
244
    public function getParserResult()
245
    {
246
        return $this->parserResult;
247
    }
248
249
    /**
250
     * Gets the EntityManager used by the parser.
251
     *
252
     * @return \Doctrine\ORM\EntityManager
253
     */
254
    public function getEntityManager()
255
    {
256
        return $this->em;
257
    }
258
259
    /**
260
     * Parses and builds AST for the given Query.
261
     *
262
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
263
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
264
     *         \Doctrine\ORM\Query\AST\DeleteStatement
265
     */
266 474
    public function getAST()
267
    {
268
        // Parse & build AST
269 474
        $AST = $this->QueryLanguage();
270
271
        // Process any deferred validations of some nodes in the AST.
272
        // This also allows post-processing of the AST for modification purposes.
273 435
        $this->processDeferredIdentificationVariables();
274
275 433
        if ($this->deferredPartialObjectExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredPartialObjectExpressions 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...
276 6
            $this->processDeferredPartialObjectExpressions();
277
        }
278
279 431
        if ($this->deferredPathExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredPathExpressions 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...
280 332
            $this->processDeferredPathExpressions($AST);
281
        }
282
283 428
        if ($this->deferredResultVariables) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredResultVariables 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...
284 25
            $this->processDeferredResultVariables();
285
        }
286
287 428
        if ($this->deferredNewObjectExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredNewObjectExpressions 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...
288 3
            $this->processDeferredNewObjectExpressions($AST);
0 ignored issues
show
Documentation introduced by
$AST is of type object<Doctrine\ORM\Query\AST\SelectStatement>, but the function expects a object<Doctrine\ORM\Query\AST\SelectClause>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
289
        }
290
291 428
        $this->processRootEntityAliasSelected();
292
293
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
294 427
        $this->fixIdentificationVariableOrder($AST);
295
296 427
        return $AST;
297
    }
298
299
    /**
300
     * Attempts to match the given token with the current lookahead token.
301
     *
302
     * If they match, updates the lookahead token; otherwise raises a syntax
303
     * error.
304
     *
305
     * @param int $token The token type.
306
     *
307
     * @return void
308
     *
309
     * @throws QueryException If the tokens don't match.
310
     */
311 485
    public function match($token)
312
    {
313 485
        $lookaheadType = $this->lexer->lookahead['type'];
314
315
        // Short-circuit on first condition, usually types match
316 485
        if ($lookaheadType !== $token) {
317
            // If parameter is not identifier (1-99) must be exact match
318 20
            if ($token < Lexer::T_IDENTIFIER) {
319 2
                $this->syntaxError($this->lexer->getLiteral($token));
320
            }
321
322
            // If parameter is keyword (200+) must be exact match
323 18
            if ($token > Lexer::T_IDENTIFIER) {
324 7
                $this->syntaxError($this->lexer->getLiteral($token));
325
            }
326
327
            // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
328 11
            if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
329 8
                $this->syntaxError($this->lexer->getLiteral($token));
330
            }
331
        }
332
333 478
        $this->lexer->moveNext();
334 478
    }
335
336
    /**
337
     * Frees this parser, enabling it to be reused.
338
     *
339
     * @param boolean $deep     Whether to clean peek and reset errors.
340
     * @param integer $position Position to reset.
341
     *
342
     * @return void
343
     */
344
    public function free($deep = false, $position = 0)
345
    {
346
        // WARNING! Use this method with care. It resets the scanner!
347
        $this->lexer->resetPosition($position);
348
349
        // Deep = true cleans peek and also any previously defined errors
350
        if ($deep) {
351
            $this->lexer->resetPeek();
352
        }
353
354
        $this->lexer->token = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $token.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
355
        $this->lexer->lookahead = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $lookahead.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
356
    }
357
358
    /**
359
     * Parses a query string.
360
     *
361
     * @return ParserResult
362
     */
363 474
    public function parse()
364
    {
365 474
        $AST = $this->getAST();
366
367 427
        if (($customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) !== false) {
368 29
            $this->customTreeWalkers = $customWalkers;
0 ignored issues
show
Documentation Bug introduced by
It seems like $customWalkers of type * is incompatible with the declared type array of property $customTreeWalkers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
369
        }
370
371 427
        if (($customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER)) !== false) {
372 29
            $this->customOutputWalker = $customOutputWalker;
373
        }
374
375
        // Run any custom tree walkers over the AST
376 427
        if ($this->customTreeWalkers) {
377 28
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
378
379 28
            foreach ($this->customTreeWalkers as $walker) {
380 28
                $treeWalkerChain->addTreeWalker($walker);
381
            }
382
383
            switch (true) {
384 28
                case ($AST instanceof AST\UpdateStatement):
385
                    $treeWalkerChain->walkUpdateStatement($AST);
386
                    break;
387
388
                case ($AST instanceof AST\DeleteStatement):
389
                    $treeWalkerChain->walkDeleteStatement($AST);
390
                    break;
391
392
                case ($AST instanceof AST\SelectStatement):
393
                default:
394 28
                    $treeWalkerChain->walkSelectStatement($AST);
395
            }
396
397 27
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
398
        }
399
400 426
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
401 426
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
402
403
        // Assign an SQL executor to the parser result
404 426
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
405
406 418
        return $this->parserResult;
407
    }
408
409
    /**
410
     * Fixes order of identification variables.
411
     *
412
     * They have to appear in the select clause in the same order as the
413
     * declarations (from ... x join ... y join ... z ...) appear in the query
414
     * as the hydration process relies on that order for proper operation.
415
     *
416
     * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
417
     *
418
     * @return void
419
     */
420 427
    private function fixIdentificationVariableOrder($AST)
421
    {
422 427
        if (count($this->identVariableExpressions) <= 1) {
423 362
            return;
424
        }
425
426 65
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
427 65
            if ( ! isset($this->identVariableExpressions[$dqlAlias])) {
428 7
                continue;
429
            }
430
431 65
            $expr = $this->identVariableExpressions[$dqlAlias];
432 65
            $key  = array_search($expr, $AST->selectClause->selectExpressions);
433
434 65
            unset($AST->selectClause->selectExpressions[$key]);
435
436 65
            $AST->selectClause->selectExpressions[] = $expr;
437
        }
438 65
    }
439
440
    /**
441
     * Generates a new syntax error.
442
     *
443
     * @param string     $expected Expected string.
444
     * @param array|null $token    Got token.
445
     *
446
     * @return void
447
     *
448
     * @throws \Doctrine\ORM\Query\QueryException
449
     */
450 17
    public function syntaxError($expected = '', $token = null)
451
    {
452 17
        if ($token === null) {
453 14
            $token = $this->lexer->lookahead;
454
        }
455
456 17
        $tokenPos = (isset($token['position'])) ? $token['position'] : '-1';
457
458 17
        $message  = "line 0, col {$tokenPos}: Error: ";
459 17
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
460 17
        $message .= ($this->lexer->lookahead === null) ? 'end of string.' : "'{$token['value']}'";
461
462 17
        throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL()));
463
    }
464
465
    /**
466
     * Generates a new semantical error.
467
     *
468
     * @param string     $message Optional message.
469
     * @param array|null $token   Optional token.
470
     *
471
     * @return void
472
     *
473
     * @throws \Doctrine\ORM\Query\QueryException
474
     */
475 29
    public function semanticalError($message = '', $token = null)
476
    {
477 29
        if ($token === null) {
478 2
            $token = $this->lexer->lookahead;
479
        }
480
481
        // Minimum exposed chars ahead of token
482 29
        $distance = 12;
483
484
        // Find a position of a final word to display in error string
485 29
        $dql    = $this->query->getDql();
486 29
        $length = strlen($dql);
487 29
        $pos    = $token['position'] + $distance;
488 29
        $pos    = strpos($dql, ' ', ($length > $pos) ? $pos : $length);
489 29
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
490
491 29
        $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1';
492 29
        $tokenStr = substr($dql, $token['position'], $length);
493
494
        // Building informative message
495 29
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
496
497 29
        throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL()));
498
    }
499
500
    /**
501
     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
502
     *
503
     * @param boolean $resetPeek Reset peek after finding the closing parenthesis.
504
     *
505
     * @return array
506
     */
507 110
    private function peekBeyondClosingParenthesis($resetPeek = true)
508
    {
509 110
        $token = $this->lexer->peek();
510 110
        $numUnmatched = 1;
511
512 110
        while ($numUnmatched > 0 && $token !== null) {
513 109
            switch ($token['type']) {
514 109
                case Lexer::T_OPEN_PARENTHESIS:
515 22
                    ++$numUnmatched;
516 22
                    break;
517
518 109
                case Lexer::T_CLOSE_PARENTHESIS:
519 109
                    --$numUnmatched;
520 109
                    break;
521
522
                default:
523
                    // Do nothing
524
            }
525
526 109
            $token = $this->lexer->peek();
527
        }
528
529 110
        if ($resetPeek) {
530 89
            $this->lexer->resetPeek();
531
        }
532
533 110
        return $token;
534
    }
535
536
    /**
537
     * Checks if the given token indicates a mathematical operator.
538
     *
539
     * @param array $token
540
     *
541
     * @return boolean TRUE if the token is a mathematical operator, FALSE otherwise.
542
     */
543 181
    private function isMathOperator($token)
544
    {
545 181
        return in_array($token['type'], [Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY]);
546
    }
547
548
    /**
549
     * Checks if the next-next (after lookahead) token starts a function.
550
     *
551
     * @return boolean TRUE if the next-next tokens start a function, FALSE otherwise.
552
     */
553 216
    private function isFunction()
554
    {
555 216
        $lookaheadType = $this->lexer->lookahead['type'];
556 216
        $peek          = $this->lexer->peek();
557
558 216
        $this->lexer->resetPeek();
559
560 216
        return ($lookaheadType >= Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_OPEN_PARENTHESIS);
561
    }
562
563
    /**
564
     * Checks whether the given token type indicates an aggregate function.
565
     *
566
     * @param int $tokenType
567
     *
568
     * @return boolean TRUE if the token type is an aggregate function, FALSE otherwise.
569
     */
570 80
    private function isAggregateFunction($tokenType)
571
    {
572 80
        return in_array($tokenType, [Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT]);
573
    }
574
575
    /**
576
     * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
577
     *
578
     * @return boolean
579
     */
580 183
    private function isNextAllAnySome()
581
    {
582 183
        return in_array($this->lexer->lookahead['type'], [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME]);
583
    }
584
585
    /**
586
     * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
587
     * It must exist in query components list.
588
     *
589
     * @return void
590
     */
591 435
    private function processDeferredIdentificationVariables()
592
    {
593 435
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
594 427
            $identVariable = $deferredItem['expression'];
595
596
            // Check if IdentificationVariable exists in queryComponents
597 427
            if ( ! isset($this->queryComponents[$identVariable])) {
598 1
                $this->semanticalError(
599 1
                    "'$identVariable' is not defined.", $deferredItem['token']
600
                );
601
            }
602
603 427
            $qComp = $this->queryComponents[$identVariable];
604
605
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
606 427
            if ( ! isset($qComp['metadata'])) {
607
                $this->semanticalError(
608
                    "'$identVariable' does not point to a Class.", $deferredItem['token']
609
                );
610
            }
611
612
            // Validate if identification variable nesting level is lower or equal than the current one
613 427
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
614 1
                $this->semanticalError(
615 427
                    "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token']
616
                );
617
            }
618
        }
619 433
    }
620
621
    /**
622
     * Validates that the given <tt>NewObjectExpression</tt>.
623
     *
624
     * @param \Doctrine\ORM\Query\AST\SelectClause $AST
625
     *
626
     * @return void
627
     */
628 3
    private function processDeferredNewObjectExpressions($AST)
629
    {
630 3
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
631 3
            $expression     = $deferredItem['expression'];
632 3
            $token          = $deferredItem['token'];
633 3
            $className      = $expression->className;
634 3
            $args           = $expression->args;
635 3
            $fromClassName  = isset($AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName)
0 ignored issues
show
Bug introduced by
The property fromClause does not seem to exist in Doctrine\ORM\Query\AST\SelectClause.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
636 3
                ? $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName
637 3
                : null;
638
639
            // If the namespace is not given then assumes the first FROM entity namespace
640 3
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
641
                $namespace  = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
642
                $fqcn       = $namespace . '\\' . $className;
643
644
                if (class_exists($fqcn)) {
645
                    $expression->className  = $fqcn;
646
                    $className              = $fqcn;
647
                }
648
            }
649
650 3
            if ( ! class_exists($className)) {
651
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
652
            }
653
654 3
            $class = new \ReflectionClass($className);
655
656 3
            if ( ! $class->isInstantiable()) {
657
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
658
            }
659
660 3
            if ($class->getConstructor() === null) {
661
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
662
            }
663
664 3
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
665 3
                $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
666
            }
667
        }
668 3
    }
669
670
    /**
671
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
672
     * It must exist in query components list.
673
     *
674
     * @return void
675
     */
676 6
    private function processDeferredPartialObjectExpressions()
677
    {
678 6
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
679 6
            $expr = $deferredItem['expression'];
680 6
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
681
682 6
            foreach ($expr->partialFieldSet as $field) {
683 6
                if (isset($class->fieldMappings[$field])) {
684 6
                    continue;
685
                }
686
687 2
                if (isset($class->associationMappings[$field]) &&
688 2
                    $class->associationMappings[$field]['isOwningSide'] &&
689 2
                    $class->associationMappings[$field]['type'] & ClassMetadata::TO_ONE) {
690 1
                    continue;
691
                }
692
693 1
                $this->semanticalError(
694 1
                    "There is no mapped field named '$field' on class " . $class->name . ".", $deferredItem['token']
695
                );
696
            }
697
698 5
            if (array_intersect($class->identifier, $expr->partialFieldSet) != $class->identifier) {
699 1
                $this->semanticalError(
700 1
                    "The partial field selection of class " . $class->name . " must contain the identifier.",
701 5
                    $deferredItem['token']
702
                );
703
            }
704
        }
705 4
    }
706
707
    /**
708
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
709
     * It must exist in query components list.
710
     *
711
     * @return void
712
     */
713 25
    private function processDeferredResultVariables()
714
    {
715 25
        foreach ($this->deferredResultVariables as $deferredItem) {
716 25
            $resultVariable = $deferredItem['expression'];
717
718
            // Check if ResultVariable exists in queryComponents
719 25
            if ( ! isset($this->queryComponents[$resultVariable])) {
720
                $this->semanticalError(
721
                    "'$resultVariable' is not defined.", $deferredItem['token']
722
                );
723
            }
724
725 25
            $qComp = $this->queryComponents[$resultVariable];
726
727
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
728 25
            if ( ! isset($qComp['resultVariable'])) {
729
                $this->semanticalError(
730
                    "'$resultVariable' does not point to a ResultVariable.", $deferredItem['token']
731
                );
732
            }
733
734
            // Validate if identification variable nesting level is lower or equal than the current one
735 25
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
736
                $this->semanticalError(
737 25
                    "'$resultVariable' is used outside the scope of its declaration.", $deferredItem['token']
738
                );
739
            }
740
        }
741 25
    }
742
743
    /**
744
     * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
745
     *
746
     * AssociationPathExpression             ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
747
     * SingleValuedPathExpression            ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
748
     * StateFieldPathExpression              ::= IdentificationVariable "." StateField
749
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
750
     * CollectionValuedPathExpression        ::= IdentificationVariable "." CollectionValuedAssociationField
751
     *
752
     * @param mixed $AST
753
     *
754
     * @return void
755
     */
756 332
    private function processDeferredPathExpressions($AST)
0 ignored issues
show
Unused Code introduced by
The parameter $AST is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
757
    {
758 332
        foreach ($this->deferredPathExpressions as $deferredItem) {
759 332
            $pathExpression = $deferredItem['expression'];
760
761 332
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
762 332
            $class = $qComp['metadata'];
763
764 332
            if (($field = $pathExpression->field) === null) {
765 23
                $field = $pathExpression->field = $class->identifier[0];
766
            }
767
768
            // Check if field or association exists
769 332
            if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) {
770 1
                $this->semanticalError(
771 1
                    'Class ' . $class->name . ' has no field or association named ' . $field,
772 1
                    $deferredItem['token']
773
                );
774
            }
775
776 331
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
777
778 331
            if (isset($class->associationMappings[$field])) {
779 66
                $assoc = $class->associationMappings[$field];
780
781 66
                $fieldType = ($assoc['type'] & ClassMetadata::TO_ONE)
782 45
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
783 66
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
784
            }
785
786
            // Validate if PathExpression is one of the expected types
787 331
            $expectedType = $pathExpression->expectedType;
788
789 331
            if ( ! ($expectedType & $fieldType)) {
790
                // We need to recognize which was expected type(s)
791 2
                $expectedStringTypes = [];
792
793
                // Validate state field type
794 2
                if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
795 1
                    $expectedStringTypes[] = 'StateFieldPathExpression';
796
                }
797
798
                // Validate single valued association (*-to-one)
799 2
                if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
800 2
                    $expectedStringTypes[] = 'SingleValuedAssociationField';
801
                }
802
803
                // Validate single valued association (*-to-many)
804 2
                if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
805
                    $expectedStringTypes[] = 'CollectionValuedAssociationField';
806
                }
807
808
                // Build the error message
809 2
                $semanticalError  = 'Invalid PathExpression. ';
810 2
                $semanticalError .= (count($expectedStringTypes) == 1)
811 1
                    ? 'Must be a ' . $expectedStringTypes[0] . '.'
812 2
                    : implode(' or ', $expectedStringTypes) . ' expected.';
813
814 2
                $this->semanticalError($semanticalError, $deferredItem['token']);
815
            }
816
817
            // We need to force the type in PathExpression
818 329
            $pathExpression->type = $fieldType;
819
        }
820 329
    }
821
822
    /**
823
     * @return void
824
     */
825 428
    private function processRootEntityAliasSelected()
826
    {
827 428
        if ( ! count($this->identVariableExpressions)) {
828 166
            return;
829
        }
830
831 268
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
832 268
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
833 268
                return;
834
            }
835
        }
836
837 1
        $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
838
    }
839
840
    /**
841
     * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
842
     *
843
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
844
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
845
     *         \Doctrine\ORM\Query\AST\DeleteStatement
846
     */
847 474
    public function QueryLanguage()
848
    {
849 474
        $this->lexer->moveNext();
850
851 474
        switch ($this->lexer->lookahead['type']) {
852 474
            case Lexer::T_SELECT:
853 415
                $statement = $this->SelectStatement();
854 380
                break;
855
856 61
            case Lexer::T_UPDATE:
857 26
                $statement = $this->UpdateStatement();
858 26
                break;
859
860 36
            case Lexer::T_DELETE:
861 35
                $statement = $this->DeleteStatement();
862 34
                break;
863
864
            default:
865 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
866
                break;
867
        }
868
869
        // Check for end of string
870 438
        if ($this->lexer->lookahead !== null) {
871 3
            $this->syntaxError('end of string');
872
        }
873
874 435
        return $statement;
0 ignored issues
show
Bug introduced by
The variable $statement does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug Best Practice introduced by
The return type of return $statement; (Doctrine\ORM\Query\AST\S...ery\AST\DeleteStatement) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::QueryLanguage of type Doctrine\ORM\Query\AST\SelectStatement.

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...
875
    }
876
877
    /**
878
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
879
     *
880
     * @return \Doctrine\ORM\Query\AST\SelectStatement
881
     */
882 415
    public function SelectStatement()
883
    {
884 415
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
885
886 384
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
887 381
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
888 380
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
889 380
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
890
891 380
        return $selectStatement;
892
    }
893
894
    /**
895
     * UpdateStatement ::= UpdateClause [WhereClause]
896
     *
897
     * @return \Doctrine\ORM\Query\AST\UpdateStatement
898
     */
899 26
    public function UpdateStatement()
900
    {
901 26
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
902
903 26
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
904
905 26
        return $updateStatement;
906
    }
907
908
    /**
909
     * DeleteStatement ::= DeleteClause [WhereClause]
910
     *
911
     * @return \Doctrine\ORM\Query\AST\DeleteStatement
912
     */
913 35
    public function DeleteStatement()
914
    {
915 35
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
916
917 34
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
918
919 34
        return $deleteStatement;
920
    }
921
922
    /**
923
     * IdentificationVariable ::= identifier
924
     *
925
     * @return string
926
     */
927 455
    public function IdentificationVariable()
928
    {
929 455
        $this->match(Lexer::T_IDENTIFIER);
930
931 455
        $identVariable = $this->lexer->token['value'];
932
933 455
        $this->deferredIdentificationVariables[] = [
934 455
            'expression'   => $identVariable,
935 455
            'nestingLevel' => $this->nestingLevel,
936 455
            'token'        => $this->lexer->token,
937
        ];
938
939 455
        return $identVariable;
940
    }
941
942
    /**
943
     * AliasIdentificationVariable = identifier
944
     *
945
     * @return string
946
     */
947 445
    public function AliasIdentificationVariable()
948
    {
949 445
        $this->match(Lexer::T_IDENTIFIER);
950
951 445
        $aliasIdentVariable = $this->lexer->token['value'];
952 445
        $exists = isset($this->queryComponents[$aliasIdentVariable]);
953
954 445
        if ($exists) {
955 2
            $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->lexer->token);
956
        }
957
958 445
        return $aliasIdentVariable;
959
    }
960
961
    /**
962
     * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier
963
     *
964
     * @return string
965
     */
966 465
    public function AbstractSchemaName()
967
    {
968 465
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
969 450
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
970
971 450
            $schemaName = $this->lexer->token['value'];
972 16
        } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
973 8
            $this->match(Lexer::T_IDENTIFIER);
974
975 8
            $schemaName = $this->lexer->token['value'];
976
        } else {
977 8
            $this->match(Lexer::T_ALIASED_NAME);
978
979 8
            list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']);
980
981 8
            $schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
982
        }
983
984 465
        return $schemaName;
985
    }
986
987
    /**
988
     * Validates an AbstractSchemaName, making sure the class exists.
989
     *
990
     * @param string $schemaName The name to validate.
991
     *
992
     * @throws QueryException if the name does not exist.
993
     */
994 460
    private function validateAbstractSchemaName($schemaName)
995
    {
996 460
        if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {
997 16
            $this->semanticalError("Class '$schemaName' is not defined.", $this->lexer->token);
998
        }
999 445
    }
1000
1001
    /**
1002
     * AliasResultVariable ::= identifier
1003
     *
1004
     * @return string
1005
     */
1006 72
    public function AliasResultVariable()
1007
    {
1008 72
        $this->match(Lexer::T_IDENTIFIER);
1009
1010 68
        $resultVariable = $this->lexer->token['value'];
1011 68
        $exists = isset($this->queryComponents[$resultVariable]);
1012
1013 68
        if ($exists) {
1014 2
            $this->semanticalError("'$resultVariable' is already defined.", $this->lexer->token);
1015
        }
1016
1017 68
        return $resultVariable;
1018
    }
1019
1020
    /**
1021
     * ResultVariable ::= identifier
1022
     *
1023
     * @return string
1024
     */
1025 25
    public function ResultVariable()
1026
    {
1027 25
        $this->match(Lexer::T_IDENTIFIER);
1028
1029 25
        $resultVariable = $this->lexer->token['value'];
1030
1031
        // Defer ResultVariable validation
1032 25
        $this->deferredResultVariables[] = [
1033 25
            'expression'   => $resultVariable,
1034 25
            'nestingLevel' => $this->nestingLevel,
1035 25
            'token'        => $this->lexer->token,
1036
        ];
1037
1038 25
        return $resultVariable;
1039
    }
1040
1041
    /**
1042
     * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
1043
     *
1044
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1045
     */
1046 99
    public function JoinAssociationPathExpression()
1047
    {
1048 99
        $identVariable = $this->IdentificationVariable();
1049
1050 99
        if ( ! isset($this->queryComponents[$identVariable])) {
1051
            $this->semanticalError(
1052
                'Identification Variable ' . $identVariable .' used in join path expression but was not defined before.'
1053
            );
1054
        }
1055
1056 99
        $this->match(Lexer::T_DOT);
1057 99
        $this->match(Lexer::T_IDENTIFIER);
1058
1059 99
        $field = $this->lexer->token['value'];
1060
1061
        // Validate association field
1062 99
        $qComp = $this->queryComponents[$identVariable];
1063 99
        $class = $qComp['metadata'];
1064
1065 99
        if ( ! $class->hasAssociation($field)) {
1066
            $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
1067
        }
1068
1069 99
        return new AST\JoinAssociationPathExpression($identVariable, $field);
1070
    }
1071
1072
    /**
1073
     * Parses an arbitrary path expression and defers semantical validation
1074
     * based on expected types.
1075
     *
1076
     * PathExpression ::= IdentificationVariable {"." identifier}*
1077
     *
1078
     * @param integer $expectedTypes
1079
     *
1080
     * @return \Doctrine\ORM\Query\AST\PathExpression
1081
     */
1082 342
    public function PathExpression($expectedTypes)
1083
    {
1084 342
        $identVariable = $this->IdentificationVariable();
1085 342
        $field = null;
1086
1087 342
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1088 340
            $this->match(Lexer::T_DOT);
1089 340
            $this->match(Lexer::T_IDENTIFIER);
1090
1091 340
            $field = $this->lexer->token['value'];
1092
1093 340
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1094 1
                $this->match(Lexer::T_DOT);
1095 1
                $this->match(Lexer::T_IDENTIFIER);
1096 1
                $field .= '.'.$this->lexer->token['value'];
1097
            }
1098
        }
1099
1100
        // Creating AST node
1101 342
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1102
1103
        // Defer PathExpression validation if requested to be deferred
1104 342
        $this->deferredPathExpressions[] = [
1105 342
            'expression'   => $pathExpr,
1106 342
            'nestingLevel' => $this->nestingLevel,
1107 342
            'token'        => $this->lexer->token,
1108
        ];
1109
1110 342
        return $pathExpr;
1111
    }
1112
1113
    /**
1114
     * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
1115
     *
1116
     * @return \Doctrine\ORM\Query\AST\PathExpression
1117
     */
1118
    public function AssociationPathExpression()
1119
    {
1120
        return $this->PathExpression(
1121
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
1122
            AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
1123
        );
1124
    }
1125
1126
    /**
1127
     * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
1128
     *
1129
     * @return \Doctrine\ORM\Query\AST\PathExpression
1130
     */
1131 263
    public function SingleValuedPathExpression()
1132
    {
1133 263
        return $this->PathExpression(
1134 263
            AST\PathExpression::TYPE_STATE_FIELD |
1135 263
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1136
        );
1137
    }
1138
1139
    /**
1140
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1141
     *
1142
     * @return \Doctrine\ORM\Query\AST\PathExpression
1143
     */
1144 141
    public function StateFieldPathExpression()
1145
    {
1146 141
        return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
1147
    }
1148
1149
    /**
1150
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
1151
     *
1152
     * @return \Doctrine\ORM\Query\AST\PathExpression
1153
     */
1154 8
    public function SingleValuedAssociationPathExpression()
1155
    {
1156 8
        return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
1157
    }
1158
1159
    /**
1160
     * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
1161
     *
1162
     * @return \Doctrine\ORM\Query\AST\PathExpression
1163
     */
1164 21
    public function CollectionValuedPathExpression()
1165
    {
1166 21
        return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
1167
    }
1168
1169
    /**
1170
     * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
1171
     *
1172
     * @return \Doctrine\ORM\Query\AST\SelectClause
1173
     */
1174 415
    public function SelectClause()
1175
    {
1176 415
        $isDistinct = false;
1177 415
        $this->match(Lexer::T_SELECT);
1178
1179
        // Check for DISTINCT
1180 415
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1181 5
            $this->match(Lexer::T_DISTINCT);
1182
1183 5
            $isDistinct = true;
1184
        }
1185
1186
        // Process SelectExpressions (1..N)
1187 415
        $selectExpressions = [];
1188 415
        $selectExpressions[] = $this->SelectExpression();
1189
1190 407
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1191 122
            $this->match(Lexer::T_COMMA);
1192
1193 122
            $selectExpressions[] = $this->SelectExpression();
1194
        }
1195
1196 406
        return new AST\SelectClause($selectExpressions, $isDistinct);
1197
    }
1198
1199
    /**
1200
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1201
     *
1202
     * @return \Doctrine\ORM\Query\AST\SimpleSelectClause
1203
     */
1204 40
    public function SimpleSelectClause()
1205
    {
1206 40
        $isDistinct = false;
1207 40
        $this->match(Lexer::T_SELECT);
1208
1209 40
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1210
            $this->match(Lexer::T_DISTINCT);
1211
1212
            $isDistinct = true;
1213
        }
1214
1215 40
        return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1216
    }
1217
1218
    /**
1219
     * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1220
     *
1221
     * @return \Doctrine\ORM\Query\AST\UpdateClause
1222
     */
1223 26
    public function UpdateClause()
1224
    {
1225 26
        $this->match(Lexer::T_UPDATE);
1226
1227 26
        $token = $this->lexer->lookahead;
1228 26
        $abstractSchemaName = $this->AbstractSchemaName();
1229
1230 26
        $this->validateAbstractSchemaName($abstractSchemaName);
1231
1232 26
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1233 2
            $this->match(Lexer::T_AS);
1234
        }
1235
1236 26
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1237
1238 26
        $class = $this->em->getClassMetadata($abstractSchemaName);
1239
1240
        // Building queryComponent
1241
        $queryComponent = [
1242 26
            'metadata'     => $class,
1243
            'parent'       => null,
1244
            'relation'     => null,
1245
            'map'          => null,
1246 26
            'nestingLevel' => $this->nestingLevel,
1247 26
            'token'        => $token,
1248
        ];
1249
1250 26
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1251
1252 26
        $this->match(Lexer::T_SET);
1253
1254 26
        $updateItems = [];
1255 26
        $updateItems[] = $this->UpdateItem();
1256
1257 26
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1258 2
            $this->match(Lexer::T_COMMA);
1259
1260 2
            $updateItems[] = $this->UpdateItem();
1261
        }
1262
1263 26
        $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1264 26
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1265
1266 26
        return $updateClause;
1267
    }
1268
1269
    /**
1270
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1271
     *
1272
     * @return \Doctrine\ORM\Query\AST\DeleteClause
1273
     */
1274 35
    public function DeleteClause()
1275
    {
1276 35
        $this->match(Lexer::T_DELETE);
1277
1278 35
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1279 7
            $this->match(Lexer::T_FROM);
1280
        }
1281
1282 35
        $token = $this->lexer->lookahead;
1283 35
        $abstractSchemaName = $this->AbstractSchemaName();
1284
1285 35
        $this->validateAbstractSchemaName($abstractSchemaName);
1286
1287 35
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1288
1289 35
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1290
            $this->match(Lexer::T_AS);
1291
        }
1292
1293 35
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1294
1295 34
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1296 34
        $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1297
1298
        // Building queryComponent
1299
        $queryComponent = [
1300 34
            'metadata'     => $class,
1301
            'parent'       => null,
1302
            'relation'     => null,
1303
            'map'          => null,
1304 34
            'nestingLevel' => $this->nestingLevel,
1305 34
            'token'        => $token,
1306
        ];
1307
1308 34
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1309
1310 34
        return $deleteClause;
1311
    }
1312
1313
    /**
1314
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1315
     *
1316
     * @return \Doctrine\ORM\Query\AST\FromClause
1317
     */
1318 406
    public function FromClause()
1319
    {
1320 406
        $this->match(Lexer::T_FROM);
1321
1322 401
        $identificationVariableDeclarations = [];
1323 401
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1324
1325 384
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1326 6
            $this->match(Lexer::T_COMMA);
1327
1328 6
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1329
        }
1330
1331 384
        return new AST\FromClause($identificationVariableDeclarations);
1332
    }
1333
1334
    /**
1335
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1336
     *
1337
     * @return \Doctrine\ORM\Query\AST\SubselectFromClause
1338
     */
1339 40
    public function SubselectFromClause()
1340
    {
1341 40
        $this->match(Lexer::T_FROM);
1342
1343 40
        $identificationVariables = [];
1344 40
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1345
1346 39
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1347
            $this->match(Lexer::T_COMMA);
1348
1349
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1350
        }
1351
1352 39
        return new AST\SubselectFromClause($identificationVariables);
1353
    }
1354
1355
    /**
1356
     * WhereClause ::= "WHERE" ConditionalExpression
1357
     *
1358
     * @return \Doctrine\ORM\Query\AST\WhereClause
1359
     */
1360 218
    public function WhereClause()
1361
    {
1362 218
        $this->match(Lexer::T_WHERE);
1363
1364 218
        return new AST\WhereClause($this->ConditionalExpression());
1365
    }
1366
1367
    /**
1368
     * HavingClause ::= "HAVING" ConditionalExpression
1369
     *
1370
     * @return \Doctrine\ORM\Query\AST\HavingClause
1371
     */
1372 15
    public function HavingClause()
1373
    {
1374 15
        $this->match(Lexer::T_HAVING);
1375
1376 15
        return new AST\HavingClause($this->ConditionalExpression());
1377
    }
1378
1379
    /**
1380
     * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1381
     *
1382
     * @return \Doctrine\ORM\Query\AST\GroupByClause
1383
     */
1384 19
    public function GroupByClause()
1385
    {
1386 19
        $this->match(Lexer::T_GROUP);
1387 19
        $this->match(Lexer::T_BY);
1388
1389 19
        $groupByItems = [$this->GroupByItem()];
1390
1391 18
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1392 2
            $this->match(Lexer::T_COMMA);
1393
1394 2
            $groupByItems[] = $this->GroupByItem();
1395
        }
1396
1397 18
        return new AST\GroupByClause($groupByItems);
1398
    }
1399
1400
    /**
1401
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1402
     *
1403
     * @return \Doctrine\ORM\Query\AST\OrderByClause
1404
     */
1405 49
    public function OrderByClause()
1406
    {
1407 49
        $this->match(Lexer::T_ORDER);
1408 49
        $this->match(Lexer::T_BY);
1409
1410 49
        $orderByItems = [];
1411 49
        $orderByItems[] = $this->OrderByItem();
1412
1413 49
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1414 4
            $this->match(Lexer::T_COMMA);
1415
1416 4
            $orderByItems[] = $this->OrderByItem();
1417
        }
1418
1419 49
        return new AST\OrderByClause($orderByItems);
1420
    }
1421
1422
    /**
1423
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1424
     *
1425
     * @return \Doctrine\ORM\Query\AST\Subselect
1426
     */
1427 40
    public function Subselect()
1428
    {
1429
        // Increase query nesting level
1430 40
        $this->nestingLevel++;
1431
1432 40
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1433
1434 39
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1435 39
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1436 39
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1437 39
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1438
1439
        // Decrease query nesting level
1440 39
        $this->nestingLevel--;
1441
1442 39
        return $subselect;
1443
    }
1444
1445
    /**
1446
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1447
     *
1448
     * @return \Doctrine\ORM\Query\AST\UpdateItem
1449
     */
1450 26
    public function UpdateItem()
1451
    {
1452 26
        $pathExpr = $this->SingleValuedPathExpression();
1453
1454 26
        $this->match(Lexer::T_EQUALS);
1455
1456 26
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1457
1458 26
        return $updateItem;
1459
    }
1460
1461
    /**
1462
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1463
     *
1464
     * @return string | \Doctrine\ORM\Query\AST\PathExpression
1465
     */
1466 19
    public function GroupByItem()
1467
    {
1468
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1469 19
        $glimpse = $this->lexer->glimpse();
1470
1471 19
        if ($glimpse['type'] === Lexer::T_DOT) {
1472 9
            return $this->SingleValuedPathExpression();
1473
        }
1474
1475
        // Still need to decide between IdentificationVariable or ResultVariable
1476 10
        $lookaheadValue = $this->lexer->lookahead['value'];
1477
1478 10
        if ( ! isset($this->queryComponents[$lookaheadValue])) {
1479 1
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1480
        }
1481
1482 9
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1483 7
            ? $this->IdentificationVariable()
1484 9
            : $this->ResultVariable();
1485
    }
1486
1487
    /**
1488
     * OrderByItem ::= (
1489
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1490
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1491
     * ) ["ASC" | "DESC"]
1492
     *
1493
     * @return \Doctrine\ORM\Query\AST\OrderByItem
1494
     */
1495 49
    public function OrderByItem()
1496
    {
1497 49
        $this->lexer->peek(); // lookahead => '.'
1498 49
        $this->lexer->peek(); // lookahead => token after '.'
1499
1500 49
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1501
1502 49
        $this->lexer->resetPeek();
1503
1504 49
        $glimpse = $this->lexer->glimpse();
1505
1506
        switch (true) {
1507 49
            case ($this->isFunction($peek)):
0 ignored issues
show
Unused Code introduced by
The call to Parser::isFunction() has too many arguments starting with $peek.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1508 1
                $expr = $this->FunctionDeclaration();
1509 1
                break;
1510
1511 48
            case ($this->isMathOperator($peek)):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1500 can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, 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...
1512 5
                $expr = $this->SimpleArithmeticExpression();
1513 5
                break;
1514
1515 44
            case ($glimpse['type'] === Lexer::T_DOT):
1516 35
                $expr = $this->SingleValuedPathExpression();
1517 35
                break;
1518
1519 12
            case ($this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis())):
0 ignored issues
show
Bug introduced by
It seems like $this->peekBeyondClosingParenthesis() targeting Doctrine\ORM\Query\Parse...ondClosingParenthesis() can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1520 2
                $expr = $this->ScalarExpression();
1521 2
                break;
1522
1523
            default:
1524 10
                $expr = $this->ResultVariable();
1525 10
                break;
1526
        }
1527
1528 49
        $type = 'ASC';
1529 49
        $item = new AST\OrderByItem($expr);
1530
1531
        switch (true) {
1532 49
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1533 20
                $this->match(Lexer::T_DESC);
1534 20
                $type = 'DESC';
1535 20
                break;
1536
1537 32
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1538 12
                $this->match(Lexer::T_ASC);
1539 12
                break;
1540
1541
            default:
1542
                // Do nothing
1543
        }
1544
1545 49
        $item->type = $type;
1546
1547 49
        return $item;
1548
    }
1549
1550
    /**
1551
     * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1552
     *      EnumPrimary | SimpleEntityExpression | "NULL"
1553
     *
1554
     * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1555
     * grammar that needs to be supported:
1556
     *
1557
     * NewValue ::= SimpleArithmeticExpression | "NULL"
1558
     *
1559
     * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1560
     *
1561
     * @return AST\ArithmeticExpression
1562
     */
1563 26
    public function NewValue()
1564
    {
1565 26
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1566 1
            $this->match(Lexer::T_NULL);
1567
1568 1
            return null;
1569
        }
1570
1571 25
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1572 15
            $this->match(Lexer::T_INPUT_PARAMETER);
1573
1574 15
            return new AST\InputParameter($this->lexer->token['value']);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Doctrine\ORM...lexer->token['value']); (Doctrine\ORM\Query\AST\InputParameter) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::NewValue of type Doctrine\ORM\Query\AST\ArithmeticExpression|null.

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...
1575
        }
1576
1577 10
        return $this->ArithmeticExpression();
1578
    }
1579
1580
    /**
1581
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1582
     *
1583
     * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1584
     */
1585 403
    public function IdentificationVariableDeclaration()
1586
    {
1587 403
        $joins                    = [];
1588 403
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1589 388
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1590 8
            ? $this->IndexBy()
1591 388
            : null;
1592
1593 388
        $rangeVariableDeclaration->isRoot = true;
1594
1595
        while (
1596 388
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1597 388
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1598 388
            $this->lexer->isNextToken(Lexer::T_JOIN)
1599
        ) {
1600 118
            $joins[] = $this->Join();
1601
        }
1602
1603 386
        return new AST\IdentificationVariableDeclaration(
1604
            $rangeVariableDeclaration, $indexBy, $joins
1605
        );
1606
    }
1607
1608
    /**
1609
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1610
     *
1611
     * {Internal note: WARNING: Solution is harder than a bare implementation.
1612
     * Desired EBNF support:
1613
     *
1614
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1615
     *
1616
     * It demands that entire SQL generation to become programmatical. This is
1617
     * needed because association based subselect requires "WHERE" conditional
1618
     * expressions to be injected, but there is no scope to do that. Only scope
1619
     * accessible is "FROM", prohibiting an easy implementation without larger
1620
     * changes.}
1621
     *
1622
     * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration |
1623
     *         \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1624
     */
1625 40
    public function SubselectIdentificationVariableDeclaration()
1626
    {
1627
        /*
1628
        NOT YET IMPLEMENTED!
1629
1630
        $glimpse = $this->lexer->glimpse();
1631
1632
        if ($glimpse['type'] == Lexer::T_DOT) {
1633
            $associationPathExpression = $this->AssociationPathExpression();
1634
1635
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1636
                $this->match(Lexer::T_AS);
1637
            }
1638
1639
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1640
            $identificationVariable      = $associationPathExpression->identificationVariable;
1641
            $field                       = $associationPathExpression->associationField;
1642
1643
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1644
            $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1645
1646
            // Building queryComponent
1647
            $joinQueryComponent = array(
1648
                'metadata'     => $targetClass,
1649
                'parent'       => $identificationVariable,
1650
                'relation'     => $class->getAssociationMapping($field),
1651
                'map'          => null,
1652
                'nestingLevel' => $this->nestingLevel,
1653
                'token'        => $this->lexer->lookahead
1654
            );
1655
1656
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1657
1658
            return new AST\SubselectIdentificationVariableDeclaration(
1659
                $associationPathExpression, $aliasIdentificationVariable
1660
            );
1661
        }
1662
        */
1663
1664 40
        return $this->IdentificationVariableDeclaration();
1665
    }
1666
1667
    /**
1668
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1669
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1670
     *          ["WITH" ConditionalExpression]
1671
     *
1672
     * @return \Doctrine\ORM\Query\AST\Join
1673
     */
1674 118
    public function Join()
1675
    {
1676
        // Check Join type
1677 118
        $joinType = AST\Join::JOIN_TYPE_INNER;
1678
1679
        switch (true) {
1680 118
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1681 37
                $this->match(Lexer::T_LEFT);
1682
1683 37
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1684
1685
                // Possible LEFT OUTER join
1686 37
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1687
                    $this->match(Lexer::T_OUTER);
1688
1689
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1690
                }
1691 37
                break;
1692
1693 85
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1694 13
                $this->match(Lexer::T_INNER);
1695 13
                break;
1696
1697
            default:
1698
                // Do nothing
1699
        }
1700
1701 118
        $this->match(Lexer::T_JOIN);
1702
1703 118
        $next            = $this->lexer->glimpse();
1704 118
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1705 116
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1706 116
        $join            = new AST\Join($joinType, $joinDeclaration);
1707
1708
        // Describe non-root join declaration
1709 116
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1710 19
            $joinDeclaration->isRoot = false;
1711
        }
1712
1713
        // Check for ad-hoc Join conditions
1714 116
        if ($adhocConditions) {
1715 20
            $this->match(Lexer::T_WITH);
1716
1717 20
            $join->conditionalExpression = $this->ConditionalExpression();
1718
        }
1719
1720 116
        return $join;
1721
    }
1722
1723
    /**
1724
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1725
     *
1726
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1727
     */
1728 403
    public function RangeVariableDeclaration()
1729
    {
1730 403
        $abstractSchemaName = $this->AbstractSchemaName();
1731
1732 403
        $this->validateAbstractSchemaName($abstractSchemaName);
1733
1734 388
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1735
            $this->match(Lexer::T_AS);
1736
        }
1737
1738 388
        $token = $this->lexer->lookahead;
1739 388
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1740 388
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1741
1742
        // Building queryComponent
1743
        $queryComponent = [
1744 388
            'metadata'     => $classMetadata,
1745
            'parent'       => null,
1746
            'relation'     => null,
1747
            'map'          => null,
1748 388
            'nestingLevel' => $this->nestingLevel,
1749 388
            'token'        => $token
1750
        ];
1751
1752 388
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1753
1754 388
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1755
    }
1756
1757
    /**
1758
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1759
     *
1760
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1761
     */
1762 99
    public function JoinAssociationDeclaration()
1763
    {
1764 99
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1765
1766 99
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1767
            $this->match(Lexer::T_AS);
1768
        }
1769
1770 99
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1771 97
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1772
1773 97
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1774 97
        $field                  = $joinAssociationPathExpression->associationField;
1775
1776 97
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1777 97
        $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1778
1779
        // Building queryComponent
1780
        $joinQueryComponent = [
1781 97
            'metadata'     => $targetClass,
1782 97
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1783 97
            'relation'     => $class->getAssociationMapping($field),
1784
            'map'          => null,
1785 97
            'nestingLevel' => $this->nestingLevel,
1786 97
            'token'        => $this->lexer->lookahead
1787
        ];
1788
1789 97
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1790
1791 97
        return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
1792
    }
1793
1794
    /**
1795
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1796
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1797
     *
1798
     * @return array
1799
     */
1800 6
    public function PartialObjectExpression()
1801
    {
1802 6
        $this->match(Lexer::T_PARTIAL);
1803
1804 6
        $partialFieldSet = [];
1805
1806 6
        $identificationVariable = $this->IdentificationVariable();
1807
1808 6
        $this->match(Lexer::T_DOT);
1809 6
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1810 6
        $this->match(Lexer::T_IDENTIFIER);
1811
1812 6
        $field = $this->lexer->token['value'];
1813
1814
        // First field in partial expression might be embeddable property
1815 6
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1816
            $this->match(Lexer::T_DOT);
1817
            $this->match(Lexer::T_IDENTIFIER);
1818
            $field .= '.'.$this->lexer->token['value'];
1819
        }
1820
1821 6
        $partialFieldSet[] = $field;
1822
1823 6
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1824 5
            $this->match(Lexer::T_COMMA);
1825 5
            $this->match(Lexer::T_IDENTIFIER);
1826
1827 5
            $field = $this->lexer->token['value'];
1828
1829 5
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1830 1
                $this->match(Lexer::T_DOT);
1831 1
                $this->match(Lexer::T_IDENTIFIER);
1832 1
                $field .= '.'.$this->lexer->token['value'];
1833
            }
1834
1835 5
            $partialFieldSet[] = $field;
1836
        }
1837
1838 6
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1839
1840 6
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1841
1842
        // Defer PartialObjectExpression validation
1843 6
        $this->deferredPartialObjectExpressions[] = [
1844 6
            'expression'   => $partialObjectExpression,
1845 6
            'nestingLevel' => $this->nestingLevel,
1846 6
            'token'        => $this->lexer->token,
1847
        ];
1848
1849 6
        return $partialObjectExpression;
1850
    }
1851
1852
    /**
1853
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1854
     *
1855
     * @return \Doctrine\ORM\Query\AST\NewObjectExpression
1856
     */
1857 3
    public function NewObjectExpression()
1858
    {
1859 3
        $this->match(Lexer::T_NEW);
1860
1861 3
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1862 3
        $token = $this->lexer->token;
1863
1864 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1865
1866 3
        $args[] = $this->NewObjectArg();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$args was never initialized. Although not strictly required by PHP, it is generally a good practice to add $args = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1867
1868 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1869 3
            $this->match(Lexer::T_COMMA);
1870
1871 3
            $args[] = $this->NewObjectArg();
1872
        }
1873
1874 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1875
1876 3
        $expression = new AST\NewObjectExpression($className, $args);
1877
1878
        // Defer NewObjectExpression validation
1879 3
        $this->deferredNewObjectExpressions[] = [
1880 3
            'token'        => $token,
1881 3
            'expression'   => $expression,
1882 3
            'nestingLevel' => $this->nestingLevel,
1883
        ];
1884
1885 3
        return $expression;
1886
    }
1887
1888
    /**
1889
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1890
     *
1891
     * @return mixed
1892
     */
1893 3
    public function NewObjectArg()
1894
    {
1895 3
        $token = $this->lexer->lookahead;
1896 3
        $peek  = $this->lexer->glimpse();
1897
1898 3
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1899 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1900 2
            $expression = $this->Subselect();
1901 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1902
1903 2
            return $expression;
1904
        }
1905
1906 3
        return $this->ScalarExpression();
1907
    }
1908
1909
    /**
1910
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1911
     *
1912
     * @return \Doctrine\ORM\Query\AST\IndexBy
1913
     */
1914 11
    public function IndexBy()
1915
    {
1916 11
        $this->match(Lexer::T_INDEX);
1917 11
        $this->match(Lexer::T_BY);
1918 11
        $pathExpr = $this->StateFieldPathExpression();
1919
1920
        // Add the INDEX BY info to the query component
1921 11
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1922
1923 11
        return new AST\IndexBy($pathExpr);
1924
    }
1925
1926
    /**
1927
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1928
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1929
     *                      InstanceOfExpression
1930
     *
1931
     * @return mixed One of the possible expressions or subexpressions.
1932
     */
1933 112
    public function ScalarExpression()
1934
    {
1935 112
        $lookahead = $this->lexer->lookahead['type'];
1936 112
        $peek      = $this->lexer->glimpse();
1937
1938
        switch (true) {
1939 112
            case ($lookahead === Lexer::T_INTEGER):
1940 110
            case ($lookahead === Lexer::T_FLOAT):
1941
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1942 110
            case ($lookahead === Lexer::T_MINUS):
1943 110
            case ($lookahead === Lexer::T_PLUS):
1944 15
                return $this->SimpleArithmeticExpression();
1945
1946 110
            case ($lookahead === Lexer::T_STRING):
1947 11
                return $this->StringPrimary();
1948
1949 108
            case ($lookahead === Lexer::T_TRUE):
1950 108
            case ($lookahead === Lexer::T_FALSE):
1951 2
                $this->match($lookahead);
1952
1953 2
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1954
1955 108
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
1956
                switch (true) {
1957 1
                    case $this->isMathOperator($peek):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->glimpse() on line 1936 can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, 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...
1958
                        // :param + u.value
1959 1
                        return $this->SimpleArithmeticExpression();
1960
                    default:
1961
                        return $this->InputParameter();
1962
                }
1963
1964 108
            case ($lookahead === Lexer::T_CASE):
1965 104
            case ($lookahead === Lexer::T_COALESCE):
1966 104
            case ($lookahead === Lexer::T_NULLIF):
1967
                // Since NULLIF and COALESCE can be identified as a function,
1968
                // we need to check these before checking for FunctionDeclaration
1969 7
                return $this->CaseExpression();
1970
1971 104
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1972 4
                return $this->SimpleArithmeticExpression();
1973
1974
            // this check must be done before checking for a filed path expression
1975 101
            case ($this->isFunction()):
1976 18
                $this->lexer->peek(); // "("
1977
1978
                switch (true) {
1979 18
                    case ($this->isMathOperator($this->peekBeyondClosingParenthesis())):
0 ignored issues
show
Bug introduced by
It seems like $this->peekBeyondClosingParenthesis() targeting Doctrine\ORM\Query\Parse...ondClosingParenthesis() can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1980
                        // SUM(u.id) + COUNT(u.id)
1981 6
                        return $this->SimpleArithmeticExpression();
1982
1983 14
                    case ($this->isAggregateFunction($this->lexer->lookahead['type'])):
1984 12
                        return $this->AggregateExpression();
1985
1986
                    default:
1987
                        // IDENTITY(u)
1988 2
                        return $this->FunctionDeclaration();
1989
                }
1990
1991
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1992
            // it is no function, so it must be a field path
1993 89
            case ($lookahead === Lexer::T_IDENTIFIER):
1994 89
                $this->lexer->peek(); // lookahead => '.'
1995 89
                $this->lexer->peek(); // lookahead => token after '.'
1996 89
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1997 89
                $this->lexer->resetPeek();
1998
1999 89
                if ($this->isMathOperator($peek)) {
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1996 can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, 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...
2000 3
                    return $this->SimpleArithmeticExpression();
2001
                }
2002
2003 87
                return $this->StateFieldPathExpression();
2004
2005
            default:
2006
                $this->syntaxError();
2007
        }
2008
    }
2009
2010
    /**
2011
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
2012
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2013
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2014
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2015
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2016
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2017
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2018
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2019
     *
2020
     * @return mixed One of the possible expressions or subexpressions.
2021
     */
2022 17
    public function CaseExpression()
2023
    {
2024 17
        $lookahead = $this->lexer->lookahead['type'];
2025
2026
        switch ($lookahead) {
2027 17
            case Lexer::T_NULLIF:
2028 5
                return $this->NullIfExpression();
2029
2030 14
            case Lexer::T_COALESCE:
2031 2
                return $this->CoalesceExpression();
2032
2033 12
            case Lexer::T_CASE:
2034 12
                $this->lexer->resetPeek();
2035 12
                $peek = $this->lexer->peek();
2036
2037 12
                if ($peek['type'] === Lexer::T_WHEN) {
2038 7
                    return $this->GeneralCaseExpression();
2039
                }
2040
2041 5
                return $this->SimpleCaseExpression();
2042
2043
            default:
2044
                // Do nothing
2045
                break;
2046
        }
2047
2048
        $this->syntaxError();
2049
    }
2050
2051
    /**
2052
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2053
     *
2054
     * @return \Doctrine\ORM\Query\AST\CoalesceExpression
2055
     */
2056 3
    public function CoalesceExpression()
2057
    {
2058 3
        $this->match(Lexer::T_COALESCE);
2059 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2060
2061
        // Process ScalarExpressions (1..N)
2062 3
        $scalarExpressions = [];
2063 3
        $scalarExpressions[] = $this->ScalarExpression();
2064
2065 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2066 3
            $this->match(Lexer::T_COMMA);
2067
2068 3
            $scalarExpressions[] = $this->ScalarExpression();
2069
        }
2070
2071 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2072
2073 3
        return new AST\CoalesceExpression($scalarExpressions);
2074
    }
2075
2076
    /**
2077
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2078
     *
2079
     * @return \Doctrine\ORM\Query\AST\NullIfExpression
2080
     */
2081 5
    public function NullIfExpression()
2082
    {
2083 5
        $this->match(Lexer::T_NULLIF);
2084 5
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2085
2086 5
        $firstExpression = $this->ScalarExpression();
2087 5
        $this->match(Lexer::T_COMMA);
2088 5
        $secondExpression = $this->ScalarExpression();
2089
2090 5
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2091
2092 5
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2093
    }
2094
2095
    /**
2096
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2097
     *
2098
     * @return \Doctrine\ORM\Query\AST\GeneralCaseExpression
2099
     */
2100 7
    public function GeneralCaseExpression()
2101
    {
2102 7
        $this->match(Lexer::T_CASE);
2103
2104
        // Process WhenClause (1..N)
2105 7
        $whenClauses = [];
2106
2107
        do {
2108 7
            $whenClauses[] = $this->WhenClause();
2109 7
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2110
2111 7
        $this->match(Lexer::T_ELSE);
2112 7
        $scalarExpression = $this->ScalarExpression();
2113 7
        $this->match(Lexer::T_END);
2114
2115 7
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2116
    }
2117
2118
    /**
2119
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2120
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2121
     *
2122
     * @return AST\SimpleCaseExpression
2123
     */
2124 5
    public function SimpleCaseExpression()
2125
    {
2126 5
        $this->match(Lexer::T_CASE);
2127 5
        $caseOperand = $this->StateFieldPathExpression();
2128
2129
        // Process SimpleWhenClause (1..N)
2130 5
        $simpleWhenClauses = [];
2131
2132
        do {
2133 5
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2134 5
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2135
2136 5
        $this->match(Lexer::T_ELSE);
2137 5
        $scalarExpression = $this->ScalarExpression();
2138 5
        $this->match(Lexer::T_END);
2139
2140 5
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2141
    }
2142
2143
    /**
2144
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2145
     *
2146
     * @return \Doctrine\ORM\Query\AST\WhenClause
2147
     */
2148 7
    public function WhenClause()
2149
    {
2150 7
        $this->match(Lexer::T_WHEN);
2151 7
        $conditionalExpression = $this->ConditionalExpression();
2152 7
        $this->match(Lexer::T_THEN);
2153
2154 7
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2155
    }
2156
2157
    /**
2158
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2159
     *
2160
     * @return \Doctrine\ORM\Query\AST\SimpleWhenClause
2161
     */
2162 5
    public function SimpleWhenClause()
2163
    {
2164 5
        $this->match(Lexer::T_WHEN);
2165 5
        $conditionalExpression = $this->ScalarExpression();
2166 5
        $this->match(Lexer::T_THEN);
2167
2168 5
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2169
    }
2170
2171
    /**
2172
     * SelectExpression ::= (
2173
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2174
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2175
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2176
     *
2177
     * @return \Doctrine\ORM\Query\AST\SelectExpression
2178
     */
2179 415
    public function SelectExpression()
2180
    {
2181 415
        $expression    = null;
2182 415
        $identVariable = null;
2183 415
        $peek          = $this->lexer->glimpse();
2184 415
        $lookaheadType = $this->lexer->lookahead['type'];
2185
2186
        switch (true) {
2187
            // ScalarExpression (u.name)
2188 415
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2189 80
                $expression = $this->ScalarExpression();
2190 80
                break;
2191
2192
            // IdentificationVariable (u)
2193 362
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2194 294
                $expression = $identVariable = $this->IdentificationVariable();
2195 294
                break;
2196
2197
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
2198 103
            case ($lookaheadType === Lexer::T_CASE):
2199 98
            case ($lookaheadType === Lexer::T_COALESCE):
2200 96
            case ($lookaheadType === Lexer::T_NULLIF):
2201 9
                $expression = $this->CaseExpression();
2202 9
                break;
2203
2204
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2205 94
            case ($this->isFunction()):
2206 51
                $this->lexer->peek(); // "("
2207
2208
                switch (true) {
2209 51
                    case ($this->isMathOperator($this->peekBeyondClosingParenthesis())):
0 ignored issues
show
Bug introduced by
It seems like $this->peekBeyondClosingParenthesis() targeting Doctrine\ORM\Query\Parse...ondClosingParenthesis() can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2210
                        // SUM(u.id) + COUNT(u.id)
2211 2
                        $expression = $this->ScalarExpression();
2212 2
                        break;
2213
2214 49
                    case ($this->isAggregateFunction($lookaheadType)):
2215
                        // COUNT(u.id)
2216 38
                        $expression = $this->AggregateExpression();
2217 38
                        break;
2218
2219
                    default:
2220
                        // IDENTITY(u)
2221 11
                        $expression = $this->FunctionDeclaration();
2222 11
                        break;
2223
                }
2224
2225 51
                break;
2226
2227
            // PartialObjectExpression (PARTIAL u.{id, name})
2228 43
            case ($lookaheadType === Lexer::T_PARTIAL):
2229 6
                $expression    = $this->PartialObjectExpression();
2230 6
                $identVariable = $expression->identificationVariable;
2231 6
                break;
2232
2233
            // Subselect
2234 37
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2235 17
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2236 17
                $expression = $this->Subselect();
2237 17
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2238 17
                break;
2239
2240
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2241 20
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2242 18
            case ($lookaheadType === Lexer::T_INTEGER):
2243 16
            case ($lookaheadType === Lexer::T_STRING):
2244 7
            case ($lookaheadType === Lexer::T_FLOAT):
2245
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2246 7
            case ($lookaheadType === Lexer::T_MINUS):
2247 7
            case ($lookaheadType === Lexer::T_PLUS):
2248 14
                $expression = $this->SimpleArithmeticExpression();
2249 14
                break;
2250
2251
            // NewObjectExpression (New ClassName(id, name))
2252 6
            case ($lookaheadType === Lexer::T_NEW):
2253 3
                $expression = $this->NewObjectExpression();
2254 3
                break;
2255
2256
            default:
2257 3
                $this->syntaxError(
2258 3
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2259 3
                    $this->lexer->lookahead
2260
                );
2261
        }
2262
2263
        // [["AS"] ["HIDDEN"] AliasResultVariable]
2264 412
        $mustHaveAliasResultVariable = false;
2265
2266 412
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2267 64
            $this->match(Lexer::T_AS);
2268
2269 64
            $mustHaveAliasResultVariable = true;
2270
        }
2271
2272 412
        $hiddenAliasResultVariable = false;
2273
2274 412
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2275 5
            $this->match(Lexer::T_HIDDEN);
2276
2277 5
            $hiddenAliasResultVariable = true;
2278
        }
2279
2280 412
        $aliasResultVariable = null;
2281
2282 412
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2283 72
            $token = $this->lexer->lookahead;
2284 72
            $aliasResultVariable = $this->AliasResultVariable();
2285
2286
            // Include AliasResultVariable in query components.
2287 67
            $this->queryComponents[$aliasResultVariable] = [
2288 67
                'resultVariable' => $expression,
2289 67
                'nestingLevel'   => $this->nestingLevel,
2290 67
                'token'          => $token,
2291
            ];
2292
        }
2293
2294
        // AST
2295
2296 407
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2297
2298 407
        if ($identVariable) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $identVariable of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2299 299
            $this->identVariableExpressions[$identVariable] = $expr;
2300
        }
2301
2302 407
        return $expr;
2303
    }
2304
2305
    /**
2306
     * SimpleSelectExpression ::= (
2307
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2308
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2309
     * ) [["AS"] AliasResultVariable]
2310
     *
2311
     * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
2312
     */
2313 40
    public function SimpleSelectExpression()
2314
    {
2315 40
        $peek = $this->lexer->glimpse();
2316
2317 40
        switch ($this->lexer->lookahead['type']) {
2318 40
            case Lexer::T_IDENTIFIER:
2319
                switch (true) {
2320 17
                    case ($peek['type'] === Lexer::T_DOT):
2321 14
                        $expression = $this->StateFieldPathExpression();
2322
2323 14
                        return new AST\SimpleSelectExpression($expression);
2324
2325 3
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2326 2
                        $expression = $this->IdentificationVariable();
2327
2328 2
                        return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Documentation introduced by
$expression is of type string, but the function expects a object<Doctrine\ORM\Query\AST\Node>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2329
2330 1
                    case ($this->isFunction()):
2331
                        // SUM(u.id) + COUNT(u.id)
2332 1
                        if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) {
0 ignored issues
show
Bug introduced by
It seems like $this->peekBeyondClosingParenthesis() targeting Doctrine\ORM\Query\Parse...ondClosingParenthesis() can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2333
                            return new AST\SimpleSelectExpression($this->ScalarExpression());
0 ignored issues
show
Bug introduced by
It seems like $this->ScalarExpression() targeting Doctrine\ORM\Query\Parser::ScalarExpression() can also be of type null; however, Doctrine\ORM\Query\AST\S...pression::__construct() does only seem to accept object<Doctrine\ORM\Query\AST\Node>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2334
                        }
2335
                        // COUNT(u.id)
2336 1
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2337
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2338
                        }
2339
                        // IDENTITY(u)
2340 1
                        return new AST\SimpleSelectExpression($this->FunctionDeclaration());
0 ignored issues
show
Bug introduced by
It seems like $this->FunctionDeclaration() can be null; however, __construct() 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...
2341
2342
                    default:
2343
                        // Do nothing
2344
                }
2345
                break;
2346
2347 24
            case Lexer::T_OPEN_PARENTHESIS:
2348 3
                if ($peek['type'] !== Lexer::T_SELECT) {
2349
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2350 3
                    $expression = $this->SimpleArithmeticExpression();
2351
2352 3
                    return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->SimpleArithmeticExpression() on line 2350 can be null; however, Doctrine\ORM\Query\AST\S...pression::__construct() 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...
2353
                }
2354
2355
                // Subselect
2356
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2357
                $expression = $this->Subselect();
2358
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2359
2360
                return new AST\SimpleSelectExpression($expression);
2361
2362
            default:
2363
                // Do nothing
2364
        }
2365
2366 21
        $this->lexer->peek();
2367
2368 21
        $expression = $this->ScalarExpression();
2369 21
        $expr       = new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->ScalarExpression() on line 2368 can also be of type null; however, Doctrine\ORM\Query\AST\S...pression::__construct() does only seem to accept object<Doctrine\ORM\Query\AST\Node>, 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...
2370
2371 21
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2372 1
            $this->match(Lexer::T_AS);
2373
        }
2374
2375 21
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2376 2
            $token = $this->lexer->lookahead;
2377 2
            $resultVariable = $this->AliasResultVariable();
2378 2
            $expr->fieldIdentificationVariable = $resultVariable;
2379
2380
            // Include AliasResultVariable in query components.
2381 2
            $this->queryComponents[$resultVariable] = [
2382 2
                'resultvariable' => $expr,
2383 2
                'nestingLevel'   => $this->nestingLevel,
2384 2
                'token'          => $token,
2385
            ];
2386
        }
2387
2388 21
        return $expr;
2389
    }
2390
2391
    /**
2392
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2393
     *
2394
     * @return \Doctrine\ORM\Query\AST\ConditionalExpression
2395
     */
2396 250
    public function ConditionalExpression()
2397
    {
2398 250
        $conditionalTerms = [];
2399 250
        $conditionalTerms[] = $this->ConditionalTerm();
2400
2401 247
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2402 12
            $this->match(Lexer::T_OR);
2403
2404 12
            $conditionalTerms[] = $this->ConditionalTerm();
2405
        }
2406
2407
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2408
        // if only one AST\ConditionalTerm is defined
2409 247
        if (count($conditionalTerms) == 1) {
2410 241
            return $conditionalTerms[0];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $conditionalTerms[0]; (Doctrine\ORM\Query\AST\ConditionalTerm) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ConditionalExpression of type Doctrine\ORM\Query\AST\ConditionalExpression.

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...
2411
        }
2412
2413 12
        return new AST\ConditionalExpression($conditionalTerms);
2414
    }
2415
2416
    /**
2417
     * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2418
     *
2419
     * @return \Doctrine\ORM\Query\AST\ConditionalTerm
2420
     */
2421 250
    public function ConditionalTerm()
2422
    {
2423 250
        $conditionalFactors = [];
2424 250
        $conditionalFactors[] = $this->ConditionalFactor();
2425
2426 247
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2427 20
            $this->match(Lexer::T_AND);
2428
2429 20
            $conditionalFactors[] = $this->ConditionalFactor();
2430
        }
2431
2432
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2433
        // if only one AST\ConditionalFactor is defined
2434 247
        if (count($conditionalFactors) == 1) {
2435 238
            return $conditionalFactors[0];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $conditionalFactors[0]; (Doctrine\ORM\Query\AST\ConditionalFactor) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ConditionalTerm of type Doctrine\ORM\Query\AST\ConditionalTerm.

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...
2436
        }
2437
2438 20
        return new AST\ConditionalTerm($conditionalFactors);
2439
    }
2440
2441
    /**
2442
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2443
     *
2444
     * @return \Doctrine\ORM\Query\AST\ConditionalFactor
2445
     */
2446 250
    public function ConditionalFactor()
2447
    {
2448 250
        $not = false;
2449
2450 250
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2451 6
            $this->match(Lexer::T_NOT);
2452
2453 6
            $not = true;
2454
        }
2455
2456 250
        $conditionalPrimary = $this->ConditionalPrimary();
2457
2458
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2459
        // if only one AST\ConditionalPrimary is defined
2460 247
        if ( ! $not) {
2461 245
            return $conditionalPrimary;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $conditionalPrimary; (Doctrine\ORM\Query\AST\ConditionalPrimary) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ConditionalFactor of type Doctrine\ORM\Query\AST\ConditionalFactor.

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...
2462
        }
2463
2464 6
        $conditionalFactor = new AST\ConditionalFactor($conditionalPrimary);
2465 6
        $conditionalFactor->not = $not;
2466
2467 6
        return $conditionalFactor;
2468
    }
2469
2470
    /**
2471
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2472
     *
2473
     * @return \Doctrine\ORM\Query\AST\ConditionalPrimary
2474
     */
2475 250
    public function ConditionalPrimary()
2476
    {
2477 250
        $condPrimary = new AST\ConditionalPrimary;
2478
2479 250
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2480 243
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2481
2482 240
            return $condPrimary;
2483
        }
2484
2485
        // Peek beyond the matching closing parenthesis ')'
2486 21
        $peek = $this->peekBeyondClosingParenthesis();
2487
2488 21
        if (in_array($peek['value'], ["=",  "<", "<=", "<>", ">", ">=", "!="]) ||
2489 18
            in_array($peek['type'], [Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS]) ||
2490 21
            $this->isMathOperator($peek)) {
2491 13
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2492
2493 13
            return $condPrimary;
2494
        }
2495
2496 17
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2497 17
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2498 17
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2499
2500 17
        return $condPrimary;
2501
    }
2502
2503
    /**
2504
     * SimpleConditionalExpression ::=
2505
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2506
     *      InExpression | NullComparisonExpression | ExistsExpression |
2507
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2508
     *      InstanceOfExpression
2509
     */
2510 250
    public function SimpleConditionalExpression()
2511
    {
2512 250
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2513 7
            return $this->ExistsExpression();
2514
        }
2515
2516 250
        $token      = $this->lexer->lookahead;
2517 250
        $peek       = $this->lexer->glimpse();
2518 250
        $lookahead  = $token;
2519
2520 250
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2521
            $token = $this->lexer->glimpse();
2522
        }
2523
2524 250
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2525
            // Peek beyond the matching closing parenthesis.
2526 230
            $beyond = $this->lexer->peek();
2527
2528 230
            switch ($peek['value']) {
2529 230
                case '(':
2530
                    // Peeks beyond the matched closing parenthesis.
2531 27
                    $token = $this->peekBeyondClosingParenthesis(false);
2532
2533 27
                    if ($token['type'] === Lexer::T_NOT) {
2534 3
                        $token = $this->lexer->peek();
2535
                    }
2536
2537 27
                    if ($token['type'] === Lexer::T_IS) {
2538 2
                        $lookahead = $this->lexer->peek();
2539
                    }
2540 27
                    break;
2541
2542
                default:
2543
                    // Peek beyond the PathExpression or InputParameter.
2544 209
                    $token = $beyond;
2545
2546 209
                    while ($token['value'] === '.') {
2547 179
                        $this->lexer->peek();
2548
2549 179
                        $token = $this->lexer->peek();
2550
                    }
2551
2552
                    // Also peek beyond a NOT if there is one.
2553 209
                    if ($token['type'] === Lexer::T_NOT) {
2554 11
                        $token = $this->lexer->peek();
2555
                    }
2556
2557
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2558 209
                    $lookahead = $this->lexer->peek();
2559
            }
2560
2561
            // Also peek beyond a NOT if there is one.
2562 230
            if ($lookahead['type'] === Lexer::T_NOT) {
2563 5
                $lookahead = $this->lexer->peek();
2564
            }
2565
2566 230
            $this->lexer->resetPeek();
2567
        }
2568
2569 250
        if ($token['type'] === Lexer::T_BETWEEN) {
2570 8
            return $this->BetweenExpression();
2571
        }
2572
2573 244
        if ($token['type'] === Lexer::T_LIKE) {
2574 13
            return $this->LikeExpression();
2575
        }
2576
2577 232
        if ($token['type'] === Lexer::T_IN) {
2578 26
            return $this->InExpression();
2579
        }
2580
2581 212
        if ($token['type'] === Lexer::T_INSTANCE) {
2582 10
            return $this->InstanceOfExpression();
2583
        }
2584
2585 202
        if ($token['type'] === Lexer::T_MEMBER) {
2586 7
            return $this->CollectionMemberExpression();
2587
        }
2588
2589 195
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2590 12
            return $this->NullComparisonExpression();
2591
        }
2592
2593 186
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2594 3
            return $this->EmptyCollectionComparisonExpression();
2595
        }
2596
2597 183
        return $this->ComparisonExpression();
2598
    }
2599
2600
    /**
2601
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2602
     *
2603
     * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression
2604
     */
2605 3
    public function EmptyCollectionComparisonExpression()
2606
    {
2607 3
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2608 3
            $this->CollectionValuedPathExpression()
2609
        );
2610 3
        $this->match(Lexer::T_IS);
2611
2612 3
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2613 1
            $this->match(Lexer::T_NOT);
2614 1
            $emptyCollectionCompExpr->not = true;
2615
        }
2616
2617 3
        $this->match(Lexer::T_EMPTY);
2618
2619 3
        return $emptyCollectionCompExpr;
2620
    }
2621
2622
    /**
2623
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2624
     *
2625
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2626
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2627
     *
2628
     * @return \Doctrine\ORM\Query\AST\CollectionMemberExpression
2629
     */
2630 7
    public function CollectionMemberExpression()
2631
    {
2632 7
        $not        = false;
2633 7
        $entityExpr = $this->EntityExpression();
2634
2635 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2636
            $this->match(Lexer::T_NOT);
2637
2638
            $not = true;
2639
        }
2640
2641 7
        $this->match(Lexer::T_MEMBER);
2642
2643 7
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2644 7
            $this->match(Lexer::T_OF);
2645
        }
2646
2647 7
        $collMemberExpr = new AST\CollectionMemberExpression(
2648 7
            $entityExpr, $this->CollectionValuedPathExpression()
2649
        );
2650 7
        $collMemberExpr->not = $not;
2651
2652 7
        return $collMemberExpr;
2653
    }
2654
2655
    /**
2656
     * Literal ::= string | char | integer | float | boolean
2657
     *
2658
     * @return \Doctrine\ORM\Query\AST\Literal
2659
     */
2660 127
    public function Literal()
2661
    {
2662 127
        switch ($this->lexer->lookahead['type']) {
2663 127
            case Lexer::T_STRING:
2664 35
                $this->match(Lexer::T_STRING);
2665
2666 35
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2667 99
            case Lexer::T_INTEGER:
2668 5
            case Lexer::T_FLOAT:
2669 94
                $this->match(
2670 94
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2671
                );
2672
2673 94
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2674 5
            case Lexer::T_TRUE:
2675 2
            case Lexer::T_FALSE:
2676 5
                $this->match(
2677 5
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2678
                );
2679
2680 5
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2681
            default:
2682
                $this->syntaxError('Literal');
2683
        }
2684
    }
2685
2686
    /**
2687
     * InParameter ::= Literal | InputParameter
2688
     *
2689
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2690
     */
2691 18
    public function InParameter()
2692
    {
2693 18
        if ($this->lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) {
2694 8
            return $this->InputParameter();
2695
        }
2696
2697 10
        return $this->Literal();
2698
    }
2699
2700
    /**
2701
     * InputParameter ::= PositionalParameter | NamedParameter
2702
     *
2703
     * @return \Doctrine\ORM\Query\AST\InputParameter
2704
     */
2705 85
    public function InputParameter()
2706
    {
2707 85
        $this->match(Lexer::T_INPUT_PARAMETER);
2708
2709 85
        return new AST\InputParameter($this->lexer->token['value']);
2710
    }
2711
2712
    /**
2713
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2714
     *
2715
     * @return \Doctrine\ORM\Query\AST\ArithmeticExpression
2716
     */
2717 211
    public function ArithmeticExpression()
2718
    {
2719 211
        $expr = new AST\ArithmeticExpression;
2720
2721 211
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2722 17
            $peek = $this->lexer->glimpse();
2723
2724 17
            if ($peek['type'] === Lexer::T_SELECT) {
2725 5
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2726 5
                $expr->subselect = $this->Subselect();
2727 5
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2728
2729 5
                return $expr;
2730
            }
2731
        }
2732
2733 211
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2734
2735 211
        return $expr;
2736
    }
2737
2738
    /**
2739
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2740
     *
2741
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2742
     */
2743 269
    public function SimpleArithmeticExpression()
2744
    {
2745 269
        $terms = [];
2746 269
        $terms[] = $this->ArithmeticTerm();
2747
2748 269
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2749 17
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2750
2751 17
            $terms[] = $this->lexer->token['value'];
2752 17
            $terms[] = $this->ArithmeticTerm();
2753
        }
2754
2755
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2756
        // if only one AST\ArithmeticTerm is defined
2757 269
        if (count($terms) == 1) {
2758 267
            return $terms[0];
0 ignored issues
show
Bug Compatibility introduced by
The expression $terms[0]; of type Doctrine\ORM\Query\AST\ArithmeticTerm|null adds the type Doctrine\ORM\Query\AST\ArithmeticTerm to the return on line 2758 which is incompatible with the return type documented by Doctrine\ORM\Query\Parse...pleArithmeticExpression of type Doctrine\ORM\Query\AST\S...ithmeticExpression|null.
Loading history...
2759
        }
2760
2761 17
        return new AST\SimpleArithmeticExpression($terms);
2762
    }
2763
2764
    /**
2765
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2766
     *
2767
     * @return \Doctrine\ORM\Query\AST\ArithmeticTerm
2768
     */
2769 269
    public function ArithmeticTerm()
2770
    {
2771 269
        $factors = [];
2772 269
        $factors[] = $this->ArithmeticFactor();
2773
2774 269
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2775 27
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2776
2777 27
            $factors[] = $this->lexer->token['value'];
2778 27
            $factors[] = $this->ArithmeticFactor();
2779
        }
2780
2781
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2782
        // if only one AST\ArithmeticFactor is defined
2783 269
        if (count($factors) == 1) {
2784 265
            return $factors[0];
0 ignored issues
show
Bug Compatibility introduced by
The expression $factors[0]; of type Doctrine\ORM\Query\AST\ArithmeticFactor|null adds the type Doctrine\ORM\Query\AST\ArithmeticFactor to the return on line 2784 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticTerm of type Doctrine\ORM\Query\AST\ArithmeticTerm|null.
Loading history...
2785
        }
2786
2787 27
        return new AST\ArithmeticTerm($factors);
2788
    }
2789
2790
    /**
2791
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2792
     *
2793
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2794
     */
2795 269
    public function ArithmeticFactor()
2796
    {
2797 269
        $sign = null;
2798
2799 269
        if (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2800 2
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2801 2
            $sign = $isPlus;
2802
        }
2803
2804 269
        $primary = $this->ArithmeticPrimary();
2805
2806
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2807
        // if only one AST\ArithmeticPrimary is defined
2808 269
        if ($sign === null) {
2809 268
            return $primary;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $primary; (Doctrine\ORM\Query\AST\P...e\ORM\Query\AST\Literal) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticFactor of type Doctrine\ORM\Query\AST\ArithmeticFactor|null.

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...
2810
        }
2811
2812 2
        return new AST\ArithmeticFactor($primary, $sign);
2813
    }
2814
2815
    /**
2816
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2817
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2818
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2819
     *          | InputParameter | CaseExpression
2820
     */
2821 269
    public function ArithmeticPrimary()
2822
    {
2823 269
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2824 21
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2825
2826 21
            $expr = $this->SimpleArithmeticExpression();
2827
2828 21
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2829
2830 21
            return new AST\ParenthesisExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr defined by $this->SimpleArithmeticExpression() on line 2826 can be null; however, Doctrine\ORM\Query\AST\P...pression::__construct() 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...
2831
        }
2832
2833 269
        switch ($this->lexer->lookahead['type']) {
2834 269
            case Lexer::T_COALESCE:
2835 269
            case Lexer::T_NULLIF:
2836 269
            case Lexer::T_CASE:
2837 3
                return $this->CaseExpression();
2838
2839 269
            case Lexer::T_IDENTIFIER:
2840 244
                $peek = $this->lexer->glimpse();
2841
2842 244
                if ($peek['value'] == '(') {
2843 24
                    return $this->FunctionDeclaration();
2844
                }
2845
2846 227
                if ($peek['value'] == '.') {
2847 223
                    return $this->SingleValuedPathExpression();
2848
                }
2849
2850 31
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2851 10
                    return $this->ResultVariable();
2852
                }
2853
2854 23
                return $this->StateFieldPathExpression();
2855
2856 186
            case Lexer::T_INPUT_PARAMETER:
2857 70
                return $this->InputParameter();
2858
2859
            default:
2860 122
                $peek = $this->lexer->glimpse();
2861
2862 122
                if ($peek['value'] == '(') {
2863 11
                    if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2864 11
                        return $this->AggregateExpression();
2865
                    }
2866
2867
                    return $this->FunctionDeclaration();
2868
                }
2869
2870 119
                return $this->Literal();
2871
        }
2872
    }
2873
2874
    /**
2875
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2876
     *
2877
     * @return \Doctrine\ORM\Query\AST\Subselect |
2878
     *         string
2879
     */
2880 13
    public function StringExpression()
2881
    {
2882 13
        $peek = $this->lexer->glimpse();
2883
2884
        // Subselect
2885 13
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
2886
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2887
            $expr = $this->Subselect();
2888
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2889
2890
            return $expr;
2891
        }
2892
2893
        // ResultVariable (string)
2894 13
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2895 13
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2896 2
            return $this->ResultVariable();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->ResultVariable(); (string) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::StringExpression of type Doctrine\ORM\Query\AST\Subselect|null.

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...
2897
        }
2898
2899 11
        return $this->StringPrimary();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->StringPrimary(); (Doctrine\ORM\Query\AST\P...AST\AggregateExpression) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::StringExpression of type Doctrine\ORM\Query\AST\Subselect|null.

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...
2900
    }
2901
2902
    /**
2903
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2904
     */
2905 35
    public function StringPrimary()
2906
    {
2907 35
        $lookaheadType = $this->lexer->lookahead['type'];
2908
2909
        switch ($lookaheadType) {
2910 35
            case Lexer::T_IDENTIFIER:
2911 22
                $peek = $this->lexer->glimpse();
2912
2913 22
                if ($peek['value'] == '.') {
2914 22
                    return $this->StateFieldPathExpression();
2915
                }
2916
2917 5
                if ($peek['value'] == '(') {
2918
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2919 5
                    return $this->FunctionDeclaration();
2920
                }
2921
2922
                $this->syntaxError("'.' or '('");
2923
                break;
2924
2925 24
            case Lexer::T_STRING:
2926 24
                $this->match(Lexer::T_STRING);
2927
2928 24
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2929
2930 2
            case Lexer::T_INPUT_PARAMETER:
2931 2
                return $this->InputParameter();
2932
2933
            case Lexer::T_CASE:
2934
            case Lexer::T_COALESCE:
2935
            case Lexer::T_NULLIF:
2936
                return $this->CaseExpression();
2937
2938
            default:
2939
                if ($this->isAggregateFunction($lookaheadType)) {
2940
                    return $this->AggregateExpression();
2941
                }
2942
        }
2943
2944
        $this->syntaxError(
2945
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2946
        );
2947
    }
2948
2949
    /**
2950
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2951
     *
2952
     * @return \Doctrine\ORM\Query\AST\PathExpression |
2953
     *         \Doctrine\ORM\Query\AST\SimpleEntityExpression
2954
     */
2955 7
    public function EntityExpression()
2956
    {
2957 7
        $glimpse = $this->lexer->glimpse();
2958
2959 7
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2960 1
            return $this->SingleValuedAssociationPathExpression();
2961
        }
2962
2963 6
        return $this->SimpleEntityExpression();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->SimpleEntityExpression(); of type Doctrine\ORM\Query\AST\I...uery\AST\PathExpression adds the type Doctrine\ORM\Query\AST\InputParameter to the return on line 2963 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::EntityExpression of type Doctrine\ORM\Query\AST\PathExpression.
Loading history...
2964
    }
2965
2966
    /**
2967
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2968
     *
2969
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2970
     */
2971 6
    public function SimpleEntityExpression()
2972
    {
2973 6
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2974 5
            return $this->InputParameter();
2975
        }
2976
2977 1
        return $this->StateFieldPathExpression();
2978
    }
2979
2980
    /**
2981
     * AggregateExpression ::=
2982
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2983
     *
2984
     * @return \Doctrine\ORM\Query\AST\AggregateExpression
2985
     */
2986 60
    public function AggregateExpression()
2987
    {
2988 60
        $lookaheadType = $this->lexer->lookahead['type'];
2989 60
        $isDistinct = false;
2990
2991 60
        if ( ! in_array($lookaheadType, [Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM])) {
2992
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2993
        }
2994
2995 60
        $this->match($lookaheadType);
2996 60
        $functionName = $this->lexer->token['value'];
2997 60
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2998
2999 60
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
3000 2
            $this->match(Lexer::T_DISTINCT);
3001 2
            $isDistinct = true;
3002
        }
3003
3004 60
        $pathExp = $this->SimpleArithmeticExpression();
3005
3006 60
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3007
3008 60
        return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
0 ignored issues
show
Bug introduced by
It seems like $pathExp defined by $this->SimpleArithmeticExpression() on line 3004 can be null; however, Doctrine\ORM\Query\AST\A...pression::__construct() 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...
3009
    }
3010
3011
    /**
3012
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
3013
     *
3014
     * @return \Doctrine\ORM\Query\AST\QuantifiedExpression
3015
     */
3016 3
    public function QuantifiedExpression()
3017
    {
3018 3
        $lookaheadType = $this->lexer->lookahead['type'];
3019 3
        $value = $this->lexer->lookahead['value'];
3020
3021 3
        if ( ! in_array($lookaheadType, [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME])) {
3022
            $this->syntaxError('ALL, ANY or SOME');
3023
        }
3024
3025 3
        $this->match($lookaheadType);
3026 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3027
3028 3
        $qExpr = new AST\QuantifiedExpression($this->Subselect());
3029 3
        $qExpr->type = $value;
3030
3031 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3032
3033 3
        return $qExpr;
3034
    }
3035
3036
    /**
3037
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3038
     *
3039
     * @return \Doctrine\ORM\Query\AST\BetweenExpression
3040
     */
3041 8
    public function BetweenExpression()
3042
    {
3043 8
        $not = false;
3044 8
        $arithExpr1 = $this->ArithmeticExpression();
3045
3046 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3047 3
            $this->match(Lexer::T_NOT);
3048 3
            $not = true;
3049
        }
3050
3051 8
        $this->match(Lexer::T_BETWEEN);
3052 8
        $arithExpr2 = $this->ArithmeticExpression();
3053 8
        $this->match(Lexer::T_AND);
3054 8
        $arithExpr3 = $this->ArithmeticExpression();
3055
3056 8
        $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3057 8
        $betweenExpr->not = $not;
3058
3059 8
        return $betweenExpr;
3060
    }
3061
3062
    /**
3063
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3064
     *
3065
     * @return \Doctrine\ORM\Query\AST\ComparisonExpression
3066
     */
3067 183
    public function ComparisonExpression()
3068
    {
3069 183
        $this->lexer->glimpse();
3070
3071 183
        $leftExpr  = $this->ArithmeticExpression();
3072 183
        $operator  = $this->ComparisonOperator();
3073 183
        $rightExpr = ($this->isNextAllAnySome())
3074 3
            ? $this->QuantifiedExpression()
3075 183
            : $this->ArithmeticExpression();
3076
3077 181
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3078
    }
3079
3080
    /**
3081
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3082
     *
3083
     * @return \Doctrine\ORM\Query\AST\InExpression
3084
     */
3085 26
    public function InExpression()
3086
    {
3087 26
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3088
3089 26
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3090 6
            $this->match(Lexer::T_NOT);
3091 6
            $inExpression->not = true;
3092
        }
3093
3094 26
        $this->match(Lexer::T_IN);
3095 26
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3096
3097 26
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3098 8
            $inExpression->subselect = $this->Subselect();
3099
        } else {
3100 18
            $literals = [];
3101 18
            $literals[] = $this->InParameter();
3102
3103 18
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3104 14
                $this->match(Lexer::T_COMMA);
3105 14
                $literals[] = $this->InParameter();
3106
            }
3107
3108 18
            $inExpression->literals = $literals;
3109
        }
3110
3111 25
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3112
3113 25
        return $inExpression;
3114
    }
3115
3116
    /**
3117
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3118
     *
3119
     * @return \Doctrine\ORM\Query\AST\InstanceOfExpression
3120
     */
3121 10
    public function InstanceOfExpression()
3122
    {
3123 10
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3124
3125 10
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3126 1
            $this->match(Lexer::T_NOT);
3127 1
            $instanceOfExpression->not = true;
3128
        }
3129
3130 10
        $this->match(Lexer::T_INSTANCE);
3131 10
        $this->match(Lexer::T_OF);
3132
3133 10
        $exprValues = [];
3134
3135 10
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3136 1
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3137
3138 1
            $exprValues[] = $this->InstanceOfParameter();
3139
3140 1
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3141 1
                $this->match(Lexer::T_COMMA);
3142
3143 1
                $exprValues[] = $this->InstanceOfParameter();
3144
            }
3145
3146 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3147
3148 1
            $instanceOfExpression->value = $exprValues;
3149
3150 1
            return $instanceOfExpression;
3151
        }
3152
3153 9
        $exprValues[] = $this->InstanceOfParameter();
3154
3155 9
        $instanceOfExpression->value = $exprValues;
3156
3157 9
        return $instanceOfExpression;
3158
    }
3159
3160
    /**
3161
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3162
     *
3163
     * @return mixed
3164
     */
3165 10
    public function InstanceOfParameter()
3166
    {
3167 10
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3168 3
            $this->match(Lexer::T_INPUT_PARAMETER);
3169
3170 3
            return new AST\InputParameter($this->lexer->token['value']);
3171
        }
3172
3173 7
        $abstractSchemaName = $this->AbstractSchemaName();
3174
3175 7
        $this->validateAbstractSchemaName($abstractSchemaName);
3176
3177 7
        return $abstractSchemaName;
3178
    }
3179
3180
    /**
3181
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3182
     *
3183
     * @return \Doctrine\ORM\Query\AST\LikeExpression
3184
     */
3185 13
    public function LikeExpression()
3186
    {
3187 13
        $stringExpr = $this->StringExpression();
3188 13
        $not = false;
3189
3190 13
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3191 3
            $this->match(Lexer::T_NOT);
3192 3
            $not = true;
3193
        }
3194
3195 13
        $this->match(Lexer::T_LIKE);
3196
3197 13
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3198 3
            $this->match(Lexer::T_INPUT_PARAMETER);
3199 3
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3200
        } else {
3201 11
            $stringPattern = $this->StringPrimary();
3202
        }
3203
3204 13
        $escapeChar = null;
3205
3206 13
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3207 2
            $this->match(Lexer::T_ESCAPE);
3208 2
            $this->match(Lexer::T_STRING);
3209
3210 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3211
        }
3212
3213 13
        $likeExpr = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $stringPattern defined by $this->StringPrimary() on line 3201 can also be of type null or object<Doctrine\ORM\Quer...ST\AggregateExpression> or object<Doctrine\ORM\Query\AST\CoalesceExpression> or object<Doctrine\ORM\Quer...Functions\FunctionNode> or object<Doctrine\ORM\Quer...\GeneralCaseExpression> or object<Doctrine\ORM\Query\AST\Literal> or object<Doctrine\ORM\Query\AST\NullIfExpression> or object<Doctrine\ORM\Query\AST\PathExpression> or object<Doctrine\ORM\Quer...T\SimpleCaseExpression>; however, Doctrine\ORM\Query\AST\L...pression::__construct() does only seem to accept object<Doctrine\ORM\Query\AST\InputParameter>, 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...
Bug introduced by
It seems like $stringExpr defined by $this->StringExpression() on line 3187 can be null; however, Doctrine\ORM\Query\AST\L...pression::__construct() 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...
3214 13
        $likeExpr->not = $not;
3215
3216 13
        return $likeExpr;
3217
    }
3218
3219
    /**
3220
     * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
3221
     *
3222
     * @return \Doctrine\ORM\Query\AST\NullComparisonExpression
3223
     */
3224 12
    public function NullComparisonExpression()
3225
    {
3226
        switch (true) {
3227 12
            case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER):
3228
                $this->match(Lexer::T_INPUT_PARAMETER);
3229
3230
                $expr = new AST\InputParameter($this->lexer->token['value']);
3231
                break;
3232
3233 12
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3234 1
                $expr = $this->NullIfExpression();
3235 1
                break;
3236
3237 12
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3238 1
                $expr = $this->CoalesceExpression();
3239 1
                break;
3240
3241 12
            case $this->isAggregateFunction($this->lexer->lookahead['type']):
3242 1
                $expr = $this->AggregateExpression();
3243 1
                break;
3244
3245 12
            case $this->isFunction():
3246 1
                $expr = $this->FunctionDeclaration();
3247 1
                break;
3248
3249
            default:
3250
                // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
3251 11
                $glimpse = $this->lexer->glimpse();
3252
3253 11
                if ($glimpse['type'] === Lexer::T_DOT) {
3254 8
                    $expr = $this->SingleValuedPathExpression();
3255
3256
                    // Leave switch statement
3257 8
                    break;
3258
                }
3259
3260 3
                $lookaheadValue = $this->lexer->lookahead['value'];
3261
3262
                // Validate existing component
3263 3
                if ( ! isset($this->queryComponents[$lookaheadValue])) {
3264
                    $this->semanticalError('Cannot add having condition on undefined result variable.');
3265
                }
3266
3267
                // Validate SingleValuedPathExpression (ie.: "product")
3268 3
                if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {
3269
                    $expr = $this->SingleValuedPathExpression();
3270
                    break;
3271
                }
3272
3273
                // Validating ResultVariable
3274 3
                if ( ! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3275
                    $this->semanticalError('Cannot add having condition on a non result variable.');
3276
                }
3277
3278 3
                $expr = $this->ResultVariable();
3279 3
                break;
3280
        }
3281
3282 12
        $nullCompExpr = new AST\NullComparisonExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr can also be of type null or string; however, Doctrine\ORM\Query\AST\N...pression::__construct() does only seem to accept object<Doctrine\ORM\Query\AST\Node>, 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...
3283
3284 12
        $this->match(Lexer::T_IS);
3285
3286 12
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3287 4
            $this->match(Lexer::T_NOT);
3288
3289 4
            $nullCompExpr->not = true;
3290
        }
3291
3292 12
        $this->match(Lexer::T_NULL);
3293
3294 12
        return $nullCompExpr;
3295
    }
3296
3297
    /**
3298
     * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"
3299
     *
3300
     * @return \Doctrine\ORM\Query\AST\ExistsExpression
3301
     */
3302 7
    public function ExistsExpression()
3303
    {
3304 7
        $not = false;
3305
3306 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3307
            $this->match(Lexer::T_NOT);
3308
            $not = true;
3309
        }
3310
3311 7
        $this->match(Lexer::T_EXISTS);
3312 7
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3313
3314 7
        $existsExpression = new AST\ExistsExpression($this->Subselect());
3315 7
        $existsExpression->not = $not;
3316
3317 7
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3318
3319 7
        return $existsExpression;
3320
    }
3321
3322
    /**
3323
     * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!="
3324
     *
3325
     * @return string
3326
     */
3327 183
    public function ComparisonOperator()
3328
    {
3329 183
        switch ($this->lexer->lookahead['value']) {
3330 183
            case '=':
3331 138
                $this->match(Lexer::T_EQUALS);
3332
3333 138
                return '=';
3334
3335 55
            case '<':
3336 16
                $this->match(Lexer::T_LOWER_THAN);
3337 16
                $operator = '<';
3338
3339 16
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3340 5
                    $this->match(Lexer::T_EQUALS);
3341 5
                    $operator .= '=';
3342 11
                } else if ($this->lexer->isNextToken(Lexer::T_GREATER_THAN)) {
3343 3
                    $this->match(Lexer::T_GREATER_THAN);
3344 3
                    $operator .= '>';
3345
                }
3346
3347 16
                return $operator;
3348
3349 47
            case '>':
3350 41
                $this->match(Lexer::T_GREATER_THAN);
3351 41
                $operator = '>';
3352
3353 41
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3354 6
                    $this->match(Lexer::T_EQUALS);
3355 6
                    $operator .= '=';
3356
                }
3357
3358 41
                return $operator;
3359
3360 6
            case '!':
3361 6
                $this->match(Lexer::T_NEGATE);
3362 6
                $this->match(Lexer::T_EQUALS);
3363
3364 6
                return '<>';
3365
3366
            default:
3367
                $this->syntaxError('=, <, <=, <>, >, >=, !=');
3368
        }
3369
    }
3370
3371
    /**
3372
     * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
3373
     *
3374
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3375
     */
3376 38
    public function FunctionDeclaration()
3377
    {
3378 38
        $token = $this->lexer->lookahead;
3379 38
        $funcName = strtolower($token['value']);
3380
3381
        // Check for built-in functions first!
3382
        switch (true) {
3383 38
            case (isset(self::$_STRING_FUNCTIONS[$funcName])):
3384 20
                return $this->FunctionsReturningStrings();
3385
3386 19
            case (isset(self::$_NUMERIC_FUNCTIONS[$funcName])):
3387 14
                return $this->FunctionsReturningNumerics();
3388
3389 5
            case (isset(self::$_DATETIME_FUNCTIONS[$funcName])):
3390 3
                return $this->FunctionsReturningDatetime();
3391
3392
            default:
3393 2
                return $this->CustomFunctionDeclaration();
3394
        }
3395
    }
3396
3397
    /**
3398
     * Helper function for FunctionDeclaration grammar rule.
3399
     *
3400
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3401
     */
3402 2
    private function CustomFunctionDeclaration()
3403
    {
3404 2
        $token = $this->lexer->lookahead;
3405 2
        $funcName = strtolower($token['value']);
3406
3407
        // Check for custom functions afterwards
3408 2
        $config = $this->em->getConfiguration();
3409
3410
        switch (true) {
3411 2
            case ($config->getCustomStringFunction($funcName) !== null):
3412 1
                return $this->CustomFunctionsReturningStrings();
3413
3414 1
            case ($config->getCustomNumericFunction($funcName) !== null):
3415 1
                return $this->CustomFunctionsReturningNumerics();
3416
3417
            case ($config->getCustomDatetimeFunction($funcName) !== null):
3418
                return $this->CustomFunctionsReturningDatetime();
3419
3420
            default:
3421
                $this->syntaxError('known function', $token);
3422
        }
3423
    }
3424
3425
    /**
3426
     * FunctionsReturningNumerics ::=
3427
     *      "LENGTH" "(" StringPrimary ")" |
3428
     *      "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" |
3429
     *      "ABS" "(" SimpleArithmeticExpression ")" |
3430
     *      "SQRT" "(" SimpleArithmeticExpression ")" |
3431
     *      "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3432
     *      "SIZE" "(" CollectionValuedPathExpression ")" |
3433
     *      "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3434
     *      "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3435
     *      "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
3436
     *
3437
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3438
     */
3439 14
    public function FunctionsReturningNumerics()
3440
    {
3441 14
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3442 14
        $funcClass     = self::$_NUMERIC_FUNCTIONS[$funcNameLower];
3443
3444 14
        $function = new $funcClass($funcNameLower);
3445 14
        $function->parse($this);
3446
3447 14
        return $function;
3448
    }
3449
3450
    /**
3451
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3452
     */
3453 1
    public function CustomFunctionsReturningNumerics()
3454
    {
3455
        // getCustomNumericFunction is case-insensitive
3456 1
        $functionName  = strtolower($this->lexer->lookahead['value']);
3457 1
        $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName);
3458
3459 1
        $function = is_string($functionClass)
3460 1
            ? new $functionClass($functionName)
3461 1
            : call_user_func($functionClass, $functionName);
3462
3463 1
        $function->parse($this);
3464
3465 1
        return $function;
3466
    }
3467
3468
    /**
3469
     * FunctionsReturningDateTime ::=
3470
     *     "CURRENT_DATE" |
3471
     *     "CURRENT_TIME" |
3472
     *     "CURRENT_TIMESTAMP" |
3473
     *     "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" |
3474
     *     "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"
3475
     *
3476
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3477
     */
3478 3
    public function FunctionsReturningDatetime()
3479
    {
3480 3
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3481 3
        $funcClass     = self::$_DATETIME_FUNCTIONS[$funcNameLower];
3482
3483 3
        $function = new $funcClass($funcNameLower);
3484 3
        $function->parse($this);
3485
3486 3
        return $function;
3487
    }
3488
3489
    /**
3490
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3491
     */
3492
    public function CustomFunctionsReturningDatetime()
3493
    {
3494
        // getCustomDatetimeFunction is case-insensitive
3495
        $functionName  = $this->lexer->lookahead['value'];
3496
        $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName);
3497
3498
        $function = is_string($functionClass)
3499
            ? new $functionClass($functionName)
3500
            : call_user_func($functionClass, $functionName);
3501
3502
        $function->parse($this);
3503
3504
        return $function;
3505
    }
3506
3507
    /**
3508
     * FunctionsReturningStrings ::=
3509
     *   "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" |
3510
     *   "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3511
     *   "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" |
3512
     *   "LOWER" "(" StringPrimary ")" |
3513
     *   "UPPER" "(" StringPrimary ")" |
3514
     *   "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"
3515
     *
3516
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3517
     */
3518 20
    public function FunctionsReturningStrings()
3519
    {
3520 20
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3521 20
        $funcClass     = self::$_STRING_FUNCTIONS[$funcNameLower];
3522
3523 20
        $function = new $funcClass($funcNameLower);
3524 20
        $function->parse($this);
3525
3526 20
        return $function;
3527
    }
3528
3529
    /**
3530
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3531
     */
3532 1
    public function CustomFunctionsReturningStrings()
3533
    {
3534
        // getCustomStringFunction is case-insensitive
3535 1
        $functionName  = $this->lexer->lookahead['value'];
3536 1
        $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName);
3537
3538 1
        $function = is_string($functionClass)
3539 1
            ? new $functionClass($functionName)
3540 1
            : call_user_func($functionClass, $functionName);
3541
3542 1
        $function->parse($this);
3543
3544 1
        return $function;
3545
    }
3546
}
3547