Failed Conditions
Push — develop ( 1e1a22...0ebeb4 )
by Michael
128:26 queued 63:28
created

Parser::Subselect()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 4
cts 4
cp 1
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 16
nop 0
crap 5
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 175 and the first side effect is on line 26.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Query;
6
7
use Doctrine\Common\Persistence\Mapping\MappingException;
8
use Doctrine\ORM\Mapping\AssociationMetadata;
9
use Doctrine\ORM\Mapping\ClassMetadata;
10
use Doctrine\ORM\Mapping\FieldMetadata;
11
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
12
use Doctrine\ORM\Query;
13
use Doctrine\ORM\Query\AST\Functions;
14
15
/**
16
 * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
17
 * Parses a DQL query, reports any errors in it, and generates an AST.
18
 *
19
 * @since   2.0
20
 * @author  Guilherme Blanco <[email protected]>
21
 * @author  Jonathan Wage <[email protected]>
22
 * @author  Roman Borschel <[email protected]>
23
 * @author  Janne Vanhala <[email protected]>
24
 * @author  Fabio B. Silva <[email protected]>
25
 */
26
class Parser
0 ignored issues
show
Bug introduced by
Possible parse error: class missing opening or closing brace
Loading history...
27
{
28
    /**
29
     * READ-ONLY: Maps BUILT-IN string function names to AST class names.
30
     *
31
     * @var array
32
     */
33
    private static $_STRING_FUNCTIONS = [
34
        'concat'    => Functions\ConcatFunction::class,
35
        'substring' => Functions\SubstringFunction::class,
36
        'trim'      => Functions\TrimFunction::class,
37
        'lower'     => Functions\LowerFunction::class,
38
        'upper'     => Functions\UpperFunction::class,
39
        'identity'  => Functions\IdentityFunction::class,
40
    ];
41
42
    /**
43
     * READ-ONLY: Maps BUILT-IN numeric function names to AST class names.
44
     *
45
     * @var array
46
     */
47
    private static $_NUMERIC_FUNCTIONS = [
48
        'length'    => Functions\LengthFunction::class,
49
        'locate'    => Functions\LocateFunction::class,
50
        'abs'       => Functions\AbsFunction::class,
51
        'sqrt'      => Functions\SqrtFunction::class,
52
        'mod'       => Functions\ModFunction::class,
53
        'size'      => Functions\SizeFunction::class,
54
        'date_diff' => Functions\DateDiffFunction::class,
55
        'bit_and'   => Functions\BitAndFunction::class,
56
        'bit_or'    => Functions\BitOrFunction::class,
57
58
        // Aggregate functions
59
        'min'       => Functions\MinFunction::class,
60
        'max'       => Functions\MaxFunction::class,
61
        'avg'       => Functions\AvgFunction::class,
62
        'sum'       => Functions\SumFunction::class,
63
        'count'     => Functions\CountFunction::class,
64
    ];
65
66
    /**
67
     * READ-ONLY: Maps BUILT-IN datetime function names to AST class names.
68
     *
69
     * @var array
70
     */
71
    private static $_DATETIME_FUNCTIONS = [
72
        'current_date'      => Functions\CurrentDateFunction::class,
73
        'current_time'      => Functions\CurrentTimeFunction::class,
74
        'current_timestamp' => Functions\CurrentTimestampFunction::class,
75
        'date_add'          => Functions\DateAddFunction::class,
76
        'date_sub'          => Functions\DateSubFunction::class,
77
    ];
78
79
    /*
80
     * Expressions that were encountered during parsing of identifiers and expressions
81
     * and still need to be validated.
82
     */
83
84
    /**
85
     * @var array
86
     */
87
    private $deferredIdentificationVariables = [];
88
89
    /**
90
     * @var array
91
     */
92
    private $deferredPartialObjectExpressions = [];
93
94
    /**
95
     * @var array
96
     */
97
    private $deferredPathExpressions = [];
98
99
    /**
100
     * @var array
101
     */
102
    private $deferredResultVariables = [];
103
104
    /**
105
     * @var array
106
     */
107
    private $deferredNewObjectExpressions = [];
108
109
    /**
110
     * The lexer.
111
     *
112
     * @var \Doctrine\ORM\Query\Lexer
113
     */
114
    private $lexer;
115
116
    /**
117
     * The parser result.
118
     *
119
     * @var \Doctrine\ORM\Query\ParserResult
120
     */
121
    private $parserResult;
122
123
    /**
124
     * The EntityManager.
125
     *
126
     * @var \Doctrine\ORM\EntityManagerInterface
127
     */
128
    private $em;
129
130
    /**
131
     * The Query to parse.
132
     *
133
     * @var Query
134
     */
135
    private $query;
136
137
    /**
138
     * Map of declared query components in the parsed query.
139
     *
140
     * @var array
141
     */
142
    private $queryComponents = [];
143
144
    /**
145
     * Keeps the nesting level of defined ResultVariables.
146
     *
147
     * @var integer
148
     */
149
    private $nestingLevel = 0;
150
151
    /**
152
     * Any additional custom tree walkers that modify the AST.
153
     *
154
     * @var array
155
     */
156
    private $customTreeWalkers = [];
157
158
    /**
159
     * The custom last tree walker, if any, that is responsible for producing the output.
160
     *
161
     * @var TreeWalker
162
     */
163
    private $customOutputWalker;
164
165
    /**
166
     * @var array
167
     */
168
    private $identVariableExpressions = [];
169
170
    /**
171
     * Creates a new query parser object.
172
     *
173
     * @param Query $query The Query to parse.
174
     */
175
    public function __construct(Query $query)
176
    {
177
        $this->query        = $query;
178
        $this->em           = $query->getEntityManager();
179
        $this->lexer        = new Lexer($query->getDQL());
180
        $this->parserResult = new ParserResult();
181 6
    }
182
183 6
    /**
184
     * Sets a custom tree walker that produces output.
185 6
     * This tree walker will be run last over the AST, after any other walkers.
186 6
     *
187 6
     * @param string $className
188
     *
189
     * @return void
190
     */
191
    public function setCustomOutputTreeWalker($className)
192
    {
193
        $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...
194
    }
195 816
196
    /**
197 816
     * Adds a custom tree walker for modifying the AST.
198 816
     *
199 816
     * @param string $className
200 816
     *
201 816
     * @return void
202
     */
203
    public function addCustomTreeWalker($className)
204
    {
205
        $this->customTreeWalkers[] = $className;
206
    }
207
208
    /**
209
     * Gets the lexer used by the parser.
210
     *
211 127
     * @return \Doctrine\ORM\Query\Lexer
212
     */
213 127
    public function getLexer()
214 127
    {
215
        return $this->lexer;
216
    }
217
218
    /**
219
     * Gets the ParserResult that is being filled with information during parsing.
220
     *
221
     * @return \Doctrine\ORM\Query\ParserResult
222
     */
223
    public function getParserResult()
224
    {
225
        return $this->parserResult;
226
    }
227
228
    /**
229
     * Gets the EntityManager used by the parser.
230
     *
231
     * @return \Doctrine\ORM\EntityManagerInterface
232
     */
233 26
    public function getEntityManager()
234
    {
235 26
        return $this->em;
236
    }
237
238
    /**
239
     * Parses and builds AST for the given Query.
240
     *
241
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
242
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
243
     *         \Doctrine\ORM\Query\AST\DeleteStatement
244
     */
245
    public function getAST()
246
    {
247
        // Parse & build AST
248
        $AST = $this->QueryLanguage();
249
250
        // Process any deferred validations of some nodes in the AST.
251
        // This also allows post-processing of the AST for modification purposes.
252
        $this->processDeferredIdentificationVariables();
253
254
        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...
255
            $this->processDeferredPartialObjectExpressions();
256
        }
257
258
        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...
259
            $this->processDeferredPathExpressions();
260
        }
261
262
        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...
263
            $this->processDeferredResultVariables();
264
        }
265 816
266
        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...
267
            $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...
268 816
        }
269
270
        $this->processRootEntityAliasSelected();
271
272 777
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
273
        $this->fixIdentificationVariableOrder($AST);
274 775
275 10
        return $AST;
276
    }
277
278 773
    /**
279 578
     * Attempts to match the given token with the current lookahead token.
280
     *
281
     * If they match, updates the lookahead token; otherwise raises a syntax
282 769
     * error.
283 30
     *
284
     * @param int $token The token type.
285
     *
286 769
     * @return void
287 28
     *
288
     * @throws QueryException If the tokens don't match.
289
     */
290 765
    public function match($token)
291
    {
292
        $lookaheadType = $this->lexer->lookahead['type'];
293 764
294
        // Short-circuit on first condition, usually types match
295 764
        if ($lookaheadType !== $token) {
296
            // If parameter is not identifier (1-99) must be exact match
297
            if ($token < Lexer::T_IDENTIFIER) {
298
                $this->syntaxError($this->lexer->getLiteral($token));
299
            }
300
301
            // If parameter is keyword (200+) must be exact match
302
            if ($token > Lexer::T_IDENTIFIER) {
303
                $this->syntaxError($this->lexer->getLiteral($token));
304
            }
305
306
            // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
307
            if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
308
                $this->syntaxError($this->lexer->getLiteral($token));
309
            }
310 827
        }
311
312 827
        $this->lexer->moveNext();
313
    }
314
315 827
    /**
316
     * Frees this parser, enabling it to be reused.
317 20
     *
318 2
     * @param boolean $deep     Whether to clean peek and reset errors.
319
     * @param integer $position Position to reset.
320
     *
321
     * @return void
322 18
     */
323 7
    public function free($deep = false, $position = 0)
324
    {
325
        // WARNING! Use this method with care. It resets the scanner!
326
        $this->lexer->resetPosition($position);
327 11
328 8
        // Deep = true cleans peek and also any previously defined errors
329
        if ($deep) {
330
            $this->lexer->resetPeek();
331
        }
332 820
333 820
        $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...
334
        $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...
335
    }
336
337
    /**
338
     * Parses a query string.
339
     *
340
     * @return ParserResult
341
     */
342
    public function parse()
343
    {
344
        $AST = $this->getAST();
345
346
        if (($customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) !== false) {
347
            $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...
348
        }
349
350
        if (($customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER)) !== false) {
351
            $this->customOutputWalker = $customOutputWalker;
352
        }
353
354
        // Run any custom tree walkers over the AST
355
        if ($this->customTreeWalkers) {
356
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
357
358
            foreach ($this->customTreeWalkers as $walker) {
359
                $treeWalkerChain->addTreeWalker($walker);
360
            }
361
362 816
            switch (true) {
363
                case ($AST instanceof AST\UpdateStatement):
364 816
                    $treeWalkerChain->walkUpdateStatement($AST);
365
                    break;
366 764
367 93
                case ($AST instanceof AST\DeleteStatement):
368
                    $treeWalkerChain->walkDeleteStatement($AST);
369
                    break;
370 764
371 78
                case ($AST instanceof AST\SelectStatement):
372
                default:
373
                    $treeWalkerChain->walkSelectStatement($AST);
374
            }
375 764
376 92
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
377
        }
378 92
379 92
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
380
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
381
382
        // Assign an SQL executor to the parser result
383 92
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
384
385
        return $this->parserResult;
386
    }
387
388
    /**
389
     * Fixes order of identification variables.
390
     *
391
     * They have to appear in the select clause in the same order as the
392
     * declarations (from ... x join ... y join ... z ...) appear in the query
393 92
     * as the hydration process relies on that order for proper operation.
394
     *
395
     * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
396 86
     *
397
     * @return void
398
     */
399 758
    private function fixIdentificationVariableOrder($AST)
