Completed
Push — master ( 32ea91...ffd146 )
by Marco
10s
created

Parser::StringPrimary()   D

Complexity

Conditions 10
Paths 10

Size

Total Lines 43
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 19.626

Importance

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

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Query;
21
22
use Doctrine\ORM\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 501
    public function SingleValuedPathExpression()
1131
    {
1132 501
        return $this->PathExpression(
1133 501
            AST\PathExpression::TYPE_STATE_FIELD |
1134 501
            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
        $isDistinct = false;
1176 750
        $this->match(Lexer::T_SELECT);
1177
1178
        // Check for DISTINCT
1179 750
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1180 6
            $this->match(Lexer::T_DISTINCT);
1181
1182 6
            $isDistinct = true;
1183
        }
1184
1185
        // Process SelectExpressions (1..N)
1186 750
        $selectExpressions = array();
1187 750
        $selectExpressions[] = $this->SelectExpression();
1188
1189 742
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1190 282
            $this->match(Lexer::T_COMMA);
1191
1192 282
            $selectExpressions[] = $this->SelectExpression();
1193
        }
1194
1195 741
        return new AST\SelectClause($selectExpressions, $isDistinct);
1196
    }
1197
1198
    /**
1199
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1200
     *
1201
     * @return \Doctrine\ORM\Query\AST\SimpleSelectClause
1202
     */
1203 48
    public function SimpleSelectClause()
1204
    {
1205 48
        $isDistinct = false;
1206 48
        $this->match(Lexer::T_SELECT);
1207
1208 48
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1209
            $this->match(Lexer::T_DISTINCT);
1210
1211
            $isDistinct = true;
1212
        }
1213
1214 48
        return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1215
    }
1216
1217
    /**
1218
     * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1219
     *
1220
     * @return \Doctrine\ORM\Query\AST\UpdateClause
1221
     */
1222 32
    public function UpdateClause()
1223
    {
1224 32
        $this->match(Lexer::T_UPDATE);
1225
1226 32
        $token = $this->lexer->lookahead;
1227 32
        $abstractSchemaName = $this->AbstractSchemaName();
1228
1229 32
        $this->validateAbstractSchemaName($abstractSchemaName);
1230
1231 32
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1232 2
            $this->match(Lexer::T_AS);
1233
        }
1234
1235 32
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1236
1237 32
        $class = $this->em->getClassMetadata($abstractSchemaName);
1238
1239
        // Building queryComponent
1240
        $queryComponent = array(
1241 32
            'metadata'     => $class,
1242
            'parent'       => null,
1243
            'relation'     => null,
1244
            'map'          => null,
1245 32
            'nestingLevel' => $this->nestingLevel,
1246 32
            'token'        => $token,
1247
        );
1248
1249 32
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1250
1251 32
        $this->match(Lexer::T_SET);
1252
1253 32
        $updateItems = array();
1254 32
        $updateItems[] = $this->UpdateItem();
1255
1256 32
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1257 5
            $this->match(Lexer::T_COMMA);
1258
1259 5
            $updateItems[] = $this->UpdateItem();
1260
        }
1261
1262 32
        $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1263 32
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1264
1265 32
        return $updateClause;
1266
    }
1267
1268
    /**
1269
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1270
     *
1271
     * @return \Doctrine\ORM\Query\AST\DeleteClause
1272
     */
1273 42
    public function DeleteClause()
1274
    {
1275 42
        $this->match(Lexer::T_DELETE);
1276
1277 42
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1278 8
            $this->match(Lexer::T_FROM);
1279
        }
1280
1281 42
        $token = $this->lexer->lookahead;
1282 42
        $abstractSchemaName = $this->AbstractSchemaName();
1283
1284 42
        $this->validateAbstractSchemaName($abstractSchemaName);
1285
1286 42
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1287
1288 42
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1289 1
            $this->match(Lexer::T_AS);
1290
        }
1291
1292 42
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1293
1294 41
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1295 41
        $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1296
1297
        // Building queryComponent
1298
        $queryComponent = array(
1299 41
            'metadata'     => $class,
1300
            'parent'       => null,
1301
            'relation'     => null,
1302
            'map'          => null,
1303 41
            'nestingLevel' => $this->nestingLevel,
1304 41
            'token'        => $token,
1305
        );
1306
1307 41
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1308
1309 41
        return $deleteClause;
1310
    }
1311
1312
    /**
1313
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1314
     *
1315
     * @return \Doctrine\ORM\Query\AST\FromClause
1316
     */
1317 741
    public function FromClause()
1318
    {
1319 741
        $this->match(Lexer::T_FROM);
1320
1321 736
        $identificationVariableDeclarations = array();
1322 736
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1323
1324 719
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1325 6
            $this->match(Lexer::T_COMMA);
1326
1327 6
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1328
        }
1329
1330 719
        return new AST\FromClause($identificationVariableDeclarations);
1331
    }
1332
1333
    /**
1334
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1335
     *
1336
     * @return \Doctrine\ORM\Query\AST\SubselectFromClause
1337
     */
1338 48
    public function SubselectFromClause()
1339
    {
1340 48
        $this->match(Lexer::T_FROM);
1341
1342 48
        $identificationVariables = array();
1343 48
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1344
1345 47
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1346
            $this->match(Lexer::T_COMMA);
1347
1348
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1349
        }
1350
1351 47
        return new AST\SubselectFromClause($identificationVariables);
1352
    }
1353
1354
    /**
1355
     * WhereClause ::= "WHERE" ConditionalExpression
1356
     *
1357
     * @return \Doctrine\ORM\Query\AST\WhereClause
1358
     */
1359 327
    public function WhereClause()
1360
    {
1361 327
        $this->match(Lexer::T_WHERE);
1362
1363 327
        return new AST\WhereClause($this->ConditionalExpression());
1364
    }
1365
1366
    /**
1367
     * HavingClause ::= "HAVING" ConditionalExpression
1368
     *
1369
     * @return \Doctrine\ORM\Query\AST\HavingClause
1370
     */
1371 21
    public function HavingClause()
1372
    {
1373 21
        $this->match(Lexer::T_HAVING);
1374
1375 21
        return new AST\HavingClause($this->ConditionalExpression());
1376
    }
1377
1378
    /**
1379
     * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1380
     *
1381
     * @return \Doctrine\ORM\Query\AST\GroupByClause
1382
     */
1383 31
    public function GroupByClause()
1384
    {
1385 31
        $this->match(Lexer::T_GROUP);
1386 31
        $this->match(Lexer::T_BY);
1387
1388 31
        $groupByItems = array($this->GroupByItem());
1389
1390 30
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1391 8
            $this->match(Lexer::T_COMMA);
1392
1393 8
            $groupByItems[] = $this->GroupByItem();
1394
        }
1395
1396 30
        return new AST\GroupByClause($groupByItems);
1397
    }
1398
1399
    /**
1400
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1401
     *
1402
     * @return \Doctrine\ORM\Query\AST\OrderByClause
1403
     */
1404 179
    public function OrderByClause()
1405
    {
1406 179
        $this->match(Lexer::T_ORDER);
1407 179
        $this->match(Lexer::T_BY);
1408
1409 179
        $orderByItems = array();
1410 179
        $orderByItems[] = $this->OrderByItem();
1411
1412 179
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1413 14
            $this->match(Lexer::T_COMMA);
1414
1415 14
            $orderByItems[] = $this->OrderByItem();
1416
        }
1417
1418 179
        return new AST\OrderByClause($orderByItems);
1419
    }
1420
1421
    /**
1422
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1423
     *
1424
     * @return \Doctrine\ORM\Query\AST\Subselect
1425
     */
1426 48
    public function Subselect()
