Completed
Pull Request — master (#5912)
by Reen
10:05
created

Parser::parse()   D

Complexity

Conditions 9
Paths 40

Size

Total Lines 45
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 10.4384

Importance

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

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

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

Loading history...
214 127
    }
215
216
    /**
217
     * Adds a custom tree walker for modifying the AST.
218
     *
219
     * @param string $className
220
     *
221
     * @return void
222
     */
223
    public function addCustomTreeWalker($className)
224
    {
225
        $this->customTreeWalkers[] = $className;
226
    }
227
228
    /**
229
     * Gets the lexer used by the parser.
230
     *
231
     * @return \Doctrine\ORM\Query\Lexer
232
     */
233 28
    public function getLexer()
234
    {
235 28
        return $this->lexer;
236
    }
237
238
    /**
239
     * Gets the ParserResult that is being filled with information during parsing.
240
     *
241
     * @return \Doctrine\ORM\Query\ParserResult
242
     */
243
    public function getParserResult()
244
    {
245
        return $this->parserResult;
246
    }
247
248
    /**
249
     * Gets the EntityManager used by the parser.
250
     *
251
     * @return \Doctrine\ORM\EntityManager
252
     */
253
    public function getEntityManager()
254
    {
255
        return $this->em;
256
    }
257
258
    /**
259
     * Parses and builds AST for the given Query.
260
     *
261
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
262
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
263
     *         \Doctrine\ORM\Query\AST\DeleteStatement
264
     */
265 816
    public function getAST()
266
    {
267
        // Parse & build AST
268 816
        $AST = $this->QueryLanguage();
269
270
        // Process any deferred validations of some nodes in the AST.
271
        // This also allows post-processing of the AST for modification purposes.
272 777
        $this->processDeferredIdentificationVariables();
273
274 775
        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...
275 11
            $this->processDeferredPartialObjectExpressions();
276
        }
277
278 773
        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...
279 582
            $this->processDeferredPathExpressions($AST);
280
        }
281
282 770
        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...
283 30
            $this->processDeferredResultVariables();
284
        }
285
286 770
        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...
287 28
            $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...
288
        }
289
290 766
        $this->processRootEntityAliasSelected();
291
292
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
293 765
        $this->fixIdentificationVariableOrder($AST);
294
295 765
        return $AST;
296
    }
297
298
    /**
299
     * Attempts to match the given token with the current lookahead token.
300
     *
301
     * If they match, updates the lookahead token; otherwise raises a syntax
302
     * error.
303
     *
304
     * @param int $token The token type.
305
     *
306
     * @return void
307
     *
308
     * @throws QueryException If the tokens don't match.
309
     */
310 827
    public function match($token)
311
    {
312 827
        $lookaheadType = $this->lexer->lookahead['type'];
313
314
        // Short-circuit on first condition, usually types match
315 827
        if ($lookaheadType !== $token) {
316
            // If parameter is not identifier (1-99) must be exact match
317 20
            if ($token < Lexer::T_IDENTIFIER) {
318 2
                $this->syntaxError($this->lexer->getLiteral($token));
319
            }
320
321
            // If parameter is keyword (200+) must be exact match
322 18
            if ($token > Lexer::T_IDENTIFIER) {
323 7
                $this->syntaxError($this->lexer->getLiteral($token));
324
            }
325
326
            // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
327 11
            if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
328 8
                $this->syntaxError($this->lexer->getLiteral($token));
329
            }
330
        }
331
332 820
        $this->lexer->moveNext();
333 820
    }
334
335
    /**
336
     * Frees this parser, enabling it to be reused.
337
     *
338
     * @param boolean $deep     Whether to clean peek and reset errors.
339
     * @param integer $position Position to reset.
340
     *
341
     * @return void
342
     */
343
    public function free($deep = false, $position = 0)
344
    {
345
        // WARNING! Use this method with care. It resets the scanner!
346
        $this->lexer->resetPosition($position);
347
348
        // Deep = true cleans peek and also any previously defined errors
349
        if ($deep) {
350
            $this->lexer->resetPeek();
351
        }
352
353
        $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...
354
        $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...
355
    }
356
357
    /**
358
     * Parses a query string.
359
     *
360
     * @return ParserResult
361
     */
362 816
    public function parse()
363
    {
364 816
        $AST = $this->getAST();
365
366 765
        if (($customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) !== false) {
367 93
            $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...
368
        }
369
370 765
        if (($customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER)) !== false) {
371 78
            $this->customOutputWalker = $customOutputWalker;
372
        }
373
374
        // Run any custom tree walkers over the AST
375 765
        if ($this->customTreeWalkers) {
376 92
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
377
378 92
            foreach ($this->customTreeWalkers as $walker) {
379 92
                $treeWalkerChain->addTreeWalker($walker);
380
            }
381
382
            switch (true) {
383 92
                case ($AST instanceof AST\UpdateStatement):
384
                    $treeWalkerChain->walkUpdateStatement($AST);
385
                    break;
386
387
                case ($AST instanceof AST\DeleteStatement):
388
                    $treeWalkerChain->walkDeleteStatement($AST);
389
                    break;
390
391
                case ($AST instanceof AST\SelectStatement):
392
                default:
393 92
                    $treeWalkerChain->walkSelectStatement($AST);
394
            }
395
396 86
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
397
        }
398
399 759
        $outputWalkerClass = $this->customOutputWalker ?: __NAMESPACE__ . '\SqlWalker';
400 759
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
401
402
        // Assign an SQL executor to the parser result
403 759
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
404
405 750
        return $this->parserResult;
406
    }
407
408
    /**
409
     * Fixes order of identification variables.
410
     *
411
     * They have to appear in the select clause in the same order as the
412
     * declarations (from ... x join ... y join ... z ...) appear in the query
413
     * as the hydration process relies on that order for proper operation.
414
     *
415
     * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
416
     *
417
     * @return void
418
     */
419 765
    private function fixIdentificationVariableOrder($AST)
420
    {
421 765
        if (count($this->identVariableExpressions) <= 1) {
422 590
            return;
423
        }
424
425 180
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
426 180
            if ( ! isset($this->identVariableExpressions[$dqlAlias])) {
427 8
                continue;
428
            }
429
430 180
            $expr = $this->identVariableExpressions[$dqlAlias];
431 180
            $key  = array_search($expr, $AST->selectClause->selectExpressions);
432
433 180
            unset($AST->selectClause->selectExpressions[$key]);
434
435 180
            $AST->selectClause->selectExpressions[] = $expr;
436
        }
437 180
    }
438
439
    /**
440
     * Generates a new syntax error.
441
     *
442
     * @param string     $expected Expected string.
443
     * @param array|null $token    Got token.
444
     *
445
     * @return void
446
     *
447
     * @throws \Doctrine\ORM\Query\QueryException
448
     */
449 17
    public function syntaxError($expected = '', $token = null)
450
    {
451 17
        if ($token === null) {
452 14
            $token = $this->lexer->lookahead;
453
        }
454
455 17
        $tokenPos = (isset($token['position'])) ? $token['position'] : '-1';
456
457 17
        $message  = "line 0, col {$tokenPos}: Error: ";
458 17
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
459 17
        $message .= ($this->lexer->lookahead === null) ? 'end of string.' : "'{$token['value']}'";
460
461 17
        throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL()));
462
    }
463
464
    /**
465
     * Generates a new semantical error.
466
     *
467
     * @param string     $message Optional message.
468
     * @param array|null $token   Optional token.
469
     *
470
     * @return void
471
     *
472
     * @throws \Doctrine\ORM\Query\QueryException
473
     */
474 33
    public function semanticalError($message = '', $token = null)
475
    {
476 33
        if ($token === null) {
477 2
            $token = $this->lexer->lookahead;
478
        }
479
480
        // Minimum exposed chars ahead of token
481 33
        $distance = 12;
482
483
        // Find a position of a final word to display in error string
484 33
        $dql    = $this->query->getDql();
485 33
        $length = strlen($dql);
486 33
        $pos    = $token['position'] + $distance;
487 33
        $pos    = strpos($dql, ' ', ($length > $pos) ? $pos : $length);
488 33
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
489
490 33
        $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1';
491 33
        $tokenStr = substr($dql, $token['position'], $length);
492
493
        // Building informative message
494 33
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
495
496 33
        throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL()));
497
    }
498
499
    /**
500
     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
501
     *
502
     * @param boolean $resetPeek Reset peek after finding the closing parenthesis.
503
     *
504
     * @return array
505
     */
506 157
    private function peekBeyondClosingParenthesis($resetPeek = true)
507
    {
508 157
        $token = $this->lexer->peek();
509 157
        $numUnmatched = 1;
510
511 157
        while ($numUnmatched > 0 && $token !== null) {
512 156
            switch ($token['type']) {
513 156
                case Lexer::T_OPEN_PARENTHESIS:
514 33
                    ++$numUnmatched;
515 33
                    break;
516
517 156
                case Lexer::T_CLOSE_PARENTHESIS:
518 156
                    --$numUnmatched;
519 156
                    break;
520
521
                default:
522
                    // Do nothing
523
            }
524
525 156
            $token = $this->lexer->peek();
526
        }
527
528 157
        if ($resetPeek) {
529 135
            $this->lexer->resetPeek();
530
        }
531
532 157
        return $token;
533
    }
534
535
    /**
536
     * Checks if the given token indicates a mathematical operator.
537
     *
538
     * @param array $token
539
     *
540
     * @return boolean TRUE if the token is a mathematical operator, FALSE otherwise.
541
     */
542 343
    private function isMathOperator($token)
543
    {
544 343
        return in_array($token['type'], array(Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY));
545
    }
546
547
    /**
548
     * Checks if the next-next (after lookahead) token starts a function.
549
     *
550
     * @return boolean TRUE if the next-next tokens start a function, FALSE otherwise.
551
     */
552 387
    private function isFunction()
553
    {
554 387
        $lookaheadType = $this->lexer->lookahead['type'];
555 387
        $peek          = $this->lexer->peek();
556
557 387
        $this->lexer->resetPeek();
558
559 387
        return ($lookaheadType >= Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_OPEN_PARENTHESIS);
560
    }
561
562
    /**
563
     * Checks whether the given token type indicates an aggregate function.
564
     *
565
     * @param int $tokenType
566
     *
567
     * @return boolean TRUE if the token type is an aggregate function, FALSE otherwise.
568
     */
569 125
    private function isAggregateFunction($tokenType)
570
    {
571 125
        return in_array($tokenType, array(Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT));
572
    }
573
574
    /**
575
     * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
576
     *
577
     * @return boolean
578
     */
579 292
    private function isNextAllAnySome()
580
    {
581 292
        return in_array($this->lexer->lookahead['type'], array(Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME));
582
    }
583
584
    /**
585
     * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
586
     * It must exist in query components list.
587
     *
588
     * @return void
589
     */
590 777
    private function processDeferredIdentificationVariables()
591
    {
592 777
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
593 765
            $identVariable = $deferredItem['expression'];
594
595
            // Check if IdentificationVariable exists in queryComponents
596 765
            if ( ! isset($this->queryComponents[$identVariable])) {
597 1
                $this->semanticalError(
598 1
                    "'$identVariable' is not defined.", $deferredItem['token']
599
                );
600
            }
601
602 765
            $qComp = $this->queryComponents[$identVariable];
603
604
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
605 765
            if ( ! isset($qComp['metadata'])) {
606
                $this->semanticalError(
607
                    "'$identVariable' does not point to a Class.", $deferredItem['token']
608
                );
609
            }
610
611
            // Validate if identification variable nesting level is lower or equal than the current one
612 765
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
613 1
                $this->semanticalError(
614 765
                    "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token']
615
                );
616
            }
617
        }
618 775
    }
619
620
    /**
621
     * Validates that the given <tt>NewObjectExpression</tt>.
622
     *
623
     * @param \Doctrine\ORM\Query\AST\SelectClause $AST
624
     *
625
     * @return void
626
     */
627 28
    private function processDeferredNewObjectExpressions($AST)
628
    {
629 28
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
630 28
            $expression     = $deferredItem['expression'];
631 28
            $token          = $deferredItem['token'];
632 28
            $className      = $expression->className;
633 28
            $args           = $expression->args;
634 28
            $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...
635 28
                ? $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName
636 28
                : null;
637
638
            // If the namespace is not given then assumes the first FROM entity namespace
639 28
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
640 11
                $namespace  = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
641 11
                $fqcn       = $namespace . '\\' . $className;
642
643 11
                if (class_exists($fqcn)) {
644 11
                    $expression->className  = $fqcn;
645 11
                    $className              = $fqcn;
646
                }
647
            }