400 758
    {
401
        if (count($this->identVariableExpressions) <= 1) {
402
            return;
403 758
        }
404
405 749
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
406
            if ( ! isset($this->identVariableExpressions[$dqlAlias])) {
407
                continue;
408
            }
409
410
            $expr = $this->identVariableExpressions[$dqlAlias];
411
            $key  = array_search($expr, $AST->selectClause->selectExpressions);
412
413
            unset($AST->selectClause->selectExpressions[$key]);
414
415
            $AST->selectClause->selectExpressions[] = $expr;
416
        }
417
    }
418
419 764
    /**
420
     * Generates a new syntax error.
421 764
     *
422 589
     * @param string     $expected Expected string.
423
     * @param array|null $token    Got token.
424
     *
425 180
     * @return void
426 180
     *
427 8
     * @throws \Doctrine\ORM\Query\QueryException
428
     */
429
    public function syntaxError($expected = '', $token = null)
430 180
    {
431 180
        if ($token === null) {
432
            $token = $this->lexer->lookahead;
433 180
        }
434
435 180
        $tokenPos = $token['position'] ?? '-1';
436
437 180
        $message  = "line 0, col {$tokenPos}: Error: ";
438
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
439
        $message .= ($this->lexer->lookahead === null) ? 'end of string.' : "'{$token['value']}'";
440
441
        throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL()));
442
    }
443
444
    /**
445
     * Generates a new semantical error.
446
     *
447
     * @param string     $message Optional message.
448
     * @param array|null $token   Optional token.
449 17
     *
450
     * @return void
451 17
     *
452 14
     * @throws \Doctrine\ORM\Query\QueryException
453
     */
454
    public function semanticalError($message = '', $token = null, ?\Throwable $previousFailure = null)
455 17
    {
456
        if ($token === null) {
457 17
            $token = $this->lexer->lookahead;
458 17
        }
459 17
460
        // Minimum exposed chars ahead of token
461 17
        $distance = 12;
462
463
        // Find a position of a final word to display in error string
464
        $dql    = $this->query->getDQL();
465
        $length = strlen($dql);
466
        $pos    = $token['position'] + $distance;
467
        $pos    = strpos($dql, ' ', ($length > $pos) ? $pos : $length);
468
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
469
470
        $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1';
471
        $tokenStr = substr($dql, (int) $token['position'], $length);
472
473
        // Building informative message
474 34
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
475
476 34
        throw QueryException::semanticalError(
477 2
            $message,
478
            QueryException::dqlError($this->query->getDQL(), $previousFailure)
479
        );
480
    }
481 34
482
    /**
483
     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
484 34
     *
485 34
     * @param boolean $resetPeek Reset peek after finding the closing parenthesis.
486 34
     *
487 34
     * @return array
488 34
     */
489
    private function peekBeyondClosingParenthesis($resetPeek = true)
490 34
    {
491 34
        $token = $this->lexer->peek();
492
        $numUnmatched = 1;
493
494 34
        while ($numUnmatched > 0 && $token !== null) {
495
            switch ($token['type']) {
496 34
                case Lexer::T_OPEN_PARENTHESIS:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
497
                    ++$numUnmatched;
498
                    break;
499
500
                case Lexer::T_CLOSE_PARENTHESIS:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
501
                    --$numUnmatched;
502
                    break;
503
504
                default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
505
                    // Do nothing
506 152
            }
507
508 152
            $token = $this->lexer->peek();
509 152
        }
510
511 152
        if ($resetPeek) {
512 151
            $this->lexer->resetPeek();
513 151
        }
514 27
515 27
        return $token;
516
    }
517 151
518 151
    /**
519 151
     * Checks if the given token indicates a mathematical operator.
520
     *
521
     * @param array $token
522
     *
523
     * @return boolean TRUE if the token is a mathematical operator, FALSE otherwise.
524
     */
525 151
    private function isMathOperator($token)
526
    {
527
        return in_array($token['type'], [Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY]);
528 152
    }
529 128
530
    /**
531
     * Checks if the next-next (after lookahead) token starts a function.
532 152
     *
533
     * @return boolean TRUE if the next-next tokens start a function, FALSE otherwise.
534
     */
535
    private function isFunction()
536
    {
537
        $lookaheadType = $this->lexer->lookahead['type'];
538
        $peek          = $this->lexer->peek();
539
540
        $this->lexer->resetPeek();
541
542 339
        return ($lookaheadType >= Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_OPEN_PARENTHESIS);
543
    }
544 339
545
    /**
546
     * Checks whether the given token type indicates an aggregate function.
547
     *
548
     * @param int $tokenType
549
     *
550
     * @return boolean TRUE if the token type is an aggregate function, FALSE otherwise.
551
     */
552 384
    private function isAggregateFunction($tokenType)
553
    {
554 384
        return in_array($tokenType, [Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT]);
555 384
    }
556
557 384
    /**
558
     * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
559 384
     *
560
     * @return boolean
561
     */
562
    private function isNextAllAnySome()
563
    {
564
        return in_array($this->lexer->lookahead['type'], [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME]);
565
    }
566
567
    /**
568
     * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
569 121
     * It must exist in query components list.
570
     *
571 121
     * @return void
572
     */
573
    private function processDeferredIdentificationVariables()
574
    {
575
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
576
            $identVariable = $deferredItem['expression'];
577
578
            // Check if IdentificationVariable exists in queryComponents
579 288
            if ( ! isset($this->queryComponents[$identVariable])) {
580
                $this->semanticalError(
581 288
                    "'$identVariable' is not defined.", $deferredItem['token']
582
                );
583
            }
584
585
            $qComp = $this->queryComponents[$identVariable];
586
587
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
588
            if ( ! isset($qComp['metadata'])) {
589
                $this->semanticalError(
590 777
                    "'$identVariable' does not point to a Class.", $deferredItem['token']
591
                );
592 777
            }
593 764
594
            // Validate if identification variable nesting level is lower or equal than the current one
595
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
596 764
                $this->semanticalError(
597 1
                    "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token']
598 1
                );
599
            }
600
        }
601
    }
602 764
603
    /**
604
     * Validates that the given <tt>NewObjectExpression</tt>.
605 764
     *
606
     * @param \Doctrine\ORM\Query\AST\SelectClause $AST
607
     *
608
     * @return void
609
     */
610
    private function processDeferredNewObjectExpressions($AST)
611
    {
612 764
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
613 1
            $expression     = $deferredItem['expression'];
614 764
            $token          = $deferredItem['token'];
615
            $className      = $expression->className;
616
            $args           = $expression->args;
617
            $fromClassName  = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null;
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...
618 775
619
            // If the namespace is not given then assumes the first FROM entity namespace
620
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
621
                $namespace  = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
622
                $fqcn       = $namespace . '\\' . $className;
623
624
                if (class_exists($fqcn)) {
625
                    $expression->className  = $fqcn;
626
                    $className              = $fqcn;
627 28
                }
628
            }
629 28
630 28
            if ( ! class_exists($className)) {
631 28
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
632 28
            }
633 28
634 28
            $class = new \ReflectionClass($className);
635 28
636 28
            if ( ! $class->isInstantiable()) {
637
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
638
            }
639 28
640 11
            if ($class->getConstructor() === null) {
641 11
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
642
            }
643 11
644 11
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
645 11
                $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
646
            }
647
        }
648
    }
649 28
650 1
    /**
651
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
652
     * It must exist in query components list.
653 27
     *
654
     * @return void
655 27
     */
656 1
    private function processDeferredPartialObjectExpressions()
657
    {
658
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
659 26
            $expr = $deferredItem['expression'];
660 1
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
661
662
            foreach ($expr->partialFieldSet as $field) {
663 25
                $property = $class->getProperty($field);
664 25
665
                if ($property instanceof FieldMetadata ||
666
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
667 24
                    continue;
668
                }
669
670
                $this->semanticalError(
671
                    "There is no mapped field named '$field' on class " . $class->getClassName() . ".", $deferredItem['token']
672
                );
673
            }
674
675 10
            if (array_intersect($class->identifier, $expr->partialFieldSet) != $class->identifier) {
676
                $this->semanticalError(
677 10
                    "The partial field selection of class " . $class->getClassName() . " must contain the identifier.",
678 10
                    $deferredItem['token']
679 10
                );
680
            }
681 10
        }
682 10
    }
683 8
684
    /**
685
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
686 3
     * It must exist in query components list.
687 3
     *
688 3
     * @return void
689 2
     */
690
    private function processDeferredResultVariables()
691
    {
692 1
        foreach ($this->deferredResultVariables as $deferredItem) {
693 1
            $resultVariable = $deferredItem['expression'];
694
695
            // Check if ResultVariable exists in queryComponents
696
            if ( ! isset($this->queryComponents[$resultVariable])) {
697 9
                $this->semanticalError(
698 1
                    "'$resultVariable' is not defined.", $deferredItem['token']
699 1
                );
700 9
            }
701
702
            $qComp = $this->queryComponents[$resultVariable];
703
704 8
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
705
            if ( ! isset($qComp['resultVariable'])) {
706
                $this->semanticalError(
707
                    "'$resultVariable' does not point to a ResultVariable.", $deferredItem['token']
708
                );
709
            }
710
711
            // Validate if identification variable nesting level is lower or equal than the current one
712 30
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
713
                $this->semanticalError(
714 30
                    "'$resultVariable' is used outside the scope of its declaration.", $deferredItem['token']
715 30
                );
716
            }
717
        }
718 30
    }
719
720
    /**
721
     * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
722
     *
723
     * AssociationPathExpression             ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
724 30
     * SingleValuedPathExpression            ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
725
     * StateFieldPathExpression              ::= IdentificationVariable "." StateField
726
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
727 30
     * CollectionValuedPathExpression        ::= IdentificationVariable "." CollectionValuedAssociationField
728
     *
729
     * @return void
730
     */
731
    private function processDeferredPathExpressions()
732
    {
733
        foreach ($this->deferredPathExpressions as $deferredItem) {
734 30
            $pathExpression = $deferredItem['expression'];
735
736 30
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
737
            $class = $qComp['metadata'];
738
739
            if (($field = $pathExpression->field) === null) {
740 30
                $field = $pathExpression->field = $class->identifier[0];
741
            }
742
743
            $property = $class->getProperty($field);
744
745
            // Check if field or association exists
746
            if (! $property) {
747
                $this->semanticalError(
748
                    'Class ' . $class->getClassName() . ' has no field or association named ' . $field,
749
                    $deferredItem['token']
750
                );
751
            }
752
753
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
754
755 578
            if ($property instanceof AssociationMetadata) {
756
                $fieldType = $property instanceof ToOneAssociationMetadata
757 578
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
758 578
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
759
                ;
760 578
            }
761 578
762
            // Validate if PathExpression is one of the expected types
763 578
            $expectedType = $pathExpression->expectedType;
764 38
765
            if ( ! ($expectedType & $fieldType)) {
766
                // We need to recognize which was expected type(s)
767
                $expectedStringTypes = [];
768 578
769 2
                // Validate state field type
770 2
                if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
771 2
                    $expectedStringTypes[] = 'StateFieldPathExpression';
772
                }
773
774
                // Validate single valued association (*-to-one)
775 576
                if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
776
                    $expectedStringTypes[] = 'SingleValuedAssociationField';
777 576
                }
778 88
779
                // Validate single valued association (*-to-many)
780 88
                if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
781 65
                    $expectedStringTypes[] = 'CollectionValuedAssociationField';
782 88
                }
783
784
                // Build the error message
785
                $semanticalError  = 'Invalid PathExpression. ';
786 576
                $semanticalError .= \count($expectedStringTypes) === 1
787
                    ? 'Must be a ' . $expectedStringTypes[0] . '.'
788 576
                    : implode(' or ', $expectedStringTypes) . ' expected.';
789
790 2
                $this->semanticalError($semanticalError, $deferredItem['token']);
791
            }
792
793 2
            // We need to force the type in PathExpression
794 1
            $pathExpression->type = $fieldType;
795
        }
796
    }
