Completed
Pull Request — master (#5713)
by Ondřej
14:07
created

Parser::processDeferredPartialObjectExpressions()   C

Complexity

Conditions 8
Paths 9

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8
Metric Value
dl 0
loc 30
ccs 18
cts 18
cp 1
rs 5.3846
cc 8
eloc 17
nc 9
nop 0
crap 8
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 815
    public function __construct(Query $query)
196
    {
197 815
        $this->query        = $query;
198 815
        $this->em           = $query->getEntityManager();
199 815
        $this->lexer        = new Lexer($query->getDql());
200 815
        $this->parserResult = new ParserResult();
201 815
    }
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 815
    public function getAST()
266
    {
267
        // Parse & build AST
268 815
        $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 776
        $this->processDeferredIdentificationVariables();
273
274 774
        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 772
        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 769
        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 769
        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 765
        $this->processRootEntityAliasSelected();
291
292
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
293 764
        $this->fixIdentificationVariableOrder($AST);
294
295 764
        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 826
    public function match($token)
311
    {
312 826
        $lookaheadType = $this->lexer->lookahead['type'];
313
314
        // Short-circuit on first condition, usually types match
315 826
        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 819
        $this->lexer->moveNext();
333 819
    }
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 815
    public function parse()
363
    {
364 815
        $AST = $this->getAST();
365
366 764
        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 764
        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 764
        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 758
        $outputWalkerClass = $this->customOutputWalker ?: __NAMESPACE__ . '\SqlWalker';
400 758
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
401
402
        // Assign an SQL executor to the parser result
403 758
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
404
405 749
        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 764
    private function fixIdentificationVariableOrder($AST)
420
    {
421 764
        if (count($this->identVariableExpressions) <= 1) {
422 590
            return;
423
        }
424
425 179
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
426 179
            if ( ! isset($this->identVariableExpressions[$dqlAlias])) {
427 8
                continue;
428
            }
429
430 179
            $expr = $this->identVariableExpressions[$dqlAlias];
431 179
            $key  = array_search($expr, $AST->selectClause->selectExpressions);
432
433 179
            unset($AST->selectClause->selectExpressions[$key]);
434
435 179
            $AST->selectClause->selectExpressions[] = $expr;
436
        }
437 179
    }
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 776
    private function processDeferredIdentificationVariables()
591
    {
592 776
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
593 764
            $identVariable = $deferredItem['expression'];
594
595
            // Check if IdentificationVariable exists in queryComponents
596 764
            if ( ! isset($this->queryComponents[$identVariable])) {
597 1
                $this->semanticalError(
598 1
                    "'$identVariable' is not defined.", $deferredItem['token']
599
                );
600
            }
601
602 764
            $qComp = $this->queryComponents[$identVariable];
603
604
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
605 764
            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 764
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
613 1
                $this->semanticalError(
614 764
                    "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token']
615
                );
616
            }
617
        }
618 774
    }
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 765
    private function processRootEntityAliasSelected()
825
    {
826 765
        if ( ! count($this->identVariableExpressions)) {
827 223
            return;
828
        }
829
830 552
        $foundRootEntity = false;
831
832 552
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
833 552
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
834 552
                $foundRootEntity = true;
835
            }
836
        }
837
838 552
        if ( ! $foundRootEntity) {
839 1
            $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
840
        }
841 551
    }
842
843
    /**
844
     * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
845
     *
846
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
847
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
848
     *         \Doctrine\ORM\Query\AST\DeleteStatement
849
     */
850 815
    public function QueryLanguage()
851
    {
852 815
        $this->lexer->moveNext();
853
854 815
        switch ($this->lexer->lookahead['type']) {
855 815
            case Lexer::T_SELECT:
856 749
                $statement = $this->SelectStatement();
857 714
                break;
858
859 72
            case Lexer::T_UPDATE:
860 32
                $statement = $this->UpdateStatement();
861 32
                break;
862
863 43
            case Lexer::T_DELETE:
864 42
                $statement = $this->DeleteStatement();
865 41
                break;
866
867
            default:
868 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
869
                break;
870
        }
871
872
        // Check for end of string
873 779
        if ($this->lexer->lookahead !== null) {
874 3
            $this->syntaxError('end of string');
875
        }
876
877 776
        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...
878
    }
879
880
    /**
881
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
882
     *
883
     * @return \Doctrine\ORM\Query\AST\SelectStatement
884
     */
885 749
    public function SelectStatement()
886
    {
887 749
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
888
889 718
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
890 715
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
891 714
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
892 714
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
893
894 714
        return $selectStatement;
895
    }
896
897
    /**
898
     * UpdateStatement ::= UpdateClause [WhereClause]
899
     *
900
     * @return \Doctrine\ORM\Query\AST\UpdateStatement
901
     */
902 32
    public function UpdateStatement()
903
    {
904 32
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
905
906 32
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
907
908 32
        return $updateStatement;
909
    }
910
911
    /**
912
     * DeleteStatement ::= DeleteClause [WhereClause]
913
     *
914
     * @return \Doctrine\ORM\Query\AST\DeleteStatement
915
     */
916 42
    public function DeleteStatement()
917
    {
918 42
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
919
920 41
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
921
922 41
        return $deleteStatement;
923
    }
924
925
    /**
926
     * IdentificationVariable ::= identifier
927
     *
928
     * @return string
929
     */
930 792
    public function IdentificationVariable()
931
    {
932 792
        $this->match(Lexer::T_IDENTIFIER);
933
934 792
        $identVariable = $this->lexer->token['value'];
935
936 792
        $this->deferredIdentificationVariables[] = array(
937 792
            'expression'   => $identVariable,
938 792
            'nestingLevel' => $this->nestingLevel,
939 792
            'token'        => $this->lexer->token,
940
        );
941
942 792
        return $identVariable;
943
    }
944
945
    /**
946
     * AliasIdentificationVariable = identifier
947
     *
948
     * @return string
949
     */
950 786
    public function AliasIdentificationVariable()
951
    {
952 786
        $this->match(Lexer::T_IDENTIFIER);
953
954 786
        $aliasIdentVariable = $this->lexer->token['value'];
955 786
        $exists = isset($this->queryComponents[$aliasIdentVariable]);
956
957 786
        if ($exists) {
958 2
            $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->lexer->token);
959
        }
960
961 786
        return $aliasIdentVariable;
962
    }
963
964
    /**
965
     * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier
966
     *
967
     * @return string
968
     */
969 806
    public function AbstractSchemaName()
970
    {
971 806
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
972 789
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
973
974 789
            $schemaName = $this->lexer->token['value'];
975 28
        } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
976 19
            $this->match(Lexer::T_IDENTIFIER);
977
978 19
            $schemaName = $this->lexer->token['value'];
979
        } else {
980 10
            $this->match(Lexer::T_ALIASED_NAME);
981
982 10
            list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']);
983
984 10
            $schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
985
        }
986
987 806
        return $schemaName;
988
    }
989
990
    /**
991
     * Validates an AbstractSchemaName, making sure the class exists.
992
     *
993
     * @param string $schemaName The name to validate.
994
     *
995
     * @throws QueryException if the name does not exist.
996
     */
997 801
    private function validateAbstractSchemaName($schemaName)
998
    {
999 801
        if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {
1000 16
            $this->semanticalError("Class '$schemaName' is not defined.", $this->lexer->token);
1001
        }
1002 786
    }
1003
1004
    /**
1005
     * AliasResultVariable ::= identifier
1006
     *
1007
     * @return string
1008
     */
1009 119
    public function AliasResultVariable()
1010
    {
1011 119
        $this->match(Lexer::T_IDENTIFIER);
1012
1013 115
        $resultVariable = $this->lexer->token['value'];
1014 115
        $exists = isset($this->queryComponents[$resultVariable]);
1015
1016 115
        if ($exists) {
1017 2
            $this->semanticalError("'$resultVariable' is already defined.", $this->lexer->token);
1018
        }
1019
1020 115
        return $resultVariable;
1021
    }
1022
1023
    /**
1024
     * ResultVariable ::= identifier
1025
     *
1026
     * @return string
1027
     */
1028 30
    public function ResultVariable()
1029
    {
1030 30
        $this->match(Lexer::T_IDENTIFIER);
1031
1032 30
        $resultVariable = $this->lexer->token['value'];
1033
1034
        // Defer ResultVariable validation
1035 30
        $this->deferredResultVariables[] = array(
1036 30
            'expression'   => $resultVariable,
1037 30
            'nestingLevel' => $this->nestingLevel,
1038 30
            'token'        => $this->lexer->token,
1039
        );
1040
1041 30
        return $resultVariable;
1042
    }
1043
1044
    /**
1045
     * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
1046
     *
1047
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1048
     */
1049 253
    public function JoinAssociationPathExpression()
1050
    {
1051 253
        $identVariable = $this->IdentificationVariable();
1052
1053 253
        if ( ! isset($this->queryComponents[$identVariable])) {
1054
            $this->semanticalError(
1055
                'Identification Variable ' . $identVariable .' used in join path expression but was not defined before.'
1056
            );
1057
        }
1058
1059 253
        $this->match(Lexer::T_DOT);
1060 253
        $this->match(Lexer::T_IDENTIFIER);
1061
1062 253
        $field = $this->lexer->token['value'];
1063
1064
        // Validate association field
1065 253
        $qComp = $this->queryComponents[$identVariable];
1066 253
        $class = $qComp['metadata'];
1067
1068 253
        if ( ! $class->hasAssociation($field)) {
1069
            $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
1070
        }
1071
1072 253
        return new AST\JoinAssociationPathExpression($identVariable, $field);
1073
    }
1074
1075
    /**
1076
     * Parses an arbitrary path expression and defers semantical validation
1077
     * based on expected types.
1078
     *
1079
     * PathExpression ::= IdentificationVariable {"." identifier}*
1080
     *
1081
     * @param integer $expectedTypes
1082
     *
1083
     * @return \Doctrine\ORM\Query\AST\PathExpression
1084
     */
1085 592
    public function PathExpression($expectedTypes)
