Completed
Pull Request — master (#6500)
by Mathew
17:08
created

Parser::OrderByClause()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 16
c 0
b 0
f 0
ccs 9
cts 9
cp 1
rs 9.4285
cc 2
eloc 9
nc 2
nop 0
crap 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Query;
21
22
use Doctrine\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
        // Aggregate functions
70
        'min'       => Functions\MinFunction::class,
71
        'max'       => Functions\MaxFunction::class,
72
        'avg'       => Functions\AvgFunction::class,
73
        'sum'       => Functions\SumFunction::class,
74
        'count'     => Functions\CountFunction::class,
75
    ];
76
77
    /**
78
     * READ-ONLY: Maps BUILT-IN datetime function names to AST class names.
79
     *
80
     * @var array
81
     */
82
    private static $_DATETIME_FUNCTIONS = [
83
        'current_date'      => Functions\CurrentDateFunction::class,
84
        'current_time'      => Functions\CurrentTimeFunction::class,
85
        'current_timestamp' => Functions\CurrentTimestampFunction::class,
86
        'date_add'          => Functions\DateAddFunction::class,
87
        'date_sub'          => Functions\DateSubFunction::class,
88
    ];
89
90
    /*
91
     * Expressions that were encountered during parsing of identifiers and expressions
92
     * and still need to be validated.
93
     */
94
95
    /**
96
     * @var array
97
     */
98
    private $deferredIdentificationVariables = [];
99
100
    /**
101
     * @var array
102
     */
103
    private $deferredPartialObjectExpressions = [];
104
105
    /**
106
     * @var array
107
     */
108
    private $deferredPathExpressions = [];
109
110
    /**
111
     * @var array
112
     */
113
    private $deferredResultVariables = [];
114
115
    /**
116
     * @var array
117
     */
118
    private $deferredNewObjectExpressions = [];
119
120
    /**
121
     * The lexer.
122
     *
123
     * @var \Doctrine\ORM\Query\Lexer
124
     */
125
    private $lexer;
126
127
    /**
128
     * The parser result.
129
     *
130
     * @var \Doctrine\ORM\Query\ParserResult
131
     */
132
    private $parserResult;
133
134
    /**
135
     * The EntityManager.
136
     *
137
     * @var \Doctrine\ORM\EntityManager
138
     */
139
    private $em;
140
141
    /**
142
     * The Query to parse.
143
     *
144
     * @var Query
145
     */
146
    private $query;
147
148
    /**
149
     * Map of declared query components in the parsed query.
150
     *
151
     * @var array
152
     */
153
    private $queryComponents = [];
154
155
    /**
156
     * Keeps the nesting level of defined ResultVariables.
157
     *
158
     * @var integer
159
     */
160
    private $nestingLevel = 0;
161
162
    /**
163
     * Any additional custom tree walkers that modify the AST.
164
     *
165
     * @var array
166
     */
167
    private $customTreeWalkers = [];
168
169
    /**
170
     * The custom last tree walker, if any, that is responsible for producing the output.
171
     *
172
     * @var TreeWalker
173
     */
174
    private $customOutputWalker;
175
176
    /**
177
     * @var array
178
     */
179
    private $identVariableExpressions = [];
180
181
    /**
182
     * Creates a new query parser object.
183
     *
184
     * @param Query $query The Query to parse.
185
     */
186 757
    public function __construct(Query $query)
187
    {
188 757
        $this->query        = $query;
189 757
        $this->em           = $query->getEntityManager();
190 757
        $this->lexer        = new Lexer($query->getDQL());
191 757
        $this->parserResult = new ParserResult();
192 757
    }
193
194
    /**
195
     * Sets a custom tree walker that produces output.
196
     * This tree walker will be run last over the AST, after any other walkers.
197
     *
198
     * @param string $className
199
     *
200
     * @return void
201
     */
202 111
    public function setCustomOutputTreeWalker($className)
203
    {
204 111
        $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...
205 111
    }
206
207
    /**
208
     * Adds a custom tree walker for modifying the AST.
209
     *
210
     * @param string $className
211
     *
212
     * @return void
213
     */
214
    public function addCustomTreeWalker($className)
215
    {
216
        $this->customTreeWalkers[] = $className;
217
    }
218
219
    /**
220
     * Gets the lexer used by the parser.
221
     *
222
     * @return \Doctrine\ORM\Query\Lexer
223
     */
224 27
    public function getLexer()
225
    {
226 27
        return $this->lexer;
227
    }
228
229
    /**
230
     * Gets the ParserResult that is being filled with information during parsing.
231
     *
232
     * @return \Doctrine\ORM\Query\ParserResult
233
     */
234
    public function getParserResult()
235
    {
236
        return $this->parserResult;
237
    }
238
239
    /**
240
     * Gets the EntityManager used by the parser.
241
     *
242
     * @return \Doctrine\ORM\EntityManager
243
     */
244
    public function getEntityManager()
245
    {
246
        return $this->em;
247
    }
248
249
    /**
250
     * Parses and builds AST for the given Query.
251
     *
252
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
253
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
254
     *         \Doctrine\ORM\Query\AST\DeleteStatement
255
     */
256 757
    public function getAST()
257
    {
258
        // Parse & build AST
259 757
        $AST = $this->QueryLanguage();
260
261
        // Process any deferred validations of some nodes in the AST.
262
        // This also allows post-processing of the AST for modification purposes.
263 708
        $this->processDeferredIdentificationVariables();
264
265 707
        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...
266 11
            $this->processDeferredPartialObjectExpressions();
267
        }
268
269 705
        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...
270 511
            $this->processDeferredPathExpressions();
271
        }
272
273 702
        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...
274 9
            $this->processDeferredResultVariables();
275
        }
276
277 702
        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...
278 26
            $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...
279
        }
280
281 698
        $this->processRootEntityAliasSelected();
282
283
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
284 697
        $this->fixIdentificationVariableOrder($AST);
285
286 697
        return $AST;
287
    }
288
289
    /**
290
     * Attempts to match the given token with the current lookahead token.
291
     *
292
     * If they match, updates the lookahead token; otherwise raises a syntax
293
     * error.
294
     *
295
     * @param int $token The token type.
296
     *
297
     * @return void
298
     *
299
     * @throws QueryException If the tokens don't match.
300
     */
301 768
    public function match($token)
302
    {
303 768
        $lookaheadType = $this->lexer->lookahead['type'];
304
305
        // Short-circuit on first condition, usually types match
306 768
        if ($lookaheadType !== $token) {
307
            // If parameter is not identifier (1-99) must be exact match
308 21
            if ($token < Lexer::T_IDENTIFIER) {
309 3
                $this->syntaxError($this->lexer->getLiteral($token));
310
            }
311
312
            // If parameter is keyword (200+) must be exact match
313 18
            if ($token > Lexer::T_IDENTIFIER) {
314 7
                $this->syntaxError($this->lexer->getLiteral($token));
315
            }
316
317
            // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
318 11
            if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
319 8
                $this->syntaxError($this->lexer->getLiteral($token));
320
            }
321
        }
322
323 761
        $this->lexer->moveNext();
324 761
    }
325
326
    /**
327
     * Frees this parser, enabling it to be reused.
328
     *
329
     * @param boolean $deep     Whether to clean peek and reset errors.
330
     * @param integer $position Position to reset.
331
     *
332
     * @return void
333
     */
334
    public function free($deep = false, $position = 0)
335
    {
336
        // WARNING! Use this method with care. It resets the scanner!
337
        $this->lexer->resetPosition($position);
338
339
        // Deep = true cleans peek and also any previously defined errors
340
        if ($deep) {
341
            $this->lexer->resetPeek();
342
        }
343
344
        $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...
345
        $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...
346
    }
347
348
    /**
349
     * Parses a query string.
350
     *
351
     * @return ParserResult
352
     */
353 757
    public function parse()
354
    {
355 757
        $AST = $this->getAST();
356
357 697
        if (($customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) !== false) {
358 83
            $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...
359
        }
360
361 697
        if (($customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER)) !== false) {
362 59
            $this->customOutputWalker = $customOutputWalker;
363
        }
364
365
        // Run any custom tree walkers over the AST
366 697
        if ($this->customTreeWalkers) {
367 82
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
368
369 82
            foreach ($this->customTreeWalkers as $walker) {
370 82
                $treeWalkerChain->addTreeWalker($walker);
371
            }
372
373
            switch (true) {
374 82
                case ($AST instanceof AST\UpdateStatement):
375
                    $treeWalkerChain->walkUpdateStatement($AST);
376
                    break;
377
378 82
                case ($AST instanceof AST\DeleteStatement):
379
                    $treeWalkerChain->walkDeleteStatement($AST);
380
                    break;
381
382 82
                case ($AST instanceof AST\SelectStatement):
383
                default:
384 82
                    $treeWalkerChain->walkSelectStatement($AST);
385
            }
386
387 79
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
388
        }
389
390 694
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
391 694
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
392
393
        // Assign an SQL executor to the parser result
394 694
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
395
396 686
        return $this->parserResult;
397
    }
398
399
    /**
400
     * Fixes order of identification variables.
401
     *
402
     * They have to appear in the select clause in the same order as the
403
     * declarations (from ... x join ... y join ... z ...) appear in the query
404
     * as the hydration process relies on that order for proper operation.
405
     *
406
     * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
407
     *
408
     * @return void
409
     */
410 697
    private function fixIdentificationVariableOrder($AST)
411
    {
412 697
        if (count($this->identVariableExpressions) <= 1) {
413 528
            return;
414
        }
415
416 174
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
417 174
            if ( ! isset($this->identVariableExpressions[$dqlAlias])) {
418 2
                continue;
419
            }
420
421 174
            $expr = $this->identVariableExpressions[$dqlAlias];
422 174
            $key  = array_search($expr, $AST->selectClause->selectExpressions);
423
424 174
            unset($AST->selectClause->selectExpressions[$key]);
425
426 174
            $AST->selectClause->selectExpressions[] = $expr;
427
        }
428 174
    }
429
430
    /**
431
     * Generates a new syntax error.
432
     *
433
     * @param string     $expected Expected string.
434
     * @param array|null $token    Got token.
435
     *
436
     * @return void
437
     *
438
     * @throws \Doctrine\ORM\Query\QueryException
439
     */
440 18
    public function syntaxError($expected = '', $token = null)
441
    {
442 18
        if ($token === null) {
443 15
            $token = $this->lexer->lookahead;
444
        }
445
446 18
        $tokenPos = (isset($token['position'])) ? $token['position'] : '-1';
447
448 18
        $message  = "line 0, col {$tokenPos}: Error: ";
449 18
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
450 18
        $message .= ($this->lexer->lookahead === null) ? 'end of string.' : "'{$token['value']}'";
451
452 18
        throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL()));
453
    }
454
455
    /**
456
     * Generates a new semantical error.
457
     *
458
     * @param string     $message Optional message.
459
     * @param array|null $token   Optional token.
460
     *
461
     * @return void
462
     *
463
     * @throws \Doctrine\ORM\Query\QueryException
464
     */
465 29
    public function semanticalError($message = '', $token = null)
466
    {
467 29
        if ($token === null) {
468 1
            $token = $this->lexer->lookahead;
469
        }
470
471
        // Minimum exposed chars ahead of token
472 29
        $distance = 12;
473
474
        // Find a position of a final word to display in error string
475 29
        $dql    = $this->query->getDQL();
476 29
        $length = strlen($dql);
477 29
        $pos    = $token['position'] + $distance;
478 29
        $pos    = strpos($dql, ' ', ($length > $pos) ? $pos : $length);
479 29
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
480
481 29
        $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1';
482 29
        $tokenStr = substr($dql, $token['position'], $length);
483
484
        // Building informative message
485 29
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
486
487 29
        throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL()));
488
    }
489
490
    /**
491
     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
492
     *
493
     * @param boolean $resetPeek Reset peek after finding the closing parenthesis.
494
     *
495
     * @return array
496
     */
497 91
    private function peekBeyondClosingParenthesis($resetPeek = true)
498
    {
499 91
        $token = $this->lexer->peek();
500 91
        $numUnmatched = 1;
501
502 91
        while ($numUnmatched > 0 && $token !== null) {
503 90
            switch ($token['type']) {
504 90
                case Lexer::T_OPEN_PARENTHESIS:
505 28
                    ++$numUnmatched;
506 28
                    break;
507
508 90
                case Lexer::T_CLOSE_PARENTHESIS:
509 90
                    --$numUnmatched;
510 90
                    break;
511
512
                default:
513
                    // Do nothing
514
            }
515
516 90
            $token = $this->lexer->peek();
517
        }
518
519 91
        if ($resetPeek) {
520 71
            $this->lexer->resetPeek();
521
        }
522
523 91
        return $token;
524
    }