797
798 2
    /**
799 2
     * @return void
800
     */
801
    private function processRootEntityAliasSelected()
802
    {
803 2
        if ( ! $this->identVariableExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->identVariableExpressions 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...
804
            return;
805
        }
806
807
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
808 2
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
809 2
                return;
810 1
            }
811 2
        }
812
813 2
        $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
814
    }
815
816
    /**
817 574
     * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
818
     *
819 574
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
820
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
821
     *         \Doctrine\ORM\Query\AST\DeleteStatement
822
     */
823
    public function QueryLanguage()
824 765
    {
825
        $this->lexer->moveNext();
826 765
827 220
        switch ($this->lexer->lookahead['type']) {
828
            case Lexer::T_SELECT:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
829
                $statement = $this->SelectStatement();
830 550
                break;
831 550
832 550
            case Lexer::T_UPDATE:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
833
                $statement = $this->UpdateStatement();
834
                break;
835
836 1
            case Lexer::T_DELETE:
837
                $statement = $this->DeleteStatement();
838
                break;
839
840
            default:
841
                $this->syntaxError('SELECT, UPDATE or DELETE');
842
                break;
843
        }
844
845
        // Check for end of string
846 816
        if ($this->lexer->lookahead !== null) {
847
            $this->syntaxError('end of string');
848 816
        }
849
850 816
        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...
851 816
    }
852 750
853 715
    /**
854
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
855 71
     *
856 31
     * @return \Doctrine\ORM\Query\AST\SelectStatement
857 31
     */
858
    public function SelectStatement()
859 42
    {
860 41
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
861 40
862
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
863
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
864 2
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
865
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
866
867
        return $selectStatement;
868
    }
869 780
870 3
    /**
871
     * UpdateStatement ::= UpdateClause [WhereClause]
872
     *
873 777
     * @return \Doctrine\ORM\Query\AST\UpdateStatement
874
     */
875
    public function UpdateStatement()
876
    {
877
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
878
879
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
880
881 750
        return $updateStatement;
882
    }
883 750
884
    /**
885 719
     * DeleteStatement ::= DeleteClause [WhereClause]
886 716
     *
887 715
     * @return \Doctrine\ORM\Query\AST\DeleteStatement
888 715
     */
889
    public function DeleteStatement()
890 715
    {
891
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
892
893
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
894
895
        return $deleteStatement;
896
    }
897
898 31
    /**
899
     * IdentificationVariable ::= identifier
900 31
     *
901
     * @return string
902 31
     */
903
    public function IdentificationVariable()
904 31
    {
905
        $this->match(Lexer::T_IDENTIFIER);
906
907
        $identVariable = $this->lexer->token['value'];
908
909
        $this->deferredIdentificationVariables[] = [
910
            'expression'   => $identVariable,
911
            'nestingLevel' => $this->nestingLevel,
912 41
            'token'        => $this->lexer->token,
913
        ];
914 41
915
        return $identVariable;
916 40
    }
917
918 40
    /**
919
     * AliasIdentificationVariable = identifier
920
     *
921
     * @return string
922
     */
923
    public function AliasIdentificationVariable()
924
    {
925
        $this->match(Lexer::T_IDENTIFIER);
926 792
927
        $aliasIdentVariable = $this->lexer->token['value'];
928 792
        $exists = isset($this->queryComponents[$aliasIdentVariable]);
929
930 792
        if ($exists) {
931
            $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->lexer->token);
932 792
        }
933 792
934 792
        return $aliasIdentVariable;
935 792
    }
936
937
    /**
938 792
     * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier
939
     *
940
     * @return string
941
     */
942
    public function AbstractSchemaName()
943
    {
944
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
945
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
946 787
947
            $schemaName = $this->lexer->token['value'];
948 787
        } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
949
            $this->match(Lexer::T_IDENTIFIER);
950 787
951 787
            $schemaName = $this->lexer->token['value'];
952
        } else {
953 787
            $this->match(Lexer::T_ALIASED_NAME);
954 2
955
            list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']);
956
957 787
            $schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
958
        }
959
960
        return $schemaName;
961
    }
962
963
    /**
964
     * Validates an AbstractSchemaName, making sure the class exists.
965 807
     *
966
     * @param string $schemaName The name to validate.
967 807
     *
968 790
     * @throws QueryException if the name does not exist.
969
     */
970 790
    private function validateAbstractSchemaName($schemaName) : void
971 28
    {
972 19
        if (class_exists($schemaName, true) || interface_exists($schemaName, true)) {
973
            return;
974 19
        }
975
976 10
        try {
977
            $this->getEntityManager()->getClassMetadata($schemaName);
978 10
979
            return;
980 10
        } catch (MappingException $mappingException) {
981
            $this->semanticalError(
982
                \sprintf('Class %s could not be mapped', $schemaName),
983 807
                $this->lexer->token
984
            );
985
        }
986
987
        $this->semanticalError("Class '$schemaName' is not defined.", $this->lexer->token);
988
    }
989
990
    /**
991
     * AliasResultVariable ::= identifier
992
     *
993 802
     * @return string
994
     */
995 802
    public function AliasResultVariable()
996 16
    {
997
        $this->match(Lexer::T_IDENTIFIER);
998 787
999
        $resultVariable = $this->lexer->token['value'];
1000
        $exists = isset($this->queryComponents[$resultVariable]);
1001
1002
        if ($exists) {
1003
            $this->semanticalError("'$resultVariable' is already defined.", $this->lexer->token);
1004
        }
1005 119
1006
        return $resultVariable;
1007 119
    }
1008
1009 115
    /**
1010 115
     * ResultVariable ::= identifier
1011
     *
1012 115
     * @return string
1013 2
     */
1014
    public function ResultVariable()
1015
    {
1016 115
        $this->match(Lexer::T_IDENTIFIER);
1017
1018
        $resultVariable = $this->lexer->token['value'];
1019
1020
        // Defer ResultVariable validation
1021
        $this->deferredResultVariables[] = [
1022
            'expression'   => $resultVariable,
1023
            'nestingLevel' => $this->nestingLevel,
1024 30
            'token'        => $this->lexer->token,
1025
        ];
1026 30
1027
        return $resultVariable;
1028 30
    }
1029
1030
    /**
1031 30
     * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
1032 30
     *
1033 30
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1034 30
     */
1035
    public function JoinAssociationPathExpression()
1036
    {
1037 30
        $identVariable = $this->IdentificationVariable();
1038
1039
        if ( ! isset($this->queryComponents[$identVariable])) {
1040
            $this->semanticalError(
1041
                'Identification Variable ' . $identVariable .' used in join path expression but was not defined before.'
1042
            );
1043
        }
1044
1045 252
        $this->match(Lexer::T_DOT);
1046
        $this->match(Lexer::T_IDENTIFIER);
1047 252
1048
        $field = $this->lexer->token['value'];
1049 252
1050
        // Validate association field
1051
        $qComp = $this->queryComponents[$identVariable];
1052
        $class = $qComp['metadata'];
1053
1054
        if (! (($property = $class->getProperty($field)) !== null && $property instanceof AssociationMetadata)) {
1055 252
            $this->semanticalError('Class ' . $class->getClassName() . ' has no association named ' . $field);
1056 252
        }
1057
1058 252
        return new AST\JoinAssociationPathExpression($identVariable, $field);
1059
    }
1060
1061 252
    /**
1062 252
     * Parses an arbitrary path expression and defers semantical validation
1063
     * based on expected types.
1064 252
     *
1065
     * PathExpression ::= IdentificationVariable {"." identifier}*
1066
     *
1067
     * @param integer $expectedTypes
1068 252
     *
1069
     * @return \Doctrine\ORM\Query\AST\PathExpression
1070
     */
1071
    public function PathExpression($expectedTypes)
1072
    {
1073
        $identVariable = $this->IdentificationVariable();
1074
        $field = null;
1075
1076
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1077
            $this->match(Lexer::T_DOT);
1078
            $this->match(Lexer::T_IDENTIFIER);
1079
1080
            $field = $this->lexer->token['value'];
1081 588
1082
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1083 588
                $this->match(Lexer::T_DOT);
1084 588
                $this->match(Lexer::T_IDENTIFIER);
1085
                $field .= '.'.$this->lexer->token['value'];
1086 588
            }
1087 582
        }
1088 582
1089
        // Creating AST node
1090 582
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1091
1092 582
        // Defer PathExpression validation if requested to be deferred
1093 2
        $this->deferredPathExpressions[] = [
1094 2
            'expression'   => $pathExpr,
1095 2
            'nestingLevel' => $this->nestingLevel,
1096
            'token'        => $this->lexer->token,
1097
        ];
1098
1099
        return $pathExpr;
1100 588
    }
1101
1102
    /**
1103 588
     * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
1104 588
     *
1105 588
     * @return \Doctrine\ORM\Query\AST\PathExpression
1106 588
     */
1107
    public function AssociationPathExpression()
1108
    {
1109 588
        return $this->PathExpression(
1110
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
1111
            AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
1112
        );
1113
    }
1114
1115
    /**
1116
     * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
1117
     *
1118
     * @return \Doctrine\ORM\Query\AST\PathExpression
1119
     */
1120
    public function SingleValuedPathExpression()
1121
    {
1122
        return $this->PathExpression(
1123
            AST\PathExpression::TYPE_STATE_FIELD |
1124
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1125
        );
1126
    }
1127
1128
    /**
1129
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1130 493
     *
1131
     * @return \Doctrine\ORM\Query\AST\PathExpression
1132 493
     */
1133 493
    public function StateFieldPathExpression()
1134 493
    {
1135
        return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
1136
    }
1137
1138
    /**
1139
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
1140
     *
1141
     * @return \Doctrine\ORM\Query\AST\PathExpression
1142
     */
1143 201
    public function SingleValuedAssociationPathExpression()
1144
    {
1145 201
        return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
1146
    }
1147
1148
    /**
1149
     * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
1150
     *
1151
     * @return \Doctrine\ORM\Query\AST\PathExpression
1152
     */
1153 8
    public function CollectionValuedPathExpression()
1154
    {
1155 8
        return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
1156
    }
1157
1158
    /**
1159
     * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
1160
     *
1161
     * @return \Doctrine\ORM\Query\AST\SelectClause
1162
     */
1163 23
    public function SelectClause()
1164
    {
1165 23
        $isDistinct = false;
1166
        $this->match(Lexer::T_SELECT);
1167
1168
        // Check for DISTINCT
1169
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1170
            $this->match(Lexer::T_DISTINCT);
1171
1172
            $isDistinct = true;
1173 750
        }
1174
1175 750
        // Process SelectExpressions (1..N)
1176 750
        $selectExpressions = [];
1177
        $selectExpressions[] = $this->SelectExpression();
1178
1179 750
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1180 6
            $this->match(Lexer::T_COMMA);
1181
1182 6
            $selectExpressions[] = $this->SelectExpression();
1183
        }
1184
1185
        return new AST\SelectClause($selectExpressions, $isDistinct);
1186 750
    }
1187 750
1188
    /**
1189 742
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1190 280
     *
1191
     * @return \Doctrine\ORM\Query\AST\SimpleSelectClause
1192 280
     */
1193
    public function SimpleSelectClause()
1194
    {
1195 741
        $isDistinct = false;
1196
        $this->match(Lexer::T_SELECT);
1197
1198
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1199
            $this->match(Lexer::T_DISTINCT);
1200
1201
            $isDistinct = true;
1202
        }
1203 47
1204
        return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1205 47
    }
1206 47
1207
    /**
1208 47
     * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1209
     *
1210
     * @return \Doctrine\ORM\Query\AST\UpdateClause
1211
     */
1212
    public function UpdateClause()
