Completed
Pull Request — master (#5586)
by Mihai
11:08
created

Parser::FromClause()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2
Metric Value
dl 0
loc 15
ccs 8
cts 8
cp 1
rs 9.4286
cc 2
eloc 8
nc 2
nop 0
crap 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Query;
21
22
use Doctrine\ORM\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 806
    public function __construct(Query $query)
196
    {
197 806
        $this->query        = $query;
198 806
        $this->em           = $query->getEntityManager();
199 806
        $this->lexer        = new Lexer($query->getDql());
200 806
        $this->parserResult = new ParserResult();
201 806
    }
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 806
    public function getAST()
266
    {
267
        // Parse & build AST
268 806
        $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 765
        $this->processDeferredIdentificationVariables();
273
274 763
        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 10
            $this->processDeferredPartialObjectExpressions();
276
        }
277
278 761
        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 572
            $this->processDeferredPathExpressions($AST);
280
        }
281
282 758
        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 26
            $this->processDeferredResultVariables();
284
        }
285
286 758
        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 754
        $this->processRootEntityAliasSelected();
291
292
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
293 753
        $this->fixIdentificationVariableOrder($AST);
294
295 753
        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 817
    public function match($token)
311
    {
312 817
        $lookaheadType = $this->lexer->lookahead['type'];
313
314
        // Short-circuit on first condition, usually types match
315 817
        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 810
        $this->lexer->moveNext();
333 810
    }
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 806
    public function parse()
363
    {
364 806
        $AST = $this->getAST();
365
366 753
        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 753
        if (($customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER)) !== false) {
371 77
            $this->customOutputWalker = $customOutputWalker;
372
        }
373
374
        // Run any custom tree walkers over the AST
375 753
        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 747
        $outputWalkerClass = $this->customOutputWalker ?: __NAMESPACE__ . '\SqlWalker';
400 747
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
401
402
        // Assign an SQL executor to the parser result
403 747
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
404
405 738
        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 753
    private function fixIdentificationVariableOrder($AST)
420
    {
421 753
        if (count($this->identVariableExpressions) <= 1) {
422 580
            return;
423
        }
424
425 178
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
426 178
            if ( ! isset($this->identVariableExpressions[$dqlAlias])) {
427 8
                continue;
428
            }
429
430 178
            $expr = $this->identVariableExpressions[$dqlAlias];
431 178
            $key  = array_search($expr, $AST->selectClause->selectExpressions);
432
433 178
            unset($AST->selectClause->selectExpressions[$key]);
434
435 178
            $AST->selectClause->selectExpressions[] = $expr;
436
        }
437 178
    }
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 153
    private function peekBeyondClosingParenthesis($resetPeek = true)