648
649 28
            if ( ! class_exists($className)) {
650 1
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
651
            }
652
653 27
            $class = new \ReflectionClass($className);
654
655 27
            if ( ! $class->isInstantiable()) {
656 1
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
657
            }
658
659 26
            if ($class->getConstructor() === null) {
660 1
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
661
            }
662
663 25
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
664 25
                $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
665
            }
666
        }
667 24
    }
668
669
    /**
670
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
671
     * It must exist in query components list.
672
     *
673
     * @return void
674
     */
675 11
    private function processDeferredPartialObjectExpressions()
676
    {
677 11
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
678 11
            $expr = $deferredItem['expression'];
679 11
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
680
681 11
            foreach ($expr->partialFieldSet as $field) {
682 11
                if (isset($class->fieldMappings[$field])) {
683 10
                    continue;
684
                }
685
686 3
                if (isset($class->associationMappings[$field]) &&
687 3
                    $class->associationMappings[$field]['isOwningSide'] &&
688 3
                    $class->associationMappings[$field]['type'] & ClassMetadata::TO_ONE) {
689 2
                    continue;
690
                }
691
692 1
                $this->semanticalError(
693 1
                    "There is no mapped field named '$field' on class " . $class->name . ".", $deferredItem['token']
694
                );
695
            }
696
697 10
            if (array_intersect($class->identifier, $expr->partialFieldSet) != $class->identifier) {
698 1
                $this->semanticalError(
699 1
                    "The partial field selection of class " . $class->name . " must contain the identifier.",
700 10
                    $deferredItem['token']
701
                );
702
            }
703
        }
704 9
    }
705
706
    /**
707
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
708
     * It must exist in query components list.
709
     *
710
     * @return void
711
     */
712 30
    private function processDeferredResultVariables()
713
    {
714 30
        foreach ($this->deferredResultVariables as $deferredItem) {
715 30
            $resultVariable = $deferredItem['expression'];
716
717
            // Check if ResultVariable exists in queryComponents
718 30
            if ( ! isset($this->queryComponents[$resultVariable])) {
719
                $this->semanticalError(
720
                    "'$resultVariable' is not defined.", $deferredItem['token']
721
                );
722
            }
723
724 30
            $qComp = $this->queryComponents[$resultVariable];
725
726
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
727 30
            if ( ! isset($qComp['resultVariable'])) {
728
                $this->semanticalError(
729
                    "'$resultVariable' does not point to a ResultVariable.", $deferredItem['token']
730
                );
731
            }
732
733
            // Validate if identification variable nesting level is lower or equal than the current one
734 30
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
735
                $this->semanticalError(
736 30
                    "'$resultVariable' is used outside the scope of its declaration.", $deferredItem['token']
737
                );
738
            }
739
        }
740 30
    }
741
742
    /**
743
     * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
744
     *
745
     * AssociationPathExpression             ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
746
     * SingleValuedPathExpression            ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
747
     * StateFieldPathExpression              ::= IdentificationVariable "." StateField
748
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
749
     * CollectionValuedPathExpression        ::= IdentificationVariable "." CollectionValuedAssociationField
750
     *
751
     * @param mixed $AST
752
     *
753
     * @return void
754
     */
755 582
    private function processDeferredPathExpressions($AST)
0 ignored issues
show
Unused Code introduced by
The parameter $AST is not used and could be removed.

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

Loading history...
756
    {
757 582
        foreach ($this->deferredPathExpressions as $deferredItem) {
758 582
            $pathExpression = $deferredItem['expression'];
759
760 582
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
761 582
            $class = $qComp['metadata'];
762
763 582
            if (($field = $pathExpression->field) === null) {
764 38
                $field = $pathExpression->field = $class->identifier[0];
765
            }
766
767
            // Check if field or association exists
768 582
            if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) {
769 1
                $this->semanticalError(
770 1
                    'Class ' . $class->name . ' has no field or association named ' . $field,
771 1
                    $deferredItem['token']
772
                );
773
            }
774
775 581
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
776
777 581
            if (isset($class->associationMappings[$field])) {
778 88
                $assoc = $class->associationMappings[$field];
779
780 88
                $fieldType = ($assoc['type'] & ClassMetadata::TO_ONE)
781 66
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
782 88
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
783
            }
784
785
            // Validate if PathExpression is one of the expected types
786 581
            $expectedType = $pathExpression->expectedType;
787
788 581
            if ( ! ($expectedType & $fieldType)) {
789
                // We need to recognize which was expected type(s)
790 2
                $expectedStringTypes = array();
791
792
                // Validate state field type
793 2
                if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
794 1
                    $expectedStringTypes[] = 'StateFieldPathExpression';
795
                }
796
797
                // Validate single valued association (*-to-one)
798 2
                if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
799 2
                    $expectedStringTypes[] = 'SingleValuedAssociationField';
800
                }
801
802
                // Validate single valued association (*-to-many)
803 2
                if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
804
                    $expectedStringTypes[] = 'CollectionValuedAssociationField';
805
                }
806
807
                // Build the error message
808 2
                $semanticalError  = 'Invalid PathExpression. ';
809 2
                $semanticalError .= (count($expectedStringTypes) == 1)
810 1
                    ? 'Must be a ' . $expectedStringTypes[0] . '.'
811 2
                    : implode(' or ', $expectedStringTypes) . ' expected.';
812
813 2
                $this->semanticalError($semanticalError, $deferredItem['token']);
814
            }
815
816
            // We need to force the type in PathExpression
817 579
            $pathExpression->type = $fieldType;
818
        }
819 579
    }
820
821
    /**
822
     * @return void
823
     */
824 766
    private function processRootEntityAliasSelected()
825
    {
826 766
        if ( ! count($this->identVariableExpressions)) {
827 223
            return;
828
        }
829
830 553
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
831 553
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
832 553
                return;
833
            }
834
        }
835
836 1
        $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
837
    }
838
839
    /**
840
     * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
841
     *
842
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
843
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
844
     *         \Doctrine\ORM\Query\AST\DeleteStatement
845
     */
846 816
    public function QueryLanguage()
847
    {
848 816
        $this->lexer->moveNext();
849
850 816
        switch ($this->lexer->lookahead['type']) {
851 816
            case Lexer::T_SELECT:
852 750
                $statement = $this->SelectStatement();
853 715
                break;
854
855 72
            case Lexer::T_UPDATE:
856 32
                $statement = $this->UpdateStatement();
857 32
                break;
858
859 43
            case Lexer::T_DELETE:
860 42
                $statement = $this->DeleteStatement();
861 41
                break;
862
863
            default:
864 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
865
                break;
866
        }
867
868
        // Check for end of string
869 780
        if ($this->lexer->lookahead !== null) {
870 3
            $this->syntaxError('end of string');
871
        }
872
873 777
        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...
874
    }
875
876
    /**
877
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
878
     *
879
     * @return \Doctrine\ORM\Query\AST\SelectStatement
880
     */
881 750
    public function SelectStatement()
882
    {
883 750
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
884
885 719
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
886 716
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
887 715
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
888 715
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
889
890 715
        return $selectStatement;
891
    }
892
893
    /**
894
     * UpdateStatement ::= UpdateClause [WhereClause]
895
     *
896
     * @return \Doctrine\ORM\Query\AST\UpdateStatement
897
     */
898 32
    public function UpdateStatement()
899
    {
900 32
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
901
902 32
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
903
904 32
        return $updateStatement;
905
    }
906
907
    /**
908
     * DeleteStatement ::= DeleteClause [WhereClause]
909
     *
910
     * @return \Doctrine\ORM\Query\AST\DeleteStatement
911
     */
912 42
    public function DeleteStatement()
913
    {
914 42
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
915
916 41
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
917
918 41
        return $deleteStatement;
919
    }
920
921
    /**
922
     * IdentificationVariable ::= identifier
923
     *
924
     * @return string
925
     */
926 793
    public function IdentificationVariable()
927
    {
928 793
        $this->match(Lexer::T_IDENTIFIER);
929
930 793
        $identVariable = $this->lexer->token['value'];
931
932 793
        $this->deferredIdentificationVariables[] = array(
933 793
            'expression'   => $identVariable,
934 793
            'nestingLevel' => $this->nestingLevel,
935 793
            'token'        => $this->lexer->token,
936
        );
937
938 793
        return $identVariable;
939
    }
940
941
    /**
942
     * AliasIdentificationVariable = identifier
943
     *
944
     * @return string
945
     */
946 787
    public function AliasIdentificationVariable()
947
    {
948 787
        $this->match(Lexer::T_IDENTIFIER);
949
950 787
        $aliasIdentVariable = $this->lexer->token['value'];
951 787
        $exists = isset($this->queryComponents[$aliasIdentVariable]);
952
953 787
        if ($exists) {
954 2
            $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->lexer->token);
955
        }
956
957 787
        return $aliasIdentVariable;
958
    }
959
960
    /**
961
     * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier
962
     *
963
     * @return string
964
     */
965 807
    public function AbstractSchemaName()
966
    {
967 807
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
968 790
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
969
970 790
            $schemaName = $this->lexer->token['value'];
971 28
        } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
972 19
            $this->match(Lexer::T_IDENTIFIER);
973
974 19
            $schemaName = $this->lexer->token['value'];
975
        } else {
976 10
            $this->match(Lexer::T_ALIASED_NAME);
977
978 10
            list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']);
979
980 10
            $schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
981
        }
982
983 807
        return $schemaName;
984
    }
985
986
    /**
987
     * Validates an AbstractSchemaName, making sure the class exists.
988
     *
989
     * @param string $schemaName The name to validate.
990
     *
991
     * @throws QueryException if the name does not exist.
992
     */
993 802
    private function validateAbstractSchemaName($schemaName)
994
    {
995 802
        if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {
996 16
            $this->semanticalError("Class '$schemaName' is not defined.", $this->lexer->token);
997
        }
998 787
    }
999
1000
    /**
1001
     * AliasResultVariable ::= identifier
1002
     *
1003
     * @return string
1004
     */
1005 119
    public function AliasResultVariable()
1006
    {
1007 119
        $this->match(Lexer::T_IDENTIFIER);
1008
1009 115
        $resultVariable = $this->lexer->token['value'];
1010 115
        $exists = isset($this->queryComponents[$resultVariable]);
1011
1012 115
        if ($exists) {
1013 2
            $this->semanticalError("'$resultVariable' is already defined.", $this->lexer->token);
1014
        }
1015
1016 115
        return $resultVariable;
1017
    }
1018
1019
    /**
1020
     * ResultVariable ::= identifier
1021
     *
1022
     * @return string
1023
     */
1024 30
    public function ResultVariable()
1025
    {
1026 30
        $this->match(Lexer::T_IDENTIFIER);
1027
1028 30
        $resultVariable = $this->lexer->token['value'];
1029
1030
        // Defer ResultVariable validation
1031 30
        $this->deferredResultVariables[] = array(
1032 30
            'expression'   => $resultVariable,
1033 30
            'nestingLevel' => $this->nestingLevel,
1034 30
            'token'        => $this->lexer->token,
1035
        );
1036
1037 30
        return $resultVariable;
1038
    }
1039
1040
    /**
1041
     * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
1042
     *
1043
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1044
     */
1045 254
    public function JoinAssociationPathExpression()
1046
    {
1047 254
        $identVariable = $this->IdentificationVariable();
1048
1049 254
        if ( ! isset($this->queryComponents[$identVariable])) {
1050
            $this->semanticalError(
1051
                'Identification Variable ' . $identVariable .' used in join path expression but was not defined before.'
1052
            );
1053
        }
1054
1055 254
        $this->match(Lexer::T_DOT);
1056 254
        $this->match(Lexer::T_IDENTIFIER);
1057
1058 254
        $field = $this->lexer->token['value'];
1059
1060
        // Validate association field
1061 254
        $qComp = $this->queryComponents[$identVariable];
1062 254
        $class = $qComp['metadata'];
1063
1064 254
        if ( ! $class->hasAssociation($field)) {
1065
            $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
1066
        }
1067
1068 254
        return new AST\JoinAssociationPathExpression($identVariable, $field);
1069
    }