1213
    {
1214 47
        $this->match(Lexer::T_UPDATE);
1215
1216
        $token = $this->lexer->lookahead;
1217
        $abstractSchemaName = $this->AbstractSchemaName();
1218
1219
        $this->validateAbstractSchemaName($abstractSchemaName);
1220
1221
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1222 31
            $this->match(Lexer::T_AS);
1223
        }
1224 31
1225
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1226 31
1227 31
        $class = $this->em->getClassMetadata($abstractSchemaName);
1228
1229 31
        // Building queryComponent
1230
        $queryComponent = [
1231 31
            'metadata'     => $class,
1232 2
            'parent'       => null,
1233
            'relation'     => null,
1234
            'map'          => null,
1235 31
            'nestingLevel' => $this->nestingLevel,
1236
            'token'        => $token,
1237 31
        ];
1238
1239
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1240
1241 31
        $this->match(Lexer::T_SET);
1242
1243
        $updateItems = [];
1244
        $updateItems[] = $this->UpdateItem();
1245 31
1246 31
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1247
            $this->match(Lexer::T_COMMA);
1248
1249 31
            $updateItems[] = $this->UpdateItem();
1250
        }
1251 31
1252
        $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1253 31
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1254 31
1255
        return $updateClause;
1256 31
    }
1257 4
1258
    /**
1259 4
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1260
     *
1261
     * @return \Doctrine\ORM\Query\AST\DeleteClause
1262 31
     */
1263 31
    public function DeleteClause()
1264
    {
1265 31
        $this->match(Lexer::T_DELETE);
1266
1267
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1268
            $this->match(Lexer::T_FROM);
1269
        }
1270
1271
        $token = $this->lexer->lookahead;
1272
        $abstractSchemaName = $this->AbstractSchemaName();
1273 41
1274
        $this->validateAbstractSchemaName($abstractSchemaName);
1275 41
1276
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1277 41
1278 8
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1279
            $this->match(Lexer::T_AS);
1280
        }
1281 41
1282 41
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1283
1284 41
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1285
        $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1286 41
1287
        // Building queryComponent
1288 41
        $queryComponent = [
1289 1
            'metadata'     => $class,
1290
            'parent'       => null,
1291
            'relation'     => null,
1292 41
            'map'          => null,
1293
            'nestingLevel' => $this->nestingLevel,
1294 40
            'token'        => $token,
1295 40
        ];
1296
1297
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1298
1299 40
        return $deleteClause;
1300
    }
1301
1302
    /**
1303 40
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1304 40
     *
1305
     * @return \Doctrine\ORM\Query\AST\FromClause
1306
     */
1307 40
    public function FromClause()
1308
    {
1309 40
        $this->match(Lexer::T_FROM);
1310
1311
        $identificationVariableDeclarations = [];
1312
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1313
1314
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1315
            $this->match(Lexer::T_COMMA);
1316
1317 741
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1318
        }
1319 741
1320
        return new AST\FromClause($identificationVariableDeclarations);
1321 736
    }
1322 736
1323
    /**
1324 719
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1325 7
     *
1326
     * @return \Doctrine\ORM\Query\AST\SubselectFromClause
1327 7
     */
1328
    public function SubselectFromClause()
1329
    {
1330 719
        $this->match(Lexer::T_FROM);
1331
1332
        $identificationVariables = [];
1333
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1334
1335
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1336
            $this->match(Lexer::T_COMMA);
1337
1338 47
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1339
        }
1340 47
1341
        return new AST\SubselectFromClause($identificationVariables);
1342 47
    }
1343 47
1344
    /**
1345 46
     * WhereClause ::= "WHERE" ConditionalExpression
1346
     *
1347
     * @return \Doctrine\ORM\Query\AST\WhereClause
1348
     */
1349
    public function WhereClause()
1350
    {
1351 46
        $this->match(Lexer::T_WHERE);
1352
1353
        return new AST\WhereClause($this->ConditionalExpression());
1354
    }
1355
1356
    /**
1357
     * HavingClause ::= "HAVING" ConditionalExpression
1358
     *
1359 325
     * @return \Doctrine\ORM\Query\AST\HavingClause
1360
     */
1361 325
    public function HavingClause()
1362
    {
1363 325
        $this->match(Lexer::T_HAVING);
1364
1365
        return new AST\HavingClause($this->ConditionalExpression());
1366
    }
1367
1368
    /**
1369
     * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1370
     *
1371 21
     * @return \Doctrine\ORM\Query\AST\GroupByClause
1372
     */
1373 21
    public function GroupByClause()
1374
    {
1375 21
        $this->match(Lexer::T_GROUP);
1376
        $this->match(Lexer::T_BY);
1377
1378
        $groupByItems = [$this->GroupByItem()];
1379
1380
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1381
            $this->match(Lexer::T_COMMA);
1382
1383 31
            $groupByItems[] = $this->GroupByItem();
1384
        }
1385 31
1386 31
        return new AST\GroupByClause($groupByItems);
1387
    }
1388 31
1389
    /**
1390 30
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1391 8
     *
1392
     * @return \Doctrine\ORM\Query\AST\OrderByClause
1393 8
     */
1394
    public function OrderByClause()
1395
    {
1396 30
        $this->match(Lexer::T_ORDER);
1397
        $this->match(Lexer::T_BY);
1398
1399
        $orderByItems = [];
1400
        $orderByItems[] = $this->OrderByItem();
1401
1402
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1403
            $this->match(Lexer::T_COMMA);
1404 177
1405
            $orderByItems[] = $this->OrderByItem();
1406 177
        }
1407 177
1408
        return new AST\OrderByClause($orderByItems);
1409 177
    }
1410 177
1411
    /**
1412 177
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1413 14
     *
1414
     * @return \Doctrine\ORM\Query\AST\Subselect
1415 14
     */
1416
    public function Subselect()
1417
    {
1418 177
        // Increase query nesting level
1419
        $this->nestingLevel++;
1420
1421
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1422
1423
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1424
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1425
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1426 47
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1427
1428
        // Decrease query nesting level
1429 47
        $this->nestingLevel--;
1430
1431 47
        return $subselect;
1432
    }
1433 46
1434 46
    /**
1435 46
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1436 46
     *
1437
     * @return \Doctrine\ORM\Query\AST\UpdateItem
1438
     */
1439 46
    public function UpdateItem()
1440
    {
1441 46
        $pathExpr = $this->SingleValuedPathExpression();
1442
1443
        $this->match(Lexer::T_EQUALS);
1444
1445
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1446
1447
        return $updateItem;
1448
    }
1449 31
1450
    /**
1451 31
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1452
     *
1453 31
     * @return string | \Doctrine\ORM\Query\AST\PathExpression
1454
     */
1455 31
    public function GroupByItem()
1456
    {
1457 31
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1458
        $glimpse = $this->lexer->glimpse();
1459
1460
        if ($glimpse['type'] === Lexer::T_DOT) {
1461
            return $this->SingleValuedPathExpression();
1462
        }
1463
1464
        // Still need to decide between IdentificationVariable or ResultVariable
1465 31
        $lookaheadValue = $this->lexer->lookahead['value'];
1466
1467
        if ( ! isset($this->queryComponents[$lookaheadValue])) {
1468 31
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1469
        }
1470 31
1471 12
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1472
            ? $this->IdentificationVariable()
1473
            : $this->ResultVariable();
1474
    }
1475 19
1476
    /**
1477 19
     * OrderByItem ::= (
1478 1
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1479
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1480
     * ) ["ASC" | "DESC"]
1481 18
     *
1482 16
     * @return \Doctrine\ORM\Query\AST\OrderByItem
1483 18
     */
1484
    public function OrderByItem()
1485
    {
1486
        $this->lexer->peek(); // lookahead => '.'
1487
        $this->lexer->peek(); // lookahead => token after '.'
1488
1489
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1490
1491
        $this->lexer->resetPeek();
1492
1493
        $glimpse = $this->lexer->glimpse();
1494 177
1495
        switch (true) {
1496 177
            case ($this->isFunction()):
1497 177
                $expr = $this->FunctionDeclaration();
1498
                break;
1499 177
1500
            case ($this->isMathOperator($peek)):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1489 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...
1501 177
                $expr = $this->SimpleArithmeticExpression();
1502
                break;
1503 177
1504
            case ($glimpse['type'] === Lexer::T_DOT):
1505
                $expr = $this->SingleValuedPathExpression();
1506 177
                break;
1507 1
1508 1
            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...
1509
                $expr = $this->ScalarExpression();
1510 176
                break;
1511 25
1512 25
            default:
1513
                $expr = $this->ResultVariable();
1514 151
                break;
1515 139
        }
1516 139
1517
        $type = 'ASC';
1518 16
        $item = new AST\OrderByItem($expr);
1519 1
1520 1
        switch (true) {
1521
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1522
                $this->match(Lexer::T_DESC);
1523 15
                $type = 'DESC';
1524 15
                break;
1525
1526
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1527 177
                $this->match(Lexer::T_ASC);
1528 177
                break;
1529
1530
            default:
1531 177
                // Do nothing
1532 92
        }
1533 92
1534 92
        $item->type = $type;
1535
1536 151
        return $item;
1537 96
    }
1538 96
1539
    /**
1540
     * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1541
     *      EnumPrimary | SimpleEntityExpression | "NULL"
1542
     *
1543
     * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1544 177
     * grammar that needs to be supported:
1545
     *
1546 177
     * NewValue ::= SimpleArithmeticExpression | "NULL"
1547
     *
1548
     * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1549
     *
1550
     * @return AST\ArithmeticExpression
1551
     */
1552
    public function NewValue()
1553
    {
1554
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1555
            $this->match(Lexer::T_NULL);
1556
1557
            return null;
1558
        }
1559
1560
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1561
            $this->match(Lexer::T_INPUT_PARAMETER);
1562 31
1563
            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...
1564 31
        }
1565 1
1566
        return $this->ArithmeticExpression();
1567 1
    }
1568
1569
    /**
1570 30
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1571 17
     *
1572
     * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1573 17
     */
1574
    public function IdentificationVariableDeclaration()
1575
    {
1576 13
        $joins                    = [];
1577
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1578
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1579
            ? $this->IndexBy()
1580
            : null;
1581
1582
        $rangeVariableDeclaration->isRoot = true;
1583
1584 738
        while (
1585
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1586 738
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1587 738
            $this->lexer->isNextToken(Lexer::T_JOIN)
1588 723
        ) {
1589 7
            $joins[] = $this->Join();
1590 723
        }
1591
1592 723
        return new AST\IdentificationVariableDeclaration(
1593
            $rangeVariableDeclaration, $indexBy, $joins
1594
        );
1595 723
    }
1596 723
1597 723
    /**
1598
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1599 273
     *
1600
     * {Internal note: WARNING: Solution is harder than a bare implementation.
1601
     * Desired EBNF support:
1602 721
     *
1603
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1604
     *
1605
     * It demands that entire SQL generation to become programmatical. This is
1606
     * needed because association based subselect requires "WHERE" conditional
1607
     * expressions to be injected, but there is no scope to do that. Only scope
1608
     * accessible is "FROM", prohibiting an easy implementation without larger
1609
     * changes.}
1610
     *
1611
     * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration |
1612
     *         \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1613
     */
1614
    public function SubselectIdentificationVariableDeclaration()
1615
    {
1616
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1617
        NOT YET IMPLEMENTED!
1618
1619
        $glimpse = $this->lexer->glimpse();
1620
1621
        if ($glimpse['type'] == Lexer::T_DOT) {
1622
            $associationPathExpression = $this->AssociationPathExpression();
1623
1624 47
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1625
                $this->match(Lexer::T_AS);
1626
            }
1627
1628
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1629
            $identificationVariable      = $associationPathExpression->identificationVariable;
1630
            $field                       = $associationPathExpression->associationField;
1631
1632
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1633
            $association = $class->getProperty($field);
1634
            $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
1635
1636
            // Building queryComponent
1637
            $joinQueryComponent = array(
1638
                'metadata'     => $targetClass,
1639
                'parent'       => $identificationVariable,
1640
                'relation'     => $association,
1641
                'map'          => null,
1642
                'nestingLevel' => $this->nestingLevel,
1643
                'token'        => $this->lexer->lookahead
1644
            );
1645
1646
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1647
1648
            return new AST\SubselectIdentificationVariableDeclaration(
1649
                $associationPathExpression, $aliasIdentificationVariable
1650
            );
1651
        }