1086
    {
1087 592
        $identVariable = $this->IdentificationVariable();
1088 592
        $field = null;
1089
1090 592
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1091 586
            $this->match(Lexer::T_DOT);
1092 586
            $this->match(Lexer::T_IDENTIFIER);
1093
1094 586
            $field = $this->lexer->token['value'];
1095
1096 586
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1097 2
                $this->match(Lexer::T_DOT);
1098 2
                $this->match(Lexer::T_IDENTIFIER);
1099 2
                $field .= '.'.$this->lexer->token['value'];
1100
            }
1101
        }
1102
1103
        // Creating AST node
1104 592
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1105
1106
        // Defer PathExpression validation if requested to be deferred
1107 592
        $this->deferredPathExpressions[] = array(
1108 592
            'expression'   => $pathExpr,
1109 592
            'nestingLevel' => $this->nestingLevel,
1110 592
            'token'        => $this->lexer->token,
1111
        );
1112
1113 592
        return $pathExpr;
1114
    }
1115
1116
    /**
1117
     * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
1118
     *
1119
     * @return \Doctrine\ORM\Query\AST\PathExpression
1120
     */
1121
    public function AssociationPathExpression()
1122
    {
1123
        return $this->PathExpression(
1124
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
1125
            AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
1126
        );
1127
    }
1128
1129
    /**
1130
     * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
1131
     *
1132
     * @return \Doctrine\ORM\Query\AST\PathExpression
1133
     */
1134 501
    public function SingleValuedPathExpression()
1135
    {
1136 501
        return $this->PathExpression(
1137 501
            AST\PathExpression::TYPE_STATE_FIELD |
1138 501
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1139
        );
1140
    }
1141
1142
    /**
1143
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1144
     *
1145
     * @return \Doctrine\ORM\Query\AST\PathExpression
1146
     */
1147 202
    public function StateFieldPathExpression()
1148
    {
1149 202
        return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
1150
    }
1151
1152
    /**
1153
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
1154
     *
1155
     * @return \Doctrine\ORM\Query\AST\PathExpression
1156
     */
1157 9
    public function SingleValuedAssociationPathExpression()
1158
    {
1159 9
        return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
1160
    }
1161
1162
    /**
1163
     * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
1164
     *
1165
     * @return \Doctrine\ORM\Query\AST\PathExpression
1166
     */
1167 22
    public function CollectionValuedPathExpression()
1168
    {
1169 22
        return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
1170
    }
1171
1172
    /**
1173
     * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
1174
     *
1175
     * @return \Doctrine\ORM\Query\AST\SelectClause
1176
     */
1177 749
    public function SelectClause()
1178
    {
1179 749
        $isDistinct = false;
1180 749
        $this->match(Lexer::T_SELECT);
1181
1182
        // Check for DISTINCT
1183 749
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1184 6
            $this->match(Lexer::T_DISTINCT);
1185
1186 6
            $isDistinct = true;
1187
        }
1188
1189
        // Process SelectExpressions (1..N)
1190 749
        $selectExpressions = array();
1191 749
        $selectExpressions[] = $this->SelectExpression();
1192
1193 741
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1194 281
            $this->match(Lexer::T_COMMA);
1195
1196 281
            $selectExpressions[] = $this->SelectExpression();
1197
        }
1198
1199 740
        return new AST\SelectClause($selectExpressions, $isDistinct);
1200
    }
1201
1202
    /**
1203
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1204
     *
1205
     * @return \Doctrine\ORM\Query\AST\SimpleSelectClause
1206
     */
1207 48
    public function SimpleSelectClause()
1208
    {
1209 48
        $isDistinct = false;
1210 48
        $this->match(Lexer::T_SELECT);
1211
1212 48
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1213
            $this->match(Lexer::T_DISTINCT);
1214
1215
            $isDistinct = true;
1216
        }
1217
1218 48
        return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1219
    }
1220
1221
    /**
1222
     * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1223
     *
1224
     * @return \Doctrine\ORM\Query\AST\UpdateClause
1225
     */
1226 32
    public function UpdateClause()
1227
    {
1228 32
        $this->match(Lexer::T_UPDATE);
1229
1230 32
        $token = $this->lexer->lookahead;
1231 32
        $abstractSchemaName = $this->AbstractSchemaName();
1232
1233 32
        $this->validateAbstractSchemaName($abstractSchemaName);
1234
1235 32
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1236 2
            $this->match(Lexer::T_AS);
1237
        }
1238
1239 32
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1240
1241 32
        $class = $this->em->getClassMetadata($abstractSchemaName);
1242
1243
        // Building queryComponent
1244
        $queryComponent = array(
1245 32
            'metadata'     => $class,
1246
            'parent'       => null,
1247
            'relation'     => null,
1248
            'map'          => null,
1249 32
            'nestingLevel' => $this->nestingLevel,
1250 32
            'token'        => $token,
1251
        );
1252
1253 32
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1254
1255 32
        $this->match(Lexer::T_SET);
1256
1257 32
        $updateItems = array();
1258 32
        $updateItems[] = $this->UpdateItem();
1259
1260 32
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1261 5
            $this->match(Lexer::T_COMMA);
1262
1263 5
            $updateItems[] = $this->UpdateItem();
1264
        }
1265
1266 32
        $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1267 32
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1268
1269 32
        return $updateClause;
1270
    }
1271
1272
    /**
1273
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1274
     *
1275
     * @return \Doctrine\ORM\Query\AST\DeleteClause
1276
     */
1277 42
    public function DeleteClause()
1278
    {
1279 42
        $this->match(Lexer::T_DELETE);
1280
1281 42
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1282 8
            $this->match(Lexer::T_FROM);
1283
        }
1284
1285 42
        $token = $this->lexer->lookahead;
1286 42
        $abstractSchemaName = $this->AbstractSchemaName();
1287
1288 42
        $this->validateAbstractSchemaName($abstractSchemaName);
1289
1290 42
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1291
1292 42
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1293 1
            $this->match(Lexer::T_AS);
1294
        }
1295
1296 42
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1297
1298 41
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1299 41
        $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1300
1301
        // Building queryComponent
1302
        $queryComponent = array(
1303 41
            'metadata'     => $class,
1304
            'parent'       => null,
1305
            'relation'     => null,
1306
            'map'          => null,
1307 41
            'nestingLevel' => $this->nestingLevel,
1308 41
            'token'        => $token,
1309
        );
1310
1311 41
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1312
1313 41
        return $deleteClause;
1314
    }
1315
1316
    /**
1317
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1318
     *
1319
     * @return \Doctrine\ORM\Query\AST\FromClause
1320
     */
1321 740
    public function FromClause()
1322
    {
1323 740
        $this->match(Lexer::T_FROM);
1324
1325 735
        $identificationVariableDeclarations = array();
1326 735
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1327
1328 718
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1329 6
            $this->match(Lexer::T_COMMA);
1330
1331 6
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1332
        }
1333
1334 718
        return new AST\FromClause($identificationVariableDeclarations);
1335
    }
1336
1337
    /**
1338
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1339
     *
1340
     * @return \Doctrine\ORM\Query\AST\SubselectFromClause
1341
     */
1342 48
    public function SubselectFromClause()
1343
    {
1344 48
        $this->match(Lexer::T_FROM);
1345
1346 48
        $identificationVariables = array();
1347 48
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1348
1349 47
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1350
            $this->match(Lexer::T_COMMA);
1351
1352
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1353
        }
1354
1355 47
        return new AST\SubselectFromClause($identificationVariables);
1356
    }
1357
1358
    /**
1359
     * WhereClause ::= "WHERE" ConditionalExpression
1360
     *
1361
     * @return \Doctrine\ORM\Query\AST\WhereClause
1362
     */
1363 327
    public function WhereClause()
1364
    {
1365 327
        $this->match(Lexer::T_WHERE);
1366
1367 327
        return new AST\WhereClause($this->ConditionalExpression());
1368
    }
1369
1370
    /**
1371
     * HavingClause ::= "HAVING" ConditionalExpression
1372
     *
1373
     * @return \Doctrine\ORM\Query\AST\HavingClause
1374
     */
1375 21
    public function HavingClause()
1376
    {
1377 21
        $this->match(Lexer::T_HAVING);
1378
1379 21
        return new AST\HavingClause($this->ConditionalExpression());
1380
    }
1381
1382
    /**
1383
     * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1384
     *
1385
     * @return \Doctrine\ORM\Query\AST\GroupByClause
1386
     */
1387 31
    public function GroupByClause()
1388
    {
1389 31
        $this->match(Lexer::T_GROUP);
1390 31
        $this->match(Lexer::T_BY);
1391
1392 31
        $groupByItems = array($this->GroupByItem());
1393
1394 30
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1395 8
            $this->match(Lexer::T_COMMA);
1396
1397 8
            $groupByItems[] = $this->GroupByItem();
1398
        }
1399
1400 30
        return new AST\GroupByClause($groupByItems);
1401
    }
1402
1403
    /**
1404
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1405
     *
1406
     * @return \Doctrine\ORM\Query\AST\OrderByClause
1407
     */
1408 179
    public function OrderByClause()
1409
    {
1410 179
        $this->match(Lexer::T_ORDER);
1411 179
        $this->match(Lexer::T_BY);
1412
1413 179
        $orderByItems = array();
1414 179
        $orderByItems[] = $this->OrderByItem();
1415
1416 179
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1417 14
            $this->match(Lexer::T_COMMA);
1418
1419 14
            $orderByItems[] = $this->OrderByItem();
1420
        }
1421
1422 179
        return new AST\OrderByClause($orderByItems);
1423
    }
1424
1425
    /**
1426
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1427
     *
1428
     * @return \Doctrine\ORM\Query\AST\Subselect
1429
     */
1430 48
    public function Subselect()
1431
    {
1432
        // Increase query nesting level
1433 48
        $this->nestingLevel++;
1434
1435 48
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1436
1437 47
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1438 47
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1439 47
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1440 47
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1441
1442
        // Decrease query nesting level
1443 47
        $this->nestingLevel--;
1444
1445 47
        return $subselect;
1446
    }
1447
1448
    /**
1449
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1450
     *
1451
     * @return \Doctrine\ORM\Query\AST\UpdateItem
1452
     */
1453 32
    public function UpdateItem()
1454
    {
1455 32
        $pathExpr = $this->SingleValuedPathExpression();
1456
1457 32
        $this->match(Lexer::T_EQUALS);
1458
1459 32
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1460
1461 32
        return $updateItem;
1462
    }