1427
    {
1428
        // Increase query nesting level
1429 48
        $this->nestingLevel++;
1430
1431 48
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1432
1433 47
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1434 47
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1435 47
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1436 47
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1437
1438
        // Decrease query nesting level
1439 47
        $this->nestingLevel--;
1440
1441 47
        return $subselect;
1442
    }
1443
1444
    /**
1445
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1446
     *
1447
     * @return \Doctrine\ORM\Query\AST\UpdateItem
1448
     */
1449 32
    public function UpdateItem()
1450
    {
1451 32
        $pathExpr = $this->SingleValuedPathExpression();
1452
1453 32
        $this->match(Lexer::T_EQUALS);
1454
1455 32
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1456
1457 32
        return $updateItem;
1458
    }
1459
1460
    /**
1461
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1462
     *
1463
     * @return string | \Doctrine\ORM\Query\AST\PathExpression
1464
     */
1465 31
    public function GroupByItem()
1466
    {
1467
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1468 31
        $glimpse = $this->lexer->glimpse();
1469
1470 31
        if ($glimpse['type'] === Lexer::T_DOT) {
1471 12
            return $this->SingleValuedPathExpression();
1472
        }
1473
1474
        // Still need to decide between IdentificationVariable or ResultVariable
1475 19
        $lookaheadValue = $this->lexer->lookahead['value'];
1476
1477 19
        if ( ! isset($this->queryComponents[$lookaheadValue])) {
1478 1
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1479
        }
1480
1481 18
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1482 16
            ? $this->IdentificationVariable()
1483 18
            : $this->ResultVariable();
1484
    }
1485
1486
    /**
1487
     * OrderByItem ::= (
1488
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1489
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1490
     * ) ["ASC" | "DESC"]
1491
     *
1492
     * @return \Doctrine\ORM\Query\AST\OrderByItem
1493
     */
1494 179
    public function OrderByItem()
1495
    {
1496 179
        $this->lexer->peek(); // lookahead => '.'
1497 179
        $this->lexer->peek(); // lookahead => token after '.'
1498
1499 179
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1500
1501 179
        $this->lexer->resetPeek();
1502
1503 179
        $glimpse = $this->lexer->glimpse();
1504
1505
        switch (true) {
1506 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...
1507 1
                $expr = $this->FunctionDeclaration();
1508 1
                break;
1509
1510 178
            case ($this->isMathOperator($peek)):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1499 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...
1511 25
                $expr = $this->SimpleArithmeticExpression();
1512 25
                break;
1513
1514 154
            case ($glimpse['type'] === Lexer::T_DOT):
1515 141
                $expr = $this->SingleValuedPathExpression();
1516 141
                break;
1517
1518 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...
1519 2
                $expr = $this->ScalarExpression();
1520 2
                break;
1521
1522
            default:
1523 15
                $expr = $this->ResultVariable();
1524 15
                break;
1525
        }
1526
1527 179
        $type = 'ASC';
1528 179
        $item = new AST\OrderByItem($expr);
1529
1530
        switch (true) {
1531 179
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1532 93
                $this->match(Lexer::T_DESC);
1533 93
                $type = 'DESC';
1534 93
                break;
1535
1536 152
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1537 96
                $this->match(Lexer::T_ASC);
1538 96
                break;
1539
1540
            default:
1541
                // Do nothing
1542
        }
1543
1544 179
        $item->type = $type;
1545
1546 179
        return $item;
1547
    }
1548
1549
    /**
1550
     * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1551
     *      EnumPrimary | SimpleEntityExpression | "NULL"
1552
     *
1553
     * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1554
     * grammar that needs to be supported:
1555
     *
1556
     * NewValue ::= SimpleArithmeticExpression | "NULL"
1557
     *
1558
     * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1559
     *
1560
     * @return AST\ArithmeticExpression
1561
     */
1562 32
    public function NewValue()
1563
    {
1564 32
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1565 1
            $this->match(Lexer::T_NULL);
1566
1567 1
            return null;
1568
        }
1569
1570 31
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1571 18
            $this->match(Lexer::T_INPUT_PARAMETER);
1572
1573 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...
1574
        }
1575
1576 13
        return $this->ArithmeticExpression();
1577
    }
1578
1579
    /**
1580
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1581
     *
1582
     * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1583
     */
1584 738
    public function IdentificationVariableDeclaration()
1585
    {
1586 738
        $joins                    = array();
1587 738
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1588 723
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1589 7
            ? $this->IndexBy()
1590 723
            : null;
1591
1592 723
        $rangeVariableDeclaration->isRoot = true;
1593
1594
        while (
1595 723
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1596 723
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1597 723
            $this->lexer->isNextToken(Lexer::T_JOIN)
1598
        ) {
1599 275
            $joins[] = $this->Join();
1600
        }
1601
1602 721
        return new AST\IdentificationVariableDeclaration(
1603
            $rangeVariableDeclaration, $indexBy, $joins
1604
        );
1605
    }
1606
1607
    /**
1608
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1609
     *
1610
     * {Internal note: WARNING: Solution is harder than a bare implementation.
1611
     * Desired EBNF support:
1612
     *
1613
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1614
     *
1615
     * It demands that entire SQL generation to become programmatical. This is
1616
     * needed because association based subselect requires "WHERE" conditional
1617
     * expressions to be injected, but there is no scope to do that. Only scope
1618
     * accessible is "FROM", prohibiting an easy implementation without larger
1619
     * changes.}
1620
     *
1621
     * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration |
1622
     *         \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1623
     */
1624 48
    public function SubselectIdentificationVariableDeclaration()
1625
    {
1626
        /*
1627
        NOT YET IMPLEMENTED!
1628
1629
        $glimpse = $this->lexer->glimpse();
1630
1631
        if ($glimpse['type'] == Lexer::T_DOT) {
1632
            $associationPathExpression = $this->AssociationPathExpression();
1633
1634
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1635
                $this->match(Lexer::T_AS);
1636
            }
1637
1638
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1639
            $identificationVariable      = $associationPathExpression->identificationVariable;
1640
            $field                       = $associationPathExpression->associationField;
1641
1642
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1643
            $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1644
1645
            // Building queryComponent
1646
            $joinQueryComponent = array(
1647
                'metadata'     => $targetClass,
1648
                'parent'       => $identificationVariable,
1649
                'relation'     => $class->getAssociationMapping($field),
1650
                'map'          => null,
1651
                'nestingLevel' => $this->nestingLevel,
1652
                'token'        => $this->lexer->lookahead
1653
            );
1654
1655
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1656
1657
            return new AST\SubselectIdentificationVariableDeclaration(
1658
                $associationPathExpression, $aliasIdentificationVariable
1659
            );
1660
        }
1661
        */
1662
1663 48
        return $this->IdentificationVariableDeclaration();
1664
    }
1665
1666
    /**
1667
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1668
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1669
     *          ["WITH" ConditionalExpression]
1670
     *
1671
     * @return \Doctrine\ORM\Query\AST\Join
1672
     */
1673 275
    public function Join()
1674
    {
1675
        // Check Join type
1676 275
        $joinType = AST\Join::JOIN_TYPE_INNER;
1677
1678
        switch (true) {
1679 275
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1680 65
                $this->match(Lexer::T_LEFT);
1681
1682 65
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1683
1684
                // Possible LEFT OUTER join
1685 65
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1686
                    $this->match(Lexer::T_OUTER);
1687
1688
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1689
                }
1690 65
                break;
1691
1692 214
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1693 20
                $this->match(Lexer::T_INNER);
1694 20
                break;
1695
1696
            default:
1697
                // Do nothing
1698
        }
1699
1700 275
        $this->match(Lexer::T_JOIN);