507
    {
508 153
        $token = $this->lexer->peek();
509 153
        $numUnmatched = 1;
510
511 153
        while ($numUnmatched > 0 && $token !== null) {
512 152
            switch ($token['type']) {
513
                case Lexer::T_OPEN_PARENTHESIS:
514 33
                    ++$numUnmatched;
515 33
                    break;
516
517 152
                case Lexer::T_CLOSE_PARENTHESIS:
518 152
                    --$numUnmatched;
519 152
                    break;
520
521
                default:
522
                    // Do nothing
523
            }
524
525 152
            $token = $this->lexer->peek();
526
        }
527
528 153
        if ($resetPeek) {
529 131
            $this->lexer->resetPeek();
530
        }
531
532 153
        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 338
    private function isMathOperator($token)
543
    {
544 338
        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 381
    private function isFunction()
553
    {
554 381
        $lookaheadType = $this->lexer->lookahead['type'];
555 381
        $peek          = $this->lexer->peek();
556
557 381
        $this->lexer->resetPeek();
558
559 381
        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 121
    private function isAggregateFunction($tokenType)
570
    {
571 121
        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 284
    private function isNextAllAnySome()
580
    {
581 284
        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 765
    private function processDeferredIdentificationVariables()
591
    {
592 765
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
593 753
            $identVariable = $deferredItem['expression'];
594
595
            // Check if IdentificationVariable exists in queryComponents
596 753
            if ( ! isset($this->queryComponents[$identVariable])) {
597 1
                $this->semanticalError(
598 1
                    "'$identVariable' is not defined.", $deferredItem['token']
599
                );
600
            }
601
602 753
            $qComp = $this->queryComponents[$identVariable];
603
604
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
605 753
            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 753
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
613 1
                $this->semanticalError(
614 753
                    "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token']
615
                );
616
            }
617
        }
618 763
    }
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 10
    private function processDeferredPartialObjectExpressions()
676
    {
677 10
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
678 10
            $expr = $deferredItem['expression'];
679 10
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
680
681 10
            foreach ($expr->partialFieldSet as $field) {
682 10
                if (isset($class->fieldMappings[$field])) {
683 9
                    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 9
            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 9
                    $deferredItem['token']
701
                );
702
            }
703
        }
704 8
    }
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 26
    private function processDeferredResultVariables()
713
    {
714 26
        foreach ($this->deferredResultVariables as $deferredItem) {
715 26
            $resultVariable = $deferredItem['expression'];
716
717
            // Check if ResultVariable exists in queryComponents
718 26
            if ( ! isset($this->queryComponents[$resultVariable])) {
719
                $this->semanticalError(
720
                    "'$resultVariable' is not defined.", $deferredItem['token']
721
                );
722
            }
723
724 26
            $qComp = $this->queryComponents[$resultVariable];
725
726
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
727 26
            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 26
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
735
                $this->semanticalError(
736 26
                    "'$resultVariable' is used outside the scope of its declaration.", $deferredItem['token']
737
                );
738
            }
739
        }
740 26
    }
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 572
    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 572
        foreach ($this->deferredPathExpressions as $deferredItem) {
758 572
            $pathExpression = $deferredItem['expression'];
759
760 572
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
761 572
            $class = $qComp['metadata'];
762
763 572
            if (($field = $pathExpression->field) === null) {
764 38
                $field = $pathExpression->field = $class->identifier[0];
765
            }
766
767
            // Check if field or association exists
768 572
            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 571
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
776
777 571
            if (isset($class->associationMappings[$field])) {
778 87
                $assoc = $class->associationMappings[$field];
779
780 87
                $fieldType = ($assoc['type'] & ClassMetadata::TO_ONE)
781 66
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
782 87
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
783
            }
784
785
            // Validate if PathExpression is one of the expected types
786 571
            $expectedType = $pathExpression->expectedType;
787
788 571
            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 569
            $pathExpression->type = $fieldType;
818
        }
819 569
    }
820
821
    /**
822
     * @return void
823
     */
824 754
    private function processRootEntityAliasSelected()
825
    {
826 754
        if ( ! count($this->identVariableExpressions)) {
827 218
            return;
828
        }
829
830 545
        $foundRootEntity = false;
831
832 545
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
833 545
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
834 545
                $foundRootEntity = true;
835
            }
836
        }
837
838 545
        if ( ! $foundRootEntity) {
839 1
            $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
840
        }
841 544
    }
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 806
    public function QueryLanguage()
851
    {
852 806
        $this->lexer->moveNext();
853
854 806
        switch ($this->lexer->lookahead['type']) {
855 1
            case Lexer::T_SELECT:
856 740
                $statement = $this->SelectStatement();
857 703
                break;
858
859 1
            case Lexer::T_UPDATE:
860 31
                $statement = $this->UpdateStatement();
861 31
                break;
862
863 2
            case Lexer::T_DELETE:
864 41
                $statement = $this->DeleteStatement();
865 40
                break;
866
867
            default:
868 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
869
                break;
870
        }
871
872
        // Check for end of string
873 768
        if ($this->lexer->lookahead !== null) {
874 3
            $this->syntaxError('end of string');
875
        }
876
877 765
        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 740
    public function SelectStatement()
886
    {
887 740
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
888
889 707
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
890 704
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
891 703
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
892 703
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
893
894 703
        return $selectStatement;
895
    }
896
897
    /**
898
     * UpdateStatement ::= UpdateClause [WhereClause]
899
     *
900
     * @return \Doctrine\ORM\Query\AST\UpdateStatement
901
     */
902 31
    public function UpdateStatement()
903
    {
904 31
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
905
906 31
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
907
908 31
        return $updateStatement;
909
    }
910
911
    /**
912
     * DeleteStatement ::= DeleteClause [WhereClause]
913
     *
914
     * @return \Doctrine\ORM\Query\AST\DeleteStatement
915
     */
916 41
    public function DeleteStatement()
917
    {
918 41
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
919
920 40
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
921
922 40
        return $deleteStatement;
923
    }
924
925
    /**
926
     * IdentificationVariable ::= identifier
927
     *
928
     * @return string
929
     */
930 783
    public function IdentificationVariable()
931
    {
932 783
        $this->match(Lexer::T_IDENTIFIER);
933
934 783
        $identVariable = $this->lexer->token['value'];
935
936 783
        $this->deferredIdentificationVariables[] = array(
937 783
            'expression'   => $identVariable,
938 783
            'nestingLevel' => $this->nestingLevel,
939 783
            'token'        => $this->lexer->token,
940
        );
941
942 783
        return $identVariable;
943
    }
944
945
    /**
946
     * AliasIdentificationVariable = identifier
947
     *
948
     * @return string
949
     */
950 777
    public function AliasIdentificationVariable()
951
    {
952 777
        $this->match(Lexer::T_IDENTIFIER);
953
954 777
        $aliasIdentVariable = $this->lexer->token['value'];
955 777
        $exists = isset($this->queryComponents[$aliasIdentVariable]);
956
957 777
        if ($exists) {
958 2
            $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->lexer->token);
959
        }
960
961 777
        return $aliasIdentVariable;
962
    }