1463
1464
    /**
1465
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1466
     *
1467
     * @return string | \Doctrine\ORM\Query\AST\PathExpression
1468
     */
1469 31
    public function GroupByItem()
1470
    {
1471
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1472 31
        $glimpse = $this->lexer->glimpse();
1473
1474 31
        if ($glimpse['type'] === Lexer::T_DOT) {
1475 12
            return $this->SingleValuedPathExpression();
1476
        }
1477
1478
        // Still need to decide between IdentificationVariable or ResultVariable
1479 19
        $lookaheadValue = $this->lexer->lookahead['value'];
1480
1481 19
        if ( ! isset($this->queryComponents[$lookaheadValue])) {
1482 1
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1483
        }
1484
1485 18
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1486 16
            ? $this->IdentificationVariable()
1487 18
            : $this->ResultVariable();
1488
    }
1489
1490
    /**
1491
     * OrderByItem ::= (
1492
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1493
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1494
     * ) ["ASC" | "DESC"]
1495
     *
1496
     * @return \Doctrine\ORM\Query\AST\OrderByItem
1497
     */
1498 179
    public function OrderByItem()
1499
    {
1500 179
        $this->lexer->peek(); // lookahead => '.'
1501 179
        $this->lexer->peek(); // lookahead => token after '.'
1502
1503 179
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1504
1505 179
        $this->lexer->resetPeek();
1506
1507 179
        $glimpse = $this->lexer->glimpse();
1508
1509
        switch (true) {
1510 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...
1511 1
                $expr = $this->FunctionDeclaration();
1512 1
                break;
1513
1514 178
            case ($this->isMathOperator($peek)):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1503 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...
1515 25
                $expr = $this->SimpleArithmeticExpression();
1516 25
                break;
1517
1518 154
            case ($glimpse['type'] === Lexer::T_DOT):
1519 141
                $expr = $this->SingleValuedPathExpression();
1520 141
                break;
1521
1522 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...
1523 2
                $expr = $this->ScalarExpression();
1524 2
                break;
1525
1526
            default:
1527 15
                $expr = $this->ResultVariable();
1528 15
                break;
1529
        }
1530
1531 179
        $type = 'ASC';
1532 179
        $item = new AST\OrderByItem($expr);
1533
1534
        switch (true) {
1535 179
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1536 93
                $this->match(Lexer::T_DESC);
1537 93
                $type = 'DESC';
1538 93
                break;
1539
1540 152
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1541 96
                $this->match(Lexer::T_ASC);
1542 96
                break;
1543
1544
            default:
1545
                // Do nothing
1546
        }
1547
1548 179
        $item->type = $type;
1549
1550 179
        return $item;
1551
    }
1552
1553
    /**
1554
     * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1555
     *      EnumPrimary | SimpleEntityExpression | "NULL"
1556
     *
1557
     * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1558
     * grammar that needs to be supported:
1559
     *
1560
     * NewValue ::= SimpleArithmeticExpression | "NULL"
1561
     *
1562
     * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1563
     *
1564
     * @return AST\ArithmeticExpression
1565
     */
1566 32
    public function NewValue()
1567
    {
1568 32
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1569 1
            $this->match(Lexer::T_NULL);
1570
1571 1
            return null;
1572
        }
1573
1574 31
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1575 18
            $this->match(Lexer::T_INPUT_PARAMETER);
1576
1577 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...
1578
        }
1579
1580 13
        return $this->ArithmeticExpression();
1581
    }
1582
1583
    /**
1584
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1585
     *
1586
     * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1587
     */
1588 737
    public function IdentificationVariableDeclaration()
1589
    {
1590 737
        $joins                    = array();
1591 737
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1592 722
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1593 7
            ? $this->IndexBy()
1594 722
            : null;
1595
1596 722
        $rangeVariableDeclaration->isRoot = true;
1597
1598
        while (
1599 722
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1600 722
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1601 722
            $this->lexer->isNextToken(Lexer::T_JOIN)
1602
        ) {
1603 274
            $joins[] = $this->Join();
1604
        }
1605
1606 720
        return new AST\IdentificationVariableDeclaration(
1607
            $rangeVariableDeclaration, $indexBy, $joins
1608
        );
1609
    }
1610
1611
    /**
1612
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1613
     *
1614
     * {Internal note: WARNING: Solution is harder than a bare implementation.
1615
     * Desired EBNF support:
1616
     *
1617
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1618
     *
1619
     * It demands that entire SQL generation to become programmatical. This is
1620
     * needed because association based subselect requires "WHERE" conditional
1621
     * expressions to be injected, but there is no scope to do that. Only scope
1622
     * accessible is "FROM", prohibiting an easy implementation without larger
1623
     * changes.}
1624
     *
1625
     * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration |
1626
     *         \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1627
     */
1628 48
    public function SubselectIdentificationVariableDeclaration()
1629
    {
1630
        /*
1631
        NOT YET IMPLEMENTED!
1632
1633
        $glimpse = $this->lexer->glimpse();
1634
1635
        if ($glimpse['type'] == Lexer::T_DOT) {
1636
            $associationPathExpression = $this->AssociationPathExpression();
1637
1638
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1639
                $this->match(Lexer::T_AS);
1640
            }
1641
1642
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1643
            $identificationVariable      = $associationPathExpression->identificationVariable;
1644
            $field                       = $associationPathExpression->associationField;
1645
1646
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1647
            $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1648
1649
            // Building queryComponent
1650
            $joinQueryComponent = array(
1651
                'metadata'     => $targetClass,
1652
                'parent'       => $identificationVariable,
1653
                'relation'     => $class->getAssociationMapping($field),
1654
                'map'          => null,
1655
                'nestingLevel' => $this->nestingLevel,
1656
                'token'        => $this->lexer->lookahead
1657
            );
1658
1659
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1660
1661
            return new AST\SubselectIdentificationVariableDeclaration(
1662
                $associationPathExpression, $aliasIdentificationVariable
1663
            );
1664
        }
1665
        */
1666
1667 48
        return $this->IdentificationVariableDeclaration();
1668
    }
1669
1670
    /**
1671
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1672
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1673
     *          ["WITH" ConditionalExpression]
1674
     *
1675
     * @return \Doctrine\ORM\Query\AST\Join
1676
     */
1677 274
    public function Join()
1678
    {
1679
        // Check Join type
1680 274
        $joinType = AST\Join::JOIN_TYPE_INNER;
1681
1682
        switch (true) {
1683 274
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1684 65
                $this->match(Lexer::T_LEFT);
1685
1686 65
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1687
1688
                // Possible LEFT OUTER join
1689 65
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1690
                    $this->match(Lexer::T_OUTER);
1691
1692
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1693
                }
1694 65
                break;
1695
1696 213
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1697 20
                $this->match(Lexer::T_INNER);
1698 20
                break;
1699
1700
            default:
1701
                // Do nothing
1702
        }
1703
1704 274
        $this->match(Lexer::T_JOIN);
1705
1706 274
        $next            = $this->lexer->glimpse();
1707 274
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1708 272
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1709 272
        $join            = new AST\Join($joinType, $joinDeclaration);
1710
1711
        // Describe non-root join declaration
1712 272
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1713 21
            $joinDeclaration->isRoot = false;
1714
        }
1715
1716
        // Check for ad-hoc Join conditions
1717 272
        if ($adhocConditions) {
1718 23
            $this->match(Lexer::T_WITH);
1719
1720 23
            $join->conditionalExpression = $this->ConditionalExpression();
1721
        }
1722
1723 272
        return $join;
1724
    }
1725
1726
    /**
1727
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1728
     *
1729
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1730
     */
1731 737
    public function RangeVariableDeclaration()
1732
    {
1733 737
        $abstractSchemaName = $this->AbstractSchemaName();
1734
1735 737
        $this->validateAbstractSchemaName($abstractSchemaName);
1736
1737 722
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1738 5
            $this->match(Lexer::T_AS);
1739
        }
1740
1741 722
        $token = $this->lexer->lookahead;
1742 722
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1743 722
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1744
1745
        // Building queryComponent
1746
        $queryComponent = array(
1747 722
            'metadata'     => $classMetadata,
1748
            'parent'       => null,
1749
            'relation'     => null,
1750
            'map'          => null,
1751 722
            'nestingLevel' => $this->nestingLevel,
1752 722
            'token'        => $token
1753
        );
1754
1755 722
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1756
1757 722
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1758
    }
1759
1760
    /**
1761
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1762
     *
1763
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1764
     */
1765 253
    public function JoinAssociationDeclaration()
1766
    {
1767 253
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1768
1769 253
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1770 4
            $this->match(Lexer::T_AS);
1771
        }
1772
1773 253
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1774 251
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1775
1776 251
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1777 251
        $field                  = $joinAssociationPathExpression->associationField;
1778
1779 251
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1780 251
        $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1781
1782
        // Building queryComponent
1783
        $joinQueryComponent = array(
1784 251
            'metadata'     => $targetClass,
1785 251
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1786 251
            'relation'     => $class->getAssociationMapping($field),
1787
            'map'          => null,
1788 251
            'nestingLevel' => $this->nestingLevel,
1789 251
            'token'        => $this->lexer->lookahead
1790
        );
1791
1792 251
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1793
1794 251
        return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
1795
    }
1796
1797
    /**
1798
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1799
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1800
     *
1801
     * @return array
1802
     */
1803 11
    public function PartialObjectExpression()
1804
    {
1805 11
        $this->match(Lexer::T_PARTIAL);
1806
1807 11
        $partialFieldSet = array();
1808
1809 11
        $identificationVariable = $this->IdentificationVariable();
1810
1811 11
        $this->match(Lexer::T_DOT);
1812 11
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1813 11
        $this->match(Lexer::T_IDENTIFIER);
1814
1815 11
        $field = $this->lexer->token['value'];
1816
1817
        // First field in partial expression might be embeddable property
1818 11
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1819 1
            $this->match(Lexer::T_DOT);
1820 1
            $this->match(Lexer::T_IDENTIFIER);
1821 1
            $field .= '.'.$this->lexer->token['value'];
1822
        }
1823
1824 11
        $partialFieldSet[] = $field;
1825
1826 11
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1827 9
            $this->match(Lexer::T_COMMA);