1652
        */
1653
1654
        return $this->IdentificationVariableDeclaration();
1655
    }
1656
1657
    /**
1658
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1659
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1660
     *          ["WITH" ConditionalExpression]
1661
     *
1662
     * @return \Doctrine\ORM\Query\AST\Join
1663 47
     */
1664
    public function Join()
1665
    {
1666
        // Check Join type
1667
        $joinType = AST\Join::JOIN_TYPE_INNER;
1668
1669
        switch (true) {
1670
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1671
                $this->match(Lexer::T_LEFT);
1672
1673 273
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1674
1675
                // Possible LEFT OUTER join
1676 273
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1677
                    $this->match(Lexer::T_OUTER);
1678
1679 273
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1680 62
                }
1681
                break;
1682 62
1683
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1684
                $this->match(Lexer::T_INNER);
1685 62
                break;
1686
1687
            default:
1688
                // Do nothing
1689
        }
1690 62
1691
        $this->match(Lexer::T_JOIN);
1692 212
1693 19
        $next            = $this->lexer->glimpse();
1694 19
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1695
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1696
        $join            = new AST\Join($joinType, $joinDeclaration);
1697
1698
        // Describe non-root join declaration
1699
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1700 273
            $joinDeclaration->isRoot = false;
1701
        }
1702 273
1703 273
        // Check for ad-hoc Join conditions
1704 271
        if ($adhocConditions) {
1705 271
            $this->match(Lexer::T_WITH);
1706
1707
            $join->conditionalExpression = $this->ConditionalExpression();
1708 271
        }
1709 21
1710
        return $join;
1711
    }
1712
1713 271
    /**
1714 21
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1715
     *
1716 21
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1717
     *
1718
     * @throws QueryException
1719 271
     */
1720
    public function RangeVariableDeclaration()
1721
    {
1722
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()['type'] === Lexer::T_SELECT) {
1723
            $this->semanticalError('Subquery is not supported here', $this->lexer->token);
1724
        }
1725
1726
        $abstractSchemaName = $this->AbstractSchemaName();
1727 738
1728
        $this->validateAbstractSchemaName($abstractSchemaName);
1729 738
1730
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1731 738
            $this->match(Lexer::T_AS);
1732
        }
1733 723
1734 5
        $token = $this->lexer->lookahead;
1735
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1736
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1737 723
1738 723
        // Building queryComponent
1739 723
        $queryComponent = [
1740
            'metadata'     => $classMetadata,
1741
            'parent'       => null,
1742
            'relation'     => null,
1743 723
            'map'          => null,
1744
            'nestingLevel' => $this->nestingLevel,
1745
            'token'        => $token
1746
        ];
1747 723
1748 723
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1749
1750
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1751 723
    }
1752
1753 723
    /**
1754
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1755
     *
1756
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1757
     */
1758
    public function JoinAssociationDeclaration()
1759
    {
1760
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1761 252
1762
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1763 252
            $this->match(Lexer::T_AS);
1764
        }
1765 252
1766 4
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1767
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1768
1769 252
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1770 250
        $field                  = $joinAssociationPathExpression->associationField;
1771
1772 250
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1773 250
        $association = $class->getProperty($field);
1774
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
1775 250
1776 250
        // Building queryComponent
1777
        $joinQueryComponent = [
1778
            'metadata'     => $targetClass,
1779
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1780 250
            'relation'     => $association,
1781 250
            'map'          => null,
1782 250
            'nestingLevel' => $this->nestingLevel,
1783
            'token'        => $this->lexer->lookahead
1784 250
        ];
1785 250
1786
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1787
1788 250
        return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
1789
    }
1790 250
1791
    /**
1792
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1793
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1794
     *
1795
     * @return \Doctrine\ORM\Query\AST\PartialObjectExpression
1796
     */
1797
    public function PartialObjectExpression()
1798
    {
1799 10
        $this->match(Lexer::T_PARTIAL);
1800
1801 10
        $partialFieldSet = [];
1802
1803 10
        $identificationVariable = $this->IdentificationVariable();
1804
1805 10
        $this->match(Lexer::T_DOT);
1806
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1807 10
        $this->match(Lexer::T_IDENTIFIER);
1808 10
1809 10
        $field = $this->lexer->token['value'];
1810
1811 10
        // First field in partial expression might be embeddable property
1812
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1813
            $this->match(Lexer::T_DOT);
1814 10
            $this->match(Lexer::T_IDENTIFIER);
1815
            $field .= '.'.$this->lexer->token['value'];
1816
        }
1817
1818
        $partialFieldSet[] = $field;
1819
1820 10
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1821
            $this->match(Lexer::T_COMMA);
1822 10
            $this->match(Lexer::T_IDENTIFIER);
1823 8
1824 8
            $field = $this->lexer->token['value'];
1825
1826 8
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1827
                $this->match(Lexer::T_DOT);
1828 8
                $this->match(Lexer::T_IDENTIFIER);
1829 1
                $field .= '.'.$this->lexer->token['value'];
1830 1
            }
1831 1
1832
            $partialFieldSet[] = $field;
1833
        }
1834 8
1835
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1836
1837 10
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1838
1839 10
        // Defer PartialObjectExpression validation
1840
        $this->deferredPartialObjectExpressions[] = [
1841
            'expression'   => $partialObjectExpression,
1842 10
            'nestingLevel' => $this->nestingLevel,
1843 10
            'token'        => $this->lexer->token,
1844 10
        ];
1845 10
1846
        return $partialObjectExpression;
1847
    }
1848 10
1849
    /**
1850
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1851
     *
1852
     * @return \Doctrine\ORM\Query\AST\NewObjectExpression
1853
     */
1854
    public function NewObjectExpression()
1855
    {
1856 28
        $this->match(Lexer::T_NEW);
1857
1858 28
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1859
        $token = $this->lexer->token;
1860 28
1861 28
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1862
1863 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...
1864
1865 28
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1866
            $this->match(Lexer::T_COMMA);
1867 28
1868 24
            $args[] = $this->NewObjectArg();
1869
        }
1870 24
1871
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1872
1873 28
        $expression = new AST\NewObjectExpression($className, $args);
1874
1875 28
        // Defer NewObjectExpression validation
1876
        $this->deferredNewObjectExpressions[] = [
1877
            'token'        => $token,
1878 28
            'expression'   => $expression,
1879 28
            'nestingLevel' => $this->nestingLevel,
1880 28
        ];
1881 28
1882
        return $expression;
1883
    }
1884 28
1885
    /**
1886
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1887
     *
1888
     * @return mixed
1889
     */
1890
    public function NewObjectArg()
1891
    {
1892 28
        $token = $this->lexer->lookahead;
1893
        $peek  = $this->lexer->glimpse();
1894 28
1895 28
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1896
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1897 28
            $expression = $this->Subselect();
1898 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1899 1
1900 1
            return $expression;
1901
        }
1902 1
1903
        return $this->ScalarExpression();
1904
    }
1905 28
1906
    /**
1907
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1908
     *
1909
     * @return \Doctrine\ORM\Query\AST\IndexBy
1910
     */
1911
    public function IndexBy()
1912
    {
1913 11
        $this->match(Lexer::T_INDEX);
1914
        $this->match(Lexer::T_BY);
1915 11
        $pathExpr = $this->StateFieldPathExpression();
1916 11
1917 11
        // Add the INDEX BY info to the query component
1918
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1919
1920 11
        return new AST\IndexBy($pathExpr);
1921
    }
1922 11
1923
    /**
1924
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1925
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1926
     *                      InstanceOfExpression
1927
     *
1928
     * @return mixed One of the possible expressions or subexpressions.
1929
     */
1930
    public function ScalarExpression()
1931
    {
1932 156
        $lookahead = $this->lexer->lookahead['type'];
1933
        $peek      = $this->lexer->glimpse();
1934 156
1935 156
        switch (true) {
1936
            case ($lookahead === Lexer::T_INTEGER):
1937
            case ($lookahead === Lexer::T_FLOAT):
1938 156
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1939 153
            case ($lookahead === Lexer::T_MINUS):
1940
            case ($lookahead === Lexer::T_PLUS):
1941 153
                return $this->SimpleArithmeticExpression();
1942 153
1943 16
            case ($lookahead === Lexer::T_STRING):
1944
                return $this->StringPrimary();
1945 153
1946 12
            case ($lookahead === Lexer::T_TRUE):
1947
            case ($lookahead === Lexer::T_FALSE):
1948 150
                $this->match($lookahead);
1949 150
1950 3
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1951
1952 3
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
1953
                switch (true) {
1954 150
                    case $this->isMathOperator($peek):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->glimpse() on line 1933 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...
1955
                        // :param + u.value
1956 1
                        return $this->SimpleArithmeticExpression();
1957
                    default:
1958 1
                        return $this->InputParameter();
1959
                }
1960
1961
            case ($lookahead === Lexer::T_CASE):
1962
            case ($lookahead === Lexer::T_COALESCE):
1963 150
            case ($lookahead === Lexer::T_NULLIF):
1964 146
                // Since NULLIF and COALESCE can be identified as a function,
1965 146
                // we need to check these before checking for FunctionDeclaration
1966
                return $this->CaseExpression();
1967
1968 7
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1969
                return $this->SimpleArithmeticExpression();
1970 146
1971 1
            // this check must be done before checking for a filed path expression
1972
            case ($this->isFunction()):
1973
                $this->lexer->peek(); // "("
1974 145
1975 23
                switch (true) {
1976
                    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...
1977
                        // SUM(u.id) + COUNT(u.id)
1978 23
                        return $this->SimpleArithmeticExpression();
1979
1980 5
                    default:
1981
                        // IDENTITY(u)
1982 18
                        return $this->FunctionDeclaration();
1983 17
                }
1984
1985
                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...
1986
            // it is no function, so it must be a field path
1987 1
            case ($lookahead === Lexer::T_IDENTIFIER):
1988
                $this->lexer->peek(); // lookahead => '.'
1989
                $this->lexer->peek(); // lookahead => token after '.'
1990
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1991
                $this->lexer->resetPeek();
1992 128
1993 128
                if ($this->isMathOperator($peek)) {
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1990 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...
1994 128
                    return $this->SimpleArithmeticExpression();
1995 128
                }
1996 128
1997
                return $this->StateFieldPathExpression();
1998 128
1999 6
            default:
2000
                $this->syntaxError();
2001
        }
2002 123
    }
2003
2004
    /**
2005
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
2006
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2007
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2008
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2009
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2010
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2011
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2012
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2013
     *
2014
     * @return mixed One of the possible expressions or subexpressions.
2015
     */
2016
    public function CaseExpression()
2017
    {
2018
        $lookahead = $this->lexer->lookahead['type'];
2019
2020
        switch ($lookahead) {
2021 18
            case Lexer::T_NULLIF:
2022
                return $this->NullIfExpression();
2023 18
2024
            case Lexer::T_COALESCE:
2025
                return $this->CoalesceExpression();
2026 18
2027 4
            case Lexer::T_CASE:
2028
                $this->lexer->resetPeek();
2029 16
                $peek = $this->lexer->peek();
2030 2
2031
                if ($peek['type'] === Lexer::T_WHEN) {
2032 14
                    return $this->GeneralCaseExpression();
2033 14
                }
2034 14
2035
                return $this->SimpleCaseExpression();
2036 14
2037 9
            default:
2038
                // Do nothing
2039
                break;
2040 5
        }
2041
2042
        $this->syntaxError();
2043
    }
