Completed
Pull Request — master (#6417)
by Luís
19:07
created

Parser::StringPrimary()   D

Complexity

Conditions 10
Paths 10

Size

Total Lines 43
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 19.626

Importance

Changes 0
Metric Value
dl 0
loc 43
c 0
b 0
f 0
ccs 13
cts 24
cp 0.5417
rs 4.8196
cc 10
eloc 25
nc 10
nop 0
crap 19.626

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 498
    public function __construct(Query $query)
197
    {
198 498
        $this->query        = $query;
199 498
        $this->em           = $query->getEntityManager();
200 498
        $this->lexer        = new Lexer($query->getDql());
201 498
        $this->parserResult = new ParserResult();
202 498
    }
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 498
    public function getAST()
267
    {
268
        // Parse & build AST
269 498
        $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 459
        $this->processDeferredIdentificationVariables();
274
275 457
        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 7
            $this->processDeferredPartialObjectExpressions();
277
        }
278
279 455
        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 351
            $this->processDeferredPathExpressions($AST);
281
        }
282
283 452
        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 452
        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 7
            $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 448
        $this->processRootEntityAliasSelected();
292
293
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
294 447
        $this->fixIdentificationVariableOrder($AST);
295
296 447
        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 509
    public function match($token)
312
    {
313 509
        $lookaheadType = $this->lexer->lookahead['type'];
314
315
        // Short-circuit on first condition, usually types match
316 509
        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 502
        $this->lexer->moveNext();
334 502
    }
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 498
    public function parse()
364
    {
365 498
        $AST = $this->getAST();
366
367 447
        if (($customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) !== false) {
368 36
            $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 447
        if (($customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER)) !== false) {
372 38
            $this->customOutputWalker = $customOutputWalker;
373
        }
374
375
        // Run any custom tree walkers over the AST
376 447
        if ($this->customTreeWalkers) {
377 35
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
378
379 35
            foreach ($this->customTreeWalkers as $walker) {
380 35
                $treeWalkerChain->addTreeWalker($walker);
381
            }
382
383
            switch (true) {
384 35
                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 35
                    $treeWalkerChain->walkSelectStatement($AST);
395
            }
396
397 30
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
398
        }
399
400 442
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
401 442
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
402
403
        // Assign an SQL executor to the parser result
404 442
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
405
406 433
        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 447
    private function fixIdentificationVariableOrder($AST)
421
    {
422 447
        if (count($this->identVariableExpressions) <= 1) {
423 372
            return;
424
        }
425
426 75
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
427 75
            if ( ! isset($this->identVariableExpressions[$dqlAlias])) {
428 7
                continue;
429
            }
430
431 75
            $expr = $this->identVariableExpressions[$dqlAlias];
432 75
            $key  = array_search($expr, $AST->selectClause->selectExpressions);
433
434 75
            unset($AST->selectClause->selectExpressions[$key]);
435
436 75
            $AST->selectClause->selectExpressions[] = $expr;
437
        }
438 75
    }
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 33
    public function semanticalError($message = '', $token = null)