1828 9
            $this->match(Lexer::T_IDENTIFIER);
1829
    
1830 9
            $field = $this->lexer->token['value'];
1831
    
1832 9
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1833 2
                $this->match(Lexer::T_DOT);
1834 2
                $this->match(Lexer::T_IDENTIFIER);
1835 2
                $field .= '.'.$this->lexer->token['value'];
1836
            }
1837
    
1838 9
            $partialFieldSet[] = $field;
1839
        }
1840
1841 11
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1842
1843 11
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1844
1845
        // Defer PartialObjectExpression validation
1846 11
        $this->deferredPartialObjectExpressions[] = array(
1847 11
            'expression'   => $partialObjectExpression,
1848 11
            'nestingLevel' => $this->nestingLevel,
1849 11
            'token'        => $this->lexer->token,
1850
        );
1851
1852 11
        return $partialObjectExpression;
1853
    }
1854
1855
    /**
1856
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1857
     *
1858
     * @return \Doctrine\ORM\Query\AST\NewObjectExpression
1859
     */
1860 28
    public function NewObjectExpression()
1861
    {
1862 28
        $this->match(Lexer::T_NEW);
1863
1864 28
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1865 28
        $token = $this->lexer->token;
1866
1867 28
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1868
1869 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...
1870
1871 28
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1872 24
            $this->match(Lexer::T_COMMA);
1873
1874 24
            $args[] = $this->NewObjectArg();
1875
        }
1876
1877 28
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1878
1879 28
        $expression = new AST\NewObjectExpression($className, $args);
1880
1881
        // Defer NewObjectExpression validation
1882 28
        $this->deferredNewObjectExpressions[] = array(
1883 28
            'token'        => $token,
1884 28
            'expression'   => $expression,
1885 28
            'nestingLevel' => $this->nestingLevel,
1886
        );
1887
1888 28
        return $expression;
1889
    }
1890
1891
    /**
1892
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1893
     *
1894
     * @return mixed
1895
     */
1896 28
    public function NewObjectArg()
1897
    {
1898 28
        $token = $this->lexer->lookahead;
1899 28
        $peek  = $this->lexer->glimpse();
1900
1901 28
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1902 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1903 2
            $expression = $this->Subselect();
1904 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1905
1906 2
            return $expression;
1907
        }
1908
1909 28
        return $this->ScalarExpression();
1910
    }
1911
1912
    /**
1913
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1914
     *
1915
     * @return \Doctrine\ORM\Query\AST\IndexBy
1916
     */
1917 11
    public function IndexBy()
1918
    {
1919 11
        $this->match(Lexer::T_INDEX);
1920 11
        $this->match(Lexer::T_BY);
1921 11
        $pathExpr = $this->StateFieldPathExpression();
1922
1923
        // Add the INDEX BY info to the query component
1924 11
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1925
1926 11
        return new AST\IndexBy($pathExpr);
1927
    }
1928
1929
    /**
1930
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1931
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1932
     *                      InstanceOfExpression
1933
     *
1934
     * @return mixed One of the possible expressions or subexpressions.
1935
     */
1936 160
    public function ScalarExpression()
1937
    {
1938 160
        $lookahead = $this->lexer->lookahead['type'];
1939 160
        $peek      = $this->lexer->glimpse();
1940
1941
        switch (true) {
1942 160
            case ($lookahead === Lexer::T_INTEGER):
1943 157
            case ($lookahead === Lexer::T_FLOAT):
1944
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1945 157
            case ($lookahead === Lexer::T_MINUS):
1946 157
            case ($lookahead === Lexer::T_PLUS):
1947 17
                return $this->SimpleArithmeticExpression();
1948
1949 157
            case ($lookahead === Lexer::T_STRING):
1950 13
                return $this->StringPrimary();
1951
1952 155
            case ($lookahead === Lexer::T_TRUE):
1953 155
            case ($lookahead === Lexer::T_FALSE):
1954 3
                $this->match($lookahead);
1955
1956 3
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1957
1958 155
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
1959
                switch (true) {
1960 1
                    case $this->isMathOperator($peek):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->glimpse() on line 1939 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...
1961
                        // :param + u.value
1962 1
                        return $this->SimpleArithmeticExpression();
1963
                    default:
1964
                        return $this->InputParameter();
1965
                }
1966
1967 155
            case ($lookahead === Lexer::T_CASE):
1968 151
            case ($lookahead === Lexer::T_COALESCE):
1969 151
            case ($lookahead === Lexer::T_NULLIF):
1970
                // Since NULLIF and COALESCE can be identified as a function,
1971
                // we need to check these before checking for FunctionDeclaration
1972 8
                return $this->CaseExpression();
1973
1974 151
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1975 4
                return $this->SimpleArithmeticExpression();
1976
1977
            // this check must be done before checking for a filed path expression
1978 148
            case ($this->isFunction()):
1979 26
                $this->lexer->peek(); // "("
1980
1981
                switch (true) {
1982 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...
1983
                        // SUM(u.id) + COUNT(u.id)
1984 7
                        return $this->SimpleArithmeticExpression();
1985
1986 21
                    case ($this->isAggregateFunction($this->lexer->lookahead['type'])):
1987 19
                        return $this->AggregateExpression();
1988
1989
                    default:
1990
                        // IDENTITY(u)
1991 2
                        return $this->FunctionDeclaration();
1992
                }
1993
1994
                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...
1995
            // it is no function, so it must be a field path
1996 130
            case ($lookahead === Lexer::T_IDENTIFIER):
1997 130
                $this->lexer->peek(); // lookahead => '.'
1998 130
                $this->lexer->peek(); // lookahead => token after '.'
1999 130
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
2000 130
                $this->lexer->resetPeek();
2001
2002 130
                if ($this->isMathOperator($peek)) {
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1999 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...
2003 7
                    return $this->SimpleArithmeticExpression();
2004
                }
2005
2006 125
                return $this->StateFieldPathExpression();
2007
2008
            default:
2009
                $this->syntaxError();
2010
        }
2011
    }
2012
2013
    /**
2014
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
2015
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2016
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2017
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2018
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2019
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2020
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2021
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2022
     *
2023
     * @return mixed One of the possible expressions or subexpressions.
2024
     */
2025 19
    public function CaseExpression()
2026
    {
2027 19
        $lookahead = $this->lexer->lookahead['type'];
2028
2029
        switch ($lookahead) {
2030 19
            case Lexer::T_NULLIF:
2031 5
                return $this->NullIfExpression();
2032
2033 16
            case Lexer::T_COALESCE:
2034 2
                return $this->CoalesceExpression();
2035
2036 14
            case Lexer::T_CASE:
2037 14
                $this->lexer->resetPeek();
2038 14
                $peek = $this->lexer->peek();
2039
2040 14
                if ($peek['type'] === Lexer::T_WHEN) {
2041 9
                    return $this->GeneralCaseExpression();
2042
                }
2043
2044 5
                return $this->SimpleCaseExpression();
2045
2046
            default:
2047
                // Do nothing
2048
                break;
2049
        }
2050
2051
        $this->syntaxError();
2052
    }
2053
2054
    /**
2055
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2056
     *
2057
     * @return \Doctrine\ORM\Query\AST\CoalesceExpression
2058
     */
2059 3
    public function CoalesceExpression()
2060
    {
2061 3
        $this->match(Lexer::T_COALESCE);
2062 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2063
2064
        // Process ScalarExpressions (1..N)
2065 3
        $scalarExpressions = array();
2066 3
        $scalarExpressions[] = $this->ScalarExpression();
2067
2068 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2069 3
            $this->match(Lexer::T_COMMA);
2070
2071 3
            $scalarExpressions[] = $this->ScalarExpression();
2072
        }
2073
2074 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2075
2076 3
        return new AST\CoalesceExpression($scalarExpressions);
2077
    }
2078
2079
    /**
2080
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2081
     *
2082
     * @return \Doctrine\ORM\Query\AST\NullIfExpression
2083
     */
2084 5
    public function NullIfExpression()
2085
    {
2086 5
        $this->match(Lexer::T_NULLIF);
2087 5
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2088
2089 5
        $firstExpression = $this->ScalarExpression();
2090 5
        $this->match(Lexer::T_COMMA);
2091 5
        $secondExpression = $this->ScalarExpression();
2092
2093 5
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2094
2095 5
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2096
    }
2097
2098
    /**
2099
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2100
     *
2101
     * @return \Doctrine\ORM\Query\AST\GeneralCaseExpression
2102
     */
2103 9
    public function GeneralCaseExpression()