525
526
    /**
527
     * Checks if the given token indicates a mathematical operator.
528
     *
529
     * @param array $token
530
     *
531
     * @return boolean TRUE if the token is a mathematical operator, FALSE otherwise.
532
     */
533 277
    private function isMathOperator($token)
534
    {
535 277
        return in_array($token['type'], [Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY]);
536
    }
537
538
    /**
539
     * Checks if the next-next (after lookahead) token starts a function.
540
     *
541
     * @return boolean TRUE if the next-next tokens start a function, FALSE otherwise.
542
     */
543 319
    private function isFunction()
544
    {
545 319
        $lookaheadType = $this->lexer->lookahead['type'];
546 319
        $peek          = $this->lexer->peek();
547
548 319
        $this->lexer->resetPeek();
549
550 319
        return ($lookaheadType >= Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_OPEN_PARENTHESIS);
551
    }
552
553
    /**
554
     * Checks whether the given token type indicates an aggregate function.
555
     *
556
     * @param int $tokenType
557
     *
558
     * @return boolean TRUE if the token type is an aggregate function, FALSE otherwise.
559
     */
560 1
    private function isAggregateFunction($tokenType)
561
    {
562 1
        return in_array($tokenType, [Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT]);
563
    }
564
565
    /**
566
     * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
567
     *
568
     * @return boolean
569
     */
570 259
    private function isNextAllAnySome()
571
    {
572 259
        return in_array($this->lexer->lookahead['type'], [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME]);
573
    }
574
575
    /**
576
     * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
577
     * It must exist in query components list.
578
     *
579
     * @return void
580
     */
581 708
    private function processDeferredIdentificationVariables()
582
    {
583 708
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
584 696
            $identVariable = $deferredItem['expression'];
585
586
            // Check if IdentificationVariable exists in queryComponents
587 696
            if ( ! isset($this->queryComponents[$identVariable])) {
588 1
                $this->semanticalError(
589 1
                    "'$identVariable' is not defined.", $deferredItem['token']
590
                );
591
            }
592
593 696
            $qComp = $this->queryComponents[$identVariable];
594
595
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
596 696
            if ( ! isset($qComp['metadata'])) {
597
                $this->semanticalError(
598
                    "'$identVariable' does not point to a Class.", $deferredItem['token']
599
                );
600
            }
601
602
            // Validate if identification variable nesting level is lower or equal than the current one
603 696
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
604
                $this->semanticalError(
605 696
                    "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token']
606
                );
607
            }
608
        }
609 707
    }
610
611
    /**
612
     * Validates that the given <tt>NewObjectExpression</tt>.
613
     *
614
     * @param \Doctrine\ORM\Query\AST\SelectClause $AST
615
     *
616
     * @return void
617
     */
618 26
    private function processDeferredNewObjectExpressions($AST)
619
    {
620 26
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
621 26
            $expression     = $deferredItem['expression'];
622 26
            $token          = $deferredItem['token'];
623 26
            $className      = $expression->className;
624 26
            $args           = $expression->args;
625 26
            $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...
626 26
                ? $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName
627 26
                : null;
628
629
            // If the namespace is not given then assumes the first FROM entity namespace
630 26
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
631 11
                $namespace  = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
632 11
                $fqcn       = $namespace . '\\' . $className;
633
634 11
                if (class_exists($fqcn)) {
635 11
                    $expression->className  = $fqcn;
636 11
                    $className              = $fqcn;
637
                }
638
            }
639
640 26
            if ( ! class_exists($className)) {
641 1
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
642
            }
643
644 25
            $class = new \ReflectionClass($className);
645
646 25
            if ( ! $class->isInstantiable()) {
647 1
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
648
            }
649
650 24
            if ($class->getConstructor() === null) {
651 1
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
652
            }
653
654 23
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
655 23
                $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
656
            }
657
        }
658 22
    }
659
660
    /**
661
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
662
     * It must exist in query components list.
663
     *
664
     * @return void
665
     */
666 11
    private function processDeferredPartialObjectExpressions()
667
    {
668 11
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
669 11
            $expr = $deferredItem['expression'];
670 11
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
671
672 11
            foreach ($expr->partialFieldSet as $field) {
673 11
                if (isset($class->fieldMappings[$field])) {
674 10
                    continue;
675
                }
676
677 3
                if (isset($class->associationMappings[$field]) &&
678 3
                    $class->associationMappings[$field]['isOwningSide'] &&
679 3
                    $class->associationMappings[$field]['type'] & ClassMetadata::TO_ONE) {
680 2
                    continue;
681
                }
682
683 1
                $this->semanticalError(
684 1
                    "There is no mapped field named '$field' on class " . $class->name . ".", $deferredItem['token']
685
                );
686
            }
687
688 10
            if (array_intersect($class->identifier, $expr->partialFieldSet) != $class->identifier) {
689 1
                $this->semanticalError(
690 1
                    "The partial field selection of class " . $class->name . " must contain the identifier.",
691 10
                    $deferredItem['token']
692
                );
693
            }
694
        }
695 9
    }
696
697
    /**
698
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
699
     * It must exist in query components list.
700
     *
701
     * @return void
702
     */
703 9
    private function processDeferredResultVariables()
704
    {
705 9
        foreach ($this->deferredResultVariables as $deferredItem) {
706 9
            $resultVariable = $deferredItem['expression'];
707
708
            // Check if ResultVariable exists in queryComponents
709 9
            if ( ! isset($this->queryComponents[$resultVariable])) {
710
                $this->semanticalError(
711
                    "'$resultVariable' is not defined.", $deferredItem['token']
712
                );
713
            }
714
715 9
            $qComp = $this->queryComponents[$resultVariable];
716
717
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
718 9
            if ( ! isset($qComp['resultVariable'])) {
719
                $this->semanticalError(
720
                    "'$resultVariable' does not point to a ResultVariable.", $deferredItem['token']
721
                );
722
            }
723
724
            // Validate if identification variable nesting level is lower or equal than the current one
725 9
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
726
                $this->semanticalError(
727 9
                    "'$resultVariable' is used outside the scope of its declaration.", $deferredItem['token']
728
                );
729
            }
730
        }
731 9
    }
732
733
    /**
734
     * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
735
     *
736
     * AssociationPathExpression             ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
737
     * SingleValuedPathExpression            ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
738
     * StateFieldPathExpression              ::= IdentificationVariable "." StateField
739
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
740
     * CollectionValuedPathExpression        ::= IdentificationVariable "." CollectionValuedAssociationField
741
     *
742
     * @return void
743
     */
744 511
    private function processDeferredPathExpressions()
745
    {
746 511
        foreach ($this->deferredPathExpressions as $deferredItem) {
747 511
            $pathExpression = $deferredItem['expression'];
748
749 511
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
750 511
            $class = $qComp['metadata'];
751
752 511
            if (($field = $pathExpression->field) === null) {
753 23
                $field = $pathExpression->field = $class->identifier[0];
754
            }
755
756
            // Check if field or association exists
757 511
            if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) {
758 1
                $this->semanticalError(
759 1
                    'Class ' . $class->name . ' has no field or association named ' . $field,
760 1
                    $deferredItem['token']
761
                );
762
            }
763
764 510
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
765
766 510
            if (isset($class->associationMappings[$field])) {
767 79
                $assoc = $class->associationMappings[$field];
768
769 79
                $fieldType = ($assoc['type'] & ClassMetadata::TO_ONE)
770 58
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
771 79
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
772
            }
773
774
            // Validate if PathExpression is one of the expected types
775 510
            $expectedType = $pathExpression->expectedType;
776
777 510
            if ( ! ($expectedType & $fieldType)) {
778
                // We need to recognize which was expected type(s)
779 2
                $expectedStringTypes = [];
780
781
                // Validate state field type
782 2
                if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
783 1
                    $expectedStringTypes[] = 'StateFieldPathExpression';
784
                }
785
786
                // Validate single valued association (*-to-one)
787 2
                if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
788 2
                    $expectedStringTypes[] = 'SingleValuedAssociationField';
789
                }
790
791
                // Validate single valued association (*-to-many)
792 2
                if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
793
                    $expectedStringTypes[] = 'CollectionValuedAssociationField';
794
                }
795
796
                // Build the error message
797 2
                $semanticalError  = 'Invalid PathExpression. ';
798 2
                $semanticalError .= (count($expectedStringTypes) == 1)
799 1
                    ? 'Must be a ' . $expectedStringTypes[0] . '.'
800 2
                    : implode(' or ', $expectedStringTypes) . ' expected.';
801
802 2
                $this->semanticalError($semanticalError, $deferredItem['token']);
803
            }
804
805
            // We need to force the type in PathExpression
806 508
            $pathExpression->type = $fieldType;
807
        }
808 508
    }
809
810
    /**
811
     * @return void
812
     */
813 698
    private function processRootEntityAliasSelected()
814
    {
815 698
        if ( ! count($this->identVariableExpressions)) {
816 181
            return;
817
        }
818
819 528
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
820 528
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
821 528
                return;
822
            }
823
        }
824
825 1
        $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
826
    }
827
828
    /**
829
     * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
830
     *
831
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
832
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
833
     *         \Doctrine\ORM\Query\AST\DeleteStatement
834
     */
835 757
    public function QueryLanguage()
836
    {
837 757
        $this->lexer->moveNext();
838
839 757
        switch ($this->lexer->lookahead['type']) {
840 757
            case Lexer::T_SELECT:
841 692
                $statement = $this->SelectStatement();
842 647
                break;
843
844 70
            case Lexer::T_UPDATE:
845 31
                $statement = $this->UpdateStatement();
846 31
                break;
847
848 41
            case Lexer::T_DELETE:
849 40
                $statement = $this->DeleteStatement();
850 39
                break;
851
852
            default:
853 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
854
                break;
855
        }
856
857
        // Check for end of string
858 711
        if ($this->lexer->lookahead !== null) {
859 3
            $this->syntaxError('end of string');
860
        }
861
862 708
        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...
863
    }
864
865
    /**
866
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
867
     *
868
     * @return \Doctrine\ORM\Query\AST\SelectStatement
869
     */
870 692
    public function SelectStatement()
871
    {
872 692
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
873
874 651
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
875 647
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
876 647
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
877 647
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
878
879 647
        return $selectStatement;
880
    }
881
882
    /**
883
     * UpdateStatement ::= UpdateClause [WhereClause]
884
     *
885
     * @return \Doctrine\ORM\Query\AST\UpdateStatement
886
     */
887 31
    public function UpdateStatement()
888
    {
889 31
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
890
891 31
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
892
893 31
        return $updateStatement;
894
    }
895
896
    /**
897
     * DeleteStatement ::= DeleteClause [WhereClause]
898
     *
899
     * @return \Doctrine\ORM\Query\AST\DeleteStatement
900
     */
901 40
    public function DeleteStatement()
902
    {
903 40
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
904
905 39
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
906
907 39
        return $deleteStatement;
908
    }
909
910
    /**
911
     * IdentificationVariable ::= identifier
912
     *
913
     * @return string
914
     */
915 734
    public function IdentificationVariable()
916
    {
917 734
        $this->match(Lexer::T_IDENTIFIER);
918
919 734
        $identVariable = $this->lexer->token['value'];
920
921 734
        $this->deferredIdentificationVariables[] = [
922 734
            'expression'   => $identVariable,
923 734
            'nestingLevel' => $this->nestingLevel,
924 734
            'token'        => $this->lexer->token,
925
        ];
926
927 734
        return $identVariable;
928
    }
929
930
    /**
931
     * AliasIdentificationVariable = identifier
932
     *
933
     * @return string
934
     */
935 717
    public function AliasIdentificationVariable()
936
    {
937 717
        $this->match(Lexer::T_IDENTIFIER);
938
939 717
        $aliasIdentVariable = $this->lexer->token['value'];
940 717
        $exists = isset($this->queryComponents[$aliasIdentVariable]);
941
942 717
        if ($exists) {
943 2
            $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->lexer->token);
944
        }
945
946 717
        return $aliasIdentVariable;
947
    }
948
949
    /**
950
     * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier
951
     *
952
     * @return string
953
     */
954 738
    public function AbstractSchemaName()
955
    {
956 738
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
957 720
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
958
959 720
            $schemaName = $this->lexer->token['value'];
960 29
        } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