1070
1071
    /**
1072
     * Parses an arbitrary path expression and defers semantical validation
1073
     * based on expected types.
1074
     *
1075
     * PathExpression ::= IdentificationVariable {"." identifier}*
1076
     *
1077
     * @param integer $expectedTypes
1078
     *
1079
     * @return \Doctrine\ORM\Query\AST\PathExpression
1080
     */
1081 592
    public function PathExpression($expectedTypes)
1082
    {
1083 592
        $identVariable = $this->IdentificationVariable();
1084 592
        $field = null;
1085
1086 592
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1087 586
            $this->match(Lexer::T_DOT);
1088 586
            $this->match(Lexer::T_IDENTIFIER);
1089
1090 586
            $field = $this->lexer->token['value'];
1091
1092 586
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1093 2
                $this->match(Lexer::T_DOT);
1094 2
                $this->match(Lexer::T_IDENTIFIER);
1095 2
                $field .= '.'.$this->lexer->token['value'];
1096
            }
1097
        }
1098
1099
        // Creating AST node
1100 592
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1101
1102
        // Defer PathExpression validation if requested to be deferred
1103 592
        $this->deferredPathExpressions[] = array(
1104 592
            'expression'   => $pathExpr,
1105 592
            'nestingLevel' => $this->nestingLevel,
1106 592
            'token'        => $this->lexer->token,
1107
        );
1108
1109 592
        return $pathExpr;
1110
    }
1111
1112
    /**
1113
     * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
1114
     *
1115
     * @return \Doctrine\ORM\Query\AST\PathExpression
1116
     */
1117
    public function AssociationPathExpression()
1118
    {
1119
        return $this->PathExpression(
1120
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
1121
            AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
1122
        );
1123
    }
1124
1125
    /**
1126
     * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
1127
     *
1128
     * @return \Doctrine\ORM\Query\AST\PathExpression
1129
     */
1130 500
    public function SingleValuedPathExpression()
1131
    {
1132 500
        return $this->PathExpression(
1133 500
            AST\PathExpression::TYPE_STATE_FIELD |
1134 500
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1135
        );
1136
    }
1137
1138
    /**
1139
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1140
     *
1141
     * @return \Doctrine\ORM\Query\AST\PathExpression
1142
     */
1143 202
    public function StateFieldPathExpression()
1144
    {
1145 202
        return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
1146
    }
1147
1148
    /**
1149
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
1150
     *
1151
     * @return \Doctrine\ORM\Query\AST\PathExpression
1152
     */
1153 9
    public function SingleValuedAssociationPathExpression()
1154
    {
1155 9
        return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
1156
    }
1157
1158
    /**
1159
     * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
1160
     *
1161
     * @return \Doctrine\ORM\Query\AST\PathExpression
1162
     */
1163 22
    public function CollectionValuedPathExpression()
1164
    {
1165 22
        return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
1166
    }
1167
1168
    /**
1169
     * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
1170
     *
1171
     * @return \Doctrine\ORM\Query\AST\SelectClause
1172
     */
1173 750
    public function SelectClause()
1174
    {
1175 750
        $this->match(Lexer::T_SELECT);
1176
1177
        // Check for DISTINCT
1178 750
        if ($isDistinct = $this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1179 6
            $this->match(Lexer::T_DISTINCT);
1180
        }
1181
1182
        // Process SelectExpressions (1..N)
1183 750
        $selectExpressions = array();
1184 750
        $selectExpressions[] = $this->SelectExpression();
1185
1186 742
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1187 282
            $this->match(Lexer::T_COMMA);
1188
1189 282
            $selectExpressions[] = $this->SelectExpression();
1190
        }
1191
1192 741
        return new AST\SelectClause($selectExpressions, $isDistinct);
1193
    }
1194
1195
    /**
1196
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1197
     *
1198
     * @return \Doctrine\ORM\Query\AST\SimpleSelectClause
1199
     */
1200 48
    public function SimpleSelectClause()
1201
    {
1202 48
        $this->match(Lexer::T_SELECT);
1203
1204 48
        if ($isDistinct = $this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1205
            $this->match(Lexer::T_DISTINCT);
1206
        }
1207
1208 48
        return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1209
    }
1210
1211
    /**
1212
     * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1213
     *
1214
     * @return \Doctrine\ORM\Query\AST\UpdateClause
1215
     */
1216 32
    public function UpdateClause()
1217
    {
1218 32
        $this->match(Lexer::T_UPDATE);
1219
1220 32
        $token = $this->lexer->lookahead;
1221 32
        $abstractSchemaName = $this->AbstractSchemaName();
1222
1223 32
        $this->validateAbstractSchemaName($abstractSchemaName);
1224
1225 32
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1226 2
            $this->match(Lexer::T_AS);
1227
        }
1228
1229 32
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1230
1231 32
        $class = $this->em->getClassMetadata($abstractSchemaName);
1232
1233
        // Building queryComponent
1234
        $queryComponent = array(
1235 32
            'metadata'     => $class,
1236
            'parent'       => null,
1237
            'relation'     => null,
1238
            'map'          => null,
1239 32
            'nestingLevel' => $this->nestingLevel,
1240 32
            'token'        => $token,
1241
        );
1242
1243 32
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1244
1245 32
        $this->match(Lexer::T_SET);
1246
1247 32
        $updateItems = array();
1248 32
        $updateItems[] = $this->UpdateItem();
1249
1250 32
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1251 5
            $this->match(Lexer::T_COMMA);
1252
1253 5
            $updateItems[] = $this->UpdateItem();
1254
        }
1255
1256 32
        $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1257 32
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1258
1259 32
        return $updateClause;
1260
    }
1261
1262
    /**
1263
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1264
     *
1265
     * @return \Doctrine\ORM\Query\AST\DeleteClause
1266
     */
1267 42
    public function DeleteClause()
1268
    {
1269 42
        $this->match(Lexer::T_DELETE);
1270
1271 42
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1272 8
            $this->match(Lexer::T_FROM);
1273
        }
1274
1275 42
        $token = $this->lexer->lookahead;
1276 42
        $abstractSchemaName = $this->AbstractSchemaName();
1277
1278 42
        $this->validateAbstractSchemaName($abstractSchemaName);
1279
1280 42
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1281
1282 42
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1283 1
            $this->match(Lexer::T_AS);
1284
        }
1285
1286 42
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1287
1288 41
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1289 41
        $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1290
1291
        // Building queryComponent
1292
        $queryComponent = array(
1293 41
            'metadata'     => $class,
1294
            'parent'       => null,
1295
            'relation'     => null,
1296
            'map'          => null,
1297 41
            'nestingLevel' => $this->nestingLevel,
1298 41
            'token'        => $token,
1299
        );
1300
1301 41
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1302
1303 41
        return $deleteClause;
1304
    }
1305
1306
    /**
1307
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1308
     *
1309
     * @return \Doctrine\ORM\Query\AST\FromClause
1310
     */
1311 741
    public function FromClause()
1312
    {
1313 741
        $this->match(Lexer::T_FROM);
1314
1315 736
        $identificationVariableDeclarations = array();
1316 736
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1317
1318 719
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1319 6
            $this->match(Lexer::T_COMMA);
1320
1321 6
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1322
        }
1323
1324 719
        return new AST\FromClause($identificationVariableDeclarations);
1325
    }
1326
1327
    /**
1328
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1329
     *
1330
     * @return \Doctrine\ORM\Query\AST\SubselectFromClause
1331
     */
1332 48
    public function SubselectFromClause()
1333
    {
1334 48
        $this->match(Lexer::T_FROM);
1335
1336 48
        $identificationVariables = array();
1337 48
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1338
1339 47
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1340
            $this->match(Lexer::T_COMMA);
1341
1342
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1343
        }
1344
1345 47
        return new AST\SubselectFromClause($identificationVariables);
1346
    }
1347
1348
    /**
1349
     * WhereClause ::= "WHERE" ConditionalExpression
1350
     *
1351
     * @return \Doctrine\ORM\Query\AST\WhereClause
1352
     */
1353 327
    public function WhereClause()
1354
    {
1355 327
        $this->match(Lexer::T_WHERE);
1356
1357 327
        return new AST\WhereClause($this->ConditionalExpression());
1358
    }
1359
1360
    /**
1361
     * HavingClause ::= "HAVING" ConditionalExpression
1362
     *
1363
     * @return \Doctrine\ORM\Query\AST\HavingClause
1364
     */
1365 21
    public function HavingClause()
1366
    {
1367 21
        $this->match(Lexer::T_HAVING);
1368
1369 21
        return new AST\HavingClause($this->ConditionalExpression());
1370
    }
1371
1372
    /**
1373
     * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1374
     *
1375
     * @return \Doctrine\ORM\Query\AST\GroupByClause
1376
     */
1377 31
    public function GroupByClause()
1378
    {
1379 31
        $this->match(Lexer::T_GROUP);
1380 31
        $this->match(Lexer::T_BY);
1381
1382 31
        $groupByItems = array($this->GroupByItem());
1383
1384 30
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1385 8
            $this->match(Lexer::T_COMMA);
1386
1387 8
            $groupByItems[] = $this->GroupByItem();
1388
        }
1389
1390 30
        return new AST\GroupByClause($groupByItems);
1391
    }
1392
1393
    /**
1394
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1395
     *
1396
     * @return \Doctrine\ORM\Query\AST\OrderByClause
1397
     */
1398 179
    public function OrderByClause()
1399
    {
1400 179
        $this->match(Lexer::T_ORDER);
1401 179
        $this->match(Lexer::T_BY);
1402
1403 179
        $orderByItems = array();
1404 179
        $orderByItems[] = $this->OrderByItem();
1405
1406 179
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1407 14
            $this->match(Lexer::T_COMMA);
1408
1409 14
            $orderByItems[] = $this->OrderByItem();
1410
        }
1411
1412 179
        return new AST\OrderByClause($orderByItems);
1413
    }
1414
1415
    /**
1416
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1417
     *
1418
     * @return \Doctrine\ORM\Query\AST\Subselect
1419
     */
1420 48
    public function Subselect()
1421
    {
1422
        // Increase query nesting level
1423 48
        $this->nestingLevel++;
1424
1425 48
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1426
1427 47
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1428 47
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1429 47
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1430 47
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1431
1432
        // Decrease query nesting level
1433 47
        $this->nestingLevel--;
1434
1435 47
        return $subselect;
1436
    }
1437
1438
    /**
1439
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1440
     *
1441
     * @return \Doctrine\ORM\Query\AST\UpdateItem
1442
     */
1443 32
    public function UpdateItem()
1444
    {
1445 32
        $pathExpr = $this->SingleValuedPathExpression();
1446
1447 32
        $this->match(Lexer::T_EQUALS);
1448
1449 32
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1450
1451 32
        return $updateItem;
1452
    }
1453
1454
    /**
1455
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1456
     *
1457
     * @return string | \Doctrine\ORM\Query\AST\PathExpression
1458
     */
1459 31
    public function GroupByItem()
1460
    {
1461
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1462 31
        $glimpse = $this->lexer->glimpse();
1463
1464 31
        if ($glimpse['type'] === Lexer::T_DOT) {
1465 12
            return $this->SingleValuedPathExpression();
1466
        }
1467
1468
        // Still need to decide between IdentificationVariable or ResultVariable
1469 19
        $lookaheadValue = $this->lexer->lookahead['value'];
1470
1471 19
        if ( ! isset($this->queryComponents[$lookaheadValue])) {
1472 1
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1473
        }
1474
1475 18
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1476 16
            ? $this->IdentificationVariable()
1477 18
            : $this->ResultVariable();
1478
    }
1479
1480
    /**
1481
     * OrderByItem ::= (
1482
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1483
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1484
     * ) ["ASC" | "DESC"]
1485
     *
1486
     * @return \Doctrine\ORM\Query\AST\OrderByItem
1487
     */
1488 179
    public function OrderByItem()