2044
2045
    /**
2046
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2047
     *
2048
     * @return \Doctrine\ORM\Query\AST\CoalesceExpression
2049
     */
2050
    public function CoalesceExpression()
2051
    {
2052
        $this->match(Lexer::T_COALESCE);
2053
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2054
2055 2
        // Process ScalarExpressions (1..N)
2056
        $scalarExpressions = [];
2057 2
        $scalarExpressions[] = $this->ScalarExpression();
2058 2
2059
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2060
            $this->match(Lexer::T_COMMA);
2061 2
2062 2
            $scalarExpressions[] = $this->ScalarExpression();
2063
        }
2064 2
2065 2
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2066
2067 2
        return new AST\CoalesceExpression($scalarExpressions);
2068
    }
2069
2070 2
    /**
2071
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2072 2
     *
2073
     * @return \Doctrine\ORM\Query\AST\NullIfExpression
2074
     */
2075
    public function NullIfExpression()
2076
    {
2077
        $this->match(Lexer::T_NULLIF);
2078
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2079
2080 4
        $firstExpression = $this->ScalarExpression();
2081
        $this->match(Lexer::T_COMMA);
2082 4
        $secondExpression = $this->ScalarExpression();
2083 4
2084
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2085 4
2086 4
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2087 4
    }
2088
2089 4
    /**
2090
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2091 4
     *
2092
     * @return \Doctrine\ORM\Query\AST\GeneralCaseExpression
2093
     */
2094
    public function GeneralCaseExpression()
2095
    {
2096
        $this->match(Lexer::T_CASE);
2097
2098
        // Process WhenClause (1..N)
2099 9
        $whenClauses = [];
2100
2101 9
        do {
2102
            $whenClauses[] = $this->WhenClause();
2103
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2104 9
2105
        $this->match(Lexer::T_ELSE);
2106
        $scalarExpression = $this->ScalarExpression();
2107 9
        $this->match(Lexer::T_END);
2108 9
2109
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2110 9
    }
2111 9
2112 9
    /**
2113
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2114 9
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2115
     *
2116
     * @return AST\SimpleCaseExpression
2117
     */
2118
    public function SimpleCaseExpression()
2119
    {
2120
        $this->match(Lexer::T_CASE);
2121
        $caseOperand = $this->StateFieldPathExpression();
2122
2123 5
        // Process SimpleWhenClause (1..N)
2124
        $simpleWhenClauses = [];
2125 5
2126 5
        do {
2127
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2128
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2129 5
2130
        $this->match(Lexer::T_ELSE);
2131
        $scalarExpression = $this->ScalarExpression();
2132 5
        $this->match(Lexer::T_END);
2133 5
2134
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2135 5
    }
2136 5
2137 5
    /**
2138
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2139 5
     *
2140
     * @return \Doctrine\ORM\Query\AST\WhenClause
2141
     */
2142
    public function WhenClause()
2143
    {
2144
        $this->match(Lexer::T_WHEN);
2145
        $conditionalExpression = $this->ConditionalExpression();
2146
        $this->match(Lexer::T_THEN);
2147 9
2148
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2149 9
    }
2150 9
2151 9
    /**
2152
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2153 9
     *
2154
     * @return \Doctrine\ORM\Query\AST\SimpleWhenClause
2155
     */
2156
    public function SimpleWhenClause()
2157
    {
2158
        $this->match(Lexer::T_WHEN);
2159
        $conditionalExpression = $this->ScalarExpression();
2160
        $this->match(Lexer::T_THEN);
2161 5
2162
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2163 5
    }
2164 5
2165 5
    /**
2166
     * SelectExpression ::= (
2167 5
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2168
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2169
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2170
     *
2171
     * @return \Doctrine\ORM\Query\AST\SelectExpression
2172
     */
2173
    public function SelectExpression()
2174
    {
2175
        $expression    = null;
2176
        $identVariable = null;
2177
        $peek          = $this->lexer->glimpse();
2178 750
        $lookaheadType = $this->lexer->lookahead['type'];
2179
2180 750
        switch (true) {
2181 750
            // ScalarExpression (u.name)
2182 750
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2183 750
                $expression = $this->ScalarExpression();
2184
                break;
2185
2186
            // IdentificationVariable (u)
2187 750
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2188 101
                $expression = $identVariable = $this->IdentificationVariable();
2189 101
                break;
2190
2191
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2192 687
            case ($lookaheadType === Lexer::T_CASE):
2193 574
            case ($lookaheadType === Lexer::T_COALESCE):
2194 574
            case ($lookaheadType === Lexer::T_NULLIF):
2195
                $expression = $this->CaseExpression();
2196
                break;
2197 172
2198 167
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2199 165
            case ($this->isFunction()):
2200 9
                $this->lexer->peek(); // "("
2201 9
2202
                switch (true) {
2203
                    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...
2204 163
                        // SUM(u.id) + COUNT(u.id)
2205 84
                        $expression = $this->ScalarExpression();
2206
                        break;
2207
2208 84
                    default:
2209
                        // IDENTITY(u)
2210 2
                        $expression = $this->FunctionDeclaration();
2211 2
                        break;
2212
                }
2213 82
2214
                break;
2215 54
2216 54
            // PartialObjectExpression (PARTIAL u.{id, name})
2217
            case ($lookaheadType === Lexer::T_PARTIAL):
2218
                $expression    = $this->PartialObjectExpression();
2219
                $identVariable = $expression->identificationVariable;
2220 28
                break;
2221 28
2222
            // Subselect
2223
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2224 84
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2225
                $expression = $this->Subselect();
2226
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2227 79
                break;
2228 10
2229 10
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2230 10
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2231
            case ($lookaheadType === Lexer::T_INTEGER):
2232
            case ($lookaheadType === Lexer::T_STRING):
2233 69
            case ($lookaheadType === Lexer::T_FLOAT):
2234 22
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2235 22
            case ($lookaheadType === Lexer::T_MINUS):
2236 22
            case ($lookaheadType === Lexer::T_PLUS):
2237 22
                $expression = $this->SimpleArithmeticExpression();
2238
                break;
2239
2240 47
            // NewObjectExpression (New ClassName(id, name))
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2241 43
            case ($lookaheadType === Lexer::T_NEW):
2242 41
                $expression = $this->NewObjectExpression();
2243 32
                break;
2244
2245 32
            default:
2246 31
                $this->syntaxError(
2247 16
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2248 16
                    $this->lexer->lookahead
2249
                );
2250
        }
2251 31
2252 28
        // [["AS"] ["HIDDEN"] AliasResultVariable]
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2253 28
        $mustHaveAliasResultVariable = false;
2254
2255
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2256 3
            $this->match(Lexer::T_AS);
2257 3
2258 3
            $mustHaveAliasResultVariable = true;
2259
        }
2260
2261
        $hiddenAliasResultVariable = false;
2262
2263 747
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2264
            $this->match(Lexer::T_HIDDEN);
2265 747
2266 110
            $hiddenAliasResultVariable = true;
2267
        }
2268 110
2269
        $aliasResultVariable = null;
2270
2271 747
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2272
            $token = $this->lexer->lookahead;
2273 747
            $aliasResultVariable = $this->AliasResultVariable();
2274 10
2275
            // Include AliasResultVariable in query components.
2276 10
            $this->queryComponents[$aliasResultVariable] = [
2277
                'resultVariable' => $expression,
2278
                'nestingLevel'   => $this->nestingLevel,
2279 747
                'token'          => $token,
2280
            ];
2281 747
        }
2282 119
2283 119
        // AST
2284
2285
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2286 114
2287 114
        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...
2288 114
            $this->identVariableExpressions[$identVariable] = $expr;
2289 114
        }
2290
2291
        return $expr;
2292
    }
2293
2294
    /**
2295 742
     * SimpleSelectExpression ::= (
2296
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2297 742
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2298 582
     * ) [["AS"] AliasResultVariable]
2299
     *
2300
     * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
2301 742
     */
2302
    public function SimpleSelectExpression()
2303
    {
2304
        $peek = $this->lexer->glimpse();
2305
2306
        switch ($this->lexer->lookahead['type']) {
2307
            case Lexer::T_IDENTIFIER:
2308
                switch (true) {
2309
                    case ($peek['type'] === Lexer::T_DOT):
2310
                        $expression = $this->StateFieldPathExpression();
2311
2312 47
                        return new AST\SimpleSelectExpression($expression);
2313
2314 47
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2315
                        $expression = $this->IdentificationVariable();
2316 47
2317 47
                        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...
2318
2319 19
                    case ($this->isFunction()):
2320 16
                        // SUM(u.id) + COUNT(u.id)
2321
                        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...
2322 16
                            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...
2323
                        }
2324 3
                        // COUNT(u.id)
2325 2
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2326
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2327 2
                        }
2328
                        // IDENTITY(u)
2329 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...
2330
2331 1
                    default:
2332
                        // Do nothing
2333
                }
2334
                break;
2335 1
2336
            case Lexer::T_OPEN_PARENTHESIS:
2337
                if ($peek['type'] !== Lexer::T_SELECT) {
2338
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2339 1
                    $expression = $this->SimpleArithmeticExpression();
2340
2341
                    return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->SimpleArithmeticExpression() on line 2339 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...
2342
                }
2343
2344
                // Subselect
2345
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2346 28
                $expression = $this->Subselect();
2347 3
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2348
2349 3
                return new AST\SimpleSelectExpression($expression);
2350
2351 3
            default:
2352
                // Do nothing
2353
        }
2354
2355
        $this->lexer->peek();
2356
2357
        $expression = $this->ScalarExpression();
2358
        $expr       = new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->ScalarExpression() on line 2357 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...
2359
2360
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2361
            $this->match(Lexer::T_AS);
2362
        }
2363
2364
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2365 25
            $token = $this->lexer->lookahead;
2366
            $resultVariable = $this->AliasResultVariable();
2367 25
            $expr->fieldIdentificationVariable = $resultVariable;
2368 25
2369
            // Include AliasResultVariable in query components.
2370 25
            $this->queryComponents[$resultVariable] = [
2371 1
                'resultvariable' => $expr,
2372
                'nestingLevel'   => $this->nestingLevel,
2373
                'token'          => $token,
2374 25
            ];
2375 2
        }
2376 2
2377 2
        return $expr;
2378
    }
2379
2380 2
    /**
2381 2
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2382 2
     *
2383 2
     * @return \Doctrine\ORM\Query\AST\ConditionalExpression
2384
     */
2385
    public function ConditionalExpression()
2386
    {
2387 25
        $conditionalTerms = [];
2388
        $conditionalTerms[] = $this->ConditionalTerm();
2389
2390
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2391
            $this->match(Lexer::T_OR);
2392
2393
            $conditionalTerms[] = $this->ConditionalTerm();
2394
        }
2395 367
2396
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2397 367
        // if only one AST\ConditionalTerm is defined
2398 367
        if (\count($conditionalTerms) === 1) {
2399
            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...
2400 364
        }
2401 15
2402
        return new AST\ConditionalExpression($conditionalTerms);
2403 15
    }
2404
2405
    /**
2406
     * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2407
     *
2408 364
     * @return \Doctrine\ORM\Query\AST\ConditionalTerm
2409 356
     */
2410
    public function ConditionalTerm()
2411
    {
2412 15
        $conditionalFactors = [];
2413
        $conditionalFactors[] = $this->ConditionalFactor();
2414
2415
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2416
            $this->match(Lexer::T_AND);
2417
2418
            $conditionalFactors[] = $this->ConditionalFactor();
2419
        }
2420 367
2421
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2422 367
        // if only one AST\ConditionalFactor is defined
2423 367
        if (\count($conditionalFactors) === 1) {
2424
            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...
2425 364
        }
2426 31
2427
        return new AST\ConditionalTerm($conditionalFactors);