961 19
            $this->match(Lexer::T_IDENTIFIER);
962
963 19
            $schemaName = $this->lexer->token['value'];
964
        } else {
965 11
            $this->match(Lexer::T_ALIASED_NAME);
966
967 10
            list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']);
968
969 10
            $schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
970
        }
971
972 737
        return $schemaName;
973
    }
974
975
    /**
976
     * Validates an AbstractSchemaName, making sure the class exists.
977
     *
978
     * @param string $schemaName The name to validate.
979
     *
980
     * @throws QueryException if the name does not exist.
981
     */
982 732
    private function validateAbstractSchemaName($schemaName)
983
    {
984 732
        if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {
985 16
            $this->semanticalError("Class '$schemaName' is not defined.", $this->lexer->token);
986
        }
987 717
    }
988
989
    /**
990
     * AliasResultVariable ::= identifier
991
     *
992
     * @return string
993
     */
994 68
    public function AliasResultVariable()
995
    {
996 68
        $this->match(Lexer::T_IDENTIFIER);
997
998 64
        $resultVariable = $this->lexer->token['value'];
999 64
        $exists = isset($this->queryComponents[$resultVariable]);
1000
1001 64
        if ($exists) {
1002
            $this->semanticalError("'$resultVariable' is already defined.", $this->lexer->token);
1003
        }
1004
1005 64
        return $resultVariable;
1006
    }
1007
1008
    /**
1009
     * ResultVariable ::= identifier
1010
     *
1011
     * @return string
1012
     */
1013 9
    public function ResultVariable()
1014
    {
1015 9
        $this->match(Lexer::T_IDENTIFIER);
1016
1017 9
        $resultVariable = $this->lexer->token['value'];
1018
1019
        // Defer ResultVariable validation
1020 9
        $this->deferredResultVariables[] = [
1021 9
            'expression'   => $resultVariable,
1022 9
            'nestingLevel' => $this->nestingLevel,
1023 9
            'token'        => $this->lexer->token,
1024
        ];
1025
1026 9
        return $resultVariable;
1027
    }
1028
1029
    /**
1030
     * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
1031
     *
1032
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1033
     */
1034 228
    public function JoinAssociationPathExpression()
1035
    {
1036 228
        $identVariable = $this->IdentificationVariable();
1037
1038 228
        if ( ! isset($this->queryComponents[$identVariable])) {
1039
            $this->semanticalError(
1040
                'Identification Variable ' . $identVariable .' used in join path expression but was not defined before.'
1041
            );
1042
        }
1043
1044 228
        $this->match(Lexer::T_DOT);
1045 228
        $this->match(Lexer::T_IDENTIFIER);
1046
1047 228
        $field = $this->lexer->token['value'];
1048
1049
        // Validate association field
1050 228
        $qComp = $this->queryComponents[$identVariable];
1051 228
        $class = $qComp['metadata'];
1052
1053 228
        if ( ! $class->hasAssociation($field)) {
1054
            $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
1055
        }
1056
1057 228
        return new AST\JoinAssociationPathExpression($identVariable, $field);
1058
    }
1059
1060
    /**
1061
     * Parses an arbitrary path expression and defers semantical validation
1062
     * based on expected types.
1063
     *
1064
     * PathExpression ::= IdentificationVariable {"." identifier}*
1065
     *
1066
     * @param integer $expectedTypes
1067
     *
1068
     * @return \Doctrine\ORM\Query\AST\PathExpression
1069
     */
1070 529
    public function PathExpression($expectedTypes)
1071
    {
1072 529
        $identVariable = $this->IdentificationVariable();
1073 529
        $field = null;
1074
1075 529
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1076 524
            $this->match(Lexer::T_DOT);
1077 524
            $this->match(Lexer::T_IDENTIFIER);
1078
1079 524
            $field = $this->lexer->token['value'];
1080
1081 524
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1082 2
                $this->match(Lexer::T_DOT);
1083 2
                $this->match(Lexer::T_IDENTIFIER);
1084 2
                $field .= '.'.$this->lexer->token['value'];
1085
            }
1086
        }
1087
1088
        // Creating AST node
1089 529
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1090
1091
        // Defer PathExpression validation if requested to be deferred
1092 529
        $this->deferredPathExpressions[] = [
1093 529
            'expression'   => $pathExpr,
1094 529
            'nestingLevel' => $this->nestingLevel,
1095 529
            'token'        => $this->lexer->token,
1096
        ];
1097
1098 529
        return $pathExpr;
1099
    }
1100
1101
    /**
1102
     * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
1103
     *
1104
     * @return \Doctrine\ORM\Query\AST\PathExpression
1105
     */
1106
    public function AssociationPathExpression()
1107
    {
1108
        return $this->PathExpression(
1109
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
1110
            AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
1111
        );
1112
    }
1113
1114
    /**
1115
     * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
1116
     *
1117
     * @return \Doctrine\ORM\Query\AST\PathExpression
1118
     */
1119 441
    public function SingleValuedPathExpression()
1120
    {
1121 441
        return $this->PathExpression(
1122 441
            AST\PathExpression::TYPE_STATE_FIELD |
1123 441
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1124
        );
1125
    }
1126
1127
    /**
1128
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1129
     *
1130
     * @return \Doctrine\ORM\Query\AST\PathExpression
1131
     */
1132 177
    public function StateFieldPathExpression()
1133
    {
1134 177
        return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
1135
    }
1136
1137
    /**
1138
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
1139
     *
1140
     * @return \Doctrine\ORM\Query\AST\PathExpression
1141
     */
1142 9
    public function SingleValuedAssociationPathExpression()
1143
    {
1144 9
        return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
1145
    }
1146
1147
    /**
1148
     * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
1149
     *
1150
     * @return \Doctrine\ORM\Query\AST\PathExpression
1151
     */
1152 21
    public function CollectionValuedPathExpression()
1153
    {
1154 21
        return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
1155
    }
1156
1157
    /**
1158
     * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
1159
     *
1160
     * @return \Doctrine\ORM\Query\AST\SelectClause
1161
     */
1162 692
    public function SelectClause()
1163
    {
1164 692
        $isDistinct = false;
1165 692
        $this->match(Lexer::T_SELECT);
1166
1167
        // Check for DISTINCT
1168 692
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1169 6
            $this->match(Lexer::T_DISTINCT);
1170
1171 6
            $isDistinct = true;
1172
        }
1173
1174
        // Process SelectExpressions (1..N)
1175 692
        $selectExpressions = [];
1176 692
        $selectExpressions[] = $this->SelectExpression();
1177
1178 681
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1179 244
            $this->match(Lexer::T_COMMA);
1180
1181 244
            $selectExpressions[] = $this->SelectExpression();
1182
        }
1183
1184 674
        return new AST\SelectClause($selectExpressions, $isDistinct);
1185
    }
1186
1187
    /**
1188
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1189
     *
1190
     * @return \Doctrine\ORM\Query\AST\SimpleSelectClause
1191
     */
1192 27
    public function SimpleSelectClause()
1193
    {
1194 27
        $isDistinct = false;
1195 27
        $this->match(Lexer::T_SELECT);
1196
1197 27
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1198
            $this->match(Lexer::T_DISTINCT);
1199
1200
            $isDistinct = true;
1201
        }
1202
1203 27
        return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1204
    }
1205
1206
    /**
1207
     * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1208
     *
1209
     * @return \Doctrine\ORM\Query\AST\UpdateClause
1210
     */
1211 31
    public function UpdateClause()
1212
    {
1213 31
        $this->match(Lexer::T_UPDATE);
1214
1215 31
        $token = $this->lexer->lookahead;
1216 31
        $abstractSchemaName = $this->AbstractSchemaName();
1217
1218 31
        $this->validateAbstractSchemaName($abstractSchemaName);
1219
1220 31
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1221 1
            $this->match(Lexer::T_AS);
1222
        }
1223
1224 31
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1225
1226 31
        $class = $this->em->getClassMetadata($abstractSchemaName);
1227
1228
        // Building queryComponent
1229
        $queryComponent = [
1230 31
            'metadata'     => $class,
1231
            'parent'       => null,
1232
            'relation'     => null,
1233
            'map'          => null,
1234 31
            'nestingLevel' => $this->nestingLevel,
1235 31
            'token'        => $token,
1236
        ];
1237
1238 31
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1239
1240 31
        $this->match(Lexer::T_SET);
1241
1242 31
        $updateItems = [];
1243 31
        $updateItems[] = $this->UpdateItem();
1244
1245 31
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1246 5
            $this->match(Lexer::T_COMMA);
1247
1248 5
            $updateItems[] = $this->UpdateItem();
1249
        }
1250
1251 31
        $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1252 31
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1253
1254 31
        return $updateClause;
1255
    }
1256
1257
    /**
1258
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1259
     *
1260
     * @return \Doctrine\ORM\Query\AST\DeleteClause
1261
     */
1262 40
    public function DeleteClause()
1263
    {
1264 40
        $this->match(Lexer::T_DELETE);
1265
1266 40
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1267 8
            $this->match(Lexer::T_FROM);
1268
        }
1269
1270 40
        $token = $this->lexer->lookahead;
1271 40
        $abstractSchemaName = $this->AbstractSchemaName();
1272
1273 40
        $this->validateAbstractSchemaName($abstractSchemaName);
1274
1275 40
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1276
1277 40
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1278
            $this->match(Lexer::T_AS);
1279
        }
1280
1281 40
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1282
1283 39
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1284 39
        $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1285
1286
        // Building queryComponent
1287
        $queryComponent = [
1288 39
            'metadata'     => $class,
1289
            'parent'       => null,
1290
            'relation'     => null,
1291
            'map'          => null,
1292 39
            'nestingLevel' => $this->nestingLevel,
1293 39
            'token'        => $token,
1294
        ];
1295
1296 39
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1297
1298 39
        return $deleteClause;
1299
    }
1300
1301
    /**
1302
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1303
     *
1304
     * @return \Doctrine\ORM\Query\AST\FromClause
1305
     */
1306 674
    public function FromClause()
1307
    {
1308 674
        $this->match(Lexer::T_FROM);
1309
1310 669
        $identificationVariableDeclarations = [];
1311 669
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1312
1313 651
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1314 6
            $this->match(Lexer::T_COMMA);
1315
1316 6
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1317
        }
1318
1319 651
        return new AST\FromClause($identificationVariableDeclarations);
1320
    }
1321
1322
    /**
1323
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1324
     *
1325
     * @return \Doctrine\ORM\Query\AST\SubselectFromClause
1326
     */
1327 26
    public function SubselectFromClause()
1328
    {
1329 26
        $this->match(Lexer::T_FROM);
1330
1331 26
        $identificationVariables = [];
1332 26
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1333
1334 25
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1335
            $this->match(Lexer::T_COMMA);
1336
1337
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1338
        }
1339
1340 25
        return new AST\SubselectFromClause($identificationVariables);
1341
    }
1342
1343
    /**
1344
     * WhereClause ::= "WHERE" ConditionalExpression
1345
     *
1346
     * @return \Doctrine\ORM\Query\AST\WhereClause
1347
     */
1348 313
    public function WhereClause()
1349
    {
1350 313
        $this->match(Lexer::T_WHERE);
1351
1352 313
        return new AST\WhereClause($this->ConditionalExpression());
1353
    }
1354
1355
    /**
1356
     * HavingClause ::= "HAVING" ConditionalExpression
1357
     *
1358
     * @return \Doctrine\ORM\Query\AST\HavingClause
1359
     */
1360 4
    public function HavingClause()
1361
    {
1362 4
        $this->match(Lexer::T_HAVING);
1363
1364 4
        return new AST\HavingClause($this->ConditionalExpression());
1365
    }
1366
1367
    /**
1368
     * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1369
     *
1370
     * @return \Doctrine\ORM\Query\AST\GroupByClause
1371
     */
1372 7
    public function GroupByClause()
1373
    {
1374 7
        $this->match(Lexer::T_GROUP);
1375 7
        $this->match(Lexer::T_BY);
1376
1377 7
        $groupByItems = [$this->GroupByItem()];
1378
1379 7
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1380 3
            $this->match(Lexer::T_COMMA);
1381
1382 3
            $groupByItems[] = $this->GroupByItem();
1383
        }
1384
1385 7
        return new AST\GroupByClause($groupByItems);
1386
    }
1387
1388
    /**
1389
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1390
     *
1391
     * @return \Doctrine\ORM\Query\AST\OrderByClause
1392
     */
1393 156
    public function OrderByClause()