1489
    {
1490 179
        $this->lexer->peek(); // lookahead => '.'
1491 179
        $this->lexer->peek(); // lookahead => token after '.'
1492
1493 179
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1494
1495 179
        $this->lexer->resetPeek();
1496
1497 179
        $glimpse = $this->lexer->glimpse();
1498
1499
        switch (true) {
1500 179
            case ($this->isFunction($peek)):
0 ignored issues
show
Unused Code introduced by
The call to Parser::isFunction() has too many arguments starting with $peek.

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

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

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

Loading history...
1501 1
                $expr = $this->FunctionDeclaration();
1502 1
                break;
1503
1504 178
            case ($this->isMathOperator($peek)):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1493 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...
1505 25
                $expr = $this->SimpleArithmeticExpression();
1506 25
                break;
1507
1508 154
            case ($glimpse['type'] === Lexer::T_DOT):
1509 141
                $expr = $this->SingleValuedPathExpression();
1510 141
                break;
1511
1512 17
            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...
1513 2
                $expr = $this->ScalarExpression();
1514 2
                break;
1515
1516
            default:
1517 15
                $expr = $this->ResultVariable();
1518 15
                break;
1519
        }
1520
1521 179
        $type = 'ASC';
1522 179
        $item = new AST\OrderByItem($expr);
1523
1524
        switch (true) {
1525 179
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1526 93
                $this->match(Lexer::T_DESC);
1527 93
                $type = 'DESC';
1528 93
                break;
1529
1530 152
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1531 96
                $this->match(Lexer::T_ASC);
1532 96
                break;
1533
1534
            default:
1535
                // Do nothing
1536
        }
1537
1538 179
        $item->type = $type;
1539
1540 179
        return $item;
1541
    }
1542
1543
    /**
1544
     * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1545
     *      EnumPrimary | SimpleEntityExpression | "NULL"
1546
     *
1547
     * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1548
     * grammar that needs to be supported:
1549
     *
1550
     * NewValue ::= SimpleArithmeticExpression | "NULL"
1551
     *
1552
     * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1553
     *
1554
     * @return AST\ArithmeticExpression
1555
     */
1556 32
    public function NewValue()
1557
    {
1558 32
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1559 1
            $this->match(Lexer::T_NULL);
1560
1561 1
            return null;
1562
        }
1563
1564 31
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1565 18
            $this->match(Lexer::T_INPUT_PARAMETER);
1566
1567 18
            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...
1568
        }
1569
1570 13
        return $this->ArithmeticExpression();
1571
    }
1572
1573
    /**
1574
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1575
     *
1576
     * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1577
     */
1578 738
    public function IdentificationVariableDeclaration()
1579
    {
1580 738
        $joins                    = array();
1581 738
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1582 723
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1583 7
            ? $this->IndexBy()
1584 723
            : null;
1585
1586 723
        $rangeVariableDeclaration->isRoot = true;
1587
1588
        while (
1589 723
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1590 723
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1591 723
            $this->lexer->isNextToken(Lexer::T_JOIN)
1592
        ) {
1593 275
            $joins[] = $this->Join();
1594
        }
1595
1596 721
        return new AST\IdentificationVariableDeclaration(
1597
            $rangeVariableDeclaration, $indexBy, $joins
1598
        );
1599
    }
1600
1601
    /**
1602
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1603
     *
1604
     * {Internal note: WARNING: Solution is harder than a bare implementation.
1605
     * Desired EBNF support:
1606
     *
1607
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1608
     *
1609
     * It demands that entire SQL generation to become programmatical. This is
1610
     * needed because association based subselect requires "WHERE" conditional
1611
     * expressions to be injected, but there is no scope to do that. Only scope
1612
     * accessible is "FROM", prohibiting an easy implementation without larger
1613
     * changes.}
1614
     *
1615
     * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration |
1616
     *         \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1617
     */
1618 48
    public function SubselectIdentificationVariableDeclaration()
1619
    {
1620
        /*
1621
        NOT YET IMPLEMENTED!
1622
1623
        $glimpse = $this->lexer->glimpse();
1624
1625
        if ($glimpse['type'] == Lexer::T_DOT) {
1626
            $associationPathExpression = $this->AssociationPathExpression();
1627
1628
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1629
                $this->match(Lexer::T_AS);
1630
            }
1631
1632
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1633
            $identificationVariable      = $associationPathExpression->identificationVariable;
1634
            $field                       = $associationPathExpression->associationField;
1635
1636
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1637
            $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1638
1639
            // Building queryComponent
1640
            $joinQueryComponent = array(
1641
                'metadata'     => $targetClass,
1642
                'parent'       => $identificationVariable,
1643
                'relation'     => $class->getAssociationMapping($field),
1644
                'map'          => null,
1645
                'nestingLevel' => $this->nestingLevel,
1646
                'token'        => $this->lexer->lookahead
1647
            );
1648
1649
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1650
1651
            return new AST\SubselectIdentificationVariableDeclaration(
1652
                $associationPathExpression, $aliasIdentificationVariable
1653
            );
1654
        }
1655
        */
1656
1657 48
        return $this->IdentificationVariableDeclaration();
1658
    }
1659
1660
    /**
1661
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1662
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1663
     *          ["WITH" ConditionalExpression]
1664
     *
1665
     * @return \Doctrine\ORM\Query\AST\Join
1666
     */
1667 275
    public function Join()
1668
    {
1669
        // Check Join type
1670 275
        $joinType = AST\Join::JOIN_TYPE_INNER;
1671
1672
        switch (true) {
1673 275
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1674 65
                $this->match(Lexer::T_LEFT);
1675
1676 65
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1677
1678
                // Possible LEFT OUTER join
1679 65
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1680
                    $this->match(Lexer::T_OUTER);
1681
1682
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1683
                }
1684 65
                break;
1685
1686 214
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1687 20
                $this->match(Lexer::T_INNER);
1688 20
                break;
1689
1690
            default:
1691
                // Do nothing
1692
        }
1693
1694 275
        $this->match(Lexer::T_JOIN);
1695
1696 275
        $next            = $this->lexer->glimpse();
1697 275
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1698 273
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1699 273
        $join            = new AST\Join($joinType, $joinDeclaration);
1700
1701
        // Describe non-root join declaration
1702 273
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1703 21
            $joinDeclaration->isRoot = false;
1704
        }
1705
1706
        // Check for ad-hoc Join conditions
1707 273
        if ($adhocConditions) {
1708 23
            $this->match(Lexer::T_WITH);
1709
1710 23
            $join->conditionalExpression = $this->ConditionalExpression();
1711
        }
1712
1713 273
        return $join;
1714
    }
1715
1716
    /**
1717
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1718
     *
1719
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1720
     */
1721 738
    public function RangeVariableDeclaration()
1722
    {
1723 738
        $abstractSchemaName = $this->AbstractSchemaName();
1724
1725 738
        $this->validateAbstractSchemaName($abstractSchemaName);
1726
1727 723
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1728 5
            $this->match(Lexer::T_AS);
1729
        }
1730
1731 723
        $token = $this->lexer->lookahead;
1732 723
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1733 723
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1734
1735
        // Building queryComponent
1736
        $queryComponent = array(
1737 723
            'metadata'     => $classMetadata,
1738
            'parent'       => null,
1739
            'relation'     => null,
1740
            'map'          => null,
1741 723
            'nestingLevel' => $this->nestingLevel,
1742 723
            'token'        => $token
1743
        );
1744
1745 723
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1746
1747 723
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1748
    }
1749
1750
    /**
1751
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1752
     *
1753
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1754
     */
1755 254
    public function JoinAssociationDeclaration()
1756
    {
1757 254
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1758
1759 254
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1760 4
            $this->match(Lexer::T_AS);
1761
        }
1762
1763 254
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1764 252
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1765
1766 252
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1767 252
        $field                  = $joinAssociationPathExpression->associationField;
1768
1769 252
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1770 252
        $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1771
1772
        // Building queryComponent
1773
        $joinQueryComponent = array(
1774 252
            'metadata'     => $targetClass,
1775 252
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1776 252
            'relation'     => $class->getAssociationMapping($field),
1777
            'map'          => null,
1778 252
            'nestingLevel' => $this->nestingLevel,
1779 252
            'token'        => $this->lexer->lookahead
1780
        );
1781
1782 252
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1783
1784 252
        return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
1785
    }
1786
1787
    /**
1788
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1789
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1790
     *
1791
     * @return array
1792
     */
1793 11
    public function PartialObjectExpression()
1794
    {
1795 11
        $this->match(Lexer::T_PARTIAL);
1796
1797 11
        $partialFieldSet = array();
1798
1799 11
        $identificationVariable = $this->IdentificationVariable();
1800
1801 11
        $this->match(Lexer::T_DOT);
1802 11
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1803 11
        $this->match(Lexer::T_IDENTIFIER);
1804
1805 11
        $field = $this->lexer->token['value'];
1806
1807
        // First field in partial expression might be embeddable property
1808 11
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1809 1
            $this->match(Lexer::T_DOT);
1810 1
            $this->match(Lexer::T_IDENTIFIER);
1811 1
            $field .= '.'.$this->lexer->token['value'];
1812
        }
1813
1814 11
        $partialFieldSet[] = $field;
1815
1816 11
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1817 9
            $this->match(Lexer::T_COMMA);
1818 9
            $this->match(Lexer::T_IDENTIFIER);
1819
    
1820 9
            $field = $this->lexer->token['value'];
1821
    
1822 9
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1823 2
                $this->match(Lexer::T_DOT);
1824 2
                $this->match(Lexer::T_IDENTIFIER);
1825 2
                $field .= '.'.$this->lexer->token['value'];
1826
            }
1827
    
1828 9
            $partialFieldSet[] = $field;
1829
        }
1830
1831 11
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1832
1833 11
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1834
1835
        // Defer PartialObjectExpression validation
1836 11
        $this->deferredPartialObjectExpressions[] = array(
1837 11
            'expression'   => $partialObjectExpression,
1838 11
            'nestingLevel' => $this->nestingLevel,
1839 11
            'token'        => $this->lexer->token,
1840
        );
1841
1842 11
        return $partialObjectExpression;
1843
    }
1844
1845
    /**
1846
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1847
     *
1848
     * @return \Doctrine\ORM\Query\AST\NewObjectExpression
1849
     */
1850 28
    public function NewObjectExpression()
1851
    {
1852 28
        $this->match(Lexer::T_NEW);
1853
1854 28
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1855 28
        $token = $this->lexer->token;
1856
1857 28
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1858
1859 28
        $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...
1860
1861 28
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1862 24
            $this->match(Lexer::T_COMMA);
1863
1864 24
            $args[] = $this->NewObjectArg();
1865
        }
1866
1867 28
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1868
1869 28
        $expression = new AST\NewObjectExpression($className, $args);
1870
1871
        // Defer NewObjectExpression validation
1872 28
        $this->deferredNewObjectExpressions[] = array(
1873 28
            'token'        => $token,
1874 28
            'expression'   => $expression,
1875 28
            'nestingLevel' => $this->nestingLevel,
1876
        );
1877
1878 28
        return $expression;
1879
    }
1880
1881
    /**
1882
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1883
     *
1884
     * @return mixed
1885
     */
1886 28
    public function NewObjectArg()
1887
    {
1888 28
        $token = $this->lexer->lookahead;
1889 28
        $peek  = $this->lexer->glimpse();
1890
1891 28
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1892 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1893 2
            $expression = $this->Subselect();
1894 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1895
1896 2
            return $expression;
1897
        }
1898
1899 28
        return $this->ScalarExpression();
1900
    }
1901
1902
    /**
1903
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1904
     *
1905
     * @return \Doctrine\ORM\Query\AST\IndexBy
1906
     */
1907 11
    public function IndexBy()
1908
    {
1909 11
        $this->match(Lexer::T_INDEX);
1910 11
        $this->match(Lexer::T_BY);
1911 11
        $pathExpr = $this->StateFieldPathExpression();
1912
1913
        // Add the INDEX BY info to the query component
1914 11
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1915
1916 11
        return new AST\IndexBy($pathExpr);
1917
    }
1918
1919
    /**
1920
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1921
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1922
     *                      InstanceOfExpression
1923
     *
1924
     * @return mixed One of the possible expressions or subexpressions.
1925
     */
1926 160
    public function ScalarExpression()