1701
1702 275
        $next            = $this->lexer->glimpse();
1703 275
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1704 273
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1705 273
        $join            = new AST\Join($joinType, $joinDeclaration);
1706
1707
        // Describe non-root join declaration
1708 273
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1709 21
            $joinDeclaration->isRoot = false;
1710
        }
1711
1712
        // Check for ad-hoc Join conditions
1713 273
        if ($adhocConditions) {
1714 23
            $this->match(Lexer::T_WITH);
1715
1716 23
            $join->conditionalExpression = $this->ConditionalExpression();
1717
        }
1718
1719 273
        return $join;
1720
    }
1721
1722
    /**
1723
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1724
     *
1725
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1726
     */
1727 738
    public function RangeVariableDeclaration()
1728
    {
1729 738
        $abstractSchemaName = $this->AbstractSchemaName();
1730
1731 738
        $this->validateAbstractSchemaName($abstractSchemaName);
1732
1733 723
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1734 5
            $this->match(Lexer::T_AS);
1735
        }
1736
1737 723
        $token = $this->lexer->lookahead;
1738 723
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1739 723
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1740
1741
        // Building queryComponent
1742
        $queryComponent = array(
1743 723
            'metadata'     => $classMetadata,
1744
            'parent'       => null,
1745
            'relation'     => null,
1746
            'map'          => null,
1747 723
            'nestingLevel' => $this->nestingLevel,
1748 723
            'token'        => $token
1749
        );
1750
1751 723
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1752
1753 723
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1754
    }
1755
1756
    /**
1757
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1758
     *
1759
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1760
     */
1761 254
    public function JoinAssociationDeclaration()
1762
    {
1763 254
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1764
1765 254
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1766 4
            $this->match(Lexer::T_AS);
1767
        }
1768
1769 254
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1770 252
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1771
1772 252
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1773 252
        $field                  = $joinAssociationPathExpression->associationField;
1774
1775 252
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1776 252
        $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1777
1778
        // Building queryComponent
1779
        $joinQueryComponent = array(
1780 252
            'metadata'     => $targetClass,
1781 252
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1782 252
            'relation'     => $class->getAssociationMapping($field),
1783
            'map'          => null,
1784 252
            'nestingLevel' => $this->nestingLevel,
1785 252
            'token'        => $this->lexer->lookahead
1786
        );
1787
1788 252
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1789
1790 252
        return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
1791
    }
1792
1793
    /**
1794
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1795
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1796
     *
1797
     * @return array
1798
     */
1799 11
    public function PartialObjectExpression()
1800
    {
1801 11
        $this->match(Lexer::T_PARTIAL);
1802
1803 11
        $partialFieldSet = array();
1804
1805 11
        $identificationVariable = $this->IdentificationVariable();
1806
1807 11
        $this->match(Lexer::T_DOT);
1808 11
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1809 11
        $this->match(Lexer::T_IDENTIFIER);
1810
1811 11
        $field = $this->lexer->token['value'];
1812
1813
        // First field in partial expression might be embeddable property
1814 11
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1815 1
            $this->match(Lexer::T_DOT);
1816 1
            $this->match(Lexer::T_IDENTIFIER);
1817 1
            $field .= '.'.$this->lexer->token['value'];
1818
        }
1819
1820 11
        $partialFieldSet[] = $field;
1821
1822 11
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1823 9
            $this->match(Lexer::T_COMMA);
1824 9
            $this->match(Lexer::T_IDENTIFIER);
1825
    
1826 9
            $field = $this->lexer->token['value'];
1827
    
1828 9
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1829 2
                $this->match(Lexer::T_DOT);
1830 2
                $this->match(Lexer::T_IDENTIFIER);
1831 2
                $field .= '.'.$this->lexer->token['value'];
1832
            }
1833
    
1834 9
            $partialFieldSet[] = $field;
1835
        }
1836
1837 11
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1838
1839 11
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1840
1841
        // Defer PartialObjectExpression validation
1842 11
        $this->deferredPartialObjectExpressions[] = array(
1843 11
            'expression'   => $partialObjectExpression,
1844 11
            'nestingLevel' => $this->nestingLevel,
1845 11
            'token'        => $this->lexer->token,
1846
        );
1847
1848 11
        return $partialObjectExpression;
1849
    }
1850
1851
    /**
1852
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1853
     *
1854
     * @return \Doctrine\ORM\Query\AST\NewObjectExpression
1855
     */
1856 28
    public function NewObjectExpression()
1857
    {
1858 28
        $this->match(Lexer::T_NEW);
1859
1860 28
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1861 28
        $token = $this->lexer->token;
1862
1863 28
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1864
1865 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...
1866
1867 28
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1868 24
            $this->match(Lexer::T_COMMA);
1869
1870 24
            $args[] = $this->NewObjectArg();
1871
        }
1872
1873 28
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1874
1875 28
        $expression = new AST\NewObjectExpression($className, $args);
1876
1877
        // Defer NewObjectExpression validation
1878 28
        $this->deferredNewObjectExpressions[] = array(
1879 28
            'token'        => $token,
1880 28
            'expression'   => $expression,
1881 28
            'nestingLevel' => $this->nestingLevel,
1882
        );
1883
1884 28
        return $expression;
1885
    }
1886
1887
    /**
1888
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1889
     *
1890
     * @return mixed
1891
     */
1892 28
    public function NewObjectArg()
1893
    {
1894 28
        $token = $this->lexer->lookahead;
1895 28
        $peek  = $this->lexer->glimpse();
1896
1897 28
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1898 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1899 2
            $expression = $this->Subselect();
1900 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1901
1902 2
            return $expression;
1903
        }
1904
1905 28
        return $this->ScalarExpression();
1906
    }
1907
1908
    /**
1909
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1910
     *
1911
     * @return \Doctrine\ORM\Query\AST\IndexBy
1912
     */
1913 11
    public function IndexBy()
1914
    {
1915 11
        $this->match(Lexer::T_INDEX);
1916 11
        $this->match(Lexer::T_BY);
1917 11
        $pathExpr = $this->StateFieldPathExpression();
1918
1919
        // Add the INDEX BY info to the query component
1920 11
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1921
1922 11
        return new AST\IndexBy($pathExpr);
1923
    }
1924
1925
    /**
1926
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1927
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1928
     *                      InstanceOfExpression
1929
     *
1930
     * @return mixed One of the possible expressions or subexpressions.
1931
     */
1932 160
    public function ScalarExpression()