2104
    {
2105 9
        $this->match(Lexer::T_CASE);
2106
2107
        // Process WhenClause (1..N)
2108 9
        $whenClauses = array();
2109
2110
        do {
2111 9
            $whenClauses[] = $this->WhenClause();
2112 9
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2113
2114 9
        $this->match(Lexer::T_ELSE);
2115 9
        $scalarExpression = $this->ScalarExpression();
2116 9
        $this->match(Lexer::T_END);
2117
2118 9
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2119
    }
2120
2121
    /**
2122
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2123
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2124
     *
2125
     * @return AST\SimpleCaseExpression
2126
     */
2127 5
    public function SimpleCaseExpression()
2128
    {
2129 5
        $this->match(Lexer::T_CASE);
2130 5
        $caseOperand = $this->StateFieldPathExpression();
2131
2132
        // Process SimpleWhenClause (1..N)
2133 5
        $simpleWhenClauses = array();
2134
2135
        do {
2136 5
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2137 5
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2138
2139 5
        $this->match(Lexer::T_ELSE);
2140 5
        $scalarExpression = $this->ScalarExpression();
2141 5
        $this->match(Lexer::T_END);
2142
2143 5
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2144
    }
2145
2146
    /**
2147
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2148
     *
2149
     * @return \Doctrine\ORM\Query\AST\WhenClause
2150
     */
2151 9
    public function WhenClause()
2152
    {
2153 9
        $this->match(Lexer::T_WHEN);
2154 9
        $conditionalExpression = $this->ConditionalExpression();
2155 9
        $this->match(Lexer::T_THEN);
2156
2157 9
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2158
    }
2159
2160
    /**
2161
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2162
     *
2163
     * @return \Doctrine\ORM\Query\AST\SimpleWhenClause
2164
     */
2165 5
    public function SimpleWhenClause()
2166
    {
2167 5
        $this->match(Lexer::T_WHEN);
2168 5
        $conditionalExpression = $this->ScalarExpression();
2169 5
        $this->match(Lexer::T_THEN);
2170
2171 5
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2172
    }
2173
2174
    /**
2175
     * SelectExpression ::= (
2176
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2177
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2178
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2179
     *
2180
     * @return \Doctrine\ORM\Query\AST\SelectExpression
2181
     */
2182 749
    public function SelectExpression()
2183
    {
2184 749
        $expression    = null;
2185 749
        $identVariable = null;
2186 749
        $peek          = $this->lexer->glimpse();
2187 749
        $lookaheadType = $this->lexer->lookahead['type'];
2188
2189
        switch (true) {
2190
            // ScalarExpression (u.name)
2191 749
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2192 102
                $expression = $this->ScalarExpression();
2193 102
                break;
2194
2195
            // IdentificationVariable (u)
2196 690
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2197 575
                $expression = $identVariable = $this->IdentificationVariable();
2198 575
                break;
2199
2200
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
2201 176
            case ($lookaheadType === Lexer::T_CASE):
2202 171
            case ($lookaheadType === Lexer::T_COALESCE):
2203 169
            case ($lookaheadType === Lexer::T_NULLIF):
2204 9
                $expression = $this->CaseExpression();
2205 9
                break;
2206
2207
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2208 167
            case ($this->isFunction()):
2209 87
                $this->lexer->peek(); // "("
2210
2211
                switch (true) {
2212 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...
2213
                        // SUM(u.id) + COUNT(u.id)
2214 2
                        $expression = $this->ScalarExpression();
2215 2
                        break;
2216
2217 85
                    case ($this->isAggregateFunction($lookaheadType)):
2218
                        // COUNT(u.id)
2219 54
                        $expression = $this->AggregateExpression();
2220 54
                        break;
2221
2222
                    default:
2223
                        // IDENTITY(u)
2224 31
                        $expression = $this->FunctionDeclaration();
2225 31
                        break;
2226
                }
2227
2228 87
                break;
2229
2230
            // PartialObjectExpression (PARTIAL u.{id, name})
2231 80
            case ($lookaheadType === Lexer::T_PARTIAL):
2232 11
                $expression    = $this->PartialObjectExpression();
2233 11
                $identVariable = $expression->identificationVariable;
2234 11
                break;
2235
2236
            // Subselect
2237 69
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2238 22
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2239 22
                $expression = $this->Subselect();
2240 22
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2241 22
                break;
2242
2243
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2244 47
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2245 43
            case ($lookaheadType === Lexer::T_INTEGER):
2246 41
            case ($lookaheadType === Lexer::T_STRING):
2247 32
            case ($lookaheadType === Lexer::T_FLOAT):
2248
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2249 32
            case ($lookaheadType === Lexer::T_MINUS):
2250 32
            case ($lookaheadType === Lexer::T_PLUS):
2251 16
                $expression = $this->SimpleArithmeticExpression();
2252 16
                break;
2253
2254
            // NewObjectExpression (New ClassName(id, name))
2255 31
            case ($lookaheadType === Lexer::T_NEW):
2256 28
                $expression = $this->NewObjectExpression();
2257 28
                break;
2258
2259
            default:
2260 3
                $this->syntaxError(
2261 3
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2262 3
                    $this->lexer->lookahead
2263
                );
2264
        }
2265
2266
        // [["AS"] ["HIDDEN"] AliasResultVariable]
2267 746
        $mustHaveAliasResultVariable = false;
2268
2269 746
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2270 110
            $this->match(Lexer::T_AS);
2271
2272 110
            $mustHaveAliasResultVariable = true;
2273
        }
2274
2275 746
        $hiddenAliasResultVariable = false;
2276
2277 746
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2278 10
            $this->match(Lexer::T_HIDDEN);
2279
2280 10
            $hiddenAliasResultVariable = true;
2281
        }
2282
2283 746
        $aliasResultVariable = null;
2284
2285 746
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2286 119
            $token = $this->lexer->lookahead;
2287 119
            $aliasResultVariable = $this->AliasResultVariable();
2288
2289
            // Include AliasResultVariable in query components.
2290 114
            $this->queryComponents[$aliasResultVariable] = array(
2291 114
                'resultVariable' => $expression,
2292 114
                'nestingLevel'   => $this->nestingLevel,
2293 114
                'token'          => $token,
2294
            );
2295
        }
2296
2297
        // AST
2298
2299 741
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2300
2301 741
        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...
2302 583
            $this->identVariableExpressions[$identVariable] = $expr;
2303
        }
2304
2305 741
        return $expr;
2306
    }
2307
2308
    /**
2309
     * SimpleSelectExpression ::= (
2310
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2311
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2312
     * ) [["AS"] AliasResultVariable]
2313
     *
2314
     * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
2315
     */
2316 48
    public function SimpleSelectExpression()
2317
    {
2318 48
        $peek = $this->lexer->glimpse();
2319
2320 48
        switch ($this->lexer->lookahead['type']) {
2321 48
            case Lexer::T_IDENTIFIER:
2322
                switch (true) {
2323 19
                    case ($peek['type'] === Lexer::T_DOT):
2324 16
                        $expression = $this->StateFieldPathExpression();
2325
2326 16
                        return new AST\SimpleSelectExpression($expression);
2327
2328 3
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2329 2
                        $expression = $this->IdentificationVariable();
2330
2331 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...
2332
2333 1
                    case ($this->isFunction()):
2334
                        // SUM(u.id) + COUNT(u.id)
2335 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...
2336
                            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...
2337
                        }
2338
                        // COUNT(u.id)
2339 1
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2340
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2341
                        }
2342
                        // IDENTITY(u)
2343 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...
2344
2345
                    default:
2346
                        // Do nothing
2347
                }
2348
                break;
2349
2350 30
            case Lexer::T_OPEN_PARENTHESIS:
2351 3
                if ($peek['type'] !== Lexer::T_SELECT) {
2352
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2353 3
                    $expression = $this->SimpleArithmeticExpression();
2354
2355 3
                    return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->SimpleArithmeticExpression() on line 2353 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...
2356
                }
2357
2358
                // Subselect
2359
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2360
                $expression = $this->Subselect();
2361
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2362
2363
                return new AST\SimpleSelectExpression($expression);
2364
2365
            default:
2366
                // Do nothing
2367
        }
2368
2369 27
        $this->lexer->peek();
2370
2371 27
        $expression = $this->ScalarExpression();
2372 27
        $expr       = new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->ScalarExpression() on line 2371 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...
2373
2374 27
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2375 1
            $this->match(Lexer::T_AS);
2376
        }
2377
2378 27
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2379 2
            $token = $this->lexer->lookahead;
2380 2
            $resultVariable = $this->AliasResultVariable();
2381 2
            $expr->fieldIdentificationVariable = $resultVariable;
2382
2383
            // Include AliasResultVariable in query components.
2384 2
            $this->queryComponents[$resultVariable] = array(
2385 2
                'resultvariable' => $expr,
2386 2
                'nestingLevel'   => $this->nestingLevel,
2387 2
                'token'          => $token,
2388
            );
2389
        }
2390
2391 27
        return $expr;
2392
    }
2393
2394
    /**
2395
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2396
     *
2397
     * @return \Doctrine\ORM\Query\AST\ConditionalExpression
2398
     */
2399 369
    public function ConditionalExpression()
2400
    {
2401 369
        $conditionalTerms = array();
2402 369
        $conditionalTerms[] = $this->ConditionalTerm();
2403
2404 366
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2405 16
            $this->match(Lexer::T_OR);
2406
2407 16
            $conditionalTerms[] = $this->ConditionalTerm();
2408
        }
2409
2410
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2411
        // if only one AST\ConditionalTerm is defined
2412 366
        if (count($conditionalTerms) == 1) {
2413 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...
2414
        }
2415
2416 16
        return new AST\ConditionalExpression($conditionalTerms);
2417
    }
2418
2419
    /**
2420
     * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2421
     *
2422
     * @return \Doctrine\ORM\Query\AST\ConditionalTerm
2423
     */
2424 369
    public function ConditionalTerm()
2425
    {
2426 369
        $conditionalFactors = array();
2427 369
        $conditionalFactors[] = $this->ConditionalFactor();
2428
2429 366
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2430 32
            $this->match(Lexer::T_AND);
2431
2432 32
            $conditionalFactors[] = $this->ConditionalFactor();
2433
        }
2434
2435
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2436
        // if only one AST\ConditionalFactor is defined
2437 366
        if (count($conditionalFactors) == 1) {
2438 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...
2439
        }
2440
2441 32
        return new AST\ConditionalTerm($conditionalFactors);
2442
    }
2443
2444
    /**
2445
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2446
     *
2447
     * @return \Doctrine\ORM\Query\AST\ConditionalFactor
2448
     */
2449 369
    public function ConditionalFactor()
2450
    {
2451 369
        $not = false;
2452
2453 369
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2454 6
            $this->match(Lexer::T_NOT);
2455
2456 6
            $not = true;
2457
        }
2458
2459 369
        $conditionalPrimary = $this->ConditionalPrimary();
2460
2461
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2462
        // if only one AST\ConditionalPrimary is defined
2463 366
        if ( ! $not) {
2464 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...
2465
        }
2466
2467 6
        $conditionalFactor = new AST\ConditionalFactor($conditionalPrimary);
2468 6
        $conditionalFactor->not = $not;
2469
2470 6
        return $conditionalFactor;
2471
    }
2472
2473
    /**
2474
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2475
     *
2476
     * @return \Doctrine\ORM\Query\AST\ConditionalPrimary
2477
     */
2478 369
    public function ConditionalPrimary()
2479
    {
2480 369
        $condPrimary = new AST\ConditionalPrimary;
2481
2482 369
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2483 360
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2484
2485 357
            return $condPrimary;
2486
        }
2487
2488
        // Peek beyond the matching closing parenthesis ')'
2489 25
        $peek = $this->peekBeyondClosingParenthesis();
2490
2491 25
        if (in_array($peek['value'], array("=",  "<", "<=", "<>", ">", ">=", "!=")) ||
2492 22
            in_array($peek['type'], array(Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS)) ||
2493 25
            $this->isMathOperator($peek)) {
2494 15
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2495
2496 15
            return $condPrimary;
2497
        }