1927
    {
1928 160
        $lookahead = $this->lexer->lookahead['type'];
1929 160
        $peek      = $this->lexer->glimpse();
1930
1931
        switch (true) {
1932 160
            case ($lookahead === Lexer::T_INTEGER):
1933 157
            case ($lookahead === Lexer::T_FLOAT):
1934
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1935 157
            case ($lookahead === Lexer::T_MINUS):
1936 157
            case ($lookahead === Lexer::T_PLUS):
1937 17
                return $this->SimpleArithmeticExpression();
1938
1939 157
            case ($lookahead === Lexer::T_STRING):
1940 13
                return $this->StringPrimary();
1941
1942 154
            case ($lookahead === Lexer::T_TRUE):
1943 154
            case ($lookahead === Lexer::T_FALSE):
1944 3
                $this->match($lookahead);
1945
1946 3
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1947
1948 154
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
1949
                switch (true) {
1950 1
                    case $this->isMathOperator($peek):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->glimpse() on line 1929 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...
1951
                        // :param + u.value
1952 1
                        return $this->SimpleArithmeticExpression();
1953
                    default:
1954
                        return $this->InputParameter();
1955
                }
1956
1957 154
            case ($lookahead === Lexer::T_CASE):
1958 150
            case ($lookahead === Lexer::T_COALESCE):
1959 150
            case ($lookahead === Lexer::T_NULLIF):
1960
                // Since NULLIF and COALESCE can be identified as a function,
1961
                // we need to check these before checking for FunctionDeclaration
1962 8
                return $this->CaseExpression();
1963
1964 150
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1965 3
                return $this->SimpleArithmeticExpression();
1966
1967
            // this check must be done before checking for a filed path expression
1968 148
            case ($this->isFunction()):
1969 26
                $this->lexer->peek(); // "("
1970
1971
                switch (true) {
1972 26
                    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...
1973
                        // SUM(u.id) + COUNT(u.id)
1974 7
                        return $this->SimpleArithmeticExpression();
1975
1976 21
                    case ($this->isAggregateFunction($this->lexer->lookahead['type'])):
1977 19
                        return $this->AggregateExpression();
1978
1979
                    default:
1980
                        // IDENTITY(u)
1981 2
                        return $this->FunctionDeclaration();
1982
                }
1983
1984
                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...
1985
            // it is no function, so it must be a field path
1986 130
            case ($lookahead === Lexer::T_IDENTIFIER):
1987 130
                $this->lexer->peek(); // lookahead => '.'
1988 130
                $this->lexer->peek(); // lookahead => token after '.'
1989 130
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1990 130
                $this->lexer->resetPeek();
1991
1992 130
                if ($this->isMathOperator($peek)) {
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1989 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...
1993 7
                    return $this->SimpleArithmeticExpression();
1994
                }
1995
1996 125
                return $this->StateFieldPathExpression();
1997
1998
            default:
1999
                $this->syntaxError();
2000
        }
2001
    }
2002
2003
    /**
2004
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
2005
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2006
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2007
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2008
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2009
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2010
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2011
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2012
     *
2013
     * @return mixed One of the possible expressions or subexpressions.
2014
     */
2015 19
    public function CaseExpression()
2016
    {
2017 19
        $lookahead = $this->lexer->lookahead['type'];
2018
2019
        switch ($lookahead) {
2020 19
            case Lexer::T_NULLIF:
2021 5
                return $this->NullIfExpression();
2022
2023 16
            case Lexer::T_COALESCE:
2024 2
                return $this->CoalesceExpression();
2025
2026 14
            case Lexer::T_CASE:
2027 14
                $this->lexer->resetPeek();
2028 14
                $peek = $this->lexer->peek();
2029
2030 14
                if ($peek['type'] === Lexer::T_WHEN) {
2031 9
                    return $this->GeneralCaseExpression();
2032
                }
2033
2034 5
                return $this->SimpleCaseExpression();
2035
2036
            default:
2037
                // Do nothing
2038
                break;
2039
        }
2040
2041
        $this->syntaxError();
2042
    }
2043
2044
    /**
2045
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2046
     *
2047
     * @return \Doctrine\ORM\Query\AST\CoalesceExpression
2048
     */
2049 3
    public function CoalesceExpression()
2050
    {
2051 3
        $this->match(Lexer::T_COALESCE);
2052 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2053
2054
        // Process ScalarExpressions (1..N)
2055 3
        $scalarExpressions = array();
2056 3
        $scalarExpressions[] = $this->ScalarExpression();
2057
2058 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2059 3
            $this->match(Lexer::T_COMMA);
2060
2061 3
            $scalarExpressions[] = $this->ScalarExpression();
2062
        }
2063
2064 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2065
2066 3
        return new AST\CoalesceExpression($scalarExpressions);
2067
    }
2068
2069
    /**
2070
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2071
     *
2072
     * @return \Doctrine\ORM\Query\AST\NullIfExpression
2073
     */
2074 5
    public function NullIfExpression()
2075
    {
2076 5
        $this->match(Lexer::T_NULLIF);
2077 5
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2078
2079 5
        $firstExpression = $this->ScalarExpression();
2080 5
        $this->match(Lexer::T_COMMA);
2081 5
        $secondExpression = $this->ScalarExpression();
2082
2083 5
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2084
2085 5
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2086
    }
2087
2088
    /**
2089
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2090
     *
2091
     * @return \Doctrine\ORM\Query\AST\GeneralCaseExpression
2092
     */
2093 9
    public function GeneralCaseExpression()
2094
    {
2095 9
        $this->match(Lexer::T_CASE);
2096
2097
        // Process WhenClause (1..N)
2098 9
        $whenClauses = array();
2099
2100
        do {
2101 9
            $whenClauses[] = $this->WhenClause();
2102 9
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2103
2104 9
        $this->match(Lexer::T_ELSE);
2105 9
        $scalarExpression = $this->ScalarExpression();
2106 9
        $this->match(Lexer::T_END);
2107
2108 9
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2109
    }
2110
2111
    /**
2112
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2113
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2114
     *
2115
     * @return AST\SimpleCaseExpression
2116
     */
2117 5
    public function SimpleCaseExpression()
2118
    {
2119 5
        $this->match(Lexer::T_CASE);
2120 5
        $caseOperand = $this->StateFieldPathExpression();
2121
2122
        // Process SimpleWhenClause (1..N)
2123 5
        $simpleWhenClauses = array();
2124
2125
        do {
2126 5
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2127 5
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2128
2129 5
        $this->match(Lexer::T_ELSE);
2130 5
        $scalarExpression = $this->ScalarExpression();
2131 5
        $this->match(Lexer::T_END);
2132
2133 5
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2134
    }
2135
2136
    /**
2137
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2138
     *
2139
     * @return \Doctrine\ORM\Query\AST\WhenClause
2140
     */
2141 9
    public function WhenClause()
2142
    {
2143 9
        $this->match(Lexer::T_WHEN);
2144 9
        $conditionalExpression = $this->ConditionalExpression();
2145 9
        $this->match(Lexer::T_THEN);
2146
2147 9
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2148
    }
2149
2150
    /**
2151
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2152
     *
2153
     * @return \Doctrine\ORM\Query\AST\SimpleWhenClause
2154
     */
2155 5
    public function SimpleWhenClause()
2156
    {
2157 5
        $this->match(Lexer::T_WHEN);
2158 5
        $conditionalExpression = $this->ScalarExpression();
2159 5
        $this->match(Lexer::T_THEN);
2160
2161 5
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2162
    }
2163
2164
    /**
2165
     * SelectExpression ::= (
2166
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2167
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2168
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2169
     *
2170
     * @return \Doctrine\ORM\Query\AST\SelectExpression
2171
     */
2172 750
    public function SelectExpression()
2173
    {
2174 750
        $expression    = null;
2175 750
        $identVariable = null;
2176 750
        $peek          = $this->lexer->glimpse();
2177 750
        $lookaheadType = $this->lexer->lookahead['type'];
2178
2179
        switch (true) {
2180
            // ScalarExpression (u.name)
2181 750
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2182 102
                $expression = $this->ScalarExpression();
2183 102
                break;
2184
2185
            // IdentificationVariable (u)
2186 691
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2187 576
                $expression = $identVariable = $this->IdentificationVariable();
2188 576
                break;
2189
2190
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
2191 176
            case ($lookaheadType === Lexer::T_CASE):
2192 171
            case ($lookaheadType === Lexer::T_COALESCE):
2193 169
            case ($lookaheadType === Lexer::T_NULLIF):
2194 9
                $expression = $this->CaseExpression();
2195 9
                break;
2196
2197
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2198 167
            case ($this->isFunction()):
2199 87
                $this->lexer->peek(); // "("
2200
2201
                switch (true) {
2202 87
                    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...
2203
                        // SUM(u.id) + COUNT(u.id)
2204 2
                        $expression = $this->ScalarExpression();
2205 2
                        break;
2206
2207 85
                    case ($this->isAggregateFunction($lookaheadType)):
2208
                        // COUNT(u.id)
2209 54
                        $expression = $this->AggregateExpression();
2210 54
                        break;
2211
2212
                    default:
2213
                        // IDENTITY(u)
2214 31
                        $expression = $this->FunctionDeclaration();
2215 31
                        break;
2216
                }
2217
2218 87
                break;
2219
2220
            // PartialObjectExpression (PARTIAL u.{id, name})
2221 80
            case ($lookaheadType === Lexer::T_PARTIAL):
2222 11
                $expression    = $this->PartialObjectExpression();
2223 11
                $identVariable = $expression->identificationVariable;
2224 11
                break;
2225
2226
            // Subselect
2227 69
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2228 22
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2229 22
                $expression = $this->Subselect();
2230 22
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2231 22
                break;
2232
2233
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2234 47
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2235 43
            case ($lookaheadType === Lexer::T_INTEGER):
2236 41
            case ($lookaheadType === Lexer::T_STRING):
2237 32
            case ($lookaheadType === Lexer::T_FLOAT):
2238
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2239 32
            case ($lookaheadType === Lexer::T_MINUS):
2240 31
            case ($lookaheadType === Lexer::T_PLUS):
2241 16
                $expression = $this->SimpleArithmeticExpression();
2242 16
                break;
2243
2244
            // NewObjectExpression (New ClassName(id, name))
2245 31
            case ($lookaheadType === Lexer::T_NEW):
2246 28
                $expression = $this->NewObjectExpression();
2247 28
                break;
2248
2249
            default:
2250 3
                $this->syntaxError(
2251 3
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2252 3
                    $this->lexer->lookahead
2253
                );
2254
        }
2255
2256
        // [["AS"] ["HIDDEN"] AliasResultVariable]
2257 747
        $mustHaveAliasResultVariable = false;
2258
2259 747
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2260 110
            $this->match(Lexer::T_AS);
2261
2262 110
            $mustHaveAliasResultVariable = true;
2263
        }
2264
2265 747
        $hiddenAliasResultVariable = false;
2266
2267 747
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2268 10
            $this->match(Lexer::T_HIDDEN);
2269
2270 10
            $hiddenAliasResultVariable = true;
2271
        }
2272
2273 747
        $aliasResultVariable = null;
2274
2275 747
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2276 119
            $token = $this->lexer->lookahead;
2277 119
            $aliasResultVariable = $this->AliasResultVariable();
2278
2279
            // Include AliasResultVariable in query components.
2280 114
            $this->queryComponents[$aliasResultVariable] = array(
2281 114
                'resultVariable' => $expression,
2282 114
                'nestingLevel'   => $this->nestingLevel,
2283 114
                'token'          => $token,
2284
            );
2285
        }
2286
2287
        // AST
2288
2289 742
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2290
2291 742
        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...
2292 584
            $this->identVariableExpressions[$identVariable] = $expr;
2293
        }
2294
2295 742
        return $expr;
2296
    }
2297
2298
    /**
2299
     * SimpleSelectExpression ::= (
2300
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2301
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2302
     * ) [["AS"] AliasResultVariable]
2303
     *
2304
     * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
2305
     */
2306 48
    public function SimpleSelectExpression()
2307
    {
2308 48
        $peek = $this->lexer->glimpse();
2309
2310 48
        switch ($this->lexer->lookahead['type']) {
2311 48
            case Lexer::T_IDENTIFIER:
2312
                switch (true) {
2313 19
                    case ($peek['type'] === Lexer::T_DOT):
2314 16
                        $expression = $this->StateFieldPathExpression();
2315
2316 16
                        return new AST\SimpleSelectExpression($expression);
2317
2318 3
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2319 2
                        $expression = $this->IdentificationVariable();
2320
2321 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...
2322
2323 1
                    case ($this->isFunction()):
2324
                        // SUM(u.id) + COUNT(u.id)
2325 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...
2326
                            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...
2327
                        }
2328
                        // COUNT(u.id)
2329 1
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2330
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2331
                        }
2332
                        // IDENTITY(u)
2333 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...
2334
2335
                    default:
2336
                        // Do nothing
2337
                }