1933
    {
1934 160
        $lookahead = $this->lexer->lookahead['type'];
1935 160
        $peek      = $this->lexer->glimpse();
1936
1937
        switch (true) {
1938 160
            case ($lookahead === Lexer::T_INTEGER):
1939 157
            case ($lookahead === Lexer::T_FLOAT):
1940
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1941 157
            case ($lookahead === Lexer::T_MINUS):
1942 157
            case ($lookahead === Lexer::T_PLUS):
1943 17
                return $this->SimpleArithmeticExpression();
1944
1945 157
            case ($lookahead === Lexer::T_STRING):
1946 13
                return $this->StringPrimary();
1947
1948 155
            case ($lookahead === Lexer::T_TRUE):
1949 155
            case ($lookahead === Lexer::T_FALSE):
1950 3
                $this->match($lookahead);
1951
1952 3
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1953
1954 155
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
1955
                switch (true) {
1956 1
                    case $this->isMathOperator($peek):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->glimpse() on line 1935 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...
1957
                        // :param + u.value
1958 1
                        return $this->SimpleArithmeticExpression();
1959
                    default:
1960
                        return $this->InputParameter();
1961
                }
1962
1963 155
            case ($lookahead === Lexer::T_CASE):
1964 151
            case ($lookahead === Lexer::T_COALESCE):
1965 151
            case ($lookahead === Lexer::T_NULLIF):
1966
                // Since NULLIF and COALESCE can be identified as a function,
1967
                // we need to check these before checking for FunctionDeclaration
1968 8
                return $this->CaseExpression();
1969
1970 151
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1971 4
                return $this->SimpleArithmeticExpression();
1972
1973
            // this check must be done before checking for a filed path expression
1974 148
            case ($this->isFunction()):
1975 26
                $this->lexer->peek(); // "("
1976
1977
                switch (true) {
1978 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...
1979
                        // SUM(u.id) + COUNT(u.id)
1980 7
                        return $this->SimpleArithmeticExpression();
1981
1982 21
                    case ($this->isAggregateFunction($this->lexer->lookahead['type'])):
1983 19
                        return $this->AggregateExpression();
1984
1985
                    default:
1986
                        // IDENTITY(u)
1987 2
                        return $this->FunctionDeclaration();
1988
                }
1989
1990
                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...
1991
            // it is no function, so it must be a field path
1992 130
            case ($lookahead === Lexer::T_IDENTIFIER):
1993 130
                $this->lexer->peek(); // lookahead => '.'
1994 130
                $this->lexer->peek(); // lookahead => token after '.'
1995 130
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1996 130
                $this->lexer->resetPeek();
1997
1998 130
                if ($this->isMathOperator($peek)) {
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1995 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...
1999 7
                    return $this->SimpleArithmeticExpression();
2000
                }
2001
2002 125
                return $this->StateFieldPathExpression();
2003
2004
            default:
2005
                $this->syntaxError();
2006
        }
2007
    }
2008
2009
    /**
2010
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
2011
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2012
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2013
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2014
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2015
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2016
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2017
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2018
     *
2019
     * @return mixed One of the possible expressions or subexpressions.
2020
     */
2021 19
    public function CaseExpression()
2022
    {
2023 19
        $lookahead = $this->lexer->lookahead['type'];
2024
2025
        switch ($lookahead) {
2026 19
            case Lexer::T_NULLIF:
2027 5
                return $this->NullIfExpression();
2028
2029 16
            case Lexer::T_COALESCE:
2030 2
                return $this->CoalesceExpression();
2031
2032 14
            case Lexer::T_CASE:
2033 14
                $this->lexer->resetPeek();
2034 14
                $peek = $this->lexer->peek();
2035
2036 14
                if ($peek['type'] === Lexer::T_WHEN) {
2037 9
                    return $this->GeneralCaseExpression();
2038
                }
2039
2040 5
                return $this->SimpleCaseExpression();
2041
2042
            default:
2043
                // Do nothing
2044
                break;
2045
        }
2046
2047
        $this->syntaxError();
2048
    }
2049
2050
    /**
2051
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2052
     *
2053
     * @return \Doctrine\ORM\Query\AST\CoalesceExpression
2054
     */
2055 3
    public function CoalesceExpression()
2056
    {
2057 3
        $this->match(Lexer::T_COALESCE);
2058 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2059
2060
        // Process ScalarExpressions (1..N)
2061 3
        $scalarExpressions = array();
2062 3
        $scalarExpressions[] = $this->ScalarExpression();
2063
2064 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2065 3
            $this->match(Lexer::T_COMMA);
2066
2067 3
            $scalarExpressions[] = $this->ScalarExpression();
2068
        }
2069
2070 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2071
2072 3
        return new AST\CoalesceExpression($scalarExpressions);
2073
    }
2074
2075
    /**
2076
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2077
     *
2078
     * @return \Doctrine\ORM\Query\AST\NullIfExpression
2079
     */
2080 5
    public function NullIfExpression()
2081
    {
2082 5
        $this->match(Lexer::T_NULLIF);
2083 5
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2084
2085 5
        $firstExpression = $this->ScalarExpression();
2086 5
        $this->match(Lexer::T_COMMA);
2087 5
        $secondExpression = $this->ScalarExpression();
2088
2089 5
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2090
2091 5
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2092
    }
2093
2094
    /**
2095
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2096
     *
2097
     * @return \Doctrine\ORM\Query\AST\GeneralCaseExpression
2098
     */
2099 9
    public function GeneralCaseExpression()
2100
    {
2101 9
        $this->match(Lexer::T_CASE);
2102
2103
        // Process WhenClause (1..N)
2104 9
        $whenClauses = array();
2105
2106
        do {
2107 9
            $whenClauses[] = $this->WhenClause();
2108 9
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2109
2110 9
        $this->match(Lexer::T_ELSE);
2111 9
        $scalarExpression = $this->ScalarExpression();
2112 9
        $this->match(Lexer::T_END);
2113
2114 9
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2115
    }
2116
2117
    /**
2118
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2119
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2120
     *
2121
     * @return AST\SimpleCaseExpression
2122
     */
2123 5
    public function SimpleCaseExpression()
2124
    {
2125 5
        $this->match(Lexer::T_CASE);
2126 5
        $caseOperand = $this->StateFieldPathExpression();
2127
2128
        // Process SimpleWhenClause (1..N)
2129 5
        $simpleWhenClauses = array();
2130
2131
        do {
2132 5
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2133 5
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2134
2135 5
        $this->match(Lexer::T_ELSE);
2136 5
        $scalarExpression = $this->ScalarExpression();
2137 5
        $this->match(Lexer::T_END);
2138
2139 5
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2140
    }
2141
2142
    /**
2143
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2144
     *
2145
     * @return \Doctrine\ORM\Query\AST\WhenClause
2146
     */
2147 9
    public function WhenClause()
2148
    {
2149 9
        $this->match(Lexer::T_WHEN);
2150 9
        $conditionalExpression = $this->ConditionalExpression();
2151 9
        $this->match(Lexer::T_THEN);
2152
2153 9
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2154
    }
2155
2156
    /**
2157
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2158
     *
2159
     * @return \Doctrine\ORM\Query\AST\SimpleWhenClause
2160
     */
2161 5
    public function SimpleWhenClause()
2162
    {
2163 5
        $this->match(Lexer::T_WHEN);
2164 5
        $conditionalExpression = $this->ScalarExpression();
2165 5
        $this->match(Lexer::T_THEN);
2166
2167 5
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2168
    }
2169
2170
    /**
2171
     * SelectExpression ::= (
2172
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2173
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2174
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2175
     *
2176
     * @return \Doctrine\ORM\Query\AST\SelectExpression
2177
     */
2178 750
    public function SelectExpression()
2179
    {
2180 750
        $expression    = null;
2181 750
        $identVariable = null;
2182 750
        $peek          = $this->lexer->glimpse();
2183 750
        $lookaheadType = $this->lexer->lookahead['type'];
2184
2185
        switch (true) {
2186
            // ScalarExpression (u.name)
2187 750
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2188 102
                $expression = $this->ScalarExpression();
2189 102
                break;
2190
2191
            // IdentificationVariable (u)
2192 691
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2193 576
                $expression = $identVariable = $this->IdentificationVariable();
2194 576
                break;
2195
2196
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
2197 176
            case ($lookaheadType === Lexer::T_CASE):
2198 171
            case ($lookaheadType === Lexer::T_COALESCE):
2199 169
            case ($lookaheadType === Lexer::T_NULLIF):
2200 9
                $expression = $this->CaseExpression();
2201 9
                break;
2202
2203
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2204 167
            case ($this->isFunction()):
2205 87
                $this->lexer->peek(); // "("
2206
2207
                switch (true) {
2208 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...
2209
                        // SUM(u.id) + COUNT(u.id)
2210 2
                        $expression = $this->ScalarExpression();
2211 2
                        break;
2212
2213 85
                    case ($this->isAggregateFunction($lookaheadType)):
2214
                        // COUNT(u.id)
2215 54
                        $expression = $this->AggregateExpression();
2216 54
                        break;
2217
2218
                    default:
2219
                        // IDENTITY(u)
2220 31
                        $expression = $this->FunctionDeclaration();
2221 31
                        break;
2222
                }
2223
2224 87
                break;
2225
2226
            // PartialObjectExpression (PARTIAL u.{id, name})
2227 80
            case ($lookaheadType === Lexer::T_PARTIAL):
2228 11
                $expression    = $this->PartialObjectExpression();
2229 11
                $identVariable = $expression->identificationVariable;
2230 11
                break;
2231
2232
            // Subselect
2233 69
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2234 22
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2235 22
                $expression = $this->Subselect();
2236 22
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2237 22
                break;
2238
2239
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2240 47
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2241 43
            case ($lookaheadType === Lexer::T_INTEGER):
2242 41
            case ($lookaheadType === Lexer::T_STRING):
2243 32
            case ($lookaheadType === Lexer::T_FLOAT):
2244
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2245 32
            case ($lookaheadType === Lexer::T_MINUS):
2246 32
            case ($lookaheadType === Lexer::T_PLUS):
2247 16
                $expression = $this->SimpleArithmeticExpression();
2248 16
                break;
2249
2250
            // NewObjectExpression (New ClassName(id, name))
2251 31
            case ($lookaheadType === Lexer::T_NEW):
2252 28
                $expression = $this->NewObjectExpression();
2253 28
                break;
2254
2255
            default:
2256 3
                $this->syntaxError(
2257 3
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2258 3
                    $this->lexer->lookahead
2259
                );
2260
        }
2261
2262
        // [["AS"] ["HIDDEN"] AliasResultVariable]
2263 747
        $mustHaveAliasResultVariable = false;
2264
2265 747
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2266 110
            $this->match(Lexer::T_AS);
2267
2268 110
            $mustHaveAliasResultVariable = true;
2269
        }
2270
2271 747
        $hiddenAliasResultVariable = false;
2272
2273 747
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2274 10
            $this->match(Lexer::T_HIDDEN);
2275
2276 10
            $hiddenAliasResultVariable = true;
2277
        }
2278
2279 747
        $aliasResultVariable = null;
2280
2281 747
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2282 119
            $token = $this->lexer->lookahead;
2283 119
            $aliasResultVariable = $this->AliasResultVariable();
2284
2285
            // Include AliasResultVariable in query components.
2286 114
            $this->queryComponents[$aliasResultVariable] = array(
2287 114
                'resultVariable' => $expression,
2288 114
                'nestingLevel'   => $this->nestingLevel,
2289 114
                'token'          => $token,
2290
            );
2291
        }