2498
2499 21
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2500 21
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2501 21
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2502
2503 21
        return $condPrimary;
2504
    }
2505
2506
    /**
2507
     * SimpleConditionalExpression ::=
2508
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2509
     *      InExpression | NullComparisonExpression | ExistsExpression |
2510
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2511
     *      InstanceOfExpression
2512
     */
2513 369
    public function SimpleConditionalExpression()
2514
    {
2515 369
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2516 7
            return $this->ExistsExpression();
2517
        }
2518
2519 369
        $token      = $this->lexer->lookahead;
2520 369
        $peek       = $this->lexer->glimpse();
2521 369
        $lookahead  = $token;
2522
2523 369
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2524
            $token = $this->lexer->glimpse();
2525
        }
2526
2527 369
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2528
            // Peek beyond the matching closing parenthesis.
2529 345
            $beyond = $this->lexer->peek();
2530
2531 345
            switch ($peek['value']) {
2532 345
                case '(':
2533
                    // Peeks beyond the matched closing parenthesis.
2534 34
                    $token = $this->peekBeyondClosingParenthesis(false);
2535
2536 34
                    if ($token['type'] === Lexer::T_NOT) {
2537 3
                        $token = $this->lexer->peek();
2538
                    }
2539
2540 34
                    if ($token['type'] === Lexer::T_IS) {
2541 2
                        $lookahead = $this->lexer->peek();
2542
                    }
2543 34
                    break;
2544
2545
                default:
2546
                    // Peek beyond the PathExpression or InputParameter.
2547 317
                    $token = $beyond;
2548
2549 317
                    while ($token['value'] === '.') {
2550 279
                        $this->lexer->peek();
2551
2552 279
                        $token = $this->lexer->peek();
2553
                    }
2554
2555
                    // Also peek beyond a NOT if there is one.
2556 317
                    if ($token['type'] === Lexer::T_NOT) {
2557 11
                        $token = $this->lexer->peek();
2558
                    }
2559
2560
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2561 317
                    $lookahead = $this->lexer->peek();
2562
            }
2563
2564
            // Also peek beyond a NOT if there is one.
2565 345
            if ($lookahead['type'] === Lexer::T_NOT) {
2566 6
                $lookahead = $this->lexer->peek();
2567
            }
2568
2569 345
            $this->lexer->resetPeek();
2570
        }
2571
2572 369
        if ($token['type'] === Lexer::T_BETWEEN) {
2573 8
            return $this->BetweenExpression();
2574
        }
2575
2576 363
        if ($token['type'] === Lexer::T_LIKE) {
2577 14
            return $this->LikeExpression();
2578
        }
2579
2580 350
        if ($token['type'] === Lexer::T_IN) {
2581 34
            return $this->InExpression();
2582
        }
2583
2584 325
        if ($token['type'] === Lexer::T_INSTANCE) {
2585 12
            return $this->InstanceOfExpression();
2586
        }
2587
2588 313
        if ($token['type'] === Lexer::T_MEMBER) {
2589 7
            return $this->CollectionMemberExpression();
2590
        }
2591
2592 306
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2593 13
            return $this->NullComparisonExpression();
2594
        }
2595
2596 296
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2597 4
            return $this->EmptyCollectionComparisonExpression();
2598
        }
2599
2600 292
        return $this->ComparisonExpression();
2601
    }
2602
2603
    /**
2604
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2605
     *
2606
     * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression
2607
     */
2608 4
    public function EmptyCollectionComparisonExpression()
2609
    {
2610 4
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2611 4
            $this->CollectionValuedPathExpression()
2612
        );
2613 4
        $this->match(Lexer::T_IS);
2614
2615 4
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2616 2
            $this->match(Lexer::T_NOT);
2617 2
            $emptyCollectionCompExpr->not = true;
2618
        }
2619
2620 4
        $this->match(Lexer::T_EMPTY);
2621
2622 4
        return $emptyCollectionCompExpr;
2623
    }
2624
2625
    /**
2626
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2627
     *
2628
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2629
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2630
     *
2631
     * @return \Doctrine\ORM\Query\AST\CollectionMemberExpression
2632
     */
2633 7
    public function CollectionMemberExpression()
2634
    {
2635 7
        $not        = false;
2636 7
        $entityExpr = $this->EntityExpression();
2637
2638 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2639
            $this->match(Lexer::T_NOT);
2640
2641
            $not = true;
2642
        }
2643
2644 7
        $this->match(Lexer::T_MEMBER);
2645
2646 7
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2647 7
            $this->match(Lexer::T_OF);
2648
        }
2649
2650 7
        $collMemberExpr = new AST\CollectionMemberExpression(
2651 7
            $entityExpr, $this->CollectionValuedPathExpression()
2652
        );
2653 7
        $collMemberExpr->not = $not;
2654
2655 7
        return $collMemberExpr;
2656
    }
2657
2658
    /**
2659
     * Literal ::= string | char | integer | float | boolean
2660
     *
2661
     * @return \Doctrine\ORM\Query\AST\Literal
2662
     */
2663 177
    public function Literal()
2664
    {
2665 177
        switch ($this->lexer->lookahead['type']) {
2666 177
            case Lexer::T_STRING:
2667 49
                $this->match(Lexer::T_STRING);
2668
2669 49
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2670 137
            case Lexer::T_INTEGER:
2671 9
            case Lexer::T_FLOAT:
2672 129
                $this->match(
2673 129
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2674
                );
2675
2676 129
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2677 8
            case Lexer::T_TRUE:
2678 4
            case Lexer::T_FALSE:
2679 8
                $this->match(
2680 8
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2681
                );
2682
2683 8
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2684
            default:
2685
                $this->syntaxError('Literal');
2686
        }
2687
    }
2688
2689
    /**
2690
     * InParameter ::= Literal | InputParameter
2691
     *
2692
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2693
     */
2694 25
    public function InParameter()
2695
    {
2696 25
        if ($this->lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) {
2697 13
            return $this->InputParameter();
2698
        }
2699
2700 12
        return $this->Literal();
2701
    }
2702
2703
    /**
2704
     * InputParameter ::= PositionalParameter | NamedParameter
2705
     *
2706
     * @return \Doctrine\ORM\Query\AST\InputParameter
2707
     */
2708 160
    public function InputParameter()
2709
    {
2710 160
        $this->match(Lexer::T_INPUT_PARAMETER);
2711
2712 160
        return new AST\InputParameter($this->lexer->token['value']);
2713
    }
2714
2715
    /**
2716
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2717
     *
2718
     * @return \Doctrine\ORM\Query\AST\ArithmeticExpression
2719
     */
2720 325
    public function ArithmeticExpression()
2721
    {
2722 325
        $expr = new AST\ArithmeticExpression;
2723
2724 325
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2725 19
            $peek = $this->lexer->glimpse();
2726
2727 19
            if ($peek['type'] === Lexer::T_SELECT) {
2728 7
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2729 7
                $expr->subselect = $this->Subselect();
2730 7
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2731
2732 7
                return $expr;
2733
            }
2734
        }
2735
2736 325
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2737
2738 325
        return $expr;
2739
    }
2740
2741
    /**
2742
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2743
     *
2744
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2745
     */
2746 428
    public function SimpleArithmeticExpression()
2747
    {
2748 428
        $terms = array();
2749 428
        $terms[] = $this->ArithmeticTerm();
2750
2751 428
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2752 21
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2753
2754 21
            $terms[] = $this->lexer->token['value'];
2755 21
            $terms[] = $this->ArithmeticTerm();
2756
        }
2757
2758
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2759
        // if only one AST\ArithmeticTerm is defined
2760 428
        if (count($terms) == 1) {
2761 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 2761 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...
2762
        }
2763
2764 21
        return new AST\SimpleArithmeticExpression($terms);
2765
    }
2766
2767
    /**
2768
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2769
     *
2770
     * @return \Doctrine\ORM\Query\AST\ArithmeticTerm
2771
     */
2772 428
    public function ArithmeticTerm()
2773
    {
2774 428
        $factors = array();
2775 428
        $factors[] = $this->ArithmeticFactor();
2776
2777 428
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2778 53
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2779
2780 53
            $factors[] = $this->lexer->token['value'];
2781 53
            $factors[] = $this->ArithmeticFactor();
2782
        }
2783
2784
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2785
        // if only one AST\ArithmeticFactor is defined
2786 428
        if (count($factors) == 1) {
2787 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 2787 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticTerm of type Doctrine\ORM\Query\AST\ArithmeticTerm|null.
Loading history...
2788
        }
2789
2790 53
        return new AST\ArithmeticTerm($factors);
2791
    }
2792
2793
    /**
2794
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2795
     *
2796
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2797
     */
2798 428
    public function ArithmeticFactor()
2799
    {
2800 428
        $sign = null;
2801
2802 428
        if (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2803 3
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2804 3
            $sign = $isPlus;
2805
        }
2806
2807 428
        $primary = $this->ArithmeticPrimary();
2808
2809
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2810
        // if only one AST\ArithmeticPrimary is defined
2811 428
        if ($sign === null) {
2812 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...
2813
        }
2814
2815 3
        return new AST\ArithmeticFactor($primary, $sign);
2816
    }
2817
2818
    /**
2819
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2820
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2821
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2822
     *          | InputParameter | CaseExpression
2823
     */
2824 432
    public function ArithmeticPrimary()
2825
    {
2826 432
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2827 25
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2828
2829 25
            $expr = $this->SimpleArithmeticExpression();
2830
2831 25
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2832
2833 25
            return new AST\ParenthesisExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr defined by $this->SimpleArithmeticExpression() on line 2829 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...
2834
        }
2835
2836 432
        switch ($this->lexer->lookahead['type']) {
2837 432
            case Lexer::T_COALESCE:
2838 432
            case Lexer::T_NULLIF:
2839 432
            case Lexer::T_CASE:
2840 4
                return $this->CaseExpression();
2841
2842 432
            case Lexer::T_IDENTIFIER:
2843 402
                $peek = $this->lexer->glimpse();
2844
2845 402
                if ($peek['value'] == '(') {
2846 29
                    return $this->FunctionDeclaration();
2847
                }
2848
2849 381
                if ($peek['value'] == '.') {
2850 370
                    return $this->SingleValuedPathExpression();
2851
                }
2852
2853 46
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2854 10
                    return $this->ResultVariable();
2855
                }