2338
                break;
2339
2340 30
            case Lexer::T_OPEN_PARENTHESIS:
2341 3
                if ($peek['type'] !== Lexer::T_SELECT) {
2342
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2343 3
                    $expression = $this->SimpleArithmeticExpression();
2344
2345 3
                    return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->SimpleArithmeticExpression() on line 2343 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...
2346
                }
2347
2348
                // Subselect
2349
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2350
                $expression = $this->Subselect();
2351
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2352
2353
                return new AST\SimpleSelectExpression($expression);
2354
2355
            default:
2356
                // Do nothing
2357
        }
2358
2359 27
        $this->lexer->peek();
2360
2361 27
        $expression = $this->ScalarExpression();
2362 27
        $expr       = new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->ScalarExpression() on line 2361 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...
2363
2364 27
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2365 1
            $this->match(Lexer::T_AS);
2366
        }
2367
2368 27
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2369 2
            $token = $this->lexer->lookahead;
2370 2
            $resultVariable = $this->AliasResultVariable();
2371 2
            $expr->fieldIdentificationVariable = $resultVariable;
2372
2373
            // Include AliasResultVariable in query components.
2374 2
            $this->queryComponents[$resultVariable] = array(
2375 2
                'resultvariable' => $expr,
2376 2
                'nestingLevel'   => $this->nestingLevel,
2377 2
                'token'          => $token,
2378
            );
2379
        }
2380
2381 27
        return $expr;
2382
    }
2383
2384
    /**
2385
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2386
     *
2387
     * @return \Doctrine\ORM\Query\AST\ConditionalExpression
2388
     */
2389 369
    public function ConditionalExpression()
2390
    {
2391 369
        $conditionalTerms = array();
2392 369
        $conditionalTerms[] = $this->ConditionalTerm();
2393
2394 366
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2395 16
            $this->match(Lexer::T_OR);
2396
2397 16
            $conditionalTerms[] = $this->ConditionalTerm();
2398
        }
2399
2400
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2401
        // if only one AST\ConditionalTerm is defined
2402 366
        if (count($conditionalTerms) == 1) {
2403 358
            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...
2404
        }
2405
2406 16
        return new AST\ConditionalExpression($conditionalTerms);
2407
    }
2408
2409
    /**
2410
     * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2411
     *
2412
     * @return \Doctrine\ORM\Query\AST\ConditionalTerm
2413
     */
2414 369
    public function ConditionalTerm()
2415
    {
2416 369
        $conditionalFactors = array();
2417 369
        $conditionalFactors[] = $this->ConditionalFactor();
2418
2419 366
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2420 32
            $this->match(Lexer::T_AND);
2421
2422 32
            $conditionalFactors[] = $this->ConditionalFactor();
2423
        }
2424
2425
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2426
        // if only one AST\ConditionalFactor is defined
2427 366
        if (count($conditionalFactors) == 1) {
2428 348
            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...
2429
        }
2430
2431 32
        return new AST\ConditionalTerm($conditionalFactors);
2432
    }
2433
2434
    /**
2435
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2436
     *
2437
     * @return \Doctrine\ORM\Query\AST\ConditionalFactor
2438
     */
2439 369
    public function ConditionalFactor()
2440
    {
2441 369
        if ($not = $this->lexer->isNextToken(Lexer::T_NOT)) {
2442 6
            $this->match(Lexer::T_NOT);
2443
        }
2444
2445 369
        $conditionalPrimary = $this->ConditionalPrimary();
2446
2447
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2448
        // if only one AST\ConditionalPrimary is defined
2449 366
        if ( ! $not) {
2450 364
            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...
2451
        }
2452
2453 6
        $conditionalFactor = new AST\ConditionalFactor($conditionalPrimary);
2454 6
        $conditionalFactor->not = $not;
2455
2456 6
        return $conditionalFactor;
2457
    }
2458
2459
    /**
2460
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2461
     *
2462
     * @return \Doctrine\ORM\Query\AST\ConditionalPrimary
2463
     */
2464 369
    public function ConditionalPrimary()
2465
    {
2466 369
        $condPrimary = new AST\ConditionalPrimary;
2467
2468 369
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2469 360
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2470
2471 357
            return $condPrimary;
2472
        }
2473
2474
        // Peek beyond the matching closing parenthesis ')'
2475 25
        $peek = $this->peekBeyondClosingParenthesis();
2476
2477 25
        if (in_array($peek['value'], array("=",  "<", "<=", "<>", ">", ">=", "!=")) ||
2478 22
            in_array($peek['type'], array(Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS)) ||
2479 25
            $this->isMathOperator($peek)) {
2480 15
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2481
2482 15
            return $condPrimary;
2483
        }
2484
2485 21
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2486 21
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2487 21
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2488
2489 21
        return $condPrimary;
2490
    }
2491
2492
    /**
2493
     * SimpleConditionalExpression ::=
2494
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2495
     *      InExpression | NullComparisonExpression | ExistsExpression |
2496
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2497
     *      InstanceOfExpression
2498
     */
2499 369
    public function SimpleConditionalExpression()
2500
    {
2501 369
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2502 7
            return $this->ExistsExpression();
2503
        }
2504
2505 369
        $token      = $this->lexer->lookahead;
2506 369
        $peek       = $this->lexer->glimpse();
2507 369
        $lookahead  = $token;
2508
2509 369
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2510
            $token = $this->lexer->glimpse();
2511
        }
2512
2513 369
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2514
            // Peek beyond the matching closing parenthesis.
2515 345
            $beyond = $this->lexer->peek();
2516
2517 345
            switch ($peek['value']) {
2518 345
                case '(':
2519
                    // Peeks beyond the matched closing parenthesis.
2520 34
                    $token = $this->peekBeyondClosingParenthesis(false);
2521
2522 34
                    if ($token['type'] === Lexer::T_NOT) {
2523 3
                        $token = $this->lexer->peek();
2524
                    }
2525
2526 34
                    if ($token['type'] === Lexer::T_IS) {
2527 2
                        $lookahead = $this->lexer->peek();
2528
                    }
2529 34
                    break;
2530
2531
                default:
2532
                    // Peek beyond the PathExpression or InputParameter.
2533 317
                    $token = $beyond;
2534
2535 317
                    while ($token['value'] === '.') {
2536 279
                        $this->lexer->peek();
2537
2538 279
                        $token = $this->lexer->peek();
2539
                    }
2540
2541
                    // Also peek beyond a NOT if there is one.
2542 317
                    if ($token['type'] === Lexer::T_NOT) {
2543 11
                        $token = $this->lexer->peek();
2544
                    }
2545
2546
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2547 317
                    $lookahead = $this->lexer->peek();
2548
            }
2549
2550
            // Also peek beyond a NOT if there is one.
2551 345
            if ($lookahead['type'] === Lexer::T_NOT) {
2552 6
                $lookahead = $this->lexer->peek();
2553
            }
2554
2555 345
            $this->lexer->resetPeek();
2556
        }
2557
2558 369
        if ($token['type'] === Lexer::T_BETWEEN) {
2559 8
            return $this->BetweenExpression();
2560
        }
2561
2562 363
        if ($token['type'] === Lexer::T_LIKE) {
2563 14
            return $this->LikeExpression();
2564
        }
2565
2566 350
        if ($token['type'] === Lexer::T_IN) {
2567 34
            return $this->InExpression();
2568
        }
2569
2570 325
        if ($token['type'] === Lexer::T_INSTANCE) {
2571 12
            return $this->InstanceOfExpression();
2572
        }
2573
2574 313
        if ($token['type'] === Lexer::T_MEMBER) {
2575 7
            return $this->CollectionMemberExpression();
2576
        }
2577
2578 306
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2579 13
            return $this->NullComparisonExpression();
2580
        }
2581
2582 296
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2583 4
            return $this->EmptyCollectionComparisonExpression();
2584
        }
2585
2586 292
        return $this->ComparisonExpression();
2587
    }
2588
2589
    /**
2590
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2591
     *
2592
     * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression
2593
     */
2594 4
    public function EmptyCollectionComparisonExpression()
2595
    {
2596 4
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2597 4
            $this->CollectionValuedPathExpression()
2598
        );
2599 4
        $this->match(Lexer::T_IS);
2600
2601 4
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2602 2
            $this->match(Lexer::T_NOT);
2603 2
            $emptyCollectionCompExpr->not = true;
2604
        }
2605
2606 4
        $this->match(Lexer::T_EMPTY);
2607
2608 4
        return $emptyCollectionCompExpr;
2609
    }
2610
2611
    /**
2612
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2613
     *
2614
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2615
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2616
     *
2617
     * @return \Doctrine\ORM\Query\AST\CollectionMemberExpression
2618
     */
2619 7
    public function CollectionMemberExpression()
2620
    {
2621 7
        $entityExpr = $this->EntityExpression();
2622
2623 7
        if ($not = $this->lexer->isNextToken(Lexer::T_NOT)) {
2624
            $this->match(Lexer::T_NOT);
2625
        }
2626
2627 7
        $this->match(Lexer::T_MEMBER);
2628
2629 7
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2630 7
            $this->match(Lexer::T_OF);
2631
        }
2632
2633 7
        $collMemberExpr = new AST\CollectionMemberExpression(
2634 7
            $entityExpr, $this->CollectionValuedPathExpression()
2635
        );
2636 7
        $collMemberExpr->not = $not;
2637
2638 7
        return $collMemberExpr;
2639
    }
2640
2641
    /**
2642
     * Literal ::= string | char | integer | float | boolean
2643
     *
2644
     * @return \Doctrine\ORM\Query\AST\Literal
2645
     */
2646 177
    public function Literal()
2647
    {
2648 177
        switch ($this->lexer->lookahead['type']) {
2649 177
            case Lexer::T_STRING:
2650 49
                $this->match(Lexer::T_STRING);
2651
2652 49
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2653 137
            case Lexer::T_INTEGER:
2654 9
            case Lexer::T_FLOAT:
2655 129
                $this->match(
2656 129
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2657
                );
2658
2659 129
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2660 8
            case Lexer::T_TRUE:
2661 4
            case Lexer::T_FALSE:
2662 8
                $this->match(
2663 8
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2664
                );
2665
2666 8
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2667
            default:
2668
                $this->syntaxError('Literal');
2669
        }
2670
    }
2671
2672
    /**
2673
     * InParameter ::= Literal | InputParameter
2674
     *
2675
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2676
     */
2677 25
    public function InParameter()
2678
    {
2679 25
        if ($this->lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) {
2680 13
            return $this->InputParameter();
2681
        }
2682
2683 12
        return $this->Literal();
2684
    }
2685
2686
    /**
2687
     * InputParameter ::= PositionalParameter | NamedParameter
2688
     *
2689
     * @return \Doctrine\ORM\Query\AST\InputParameter
2690
     */
2691 159
    public function InputParameter()
2692
    {
2693 159
        $this->match(Lexer::T_INPUT_PARAMETER);
2694
2695 159
        return new AST\InputParameter($this->lexer->token['value']);
2696
    }
2697
2698
    /**
2699
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2700
     *
2701
     * @return \Doctrine\ORM\Query\AST\ArithmeticExpression
2702
     */
2703 325
    public function ArithmeticExpression()
2704
    {
2705 325
        $expr = new AST\ArithmeticExpression;
2706
2707 325
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2708 19
            $peek = $this->lexer->glimpse();
2709
2710 19
            if ($peek['type'] === Lexer::T_SELECT) {
2711 7
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2712 7
                $expr->subselect = $this->Subselect();
2713 7
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2714
2715 7
                return $expr;
2716
            }
2717
        }
2718
2719 325
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2720
2721 325
        return $expr;
2722
    }
2723
2724
    /**
2725
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2726
     *
2727
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2728
     */
2729 428
    public function SimpleArithmeticExpression()
2730
    {
2731 428
        $terms = array();
2732 428
        $terms[] = $this->ArithmeticTerm();
2733
2734 428
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2735 21
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2736
2737 21
            $terms[] = $this->lexer->token['value'];
2738 21
            $terms[] = $this->ArithmeticTerm();
2739
        }