1394
    {
1395 156
        $this->match(Lexer::T_ORDER);
1396 156
        $this->match(Lexer::T_BY);
1397
1398 156
        $orderByItems = [];
1399 156
        $orderByItems[] = $this->OrderByItem();
1400
1401 156
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1402 11
            $this->match(Lexer::T_COMMA);
1403
1404 11
            $orderByItems[] = $this->OrderByItem();
1405
        }
1406
1407 156
        return new AST\OrderByClause($orderByItems);
1408
    }
1409
1410
    /**
1411
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1412
     *
1413
     * @return \Doctrine\ORM\Query\AST\Subselect
1414
     */
1415 27
    public function Subselect()
1416
    {
1417
        // Increase query nesting level
1418 27
        $this->nestingLevel++;
1419
1420 27
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1421
1422 25
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1423 25
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1424 25
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1425 25
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1426
1427
        // Decrease query nesting level
1428 25
        $this->nestingLevel--;
1429
1430 25
        return $subselect;
1431
    }
1432
1433
    /**
1434
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1435
     *
1436
     * @return \Doctrine\ORM\Query\AST\UpdateItem
1437
     */
1438 31
    public function UpdateItem()
1439
    {
1440 31
        $pathExpr = $this->SingleValuedPathExpression();
1441
1442 31
        $this->match(Lexer::T_EQUALS);
1443
1444 31
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1445
1446 31
        return $updateItem;
1447
    }
1448
1449
    /**
1450
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1451
     *
1452
     * @return string | \Doctrine\ORM\Query\AST\PathExpression
1453
     */
1454 7
    public function GroupByItem()
1455
    {
1456
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1457 7
        $glimpse = $this->lexer->glimpse();
1458
1459 7
        if ($glimpse['type'] === Lexer::T_DOT) {
1460 1
            return $this->SingleValuedPathExpression();
1461
        }
1462
1463
        // Still need to decide between IdentificationVariable or ResultVariable
1464 6
        $lookaheadValue = $this->lexer->lookahead['value'];
1465
1466 6
        if ( ! isset($this->queryComponents[$lookaheadValue])) {
1467
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1468
        }
1469
1470 6
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1471 4
            ? $this->IdentificationVariable()
1472 6
            : $this->ResultVariable();
1473
    }
1474
1475
    /**
1476
     * OrderByItem ::= (
1477
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1478
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1479
     * ) ["ASC" | "DESC"]
1480
     *
1481
     * @return \Doctrine\ORM\Query\AST\OrderByItem
1482
     */
1483 156
    public function OrderByItem()
1484
    {
1485 156
        $this->lexer->peek(); // lookahead => '.'
1486 156
        $this->lexer->peek(); // lookahead => token after '.'
1487
1488 156
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1489
1490 156
        $this->lexer->resetPeek();
1491
1492 156
        $glimpse = $this->lexer->glimpse();
1493
1494
        switch (true) {
1495 156
            case ($this->isFunction()):
1496 1
                $expr = $this->FunctionDeclaration();
1497 1
                break;
1498
1499 155
            case ($this->isMathOperator($peek)):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1488 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...
1500 25
                $expr = $this->SimpleArithmeticExpression();
1501 25
                break;
1502
1503 131
            case ($glimpse['type'] === Lexer::T_DOT):
1504 126
                $expr = $this->SingleValuedPathExpression();
1505 126
                break;
1506
1507 6
            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...
1508 2
                $expr = $this->ScalarExpression();
1509 2
                break;
1510
1511
            default:
1512 4
                $expr = $this->ResultVariable();
1513 4
                break;
1514
        }
1515
1516 156
        $type = 'ASC';
1517 156
        $item = new AST\OrderByItem($expr);
1518
1519
        switch (true) {
1520 156
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1521 80
                $this->match(Lexer::T_DESC);
1522 80
                $type = 'DESC';
1523 80
                break;
1524
1525 139
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1526 93
                $this->match(Lexer::T_ASC);
1527 93
                break;
1528
1529
            default:
1530
                // Do nothing
1531
        }
1532
1533 156
        $item->type = $type;
1534
1535 156
        return $item;
1536
    }
1537
1538
    /**
1539
     * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1540
     *      EnumPrimary | SimpleEntityExpression | "NULL"
1541
     *
1542
     * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1543
     * grammar that needs to be supported:
1544
     *
1545
     * NewValue ::= SimpleArithmeticExpression | "NULL"
1546
     *
1547
     * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1548
     *
1549
     * @return AST\ArithmeticExpression
1550
     */
1551 31
    public function NewValue()
1552
    {
1553 31
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1554 1
            $this->match(Lexer::T_NULL);
1555
1556 1
            return null;
1557
        }
1558
1559 30
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1560 19
            $this->match(Lexer::T_INPUT_PARAMETER);
1561
1562 19
            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...
1563
        }
1564
1565 11
        return $this->ArithmeticExpression();
1566
    }
1567
1568
    /**
1569
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1570
     *
1571
     * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1572
     */
1573 670
    public function IdentificationVariableDeclaration()
1574
    {
1575 670
        $joins                    = [];
1576 670
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1577 654
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1578 7
            ? $this->IndexBy()
1579 654
            : null;
1580
1581 654
        $rangeVariableDeclaration->isRoot = true;
1582
1583
        while (
1584 654
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1585 654
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1586 654
            $this->lexer->isNextToken(Lexer::T_JOIN)
1587
        ) {
1588 247
            $joins[] = $this->Join();
1589
        }
1590
1591 652
        return new AST\IdentificationVariableDeclaration(
1592 652
            $rangeVariableDeclaration, $indexBy, $joins
1593
        );
1594
    }
1595
1596
    /**
1597
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1598
     *
1599
     * {Internal note: WARNING: Solution is harder than a bare implementation.
1600
     * Desired EBNF support:
1601
     *
1602
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1603
     *
1604
     * It demands that entire SQL generation to become programmatical. This is
1605
     * needed because association based subselect requires "WHERE" conditional
1606
     * expressions to be injected, but there is no scope to do that. Only scope
1607
     * accessible is "FROM", prohibiting an easy implementation without larger
1608
     * changes.}
1609
     *
1610
     * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration |
1611
     *         \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1612
     */
1613 26
    public function SubselectIdentificationVariableDeclaration()
1614
    {
1615
        /*
1616
        NOT YET IMPLEMENTED!
1617
1618
        $glimpse = $this->lexer->glimpse();
1619
1620
        if ($glimpse['type'] == Lexer::T_DOT) {
1621
            $associationPathExpression = $this->AssociationPathExpression();
1622
1623
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1624
                $this->match(Lexer::T_AS);
1625
            }
1626
1627
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1628
            $identificationVariable      = $associationPathExpression->identificationVariable;
1629
            $field                       = $associationPathExpression->associationField;
1630
1631
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1632
            $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1633
1634
            // Building queryComponent
1635
            $joinQueryComponent = array(
1636
                'metadata'     => $targetClass,
1637
                'parent'       => $identificationVariable,
1638
                'relation'     => $class->getAssociationMapping($field),
1639
                'map'          => null,
1640
                'nestingLevel' => $this->nestingLevel,
1641
                'token'        => $this->lexer->lookahead
1642
            );
1643
1644
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1645
1646
            return new AST\SubselectIdentificationVariableDeclaration(
1647
                $associationPathExpression, $aliasIdentificationVariable
1648
            );
1649
        }
1650
        */
1651
1652 26
        return $this->IdentificationVariableDeclaration();
1653
    }
1654
1655
    /**
1656
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1657
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1658
     *          ["WITH" ConditionalExpression]
1659
     *
1660
     * @return \Doctrine\ORM\Query\AST\Join
1661
     */
1662 247
    public function Join()
1663
    {
1664
        // Check Join type
1665 247
        $joinType = AST\Join::JOIN_TYPE_INNER;
1666
1667
        switch (true) {
1668 247
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1669 54
                $this->match(Lexer::T_LEFT);
1670
1671 54
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1672
1673
                // Possible LEFT OUTER join
1674 54
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1675
                    $this->match(Lexer::T_OUTER);
1676
1677
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1678
                }
1679 54
                break;
1680
1681 197
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1682 20
                $this->match(Lexer::T_INNER);
1683 20
                break;
1684
1685
            default:
1686
                // Do nothing
1687
        }
1688
1689 247
        $this->match(Lexer::T_JOIN);
1690
1691 247
        $next            = $this->lexer->glimpse();
1692 247
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1693 245
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1694 245
        $join            = new AST\Join($joinType, $joinDeclaration);
1695
1696
        // Describe non-root join declaration
1697 245
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1698 19
            $joinDeclaration->isRoot = false;
1699
        }
1700
1701
        // Check for ad-hoc Join conditions
1702 245
        if ($adhocConditions) {
1703 21
            $this->match(Lexer::T_WITH);
1704
1705 21
            $join->conditionalExpression = $this->ConditionalExpression();
1706
        }
1707
1708 245
        return $join;
1709
    }
1710
1711
    /**
1712
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1713
     *
1714
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1715
     */
1716 670
    public function RangeVariableDeclaration()
1717
    {
1718 670
        $abstractSchemaName = $this->AbstractSchemaName();
1719
1720 669
        $this->validateAbstractSchemaName($abstractSchemaName);
1721
1722 654
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1723 5
            $this->match(Lexer::T_AS);
1724
        }
1725
1726 654
        $token = $this->lexer->lookahead;
1727 654
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1728 654
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1729
1730
        // Building queryComponent
1731
        $queryComponent = [
1732 654
            'metadata'     => $classMetadata,
1733
            'parent'       => null,
1734
            'relation'     => null,
1735
            'map'          => null,
1736 654
            'nestingLevel' => $this->nestingLevel,
1737 654
            'token'        => $token
1738
        ];
1739
1740 654
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1741
1742 654
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1743
    }
1744
1745
    /**
1746
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1747
     *
1748
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1749
     */
1750 228
    public function JoinAssociationDeclaration()
1751
    {
1752 228
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1753
1754 228
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1755 4
            $this->match(Lexer::T_AS);
1756
        }
1757
1758 228
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1759 226
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1760
1761 226
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1762 226
        $field                  = $joinAssociationPathExpression->associationField;
1763
1764 226
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1765 226
        $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1766
1767
        // Building queryComponent
1768
        $joinQueryComponent = [
1769 226
            'metadata'     => $targetClass,
1770 226
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1771 226
            'relation'     => $class->getAssociationMapping($field),
1772
            'map'          => null,
1773 226
            'nestingLevel' => $this->nestingLevel,
1774 226
            'token'        => $this->lexer->lookahead
1775
        ];
1776
1777 226
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1778
1779 226
        return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
1780
    }
1781
1782
    /**
1783
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1784
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1785
     *
1786
     * @return \Doctrine\ORM\Query\AST\PartialObjectExpression
1787
     */
1788 11
    public function PartialObjectExpression()
1789
    {
1790 11
        $this->match(Lexer::T_PARTIAL);
1791
1792 11
        $partialFieldSet = [];
1793
1794 11
        $identificationVariable = $this->IdentificationVariable();
1795
1796 11
        $this->match(Lexer::T_DOT);
1797 11
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1798 11
        $this->match(Lexer::T_IDENTIFIER);
1799
1800 11
        $field = $this->lexer->token['value'];
1801
1802
        // First field in partial expression might be embeddable property
1803 11
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1804 1
            $this->match(Lexer::T_DOT);
1805 1
            $this->match(Lexer::T_IDENTIFIER);
1806 1
            $field .= '.'.$this->lexer->token['value'];
1807
        }
1808
1809 11
        $partialFieldSet[] = $field;
1810
1811 11
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1812 9
            $this->match(Lexer::T_COMMA);
1813 9
            $this->match(Lexer::T_IDENTIFIER);
1814
1815 9
            $field = $this->lexer->token['value'];
1816
1817 9
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1818 2
                $this->match(Lexer::T_DOT);
1819 2
                $this->match(Lexer::T_IDENTIFIER);
1820 2
                $field .= '.'.$this->lexer->token['value'];
1821
            }
1822
1823 9
            $partialFieldSet[] = $field;
1824
        }
1825
1826 11
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1827
1828 11
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1829
1830
        // Defer PartialObjectExpression validation
1831 11
        $this->deferredPartialObjectExpressions[] = [
1832 11
            'expression'   => $partialObjectExpression,
1833 11
            'nestingLevel' => $this->nestingLevel,
1834 11
            'token'        => $this->lexer->token,
1835
        ];
1836
1837 11
        return $partialObjectExpression;
1838
    }