2856
2857 38
                return $this->StateFieldPathExpression();
2858
2859 305
            case Lexer::T_INPUT_PARAMETER:
2860 142
                return $this->InputParameter();
2861
2862
            default:
2863 171
                $peek = $this->lexer->glimpse();
2864
2865 171
                if ($peek['value'] == '(') {
2866 18
                    if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2867 18
                        return $this->AggregateExpression();
2868
                    }
2869
2870
                    return $this->FunctionDeclaration();
2871
                }
2872
2873 167
                return $this->Literal();
2874
        }
2875
    }
2876
2877
    /**
2878
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2879
     *
2880
     * @return \Doctrine\ORM\Query\AST\StringPrimary |
2881
     *         \Doctrine\ORM\Query\AST\Subselect |
2882
     *         string
2883
     */
2884 14
    public function StringExpression()
2885
    {
2886 14
        $peek = $this->lexer->glimpse();
2887
2888
        // Subselect
2889 14
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
2890
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2891
            $expr = $this->Subselect();
2892
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2893
2894
            return $expr;
2895
        }
2896
2897
        // ResultVariable (string)
2898 14
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2899 14
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2900 2
            return $this->ResultVariable();
2901
        }
2902
2903 12
        return $this->StringPrimary();
2904
    }
2905
2906
    /**
2907
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2908
     */
2909 51
    public function StringPrimary()
2910
    {
2911 51
        $lookaheadType = $this->lexer->lookahead['type'];
2912
2913
        switch ($lookaheadType) {
2914 51
            case Lexer::T_IDENTIFIER:
2915 32
                $peek = $this->lexer->glimpse();
2916
2917 32
                if ($peek['value'] == '.') {
2918 32
                    return $this->StateFieldPathExpression();
2919
                }
2920
2921 8
                if ($peek['value'] == '(') {
2922
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2923 8
                    return $this->FunctionDeclaration();
2924
                }
2925
2926
                $this->syntaxError("'.' or '('");
2927
                break;
2928
2929 32
            case Lexer::T_STRING:
2930 32
                $this->match(Lexer::T_STRING);
2931
2932 32
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2933
2934 2
            case Lexer::T_INPUT_PARAMETER:
2935 2
                return $this->InputParameter();
2936
2937
            case Lexer::T_CASE:
2938
            case Lexer::T_COALESCE:
2939
            case Lexer::T_NULLIF:
2940
                return $this->CaseExpression();
2941
2942
            default:
2943
                if ($this->isAggregateFunction($lookaheadType)) {
2944
                    return $this->AggregateExpression();
2945
                }
2946
        }
2947
2948
        $this->syntaxError(
2949
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2950
        );
2951
    }
2952
2953
    /**
2954
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2955
     *
2956
     * @return \Doctrine\ORM\Query\AST\SingleValuedAssociationPathExpression |
2957
     *         \Doctrine\ORM\Query\AST\SimpleEntityExpression
2958
     */
2959 7
    public function EntityExpression()
2960
    {
2961 7
        $glimpse = $this->lexer->glimpse();
2962
2963 7
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2964 1
            return $this->SingleValuedAssociationPathExpression();
2965
        }
2966
2967 6
        return $this->SimpleEntityExpression();
2968
    }
2969
2970
    /**
2971
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2972
     *
2973
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2974
     */
2975 6
    public function SimpleEntityExpression()
2976
    {
2977 6
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2978 5
            return $this->InputParameter();
2979
        }
2980
2981 1
        return $this->StateFieldPathExpression();
2982
    }
2983
2984
    /**
2985
     * AggregateExpression ::=
2986
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2987
     *
2988
     * @return \Doctrine\ORM\Query\AST\AggregateExpression
2989
     */
2990 84
    public function AggregateExpression()
2991
    {
2992 84
        $lookaheadType = $this->lexer->lookahead['type'];
2993 84
        $isDistinct = false;
2994
2995 84
        if ( ! in_array($lookaheadType, array(Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM))) {
2996
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2997
        }
2998
2999 84
        $this->match($lookaheadType);
3000 84
        $functionName = $this->lexer->token['value'];
3001 84
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3002
3003 84
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
3004 2
            $this->match(Lexer::T_DISTINCT);
3005 2
            $isDistinct = true;
3006
        }
3007
3008 84
        $pathExp = $this->SimpleArithmeticExpression();
3009
3010 84
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3011
3012 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 3008 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...
3013
    }
3014
3015
    /**
3016
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
3017
     *
3018
     * @return \Doctrine\ORM\Query\AST\QuantifiedExpression
3019
     */
3020 3
    public function QuantifiedExpression()
3021
    {
3022 3
        $lookaheadType = $this->lexer->lookahead['type'];
3023 3
        $value = $this->lexer->lookahead['value'];
3024
3025 3
        if ( ! in_array($lookaheadType, array(Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME))) {
3026
            $this->syntaxError('ALL, ANY or SOME');
3027
        }
3028
3029 3
        $this->match($lookaheadType);
3030 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3031
3032 3
        $qExpr = new AST\QuantifiedExpression($this->Subselect());
3033 3
        $qExpr->type = $value;
3034
3035 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3036
3037 3
        return $qExpr;
3038
    }
3039
3040
    /**
3041
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3042
     *
3043
     * @return \Doctrine\ORM\Query\AST\BetweenExpression
3044
     */
3045 8
    public function BetweenExpression()
3046
    {
3047 8
        $not = false;
3048 8
        $arithExpr1 = $this->ArithmeticExpression();
3049
3050 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3051 3
            $this->match(Lexer::T_NOT);
3052 3
            $not = true;
3053
        }
3054
3055 8
        $this->match(Lexer::T_BETWEEN);
3056 8
        $arithExpr2 = $this->ArithmeticExpression();
3057 8
        $this->match(Lexer::T_AND);
3058 8
        $arithExpr3 = $this->ArithmeticExpression();
3059
3060 8
        $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3061 8
        $betweenExpr->not = $not;
3062
3063 8
        return $betweenExpr;
3064
    }
3065
3066
    /**
3067
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3068
     *
3069
     * @return \Doctrine\ORM\Query\AST\ComparisonExpression
3070
     */
3071 292
    public function ComparisonExpression()
3072
    {
3073 292
        $this->lexer->glimpse();
3074
3075 292
        $leftExpr  = $this->ArithmeticExpression();
3076 292
        $operator  = $this->ComparisonOperator();
3077 292
        $rightExpr = ($this->isNextAllAnySome())
3078 3
            ? $this->QuantifiedExpression()
3079 292
            : $this->ArithmeticExpression();
3080
3081 290
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3082
    }
3083
3084
    /**
3085
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3086
     *
3087
     * @return \Doctrine\ORM\Query\AST\InExpression
3088
     */
3089 34
    public function InExpression()
3090
    {
3091 34
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3092
3093 34
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3094 6
            $this->match(Lexer::T_NOT);
3095 6
            $inExpression->not = true;
3096
        }
3097
3098 34
        $this->match(Lexer::T_IN);
3099 34
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3100
3101 34
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3102 9
            $inExpression->subselect = $this->Subselect();
3103
        } else {
3104 25
            $literals = array();
3105 25
            $literals[] = $this->InParameter();
3106
3107 25
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3108 16
                $this->match(Lexer::T_COMMA);
3109 16
                $literals[] = $this->InParameter();
3110
            }
3111
3112 25
            $inExpression->literals = $literals;
3113
        }
3114
3115 33
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3116
3117 33
        return $inExpression;
3118
    }
3119
3120
    /**
3121
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3122
     *
3123
     * @return \Doctrine\ORM\Query\AST\InstanceOfExpression
3124
     */
3125 12
    public function InstanceOfExpression()
3126
    {
3127 12
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3128
3129 12
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3130 1
            $this->match(Lexer::T_NOT);
3131 1
            $instanceOfExpression->not = true;
3132
        }
3133
3134 12
        $this->match(Lexer::T_INSTANCE);
3135 12
        $this->match(Lexer::T_OF);
3136
3137 12
        $exprValues = array();
3138
3139 12
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3140 1
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3141
3142 1
            $exprValues[] = $this->InstanceOfParameter();
3143
3144 1
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3145 1
                $this->match(Lexer::T_COMMA);
3146
3147 1
                $exprValues[] = $this->InstanceOfParameter();
3148
            }
3149
3150 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3151
3152 1
            $instanceOfExpression->value = $exprValues;
3153
3154 1
            return $instanceOfExpression;
3155
        }
3156
3157 11
        $exprValues[] = $this->InstanceOfParameter();
3158
3159 11
        $instanceOfExpression->value = $exprValues;
3160
3161 11
        return $instanceOfExpression;
3162
    }
3163
3164
    /**
3165
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3166
     *
3167
     * @return mixed
3168
     */
3169 12
    public function InstanceOfParameter()
3170
    {
3171 12
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3172 5
            $this->match(Lexer::T_INPUT_PARAMETER);
3173
3174 5
            return new AST\InputParameter($this->lexer->token['value']);
3175
        }
3176
3177 7
        $abstractSchemaName = $this->AbstractSchemaName();
3178
3179 7
        $this->validateAbstractSchemaName($abstractSchemaName);
3180
3181 7
        return $abstractSchemaName;
3182
    }
3183
3184
    /**
3185
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3186
     *
3187
     * @return \Doctrine\ORM\Query\AST\LikeExpression
3188
     */
3189 14
    public function LikeExpression()
3190
    {
3191 14
        $stringExpr = $this->StringExpression();
3192 14
        $not = false;
3193
3194 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3195 3
            $this->match(Lexer::T_NOT);
3196 3
            $not = true;
3197
        }
3198
3199 14
        $this->match(Lexer::T_LIKE);
3200
3201 14
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3202 4
            $this->match(Lexer::T_INPUT_PARAMETER);
3203 4
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3204
        } else {
3205 11
            $stringPattern = $this->StringPrimary();
3206
        }
3207
3208 14
        $escapeChar = null;
3209
3210 14
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3211 2
            $this->match(Lexer::T_ESCAPE);
3212 2
            $this->match(Lexer::T_STRING);