963
964
    /**
965
     * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier
966
     *
967
     * @return string
968
     */
969 797
    public function AbstractSchemaName()
970
    {
971 797
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
972 780
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
973
974 780
            $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 797
        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 792
    private function validateAbstractSchemaName($schemaName)
998
    {
999 792
        if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {
1000 16
            $this->semanticalError("Class '$schemaName' is not defined.", $this->lexer->token);
1001
        }
1002 777
    }
1003
1004
    /**
1005
     * AliasResultVariable ::= identifier
1006
     *
1007
     * @return string
1008
     */
1009 115
    public function AliasResultVariable()
1010
    {
1011 115
        $this->match(Lexer::T_IDENTIFIER);
1012
1013 111
        $resultVariable = $this->lexer->token['value'];
1014 111
        $exists = isset($this->queryComponents[$resultVariable]);
1015
1016 111
        if ($exists) {
1017 2
            $this->semanticalError("'$resultVariable' is already defined.", $this->lexer->token);
1018
        }
1019
1020 111
        return $resultVariable;
1021
    }
1022
1023
    /**
1024
     * ResultVariable ::= identifier
1025
     *
1026
     * @return string
1027
     */
1028 26
    public function ResultVariable()
1029
    {
1030 26
        $this->match(Lexer::T_IDENTIFIER);
1031
1032 26
        $resultVariable = $this->lexer->token['value'];
1033
1034
        // Defer ResultVariable validation
1035 26
        $this->deferredResultVariables[] = array(
1036 26
            'expression'   => $resultVariable,
1037 26
            'nestingLevel' => $this->nestingLevel,
1038 26
            'token'        => $this->lexer->token,
1039
        );
1040
1041 26
        return $resultVariable;
1042
    }
1043
1044
    /**
1045
     * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
1046
     *
1047
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1048
     */
1049 251
    public function JoinAssociationPathExpression()
1050
    {
1051 251
        $identVariable = $this->IdentificationVariable();
1052
1053 251
        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 251
        $this->match(Lexer::T_DOT);
1060 251
        $this->match(Lexer::T_IDENTIFIER);
1061
1062 251
        $field = $this->lexer->token['value'];
1063
1064
        // Validate association field
1065 251
        $qComp = $this->queryComponents[$identVariable];
1066 251
        $class = $qComp['metadata'];
1067
1068 251
        if ( ! $class->hasAssociation($field)) {
1069
            $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
1070
        }
1071
1072 251
        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 582
    public function PathExpression($expectedTypes)
1086
    {
1087 582
        $identVariable = $this->IdentificationVariable();
1088 582
        $field = null;
1089
1090 582
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1091 576
            $this->match(Lexer::T_DOT);
1092 576
            $this->match(Lexer::T_IDENTIFIER);
1093
1094 576
            $field = $this->lexer->token['value'];
1095
1096 576
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1097 1
                $this->match(Lexer::T_DOT);
1098 1
                $this->match(Lexer::T_IDENTIFIER);
1099 1
                $field .= '.'.$this->lexer->token['value'];
1100
            }
1101
        }
1102
1103
        // Creating AST node
1104 582
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1105
1106
        // Defer PathExpression validation if requested to be deferred
1107 582
        $this->deferredPathExpressions[] = array(
1108 582
            'expression'   => $pathExpr,
1109 582
            'nestingLevel' => $this->nestingLevel,
1110 582
            'token'        => $this->lexer->token,
1111
        );
1112
1113 582
        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 492
    public function SingleValuedPathExpression()
1135
    {
1136 492
        return $this->PathExpression(
1137 492
            AST\PathExpression::TYPE_STATE_FIELD |
1138 492
            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 21
    public function CollectionValuedPathExpression()
1168
    {
1169 21
        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 740
    public function SelectClause()
1178
    {
1179 740
        $isDistinct = false;
1180 740
        $this->match(Lexer::T_SELECT);
1181
1182
        // Check for DISTINCT
1183 740
        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 740
        $selectExpressions = array();
1191 740
        $selectExpressions[] = $this->SelectExpression();
1192
1193 732
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1194 280
            $this->match(Lexer::T_COMMA);
1195
1196 280
            $selectExpressions[] = $this->SelectExpression();
1197
        }
1198
1199 731
        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 31
    public function UpdateClause()
1227
    {
1228 31
        $this->match(Lexer::T_UPDATE);
1229
1230 31
        $token = $this->lexer->lookahead;
1231 31
        $abstractSchemaName = $this->AbstractSchemaName();
1232
1233 31
        $this->validateAbstractSchemaName($abstractSchemaName);
1234
1235 31
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1236 2
            $this->match(Lexer::T_AS);
1237
        }
1238
1239 31
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1240
1241 31
        $class = $this->em->getClassMetadata($abstractSchemaName);
1242
1243
        // Building queryComponent
1244
        $queryComponent = array(
1245 31
            'metadata'     => $class,
1246
            'parent'       => null,
1247
            'relation'     => null,
1248
            'map'          => null,
1249 31
            'nestingLevel' => $this->nestingLevel,
1250 31
            'token'        => $token,
1251
        );
1252
1253 31
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1254
1255 31
        $this->match(Lexer::T_SET);
1256
1257 31
        $updateItems = array();
1258 31
        $updateItems[] = $this->UpdateItem();
1259
1260 31
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1261 4
            $this->match(Lexer::T_COMMA);
1262
1263 4
            $updateItems[] = $this->UpdateItem();
1264
        }
1265
1266 31
        $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1267 31
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1268
1269 31
        return $updateClause;
1270
    }
1271
1272
    /**
1273
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1274
     *
1275
     * @return \Doctrine\ORM\Query\AST\DeleteClause
1276
     */
1277 41
    public function DeleteClause()
1278
    {
1279 41
        $this->match(Lexer::T_DELETE);
1280
1281 41
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1282 8
            $this->match(Lexer::T_FROM);
1283
        }
1284
1285 41
        $token = $this->lexer->lookahead;
1286 41
        $abstractSchemaName = $this->AbstractSchemaName();
1287
1288 41
        $this->validateAbstractSchemaName($abstractSchemaName);
1289
1290 41
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1291
1292 41
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1293 1
            $this->match(Lexer::T_AS);
1294
        }
1295
1296 41
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1297
1298 40
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1299 40
        $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1300
1301
        // Building queryComponent
1302
        $queryComponent = array(
1303 40
            'metadata'     => $class,
1304
            'parent'       => null,
1305
            'relation'     => null,
1306
            'map'          => null,
1307 40
            'nestingLevel' => $this->nestingLevel,
1308 40
            'token'        => $token,
1309
        );
1310
1311 40
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1312
1313 40
        return $deleteClause;
1314
    }