1839
1840
    /**
1841
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1842
     *
1843
     * @return \Doctrine\ORM\Query\AST\NewObjectExpression
1844
     */
1845 26
    public function NewObjectExpression()
1846
    {
1847 26
        $this->match(Lexer::T_NEW);
1848
1849 26
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1850 26
        $token = $this->lexer->token;
1851
1852 26
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1853
1854 26
        $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...
1855
1856 26
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1857 22
            $this->match(Lexer::T_COMMA);
1858
1859 22
            $args[] = $this->NewObjectArg();
1860
        }
1861
1862 26
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1863
1864 26
        $expression = new AST\NewObjectExpression($className, $args);
1865
1866
        // Defer NewObjectExpression validation
1867 26
        $this->deferredNewObjectExpressions[] = [
1868 26
            'token'        => $token,
1869 26
            'expression'   => $expression,
1870 26
            'nestingLevel' => $this->nestingLevel,
1871
        ];
1872
1873 26
        return $expression;
1874
    }
1875
1876
    /**
1877
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1878
     *
1879
     * @return mixed
1880
     */
1881 26
    public function NewObjectArg()
1882
    {
1883 26
        $token = $this->lexer->lookahead;
1884 26
        $peek  = $this->lexer->glimpse();
1885
1886 26
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1887 1
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1888 1
            $expression = $this->Subselect();
1889 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1890
1891 1
            return $expression;
1892
        }
1893
1894 26
        return $this->ScalarExpression();
1895
    }
1896
1897
    /**
1898
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1899
     *
1900
     * @return \Doctrine\ORM\Query\AST\IndexBy
1901
     */
1902 11
    public function IndexBy()
1903
    {
1904 11
        $this->match(Lexer::T_INDEX);
1905 11
        $this->match(Lexer::T_BY);
1906 11
        $pathExpr = $this->StateFieldPathExpression();
1907
1908
        // Add the INDEX BY info to the query component
1909 11
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1910
1911 11
        return new AST\IndexBy($pathExpr);
1912
    }
1913
1914
    /**
1915
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1916
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1917
     *                      InstanceOfExpression
1918
     *
1919
     * @return mixed One of the possible expressions or subexpressions.
1920
     */
1921 129
    public function ScalarExpression()
1922
    {
1923 129
        $lookahead = $this->lexer->lookahead['type'];
1924 129
        $peek      = $this->lexer->glimpse();
1925
1926
        switch (true) {
1927 129
            case ($lookahead === Lexer::T_INTEGER):
1928 126
            case ($lookahead === Lexer::T_FLOAT):
1929
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1930 126
            case ($lookahead === Lexer::T_MINUS):
1931 126
            case ($lookahead === Lexer::T_PLUS):
1932 15
                return $this->SimpleArithmeticExpression();
1933
1934 126
            case ($lookahead === Lexer::T_STRING):
1935 13
                return $this->StringPrimary();
1936
1937 124
            case ($lookahead === Lexer::T_TRUE):
1938 124
            case ($lookahead === Lexer::T_FALSE):
1939 3
                $this->match($lookahead);
1940
1941 3
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1942
1943 124
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
1944
                switch (true) {
1945
                    case $this->isMathOperator($peek):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->glimpse() on line 1924 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...
1946
                        // :param + u.value
1947
                        return $this->SimpleArithmeticExpression();
1948
                    default:
1949
                        return $this->InputParameter();
1950
                }
1951
1952 124
            case ($lookahead === Lexer::T_CASE):
1953 120
            case ($lookahead === Lexer::T_COALESCE):
1954 120
            case ($lookahead === Lexer::T_NULLIF):
1955
                // Since NULLIF and COALESCE can be identified as a function,
1956
                // we need to check these before checking for FunctionDeclaration
1957 8
                return $this->CaseExpression();
1958
1959 120
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1960 3
                return $this->SimpleArithmeticExpression();
1961
1962
            // this check must be done before checking for a filed path expression
1963 117
            case ($this->isFunction()):
1964 4
                $this->lexer->peek(); // "("
1965
1966
                switch (true) {
1967 4
                    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...
1968
                        // SUM(u.id) + COUNT(u.id)
1969
                        return $this->SimpleArithmeticExpression();
1970
1971
                    default:
1972
                        // IDENTITY(u)
1973 4
                        return $this->FunctionDeclaration();
1974
                }
1975
1976
                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...
1977
            // it is no function, so it must be a field path
1978 116
            case ($lookahead === Lexer::T_IDENTIFIER):
1979 116
                $this->lexer->peek(); // lookahead => '.'
1980 116
                $this->lexer->peek(); // lookahead => token after '.'
1981 116
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1982 116
                $this->lexer->resetPeek();
1983
1984 116
                if ($this->isMathOperator($peek)) {
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1981 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...
1985 6
                    return $this->SimpleArithmeticExpression();
1986
                }
1987
1988 112
                return $this->StateFieldPathExpression();
1989
1990
            default:
1991
                $this->syntaxError();
1992
        }
1993
    }
1994
1995
    /**
1996
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
1997
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
1998
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
1999
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2000
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2001
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2002
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2003
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2004
     *
2005
     * @return mixed One of the possible expressions or subexpressions.
2006
     */
2007 18
    public function CaseExpression()
2008
    {
2009 18
        $lookahead = $this->lexer->lookahead['type'];
2010
2011
        switch ($lookahead) {
2012 18
            case Lexer::T_NULLIF:
2013 5
                return $this->NullIfExpression();
2014
2015 15
            case Lexer::T_COALESCE:
2016 2
                return $this->CoalesceExpression();
2017
2018 13
            case Lexer::T_CASE:
2019 13
                $this->lexer->resetPeek();
2020 13
                $peek = $this->lexer->peek();
2021
2022 13
                if ($peek['type'] === Lexer::T_WHEN) {
2023 8
                    return $this->GeneralCaseExpression();
2024
                }
2025
2026 5
                return $this->SimpleCaseExpression();
2027
2028
            default:
2029
                // Do nothing
2030
                break;
2031
        }
2032
2033
        $this->syntaxError();
2034
    }
2035
2036
    /**
2037
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2038
     *
2039
     * @return \Doctrine\ORM\Query\AST\CoalesceExpression
2040
     */
2041 3
    public function CoalesceExpression()
2042
    {
2043 3
        $this->match(Lexer::T_COALESCE);
2044 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2045
2046
        // Process ScalarExpressions (1..N)
2047 3
        $scalarExpressions = [];
2048 3
        $scalarExpressions[] = $this->ScalarExpression();
2049
2050 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2051 3
            $this->match(Lexer::T_COMMA);
2052
2053 3
            $scalarExpressions[] = $this->ScalarExpression();
2054
        }
2055
2056 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2057
2058 3
        return new AST\CoalesceExpression($scalarExpressions);
2059
    }
2060
2061
    /**
2062
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2063
     *
2064
     * @return \Doctrine\ORM\Query\AST\NullIfExpression
2065
     */
2066 5
    public function NullIfExpression()
2067
    {
2068 5
        $this->match(Lexer::T_NULLIF);
2069 5
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2070
2071 5
        $firstExpression = $this->ScalarExpression();
2072 5
        $this->match(Lexer::T_COMMA);
2073 5
        $secondExpression = $this->ScalarExpression();
2074
2075 5
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2076
2077 5
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2078
    }
2079
2080
    /**
2081
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2082
     *
2083
     * @return \Doctrine\ORM\Query\AST\GeneralCaseExpression
2084
     */
2085 8
    public function GeneralCaseExpression()
2086
    {
2087 8
        $this->match(Lexer::T_CASE);
2088
2089
        // Process WhenClause (1..N)
2090 8
        $whenClauses = [];
2091
2092
        do {
2093 8
            $whenClauses[] = $this->WhenClause();
2094 8
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2095
2096 8
        $this->match(Lexer::T_ELSE);
2097 8
        $scalarExpression = $this->ScalarExpression();
2098 8
        $this->match(Lexer::T_END);
2099
2100 8
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2101
    }
2102
2103
    /**
2104
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2105
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2106
     *
2107
     * @return AST\SimpleCaseExpression
2108
     */
2109 5
    public function SimpleCaseExpression()
2110
    {
2111 5
        $this->match(Lexer::T_CASE);
2112 5
        $caseOperand = $this->StateFieldPathExpression();
2113
2114
        // Process SimpleWhenClause (1..N)
2115 5
        $simpleWhenClauses = [];
2116
2117
        do {
2118 5
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2119 5
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2120
2121 5
        $this->match(Lexer::T_ELSE);
2122 5
        $scalarExpression = $this->ScalarExpression();
2123 5
        $this->match(Lexer::T_END);
2124
2125 5
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2126
    }
2127
2128
    /**
2129
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2130
     *
2131
     * @return \Doctrine\ORM\Query\AST\WhenClause
2132
     */
2133 8
    public function WhenClause()
2134
    {
2135 8
        $this->match(Lexer::T_WHEN);
2136 8
        $conditionalExpression = $this->ConditionalExpression();
2137 8
        $this->match(Lexer::T_THEN);
2138
2139 8
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2140
    }
2141
2142
    /**
2143
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2144
     *
2145
     * @return \Doctrine\ORM\Query\AST\SimpleWhenClause
2146
     */
2147 5
    public function SimpleWhenClause()
2148
    {
2149 5
        $this->match(Lexer::T_WHEN);
2150 5
        $conditionalExpression = $this->ScalarExpression();
2151 5
        $this->match(Lexer::T_THEN);
2152
2153 5
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2154
    }
2155
2156
    /**
2157
     * SelectExpression ::= (
2158
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2159
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2160
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2161
     *
2162
     * @return \Doctrine\ORM\Query\AST\SelectExpression
2163
     */
2164 692
    public function SelectExpression()
2165
    {
2166 692
        $expression    = null;
2167 692
        $identVariable = null;
2168 692
        $peek          = $this->lexer->glimpse();
2169 692
        $lookaheadType = $this->lexer->lookahead['type'];
2170
2171
        switch (true) {
2172
            // ScalarExpression (u.name)
2173 692
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2174 91
                $expression = $this->ScalarExpression();
2175 91
                break;
2176
2177
            // IdentificationVariable (u)
2178 634
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2179 557
                $expression = $identVariable = $this->IdentificationVariable();
2180 557
                break;
2181
2182
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
2183 111
            case ($lookaheadType === Lexer::T_CASE):
2184 106
            case ($lookaheadType === Lexer::T_COALESCE):
2185 104
            case ($lookaheadType === Lexer::T_NULLIF):
2186 9
                $expression = $this->CaseExpression();
2187 9
                break;
2188
2189
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2190 102
            case ($this->isFunction()):
2191 42
                $this->lexer->peek(); // "("
2192
2193
                switch (true) {
2194 42
                    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...
2195
                        // SUM(u.id) + COUNT(u.id)
2196
                        $expression = $this->ScalarExpression();
2197
                        break;
2198
2199
                    default:
2200
                        // IDENTITY(u)
2201 42
                        $expression = $this->FunctionDeclaration();
2202 31
                        break;
2203
                }
2204
2205 31
                break;
2206
2207
            // PartialObjectExpression (PARTIAL u.{id, name})
2208 60
            case ($lookaheadType === Lexer::T_PARTIAL):
2209 11
                $expression    = $this->PartialObjectExpression();
2210 11
                $identVariable = $expression->identificationVariable;
2211 11
                break;
2212
2213
            // Subselect
2214 49
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2215 5
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2216 5
                $expression = $this->Subselect();
2217 5
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2218 5
                break;
2219
2220
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2221 44
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2222 41
            case ($lookaheadType === Lexer::T_INTEGER):
2223 39
            case ($lookaheadType === Lexer::T_STRING):
2224 30
            case ($lookaheadType === Lexer::T_FLOAT):
2225
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2226 30
            case ($lookaheadType === Lexer::T_MINUS):
2227 30
            case ($lookaheadType === Lexer::T_PLUS):
2228 15
                $expression = $this->SimpleArithmeticExpression();
2229 15
                break;
2230
2231
            // NewObjectExpression (New ClassName(id, name))
2232 29
            case ($lookaheadType === Lexer::T_NEW):
2233 26
                $expression = $this->NewObjectExpression();
2234 26
                break;
2235
2236
            default:
2237 3
                $this->syntaxError(
2238 3
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2239 3
                    $this->lexer->lookahead
2240
                );
2241
        }
2242
2243
        // [["AS"] ["HIDDEN"] AliasResultVariable]
2244 685
        $mustHaveAliasResultVariable = false;
2245
2246 685
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2247 66
            $this->match(Lexer::T_AS);
2248
2249 66
            $mustHaveAliasResultVariable = true;
2250
        }
2251
2252 685
        $hiddenAliasResultVariable = false;
2253
2254 685
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2255 2
            $this->match(Lexer::T_HIDDEN);
2256
2257 2
            $hiddenAliasResultVariable = true;
2258
        }
2259
2260 685
        $aliasResultVariable = null;
2261
2262 685
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2263 68
            $token = $this->lexer->lookahead;
2264 68
            $aliasResultVariable = $this->AliasResultVariable();
2265
2266
            // Include AliasResultVariable in query components.
2267 64
            $this->queryComponents[$aliasResultVariable] = [
2268 64
                'resultVariable' => $expression,
2269 64
                'nestingLevel'   => $this->nestingLevel,
2270 64
                'token'          => $token,
2271
            ];
2272
        }