2292
2293
        // AST
2294
2295 742
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2296
2297 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...
2298 584
            $this->identVariableExpressions[$identVariable] = $expr;
2299
        }
2300
2301 742
        return $expr;
2302
    }
2303
2304
    /**
2305
     * SimpleSelectExpression ::= (
2306
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2307
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2308
     * ) [["AS"] AliasResultVariable]
2309
     *
2310
     * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
2311
     */
2312 48
    public function SimpleSelectExpression()
2313
    {
2314 48
        $peek = $this->lexer->glimpse();
2315
2316 48
        switch ($this->lexer->lookahead['type']) {
2317 48
            case Lexer::T_IDENTIFIER:
2318
                switch (true) {
2319 19
                    case ($peek['type'] === Lexer::T_DOT):
2320 16
                        $expression = $this->StateFieldPathExpression();
2321
2322 16
                        return new AST\SimpleSelectExpression($expression);
2323
2324 3
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2325 2
                        $expression = $this->IdentificationVariable();
2326
2327 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...
2328
2329 1
                    case ($this->isFunction()):
2330
                        // SUM(u.id) + COUNT(u.id)
2331 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...
2332
                            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...
2333
                        }
2334
                        // COUNT(u.id)
2335 1
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2336
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2337
                        }
2338
                        // IDENTITY(u)
2339 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...
2340
2341
                    default:
2342
                        // Do nothing
2343
                }
2344
                break;
2345
2346 30
            case Lexer::T_OPEN_PARENTHESIS:
2347 3
                if ($peek['type'] !== Lexer::T_SELECT) {
2348
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2349 3
                    $expression = $this->SimpleArithmeticExpression();
2350
2351 3
                    return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->SimpleArithmeticExpression() on line 2349 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...
2352
                }
2353
2354
                // Subselect
2355
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2356
                $expression = $this->Subselect();
2357
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2358
2359
                return new AST\SimpleSelectExpression($expression);
2360
2361
            default:
2362
                // Do nothing
2363
        }
2364
2365 27
        $this->lexer->peek();
2366
2367 27
        $expression = $this->ScalarExpression();
2368 27
        $expr       = new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->ScalarExpression() on line 2367 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...
2369
2370 27
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2371 1
            $this->match(Lexer::T_AS);
2372
        }
2373
2374 27
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2375 2
            $token = $this->lexer->lookahead;
2376 2
            $resultVariable = $this->AliasResultVariable();
2377 2
            $expr->fieldIdentificationVariable = $resultVariable;
2378
2379
            // Include AliasResultVariable in query components.
2380 2
            $this->queryComponents[$resultVariable] = array(
2381 2
                'resultvariable' => $expr,
2382 2
                'nestingLevel'   => $this->nestingLevel,
2383 2
                'token'          => $token,
2384
            );
2385
        }
2386
2387 27
        return $expr;
2388
    }
2389
2390
    /**
2391
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2392
     *
2393
     * @return \Doctrine\ORM\Query\AST\ConditionalExpression
2394
     */
2395 369
    public function ConditionalExpression()
2396
    {
2397 369
        $conditionalTerms = array();
2398 369
        $conditionalTerms[] = $this->ConditionalTerm();
2399
2400 366
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2401 16
            $this->match(Lexer::T_OR);
2402
2403 16
            $conditionalTerms[] = $this->ConditionalTerm();
2404
        }
2405
2406
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2407
        // if only one AST\ConditionalTerm is defined
2408 366
        if (count($conditionalTerms) == 1) {
2409 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...
2410
        }
2411
2412 16
        return new AST\ConditionalExpression($conditionalTerms);
2413
    }
2414
2415
    /**
2416
     * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2417
     *
2418
     * @return \Doctrine\ORM\Query\AST\ConditionalTerm
2419
     */
2420 369
    public function ConditionalTerm()
2421
    {
2422 369
        $conditionalFactors = array();
2423 369
        $conditionalFactors[] = $this->ConditionalFactor();
2424
2425 366
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2426 32
            $this->match(Lexer::T_AND);
2427
2428 32
            $conditionalFactors[] = $this->ConditionalFactor();
2429
        }
2430
2431
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2432
        // if only one AST\ConditionalFactor is defined
2433 366
        if (count($conditionalFactors) == 1) {
2434 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...
2435
        }
2436
2437 32
        return new AST\ConditionalTerm($conditionalFactors);
2438
    }
2439
2440
    /**
2441
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2442
     *
2443
     * @return \Doctrine\ORM\Query\AST\ConditionalFactor
2444
     */
2445 369
    public function ConditionalFactor()
2446
    {
2447 369
        $not = false;
2448
2449 369
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2450 6
            $this->match(Lexer::T_NOT);
2451
2452 6
            $not = true;
2453
        }
2454
2455 369
        $conditionalPrimary = $this->ConditionalPrimary();
2456
2457
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2458
        // if only one AST\ConditionalPrimary is defined
2459 366
        if ( ! $not) {
2460 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...
2461
        }