1315
1316
    /**
1317
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1318
     *
1319
     * @return \Doctrine\ORM\Query\AST\FromClause
1320
     */
1321 731
    public function FromClause()
1322
    {
1323 731
        $this->match(Lexer::T_FROM);
1324
1325 726
        $identificationVariableDeclarations = array();
1326 726
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1327
1328 707
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1329 6
            $this->match(Lexer::T_COMMA);
1330
1331 6
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1332
        }
1333
1334 707
        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 322
    public function WhereClause()
1364
    {
1365 322
        $this->match(Lexer::T_WHERE);
1366
1367 322
        return new AST\WhereClause($this->ConditionalExpression());
1368
    }
1369
1370
    /**
1371
     * HavingClause ::= "HAVING" ConditionalExpression
1372
     *
1373
     * @return \Doctrine\ORM\Query\AST\HavingClause
1374
     */
1375 17
    public function HavingClause()
1376
    {
1377 17
        $this->match(Lexer::T_HAVING);
1378
1379 17
        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 178
    public function OrderByClause()
1409
    {
1410 178
        $this->match(Lexer::T_ORDER);
1411 178
        $this->match(Lexer::T_BY);
1412
1413 178
        $orderByItems = array();
1414 178
        $orderByItems[] = $this->OrderByItem();
1415
1416 178
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1417 14
            $this->match(Lexer::T_COMMA);
1418
1419 14
            $orderByItems[] = $this->OrderByItem();
1420
        }
1421
1422 178
        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 31
    public function UpdateItem()
1454
    {
1455 31
        $pathExpr = $this->SingleValuedPathExpression();
1456
1457 31
        $this->match(Lexer::T_EQUALS);
1458
1459 31
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1460
1461 31
        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 178
    public function OrderByItem()
1499
    {
1500 178
        $this->lexer->peek(); // lookahead => '.'
1501 178
        $this->lexer->peek(); // lookahead => token after '.'
1502
1503 178
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1504
1505 178
        $this->lexer->resetPeek();
1506
1507 178
        $glimpse = $this->lexer->glimpse();
1508
1509
        switch (true) {
1510 178
            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 177
            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 153
            case ($glimpse['type'] === Lexer::T_DOT):
1519 140
                $expr = $this->SingleValuedPathExpression();
1520 140
                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 178
        $type = 'ASC';
1532 178
        $item = new AST\OrderByItem($expr);
1533
1534
        switch (true) {
1535 178
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1536 92
                $this->match(Lexer::T_DESC);
1537 92
                $type = 'DESC';
1538 92
                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 178
        $item->type = $type;
1549
1550 178
        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 31
    public function NewValue()
1567
    {
1568 31
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1569 1
            $this->match(Lexer::T_NULL);
1570
1571 1
            return null;
1572
        }
1573
1574 30
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1575 17
            $this->match(Lexer::T_INPUT_PARAMETER);
1576
1577 17
            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 728
    public function IdentificationVariableDeclaration()
1589
    {
1590 728
        $joins                    = array();
1591 728
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1592 711
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1593 7
            ? $this->IndexBy()
1594 711
            : null;
1595
1596 711
        $rangeVariableDeclaration->isRoot = true;
1597
1598
        while (
1599 711
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1600 711
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1601 711
            $this->lexer->isNextToken(Lexer::T_JOIN)
1602
        ) {
1603 272
            $joins[] = $this->Join();
1604
        }
1605
1606 709
        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 272
    public function Join()
1678
    {
1679
        // Check Join type
1680 272
        $joinType = AST\Join::JOIN_TYPE_INNER;
1681
1682
        switch (true) {
1683 272
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1684 64
                $this->match(Lexer::T_LEFT);
1685
1686 64
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1687
1688
                // Possible LEFT OUTER join
1689 64
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1690
                    $this->match(Lexer::T_OUTER);
1691
1692
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1693
                }
1694 64
                break;
1695
1696 212
            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 272
        $this->match(Lexer::T_JOIN);
1705
1706 272
        $next            = $this->lexer->glimpse();
1707 272
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1708 270
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1709 270
        $join            = new AST\Join($joinType, $joinDeclaration);
1710
1711
        // Describe non-root join declaration
1712 270
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1713 21
            $joinDeclaration->isRoot = false;
1714
        }
1715
1716
        // Check for ad-hoc Join conditions
1717 270
        if ($adhocConditions) {
1718 23
            $this->match(Lexer::T_WITH);
1719
1720 23
            $join->conditionalExpression = $this->ConditionalExpression();
1721
        }
1722
1723 270
        return $join;
1724
    }
1725
1726
    /**
1727
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1728
     *
1729
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1730
     */
1731 728
    public function RangeVariableDeclaration()
1732
    {
1733 728
        $abstractSchemaName = $this->AbstractSchemaName();
1734
1735 728
        $this->validateAbstractSchemaName($abstractSchemaName);
1736
1737 713
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1738 5
            $this->match(Lexer::T_AS);
1739
        }
1740
1741 713
        $token = $this->lexer->lookahead;
1742 713
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1743 713
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1744
1745
        // Building queryComponent
1746
        $queryComponent = array(
1747 711
            'metadata'     => $classMetadata,
1748
            'parent'       => null,
1749
            'relation'     => null,
1750
            'map'          => null,
1751 711
            'nestingLevel' => $this->nestingLevel,
1752 711
            'token'        => $token
1753
        );
1754
1755 711
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1756
1757 711
        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 251
    public function JoinAssociationDeclaration()
1766
    {
1767 251
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1768
1769 251
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1770 4
            $this->match(Lexer::T_AS);
1771
        }
1772
1773 251
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1774 249
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1775
1776 249
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1777 249
        $field                  = $joinAssociationPathExpression->associationField;
1778
1779 249
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1780 249
        $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1781
1782
        // Building queryComponent
1783
        $joinQueryComponent = array(
1784 249
            'metadata'     => $targetClass,
1785 249
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1786 249
            'relation'     => $class->getAssociationMapping($field),
1787
            'map'          => null,
1788 249
            'nestingLevel' => $this->nestingLevel,
1789 249
            'token'        => $this->lexer->lookahead
1790
        );
1791
1792 249
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1793
1794 249
        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 10
    public function PartialObjectExpression()
1804
    {
1805 10
        $this->match(Lexer::T_PARTIAL);
1806
1807 10
        $partialFieldSet = array();
1808
1809 10
        $identificationVariable = $this->IdentificationVariable();
1810
1811 10
        $this->match(Lexer::T_DOT);
1812 10
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1813 10
        $this->match(Lexer::T_IDENTIFIER);
1814
1815 10
        $field = $this->lexer->token['value'];
1816
1817
        // First field in partial expression might be embeddable property
1818 10
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1819
            $this->match(Lexer::T_DOT);
1820
            $this->match(Lexer::T_IDENTIFIER);
1821
            $field .= '.'.$this->lexer->token['value'];
1822
        }
1823
1824 10
        $partialFieldSet[] = $field;
1825
1826 10
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1827 8
            $this->match(Lexer::T_COMMA);
1828 8
            $this->match(Lexer::T_IDENTIFIER);
1829
    
1830 8
            $field = $this->lexer->token['value'];
1831
    
1832 8
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1833 1
                $this->match(Lexer::T_DOT);
1834 1
                $this->match(Lexer::T_IDENTIFIER);
1835 1
                $field .= '.'.$this->lexer->token['value'];
1836
            }
1837
    
1838 8
            $partialFieldSet[] = $field;
1839
        }
1840
1841 10
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1842
1843 10
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1844
1845
        // Defer PartialObjectExpression validation
1846 10
        $this->deferredPartialObjectExpressions[] = array(
1847 10
            'expression'   => $partialObjectExpression,
1848 10
            'nestingLevel' => $this->nestingLevel,
1849 10
            'token'        => $this->lexer->token,
1850
        );
1851
1852 10
        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
            case Lexer::T_COALESCE:
2034 2
                return $this->CoalesceExpression();
2035
2036
            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 740
    public function SelectExpression()
2183
    {
2184 740
        $expression    = null;
2185 740
        $identVariable = null;
2186 740
        $peek          = $this->lexer->glimpse();
2187 740
        $lookaheadType = $this->lexer->lookahead['type'];
2188
2189
        switch (true) {
2190
            // ScalarExpression (u.name)
2191 740
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2192 102
                $expression = $this->ScalarExpression();
2193 102
                break;
2194
2195
            // IdentificationVariable (u)
2196 681
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2197 570
                $expression = $identVariable = $this->IdentificationVariable();
2198 570
                break;
2199
2200
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
2201 171
            case ($lookaheadType === Lexer::T_CASE):
2202 166
            case ($lookaheadType === Lexer::T_COALESCE):
2203 164
            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 162
            case ($this->isFunction()):
2209 83
                $this->lexer->peek(); // "("
2210
2211
                switch (true) {
2212 83
                    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 81
                    case ($this->isAggregateFunction($lookaheadType)):
2218
                        // COUNT(u.id)
2219 50
                        $expression = $this->AggregateExpression();
2220 50
                        break;
2221
2222
                    default:
2223
                        // IDENTITY(u)
2224 31
                        $expression = $this->FunctionDeclaration();
2225 31
                        break;
2226
                }
2227
2228 83
                break;
2229
2230
            // PartialObjectExpression (PARTIAL u.{id, name})
2231 79
            case ($lookaheadType === Lexer::T_PARTIAL):
2232 10
                $expression    = $this->PartialObjectExpression();
2233 10
                $identVariable = $expression->identificationVariable;
2234 10
                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 737
        $mustHaveAliasResultVariable = false;
2268
2269 737
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2270 106
            $this->match(Lexer::T_AS);
2271
2272 106
            $mustHaveAliasResultVariable = true;
2273
        }
2274
2275 737
        $hiddenAliasResultVariable = false;
2276
2277 737
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2278 10
            $this->match(Lexer::T_HIDDEN);
2279
2280 10
            $hiddenAliasResultVariable = true;
2281
        }
2282
2283 737
        $aliasResultVariable = null;
2284
2285 737
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2286 115
            $token = $this->lexer->lookahead;
2287 115
            $aliasResultVariable = $this->AliasResultVariable();
2288
2289
            // Include AliasResultVariable in query components.
2290 110
            $this->queryComponents[$aliasResultVariable] = array(
2291 110
                'resultVariable' => $expression,
2292 110
                'nestingLevel'   => $this->nestingLevel,
2293 110
                'token'          => $token,
2294
            );
2295
        }
2296
2297
        // AST
2298
2299 732
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2300
2301 732
        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 578
            $this->identVariableExpressions[$identVariable] = $expr;
2303
        }
2304
2305 732
        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
            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 27
            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 360
    public function ConditionalExpression()
2400
    {
2401 360
        $conditionalTerms = array();
2402 360
        $conditionalTerms[] = $this->ConditionalTerm();
2403
2404 357
        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 357
        if (count($conditionalTerms) == 1) {
2413 349
            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 360
    public function ConditionalTerm()
2425
    {
2426 360
        $conditionalFactors = array();
2427 360
        $conditionalFactors[] = $this->ConditionalFactor();
2428
2429 357
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2430 31
            $this->match(Lexer::T_AND);
2431
2432 31
            $conditionalFactors[] = $this->ConditionalFactor();
2433
        }
2434
2435
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2436
        // if only one AST\ConditionalFactor is defined
2437 357
        if (count($conditionalFactors) == 1) {
2438 339
            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 31
        return new AST\ConditionalTerm($conditionalFactors);
2442
    }
2443
2444
    /**
2445
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2446
     *
2447
     * @return \Doctrine\ORM\Query\AST\ConditionalFactor
2448
     */
2449 360
    public function ConditionalFactor()
2450
    {
2451 360
        $not = false;
2452
2453 360
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2454 6
            $this->match(Lexer::T_NOT);
2455
2456 6
            $not = true;
2457
        }
2458
2459 360
        $conditionalPrimary = $this->ConditionalPrimary();
2460
2461
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2462
        // if only one AST\ConditionalPrimary is defined
2463 357
        if ( ! $not) {
2464 355
            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 360
    public function ConditionalPrimary()
2479
    {
2480 360
        $condPrimary = new AST\ConditionalPrimary;
2481
2482 360
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2483 351
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2484
2485 348
            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 360
    public function SimpleConditionalExpression()
2514
    {
2515 360
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2516 7
            return $this->ExistsExpression();
2517
        }
2518
2519 360
        $token      = $this->lexer->lookahead;
2520 360
        $peek       = $this->lexer->glimpse();
2521 360
        $lookahead  = $token;
2522
2523 360
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2524
            $token = $this->lexer->glimpse();
2525
        }
2526
2527 360
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2528
            // Peek beyond the matching closing parenthesis.
2529 340
            $beyond = $this->lexer->peek();
2530
2531 340
            switch ($peek['value']) {
2532 312
                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 312
                    $token = $beyond;
2548
2549 312
                    while ($token['value'] === '.') {
2550 274
                        $this->lexer->peek();
2551
2552 274
                        $token = $this->lexer->peek();
2553
                    }
2554
2555
                    // Also peek beyond a NOT if there is one.
2556 312
                    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 312
                    $lookahead = $this->lexer->peek();
2562
            }
2563
2564
            // Also peek beyond a NOT if there is one.
2565 340
            if ($lookahead['type'] === Lexer::T_NOT) {
2566 6
                $lookahead = $this->lexer->peek();
2567
            }
2568
2569 340
            $this->lexer->resetPeek();
2570
        }
2571
2572 360
        if ($token['type'] === Lexer::T_BETWEEN) {
2573 8
            return $this->BetweenExpression();
2574
        }
2575
2576 354
        if ($token['type'] === Lexer::T_LIKE) {
2577 14
            return $this->LikeExpression();
2578
        }
2579
2580 341
        if ($token['type'] === Lexer::T_IN) {
2581 34
            return $this->InExpression();
2582
        }
2583
2584 316
        if ($token['type'] === Lexer::T_INSTANCE) {
2585 12
            return $this->InstanceOfExpression();
2586
        }
2587
2588 304
        if ($token['type'] === Lexer::T_MEMBER) {
2589 7
            return $this->CollectionMemberExpression();
2590
        }
2591
2592 297
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2593 13
            return $this->NullComparisonExpression();
2594
        }
2595
2596 287
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2597 3
            return $this->EmptyCollectionComparisonExpression();
2598
        }
2599
2600 284
        return $this->ComparisonExpression();
2601
    }