2273
2274
        // AST
2275
2276 681
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2277
2278 681
        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...
2279 565
            $this->identVariableExpressions[$identVariable] = $expr;
2280
        }
2281
2282 681
        return $expr;
2283
    }
2284
2285
    /**
2286
     * SimpleSelectExpression ::= (
2287
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2288
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2289
     * ) [["AS"] AliasResultVariable]
2290
     *
2291
     * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
2292
     */
2293 27
    public function SimpleSelectExpression()
2294
    {
2295 27
        $peek = $this->lexer->glimpse();
2296
2297 27
        switch ($this->lexer->lookahead['type']) {
2298 27
            case Lexer::T_IDENTIFIER:
2299
                switch (true) {
2300 19
                    case ($peek['type'] === Lexer::T_DOT):
2301 16
                        $expression = $this->StateFieldPathExpression();
2302
2303 16
                        return new AST\SimpleSelectExpression($expression);
2304
2305 3
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2306 2
                        $expression = $this->IdentificationVariable();
2307
2308 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...
2309
2310 1
                    case ($this->isFunction()):
2311
                        // SUM(u.id) + COUNT(u.id)
2312 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...
2313
                            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...
2314
                        }
2315
                        // COUNT(u.id)
2316 1
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2317
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2318
                        }
2319
                        // IDENTITY(u)
2320 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...
2321
2322
                    default:
2323
                        // Do nothing
2324
                }
2325
                break;
2326
2327 8
            case Lexer::T_OPEN_PARENTHESIS:
2328
                if ($peek['type'] !== Lexer::T_SELECT) {
2329
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2330
                    $expression = $this->SimpleArithmeticExpression();
2331
2332
                    return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->SimpleArithmeticExpression() on line 2330 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...
2333
                }
2334
2335
                // Subselect
2336
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2337
                $expression = $this->Subselect();
2338
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2339
2340
                return new AST\SimpleSelectExpression($expression);
2341
2342
            default:
2343
                // Do nothing
2344
        }
2345
2346 8
        $this->lexer->peek();
2347
2348 8
        $expression = $this->ScalarExpression();
2349 7
        $expr       = new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->ScalarExpression() on line 2348 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...
2350
2351 7
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2352
            $this->match(Lexer::T_AS);
2353
        }
2354
2355 7
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2356
            $token = $this->lexer->lookahead;
2357
            $resultVariable = $this->AliasResultVariable();
2358
            $expr->fieldIdentificationVariable = $resultVariable;
2359
2360
            // Include AliasResultVariable in query components.
2361
            $this->queryComponents[$resultVariable] = [
2362
                'resultvariable' => $expr,
2363
                'nestingLevel'   => $this->nestingLevel,
2364
                'token'          => $token,
2365
            ];
2366
        }
2367
2368 7
        return $expr;
2369
    }
2370
2371
    /**
2372
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2373
     *
2374
     * @return \Doctrine\ORM\Query\AST\ConditionalExpression
2375
     */
2376 337
    public function ConditionalExpression()
2377
    {
2378 337
        $conditionalTerms = [];
2379 337
        $conditionalTerms[] = $this->ConditionalTerm();
2380
2381 333
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2382 16
            $this->match(Lexer::T_OR);
2383
2384 16
            $conditionalTerms[] = $this->ConditionalTerm();
2385
        }
2386
2387
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2388
        // if only one AST\ConditionalTerm is defined
2389 333
        if (count($conditionalTerms) == 1) {
2390 325
            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...
2391
        }
2392
2393 16
        return new AST\ConditionalExpression($conditionalTerms);
2394
    }
2395
2396
    /**
2397
     * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2398
     *
2399
     * @return \Doctrine\ORM\Query\AST\ConditionalTerm
2400
     */
2401 337
    public function ConditionalTerm()
2402
    {
2403 337
        $conditionalFactors = [];
2404 337
        $conditionalFactors[] = $this->ConditionalFactor();
2405
2406 333
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2407 30
            $this->match(Lexer::T_AND);
2408
2409 30
            $conditionalFactors[] = $this->ConditionalFactor();
2410
        }
2411
2412
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2413
        // if only one AST\ConditionalFactor is defined
2414 333
        if (count($conditionalFactors) == 1) {
2415 315
            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...
2416
        }
2417
2418 30
        return new AST\ConditionalTerm($conditionalFactors);
2419
    }
2420
2421
    /**
2422
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2423
     *
2424
     * @return \Doctrine\ORM\Query\AST\ConditionalFactor
2425
     */
2426 337
    public function ConditionalFactor()
2427
    {
2428 337
        $not = false;
2429
2430 337
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2431 6
            $this->match(Lexer::T_NOT);
2432
2433 6
            $not = true;
2434
        }
2435
2436 337
        $conditionalPrimary = $this->ConditionalPrimary();
2437
2438
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2439
        // if only one AST\ConditionalPrimary is defined
2440 333
        if ( ! $not) {
2441 331
            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...
2442
        }
2443
2444 6
        $conditionalFactor = new AST\ConditionalFactor($conditionalPrimary);
2445 6
        $conditionalFactor->not = $not;
2446
2447 6
        return $conditionalFactor;
2448
    }
2449
2450
    /**
2451
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2452
     *
2453
     * @return \Doctrine\ORM\Query\AST\ConditionalPrimary
2454
     */
2455 337
    public function ConditionalPrimary()
2456
    {
2457 337
        $condPrimary = new AST\ConditionalPrimary;
2458
2459 337
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2460 331
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2461
2462 328
            return $condPrimary;
2463
        }
2464
2465
        // Peek beyond the matching closing parenthesis ')'
2466 22
        $peek = $this->peekBeyondClosingParenthesis();
2467
2468 22
        if (in_array($peek['value'], ["=",  "<", "<=", "<>", ">", ">=", "!="]) ||
2469 19
            in_array($peek['type'], [Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS]) ||
2470 22
            $this->isMathOperator($peek)) {
2471 12
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2472
2473 11
            return $condPrimary;
2474
        }
2475
2476 18
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2477 18
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2478 17
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2479
2480 17
        return $condPrimary;
2481
    }
2482
2483
    /**
2484
     * SimpleConditionalExpression ::=
2485
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2486
     *      InExpression | NullComparisonExpression | ExistsExpression |
2487
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2488
     *      InstanceOfExpression
2489
     */
2490 337
    public function SimpleConditionalExpression()
2491
    {
2492 337
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2493 7
            return $this->ExistsExpression();
2494
        }
2495
2496 337
        $token      = $this->lexer->lookahead;
2497 337
        $peek       = $this->lexer->glimpse();
2498 337
        $lookahead  = $token;
2499
2500 337
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2501
            $token = $this->lexer->glimpse();
2502
        }
2503
2504 337
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2505
            // Peek beyond the matching closing parenthesis.
2506 320
            $beyond = $this->lexer->peek();
2507
2508 320
            switch ($peek['value']) {
2509 320
                case '(':
2510
                    // Peeks beyond the matched closing parenthesis.
2511 26
                    $token = $this->peekBeyondClosingParenthesis(false);
2512
2513 26
                    if ($token['type'] === Lexer::T_NOT) {
2514 3
                        $token = $this->lexer->peek();
2515
                    }
2516
2517 26
                    if ($token['type'] === Lexer::T_IS) {
2518 2
                        $lookahead = $this->lexer->peek();
2519
                    }
2520 26
                    break;
2521
2522
                default:
2523
                    // Peek beyond the PathExpression or InputParameter.
2524 300
                    $token = $beyond;
2525
2526 300
                    while ($token['value'] === '.') {
2527 265
                        $this->lexer->peek();
2528
2529 265
                        $token = $this->lexer->peek();
2530
                    }
2531
2532
                    // Also peek beyond a NOT if there is one.
2533 300
                    if ($token['type'] === Lexer::T_NOT) {
2534 11
                        $token = $this->lexer->peek();
2535
                    }
2536
2537
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2538 300
                    $lookahead = $this->lexer->peek();
2539
            }
2540
2541
            // Also peek beyond a NOT if there is one.
2542 320
            if ($lookahead['type'] === Lexer::T_NOT) {
2543 5
                $lookahead = $this->lexer->peek();
2544
            }
2545
2546 320
            $this->lexer->resetPeek();
2547
        }
2548
2549 337
        if ($token['type'] === Lexer::T_BETWEEN) {
2550 8
            return $this->BetweenExpression();
2551
        }
2552
2553 331
        if ($token['type'] === Lexer::T_LIKE) {
2554 13
            return $this->LikeExpression();
2555
        }
2556
2557 319
        if ($token['type'] === Lexer::T_IN) {
2558 34
            return $this->InExpression();
2559
        }
2560
2561 294
        if ($token['type'] === Lexer::T_INSTANCE) {
2562 12
            return $this->InstanceOfExpression();
2563
        }
2564
2565 282
        if ($token['type'] === Lexer::T_MEMBER) {
2566 7
            return $this->CollectionMemberExpression();
2567
        }
2568
2569 275
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2570 11
            return $this->NullComparisonExpression();
2571
        }
2572
2573 265
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2574 4
            return $this->EmptyCollectionComparisonExpression();
2575
        }
2576
2577 261
        return $this->ComparisonExpression();
2578
    }
2579
2580
    /**
2581
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2582
     *
2583
     * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression
2584
     */
2585 4
    public function EmptyCollectionComparisonExpression()
2586
    {
2587 4
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2588 4
            $this->CollectionValuedPathExpression()
2589
        );
2590 4
        $this->match(Lexer::T_IS);
2591
2592 4
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2593 2
            $this->match(Lexer::T_NOT);
2594 2
            $emptyCollectionCompExpr->not = true;
2595
        }
2596
2597 4
        $this->match(Lexer::T_EMPTY);
2598
2599 4
        return $emptyCollectionCompExpr;
2600
    }
2601
2602
    /**
2603
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2604
     *
2605
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2606
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2607
     *
2608
     * @return \Doctrine\ORM\Query\AST\CollectionMemberExpression
2609
     */
2610 7
    public function CollectionMemberExpression()
2611
    {
2612 7
        $not        = false;
2613 7
        $entityExpr = $this->EntityExpression();
2614
2615 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2616
            $this->match(Lexer::T_NOT);
2617
2618
            $not = true;
2619
        }
2620
2621 7
        $this->match(Lexer::T_MEMBER);
2622
2623 7
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2624 7
            $this->match(Lexer::T_OF);
2625
        }
2626
2627 7
        $collMemberExpr = new AST\CollectionMemberExpression(
2628 7
            $entityExpr, $this->CollectionValuedPathExpression()
2629
        );
2630 7
        $collMemberExpr->not = $not;
2631
2632 7
        return $collMemberExpr;
2633
    }
2634
2635
    /**
2636
     * Literal ::= string | char | integer | float | boolean
2637
     *
2638
     * @return \Doctrine\ORM\Query\AST\Literal
2639
     */
2640 146
    public function Literal()
2641
    {
2642 146
        switch ($this->lexer->lookahead['type']) {
2643 146
            case Lexer::T_STRING:
2644 42
                $this->match(Lexer::T_STRING);
2645
2646 42
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2647 109
            case Lexer::T_INTEGER:
2648 9
            case Lexer::T_FLOAT:
2649 101
                $this->match(
2650 101
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2651
                );
2652
2653 101
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2654 8
            case Lexer::T_TRUE:
2655 4
            case Lexer::T_FALSE:
2656 8
                $this->match(
2657 8
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2658
                );
2659
2660 8
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2661
            default:
2662
                $this->syntaxError('Literal');
2663
        }
2664
    }
2665
2666
    /**
2667
     * InParameter ::= Literal | InputParameter
2668
     *
2669
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2670
     */