3213
3214 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3215
        }
3216
3217 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 3191 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 3205 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...
3218 14
        $likeExpr->not = $not;
3219
3220 14
        return $likeExpr;
3221
    }
3222
3223
    /**
3224
     * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
3225
     *
3226
     * @return \Doctrine\ORM\Query\AST\NullComparisonExpression
3227
     */
3228 13
    public function NullComparisonExpression()
3229
    {
3230
        switch (true) {
3231 13
            case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER):
3232
                $this->match(Lexer::T_INPUT_PARAMETER);
3233
3234
                $expr = new AST\InputParameter($this->lexer->token['value']);
3235
                break;
3236
3237 13
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3238 1
                $expr = $this->NullIfExpression();
3239 1
                break;
3240
3241 13
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3242 1
                $expr = $this->CoalesceExpression();
3243 1
                break;
3244
3245 13
            case $this->isAggregateFunction($this->lexer->lookahead['type']):
3246 1
                $expr = $this->AggregateExpression();
3247 1
                break;
3248
3249 13
            case $this->isFunction():
3250 1
                $expr = $this->FunctionDeclaration();
3251 1
                break;
3252
3253
            default:
3254
                // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
3255 12
                $glimpse = $this->lexer->glimpse();
3256
3257 12
                if ($glimpse['type'] === Lexer::T_DOT) {
3258 9
                    $expr = $this->SingleValuedPathExpression();
3259
3260
                    // Leave switch statement
3261 9
                    break;
3262
                }
3263
3264 3
                $lookaheadValue = $this->lexer->lookahead['value'];
3265
3266
                // Validate existing component
3267 3
                if ( ! isset($this->queryComponents[$lookaheadValue])) {
3268
                    $this->semanticalError('Cannot add having condition on undefined result variable.');
3269
                }
3270
3271
                // Validating ResultVariable
3272 3
                if ( ! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3273
                    $this->semanticalError('Cannot add having condition on a non result variable.');
3274
                }
3275
3276 3
                $expr = $this->ResultVariable();
3277 3
                break;
3278
        }
3279
3280 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...
3281
3282 13
        $this->match(Lexer::T_IS);
3283
3284 13
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3285 4
            $this->match(Lexer::T_NOT);
3286
3287 4
            $nullCompExpr->not = true;
3288
        }
3289
3290 13
        $this->match(Lexer::T_NULL);
3291
3292 13
        return $nullCompExpr;
3293
    }
3294
3295
    /**
3296
     * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"
3297
     *
3298
     * @return \Doctrine\ORM\Query\AST\ExistsExpression
3299
     */
3300 7
    public function ExistsExpression()
3301
    {
3302 7
        $not = false;
3303
3304 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3305
            $this->match(Lexer::T_NOT);
3306
            $not = true;
3307
        }
3308
3309 7
        $this->match(Lexer::T_EXISTS);
3310 7
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3311
3312 7
        $existsExpression = new AST\ExistsExpression($this->Subselect());
3313 7
        $existsExpression->not = $not;
3314
3315 7
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3316
3317 7
        return $existsExpression;
3318
    }
3319
3320
    /**
3321
     * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!="
3322
     *
3323
     * @return string
3324
     */
3325 292
    public function ComparisonOperator()
3326
    {
3327 292
        switch ($this->lexer->lookahead['value']) {
3328 292
            case '=':
3329 241
                $this->match(Lexer::T_EQUALS);
3330
3331 241
                return '=';
3332
3333 62
            case '<':
3334 17
                $this->match(Lexer::T_LOWER_THAN);
3335 17
                $operator = '<';
3336
3337 17
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3338 5
                    $this->match(Lexer::T_EQUALS);
3339 5
                    $operator .= '=';
3340 12
                } else if ($this->lexer->isNextToken(Lexer::T_GREATER_THAN)) {
3341 3
                    $this->match(Lexer::T_GREATER_THAN);
3342 3
                    $operator .= '>';
3343
                }
3344
3345 17
                return $operator;
3346
3347 53
            case '>':
3348 47
                $this->match(Lexer::T_GREATER_THAN);
3349 47
                $operator = '>';
3350
3351 47
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3352 6
                    $this->match(Lexer::T_EQUALS);
3353 6
                    $operator .= '=';
3354
                }
3355
3356 47
                return $operator;
3357
3358 6
            case '!':
3359 6
                $this->match(Lexer::T_NEGATE);
3360 6
                $this->match(Lexer::T_EQUALS);
3361
3362 6
                return '<>';
3363
3364
            default:
3365
                $this->syntaxError('=, <, <=, <>, >, >=, !=');
3366
        }
3367
    }
3368
3369
    /**
3370
     * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
3371
     *
3372
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3373
     */
3374 59
    public function FunctionDeclaration()
3375
    {
3376 59
        $token = $this->lexer->lookahead;
3377 59
        $funcName = strtolower($token['value']);
3378
3379
        // Check for built-in functions first!
3380
        switch (true) {
3381 59
            case (isset(self::$_STRING_FUNCTIONS[$funcName])):
3382 30
                return $this->FunctionsReturningStrings();
3383
3384 32
            case (isset(self::$_NUMERIC_FUNCTIONS[$funcName])):
3385 23
                return $this->FunctionsReturningNumerics();
3386
3387 10
            case (isset(self::$_DATETIME_FUNCTIONS[$funcName])):
3388 7
                return $this->FunctionsReturningDatetime();
3389
3390
            default:
3391 3
                return $this->CustomFunctionDeclaration();
3392
        }
3393
    }
3394
3395
    /**
3396
     * Helper function for FunctionDeclaration grammar rule.
3397
     *
3398
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3399
     */
3400 3
    private function CustomFunctionDeclaration()
3401
    {
3402 3
        $token = $this->lexer->lookahead;
3403 3
        $funcName = strtolower($token['value']);
3404
3405
        // Check for custom functions afterwards
3406 3
        $config = $this->em->getConfiguration();
3407
3408
        switch (true) {
3409 3
            case ($config->getCustomStringFunction($funcName) !== null):
3410 2
                return $this->CustomFunctionsReturningStrings();
3411
3412 2
            case ($config->getCustomNumericFunction($funcName) !== null):
3413 2
                return $this->CustomFunctionsReturningNumerics();
3414
3415
            case ($config->getCustomDatetimeFunction($funcName) !== null):
3416
                return $this->CustomFunctionsReturningDatetime();
3417
3418
            default:
3419
                $this->syntaxError('known function', $token);
3420
        }
3421
    }
3422
3423
    /**
3424
     * FunctionsReturningNumerics ::=
3425
     *      "LENGTH" "(" StringPrimary ")" |
3426
     *      "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" |
3427
     *      "ABS" "(" SimpleArithmeticExpression ")" |
3428
     *      "SQRT" "(" SimpleArithmeticExpression ")" |
3429
     *      "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3430
     *      "SIZE" "(" CollectionValuedPathExpression ")" |
3431
     *      "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3432
     *      "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3433
     *      "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
3434
     *
3435
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3436
     */
3437 23
    public function FunctionsReturningNumerics()
3438
    {
3439 23
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3440 23
        $funcClass     = self::$_NUMERIC_FUNCTIONS[$funcNameLower];
3441
3442 23
        $function = new $funcClass($funcNameLower);
3443 23
        $function->parse($this);
3444
3445 23
        return $function;
3446
    }
3447
3448
    /**
3449
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3450
     */
3451 2
    public function CustomFunctionsReturningNumerics()
3452
    {
3453
        // getCustomNumericFunction is case-insensitive
3454 2
        $functionName  = strtolower($this->lexer->lookahead['value']);
3455 2
        $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName);
3456
3457 2
        $function = is_string($functionClass)
3458 1
            ? new $functionClass($functionName)
3459 2
            : call_user_func($functionClass, $functionName);
3460
3461 2
        $function->parse($this);
3462
3463 2
        return $function;
3464
    }
3465
3466
    /**
3467
     * FunctionsReturningDateTime ::=
3468
     *     "CURRENT_DATE" |
3469
     *     "CURRENT_TIME" |
3470
     *     "CURRENT_TIMESTAMP" |
3471
     *     "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" |
3472
     *     "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"
3473
     *
3474
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3475
     */
3476 7
    public function FunctionsReturningDatetime()
3477
    {
3478 7
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3479 7
        $funcClass     = self::$_DATETIME_FUNCTIONS[$funcNameLower];
3480
3481 7
        $function = new $funcClass($funcNameLower);
3482 7
        $function->parse($this);
3483
3484 7
        return $function;
3485
    }
3486
3487
    /**
3488
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3489
     */
3490
    public function CustomFunctionsReturningDatetime()
3491
    {
3492
        // getCustomDatetimeFunction is case-insensitive
3493
        $functionName  = $this->lexer->lookahead['value'];
3494
        $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName);
3495
3496
        $function = is_string($functionClass)
3497
            ? new $functionClass($functionName)
3498
            : call_user_func($functionClass, $functionName);
3499
3500
        $function->parse($this);
3501
3502
        return $function;
3503
    }
3504
3505
    /**
3506
     * FunctionsReturningStrings ::=
3507
     *   "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" |
3508
     *   "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3509
     *   "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" |
3510
     *   "LOWER" "(" StringPrimary ")" |
3511
     *   "UPPER" "(" StringPrimary ")" |
3512
     *   "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"
3513
     *
3514
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3515
     */
3516 30
    public function FunctionsReturningStrings()
3517
    {
3518 30
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3519 30
        $funcClass     = self::$_STRING_FUNCTIONS[$funcNameLower];
3520
3521 30
        $function = new $funcClass($funcNameLower);
3522 30
        $function->parse($this);
3523
3524 30
        return $function;
3525
    }
3526
3527
    /**
3528
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3529
     */
3530 2
    public function CustomFunctionsReturningStrings()
3531
    {
3532
        // getCustomStringFunction is case-insensitive
3533 2
        $functionName  = $this->lexer->lookahead['value'];
3534 2
        $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName);
3535
3536 2
        $function = is_string($functionClass)
3537 1
            ? new $functionClass($functionName)
3538 2
            : call_user_func($functionClass, $functionName);
3539
3540 2
        $function->parse($this);
3541
3542 2
        return $function;
3543
    }
3544
}
3545