2602
2603
    /**
2604
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2605
     *
2606
     * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression
2607
     */
2608 3
    public function EmptyCollectionComparisonExpression()
2609
    {
2610 3
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2611 3
            $this->CollectionValuedPathExpression()
2612
        );
2613 3
        $this->match(Lexer::T_IS);
2614
2615 3
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2616 2
            $this->match(Lexer::T_NOT);
2617 2
            $emptyCollectionCompExpr->not = true;
2618
        }
2619
2620 3
        $this->match(Lexer::T_EMPTY);
2621
2622 3
        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 172
    public function Literal()
2664
    {
2665 172
        switch ($this->lexer->lookahead['type']) {
2666
            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
            case Lexer::T_INTEGER:
2671
            case Lexer::T_FLOAT:
2672 124
                $this->match(
2673 124
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2674
                );
2675
2676 124
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2677
            case Lexer::T_TRUE:
2678
            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 157
    public function InputParameter()
2709
    {
2710 157
        $this->match(Lexer::T_INPUT_PARAMETER);
2711
2712 157
        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 317
    public function ArithmeticExpression()
2721
    {
2722 317
        $expr = new AST\ArithmeticExpression;
2723
2724 317
        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 317
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2737
2738 317
        return $expr;
2739
    }
2740
2741
    /**
2742
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2743
     *
2744
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2745
     */
2746 420
    public function SimpleArithmeticExpression()
2747
    {
2748 420
        $terms = array();
2749 420
        $terms[] = $this->ArithmeticTerm();
2750
2751 420
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2752 19
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2753
2754 19
            $terms[] = $this->lexer->token['value'];
2755 19
            $terms[] = $this->ArithmeticTerm();
2756
        }
2757
2758
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2759
        // if only one AST\ArithmeticTerm is defined
2760 420
        if (count($terms) == 1) {
2761 415
            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 19
        return new AST\SimpleArithmeticExpression($terms);
2765
    }
2766
2767
    /**
2768
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2769
     *
2770
     * @return \Doctrine\ORM\Query\AST\ArithmeticTerm
2771
     */
2772 420
    public function ArithmeticTerm()
2773
    {
2774 420
        $factors = array();
2775 420
        $factors[] = $this->ArithmeticFactor();
2776
2777 420
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2778 51
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2779
2780 51
            $factors[] = $this->lexer->token['value'];
2781 51
            $factors[] = $this->ArithmeticFactor();
2782
        }
2783
2784
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2785
        // if only one AST\ArithmeticFactor is defined
2786 420
        if (count($factors) == 1) {
2787 392
            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 51
        return new AST\ArithmeticTerm($factors);
2791
    }
2792
2793
    /**
2794
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2795
     *
2796
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2797
     */
2798 420
    public function ArithmeticFactor()
2799
    {
2800 420
        $sign = null;
2801
2802 420
        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 420
        $primary = $this->ArithmeticPrimary();
2808
2809
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2810
        // if only one AST\ArithmeticPrimary is defined
2811 420
        if ($sign === null) {
2812 419
            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 424
    public function ArithmeticPrimary()
2825
    {
2826 424
        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 424
        switch ($this->lexer->lookahead['type']) {
2837
            case Lexer::T_COALESCE:
2838
            case Lexer::T_NULLIF:
2839
            case Lexer::T_CASE:
2840 4
                return $this->CaseExpression();
2841
2842
            case Lexer::T_IDENTIFIER:
2843 394
                $peek = $this->lexer->glimpse();
2844
2845 394
                if ($peek['value'] == '(') {
2846 29
                    return $this->FunctionDeclaration();
2847
                }
2848
2849 373
                if ($peek['value'] == '.') {
2850 362
                    return $this->SingleValuedPathExpression();
2851
                }
2852
2853 42
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2854 6
                    return $this->ResultVariable();
2855
                }
2856
2857 38
                return $this->StateFieldPathExpression();
2858
2859 166
            case Lexer::T_INPUT_PARAMETER:
2860 139
                return $this->InputParameter();
2861
2862
            default:
2863 166
                $peek = $this->lexer->glimpse();
2864
2865 166
                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 162
                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
            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
            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 80
    public function AggregateExpression()
2991
    {
2992 80
        $lookaheadType = $this->lexer->lookahead['type'];
2993 80
        $isDistinct = false;
2994
2995 80
        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 80
        $this->match($lookaheadType);
3000 80
        $functionName = $this->lexer->token['value'];
3001 80
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3002
3003 80
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
3004 2
            $this->match(Lexer::T_DISTINCT);
3005 2
            $isDistinct = true;
3006
        }
3007
3008 80
        $pathExp = $this->SimpleArithmeticExpression();
3009
3010 80
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3011
3012 80
        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 284
    public function ComparisonExpression()
3072
    {
3073 284
        $this->lexer->glimpse();
3074
3075 284
        $leftExpr  = $this->ArithmeticExpression();
3076 284
        $operator  = $this->ComparisonOperator();
3077 284
        $rightExpr = ($this->isNextAllAnySome())
3078 3
            ? $this->QuantifiedExpression()
3079 284
            : $this->ArithmeticExpression();
3080
3081 282
        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 284
    public function ComparisonOperator()
3326
    {
3327 284
        switch ($this->lexer->lookahead['value']) {
3328
            case '=':
3329 237
                $this->match(Lexer::T_EQUALS);
3330
3331 237
                return '=';
3332
3333
            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
            case '>':
3348 43
                $this->match(Lexer::T_GREATER_THAN);
3349 43
                $operator = '>';
3350
3351 43
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3352 6
                    $this->match(Lexer::T_EQUALS);
3353 6
                    $operator .= '=';
3354
                }
3355
3356 43
                return $operator;
3357
3358
            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