2462
2463 6
        $conditionalFactor = new AST\ConditionalFactor($conditionalPrimary);
2464 6
        $conditionalFactor->not = $not;
2465
2466 6
        return $conditionalFactor;
2467
    }
2468
2469
    /**
2470
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2471
     *
2472
     * @return \Doctrine\ORM\Query\AST\ConditionalPrimary
2473
     */
2474 369
    public function ConditionalPrimary()
2475
    {
2476 369
        $condPrimary = new AST\ConditionalPrimary;
2477
2478 369
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2479 360
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2480
2481 357
            return $condPrimary;
2482
        }
2483
2484
        // Peek beyond the matching closing parenthesis ')'
2485 25
        $peek = $this->peekBeyondClosingParenthesis();
2486
2487 25
        if (in_array($peek['value'], array("=",  "<", "<=", "<>", ">", ">=", "!=")) ||
2488 22
            in_array($peek['type'], array(Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS)) ||
2489 25
            $this->isMathOperator($peek)) {
2490 15
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2491
2492 15
            return $condPrimary;
2493
        }
2494
2495 21
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2496 21
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2497 21
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2498
2499 21
        return $condPrimary;
2500
    }
2501
2502
    /**
2503
     * SimpleConditionalExpression ::=
2504
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2505
     *      InExpression | NullComparisonExpression | ExistsExpression |
2506
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2507
     *      InstanceOfExpression
2508
     */
2509 369
    public function SimpleConditionalExpression()
2510
    {
2511 369
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2512 7
            return $this->ExistsExpression();
2513
        }
2514
2515 369
        $token      = $this->lexer->lookahead;
2516 369
        $peek       = $this->lexer->glimpse();
2517 369
        $lookahead  = $token;
2518
2519 369
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2520
            $token = $this->lexer->glimpse();
2521
        }
2522
2523 369
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2524
            // Peek beyond the matching closing parenthesis.
2525 345
            $beyond = $this->lexer->peek();
2526
2527 345
            switch ($peek['value']) {
2528 345
                case '(':
2529
                    // Peeks beyond the matched closing parenthesis.
2530 34
                    $token = $this->peekBeyondClosingParenthesis(false);
2531
2532 34
                    if ($token['type'] === Lexer::T_NOT) {
2533 3
                        $token = $this->lexer->peek();
2534
                    }
2535
2536 34
                    if ($token['type'] === Lexer::T_IS) {
2537 2
                        $lookahead = $this->lexer->peek();
2538
                    }
2539 34
                    break;
2540
2541
                default:
2542
                    // Peek beyond the PathExpression or InputParameter.
2543 317
                    $token = $beyond;
2544
2545 317
                    while ($token['value'] === '.') {
2546 279
                        $this->lexer->peek();
2547
2548 279
                        $token = $this->lexer->peek();
2549
                    }
2550
2551
                    // Also peek beyond a NOT if there is one.
2552 317
                    if ($token['type'] === Lexer::T_NOT) {
2553 11
                        $token = $this->lexer->peek();
2554
                    }
2555
2556
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2557 317
                    $lookahead = $this->lexer->peek();
2558
            }
2559
2560
            // Also peek beyond a NOT if there is one.
2561 345
            if ($lookahead['type'] === Lexer::T_NOT) {
2562 6
                $lookahead = $this->lexer->peek();
2563
            }
2564
2565 345
            $this->lexer->resetPeek();
2566
        }
2567
2568 369
        if ($token['type'] === Lexer::T_BETWEEN) {
2569 8
            return $this->BetweenExpression();
2570
        }
2571
2572 363
        if ($token['type'] === Lexer::T_LIKE) {
2573 14
            return $this->LikeExpression();
2574
        }
2575
2576 350
        if ($token['type'] === Lexer::T_IN) {
2577 34
            return $this->InExpression();
2578
        }
2579
2580 325
        if ($token['type'] === Lexer::T_INSTANCE) {
2581 12
            return $this->InstanceOfExpression();
2582
        }
2583
2584 313
        if ($token['type'] === Lexer::T_MEMBER) {
2585 7
            return $this->CollectionMemberExpression();
2586
        }
2587
2588 306
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2589 13
            return $this->NullComparisonExpression();
2590
        }
2591
2592 296
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2593 4
            return $this->EmptyCollectionComparisonExpression();
2594
        }
2595
2596 292
        return $this->ComparisonExpression();
2597
    }
2598
2599
    /**
2600
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2601
     *
2602
     * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression
2603
     */
2604 4
    public function EmptyCollectionComparisonExpression()
2605
    {
2606 4
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2607 4
            $this->CollectionValuedPathExpression()
2608
        );
2609 4
        $this->match(Lexer::T_IS);
2610
2611 4
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2612 2
            $this->match(Lexer::T_NOT);
2613 2
            $emptyCollectionCompExpr->not = true;
2614
        }
2615
2616 4
        $this->match(Lexer::T_EMPTY);
2617
2618 4
        return $emptyCollectionCompExpr;
2619
    }
2620
2621
    /**
2622
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2623
     *
2624
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2625
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2626
     *
2627
     * @return \Doctrine\ORM\Query\AST\CollectionMemberExpression
2628
     */
2629 7
    public function CollectionMemberExpression()
2630
    {
2631 7
        $not        = false;
2632 7
        $entityExpr = $this->EntityExpression();
2633
2634 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2635
            $this->match(Lexer::T_NOT);
2636
2637
            $not = true;
2638
        }
2639
2640 7
        $this->match(Lexer::T_MEMBER);
2641
2642 7
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2643 7
            $this->match(Lexer::T_OF);
2644
        }
2645
2646 7
        $collMemberExpr = new AST\CollectionMemberExpression(
2647 7
            $entityExpr, $this->CollectionValuedPathExpression()
2648
        );
2649 7
        $collMemberExpr->not = $not;
2650
2651 7
        return $collMemberExpr;
2652
    }
2653
2654
    /**
2655
     * Literal ::= string | char | integer | float | boolean
2656
     *
2657
     * @return \Doctrine\ORM\Query\AST\Literal
2658
     */
2659 177
    public function Literal()
2660
    {
2661 177
        switch ($this->lexer->lookahead['type']) {
2662 177
            case Lexer::T_STRING:
2663 49
                $this->match(Lexer::T_STRING);
2664
2665 49
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2666 137
            case Lexer::T_INTEGER:
2667 9
            case Lexer::T_FLOAT:
2668 129
                $this->match(
2669 129
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2670
                );
2671
2672 129
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2673 8
            case Lexer::T_TRUE:
2674 4
            case Lexer::T_FALSE:
2675 8
                $this->match(
2676 8
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2677
                );
2678
2679 8
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2680
            default:
2681
                $this->syntaxError('Literal');
2682
        }
2683
    }
2684
2685
    /**
2686
     * InParameter ::= Literal | InputParameter
2687
     *
2688
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2689
     */
2690 25
    public function InParameter()
2691
    {
2692 25
        if ($this->lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) {
2693 13
            return $this->InputParameter();
2694
        }
2695
2696 12
        return $this->Literal();
2697
    }
2698
2699
    /**
2700
     * InputParameter ::= PositionalParameter | NamedParameter
2701
     *
2702
     * @return \Doctrine\ORM\Query\AST\InputParameter
2703
     */
2704 160
    public function InputParameter()
2705
    {
2706 160
        $this->match(Lexer::T_INPUT_PARAMETER);
2707
2708 160
        return new AST\InputParameter($this->lexer->token['value']);
2709
    }
2710
2711
    /**
2712
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2713
     *
2714
     * @return \Doctrine\ORM\Query\AST\ArithmeticExpression
2715
     */
2716 325
    public function ArithmeticExpression()