2428 31
    }
2429
2430
    /**
2431
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2432
     *
2433 364
     * @return \Doctrine\ORM\Query\AST\ConditionalFactor
2434 345
     */
2435
    public function ConditionalFactor()
2436
    {
2437 31
        $not = false;
2438
2439
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2440
            $this->match(Lexer::T_NOT);
2441
2442
            $not = true;
2443
        }
2444
2445 367
        $conditionalPrimary = $this->ConditionalPrimary();
2446
2447 367
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2448
        // if only one AST\ConditionalPrimary is defined
2449 367
        if ( ! $not) {
2450 6
            return $conditionalPrimary;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $conditionalPrimary; (Doctrine\ORM\Query\AST\ConditionalPrimary) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ConditionalFactor of type Doctrine\ORM\Query\AST\ConditionalFactor.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
2451
        }
2452 6
2453
        $conditionalFactor = new AST\ConditionalFactor($conditionalPrimary);
2454
        $conditionalFactor->not = $not;
2455 367
2456
        return $conditionalFactor;
2457
    }
2458
2459 364
    /**
2460 362
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2461
     *
2462
     * @return \Doctrine\ORM\Query\AST\ConditionalPrimary
2463 6
     */
2464 6
    public function ConditionalPrimary()
2465
    {
2466 6
        $condPrimary = new AST\ConditionalPrimary;
2467
2468
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2469
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2470
2471
            return $condPrimary;
2472
        }
2473
2474 367
        // Peek beyond the matching closing parenthesis ')'
2475
        $peek = $this->peekBeyondClosingParenthesis();
2476 367
2477
        if (in_array($peek['value'], ["=",  "<", "<=", "<>", ">", ">=", "!="]) ||
2478 367
            in_array($peek['type'], [Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS]) ||
2479 358
            $this->isMathOperator($peek)) {
2480
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2481 355
2482
            return $condPrimary;
2483
        }
2484
2485 24
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2486
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2487 24
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2488 21
2489 24
        return $condPrimary;
2490 14
    }
2491
2492 14
    /**
2493
     * SimpleConditionalExpression ::=
2494
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2495 21
     *      InExpression | NullComparisonExpression | ExistsExpression |
2496 21
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2497 21
     *      InstanceOfExpression
2498
     */
2499 21
    public function SimpleConditionalExpression()
2500
    {
2501
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2502
            return $this->ExistsExpression();
2503
        }
2504
2505
        $token      = $this->lexer->lookahead;
2506
        $peek       = $this->lexer->glimpse();
2507
        $lookahead  = $token;
2508
2509 367
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2510
            $token = $this->lexer->glimpse();
2511 367
        }
2512 7
2513
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2514
            // Peek beyond the matching closing parenthesis.
2515 367
            $beyond = $this->lexer->peek();
2516 367
2517 367
            switch ($peek['value']) {
2518
                case '(':
2519 367
                    // Peeks beyond the matched closing parenthesis.
2520
                    $token = $this->peekBeyondClosingParenthesis(false);
2521
2522
                    if ($token['type'] === Lexer::T_NOT) {
2523 367
                        $token = $this->lexer->peek();
2524
                    }
2525 344
2526
                    if ($token['type'] === Lexer::T_IS) {
2527 344
                        $lookahead = $this->lexer->peek();
2528 344
                    }
2529
                    break;
2530 31
2531
                default:
2532 31
                    // Peek beyond the PathExpression or InputParameter.
2533 3
                    $token = $beyond;
2534
2535
                    while ($token['value'] === '.') {
2536 31
                        $this->lexer->peek();
2537 1
2538
                        $token = $this->lexer->peek();
2539 31
                    }
2540
2541
                    // Also peek beyond a NOT if there is one.
2542
                    if ($token['type'] === Lexer::T_NOT) {
2543 313
                        $token = $this->lexer->peek();
2544
                    }
2545 313
2546 274
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2547
                    $lookahead = $this->lexer->peek();
2548 274
            }
2549
2550
            // Also peek beyond a NOT if there is one.
2551
            if ($lookahead['type'] === Lexer::T_NOT) {
2552 313
                $lookahead = $this->lexer->peek();
2553 10
            }
2554
2555
            $this->lexer->resetPeek();
2556
        }
2557 313
2558
        if ($token['type'] === Lexer::T_BETWEEN) {
2559
            return $this->BetweenExpression();
2560
        }
2561 344
2562 4
        if ($token['type'] === Lexer::T_LIKE) {
2563
            return $this->LikeExpression();
2564
        }
2565 344
2566
        if ($token['type'] === Lexer::T_IN) {
2567
            return $this->InExpression();
2568 367
        }
2569 8
2570
        if ($token['type'] === Lexer::T_INSTANCE) {
2571
            return $this->InstanceOfExpression();
2572 361
        }
2573 13
2574
        if ($token['type'] === Lexer::T_MEMBER) {
2575
            return $this->CollectionMemberExpression();
2576 348
        }
2577 34
2578
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2579
            return $this->NullComparisonExpression();
2580 322
        }
2581 12
2582
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2583
            return $this->EmptyCollectionComparisonExpression();
2584 310
        }
2585 8
2586
        return $this->ComparisonExpression();
2587
    }
2588 302
2589 13
    /**
2590
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2591
     *
2592 292
     * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression
2593 4
     */
2594
    public function EmptyCollectionComparisonExpression()
2595
    {
2596 288
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2597
            $this->CollectionValuedPathExpression()
2598
        );
2599
        $this->match(Lexer::T_IS);
2600
2601
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2602
            $this->match(Lexer::T_NOT);
2603
            $emptyCollectionCompExpr->not = true;
2604 4
        }
2605
2606 4
        $this->match(Lexer::T_EMPTY);
2607 4
2608
        return $emptyCollectionCompExpr;
2609 4
    }
2610
2611 4
    /**
2612 1
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2613 1
     *
2614
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2615
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2616 4
     *
2617
     * @return \Doctrine\ORM\Query\AST\CollectionMemberExpression
2618 4
     */
2619
    public function CollectionMemberExpression()
2620
    {
2621
        $not        = false;
2622
        $entityExpr = $this->EntityExpression();
2623
2624
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2625
            $this->match(Lexer::T_NOT);
2626
2627
            $not = true;
2628
        }
2629 8
2630
        $this->match(Lexer::T_MEMBER);
2631 8
2632 8
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2633
            $this->match(Lexer::T_OF);
2634 8
        }
2635
2636
        $collMemberExpr = new AST\CollectionMemberExpression(
2637
            $entityExpr, $this->CollectionValuedPathExpression()
2638
        );
2639
        $collMemberExpr->not = $not;
2640 8
2641
        return $collMemberExpr;
2642 8
    }
2643 8
2644
    /**
2645
     * Literal ::= string | char | integer | float | boolean
2646 8
     *
2647 8
     * @return \Doctrine\ORM\Query\AST\Literal
2648
     */
2649 8
    public function Literal()
2650
    {
2651 8
        switch ($this->lexer->lookahead['type']) {
2652
            case Lexer::T_STRING:
2653
                $this->match(Lexer::T_STRING);
2654
2655
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2656
            case Lexer::T_INTEGER:
2657
            case Lexer::T_FLOAT:
2658
                $this->match(
2659 176
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2660
                );
2661 176
2662 176
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2663 49
            case Lexer::T_TRUE:
2664
            case Lexer::T_FALSE:
2665 49
                $this->match(
2666 136
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2667 9
                );
2668 128
2669 128
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2670
            default:
2671
                $this->syntaxError('Literal');
2672 128
        }
2673 8
    }
2674 2
2675 8
    /**
2676 8
     * InParameter ::= Literal | InputParameter
2677
     *
2678
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2679 8
     */
2680
    public function InParameter()
2681
    {
2682
        if ($this->lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) {
2683
            return $this->InputParameter();
2684
        }
2685
2686
        return $this->Literal();
2687
    }
2688
2689
    /**
2690 25
     * InputParameter ::= PositionalParameter | NamedParameter
2691
     *
2692 25
     * @return \Doctrine\ORM\Query\AST\InputParameter
2693 13
     */
2694
    public function InputParameter()
2695
    {
2696 12
        $this->match(Lexer::T_INPUT_PARAMETER);
2697
2698
        return new AST\InputParameter($this->lexer->token['value']);
2699
    }
2700
2701
    /**
2702
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2703
     *
2704 155
     * @return \Doctrine\ORM\Query\AST\ArithmeticExpression
2705
     */
2706 155
    public function ArithmeticExpression()
2707
    {
2708 155
        $expr = new AST\ArithmeticExpression;
2709
2710
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2711
            $peek = $this->lexer->glimpse();
2712
2713
            if ($peek['type'] === Lexer::T_SELECT) {
2714
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2715
                $expr->subselect = $this->Subselect();
2716 322
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2717
2718 322
                return $expr;
2719
            }
2720 322
        }
2721 17
2722
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2723 17
2724 6
        return $expr;
2725 6
    }
2726 6
2727
    /**
2728 6
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2729
     *
2730
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2731
     */
2732 322
    public function SimpleArithmeticExpression()
2733
    {
2734 322
        $terms = [];
2735
        $terms[] = $this->ArithmeticTerm();
2736
2737
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2738
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2739
2740
            $terms[] = $this->lexer->token['value'];
2741
            $terms[] = $this->ArithmeticTerm();
2742 424
        }
2743
2744 424
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2745 424
        // if only one AST\ArithmeticTerm is defined
2746
        if (\count($terms) === 1) {
2747 424
            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 2747 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...
2748 18
        }
2749
2750 18
        return new AST\SimpleArithmeticExpression($terms);
2751 18
    }
2752
2753
    /**
2754
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2755
     *
2756 424
     * @return \Doctrine\ORM\Query\AST\ArithmeticTerm
2757 418
     */
2758
    public function ArithmeticTerm()
2759
    {
2760 18
        $factors = [];
2761
        $factors[] = $this->ArithmeticFactor();
2762
2763
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2764
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2765
2766
            $factors[] = $this->lexer->token['value'];
2767
            $factors[] = $this->ArithmeticFactor();
2768 424
        }
2769
2770 424
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2771 424
        // if only one AST\ArithmeticFactor is defined
2772
        if (\count($factors) === 1) {
2773 424
            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 2773 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticTerm of type Doctrine\ORM\Query\AST\ArithmeticTerm|null.
Loading history...
2774 52
        }
2775
2776 52
        return new AST\ArithmeticTerm($factors);
2777 52
    }
2778
2779
    /**
2780
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2781
     *
2782 424
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2783 395
     */
2784
    public function ArithmeticFactor()
2785
    {
2786 52
        $sign = null;
2787
2788
        if (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2789
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2790
            $sign = $isPlus;
2791
        }
2792
2793
        $primary = $this->ArithmeticPrimary();
2794 424
2795
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2796 424
        // if only one AST\ArithmeticPrimary is defined
2797
        if ($sign === null) {
2798 424
            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...
2799 3
        }
2800 3
2801
        return new AST\ArithmeticFactor($primary, $sign);
2802
    }
2803 424
2804
    /**
2805
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2806
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2807 424
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2808 422
     *          | InputParameter | CaseExpression
2809
     */
2810
    public function ArithmeticPrimary()
2811 3
    {
2812
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2813
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2814
2815
            $expr = $this->SimpleArithmeticExpression();
2816
2817
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2818
2819
            return new AST\ParenthesisExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr defined by $this->SimpleArithmeticExpression() on line 2815 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...
2820 429
        }