2740
2741
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2742
        // if only one AST\ArithmeticTerm is defined
2743 428
        if (count($terms) == 1) {
2744 423
            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 2744 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...
2745
        }
2746
2747 21
        return new AST\SimpleArithmeticExpression($terms);
2748
    }
2749
2750
    /**
2751
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2752
     *
2753
     * @return \Doctrine\ORM\Query\AST\ArithmeticTerm
2754
     */
2755 428
    public function ArithmeticTerm()
2756
    {
2757 428
        $factors = array();
2758 428
        $factors[] = $this->ArithmeticFactor();
2759
2760 428
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2761 53
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2762
2763 53
            $factors[] = $this->lexer->token['value'];
2764 53
            $factors[] = $this->ArithmeticFactor();
2765
        }
2766
2767
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2768
        // if only one AST\ArithmeticFactor is defined
2769 428
        if (count($factors) == 1) {
2770 400
            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 2770 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticTerm of type Doctrine\ORM\Query\AST\ArithmeticTerm|null.
Loading history...
2771
        }
2772
2773 53
        return new AST\ArithmeticTerm($factors);
2774
    }
2775
2776
    /**
2777
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2778
     *
2779
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2780
     */
2781 428
    public function ArithmeticFactor()
2782
    {
2783 428
        if ($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) {
2784
            $this->match(Lexer::T_PLUS);
2785 428
        } elseif ($this->lexer->isNextToken(Lexer::T_MINUS)) {
2786 3
            $this->match(Lexer::T_MINUS);
2787
        }
2788
2789 428
        $primary = $this->ArithmeticPrimary();
2790
2791
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2792
        // if only one AST\ArithmeticPrimary is defined
2793 428
        return $isPlus
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $isPlus ? new \Do...y, $isPlus) : $primary; (Doctrine\ORM\Query\AST\P...ry\AST\ArithmeticFactor) 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...
2794
            ? new AST\ArithmeticFactor($primary, $isPlus)
2795 428
            : $primary;
2796
    }
2797
2798
    /**
2799
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2800
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2801
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2802
     *          | InputParameter | CaseExpression
2803
     */
2804 432
    public function ArithmeticPrimary()
2805
    {
2806 432
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2807 24
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2808
2809 24
            $expr = $this->SimpleArithmeticExpression();
2810
2811 24
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2812
2813 24
            return new AST\ParenthesisExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr defined by $this->SimpleArithmeticExpression() on line 2809 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...
2814
        }
2815
2816 432
        switch ($this->lexer->lookahead['type']) {
2817 432
            case Lexer::T_COALESCE:
2818 432
            case Lexer::T_NULLIF:
2819 432
            case Lexer::T_CASE:
2820 4
                return $this->CaseExpression();
2821
2822 432
            case Lexer::T_IDENTIFIER:
2823 401
                $peek = $this->lexer->glimpse();
2824
2825 401
                if ($peek['value'] == '(') {
2826 29
                    return $this->FunctionDeclaration();
2827
                }
2828
2829 380
                if ($peek['value'] == '.') {
2830 369
                    return $this->SingleValuedPathExpression();
2831
                }
2832
2833 46
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2834 10
                    return $this->ResultVariable();
2835
                }
2836
2837 38
                return $this->StateFieldPathExpression();
2838
2839 305
            case Lexer::T_INPUT_PARAMETER:
2840 141
                return $this->InputParameter();
2841
2842
            default:
2843 171
                $peek = $this->lexer->glimpse();
2844
2845 171
                if ($peek['value'] == '(') {
2846 18
                    if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2847 18
                        return $this->AggregateExpression();
2848
                    }
2849
2850
                    return $this->FunctionDeclaration();
2851
                }
2852
2853 167
                return $this->Literal();
2854
        }
2855
    }
2856
2857
    /**
2858
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2859
     *
2860
     * @return \Doctrine\ORM\Query\AST\StringPrimary |
2861
     *         \Doctrine\ORM\Query\AST\Subselect |
2862
     *         string
2863
     */
2864 14
    public function StringExpression()
2865
    {
2866 14
        $peek = $this->lexer->glimpse();
2867
2868
        // Subselect
2869 14
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
2870
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2871
            $expr = $this->Subselect();
2872
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2873
2874
            return $expr;
2875
        }
2876
2877
        // ResultVariable (string)
2878 14
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2879 14
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2880 2
            return $this->ResultVariable();
2881
        }
2882
2883 12
        return $this->StringPrimary();
2884
    }
2885
2886
    /**
2887
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2888
     */
2889 51
    public function StringPrimary()
2890
    {
2891 51
        $lookaheadType = $this->lexer->lookahead['type'];
2892
2893
        switch ($lookaheadType) {
2894 51
            case Lexer::T_IDENTIFIER:
2895 32
                $peek = $this->lexer->glimpse();
2896
2897 32
                if ($peek['value'] == '.') {
2898 32
                    return $this->StateFieldPathExpression();
2899
                }
2900
2901 8
                if ($peek['value'] == '(') {
2902
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2903 8
                    return $this->FunctionDeclaration();
2904
                }
2905
2906
                $this->syntaxError("'.' or '('");
2907
                break;
2908
2909 32
            case Lexer::T_STRING:
2910 32
                $this->match(Lexer::T_STRING);
2911
2912 32
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2913
2914 2
            case Lexer::T_INPUT_PARAMETER:
2915 2
                return $this->InputParameter();
2916
2917
            case Lexer::T_CASE:
2918
            case Lexer::T_COALESCE:
2919
            case Lexer::T_NULLIF:
2920
                return $this->CaseExpression();
2921
2922
            default:
2923
                if ($this->isAggregateFunction($lookaheadType)) {
2924
                    return $this->AggregateExpression();
2925
                }
2926
        }
2927
2928
        $this->syntaxError(
2929
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2930
        );
2931
    }
2932
2933
    /**
2934
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2935
     *
2936
     * @return \Doctrine\ORM\Query\AST\PathExpression |
2937
     *         \Doctrine\ORM\Query\AST\SimpleEntityExpression
2938
     */
2939 7
    public function EntityExpression()
2940
    {
2941 7
        $glimpse = $this->lexer->glimpse();
2942
2943 7
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2944 1
            return $this->SingleValuedAssociationPathExpression();
2945
        }
2946
2947 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 2947 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::EntityExpression of type Doctrine\ORM\Query\AST\PathExpression.
Loading history...
2948
    }
2949
2950
    /**
2951
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2952
     *
2953
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2954
     */
2955 6
    public function SimpleEntityExpression()
2956
    {
2957 6
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2958 5
            return $this->InputParameter();
2959
        }
2960
2961 1
        return $this->StateFieldPathExpression();
2962
    }
2963
2964
    /**
2965
     * AggregateExpression ::=
2966
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2967
     *
2968
     * @return \Doctrine\ORM\Query\AST\AggregateExpression
2969
     */
2970 84
    public function AggregateExpression()
2971
    {
2972 84
        $lookaheadType = $this->lexer->lookahead['type'];
2973
2974 84
        if ( ! in_array($lookaheadType, array(Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM))) {
2975
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2976
        }
2977
2978 84
        $this->match($lookaheadType);
2979 84
        $functionName = $this->lexer->token['value'];
2980 84
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2981
2982 84
        if ($isDistinct = $this->lexer->isNextToken(Lexer::T_DISTINCT)) {
2983 2
            $this->match(Lexer::T_DISTINCT);
2984
        }
2985
2986 84
        $pathExp = $this->SimpleArithmeticExpression();
2987
2988 84
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2989
2990 84
        return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
0 ignored issues
show
Bug introduced by
It seems like $pathExp defined by $this->SimpleArithmeticExpression() on line 2986 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...
2991
    }
2992
2993
    /**
2994
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
2995
     *
2996
     * @return \Doctrine\ORM\Query\AST\QuantifiedExpression
2997
     */
2998 3
    public function QuantifiedExpression()
2999
    {
3000 3
        $lookaheadType = $this->lexer->lookahead['type'];
3001 3
        $value = $this->lexer->lookahead['value'];
3002
3003 3
        if ( ! in_array($lookaheadType, array(Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME))) {
3004
            $this->syntaxError('ALL, ANY or SOME');
3005
        }
3006
3007 3
        $this->match($lookaheadType);
3008 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3009
3010 3
        $qExpr = new AST\QuantifiedExpression($this->Subselect());
3011 3
        $qExpr->type = $value;
3012
3013 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3014
3015 3
        return $qExpr;
3016
    }
3017
3018
    /**
3019
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3020
     *
3021
     * @return \Doctrine\ORM\Query\AST\BetweenExpression
3022
     */
3023 8
    public function BetweenExpression()
3024
    {
3025 8
        $arithExpr1 = $this->ArithmeticExpression();
3026
3027 8
        if ($not = $this->lexer->isNextToken(Lexer::T_NOT)) {
3028 3
            $this->match(Lexer::T_NOT);
3029
        }
3030
3031 8
        $this->match(Lexer::T_BETWEEN);
3032 8
        $arithExpr2 = $this->ArithmeticExpression();
3033 8
        $this->match(Lexer::T_AND);
3034 8
        $arithExpr3 = $this->ArithmeticExpression();
3035
3036 8
        $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3037 8
        $betweenExpr->not = $not;
3038
3039 8
        return $betweenExpr;
3040
    }
3041
3042
    /**
3043
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3044
     *
3045
     * @return \Doctrine\ORM\Query\AST\ComparisonExpression
3046
     */
3047 292
    public function ComparisonExpression()
3048
    {
3049 292
        $this->lexer->glimpse();
3050
3051 292
        $leftExpr  = $this->ArithmeticExpression();
3052 292
        $operator  = $this->ComparisonOperator();
3053 292
        $rightExpr = ($this->isNextAllAnySome())
3054 3
            ? $this->QuantifiedExpression()
3055 292
            : $this->ArithmeticExpression();
3056
3057 290
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3058
    }
3059
3060
    /**
3061
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3062
     *
3063
     * @return \Doctrine\ORM\Query\AST\InExpression
3064
     */
3065 34
    public function InExpression()
3066
    {
3067 34
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3068
3069 34
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3070 6
            $this->match(Lexer::T_NOT);
3071 6
            $inExpression->not = true;
3072
        }
3073
3074 34
        $this->match(Lexer::T_IN);
3075 34
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3076
3077 34
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3078 9
            $inExpression->subselect = $this->Subselect();
3079
        } else {
3080 25
            $literals = array();
3081 25
            $literals[] = $this->InParameter();
3082
3083 25
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3084 16
                $this->match(Lexer::T_COMMA);
3085 16
                $literals[] = $this->InParameter();
3086
            }
3087
3088 25
            $inExpression->literals = $literals;
3089
        }
3090
3091 33
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3092
3093 33
        return $inExpression;
3094
    }
3095
3096
    /**
3097
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3098
     *
3099
     * @return \Doctrine\ORM\Query\AST\InstanceOfExpression
3100
     */
3101 12
    public function InstanceOfExpression()
3102
    {
3103 12
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3104
3105 12
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3106 1
            $this->match(Lexer::T_NOT);
3107 1
            $instanceOfExpression->not = true;
3108
        }
3109
3110 12
        $this->match(Lexer::T_INSTANCE);
3111 12
        $this->match(Lexer::T_OF);
3112
3113 12
        $exprValues = array();
3114
3115 12
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3116 1
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3117
3118 1
            $exprValues[] = $this->InstanceOfParameter();
3119
3120 1
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3121 1
                $this->match(Lexer::T_COMMA);
3122
3123 1
                $exprValues[] = $this->InstanceOfParameter();
3124
            }
3125
3126 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3127
3128 1
            $instanceOfExpression->value = $exprValues;
3129
3130 1
            return $instanceOfExpression;
3131
        }
3132
3133 11
        $exprValues[] = $this->InstanceOfParameter();
3134
3135 11
        $instanceOfExpression->value = $exprValues;
3136
3137 11
        return $instanceOfExpression;
3138
    }
3139
3140
    /**
3141
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3142
     *
3143
     * @return mixed
3144
     */
3145 12
    public function InstanceOfParameter()
3146
    {
3147 12
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3148 5
            $this->match(Lexer::T_INPUT_PARAMETER);
3149
3150 5
            return new AST\InputParameter($this->lexer->token['value']);
3151
        }