2717
    {
2718 325
        $expr = new AST\ArithmeticExpression;
2719
2720 325
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2721 19
            $peek = $this->lexer->glimpse();
2722
2723 19
            if ($peek['type'] === Lexer::T_SELECT) {
2724 7
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2725 7
                $expr->subselect = $this->Subselect();
2726 7
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2727
2728 7
                return $expr;
2729
            }
2730
        }
2731
2732 325
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2733
2734 325
        return $expr;
2735
    }
2736
2737
    /**
2738
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2739
     *
2740
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2741
     */
2742 428
    public function SimpleArithmeticExpression()
2743
    {
2744 428
        $terms = array();
2745 428
        $terms[] = $this->ArithmeticTerm();
2746
2747 428
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2748 21
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2749
2750 21
            $terms[] = $this->lexer->token['value'];
2751 21
            $terms[] = $this->ArithmeticTerm();
2752
        }
2753
2754
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2755
        // if only one AST\ArithmeticTerm is defined
2756 428
        if (count($terms) == 1) {
2757 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 2757 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...
2758
        }
2759
2760 21
        return new AST\SimpleArithmeticExpression($terms);
2761
    }
2762
2763
    /**
2764
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2765
     *
2766
     * @return \Doctrine\ORM\Query\AST\ArithmeticTerm
2767
     */
2768 428
    public function ArithmeticTerm()
2769
    {
2770 428
        $factors = array();
2771 428
        $factors[] = $this->ArithmeticFactor();
2772
2773 428
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2774 53
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2775
2776 53
            $factors[] = $this->lexer->token['value'];
2777 53
            $factors[] = $this->ArithmeticFactor();
2778
        }
2779
2780
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2781
        // if only one AST\ArithmeticFactor is defined
2782 428
        if (count($factors) == 1) {
2783 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 2783 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticTerm of type Doctrine\ORM\Query\AST\ArithmeticTerm|null.
Loading history...
2784
        }
2785
2786 53
        return new AST\ArithmeticTerm($factors);
2787
    }
2788
2789
    /**
2790
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2791
     *
2792
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2793
     */
2794 428
    public function ArithmeticFactor()
2795
    {
2796 428
        $sign = null;
2797
2798 428
        if (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2799 3
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2800 3
            $sign = $isPlus;
2801
        }
2802
2803 428
        $primary = $this->ArithmeticPrimary();
2804
2805
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2806
        // if only one AST\ArithmeticPrimary is defined
2807 428
        if ($sign === null) {
2808 427
            return $primary;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $primary; (Doctrine\ORM\Query\AST\P...e\ORM\Query\AST\Literal) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticFactor of type Doctrine\ORM\Query\AST\ArithmeticFactor|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2809
        }
2810
2811 3
        return new AST\ArithmeticFactor($primary, $sign);
2812
    }
2813
2814
    /**
2815
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2816
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2817
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2818
     *          | InputParameter | CaseExpression
2819
     */
2820 432
    public function ArithmeticPrimary()
2821
    {
2822 432
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2823 25
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2824
2825 25
            $expr = $this->SimpleArithmeticExpression();
2826
2827 25
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2828
2829 25
            return new AST\ParenthesisExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr defined by $this->SimpleArithmeticExpression() on line 2825 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...
2830
        }
2831
2832 432
        switch ($this->lexer->lookahead['type']) {
2833 432
            case Lexer::T_COALESCE:
2834 432
            case Lexer::T_NULLIF:
2835 432
            case Lexer::T_CASE:
2836 4
                return $this->CaseExpression();
2837
2838 432
            case Lexer::T_IDENTIFIER:
2839 402
                $peek = $this->lexer->glimpse();
2840
2841 402
                if ($peek['value'] == '(') {
2842 29
                    return $this->FunctionDeclaration();
2843
                }
2844
2845 381
                if ($peek['value'] == '.') {
2846 370
                    return $this->SingleValuedPathExpression();
2847
                }
2848
2849 46
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2850 10
                    return $this->ResultVariable();
2851
                }
2852
2853 38
                return $this->StateFieldPathExpression();
2854
2855 305
            case Lexer::T_INPUT_PARAMETER:
2856 142
                return $this->InputParameter();
2857
2858
            default:
2859 171
                $peek = $this->lexer->glimpse();
2860
2861 171
                if ($peek['value'] == '(') {
2862 18
                    if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2863 18
                        return $this->AggregateExpression();
2864
                    }
2865
2866
                    return $this->FunctionDeclaration();
2867
                }
2868
2869 167
                return $this->Literal();
2870
        }
2871
    }
2872
2873
    /**
2874
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2875
     *
2876
     * @return \Doctrine\ORM\Query\AST\StringPrimary |
2877
     *         \Doctrine\ORM\Query\AST\Subselect |
2878
     *         string
2879
     */
2880 14
    public function StringExpression()
2881
    {
2882 14
        $peek = $this->lexer->glimpse();
2883
2884
        // Subselect
2885 14
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
2886
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2887
            $expr = $this->Subselect();
2888
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2889
2890
            return $expr;
2891
        }
2892
2893
        // ResultVariable (string)
2894 14
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2895 14
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2896 2
            return $this->ResultVariable();
2897
        }
2898
2899 12
        return $this->StringPrimary();
2900
    }
2901
2902
    /**
2903
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2904
     */
2905 51
    public function StringPrimary()
2906
    {
2907 51
        $lookaheadType = $this->lexer->lookahead['type'];
2908
2909
        switch ($lookaheadType) {
2910 51
            case Lexer::T_IDENTIFIER:
2911 32
                $peek = $this->lexer->glimpse();
2912
2913 32
                if ($peek['value'] == '.') {
2914 32
                    return $this->StateFieldPathExpression();
2915
                }
2916
2917 8
                if ($peek['value'] == '(') {
2918
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2919 8
                    return $this->FunctionDeclaration();
2920
                }
2921
2922
                $this->syntaxError("'.' or '('");
2923
                break;
2924
2925 32
            case Lexer::T_STRING:
2926 32
                $this->match(Lexer::T_STRING);
2927
2928 32
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2929
2930 2
            case Lexer::T_INPUT_PARAMETER:
2931 2
                return $this->InputParameter();
2932
2933
            case Lexer::T_CASE:
2934
            case Lexer::T_COALESCE:
2935
            case Lexer::T_NULLIF:
2936
                return $this->CaseExpression();
2937
2938
            default:
2939
                if ($this->isAggregateFunction($lookaheadType)) {
2940
                    return $this->AggregateExpression();
2941
                }
2942
        }
2943
2944
        $this->syntaxError(
2945
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2946
        );
2947
    }
2948
2949
    /**
2950
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2951
     *
2952
     * @return \Doctrine\ORM\Query\AST\PathExpression |
2953
     *         \Doctrine\ORM\Query\AST\SimpleEntityExpression
2954
     */
2955 7
    public function EntityExpression()
2956
    {
2957 7
        $glimpse = $this->lexer->glimpse();
2958
2959 7
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2960 1
            return $this->SingleValuedAssociationPathExpression();
2961
        }
2962
2963 6
        return $this->SimpleEntityExpression();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->SimpleEntityExpression(); of type Doctrine\ORM\Query\AST\I...uery\AST\PathExpression adds the type Doctrine\ORM\Query\AST\InputParameter to the return on line 2963 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::EntityExpression of type Doctrine\ORM\Query\AST\PathExpression.
Loading history...
2964
    }
2965
2966
    /**
2967
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2968
     *
2969
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2970
     */
2971 6
    public function SimpleEntityExpression()
2972
    {
2973 6
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2974 5
            return $this->InputParameter();
2975
        }
2976
2977 1
        return $this->StateFieldPathExpression();