2671 25
    public function InParameter()
2672
    {
2673 25
        if ($this->lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) {
2674 13
            return $this->InputParameter();
2675
        }
2676
2677 12
        return $this->Literal();
2678
    }
2679
2680
    /**
2681
     * InputParameter ::= PositionalParameter | NamedParameter
2682
     *
2683
     * @return \Doctrine\ORM\Query\AST\InputParameter
2684
     */
2685 161
    public function InputParameter()
2686
    {
2687 161
        $this->match(Lexer::T_INPUT_PARAMETER);
2688
2689 161
        return new AST\InputParameter($this->lexer->token['value']);
2690
    }
2691
2692
    /**
2693
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2694
     *
2695
     * @return \Doctrine\ORM\Query\AST\ArithmeticExpression
2696
     */
2697 294
    public function ArithmeticExpression()
2698
    {
2699 294
        $expr = new AST\ArithmeticExpression;
2700
2701 294
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2702 15
            $peek = $this->lexer->glimpse();
2703
2704 15
            if ($peek['type'] === Lexer::T_SELECT) {
2705 3
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2706 3
                $expr->subselect = $this->Subselect();
2707 2
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2708
2709 2
                return $expr;
2710
            }
2711
        }
2712
2713 293
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2714
2715 293
        return $expr;
2716
    }
2717
2718
    /**
2719
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2720
     *
2721
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2722
     */
2723 364
    public function SimpleArithmeticExpression()
2724
    {
2725 364
        $terms = [];
2726 364
        $terms[] = $this->ArithmeticTerm();
2727
2728 364
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2729 13
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2730
2731 13
            $terms[] = $this->lexer->token['value'];
2732 13
            $terms[] = $this->ArithmeticTerm();
2733
        }
2734
2735
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2736
        // if only one AST\ArithmeticTerm is defined
2737 364
        if (count($terms) == 1) {
2738 359
            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 2738 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...
2739
        }
2740
2741 13
        return new AST\SimpleArithmeticExpression($terms);
2742
    }
2743
2744
    /**
2745
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2746
     *
2747
     * @return \Doctrine\ORM\Query\AST\ArithmeticTerm
2748
     */
2749 364
    public function ArithmeticTerm()
2750
    {
2751 364
        $factors = [];
2752 364
        $factors[] = $this->ArithmeticFactor();
2753
2754 364
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2755 43
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2756
2757 43
            $factors[] = $this->lexer->token['value'];
2758 43
            $factors[] = $this->ArithmeticFactor();
2759
        }
2760
2761
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2762
        // if only one AST\ArithmeticFactor is defined
2763 364
        if (count($factors) == 1) {
2764 336
            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 2764 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticTerm of type Doctrine\ORM\Query\AST\ArithmeticTerm|null.
Loading history...
2765
        }
2766
2767 43
        return new AST\ArithmeticTerm($factors);
2768
    }
2769
2770
    /**
2771
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2772
     *
2773
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2774
     */
2775 364
    public function ArithmeticFactor()
2776
    {
2777 364
        $sign = null;
2778
2779 364
        if (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2780 3
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2781 3
            $sign = $isPlus;
2782
        }
2783
2784 364
        $primary = $this->ArithmeticPrimary();
2785
2786
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2787
        // if only one AST\ArithmeticPrimary is defined
2788 364
        if ($sign === null) {
2789 363
            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...
2790
        }
2791
2792 3
        return new AST\ArithmeticFactor($primary, $sign);
2793
    }
2794
2795
    /**
2796
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2797
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2798
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2799
     *          | InputParameter | CaseExpression
2800
     */
2801 368
    public function ArithmeticPrimary()
2802
    {
2803 368
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2804 20
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2805
2806 20
            $expr = $this->SimpleArithmeticExpression();
2807
2808 20
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2809
2810 20
            return new AST\ParenthesisExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr defined by $this->SimpleArithmeticExpression() on line 2806 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...
2811
        }
2812
2813 368
        switch ($this->lexer->lookahead['type']) {
2814 368
            case Lexer::T_COALESCE:
2815 368
            case Lexer::T_NULLIF:
2816 368
            case Lexer::T_CASE:
2817 3
                return $this->CaseExpression();
2818
2819 368
            case Lexer::T_IDENTIFIER:
2820 338
                $peek = $this->lexer->glimpse();
2821
2822 338
                if ($peek['value'] == '(') {
2823 27
                    return $this->FunctionDeclaration();
2824
                }
2825
2826 319
                if ($peek['value'] == '.') {
2827 312
                    return $this->SingleValuedPathExpression();
2828
                }
2829
2830 26
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2831 2
                    return $this->ResultVariable();
2832
                }
2833
2834 24
                return $this->StateFieldPathExpression();
2835
2836 274
            case Lexer::T_INPUT_PARAMETER:
2837 143
                return $this->InputParameter();
2838
2839
            default:
2840 137
                $peek = $this->lexer->glimpse();
2841
2842 137
                if ($peek['value'] == '(') {
2843 1
                    return $this->FunctionDeclaration();
2844
                }
2845
2846 136
                return $this->Literal();
2847
        }
2848
    }
2849
2850
    /**
2851
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2852
     *
2853
     * @return \Doctrine\ORM\Query\AST\Subselect |
2854
     *         string
2855
     */
2856 13
    public function StringExpression()
2857
    {
2858 13
        $peek = $this->lexer->glimpse();
2859
2860
        // Subselect
2861 13
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
2862
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2863
            $expr = $this->Subselect();
2864
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2865
2866
            return $expr;
2867
        }
2868
2869
        // ResultVariable (string)
2870 13
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2871 13
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2872 1
            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...
2873
        }
2874
2875 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...mpleCaseExpression|null) 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...
2876
    }
2877
2878
    /**
2879
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2880
     */
2881 49
    public function StringPrimary()
2882
    {
2883 49
        $lookaheadType = $this->lexer->lookahead['type'];
2884
2885
        switch ($lookaheadType) {
2886 49
            case Lexer::T_IDENTIFIER:
2887 31
                $peek = $this->lexer->glimpse();
2888
2889 31
                if ($peek['value'] == '.') {
2890 31
                    return $this->StateFieldPathExpression();
2891
                }
2892
2893 8
                if ($peek['value'] == '(') {
2894
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2895 8
                    return $this->FunctionDeclaration();
2896
                }
2897
2898
                $this->syntaxError("'.' or '('");
2899
                break;
2900
2901 31
            case Lexer::T_STRING:
2902 31
                $this->match(Lexer::T_STRING);
2903
2904 31
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2905
2906 2
            case Lexer::T_INPUT_PARAMETER:
2907 2
                return $this->InputParameter();
2908
2909
            case Lexer::T_CASE:
2910
            case Lexer::T_COALESCE:
2911
            case Lexer::T_NULLIF:
2912
                return $this->CaseExpression();
2913
        }
2914
2915
        $this->syntaxError(
2916
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2917
        );
2918
    }
2919
2920
    /**
2921
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2922
     *
2923
     * @return \Doctrine\ORM\Query\AST\PathExpression |
2924
     *         \Doctrine\ORM\Query\AST\SimpleEntityExpression
2925
     */
2926 7
    public function EntityExpression()
2927
    {
2928 7
        $glimpse = $this->lexer->glimpse();
2929
2930 7
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2931 1
            return $this->SingleValuedAssociationPathExpression();
2932
        }
2933
2934 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 2934 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::EntityExpression of type Doctrine\ORM\Query\AST\PathExpression.
Loading history...
2935
    }
2936
2937
    /**
2938
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2939
     *
2940
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2941
     */
2942 6
    public function SimpleEntityExpression()
2943
    {
2944 6
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2945 5
            return $this->InputParameter();
2946
        }
2947
2948 1
        return $this->StateFieldPathExpression();
2949
    }
2950
2951
    /**
2952
     * AggregateExpression ::=
2953
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2954
     *
2955
     * @return \Doctrine\ORM\Query\AST\AggregateExpression
2956
     */
2957 15
    public function AggregateExpression()
2958
    {
2959 15
        $lookaheadType = $this->lexer->lookahead['type'];
2960 15
        $isDistinct = false;
2961
2962 15
        if ( ! in_array($lookaheadType, [Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM])) {
2963
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2964
        }
2965
2966 15
        $this->match($lookaheadType);
2967 15
        $functionName = $this->lexer->token['value'];
2968 15
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2969
2970 15
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
2971
            $this->match(Lexer::T_DISTINCT);
2972
            $isDistinct = true;
2973
        }
2974
2975 15
        $pathExp = $this->SimpleArithmeticExpression();
2976
2977 15
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2978
2979 15
        return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
0 ignored issues
show
Bug introduced by
It seems like $pathExp defined by $this->SimpleArithmeticExpression() on line 2975 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...
2980
    }
2981
2982
    /**
2983
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
2984
     *
2985
     * @return \Doctrine\ORM\Query\AST\QuantifiedExpression
2986
     */
2987 3
    public function QuantifiedExpression()
2988
    {
2989 3
        $lookaheadType = $this->lexer->lookahead['type'];
2990 3
        $value = $this->lexer->lookahead['value'];
2991
2992 3
        if ( ! in_array($lookaheadType, [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME])) {
2993
            $this->syntaxError('ALL, ANY or SOME');
2994
        }
2995
2996 3
        $this->match($lookaheadType);
2997 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2998
2999 3
        $qExpr = new AST\QuantifiedExpression($this->Subselect());
3000 3
        $qExpr->type = $value;
3001
3002 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3003
3004 3
        return $qExpr;
3005
    }
3006
3007
    /**
3008
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3009
     *
3010
     * @return \Doctrine\ORM\Query\AST\BetweenExpression
3011
     */
3012 8
    public function BetweenExpression()
3013
    {
3014 8
        $not = false;
3015 8
        $arithExpr1 = $this->ArithmeticExpression();
3016
3017 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3018 3
            $this->match(Lexer::T_NOT);
3019 3
            $not = true;
3020
        }
3021
3022 8
        $this->match(Lexer::T_BETWEEN);
3023 8
        $arithExpr2 = $this->ArithmeticExpression();
3024 8
        $this->match(Lexer::T_AND);
3025 8
        $arithExpr3 = $this->ArithmeticExpression();
3026
3027 8
        $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3028 8
        $betweenExpr->not = $not;
3029
3030 8
        return $betweenExpr;
3031
    }
3032
3033
    /**
3034
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3035
     *
3036
     * @return \Doctrine\ORM\Query\AST\ComparisonExpression
3037
     */
3038 261
    public function ComparisonExpression()
3039
    {
3040 261
        $this->lexer->glimpse();
3041
3042 261
        $leftExpr  = $this->ArithmeticExpression();
3043 259
        $operator  = $this->ComparisonOperator();
3044 259
        $rightExpr = ($this->isNextAllAnySome())
3045 3
            ? $this->QuantifiedExpression()
3046 259
            : $this->ArithmeticExpression();
3047
3048 257
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3049
    }
3050
3051
    /**
3052
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3053
     *
3054
     * @return \Doctrine\ORM\Query\AST\InExpression
3055
     */
3056 34
    public function InExpression()
3057
    {
3058 34
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3059
3060 34
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3061 6
            $this->match(Lexer::T_NOT);
3062 6
            $inExpression->not = true;
3063
        }
3064
3065 34
        $this->match(Lexer::T_IN);
3066 34
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3067
3068 34
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3069 9
            $inExpression->subselect = $this->Subselect();
3070
        } else {
3071 25
            $literals = [];
3072 25
            $literals[] = $this->InParameter();
3073
3074 25
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3075 16
                $this->match(Lexer::T_COMMA);
3076 16
                $literals[] = $this->InParameter();
3077
            }
3078
3079 25
            $inExpression->literals = $literals;
3080
        }
3081
3082 33
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3083
3084 33
        return $inExpression;
3085
    }
3086
3087
    /**
3088
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3089
     *
3090
     * @return \Doctrine\ORM\Query\AST\InstanceOfExpression
3091
     */
3092 12
    public function InstanceOfExpression()
3093
    {
3094 12
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3095
3096 12
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3097 1
            $this->match(Lexer::T_NOT);
3098 1
            $instanceOfExpression->not = true;
3099
        }
3100
3101 12
        $this->match(Lexer::T_INSTANCE);
3102 12
        $this->match(Lexer::T_OF);
3103
3104 12
        $exprValues = [];
3105
3106 12
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3107 1
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3108
3109 1
            $exprValues[] = $this->InstanceOfParameter();