3152
3153 7
        $abstractSchemaName = $this->AbstractSchemaName();
3154
3155 7
        $this->validateAbstractSchemaName($abstractSchemaName);
3156
3157 7
        return $abstractSchemaName;
3158
    }
3159
3160
    /**
3161
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3162
     *
3163
     * @return \Doctrine\ORM\Query\AST\LikeExpression
3164
     */
3165 14
    public function LikeExpression()
3166
    {
3167 14
        $stringExpr = $this->StringExpression();
3168
3169 14
        if ($not = $this->lexer->isNextToken(Lexer::T_NOT)) {
3170 3
            $this->match(Lexer::T_NOT);
3171
        }
3172
3173 14
        $this->match(Lexer::T_LIKE);
3174
3175 14
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3176 4
            $this->match(Lexer::T_INPUT_PARAMETER);
3177 4
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3178
        } else {
3179 11
            $stringPattern = $this->StringPrimary();
3180
        }
3181
3182 14
        $escapeChar = null;
3183
3184 14
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3185 2
            $this->match(Lexer::T_ESCAPE);
3186 2
            $this->match(Lexer::T_STRING);
3187
3188 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3189
        }
3190
3191 14
        $likeExpr = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $stringExpr defined by $this->StringExpression() on line 3167 can also be of type null or string; however, Doctrine\ORM\Query\AST\L...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...
Bug introduced by
It seems like $stringPattern defined by $this->StringPrimary() on line 3179 can also be of type null or object<Doctrine\ORM\Quer...ST\AggregateExpression> or object<Doctrine\ORM\Query\AST\CoalesceExpression> or object<Doctrine\ORM\Quer...Functions\FunctionNode> or object<Doctrine\ORM\Quer...\GeneralCaseExpression> or object<Doctrine\ORM\Query\AST\Literal> or object<Doctrine\ORM\Query\AST\NullIfExpression> or object<Doctrine\ORM\Query\AST\PathExpression> or object<Doctrine\ORM\Quer...T\SimpleCaseExpression>; however, Doctrine\ORM\Query\AST\L...pression::__construct() does only seem to accept object<Doctrine\ORM\Query\AST\InputParameter>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
3192 14
        $likeExpr->not = $not;
3193
3194 14
        return $likeExpr;
3195
    }
3196
3197
    /**
3198
     * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
3199
     *
3200
     * @return \Doctrine\ORM\Query\AST\NullComparisonExpression
3201
     */
3202 13
    public function NullComparisonExpression()
3203
    {
3204
        switch (true) {
3205 13
            case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER):
3206
                $this->match(Lexer::T_INPUT_PARAMETER);
3207
3208
                $expr = new AST\InputParameter($this->lexer->token['value']);
3209
                break;
3210
3211 13
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3212 1
                $expr = $this->NullIfExpression();
3213 1
                break;
3214
3215 13
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3216 1
                $expr = $this->CoalesceExpression();
3217 1
                break;
3218
3219 13
            case $this->isAggregateFunction($this->lexer->lookahead['type']):
3220 1
                $expr = $this->AggregateExpression();
3221 1
                break;
3222
3223 13
            case $this->isFunction():
3224 1
                $expr = $this->FunctionDeclaration();
3225 1
                break;
3226
3227
            default:
3228
                // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
3229 12
                $glimpse = $this->lexer->glimpse();
3230
3231 12
                if ($glimpse['type'] === Lexer::T_DOT) {
3232 9
                    $expr = $this->SingleValuedPathExpression();
3233
3234
                    // Leave switch statement
3235 9
                    break;
3236
                }
3237
3238 3
                $lookaheadValue = $this->lexer->lookahead['value'];
3239
3240
                // Validate existing component
3241 3
                if ( ! isset($this->queryComponents[$lookaheadValue])) {
3242
                    $this->semanticalError('Cannot add having condition on undefined result variable.');
3243
                }
3244
3245
                // Validating ResultVariable
3246 3
                if ( ! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3247
                    $this->semanticalError('Cannot add having condition on a non result variable.');
3248
                }
3249
3250 3
                $expr = $this->ResultVariable();
3251 3
                break;
3252
        }
3253
3254 13
        $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...
3255
3256 13
        $this->match(Lexer::T_IS);
3257
3258 13
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3259 4
            $this->match(Lexer::T_NOT);
3260
3261 4
            $nullCompExpr->not = true;
3262
        }
3263
3264 13
        $this->match(Lexer::T_NULL);
3265
3266 13
        return $nullCompExpr;
3267
    }
3268
3269
    /**
3270
     * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"
3271
     *
3272
     * @return \Doctrine\ORM\Query\AST\ExistsExpression
3273
     */
3274 7
    public function ExistsExpression()
3275
    {
3276 7
        if ($not = $this->lexer->isNextToken(Lexer::T_NOT)) {
3277
            $this->match(Lexer::T_NOT);
3278
        }
3279
3280 7
        $this->match(Lexer::T_EXISTS);
3281 7
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3282
3283 7
        $existsExpression = new AST\ExistsExpression($this->Subselect());
3284 7
        $existsExpression->not = $not;
3285
3286 7
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3287
3288 7
        return $existsExpression;
3289
    }
3290
3291
    /**
3292
     * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!="
3293
     *
3294
     * @return string
3295
     */
3296 292
    public function ComparisonOperator()
3297
    {
3298 292
        switch ($this->lexer->lookahead['value']) {
3299 292
            case '=':
3300 241
                $this->match(Lexer::T_EQUALS);
3301
3302 241
                return '=';
3303
3304 62
            case '<':
3305 17
                $this->match(Lexer::T_LOWER_THAN);
3306 17
                $operator = '<';
3307
3308 17
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3309 5
                    $this->match(Lexer::T_EQUALS);
3310 5
                    $operator .= '=';
3311 12
                } else if ($this->lexer->isNextToken(Lexer::T_GREATER_THAN)) {
3312 3
                    $this->match(Lexer::T_GREATER_THAN);
3313 3
                    $operator .= '>';
3314
                }
3315
3316 17
                return $operator;
3317
3318 53
            case '>':
3319 47
                $this->match(Lexer::T_GREATER_THAN);
3320 47
                $operator = '>';
3321
3322 47
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3323 6
                    $this->match(Lexer::T_EQUALS);
3324 6
                    $operator .= '=';
3325
                }
3326
3327 47
                return $operator;
3328
3329 6
            case '!':
3330 6
                $this->match(Lexer::T_NEGATE);
3331 6
                $this->match(Lexer::T_EQUALS);
3332
3333 6
                return '<>';
3334
3335
            default:
3336
                $this->syntaxError('=, <, <=, <>, >, >=, !=');
3337
        }
3338
    }
3339
3340
    /**
3341
     * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
3342
     *
3343
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3344
     */
3345 59
    public function FunctionDeclaration()
3346
    {
3347 59
        $token = $this->lexer->lookahead;
3348 59
        $funcName = strtolower($token['value']);
3349
3350
        // Check for built-in functions first!
3351
        switch (true) {
3352 59
            case (isset(self::$_STRING_FUNCTIONS[$funcName])):
3353 30
                return $this->FunctionsReturningStrings();
3354
3355 32
            case (isset(self::$_NUMERIC_FUNCTIONS[$funcName])):
3356 23
                return $this->FunctionsReturningNumerics();
3357
3358 10
            case (isset(self::$_DATETIME_FUNCTIONS[$funcName])):
3359 7
                return $this->FunctionsReturningDatetime();
3360
3361
            default:
3362 3
                return $this->CustomFunctionDeclaration();
3363
        }
3364
    }
3365
3366
    /**
3367
     * Helper function for FunctionDeclaration grammar rule.
3368
     *
3369
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3370
     */
3371 3
    private function CustomFunctionDeclaration()
3372
    {
3373 3
        $token = $this->lexer->lookahead;
3374 3
        $funcName = strtolower($token['value']);
3375
3376
        // Check for custom functions afterwards
3377 3
        $config = $this->em->getConfiguration();
3378
3379
        switch (true) {
3380 3
            case ($config->getCustomStringFunction($funcName) !== null):
3381 2
                return $this->CustomFunctionsReturningStrings();
3382
3383 2
            case ($config->getCustomNumericFunction($funcName) !== null):
3384 2
                return $this->CustomFunctionsReturningNumerics();
3385
3386
            case ($config->getCustomDatetimeFunction($funcName) !== null):
3387
                return $this->CustomFunctionsReturningDatetime();
3388
3389
            default:
3390
                $this->syntaxError('known function', $token);
3391
        }
3392
    }
3393
3394
    /**
3395
     * FunctionsReturningNumerics ::=
3396
     *      "LENGTH" "(" StringPrimary ")" |
3397
     *      "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" |
3398
     *      "ABS" "(" SimpleArithmeticExpression ")" |
3399
     *      "SQRT" "(" SimpleArithmeticExpression ")" |
3400
     *      "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3401
     *      "SIZE" "(" CollectionValuedPathExpression ")" |
3402
     *      "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3403
     *      "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3404
     *      "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
3405
     *
3406
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3407
     */
3408 23
    public function FunctionsReturningNumerics()
3409
    {
3410 23
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3411 23
        $funcClass     = self::$_NUMERIC_FUNCTIONS[$funcNameLower];
3412
3413 23
        $function = new $funcClass($funcNameLower);
3414 23
        $function->parse($this);
3415
3416 23
        return $function;
3417
    }
3418
3419
    /**
3420
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3421
     */
3422 2
    public function CustomFunctionsReturningNumerics()
3423
    {
3424
        // getCustomNumericFunction is case-insensitive
3425 2
        $functionName  = strtolower($this->lexer->lookahead['value']);
3426 2
        $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName);
3427
3428 2
        $function = is_string($functionClass)
3429 1
            ? new $functionClass($functionName)
3430 2
            : call_user_func($functionClass, $functionName);
3431
3432 2
        $function->parse($this);
3433
3434 2
        return $function;
3435
    }
3436
3437
    /**
3438
     * FunctionsReturningDateTime ::=
3439
     *     "CURRENT_DATE" |
3440
     *     "CURRENT_TIME" |
3441
     *     "CURRENT_TIMESTAMP" |
3442
     *     "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" |
3443
     *     "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"
3444
     *
3445
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3446
     */
3447 7
    public function FunctionsReturningDatetime()
3448
    {
3449 7
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3450 7
        $funcClass     = self::$_DATETIME_FUNCTIONS[$funcNameLower];
3451
3452 7
        $function = new $funcClass($funcNameLower);
3453 7
        $function->parse($this);
3454
3455 7
        return $function;
3456
    }
3457
3458
    /**
3459
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3460
     */
3461
    public function CustomFunctionsReturningDatetime()
3462
    {
3463
        // getCustomDatetimeFunction is case-insensitive
3464
        $functionName  = $this->lexer->lookahead['value'];
3465
        $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName);
3466
3467
        $function = is_string($functionClass)
3468
            ? new $functionClass($functionName)
3469
            : call_user_func($functionClass, $functionName);
3470
3471
        $function->parse($this);
3472
3473
        return $function;
3474
    }
3475
3476
    /**
3477
     * FunctionsReturningStrings ::=
3478
     *   "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" |
3479
     *   "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3480
     *   "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" |
3481
     *   "LOWER" "(" StringPrimary ")" |
3482
     *   "UPPER" "(" StringPrimary ")" |
3483
     *   "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"
3484
     *
3485
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3486
     */
3487 30
    public function FunctionsReturningStrings()
3488
    {
3489 30
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3490 30
        $funcClass     = self::$_STRING_FUNCTIONS[$funcNameLower];
3491
3492 30
        $function = new $funcClass($funcNameLower);
3493 30
        $function->parse($this);
3494
3495 30
        return $function;
3496
    }
3497
3498
    /**
3499
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3500
     */
3501 2
    public function CustomFunctionsReturningStrings()
3502
    {
3503
        // getCustomStringFunction is case-insensitive
3504 2
        $functionName  = $this->lexer->lookahead['value'];
3505 2
        $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName);
3506
3507 2
        $function = is_string($functionClass)
3508 1
            ? new $functionClass($functionName)
3509 2
            : call_user_func($functionClass, $functionName);
3510
3511 2
        $function->parse($this);
3512
3513 2
        return $function;
3514
    }
3515
}
3516