2978
    }
2979
2980
    /**
2981
     * AggregateExpression ::=
2982
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2983
     *
2984
     * @return \Doctrine\ORM\Query\AST\AggregateExpression
2985
     */
2986 84
    public function AggregateExpression()
2987
    {
2988 84
        $lookaheadType = $this->lexer->lookahead['type'];
2989 84
        $isDistinct = false;
2990
2991 84
        if ( ! in_array($lookaheadType, array(Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM))) {
2992
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2993
        }
2994
2995 84
        $this->match($lookaheadType);
2996 84
        $functionName = $this->lexer->token['value'];
2997 84
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2998
2999 84
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
3000 2
            $this->match(Lexer::T_DISTINCT);
3001 2
            $isDistinct = true;
3002
        }
3003
3004 84
        $pathExp = $this->SimpleArithmeticExpression();
3005
3006 84
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3007
3008 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 3004 can be null; however, Doctrine\ORM\Query\AST\A...pression::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
3009
    }
3010
3011
    /**
3012
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
3013
     *
3014
     * @return \Doctrine\ORM\Query\AST\QuantifiedExpression
3015
     */
3016 3
    public function QuantifiedExpression()
3017
    {
3018 3
        $lookaheadType = $this->lexer->lookahead['type'];
3019 3
        $value = $this->lexer->lookahead['value'];
3020
3021 3
        if ( ! in_array($lookaheadType, array(Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME))) {
3022
            $this->syntaxError('ALL, ANY or SOME');
3023
        }
3024
3025 3
        $this->match($lookaheadType);
3026 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3027
3028 3
        $qExpr = new AST\QuantifiedExpression($this->Subselect());
3029 3
        $qExpr->type = $value;
3030
3031 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3032
3033 3
        return $qExpr;
3034
    }
3035
3036
    /**
3037
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3038
     *
3039
     * @return \Doctrine\ORM\Query\AST\BetweenExpression
3040
     */
3041 8
    public function BetweenExpression()
3042
    {
3043 8
        $not = false;
3044 8
        $arithExpr1 = $this->ArithmeticExpression();
3045
3046 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3047 3
            $this->match(Lexer::T_NOT);
3048 3
            $not = true;
3049
        }
3050
3051 8
        $this->match(Lexer::T_BETWEEN);
3052 8
        $arithExpr2 = $this->ArithmeticExpression();
3053 8
        $this->match(Lexer::T_AND);
3054 8
        $arithExpr3 = $this->ArithmeticExpression();
3055
3056 8
        $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3057 8
        $betweenExpr->not = $not;
3058
3059 8
        return $betweenExpr;
3060
    }
3061
3062
    /**
3063
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3064
     *
3065
     * @return \Doctrine\ORM\Query\AST\ComparisonExpression
3066
     */
3067 292
    public function ComparisonExpression()
3068
    {
3069 292
        $this->lexer->glimpse();
3070
3071 292
        $leftExpr  = $this->ArithmeticExpression();
3072 292
        $operator  = $this->ComparisonOperator();
3073 292
        $rightExpr = ($this->isNextAllAnySome())
3074 3
            ? $this->QuantifiedExpression()
3075 292
            : $this->ArithmeticExpression();
3076
3077 290
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3078
    }
3079
3080
    /**
3081
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3082
     *
3083
     * @return \Doctrine\ORM\Query\AST\InExpression
3084
     */
3085 34
    public function InExpression()
3086
    {
3087 34
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3088
3089 34
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3090 6
            $this->match(Lexer::T_NOT);
3091 6
            $inExpression->not = true;
3092
        }
3093
3094 34
        $this->match(Lexer::T_IN);
3095 34
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3096
3097 34
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3098 9
            $inExpression->subselect = $this->Subselect();
3099
        } else {
3100 25
            $literals = array();
3101 25
            $literals[] = $this->InParameter();
3102
3103 25
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3104 16
                $this->match(Lexer::T_COMMA);
3105 16
                $literals[] = $this->InParameter();
3106
            }
3107
3108 25
            $inExpression->literals = $literals;
3109
        }
3110
3111 33
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3112
3113 33
        return $inExpression;
3114
    }
3115
3116
    /**
3117
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3118
     *
3119
     * @return \Doctrine\ORM\Query\AST\InstanceOfExpression
3120
     */
3121 12
    public function InstanceOfExpression()
3122
    {
3123 12
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3124
3125 12
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3126 1
            $this->match(Lexer::T_NOT);
3127 1
            $instanceOfExpression->not = true;
3128
        }
3129
3130 12
        $this->match(Lexer::T_INSTANCE);
3131 12
        $this->match(Lexer::T_OF);
3132
3133 12
        $exprValues = array();
3134
3135 12
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3136 1
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3137
3138 1
            $exprValues[] = $this->InstanceOfParameter();
3139
3140 1
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3141 1
                $this->match(Lexer::T_COMMA);
3142
3143 1
                $exprValues[] = $this->InstanceOfParameter();
3144
            }
3145
3146 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3147
3148 1
            $instanceOfExpression->value = $exprValues;
3149
3150 1
            return $instanceOfExpression;
3151
        }
3152
3153 11
        $exprValues[] = $this->InstanceOfParameter();
3154
3155 11
        $instanceOfExpression->value = $exprValues;
3156
3157 11
        return $instanceOfExpression;
3158
    }
3159
3160
    /**
3161
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3162
     *
3163
     * @return mixed
3164
     */
3165 12
    public function InstanceOfParameter()
3166
    {
3167 12
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3168 5
            $this->match(Lexer::T_INPUT_PARAMETER);
3169
3170 5
            return new AST\InputParameter($this->lexer->token['value']);
3171
        }
3172
3173 7
        $abstractSchemaName = $this->AbstractSchemaName();
3174
3175 7
        $this->validateAbstractSchemaName($abstractSchemaName);
3176
3177 7
        return $abstractSchemaName;
3178
    }
3179
3180
    /**
3181
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3182
     *
3183
     * @return \Doctrine\ORM\Query\AST\LikeExpression
3184
     */
3185 14
    public function LikeExpression()
3186
    {
3187 14
        $stringExpr = $this->StringExpression();
3188 14
        $not = false;
3189
3190 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3191 3
            $this->match(Lexer::T_NOT);
3192 3
            $not = true;
3193
        }
3194
3195 14
        $this->match(Lexer::T_LIKE);
3196
3197 14
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3198 4
            $this->match(Lexer::T_INPUT_PARAMETER);
3199 4
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3200
        } else {
3201 11
            $stringPattern = $this->StringPrimary();
3202
        }
3203
3204 14
        $escapeChar = null;
3205
3206 14
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3207 2
            $this->match(Lexer::T_ESCAPE);
3208 2
            $this->match(Lexer::T_STRING);
3209
3210 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3211
        }
3212
3213 14
        $likeExpr = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $stringExpr defined by $this->StringExpression() on line 3187 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 3201 can also be of type null or object<Doctrine\ORM\Quer...ST\AggregateExpression> or object<Doctrine\ORM\Query\AST\CoalesceExpression> or object<Doctrine\ORM\Quer...Functions\FunctionNode> or object<Doctrine\ORM\Quer...\GeneralCaseExpression> or object<Doctrine\ORM\Query\AST\Literal> or object<Doctrine\ORM\Query\AST\NullIfExpression> or object<Doctrine\ORM\Query\AST\PathExpression> or object<Doctrine\ORM\Quer...T\SimpleCaseExpression>; however, Doctrine\ORM\Query\AST\L...pression::__construct() does only seem to accept object<Doctrine\ORM\Query\AST\InputParameter>, maybe add an additional type check?

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

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

    return array();
}

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

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

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