3110
3111 1
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3112 1
                $this->match(Lexer::T_COMMA);
3113
3114 1
                $exprValues[] = $this->InstanceOfParameter();
3115
            }
3116
3117 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3118
3119 1
            $instanceOfExpression->value = $exprValues;
3120
3121 1
            return $instanceOfExpression;
3122
        }
3123
3124 11
        $exprValues[] = $this->InstanceOfParameter();
3125
3126 11
        $instanceOfExpression->value = $exprValues;
3127
3128 11
        return $instanceOfExpression;
3129
    }
3130
3131
    /**
3132
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3133
     *
3134
     * @return mixed
3135
     */
3136 12
    public function InstanceOfParameter()
3137
    {
3138 12
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3139 5
            $this->match(Lexer::T_INPUT_PARAMETER);
3140
3141 5
            return new AST\InputParameter($this->lexer->token['value']);
3142
        }
3143
3144 7
        $abstractSchemaName = $this->AbstractSchemaName();
3145
3146 7
        $this->validateAbstractSchemaName($abstractSchemaName);
3147
3148 7
        return $abstractSchemaName;
3149
    }
3150
3151
    /**
3152
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3153
     *
3154
     * @return \Doctrine\ORM\Query\AST\LikeExpression
3155
     */
3156 13
    public function LikeExpression()
3157
    {
3158 13
        $stringExpr = $this->StringExpression();
3159 13
        $not = false;
3160
3161 13
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3162 3
            $this->match(Lexer::T_NOT);
3163 3
            $not = true;
3164
        }
3165
3166 13
        $this->match(Lexer::T_LIKE);
3167
3168 13
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3169 4
            $this->match(Lexer::T_INPUT_PARAMETER);
3170 4
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3171
        } else {
3172 10
            $stringPattern = $this->StringPrimary();
3173
        }
3174
3175 13
        $escapeChar = null;
3176
3177 13
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3178 2
            $this->match(Lexer::T_ESCAPE);
3179 2
            $this->match(Lexer::T_STRING);
3180
3181 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3182
        }
3183
3184 13
        $likeExpr = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $stringExpr defined by $this->StringExpression() on line 3158 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...
Bug introduced by
It seems like $stringPattern defined by $this->StringPrimary() on line 3172 can also be of type null 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...
3185 13
        $likeExpr->not = $not;
3186
3187 13
        return $likeExpr;
3188
    }
3189
3190
    /**
3191
     * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
3192
     *
3193
     * @return \Doctrine\ORM\Query\AST\NullComparisonExpression
3194
     */
3195 11
    public function NullComparisonExpression()
3196
    {
3197
        switch (true) {
3198 11
            case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER):
3199
                $this->match(Lexer::T_INPUT_PARAMETER);
3200
3201
                $expr = new AST\InputParameter($this->lexer->token['value']);
3202
                break;
3203
3204 11
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3205 1
                $expr = $this->NullIfExpression();
3206 1
                break;
3207
3208 11
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3209 1
                $expr = $this->CoalesceExpression();
3210 1
                break;
3211
3212 11
            case $this->isFunction():
3213 2
                $expr = $this->FunctionDeclaration();
3214 1
                break;
3215
3216
            default:
3217
                // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
3218 10
                $glimpse = $this->lexer->glimpse();
3219
3220 10
                if ($glimpse['type'] === Lexer::T_DOT) {
3221 9
                    $expr = $this->SingleValuedPathExpression();
3222
3223
                    // Leave switch statement
3224 9
                    break;
3225
                }
3226
3227 1
                $lookaheadValue = $this->lexer->lookahead['value'];
3228
3229
                // Validate existing component
3230 1
                if ( ! isset($this->queryComponents[$lookaheadValue])) {
3231
                    $this->semanticalError('Cannot add having condition on undefined result variable.');
3232
                }
3233
3234
                // Validate SingleValuedPathExpression (ie.: "product")
3235 1
                if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {
3236 1
                    $expr = $this->SingleValuedPathExpression();
3237 1
                    break;
3238
                }
3239
3240
                // Validating ResultVariable
3241
                if ( ! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3242
                    $this->semanticalError('Cannot add having condition on a non result variable.');
3243
                }
3244
3245
                $expr = $this->ResultVariable();
3246
                break;
3247
        }
3248
3249 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...
3250
3251 11
        $this->match(Lexer::T_IS);
3252
3253 11
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3254 3
            $this->match(Lexer::T_NOT);
3255
3256 3
            $nullCompExpr->not = true;
3257
        }
3258
3259 11
        $this->match(Lexer::T_NULL);
3260
3261 11
        return $nullCompExpr;
3262
    }
3263
3264
    /**
3265
     * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"
3266
     *
3267
     * @return \Doctrine\ORM\Query\AST\ExistsExpression
3268
     */
3269 7
    public function ExistsExpression()
3270
    {
3271 7
        $not = false;
3272
3273 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3274
            $this->match(Lexer::T_NOT);
3275
            $not = true;
3276
        }
3277
3278 7
        $this->match(Lexer::T_EXISTS);
3279 7
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3280
3281 7
        $existsExpression = new AST\ExistsExpression($this->Subselect());
3282 7
        $existsExpression->not = $not;
3283
3284 7
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3285
3286 7
        return $existsExpression;
3287
    }
3288
3289
    /**
3290
     * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!="
3291
     *
3292
     * @return string
3293
     */
3294 259
    public function ComparisonOperator()
3295
    {
3296 259
        switch ($this->lexer->lookahead['value']) {
3297 259
            case '=':
3298 221
                $this->match(Lexer::T_EQUALS);
3299
3300 221
                return '=';
3301
3302 45
            case '<':
3303 15
                $this->match(Lexer::T_LOWER_THAN);
3304 15
                $operator = '<';
3305
3306 15
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3307 3
                    $this->match(Lexer::T_EQUALS);
3308 3
                    $operator .= '=';
3309 12
                } else if ($this->lexer->isNextToken(Lexer::T_GREATER_THAN)) {
3310 3
                    $this->match(Lexer::T_GREATER_THAN);
3311 3
                    $operator .= '>';
3312
                }
3313
3314 15
                return $operator;
3315
3316 37
            case '>':
3317 31
                $this->match(Lexer::T_GREATER_THAN);
3318 31
                $operator = '>';
3319
3320 31
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3321 3
                    $this->match(Lexer::T_EQUALS);
3322 3
                    $operator .= '=';
3323
                }
3324
3325 31
                return $operator;
3326
3327 6
            case '!':
3328 6
                $this->match(Lexer::T_NEGATE);
3329 6
                $this->match(Lexer::T_EQUALS);
3330
3331 6
                return '<>';
3332
3333
            default:
3334
                $this->syntaxError('=, <, <=, <>, >, >=, !=');
3335
        }
3336
    }
3337
3338
    /**
3339
     * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
3340
     *
3341
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3342
     */
3343 71
    public function FunctionDeclaration()
3344
    {
3345 71
        $token = $this->lexer->lookahead;
3346 71
        $funcName = strtolower($token['value']);
3347
3348 71
        $customFunctionDeclaration = $this->CustomFunctionDeclaration();
3349
3350
        // Check for custom functions functions first!
3351
        switch (true) {
3352 71
            case $customFunctionDeclaration !== null:
3353 3
                return $customFunctionDeclaration;
3354
            
3355 68
            case (isset(self::$_STRING_FUNCTIONS[$funcName])):
3356 29
                return $this->FunctionsReturningStrings();
3357
3358 43
            case (isset(self::$_NUMERIC_FUNCTIONS[$funcName])):
3359 37
                return $this->FunctionsReturningNumerics();
3360
3361 7
            case (isset(self::$_DATETIME_FUNCTIONS[$funcName])):
3362 7
                return $this->FunctionsReturningDatetime();
3363
3364
            default:
3365
                $this->syntaxError('known function', $token);
3366
        }
3367
    }
3368
3369
    /**
3370
     * Helper function for FunctionDeclaration grammar rule.
3371
     *
3372
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3373
     */
3374 71
    private function CustomFunctionDeclaration()
3375
    {
3376 71
        $token = $this->lexer->lookahead;
3377 71
        $funcName = strtolower($token['value']);
3378
3379
        // Check for custom functions afterwards
3380 71
        $config = $this->em->getConfiguration();
3381
3382
        switch (true) {
3383 71
            case ($config->getCustomStringFunction($funcName) !== null):
3384 2
                return $this->CustomFunctionsReturningStrings();
3385
3386 70
            case ($config->getCustomNumericFunction($funcName) !== null):
3387 2
                return $this->CustomFunctionsReturningNumerics();
3388
3389 68
            case ($config->getCustomDatetimeFunction($funcName) !== null):
3390
                return $this->CustomFunctionsReturningDatetime();
3391
3392
            default:
3393 68
                return null;
3394
        }
3395
    }
3396
3397
    /**
3398
     * FunctionsReturningNumerics ::=
3399
     *      "LENGTH" "(" StringPrimary ")" |
3400
     *      "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" |
3401
     *      "ABS" "(" SimpleArithmeticExpression ")" |
3402
     *      "SQRT" "(" SimpleArithmeticExpression ")" |
3403
     *      "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3404
     *      "SIZE" "(" CollectionValuedPathExpression ")" |
3405
     *      "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3406
     *      "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3407
     *      "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
3408
     *
3409
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3410
     */
3411 37
    public function FunctionsReturningNumerics()
3412
    {
3413 37
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3414 37
        $funcClass     = self::$_NUMERIC_FUNCTIONS[$funcNameLower];
3415
3416 37
        $function = new $funcClass($funcNameLower);
3417 37
        $function->parse($this);
3418
3419 22
        return $function;
3420
    }
3421
3422
    /**
3423
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3424
     */
3425 2
    public function CustomFunctionsReturningNumerics()
3426
    {
3427
        // getCustomNumericFunction is case-insensitive
3428 2
        $functionName  = strtolower($this->lexer->lookahead['value']);
3429 2
        $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName);
3430
3431 2
        $function = is_string($functionClass)
3432 1
            ? new $functionClass($functionName)
3433 2
            : call_user_func($functionClass, $functionName);
3434
3435 2
        $function->parse($this);
3436
3437 2
        return $function;
3438
    }
3439
3440
    /**
3441
     * FunctionsReturningDateTime ::=
3442
     *     "CURRENT_DATE" |
3443
     *     "CURRENT_TIME" |
3444
     *     "CURRENT_TIMESTAMP" |
3445
     *     "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" |
3446
     *     "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"
3447
     *
3448
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3449
     */
3450 7
    public function FunctionsReturningDatetime()
3451
    {
3452 7
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3453 7
        $funcClass     = self::$_DATETIME_FUNCTIONS[$funcNameLower];
3454
3455 7
        $function = new $funcClass($funcNameLower);
3456 7
        $function->parse($this);
3457
3458 7
        return $function;
3459
    }
3460
3461
    /**
3462
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3463
     */
3464
    public function CustomFunctionsReturningDatetime()
3465
    {
3466
        // getCustomDatetimeFunction is case-insensitive
3467
        $functionName  = $this->lexer->lookahead['value'];
3468
        $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName);
3469
3470
        $function = is_string($functionClass)
3471
            ? new $functionClass($functionName)
3472
            : call_user_func($functionClass, $functionName);
3473
3474
        $function->parse($this);
3475
3476
        return $function;
3477
    }
3478
3479
    /**
3480
     * FunctionsReturningStrings ::=
3481
     *   "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" |
3482
     *   "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3483
     *   "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" |
3484
     *   "LOWER" "(" StringPrimary ")" |
3485
     *   "UPPER" "(" StringPrimary ")" |
3486
     *   "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"
3487
     *
3488
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3489
     */
3490 29
    public function FunctionsReturningStrings()
3491
    {
3492 29
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3493 29
        $funcClass     = self::$_STRING_FUNCTIONS[$funcNameLower];
3494
3495 29
        $function = new $funcClass($funcNameLower);
3496 29
        $function->parse($this);
3497
3498 29
        return $function;
3499
    }
3500
3501
    /**
3502
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3503
     */
3504 2
    public function CustomFunctionsReturningStrings()
3505
    {
3506
        // getCustomStringFunction is case-insensitive
3507 2
        $functionName  = $this->lexer->lookahead['value'];
3508 2
        $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName);
3509
3510 2
        $function = is_string($functionClass)
3511 1
            ? new $functionClass($functionName)
3512 2
            : call_user_func($functionClass, $functionName);
3513
3514 2
        $function->parse($this);
3515
3516 2
        return $function;
3517
    }
3518
}
3519