2821
2822 429
        switch ($this->lexer->lookahead['type']) {
2823 22
            case Lexer::T_COALESCE:
2824
            case Lexer::T_NULLIF:
2825 22
            case Lexer::T_CASE:
2826
                return $this->CaseExpression();
2827 22
2828
            case Lexer::T_IDENTIFIER:
2829 22
                $peek = $this->lexer->glimpse();
2830
2831
                if ($peek['value'] == '(') {
2832 429
                    return $this->FunctionDeclaration();
2833 429
                }
2834 429
2835 429
                if ($peek['value'] == '.') {
2836 4
                    return $this->SingleValuedPathExpression();
2837
                }
2838 429
2839 396
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2840
                    return $this->ResultVariable();
2841 396
                }
2842 28
2843
                return $this->StateFieldPathExpression();
2844
2845 373
            case Lexer::T_INPUT_PARAMETER:
2846 362
                return $this->InputParameter();
2847
2848
            default:
2849 45
                $peek = $this->lexer->glimpse();
2850 10
2851
                if ($peek['value'] == '(') {
2852
                    return $this->FunctionDeclaration();
2853 37
                }
2854
2855 302
                return $this->Literal();
2856 139
        }
2857
    }
2858
2859 169
    /**
2860
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2861 169
     *
2862 16
     * @return \Doctrine\ORM\Query\AST\Subselect |
2863 16
     *         string
2864
     */
2865
    public function StringExpression()
2866
    {
2867
        $peek = $this->lexer->glimpse();
2868
2869 166
        // Subselect
2870
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
2871
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2872
            $expr = $this->Subselect();
2873
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2874
2875
            return $expr;
2876
        }
2877
2878
        // ResultVariable (string)
2879
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2880 13
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2881
            return $this->ResultVariable();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->ResultVariable(); (string) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::StringExpression of type Doctrine\ORM\Query\AST\Subselect|null.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
2882 13
        }
2883
2884
        return $this->StringPrimary();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->StringPrimary(); (Doctrine\ORM\Query\AST\P...mpleCaseExpression|null) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::StringExpression of type Doctrine\ORM\Query\AST\Subselect|null.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
2885 13
    }
2886
2887
    /**
2888
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2889
     */
2890
    public function StringPrimary()
2891
    {
2892
        $lookaheadType = $this->lexer->lookahead['type'];
2893
2894 13
        switch ($lookaheadType) {
2895 13
            case Lexer::T_IDENTIFIER:
2896 2
                $peek = $this->lexer->glimpse();
2897
2898
                if ($peek['value'] == '.') {
2899 11
                    return $this->StateFieldPathExpression();
2900
                }
2901
2902
                if ($peek['value'] == '(') {
2903
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2904
                    return $this->FunctionDeclaration();
2905 49
                }
2906
2907 49
                $this->syntaxError("'.' or '('");
2908
                break;
2909
2910 49
            case Lexer::T_STRING:
2911 31
                $this->match(Lexer::T_STRING);
2912
2913 31
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2914 31
2915
            case Lexer::T_INPUT_PARAMETER:
2916
                return $this->InputParameter();
2917 6
2918
            case Lexer::T_CASE:
2919 6
            case Lexer::T_COALESCE:
2920
            case Lexer::T_NULLIF:
2921
                return $this->CaseExpression();
2922
        }
2923
2924
        $this->syntaxError(
2925 30
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2926 30
        );
2927
    }
2928 30
2929
    /**
2930
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2931
     *
2932
     * @return \Doctrine\ORM\Query\AST\PathExpression |
2933
     *         \Doctrine\ORM\Query\AST\SimpleEntityExpression
2934
     */
2935
    public function EntityExpression()
2936
    {
2937
        $glimpse = $this->lexer->glimpse();
2938
2939
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2940
            return $this->SingleValuedAssociationPathExpression();
2941
        }
2942
2943
        return $this->SimpleEntityExpression();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->SimpleEntityExpression(); of type Doctrine\ORM\Query\AST\I...uery\AST\PathExpression adds the type Doctrine\ORM\Query\AST\InputParameter to the return on line 2943 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::EntityExpression of type Doctrine\ORM\Query\AST\PathExpression.
Loading history...
2944
    }
2945
2946
    /**
2947
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2948
     *
2949
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2950
     */
2951
    public function SimpleEntityExpression()
2952
    {
2953
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2954
            return $this->InputParameter();
2955 8
        }
2956
2957 8
        return $this->StateFieldPathExpression();
2958
    }
2959 8
2960 1
    /**
2961
     * AggregateExpression ::=
2962
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2963 7
     *
2964
     * @return \Doctrine\ORM\Query\AST\AggregateExpression
2965
     */
2966
    public function AggregateExpression()
2967
    {
2968
        $lookaheadType = $this->lexer->lookahead['type'];
2969
        $isDistinct = false;
2970
2971 7
        if ( ! in_array($lookaheadType, [Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM])) {
2972
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2973 7
        }
2974 5
2975
        $this->match($lookaheadType);
2976
        $functionName = $this->lexer->token['value'];
2977 2
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2978
2979
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
2980
            $this->match(Lexer::T_DISTINCT);
2981
            $isDistinct = true;
2982
        }
2983
2984
        $pathExp = $this->SimpleArithmeticExpression();
2985
2986 81
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2987
2988 81
        return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
0 ignored issues
show
Bug introduced by
It seems like $pathExp defined by $this->SimpleArithmeticExpression() on line 2984 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...
2989 81
    }
2990
2991 81
    /**
2992
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
2993
     *
2994
     * @return \Doctrine\ORM\Query\AST\QuantifiedExpression
2995 81
     */
2996 81
    public function QuantifiedExpression()
2997 81
    {
2998
        $lookaheadType = $this->lexer->lookahead['type'];
2999 81
        $value = $this->lexer->lookahead['value'];
3000 2
3001 2
        if ( ! in_array($lookaheadType, [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME])) {
3002
            $this->syntaxError('ALL, ANY or SOME');
3003
        }
3004 81
3005
        $this->match($lookaheadType);
3006 81
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3007
3008 81
        $qExpr = new AST\QuantifiedExpression($this->Subselect());
3009
        $qExpr->type = $value;
3010
3011
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3012
3013
        return $qExpr;
3014
    }
3015
3016 3
    /**
3017
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3018 3
     *
3019 3
     * @return \Doctrine\ORM\Query\AST\BetweenExpression
3020
     */
3021 3
    public function BetweenExpression()
3022
    {
3023
        $not = false;
3024
        $arithExpr1 = $this->ArithmeticExpression();
3025 3
3026 3
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3027
            $this->match(Lexer::T_NOT);
3028 3
            $not = true;
3029 3
        }
3030
3031 3
        $this->match(Lexer::T_BETWEEN);
3032
        $arithExpr2 = $this->ArithmeticExpression();
3033 3
        $this->match(Lexer::T_AND);
3034
        $arithExpr3 = $this->ArithmeticExpression();
3035
3036
        $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3037
        $betweenExpr->not = $not;
3038
3039
        return $betweenExpr;
3040
    }
3041 8
3042
    /**
3043 8
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3044 8
     *
3045
     * @return \Doctrine\ORM\Query\AST\ComparisonExpression
3046 8
     */
3047 3
    public function ComparisonExpression()
3048 3
    {
3049
        $this->lexer->glimpse();
3050
3051 8
        $leftExpr  = $this->ArithmeticExpression();
3052 8
        $operator  = $this->ComparisonOperator();
3053 8
        $rightExpr = ($this->isNextAllAnySome())
3054 8
            ? $this->QuantifiedExpression()
3055
            : $this->ArithmeticExpression();
3056 8
3057 8
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3058
    }
3059 8
3060
    /**
3061
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3062
     *
3063
     * @return \Doctrine\ORM\Query\AST\InExpression
3064
     */
3065
    public function InExpression()
3066
    {
3067 288
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3068
3069 288
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3070
            $this->match(Lexer::T_NOT);
3071 288
            $inExpression->not = true;
3072 288
        }
3073 288
3074 3
        $this->match(Lexer::T_IN);
3075 288
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3076
3077 286
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3078
            $inExpression->subselect = $this->Subselect();
3079
        } else {
3080
            $literals = [];
3081
            $literals[] = $this->InParameter();
3082
3083
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3084
                $this->match(Lexer::T_COMMA);
3085 34
                $literals[] = $this->InParameter();
3086
            }
3087 34
3088
            $inExpression->literals = $literals;
3089 34
        }
3090 6
3091 6
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3092
3093
        return $inExpression;
3094 34
    }
3095 34
3096
    /**
3097 34
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3098 9
     *
3099
     * @return \Doctrine\ORM\Query\AST\InstanceOfExpression
3100 25
     */
3101 25
    public function InstanceOfExpression()
3102
    {
3103 25
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3104 16
3105 16
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3106
            $this->match(Lexer::T_NOT);
3107
            $instanceOfExpression->not = true;
3108 25
        }
3109
3110
        $this->match(Lexer::T_INSTANCE);
3111 33
        $this->match(Lexer::T_OF);
3112
3113 33
        $exprValues = [];
3114
3115
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3116
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3117
3118
            $exprValues[] = $this->InstanceOfParameter();
3119
3120
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3121 12
                $this->match(Lexer::T_COMMA);
3122
3123 12
                $exprValues[] = $this->InstanceOfParameter();
3124
            }
3125 12
3126 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3127 1
3128
            $instanceOfExpression->value = $exprValues;
3129
3130 12
            return $instanceOfExpression;
3131 12
        }
3132
3133 12
        $exprValues[] = $this->InstanceOfParameter();
3134
3135 12
        $instanceOfExpression->value = $exprValues;
3136 1
3137
        return $instanceOfExpression;
3138 1
    }
3139
3140 1
    /**
3141 1
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3142
     *
3143 1
     * @return mixed
3144
     */
3145
    public function InstanceOfParameter()
3146 1
    {
3147
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3148 1
            $this->match(Lexer::T_INPUT_PARAMETER);
3149
3150 1
            return new AST\InputParameter($this->lexer->token['value']);
3151
        }
3152
3153 11
        $abstractSchemaName = $this->AbstractSchemaName();
3154
3155 11
        $this->validateAbstractSchemaName($abstractSchemaName);
3156
3157 11
        return $abstractSchemaName;
3158
    }
3159
3160
    /**
3161
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3162
     *
3163
     * @return \Doctrine\ORM\Query\AST\LikeExpression
3164
     */
3165 12
    public function LikeExpression()
3166
    {
3167 12
        $stringExpr = $this->StringExpression();
3168 5
        $not = false;
3169
3170 5
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3171
            $this->match(Lexer::T_NOT);
3172
            $not = true;
3173 7
        }
3174
3175 7
        $this->match(Lexer::T_LIKE);
3176
3177 7
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3178
            $this->match(Lexer::T_INPUT_PARAMETER);
3179
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3180
        } else {
3181
            $stringPattern = $this->StringPrimary();
3182
        }
3183
3184
        $escapeChar = null;
3185 13
3186
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3187 13
            $this->match(Lexer::T_ESCAPE);
3188 13
            $this->match(Lexer::T_STRING);
3189
3190 13
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3191 3
        }
3192 3
3193
        $likeExpr = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $stringExpr defined by $this->StringExpression() on line 3167 can be null; however, Doctrine\ORM\Query\AST\L...pression::__construct() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Bug introduced by
It seems like $stringPattern defined by $this->StringPrimary() on line 3181 can also be of type null or object<Doctrine\ORM\Query\AST\CoalesceExpression> or object<Doctrine\ORM\Quer...Functions\FunctionNode> or object<Doctrine\ORM\Quer...\GeneralCaseExpression> or object<Doctrine\ORM\Query\AST\Literal> or object<Doctrine\ORM\Query\AST\NullIfExpression> or object<Doctrine\ORM\Query\AST\PathExpression> or object<Doctrine\ORM\Quer...T\SimpleCaseExpression>; however, Doctrine\ORM\Query\AST\L...pression::__construct() does only seem to accept object<Doctrine\ORM\Query\AST\InputParameter>, maybe add an additional type check?

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

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

    return array();
}

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

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

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