476
    {
477 33
        if ($token === null) {
478 2
            $token = $this->lexer->lookahead;
479
        }
480
481
        // Minimum exposed chars ahead of token
482 33
        $distance = 12;
483
484
        // Find a position of a final word to display in error string
485 33
        $dql    = $this->query->getDql();
486 33
        $length = strlen($dql);
487 33
        $pos    = $token['position'] + $distance;
488 33
        $pos    = strpos($dql, ' ', ($length > $pos) ? $pos : $length);
489 33
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
490
491 33
        $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1';
492 33
        $tokenStr = substr($dql, $token['position'], $length);
493
494
        // Building informative message
495 33
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
496
497 33
        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 112
    private function peekBeyondClosingParenthesis($resetPeek = true)
508
    {
509 112
        $token = $this->lexer->peek();
510 112
        $numUnmatched = 1;
511
512 112
        while ($numUnmatched > 0 && $token !== null) {
513 111
            switch ($token['type']) {
514 111
                case Lexer::T_OPEN_PARENTHESIS:
515 23
                    ++$numUnmatched;
516 23
                    break;
517
518 111
                case Lexer::T_CLOSE_PARENTHESIS:
519 111
                    --$numUnmatched;
520 111
                    break;
521
522
                default:
523
                    // Do nothing
524
            }
525
526 111
            $token = $this->lexer->peek();
527
        }
528
529 112
        if ($resetPeek) {
530 92
            $this->lexer->resetPeek();
531
        }
532
533 112
        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 186
    private function isMathOperator($token)
544
    {
545 186
        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 223
    private function isFunction()
554
    {
555 223
        $lookaheadType = $this->lexer->lookahead['type'];
556 223
        $peek          = $this->lexer->peek();
557
558 223
        $this->lexer->resetPeek();
559
560 223
        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 83
    private function isAggregateFunction($tokenType)
571
    {
572 83
        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 201
    private function isNextAllAnySome()
581
    {
582 201
        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 459
    private function processDeferredIdentificationVariables()
592
    {
593 459
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
594 452
            $identVariable = $deferredItem['expression'];
595
596
            // Check if IdentificationVariable exists in queryComponents
597 452
            if ( ! isset($this->queryComponents[$identVariable])) {
598 1
                $this->semanticalError(
599 1
                    "'$identVariable' is not defined.", $deferredItem['token']
600
                );
601
            }
602
603 452
            $qComp = $this->queryComponents[$identVariable];
604
605
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
606 452
            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 452
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
614 1
                $this->semanticalError(
615 452
                    "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token']
616
                );
617
            }
618
        }
619 457
    }
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 7
    private function processDeferredNewObjectExpressions($AST)
629
    {
630 7
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
631 7
            $expression     = $deferredItem['expression'];
632 7
            $token          = $deferredItem['token'];
633 7
            $className      = $expression->className;
634 7
            $args           = $expression->args;
635 7
            $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 7
                ? $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName
637 7
                : null;
638
639
            // If the namespace is not given then assumes the first FROM entity namespace
640 7
            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 7
            if ( ! class_exists($className)) {
651 1
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
652
            }
653
654 6
            $class = new \ReflectionClass($className);
655
656 6
            if ( ! $class->isInstantiable()) {
657 1
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
658
            }
659
660 5
            if ($class->getConstructor() === null) {
661 1
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
662
            }
663
664 4
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
665 4
                $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 7
    private function processDeferredPartialObjectExpressions()
677
    {
678 7
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
679 7
            $expr = $deferredItem['expression'];
680 7
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
681
682 7
            foreach ($expr->partialFieldSet as $field) {
683 7
                if (isset($class->fieldMappings[$field])) {
684 7
                    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 6
            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 6
                    $deferredItem['token']
702
                );
703
            }
704
        }
705 5
    }
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 351
    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 351
        foreach ($this->deferredPathExpressions as $deferredItem) {
759 351
            $pathExpression = $deferredItem['expression'];
760
761 351
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
762 351
            $class = $qComp['metadata'];
763
764 351
            if (($field = $pathExpression->field) === null) {
765 23
                $field = $pathExpression->field = $class->identifier[0];
766
            }
767
768
            // Check if field or association exists
769 351
            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 350
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
777
778 350
            if (isset($class->associationMappings[$field])) {
779 70
                $assoc = $class->associationMappings[$field];
780
781 70
                $fieldType = ($assoc['type'] & ClassMetadata::TO_ONE)
782 50
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
783 70
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
784
            }
785
786
            // Validate if PathExpression is one of the expected types
787 350
            $expectedType = $pathExpression->expectedType;
788
789 350
            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 348
            $pathExpression->type = $fieldType;
819
        }
820 348
    }
821
822
    /**
823
     * @return void
824
     */
825 448
    private function processRootEntityAliasSelected()
826
    {
827 448
        if ( ! count($this->identVariableExpressions)) {
828 163
            return;
829
        }
830
831 290
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
832 290
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
833 290
                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 498
    public function QueryLanguage()
848
    {
849 498
        $this->lexer->moveNext();
850
851 498
        switch ($this->lexer->lookahead['type']) {
852 498
            case Lexer::T_SELECT:
853 441
                $statement = $this->SelectStatement();
854 406
                break;
855
856 58
            case Lexer::T_UPDATE:
857 24
                $statement = $this->UpdateStatement();
858 24
                break;
859
860 34
            case Lexer::T_DELETE:
861 33
                $statement = $this->DeleteStatement();
862 32
                break;
863
864
            default:
865 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
866
                break;
867
        }
868
869
        // Check for end of string
870 462
        if ($this->lexer->lookahead !== null) {
871 3
            $this->syntaxError('end of string');
872
        }
873
874 459
        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 441
    public function SelectStatement()
883
    {
884 441
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
885
886 410
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
887 407
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
888 406
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
889 406
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
890
891 406
        return $selectStatement;
892
    }
893
894
    /**
895
     * UpdateStatement ::= UpdateClause [WhereClause]
896
     *
897
     * @return \Doctrine\ORM\Query\AST\UpdateStatement
898
     */
899 24
    public function UpdateStatement()
900
    {
901 24
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
902
903 24
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
904
905 24
        return $updateStatement;
906
    }
907
908
    /**
909
     * DeleteStatement ::= DeleteClause [WhereClause]
910
     *
911
     * @return \Doctrine\ORM\Query\AST\DeleteStatement
912
     */
913 33
    public function DeleteStatement()
914
    {
915 33
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
916
917 32
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
918
919 32
        return $deleteStatement;
920
    }
921
922
    /**
923
     * IdentificationVariable ::= identifier
924
     *
925
     * @return string
926
     */
927 480
    public function IdentificationVariable()
928
    {
929 480
        $this->match(Lexer::T_IDENTIFIER);
930
931 480
        $identVariable = $this->lexer->token['value'];
932
933 480
        $this->deferredIdentificationVariables[] = [
934 480
            'expression'   => $identVariable,
935 480
            'nestingLevel' => $this->nestingLevel,
936 480
            'token'        => $this->lexer->token,
937
        ];
938
939 480
        return $identVariable;
940
    }
941
942
    /**
943
     * AliasIdentificationVariable = identifier
944
     *
945
     * @return string
946
     */
947 469
    public function AliasIdentificationVariable()
948
    {
949 469
        $this->match(Lexer::T_IDENTIFIER);
950
951 469
        $aliasIdentVariable = $this->lexer->token['value'];
952 469
        $exists = isset($this->queryComponents[$aliasIdentVariable]);
953
954 469
        if ($exists) {
955 2
            $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->lexer->token);
956
        }
957
958 469
        return $aliasIdentVariable;
959
    }
960
961
    /**
962
     * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier
963
     *
964
     * @return string
965
     */
966 489
    public function AbstractSchemaName()
967
    {
968 489
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
969 474
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
970
971 474
            $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 489
        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 484
    private function validateAbstractSchemaName($schemaName)
995
    {
996 484
        if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {
997 16
            $this->semanticalError("Class '$schemaName' is not defined.", $this->lexer->token);
998
        }
999 469
    }
1000
1001
    /**
1002
     * AliasResultVariable ::= identifier
1003
     *
1004
     * @return string
1005
     */
1006 76
    public function AliasResultVariable()
1007
    {
1008 76
        $this->match(Lexer::T_IDENTIFIER);
1009
1010 72
        $resultVariable = $this->lexer->token['value'];
1011 72
        $exists = isset($this->queryComponents[$resultVariable]);
1012
1013 72
        if ($exists) {
1014 2
            $this->semanticalError("'$resultVariable' is already defined.", $this->lexer->token);
1015
        }
1016
1017 72
        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 111
    public function JoinAssociationPathExpression()
1047
    {
1048 111
        $identVariable = $this->IdentificationVariable();
1049
1050 111
        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 111
        $this->match(Lexer::T_DOT);
1057 111
        $this->match(Lexer::T_IDENTIFIER);
1058
1059 111
        $field = $this->lexer->token['value'];
1060
1061
        // Validate association field
1062 111
        $qComp = $this->queryComponents[$identVariable];
1063 111
        $class = $qComp['metadata'];
1064
1065 111
        if ( ! $class->hasAssociation($field)) {
1066
            $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
1067
        }
1068
1069 111
        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 361
    public function PathExpression($expectedTypes)
1083
    {
1084 361
        $identVariable = $this->IdentificationVariable();
1085 361
        $field = null;
1086
1087 361
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1088 359
            $this->match(Lexer::T_DOT);
1089 359
            $this->match(Lexer::T_IDENTIFIER);
1090
1091 359
            $field = $this->lexer->token['value'];
1092
1093 359
            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 361
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1102
1103
        // Defer PathExpression validation if requested to be deferred
1104 361
        $this->deferredPathExpressions[] = [
1105 361
            'expression'   => $pathExpr,
1106 361
            'nestingLevel' => $this->nestingLevel,
1107 361
            'token'        => $this->lexer->token,
1108
        ];
1109
1110 361
        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 280
    public function SingleValuedPathExpression()
1132
    {
1133 280
        return $this->PathExpression(
1134 280
            AST\PathExpression::TYPE_STATE_FIELD |
1135 280
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1136
        );
1137
    }
1138
1139
    /**
1140
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1141
     *
1142
     * @return \Doctrine\ORM\Query\AST\PathExpression
1143
     */
1144 137
    public function StateFieldPathExpression()
1145
    {
1146 137
        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 20
    public function CollectionValuedPathExpression()
1165
    {
1166 20
        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 441
    public function SelectClause()
1175
    {
1176 441
        $isDistinct = false;
1177 441
        $this->match(Lexer::T_SELECT);
1178
1179
        // Check for DISTINCT
1180 441
        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 441
        $selectExpressions = [];
1188 441
        $selectExpressions[] = $this->SelectExpression();
1189
1190 433
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1191 136
            $this->match(Lexer::T_COMMA);
1192
1193 136
            $selectExpressions[] = $this->SelectExpression();
1194
        }
1195
1196 432
        return new AST\SelectClause($selectExpressions, $isDistinct);
1197
    }
1198
1199
    /**
1200
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1201
     *
1202
     * @return \Doctrine\ORM\Query\AST\SimpleSelectClause
1203
     */
1204 41
    public function SimpleSelectClause()
1205
    {
1206 41
        $isDistinct = false;
1207 41
        $this->match(Lexer::T_SELECT);
1208
1209 41
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1210
            $this->match(Lexer::T_DISTINCT);
1211
1212
            $isDistinct = true;
1213
        }
1214
1215 41
        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 24
    public function UpdateClause()
1224
    {
1225 24
        $this->match(Lexer::T_UPDATE);
1226
1227 24
        $token = $this->lexer->lookahead;
1228 24
        $abstractSchemaName = $this->AbstractSchemaName();
1229
1230 24
        $this->validateAbstractSchemaName($abstractSchemaName);
1231
1232 24
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1233
            $this->match(Lexer::T_AS);
1234
        }
1235
1236 24
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1237
1238 24
        $class = $this->em->getClassMetadata($abstractSchemaName);
1239
1240
        // Building queryComponent
1241
        $queryComponent = [
1242 24
            'metadata'     => $class,
1243
            'parent'       => null,
1244
            'relation'     => null,
1245
            'map'          => null,
1246 24
            'nestingLevel' => $this->nestingLevel,
1247 24
            'token'        => $token,
1248
        ];
1249
1250 24
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1251
1252 24
        $this->match(Lexer::T_SET);
1253
1254 24
        $updateItems = [];
1255 24
        $updateItems[] = $this->UpdateItem();
1256
1257 24
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1258 2
            $this->match(Lexer::T_COMMA);
1259
1260 2
            $updateItems[] = $this->UpdateItem();
1261
        }
1262
1263 24
        $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1264 24
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1265
1266 24
        return $updateClause;
1267
    }
1268
1269
    /**
1270
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1271
     *
1272
     * @return \Doctrine\ORM\Query\AST\DeleteClause
1273
     */
1274 33
    public function DeleteClause()
1275
    {
1276 33
        $this->match(Lexer::T_DELETE);
1277
1278 33
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1279 6
            $this->match(Lexer::T_FROM);
1280
        }
1281
1282 33
        $token = $this->lexer->lookahead;
1283 33
        $abstractSchemaName = $this->AbstractSchemaName();
1284
1285 33
        $this->validateAbstractSchemaName($abstractSchemaName);
1286
1287 33
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1288
1289 33
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1290
            $this->match(Lexer::T_AS);
1291
        }
1292
1293 33
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1294
1295 32
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1296 32
        $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1297
1298
        // Building queryComponent
1299
        $queryComponent = [
1300 32
            'metadata'     => $class,
1301
            'parent'       => null,
1302
            'relation'     => null,
1303
            'map'          => null,
1304 32
            'nestingLevel' => $this->nestingLevel,
1305 32
            'token'        => $token,
1306
        ];
1307
1308 32
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1309
1310 32
        return $deleteClause;
1311
    }
1312
1313
    /**
1314
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1315
     *
1316
     * @return \Doctrine\ORM\Query\AST\FromClause
1317
     */
1318 432
    public function FromClause()
1319
    {
1320 432
        $this->match(Lexer::T_FROM);
1321
1322 427
        $identificationVariableDeclarations = [];
1323 427
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1324
1325 410
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1326 6
            $this->match(Lexer::T_COMMA);
1327
1328 6
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1329
        }
1330
1331 410
        return new AST\FromClause($identificationVariableDeclarations);
1332
    }
1333
1334
    /**
1335
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1336
     *
1337
     * @return \Doctrine\ORM\Query\AST\SubselectFromClause
1338
     */
1339 41
    public function SubselectFromClause()
1340
    {
1341 41
        $this->match(Lexer::T_FROM);
1342
1343 41
        $identificationVariables = [];
1344 41
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1345
1346 40
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1347
            $this->match(Lexer::T_COMMA);
1348
1349
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1350
        }
1351
1352 40
        return new AST\SubselectFromClause($identificationVariables);
1353
    }
1354
1355
    /**
1356
     * WhereClause ::= "WHERE" ConditionalExpression
1357
     *
1358
     * @return \Doctrine\ORM\Query\AST\WhereClause
1359
     */
1360 234
    public function WhereClause()
1361
    {
1362 234
        $this->match(Lexer::T_WHERE);
1363
1364 234
        return new AST\WhereClause($this->ConditionalExpression());
1365
    }
1366
1367
    /**
1368
     * HavingClause ::= "HAVING" ConditionalExpression
1369
     *
1370
     * @return \Doctrine\ORM\Query\AST\HavingClause
1371
     */
1372 18
    public function HavingClause()
1373
    {
1374 18
        $this->match(Lexer::T_HAVING);
1375
1376 18
        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 22
    public function GroupByClause()
1385
    {
1386 22
        $this->match(Lexer::T_GROUP);
1387 22
        $this->match(Lexer::T_BY);
1388
1389 22
        $groupByItems = [$this->GroupByItem()];
1390
1391 21
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1392 2
            $this->match(Lexer::T_COMMA);
1393
1394 2
            $groupByItems[] = $this->GroupByItem();
1395
        }
1396
1397 21
        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 5
            $this->match(Lexer::T_COMMA);
1415
1416 5
            $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 41
    public function Subselect()
1428
    {
1429
        // Increase query nesting level
1430 41
        $this->nestingLevel++;
1431
1432 41
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1433
1434 40
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1435 40
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1436 40
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1437 40
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1438
1439
        // Decrease query nesting level
1440 40
        $this->nestingLevel--;
1441
1442 40
        return $subselect;
1443
    }
1444
1445
    /**
1446
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1447
     *
1448
     * @return \Doctrine\ORM\Query\AST\UpdateItem
1449
     */
1450 24
    public function UpdateItem()
1451
    {
1452 24
        $pathExpr = $this->SingleValuedPathExpression();
1453
1454 24
        $this->match(Lexer::T_EQUALS);
1455
1456 24
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1457
1458 24
        return $updateItem;
1459
    }
1460
1461
    /**
1462
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1463
     *
1464
     * @return string | \Doctrine\ORM\Query\AST\PathExpression
1465
     */
1466 22
    public function GroupByItem()
1467
    {
1468
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1469 22
        $glimpse = $this->lexer->glimpse();
1470
1471 22
        if ($glimpse['type'] === Lexer::T_DOT) {
1472 9
            return $this->SingleValuedPathExpression();
1473
        }
1474
1475
        // Still need to decide between IdentificationVariable or ResultVariable
1476 13
        $lookaheadValue = $this->lexer->lookahead['value'];
1477
1478 13
        if ( ! isset($this->queryComponents[$lookaheadValue])) {
1479 1
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1480
        }
1481
1482 12
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1483 10
            ? $this->IdentificationVariable()
1484 12
            : $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 7
                $expr = $this->SimpleArithmeticExpression();
1513 7
                break;
1514
1515 42
            case ($glimpse['type'] === Lexer::T_DOT):
1516 33
                $expr = $this->SingleValuedPathExpression();
1517 33
                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 23
                $this->match(Lexer::T_DESC);
1534 23
                $type = 'DESC';
1535 23
                break;
1536
1537 29
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1538 9
                $this->match(Lexer::T_ASC);
1539 9
                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 24
    public function NewValue()
1564
    {
1565 24
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1566 1
            $this->match(Lexer::T_NULL);
1567
1568 1
            return null;
1569
        }
1570
1571 23
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1572 14
            $this->match(Lexer::T_INPUT_PARAMETER);
1573
1574 14
            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 9
        return $this->ArithmeticExpression();
1578
    }
1579
1580
    /**
1581
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1582
     *
1583
     * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1584
     */
1585 429
    public function IdentificationVariableDeclaration()
1586
    {
1587 429
        $joins                    = [];
1588 429
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1589 414
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1590 2
            ? $this->IndexBy()
1591 414
            : null;
1592
1593 414
        $rangeVariableDeclaration->isRoot = true;
1594
1595
        while (
1596 414
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1597 414
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1598 414
            $this->lexer->isNextToken(Lexer::T_JOIN)
1599
        ) {
1600 130
            $joins[] = $this->Join();
1601
        }
1602
1603 412
        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 41
    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 41
        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 130
    public function Join()
1675
    {
1676
        // Check Join type
1677 130
        $joinType = AST\Join::JOIN_TYPE_INNER;
1678
1679
        switch (true) {
1680 130
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1681 35
                $this->match(Lexer::T_LEFT);
1682
1683 35
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1684
1685
                // Possible LEFT OUTER join
1686 35
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1687
                    $this->match(Lexer::T_OUTER);
1688
1689
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1690
                }
1691 35
                break;
1692
1693 99
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1694 9
                $this->match(Lexer::T_INNER);
1695 9
                break;
1696
1697
            default:
1698
                // Do nothing
1699
        }
1700
1701 130
        $this->match(Lexer::T_JOIN);
1702
1703 130
        $next            = $this->lexer->glimpse();
1704 130
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1705 128
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1706 128
        $join            = new AST\Join($joinType, $joinDeclaration);
1707
1708
        // Describe non-root join declaration
1709 128
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1710 19
            $joinDeclaration->isRoot = false;
1711
        }
1712
1713
        // Check for ad-hoc Join conditions
1714 128
        if ($adhocConditions) {
1715 20
            $this->match(Lexer::T_WITH);
1716
1717 20
            $join->conditionalExpression = $this->ConditionalExpression();
1718
        }
1719
1720 128
        return $join;
1721
    }
1722
1723
    /**
1724
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1725
     *
1726
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1727
     */
1728 429
    public function RangeVariableDeclaration()
1729
    {
1730 429
        $abstractSchemaName = $this->AbstractSchemaName();
1731
1732 429
        $this->validateAbstractSchemaName($abstractSchemaName);
1733
1734 414
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1735
            $this->match(Lexer::T_AS);
1736
        }
1737
1738 414
        $token = $this->lexer->lookahead;
1739 414
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1740 414
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1741
1742
        // Building queryComponent
1743
        $queryComponent = [
1744 414
            'metadata'     => $classMetadata,
1745
            'parent'       => null,
1746
            'relation'     => null,
1747
            'map'          => null,
1748 414
            'nestingLevel' => $this->nestingLevel,
1749 414
            'token'        => $token
1750
        ];
1751
1752 414
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1753
1754 414
        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 111
    public function JoinAssociationDeclaration()
1763
    {
1764 111
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1765
1766 111
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1767
            $this->match(Lexer::T_AS);
1768
        }
1769
1770 111
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1771 109
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1772
1773 109
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1774 109
        $field                  = $joinAssociationPathExpression->associationField;
1775
1776 109
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1777 109
        $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1778
1779
        // Building queryComponent
1780
        $joinQueryComponent = [
1781 109
            'metadata'     => $targetClass,
1782 109
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1783 109
            'relation'     => $class->getAssociationMapping($field),
1784
            'map'          => null,
1785 109
            'nestingLevel' => $this->nestingLevel,
1786 109
            'token'        => $this->lexer->lookahead
1787
        ];
1788
1789 109
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1790
1791 109
        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 7
    public function PartialObjectExpression()
1801
    {
1802 7
        $this->match(Lexer::T_PARTIAL);
1803
1804 7
        $partialFieldSet = [];
1805
1806 7
        $identificationVariable = $this->IdentificationVariable();
1807
1808 7
        $this->match(Lexer::T_DOT);
1809 7
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1810 7
        $this->match(Lexer::T_IDENTIFIER);
1811
1812 7
        $field = $this->lexer->token['value'];
1813
1814
        // First field in partial expression might be embeddable property
1815 7
        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 7
        $partialFieldSet[] = $field;
1822
1823 7
        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 7
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1839
1840 7
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1841
1842
        // Defer PartialObjectExpression validation
1843 7
        $this->deferredPartialObjectExpressions[] = [
1844 7
            'expression'   => $partialObjectExpression,
1845 7
            'nestingLevel' => $this->nestingLevel,
1846 7
            'token'        => $this->lexer->token,
1847
        ];
1848
1849 7
        return $partialObjectExpression;
1850
    }
1851
1852
    /**
1853
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1854
     *
1855
     * @return \Doctrine\ORM\Query\AST\NewObjectExpression
1856
     */
1857 7
    public function NewObjectExpression()
1858
    {
1859 7
        $this->match(Lexer::T_NEW);
1860
1861 7
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1862 7
        $token = $this->lexer->token;
1863
1864 7
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1865
1866 7
        $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 7
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1869 3
            $this->match(Lexer::T_COMMA);
1870
1871 3
            $args[] = $this->NewObjectArg();
1872
        }
1873
1874 7
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1875
1876 7
        $expression = new AST\NewObjectExpression($className, $args);
1877
1878
        // Defer NewObjectExpression validation
1879 7
        $this->deferredNewObjectExpressions[] = [
1880 7
            'token'        => $token,
1881 7
            'expression'   => $expression,
1882 7
            'nestingLevel' => $this->nestingLevel,
1883
        ];
1884
1885 7
        return $expression;
1886
    }
1887
1888
    /**
1889
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1890
     *
1891
     * @return mixed
1892
     */
1893 7
    public function NewObjectArg()
1894
    {
1895 7
        $token = $this->lexer->lookahead;
1896 7
        $peek  = $this->lexer->glimpse();
1897
1898 7
        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 7
        return $this->ScalarExpression();
1907
    }
1908
1909
    /**
1910
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1911
     *
1912
     * @return \Doctrine\ORM\Query\AST\IndexBy
1913
     */
1914 3
    public function IndexBy()
1915
    {
1916 3
        $this->match(Lexer::T_INDEX);
1917 3
        $this->match(Lexer::T_BY);
1918 3
        $pathExpr = $this->StateFieldPathExpression();
1919
1920
        // Add the INDEX BY info to the query component
1921 3
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1922
1923 3
        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 117
    public function ScalarExpression()
1934
    {
1935 117
        $lookahead = $this->lexer->lookahead['type'];
1936 117
        $peek      = $this->lexer->glimpse();
1937
1938
        switch (true) {
1939 117
            case ($lookahead === Lexer::T_INTEGER):
1940 114
            case ($lookahead === Lexer::T_FLOAT):
1941
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1942 114
            case ($lookahead === Lexer::T_MINUS):
1943 114
            case ($lookahead === Lexer::T_PLUS):
1944 16
                return $this->SimpleArithmeticExpression();
1945
1946 114
            case ($lookahead === Lexer::T_STRING):
1947 11
                return $this->StringPrimary();
1948
1949 112
            case ($lookahead === Lexer::T_TRUE):
1950 112
            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 112
            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 112
            case ($lookahead === Lexer::T_CASE):
1965 108
            case ($lookahead === Lexer::T_COALESCE):
1966 108
            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 108
            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 105
            case ($this->isFunction()):
1976 19
                $this->lexer->peek(); // "("
1977
1978
                switch (true) {
1979 19
                    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 15
                    case ($this->isAggregateFunction($this->lexer->lookahead['type'])):
1984 13
                        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 92
            case ($lookahead === Lexer::T_IDENTIFIER):
1994 92
                $this->lexer->peek(); // lookahead => '.'
1995 92
                $this->lexer->peek(); // lookahead => token after '.'
1996 92
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1997 92
                $this->lexer->resetPeek();
1998
1999 92
                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 90
                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 18
    public function CaseExpression()
2023
    {
2024 18
        $lookahead = $this->lexer->lookahead['type'];
2025
2026
        switch ($lookahead) {
2027 18
            case Lexer::T_NULLIF:
2028 5
                return $this->NullIfExpression();
2029
2030 15
            case Lexer::T_COALESCE:
2031 2
                return $this->CoalesceExpression();
2032
2033 13
            case Lexer::T_CASE:
2034 13
                $this->lexer->resetPeek();
2035 13
                $peek = $this->lexer->peek();
2036
2037 13
                if ($peek['type'] === Lexer::T_WHEN) {
2038 8
                    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 8
    public function GeneralCaseExpression()
2101
    {
2102 8
        $this->match(Lexer::T_CASE);
2103
2104
        // Process WhenClause (1..N)
2105 8
        $whenClauses = [];
2106
2107
        do {
2108 8
            $whenClauses[] = $this->WhenClause();
2109 8
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2110
2111 8
        $this->match(Lexer::T_ELSE);
2112 8
        $scalarExpression = $this->ScalarExpression();
2113 8
        $this->match(Lexer::T_END);
2114
2115 8
        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 8
    public function WhenClause()
2149
    {
2150 8
        $this->match(Lexer::T_WHEN);
2151 8
        $conditionalExpression = $this->ConditionalExpression();
2152 8
        $this->match(Lexer::T_THEN);
2153
2154 8
        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 441
    public function SelectExpression()
2180
    {
2181 441
        $expression    = null;
2182 441
        $identVariable = null;
2183 441
        $peek          = $this->lexer->glimpse();
2184 441
        $lookaheadType = $this->lexer->lookahead['type'];
2185
2186
        switch (true) {
2187
            // ScalarExpression (u.name)
2188 441
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2189 79
                $expression = $this->ScalarExpression();
2190 79
                break;
2191
2192
            // IdentificationVariable (u)
2193 388
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2194 315
                $expression = $identVariable = $this->IdentificationVariable();
2195 315
                break;
2196
2197
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
2198 112
            case ($lookaheadType === Lexer::T_CASE):
2199 107
            case ($lookaheadType === Lexer::T_COALESCE):
2200 105
            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 103
            case ($this->isFunction()):
2206 54
                $this->lexer->peek(); // "("
2207
2208
                switch (true) {
2209 54
                    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 52
                    case ($this->isAggregateFunction($lookaheadType)):
2215
                        // COUNT(u.id)
2216 41
                        $expression = $this->AggregateExpression();
2217 41
                        break;
2218
2219
                    default:
2220
                        // IDENTITY(u)
2221 11
                        $expression = $this->FunctionDeclaration();
2222 11
                        break;
2223
                }
2224
2225 54
                break;
2226
2227
            // PartialObjectExpression (PARTIAL u.{id, name})
2228 49
            case ($lookaheadType === Lexer::T_PARTIAL):
2229 7
                $expression    = $this->PartialObjectExpression();
2230 7
                $identVariable = $expression->identificationVariable;
2231 7
                break;
2232
2233
            // Subselect
2234 42
            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 25
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2242 22
            case ($lookaheadType === Lexer::T_INTEGER):
2243 20
            case ($lookaheadType === Lexer::T_STRING):
2244 11
            case ($lookaheadType === Lexer::T_FLOAT):
2245
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2246 11
            case ($lookaheadType === Lexer::T_MINUS):
2247 11
            case ($lookaheadType === Lexer::T_PLUS):
2248 15
                $expression = $this->SimpleArithmeticExpression();
2249 15
                break;
2250
2251
            // NewObjectExpression (New ClassName(id, name))
2252 10
            case ($lookaheadType === Lexer::T_NEW):
2253 7
                $expression = $this->NewObjectExpression();
2254 7
                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 438
        $mustHaveAliasResultVariable = false;
2265
2266 438
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2267 68
            $this->match(Lexer::T_AS);
2268
2269 68
            $mustHaveAliasResultVariable = true;
2270
        }
2271
2272 438
        $hiddenAliasResultVariable = false;
2273
2274 438
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2275 6
            $this->match(Lexer::T_HIDDEN);
2276
2277 6
            $hiddenAliasResultVariable = true;
2278
        }
2279
2280 438
        $aliasResultVariable = null;
2281
2282 438
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2283 76
            $token = $this->lexer->lookahead;
2284 76
            $aliasResultVariable = $this->AliasResultVariable();
2285
2286
            // Include AliasResultVariable in query components.
2287 71
            $this->queryComponents[$aliasResultVariable] = [
2288 71
                'resultVariable' => $expression,
2289 71
                'nestingLevel'   => $this->nestingLevel,
2290 71
                'token'          => $token,
2291
            ];
2292
        }
2293
2294
        // AST
2295
2296 433
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2297
2298 433
        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 321
            $this->identVariableExpressions[$identVariable] = $expr;
2300
        }
2301
2302 433
        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 41
    public function SimpleSelectExpression()
2314
    {
2315 41
        $peek = $this->lexer->glimpse();
2316
2317 41
        switch ($this->lexer->lookahead['type']) {
2318 41
            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 25
            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 22
        $this->lexer->peek();
2367
2368 22
        $expression = $this->ScalarExpression();
2369 22
        $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 22
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2372 1
            $this->match(Lexer::T_AS);
2373
        }
2374
2375 22
        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 22
        return $expr;
2389
    }
2390
2391
    /**
2392
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2393
     *
2394
     * @return \Doctrine\ORM\Query\AST\ConditionalExpression
2395
     */
2396 269
    public function ConditionalExpression()
2397
    {
2398 269
        $conditionalTerms = [];
2399 269
        $conditionalTerms[] = $this->ConditionalTerm();
2400
2401 266
        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 266
        if (count($conditionalTerms) == 1) {
2410 260
            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 269
    public function ConditionalTerm()
2422
    {
2423 269
        $conditionalFactors = [];
2424 269
        $conditionalFactors[] = $this->ConditionalFactor();
2425
2426 266
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2427 21
            $this->match(Lexer::T_AND);
2428
2429 21
            $conditionalFactors[] = $this->ConditionalFactor();
2430
        }
2431
2432
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2433
        // if only one AST\ConditionalFactor is defined
2434 266
        if (count($conditionalFactors) == 1) {
2435 256
            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 21
        return new AST\ConditionalTerm($conditionalFactors);
2439
    }
2440
2441
    /**
2442
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2443
     *
2444
     * @return \Doctrine\ORM\Query\AST\ConditionalFactor
2445
     */
2446 269
    public function ConditionalFactor()
2447
    {
2448 269
        $not = false;
2449
2450 269
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2451 6
            $this->match(Lexer::T_NOT);
2452
2453 6
            $not = true;
2454
        }
2455
2456 269
        $conditionalPrimary = $this->ConditionalPrimary();
2457
2458
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2459
        // if only one AST\ConditionalPrimary is defined
2460 266
        if ( ! $not) {
2461 264
            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 269
    public function ConditionalPrimary()
2476
    {
2477 269
        $condPrimary = new AST\ConditionalPrimary;
2478
2479 269
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2480 261
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2481
2482 258
            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 14
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2492
2493 14
            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 269
    public function SimpleConditionalExpression()
2511
    {
2512 269
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2513 7
            return $this->ExistsExpression();
2514
        }
2515
2516 269
        $token      = $this->lexer->lookahead;
2517 269
        $peek       = $this->lexer->glimpse();
2518 269
        $lookahead  = $token;
2519
2520 269
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2521
            $token = $this->lexer->glimpse();
2522
        }
2523
2524 269
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2525
            // Peek beyond the matching closing parenthesis.
2526 248
            $beyond = $this->lexer->peek();
2527
2528 248
            switch ($peek['value']) {
2529 248
                case '(':
2530
                    // Peeks beyond the matched closing parenthesis.
2531 29
                    $token = $this->peekBeyondClosingParenthesis(false);
2532
2533 29
                    if ($token['type'] === Lexer::T_NOT) {
2534 3
                        $token = $this->lexer->peek();
2535
                    }
2536
2537 29
                    if ($token['type'] === Lexer::T_IS) {
2538 2
                        $lookahead = $this->lexer->peek();
2539
                    }
2540 29
                    break;
2541
2542
                default:
2543
                    // Peek beyond the PathExpression or InputParameter.
2544 225
                    $token = $beyond;
2545
2546 225
                    while ($token['value'] === '.') {
2547 195
                        $this->lexer->peek();
2548
2549 195
                        $token = $this->lexer->peek();
2550
                    }
2551
2552
                    // Also peek beyond a NOT if there is one.
2553 225
                    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 225
                    $lookahead = $this->lexer->peek();
2559
            }
2560
2561
            // Also peek beyond a NOT if there is one.
2562 248
            if ($lookahead['type'] === Lexer::T_NOT) {
2563 5
                $lookahead = $this->lexer->peek();
2564
            }
2565
2566 248
            $this->lexer->resetPeek();
2567
        }
2568
2569 269
        if ($token['type'] === Lexer::T_BETWEEN) {
2570 8
            return $this->BetweenExpression();
2571
        }
2572
2573 263
        if ($token['type'] === Lexer::T_LIKE) {
2574 14
            return $this->LikeExpression();
2575
        }
2576
2577 250
        if ($token['type'] === Lexer::T_IN) {
2578 26
            return $this->InExpression();
2579
        }
2580
2581 230
        if ($token['type'] === Lexer::T_INSTANCE) {
2582 10
            return $this->InstanceOfExpression();
2583
        }
2584
2585 220
        if ($token['type'] === Lexer::T_MEMBER) {
2586 7
            return $this->CollectionMemberExpression();
2587
        }
2588
2589 213
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2590 11
            return $this->NullComparisonExpression();
2591
        }
2592
2593 204
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2594 3
            return $this->EmptyCollectionComparisonExpression();
2595
        }
2596
2597 201
        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 133
    public function Literal()
2661
    {
2662 133
        switch ($this->lexer->lookahead['type']) {
2663 133
            case Lexer::T_STRING:
2664 37
                $this->match(Lexer::T_STRING);
2665
2666 37
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2667 103
            case Lexer::T_INTEGER:
2668 5
            case Lexer::T_FLOAT:
2669 98
                $this->match(
2670 98
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2671
                );
2672
2673 98
                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 98
    public function InputParameter()
2706
    {
2707 98
        $this->match(Lexer::T_INPUT_PARAMETER);
2708
2709 98
        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 229
    public function ArithmeticExpression()
2718
    {
2719 229
        $expr = new AST\ArithmeticExpression;
2720
2721 229
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2722 18
            $peek = $this->lexer->glimpse();
2723
2724 18
            if ($peek['type'] === Lexer::T_SELECT) {
2725 6
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2726 6
                $expr->subselect = $this->Subselect();
2727 6
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2728
2729 6
                return $expr;
2730
            }
2731
        }
2732
2733 229
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2734
2735 229
        return $expr;
2736
    }
2737
2738
    /**
2739
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2740
     *
2741
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2742
     */
2743 289
    public function SimpleArithmeticExpression()
2744
    {
2745 289
        $terms = [];
2746 289
        $terms[] = $this->ArithmeticTerm();
2747
2748 289
        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 289
        if (count($terms) == 1) {
2758 287
            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 289
    public function ArithmeticTerm()
2770
    {
2771 289
        $factors = [];
2772 289
        $factors[] = $this->ArithmeticFactor();
2773
2774 289
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2775 29
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2776
2777 29
            $factors[] = $this->lexer->token['value'];
2778 29
            $factors[] = $this->ArithmeticFactor();
2779
        }
2780
2781
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2782
        // if only one AST\ArithmeticFactor is defined
2783 289
        if (count($factors) == 1) {
2784 283
            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 29
        return new AST\ArithmeticTerm($factors);
2788
    }
2789
2790
    /**
2791
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2792
     *
2793
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2794
     */
2795 289
    public function ArithmeticFactor()
2796
    {
2797 289
        $sign = null;
2798
2799 289
        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 289
        $primary = $this->ArithmeticPrimary();
2805
2806
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2807
        // if only one AST\ArithmeticPrimary is defined
2808 289
        if ($sign === null) {
2809 288
            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 289
    public function ArithmeticPrimary()
2822
    {
2823 289
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2824 22
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2825
2826 22
            $expr = $this->SimpleArithmeticExpression();
2827
2828 22
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2829
2830 22
            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 289
        switch ($this->lexer->lookahead['type']) {
2834 289
            case Lexer::T_COALESCE:
2835 289
            case Lexer::T_NULLIF:
2836 289
            case Lexer::T_CASE:
2837 4
                return $this->CaseExpression();
2838
2839 289
            case Lexer::T_IDENTIFIER:
2840 264
                $peek = $this->lexer->glimpse();
2841
2842 264
                if ($peek['value'] == '(') {
2843 23
                    return $this->FunctionDeclaration();
2844
                }
2845
2846 248
                if ($peek['value'] == '.') {
2847 244
                    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 204
            case Lexer::T_INPUT_PARAMETER:
2857 83
                return $this->InputParameter();
2858
2859
            default:
2860 128
                $peek = $this->lexer->glimpse();
2861
2862 128
                if ($peek['value'] == '(') {
2863 14
                    if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2864 14
                        return $this->AggregateExpression();
2865
                    }
2866
2867
                    return $this->FunctionDeclaration();
2868
                }
2869
2870 125
                return $this->Literal();
2871
        }
2872
    }
2873
2874
    /**
2875
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2876
     *
2877
     * @return \Doctrine\ORM\Query\AST\Subselect |
2878
     *         string
2879
     */
2880 14
    public function StringExpression()
2881
    {
2882 14
        $peek = $this->lexer->glimpse();
2883
2884
        // Subselect
2885 14
        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 14
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2895 14
            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 12
        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 36
    public function StringPrimary()
2906
    {
2907 36
        $lookaheadType = $this->lexer->lookahead['type'];
2908
2909
        switch ($lookaheadType) {
2910 36
            case Lexer::T_IDENTIFIER:
2911 23
                $peek = $this->lexer->glimpse();
2912
2913 23
                if ($peek['value'] == '.') {
2914 23
                    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 64
    public function AggregateExpression()
2987
    {
2988 64
        $lookaheadType = $this->lexer->lookahead['type'];
2989 64
        $isDistinct = false;
2990
2991 64
        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 64
        $this->match($lookaheadType);
2996 64
        $functionName = $this->lexer->token['value'];
2997 64
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2998
2999 64
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
3000 2
            $this->match(Lexer::T_DISTINCT);
3001 2
            $isDistinct = true;
3002
        }
3003
3004 64
        $pathExp = $this->SimpleArithmeticExpression();
3005
3006 64
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3007
3008 64
        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 201
    public function ComparisonExpression()
3068
    {
3069 201
        $this->lexer->glimpse();
3070
3071 201
        $leftExpr  = $this->ArithmeticExpression();
3072 201
        $operator  = $this->ComparisonOperator();
3073 201
        $rightExpr = ($this->isNextAllAnySome())
3074 3
            ? $this->QuantifiedExpression()
3075 201
            : $this->ArithmeticExpression();
3076
3077 199
        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 14
    public function LikeExpression()
3186
    {
3187 14
        $stringExpr = $this->StringExpression();
3188 14
        $not = false;
3189
3190 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3191 3
            $this->match(Lexer::T_NOT);
3192 3
            $not = true;
3193
        }
3194
3195 14
        $this->match(Lexer::T_LIKE);
3196
3197 14
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3198 4
            $this->match(Lexer::T_INPUT_PARAMETER);
3199 4
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3200
        } else {
3201 11
            $stringPattern = $this->StringPrimary();
3202
        }
3203
3204 14
        $escapeChar = null;
3205
3206 14
        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 14
        $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 14
        $likeExpr->not = $not;
3215
3216 14
        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 11
    public function NullComparisonExpression()
3225
    {
3226
        switch (true) {
3227 11
            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 11
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3234 1
                $expr = $this->NullIfExpression();
3235 1
                break;
3236
3237 11
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3238 1
                $expr = $this->CoalesceExpression();
3239 1
                break;
3240
3241 11
            case $this->isAggregateFunction($this->lexer->lookahead['type']):
3242 1
                $expr = $this->AggregateExpression();
3243 1
                break;
3244
3245 11
            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 10
                $glimpse = $this->lexer->glimpse();
3252
3253 10
                if ($glimpse['type'] === Lexer::T_DOT) {
3254 7
                    $expr = $this->SingleValuedPathExpression();
3255
3256
                    // Leave switch statement
3257 7
                    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 11
        $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 11
        $this->match(Lexer::T_IS);
3285
3286 11
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3287 4
            $this->match(Lexer::T_NOT);
3288
3289 4
            $nullCompExpr->not = true;
3290
        }
3291
3292 11
        $this->match(Lexer::T_NULL);
3293
3294 11
        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 201
    public function ComparisonOperator()
3328
    {
3329 201
        switch ($this->lexer->lookahead['value']) {
3330 201
            case '=':
3331 154
                $this->match(Lexer::T_EQUALS);
3332
3333 154
                return '=';
3334
3335 58
            case '<':
3336 17
                $this->match(Lexer::T_LOWER_THAN);
3337 17
                $operator = '<';
3338
3339 17
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3340 5
                    $this->match(Lexer::T_EQUALS);
3341 5
                    $operator .= '=';
3342 12
                } else if ($this->lexer->isNextToken(Lexer::T_GREATER_THAN)) {
3343 3
                    $this->match(Lexer::T_GREATER_THAN);
3344 3
                    $operator .= '>';
3345
                }
3346
3347 17
                return $operator;
3348
3349 49
            case '>':
3350 43
                $this->match(Lexer::T_GREATER_THAN);
3351 43
                $operator = '>';
3352
3353 43
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3354 6
                    $this->match(Lexer::T_EQUALS);
3355 6
                    $operator .= '=';
3356
                }
3357
3358 43
                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 37
    public function FunctionDeclaration()
3377
    {
3378 37
        $token = $this->lexer->lookahead;
3379 37
        $funcName = strtolower($token['value']);
3380
3381
        // Check for built-in functions first!
3382
        switch (true) {
3383 37
            case (isset(self::$_STRING_FUNCTIONS[$funcName])):
3384 20
                return $this->FunctionsReturningStrings();
3385
3386 18
            case (isset(self::$_NUMERIC_FUNCTIONS[$funcName])):
3387 13
                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 13
    public function FunctionsReturningNumerics()
3440
    {
3441 13
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3442 13
        $funcClass     = self::$_NUMERIC_FUNCTIONS[$funcNameLower];
3443
3444 13
        $function = new $funcClass($funcNameLower);
3445 13
        $function->parse($this);
3446
3447 13
        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