Failed Conditions
Push — master ( f351f5...92445d )
by Marco
17:36
created

Parser::ArithmeticFactor()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 4
nop 0
dl 0
loc 19
ccs 10
cts 10
cp 1
crap 5
rs 8.8571
c 0
b 0
f 0
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 184 and the first side effect is on line 35.

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\EntityManagerInterface;
9
use Doctrine\ORM\Mapping\AssociationMetadata;
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
use Doctrine\ORM\Query\AST\Node;
15
use function array_intersect;
16
use function array_search;
17
use function call_user_func;
18
use function class_exists;
19
use function count;
20
use function implode;
21
use function in_array;
22
use function interface_exists;
23
use function is_string;
24
use function sprintf;
25
use function strlen;
26
use function strpos;
27
use function strrpos;
28
use function strtolower;
29
use function substr;
30
31
/**
32
 * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
33
 * Parses a DQL query, reports any errors in it, and generates an AST.
34
 */
35
class Parser
0 ignored issues
show
Bug introduced by
Possible parse error: class missing opening or closing brace
Loading history...
36
{
37
    /**
38
     * READ-ONLY: Maps BUILT-IN string function names to AST class names.
39
     *
40
     * @var string[]
41
     */
42
    private static $_STRING_FUNCTIONS = [
43
        'concat'    => Functions\ConcatFunction::class,
44
        'substring' => Functions\SubstringFunction::class,
45
        'trim'      => Functions\TrimFunction::class,
46
        'lower'     => Functions\LowerFunction::class,
47
        'upper'     => Functions\UpperFunction::class,
48
        'identity'  => Functions\IdentityFunction::class,
49
    ];
50
51
    /**
52
     * READ-ONLY: Maps BUILT-IN numeric function names to AST class names.
53
     *
54
     * @var string[]
55
     */
56
    private static $_NUMERIC_FUNCTIONS = [
57
        'length'    => Functions\LengthFunction::class,
58
        'locate'    => Functions\LocateFunction::class,
59
        'abs'       => Functions\AbsFunction::class,
60
        'sqrt'      => Functions\SqrtFunction::class,
61
        'mod'       => Functions\ModFunction::class,
62
        'size'      => Functions\SizeFunction::class,
63
        'date_diff' => Functions\DateDiffFunction::class,
64
        'bit_and'   => Functions\BitAndFunction::class,
65
        'bit_or'    => Functions\BitOrFunction::class,
66
67
        // Aggregate functions
68
        'min'       => Functions\MinFunction::class,
69
        'max'       => Functions\MaxFunction::class,
70
        'avg'       => Functions\AvgFunction::class,
71
        'sum'       => Functions\SumFunction::class,
72
        'count'     => Functions\CountFunction::class,
73
    ];
74
75
    /**
76
     * READ-ONLY: Maps BUILT-IN datetime function names to AST class names.
77
     *
78
     * @var string[]
79
     */
80
    private static $_DATETIME_FUNCTIONS = [
81
        'current_date'      => Functions\CurrentDateFunction::class,
82
        'current_time'      => Functions\CurrentTimeFunction::class,
83
        'current_timestamp' => Functions\CurrentTimestampFunction::class,
84
        'date_add'          => Functions\DateAddFunction::class,
85
        'date_sub'          => Functions\DateSubFunction::class,
86
    ];
87
88
    /*
89
     * Expressions that were encountered during parsing of identifiers and expressions
90
     * and still need to be validated.
91
     */
92
93
    /**
94
     * @var mixed[][]
95
     */
96
    private $deferredIdentificationVariables = [];
97
98
    /**
99
     * @var mixed[][]
100
     */
101
    private $deferredPartialObjectExpressions = [];
102
103
    /**
104
     * @var mixed[][]
105
     */
106
    private $deferredPathExpressions = [];
107
108
    /**
109
     * @var mixed[][]
110
     */
111
    private $deferredResultVariables = [];
112
113
    /**
114
     * @var mixed[][]
115
     */
116
    private $deferredNewObjectExpressions = [];
117
118
    /**
119
     * The lexer.
120
     *
121
     * @var Lexer
122
     */
123
    private $lexer;
124
125
    /**
126
     * The parser result.
127
     *
128
     * @var ParserResult
129
     */
130
    private $parserResult;
131
132
    /**
133
     * The EntityManager.
134
     *
135
     * @var EntityManagerInterface
136
     */
137
    private $em;
138
139
    /**
140
     * The Query to parse.
141
     *
142
     * @var Query
143
     */
144
    private $query;
145
146
    /**
147
     * Map of declared query components in the parsed query.
148
     *
149
     * @var mixed[][]
150
     */
151
    private $queryComponents = [];
152
153
    /**
154
     * Keeps the nesting level of defined ResultVariables.
155
     *
156
     * @var int
157
     */
158
    private $nestingLevel = 0;
159
160
    /**
161
     * Any additional custom tree walkers that modify the AST.
162
     *
163
     * @var string[]
164
     */
165
    private $customTreeWalkers = [];
166
167
    /**
168
     * The custom last tree walker, if any, that is responsible for producing the output.
169
     *
170
     * @var TreeWalker
171
     */
172
    private $customOutputWalker;
173
174
    /**
175
     * @var Node[]
176
     */
177
    private $identVariableExpressions = [];
178
179
    /**
180
     * Creates a new query parser object.
181
     *
182
     * @param Query $query The Query to parse.
183
     */
184 840
    public function __construct(Query $query)
185
    {
186 840
        $this->query        = $query;
187 840
        $this->em           = $query->getEntityManager();
188 840
        $this->lexer        = new Lexer($query->getDQL());
189 840
        $this->parserResult = new ParserResult();
190 840
    }
191
192
    /**
193
     * Sets a custom tree walker that produces output.
194
     * This tree walker will be run last over the AST, after any other walkers.
195
     *
196
     * @param string $className
197
     */
198 120
    public function setCustomOutputTreeWalker($className)
199
    {
200 120
        $this->customOutputWalker = $className;
0 ignored issues
show
Documentation Bug introduced by
It seems like $className of type string is incompatible with the declared type 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...
201 120
    }
202
203
    /**
204
     * Adds a custom tree walker for modifying the AST.
205
     *
206
     * @param string $className
207
     */
208
    public function addCustomTreeWalker($className)
209
    {
210
        $this->customTreeWalkers[] = $className;
211
    }
212
213
    /**
214
     * Gets the lexer used by the parser.
215
     *
216
     * @return Lexer
217
     */
218 31
    public function getLexer()
219
    {
220 31
        return $this->lexer;
221
    }
222
223
    /**
224
     * Gets the ParserResult that is being filled with information during parsing.
225
     *
226
     * @return ParserResult
227
     */
228
    public function getParserResult()
229
    {
230
        return $this->parserResult;
231
    }
232
233
    /**
234
     * Gets the EntityManager used by the parser.
235
     *
236
     * @return EntityManagerInterface
237
     */
238 10
    public function getEntityManager()
239
    {
240 10
        return $this->em;
241
    }
242
243
    /**
244
     * Parses and builds AST for the given Query.
245
     *
246
     * @return AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement
247
     */
248 840
    public function getAST()
249
    {
250
        // Parse & build AST
251 840
        $AST = $this->QueryLanguage();
252
253
        // Process any deferred validations of some nodes in the AST.
254
        // This also allows post-processing of the AST for modification purposes.
255 805
        $this->processDeferredIdentificationVariables();
256
257 803
        if ($this->deferredPartialObjectExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredPartialObjectExpressions of type array<mixed,array<mixed,mixed>> 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...
258 9
            $this->processDeferredPartialObjectExpressions();
259
        }
260
261 802
        if ($this->deferredPathExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredPathExpressions of type array<mixed,array<mixed,mixed>> 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...
262 592
            $this->processDeferredPathExpressions();
263
        }
264
265 800
        if ($this->deferredResultVariables) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredResultVariables of type array<mixed,array<mixed,mixed>> 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...
266 32
            $this->processDeferredResultVariables();
267
        }
268
269 800
        if ($this->deferredNewObjectExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredNewObjectExpressions of type array<mixed,array<mixed,mixed>> 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...
270 26
            $this->processDeferredNewObjectExpressions($AST);
0 ignored issues
show
Bug introduced by
$AST of type Doctrine\ORM\Query\AST\U...ery\AST\DeleteStatement is incompatible with the type Doctrine\ORM\Query\AST\SelectClause expected by parameter $AST of Doctrine\ORM\Query\Parse...dNewObjectExpressions(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
            $this->processDeferredNewObjectExpressions(/** @scrutinizer ignore-type */ $AST);
Loading history...
271
        }
272
273 796
        $this->processRootEntityAliasSelected();
274
275
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
276 795
        $this->fixIdentificationVariableOrder($AST);
277
278 795
        return $AST;
279
    }
280
281
    /**
282
     * Attempts to match the given token with the current lookahead token.
283
     *
284
     * If they match, updates the lookahead token; otherwise raises a syntax
285
     * error.
286
     *
287
     * @param int $token The token type.
288
     *
289
     * @throws QueryException If the tokens don't match.
290
     */
291 851
    public function match($token)
292
    {
293 851
        $lookaheadType = $this->lexer->lookahead['type'];
294
295
        // Short-circuit on first condition, usually types match
296 851
        if ($lookaheadType === $token) {
297 843
            $this->lexer->moveNext();
298 843
            return;
299
        }
300
301
        // If parameter is not identifier (1-99) must be exact match
302 21
        if ($token < Lexer::T_IDENTIFIER) {
303 3
            $this->syntaxError($this->lexer->getLiteral($token));
304
        }
305
306
        // If parameter is keyword (200+) must be exact match
307 18
        if ($token > Lexer::T_IDENTIFIER) {
308 7
            $this->syntaxError($this->lexer->getLiteral($token));
309
        }
310
311
        // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
312 11
        if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
313 8
            $this->syntaxError($this->lexer->getLiteral($token));
314
        }
315
316 3
        $this->lexer->moveNext();
317 3
    }
318
319
    /**
320
     * Frees this parser, enabling it to be reused.
321
     *
322
     * @param bool $deep     Whether to clean peek and reset errors.
323
     * @param int  $position Position to reset.
324
     */
325
    public function free($deep = false, $position = 0)
326
    {
327
        // WARNING! Use this method with care. It resets the scanner!
328
        $this->lexer->resetPosition($position);
329
330
        // Deep = true cleans peek and also any previously defined errors
331
        if ($deep) {
332
            $this->lexer->resetPeek();
333
        }
334
335
        $this->lexer->token     = null;
336
        $this->lexer->lookahead = null;
337
    }
338
339
    /**
340
     * Parses a query string.
341
     *
342
     * @return ParserResult
343
     */
344 840
    public function parse()
345
    {
346 840
        $AST = $this->getAST();
347
348 795
        $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
349 795
        if ($customWalkers !== false) {
350 96
            $this->customTreeWalkers = $customWalkers;
351
        }
352
353 795
        $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER);
354
355 795
        if ($customOutputWalker !== false) {
356 79
            $this->customOutputWalker = $customOutputWalker;
357
        }
358
359
        // Run any custom tree walkers over the AST
360 795
        if ($this->customTreeWalkers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customTreeWalkers of type string[] 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...
361 95
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
362
363 95
            foreach ($this->customTreeWalkers as $walker) {
364 95
                $treeWalkerChain->addTreeWalker($walker);
365
            }
366
367
            switch (true) {
368 95
                case ($AST instanceof AST\UpdateStatement):
369
                    $treeWalkerChain->walkUpdateStatement($AST);
370
                    break;
371
372 95
                case ($AST instanceof AST\DeleteStatement):
373
                    $treeWalkerChain->walkDeleteStatement($AST);
374
                    break;
375
376 95
                case ($AST instanceof AST\SelectStatement):
377
                default:
378 95
                    $treeWalkerChain->walkSelectStatement($AST);
0 ignored issues
show
Bug introduced by
It seems like $AST can also be of type Doctrine\ORM\Query\AST\UpdateStatement and Doctrine\ORM\Query\AST\DeleteStatement; however, parameter $AST of Doctrine\ORM\Query\TreeW...::walkSelectStatement() does only seem to accept Doctrine\ORM\Query\AST\SelectStatement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

378
                    $treeWalkerChain->walkSelectStatement(/** @scrutinizer ignore-type */ $AST);
Loading history...
379
            }
380
381 89
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
382
        }
383
384 789
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
385 789
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
386
387
        // Assign an SQL executor to the parser result
388 789
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
389
390 781
        return $this->parserResult;
391
    }
392
393
    /**
394
     * Fixes order of identification variables.
395
     *
396
     * They have to appear in the select clause in the same order as the
397
     * declarations (from ... x join ... y join ... z ...) appear in the query
398
     * as the hydration process relies on that order for proper operation.
399
     *
400
     * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
401
     */
402 795
    private function fixIdentificationVariableOrder($AST)
403
    {
404 795
        if (count($this->identVariableExpressions) <= 1) {
405 620
            return;
406
        }
407
408 180
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
409 180
            if (! isset($this->identVariableExpressions[$dqlAlias])) {
410 8
                continue;
411
            }
412
413 180
            $expr = $this->identVariableExpressions[$dqlAlias];
414 180
            $key  = array_search($expr, $AST->selectClause->selectExpressions, true);
0 ignored issues
show
Bug introduced by
The property selectClause does not seem to exist on Doctrine\ORM\Query\AST\UpdateStatement.
Loading history...
Bug introduced by
The property selectClause does not seem to exist on Doctrine\ORM\Query\AST\DeleteStatement.
Loading history...
415
416 180
            unset($AST->selectClause->selectExpressions[$key]);
417
418 180
            $AST->selectClause->selectExpressions[] = $expr;
419
        }
420 180
    }
421
422
    /**
423
     * Generates a new syntax error.
424
     *
425
     * @param string       $expected Expected string.
426
     * @param mixed[]|null $token    Got token.
427
     *
428
     * @throws QueryException
429
     */
430 18
    public function syntaxError($expected = '', $token = null)
431
    {
432 18
        if ($token === null) {
433 15
            $token = $this->lexer->lookahead;
434
        }
435
436 18
        $tokenPos = $token['position'] ?? '-1';
437
438 18
        $message  = sprintf('line 0, col %d: Error: ', $tokenPos);
439 18
        $message .= ($expected !== '') ? sprintf('Expected %s, got ', $expected) : 'Unexpected ';
440 18
        $message .= ($this->lexer->lookahead === null) ? 'end of string.' : sprintf("'%s'", $token['value']);
0 ignored issues
show
introduced by
The condition $this->lexer->lookahead === null can never be true.
Loading history...
441
442 18
        throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL()));
443
    }
444
445
    /**
446
     * Generates a new semantical error.
447
     *
448
     * @param string       $message Optional message.
449
     * @param mixed[]|null $token   Optional token.
450
     *
451
     * @throws QueryException
452
     */
453 26
    public function semanticalError($message = '', $token = null, ?\Throwable $previousFailure = null)
454
    {
455 26
        if ($token === null) {
456 2
            $token = $this->lexer->lookahead;
457
        }
458
459
        // Minimum exposed chars ahead of token
460 26
        $distance = 12;
461
462
        // Find a position of a final word to display in error string
463 26
        $dql    = $this->query->getDQL();
464 26
        $length = strlen($dql);
465 26
        $pos    = $token['position'] + $distance;
466 26
        $pos    = strpos($dql, ' ', ($length > $pos) ? $pos : $length);
467 26
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
0 ignored issues
show
introduced by
The condition $pos !== false can never be false.
Loading history...
468
469 26
        $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1';
470 26
        $tokenStr = substr($dql, (int) $token['position'], $length);
471
472
        // Building informative message
473 26
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
474
475 26
        throw QueryException::semanticalError(
476 26
            $message,
477 26
            QueryException::dqlError($this->query->getDQL(), $previousFailure)
478
        );
479
    }
480
481
    /**
482
     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
483
     *
484
     * @param bool $resetPeek Reset peek after finding the closing parenthesis.
485
     *
486
     * @return mixed[]
487
     */
488 173
    private function peekBeyondClosingParenthesis($resetPeek = true)
489
    {
490 173
        $token        = $this->lexer->peek();
491 173
        $numUnmatched = 1;
492
493 173
        while ($numUnmatched > 0 && $token !== null) {
494 172
            switch ($token['type']) {
495
                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...
496 44
                    ++$numUnmatched;
497 44
                    break;
498
499
                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...
500 172
                    --$numUnmatched;
501 172
                    break;
502
503
                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...
504
                    // Do nothing
505
            }
506
507 172
            $token = $this->lexer->peek();
508
        }
509
510 173
        if ($resetPeek) {
511 152
            $this->lexer->resetPeek();
512
        }
513
514 173
        return $token;
515
    }
516
517
    /**
518
     * Checks if the given token indicates a mathematical operator.
519
     *
520
     * @param mixed[] $token
521
     *
522
     * @return bool TRUE if the token is a mathematical operator, FALSE otherwise.
523
     */
524 359
    private function isMathOperator($token)
525
    {
526 359
        return in_array($token['type'], [Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY], true);
527
    }
528
529
    /**
530
     * Checks if the next-next (after lookahead) token starts a function.
531
     *
532
     * @return bool TRUE if the next-next tokens start a function, FALSE otherwise.
533
     */
534 402
    private function isFunction()
535
    {
536 402
        $lookaheadType = $this->lexer->lookahead['type'];
537 402
        $peek          = $this->lexer->peek();
538
539 402
        $this->lexer->resetPeek();
540
541 402
        return $lookaheadType >= Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_OPEN_PARENTHESIS;
542
    }
543
544
    /**
545
     * Checks whether the given token type indicates an aggregate function.
546
     *
547
     * @param int $tokenType
548
     *
549
     * @return bool TRUE if the token type is an aggregate function, FALSE otherwise.
550
     */
551 1
    private function isAggregateFunction($tokenType)
552
    {
553 1
        return in_array($tokenType, [Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT], true);
554
    }
555
556
    /**
557
     * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
558
     *
559
     * @return bool
560
     */
561 300
    private function isNextAllAnySome()
562
    {
563 300
        return in_array($this->lexer->lookahead['type'], [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME], true);
564
    }
565
566
    /**
567
     * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
568
     * It must exist in query components list.
569
     */
570 805
    private function processDeferredIdentificationVariables()
571
    {
572 805
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
573 781
            $identVariable = $deferredItem['expression'];
574
575
            // Check if IdentificationVariable exists in queryComponents
576 781
            if (! isset($this->queryComponents[$identVariable])) {
577 1
                $this->semanticalError(
578 1
                    sprintf("'%s' is not defined.", $identVariable),
579 1
                    $deferredItem['token']
580
                );
581
            }
582
583 781
            $qComp = $this->queryComponents[$identVariable];
584
585
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
586 781
            if (! isset($qComp['metadata'])) {
587
                $this->semanticalError(
588
                    sprintf("'%s' does not point to a Class.", $identVariable),
589
                    $deferredItem['token']
590
                );
591
            }
592
593
            // Validate if identification variable nesting level is lower or equal than the current one
594 781
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
595 1
                $this->semanticalError(
596 1
                    sprintf("'%s' is used outside the scope of its declaration.", $identVariable),
597 781
                    $deferredItem['token']
598
                );
599
            }
600
        }
601 803
    }
602
603
    /**
604
     * Validates that the given <tt>NewObjectExpression</tt>.
605
     *
606
     * @param AST\SelectClause $AST
607
     */
608 26
    private function processDeferredNewObjectExpressions($AST)
609
    {
610 26
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
611 26
            $expression    = $deferredItem['expression'];
612 26
            $token         = $deferredItem['token'];
613 26
            $className     = $expression->className;
614 26
            $args          = $expression->args;
615 26
            $fromClassName = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null;
0 ignored issues
show
Bug introduced by
The property fromClause does not seem to exist on Doctrine\ORM\Query\AST\SelectClause.
Loading history...
616
617
            // If the namespace is not given then assumes the first FROM entity namespace
618 26
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
619 10
                $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
620 10
                $fqcn      = $namespace . '\\' . $className;
621
622 10
                if (class_exists($fqcn)) {
623 10
                    $expression->className = $fqcn;
624 10
                    $className             = $fqcn;
625
                }
626
            }
627
628 26
            if (! class_exists($className)) {
629 1
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
630
            }
631
632 25
            $class = new \ReflectionClass($className);
633
634 25
            if (! $class->isInstantiable()) {
635 1
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
636
            }
637
638 24
            if ($class->getConstructor() === null) {
639 1
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
640
            }
641
642 23
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
643 23
                $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
644
            }
645
        }
646 22
    }
647
648
    /**
649
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
650
     * It must exist in query components list.
651
     */
652 9
    private function processDeferredPartialObjectExpressions()
653
    {
654 9
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
655 9
            $expr  = $deferredItem['expression'];
656 9
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
657
658 9
            foreach ($expr->partialFieldSet as $field) {
659 9
                $property = $class->getProperty($field);
660
661 9
                if ($property instanceof FieldMetadata ||
662 9
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
663 9
                    continue;
664
                }
665
666
                $this->semanticalError(
667
                    sprintf("There is no mapped field named '%s' on class %s.", $field, $class->getClassName()),
668
                    $deferredItem['token']
669
                );
670
            }
671
672 9
            if (array_intersect($class->identifier, $expr->partialFieldSet) !== $class->identifier) {
673 1
                $this->semanticalError(
674 1
                    sprintf('The partial field selection of class %s must contain the identifier.', $class->getClassName()),
675 9
                    $deferredItem['token']
676
                );
677
            }
678
        }
679 8
    }
680
681
    /**
682
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
683
     * It must exist in query components list.
684
     */
685 32
    private function processDeferredResultVariables()
686
    {
687 32
        foreach ($this->deferredResultVariables as $deferredItem) {
688 32
            $resultVariable = $deferredItem['expression'];
689
690
            // Check if ResultVariable exists in queryComponents
691 32
            if (! isset($this->queryComponents[$resultVariable])) {
692
                $this->semanticalError(
693
                    sprintf("'%s' is not defined.", $resultVariable),
694
                    $deferredItem['token']
695
                );
696
            }
697
698 32
            $qComp = $this->queryComponents[$resultVariable];
699
700
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
701 32
            if (! isset($qComp['resultVariable'])) {
702
                $this->semanticalError(
703
                    sprintf("'%s' does not point to a ResultVariable.", $resultVariable),
704
                    $deferredItem['token']
705
                );
706
            }
707
708
            // Validate if identification variable nesting level is lower or equal than the current one
709 32
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
710
                $this->semanticalError(
711
                    sprintf("'%s' is used outside the scope of its declaration.", $resultVariable),
712 32
                    $deferredItem['token']
713
                );
714
            }
715
        }
716 32
    }
717
718
    /**
719
     * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
720
     *
721
     * AssociationPathExpression             ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
722
     * SingleValuedPathExpression            ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
723
     * StateFieldPathExpression              ::= IdentificationVariable "." StateField
724
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
725
     * CollectionValuedPathExpression        ::= IdentificationVariable "." CollectionValuedAssociationField
726
     */
727 592
    private function processDeferredPathExpressions()
728
    {
729 592
        foreach ($this->deferredPathExpressions as $deferredItem) {
730 592
            $pathExpression = $deferredItem['expression'];
731
732 592
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
733 592
            $class = $qComp['metadata'];
734 592
            $field = $pathExpression->field;
735
736 592
            if ($field === null) {
737 40
                $field = $pathExpression->field = $class->identifier[0];
738
            }
739
740 592
            $property = $class->getProperty($field);
741
742
            // Check if field or association exists
743 592
            if (! $property) {
744
                $this->semanticalError(
745
                    'Class ' . $class->getClassName() . ' has no field or association named ' . $field,
746
                    $deferredItem['token']
747
                );
748
            }
749
750 592
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
751
752 592
            if ($property instanceof AssociationMetadata) {
753 89
                $fieldType = $property instanceof ToOneAssociationMetadata
754 66
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
755 89
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
756
                ;
757
            }
758
759
            // Validate if PathExpression is one of the expected types
760 592
            $expectedType = $pathExpression->expectedType;
761
762 592
            if (! ($expectedType & $fieldType)) {
763
                // We need to recognize which was expected type(s)
764 2
                $expectedStringTypes = [];
765
766
                // Validate state field type
767 2
                if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
768 1
                    $expectedStringTypes[] = 'StateFieldPathExpression';
769
                }
770
771
                // Validate single valued association (*-to-one)
772 2
                if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
773 2
                    $expectedStringTypes[] = 'SingleValuedAssociationField';
774
                }
775
776
                // Validate single valued association (*-to-many)
777 2
                if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
778
                    $expectedStringTypes[] = 'CollectionValuedAssociationField';
779
                }
780
781
                // Build the error message
782 2
                $semanticalError  = 'Invalid PathExpression. ';
783 2
                $semanticalError .= count($expectedStringTypes) === 1
784 1
                    ? 'Must be a ' . $expectedStringTypes[0] . '.'
785 2
                    : implode(' or ', $expectedStringTypes) . ' expected.';
786
787 2
                $this->semanticalError($semanticalError, $deferredItem['token']);
788
            }
789
790
            // We need to force the type in PathExpression
791 590
            $pathExpression->type = $fieldType;
792
        }
793 590
    }
794
795 796
    private function processRootEntityAliasSelected()
796
    {
797 796
        if (! $this->identVariableExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->identVariableExpressions of type Doctrine\ORM\Query\AST\Node[] 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...
798 236
            return;
799
        }
800
801 570
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
802 570
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
803 570
                return;
804
            }
805
        }
806
807 1
        $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
808
    }
809
810
    /**
811
     * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
812
     *
813
     * @return AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement
814
     */
815 840
    public function QueryLanguage()
816
    {
817 840
        $statement = null;
818
819 840
        $this->lexer->moveNext();
820
821 840
        switch ($this->lexer->lookahead['type']) {
822
            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...
823 775
                $statement = $this->SelectStatement();
824 744
                break;
825
826
            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...
827 31
                $statement = $this->UpdateStatement();
828 31
                break;
829
830
            case Lexer::T_DELETE:
831 42
                $statement = $this->DeleteStatement();
832 42
                break;
833
834
            default:
835 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
836
                break;
837
        }
838
839
        // Check for end of string
840 809
        if ($this->lexer->lookahead !== null) {
841 4
            $this->syntaxError('end of string');
842
        }
843
844 805
        return $statement;
845
    }
846
847
    /**
848
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
849
     *
850
     * @return AST\SelectStatement
851
     */
852 775
    public function SelectStatement()
853
    {
854 775
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
855
856 748
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
857 745
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
858 744
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
859 744
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
860
861 744
        return $selectStatement;
862
    }
863
864
    /**
865
     * UpdateStatement ::= UpdateClause [WhereClause]
866
     *
867
     * @return AST\UpdateStatement
868
     */
869 31
    public function UpdateStatement()
870
    {
871 31
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
872
873 31
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
874
875 31
        return $updateStatement;
876
    }
877
878
    /**
879
     * DeleteStatement ::= DeleteClause [WhereClause]
880
     *
881
     * @return AST\DeleteStatement
882
     */
883 42
    public function DeleteStatement()
884
    {
885 42
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
886
887 42
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
888
889 42
        return $deleteStatement;
890
    }
891
892
    /**
893
     * IdentificationVariable ::= identifier
894
     *
895
     * @return string
896
     */
897 805
    public function IdentificationVariable()
898
    {
899 805
        $this->match(Lexer::T_IDENTIFIER);
900
901 805
        $identVariable = $this->lexer->token['value'];
902
903 805
        $this->deferredIdentificationVariables[] = [
904 805
            'expression'   => $identVariable,
905 805
            'nestingLevel' => $this->nestingLevel,
906 805
            'token'        => $this->lexer->token,
907
        ];
908
909 805
        return $identVariable;
910
    }
911
912
    /**
913
     * AliasIdentificationVariable = identifier
914
     *
915
     * @return string
916
     */
917 815
    public function AliasIdentificationVariable()
918
    {
919 815
        $this->match(Lexer::T_IDENTIFIER);
920
921 815
        $aliasIdentVariable = $this->lexer->token['value'];
922 815
        $exists             = isset($this->queryComponents[$aliasIdentVariable]);
923
924 815
        if ($exists) {
925 2
            $this->semanticalError(sprintf("'%s' is already defined.", $aliasIdentVariable), $this->lexer->token);
926
        }
927
928 815
        return $aliasIdentVariable;
929
    }
930
931
    /**
932
     * AbstractSchemaName ::= fully_qualified_name | identifier
933
     *
934
     * @return string
935
     */
936 828
    public function AbstractSchemaName()
937
    {
938 828
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
939 821
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
940
941 821
            return $this->lexer->token['value'];
942
        }
943
944 18
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
945 17
            $this->match(Lexer::T_IDENTIFIER);
946
947 17
            return $this->lexer->token['value'];
948
        }
949
950 1
        $this->match(Lexer::T_ALIASED_NAME);
951
952
        [$namespaceAlias, $simpleClassName] = explode(':', $this->lexer->token['value']);
953
954
        return $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
0 ignored issues
show
Bug introduced by
The method getEntityNamespace() does not exist on Doctrine\ORM\Configuration. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

954
        return $this->em->getConfiguration()->/** @scrutinizer ignore-call */ getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
955
    }
956
957
    /**
958
     * Validates an AbstractSchemaName, making sure the class exists.
959
     *
960
     * @param string $schemaName The name to validate.
961
     *
962
     * @throws QueryException If the name does not exist.
963
     */
964 824
    private function validateAbstractSchemaName($schemaName) : void
965
    {
966 824
        if (class_exists($schemaName, true) || interface_exists($schemaName, true)) {
967 815
            return;
968
        }
969
970
        try {
971 10
            $this->getEntityManager()->getClassMetadata($schemaName);
972
973 1
            return;
974 9
        } catch (MappingException $mappingException) {
975 9
            $this->semanticalError(
976 9
                sprintf('Class %s could not be mapped', $schemaName),
977 9
                $this->lexer->token
978
            );
979
        }
980
981
        $this->semanticalError(sprintf("Class '%s' is not defined.", $schemaName), $this->lexer->token);
982
    }
983
984
    /**
985
     * AliasResultVariable ::= identifier
986
     *
987
     * @return string
988
     */
989 132
    public function AliasResultVariable()
990
    {
991 132
        $this->match(Lexer::T_IDENTIFIER);
992
993 128
        $resultVariable = $this->lexer->token['value'];
994 128
        $exists         = isset($this->queryComponents[$resultVariable]);
995
996 128
        if ($exists) {
997 2
            $this->semanticalError(sprintf("'%s' is already defined.", $resultVariable), $this->lexer->token);
998
        }
999
1000 128
        return $resultVariable;
1001
    }
1002
1003
    /**
1004
     * ResultVariable ::= identifier
1005
     *
1006
     * @return string
1007
     */
1008 32
    public function ResultVariable()
1009
    {
1010 32
        $this->match(Lexer::T_IDENTIFIER);
1011
1012 32
        $resultVariable = $this->lexer->token['value'];
1013
1014
        // Defer ResultVariable validation
1015 32
        $this->deferredResultVariables[] = [
1016 32
            'expression'   => $resultVariable,
1017 32
            'nestingLevel' => $this->nestingLevel,
1018 32
            'token'        => $this->lexer->token,
1019
        ];
1020
1021 32
        return $resultVariable;
1022
    }
1023
1024
    /**
1025
     * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
1026
     *
1027
     * @return AST\JoinAssociationPathExpression
1028
     */
1029 256
    public function JoinAssociationPathExpression()
1030
    {
1031 256
        $identVariable = $this->IdentificationVariable();
1032
1033 256
        if (! isset($this->queryComponents[$identVariable])) {
1034
            $this->semanticalError(
1035
                'Identification Variable ' . $identVariable . ' used in join path expression but was not defined before.'
1036
            );
1037
        }
1038
1039 256
        $this->match(Lexer::T_DOT);
1040 256
        $this->match(Lexer::T_IDENTIFIER);
1041
1042 256
        $field = $this->lexer->token['value'];
1043
1044
        // Validate association field
1045 256
        $qComp    = $this->queryComponents[$identVariable];
1046 256
        $class    = $qComp['metadata'];
1047 256
        $property = $class->getProperty($field);
1048
1049 256
        if (! ($property !== null && $property instanceof AssociationMetadata)) {
1050
            $this->semanticalError('Class ' . $class->getClassName() . ' has no association named ' . $field);
1051
        }
1052
1053 256
        return new AST\JoinAssociationPathExpression($identVariable, $field);
1054
    }
1055
1056
    /**
1057
     * Parses an arbitrary path expression and defers semantical validation
1058
     * based on expected types.
1059
     *
1060
     * PathExpression ::= IdentificationVariable {"." identifier}*
1061
     *
1062
     * @param int $expectedTypes
1063
     *
1064
     * @return AST\PathExpression
1065
     */
1066 602
    public function PathExpression($expectedTypes)
1067
    {
1068 602
        $identVariable = $this->IdentificationVariable();
1069 602
        $field         = null;
1070
1071 602
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1072 596
            $this->match(Lexer::T_DOT);
1073 596
            $this->match(Lexer::T_IDENTIFIER);
1074
1075 596
            $field = $this->lexer->token['value'];
1076
1077 596
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1078
                $this->match(Lexer::T_DOT);
1079
                $this->match(Lexer::T_IDENTIFIER);
1080
                $field .= '.' . $this->lexer->token['value'];
1081
            }
1082
        }
1083
1084
        // Creating AST node
1085 602
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1086
1087
        // Defer PathExpression validation if requested to be deferred
1088 602
        $this->deferredPathExpressions[] = [
1089 602
            'expression'   => $pathExpr,
1090 602
            'nestingLevel' => $this->nestingLevel,
1091 602
            'token'        => $this->lexer->token,
1092
        ];
1093
1094 602
        return $pathExpr;
1095
    }
1096
1097
    /**
1098
     * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
1099
     *
1100
     * @return AST\PathExpression
1101
     */
1102
    public function AssociationPathExpression()
1103
    {
1104
        return $this->PathExpression(
1105
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
1106
            AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
1107
        );
1108
    }
1109
1110
    /**
1111
     * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
1112
     *
1113
     * @return AST\PathExpression
1114
     */
1115 511
    public function SingleValuedPathExpression()
1116
    {
1117 511
        return $this->PathExpression(
1118 511
            AST\PathExpression::TYPE_STATE_FIELD |
1119 511
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1120
        );
1121
    }
1122
1123
    /**
1124
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1125
     *
1126
     * @return AST\PathExpression
1127
     */
1128 205
    public function StateFieldPathExpression()
1129
    {
1130 205
        return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
1131
    }
1132
1133
    /**
1134
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
1135
     *
1136
     * @return AST\PathExpression
1137
     */
1138 9
    public function SingleValuedAssociationPathExpression()
1139
    {
1140 9
        return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
1141
    }
1142
1143
    /**
1144
     * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
1145
     *
1146
     * @return AST\PathExpression
1147
     */
1148 23
    public function CollectionValuedPathExpression()
1149
    {
1150 23
        return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
1151
    }
1152
1153
    /**
1154
     * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
1155
     *
1156
     * @return AST\SelectClause
1157
     */
1158 775
    public function SelectClause()
1159
    {
1160 775
        $isDistinct = false;
1161 775
        $this->match(Lexer::T_SELECT);
1162
1163
        // Check for DISTINCT
1164 775
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1165 6
            $this->match(Lexer::T_DISTINCT);
1166
1167 6
            $isDistinct = true;
1168
        }
1169
1170
        // Process SelectExpressions (1..N)
1171 775
        $selectExpressions   = [];
1172 775
        $selectExpressions[] = $this->SelectExpression();
1173
1174 767
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1175 298
            $this->match(Lexer::T_COMMA);
1176
1177 298
            $selectExpressions[] = $this->SelectExpression();
1178
        }
1179
1180 766
        return new AST\SelectClause($selectExpressions, $isDistinct);
1181
    }
1182
1183
    /**
1184
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1185
     *
1186
     * @return AST\SimpleSelectClause
1187
     */
1188 49
    public function SimpleSelectClause()
1189
    {
1190 49
        $isDistinct = false;
1191 49
        $this->match(Lexer::T_SELECT);
1192
1193 49
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1194
            $this->match(Lexer::T_DISTINCT);
1195
1196
            $isDistinct = true;
1197
        }
1198
1199 49
        return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1200
    }
1201
1202
    /**
1203
     * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1204
     *
1205
     * @return AST\UpdateClause
1206
     */
1207 31
    public function UpdateClause()
1208
    {
1209 31
        $this->match(Lexer::T_UPDATE);
1210
1211 31
        $token              = $this->lexer->lookahead;
1212 31
        $abstractSchemaName = $this->AbstractSchemaName();
1213
1214 31
        $this->validateAbstractSchemaName($abstractSchemaName);
1215
1216 31
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1217 2
            $this->match(Lexer::T_AS);
1218
        }
1219
1220 31
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1221
1222 31
        $class = $this->em->getClassMetadata($abstractSchemaName);
1223
1224
        // Building queryComponent
1225
        $queryComponent = [
1226 31
            'metadata'     => $class,
1227
            'parent'       => null,
1228
            'relation'     => null,
1229
            'map'          => null,
1230 31
            'nestingLevel' => $this->nestingLevel,
1231 31
            'token'        => $token,
1232
        ];
1233
1234 31
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1235
1236 31
        $this->match(Lexer::T_SET);
1237
1238 31
        $updateItems   = [];
1239 31
        $updateItems[] = $this->UpdateItem();
1240
1241 31
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1242 4
            $this->match(Lexer::T_COMMA);
1243
1244 4
            $updateItems[] = $this->UpdateItem();
1245
        }
1246
1247 31
        $updateClause                              = new AST\UpdateClause($abstractSchemaName, $updateItems);
1248 31
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1249
1250 31
        return $updateClause;
1251
    }
1252
1253
    /**
1254
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1255
     *
1256
     * @return AST\DeleteClause
1257
     */
1258 42
    public function DeleteClause()
1259
    {
1260 42
        $this->match(Lexer::T_DELETE);
1261
1262 42
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1263 9
            $this->match(Lexer::T_FROM);
1264
        }
1265
1266 42
        $token              = $this->lexer->lookahead;
1267 42
        $abstractSchemaName = $this->AbstractSchemaName();
1268
1269 42
        $this->validateAbstractSchemaName($abstractSchemaName);
1270
1271 42
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1272
1273 42
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1274 1
            $this->match(Lexer::T_AS);
1275
        }
1276
1277 42
        $aliasIdentificationVariable = $this->lexer->isNextToken(Lexer::T_IDENTIFIER)
1278 40
            ? $this->AliasIdentificationVariable()
1279 42
            : 'alias_should_have_been_set';
1280
1281 42
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1282 42
        $class                                     = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1283
1284
        // Building queryComponent
1285
        $queryComponent = [
1286 42
            'metadata'     => $class,
1287
            'parent'       => null,
1288
            'relation'     => null,
1289
            'map'          => null,
1290 42
            'nestingLevel' => $this->nestingLevel,
1291 42
            'token'        => $token,
1292
        ];
1293
1294 42
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1295
1296 42
        return $deleteClause;
1297
    }
1298
1299
    /**
1300
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1301
     *
1302
     * @return AST\FromClause
1303
     */
1304 766
    public function FromClause()
1305
    {
1306 766
        $this->match(Lexer::T_FROM);
1307
1308 761
        $identificationVariableDeclarations   = [];
1309 761
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1310
1311 748
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1312 7
            $this->match(Lexer::T_COMMA);
1313
1314 7
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1315
        }
1316
1317 748
        return new AST\FromClause($identificationVariableDeclarations);
1318
    }
1319
1320
    /**
1321
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1322
     *
1323
     * @return AST\SubselectFromClause
1324
     */
1325 49
    public function SubselectFromClause()
1326
    {
1327 49
        $this->match(Lexer::T_FROM);
1328
1329 49
        $identificationVariables   = [];
1330 49
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1331
1332 48
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1333
            $this->match(Lexer::T_COMMA);
1334
1335
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1336
        }
1337
1338 48
        return new AST\SubselectFromClause($identificationVariables);
1339
    }
1340
1341
    /**
1342
     * WhereClause ::= "WHERE" ConditionalExpression
1343
     *
1344
     * @return AST\WhereClause
1345
     */
1346 340
    public function WhereClause()
1347
    {
1348 340
        $this->match(Lexer::T_WHERE);
1349
1350 340
        return new AST\WhereClause($this->ConditionalExpression());
1351
    }
1352
1353
    /**
1354
     * HavingClause ::= "HAVING" ConditionalExpression
1355
     *
1356
     * @return AST\HavingClause
1357
     */
1358 21
    public function HavingClause()
1359
    {
1360 21
        $this->match(Lexer::T_HAVING);
1361
1362 21
        return new AST\HavingClause($this->ConditionalExpression());
1363
    }
1364
1365
    /**
1366
     * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1367
     *
1368
     * @return AST\GroupByClause
1369
     */
1370 33
    public function GroupByClause()
1371
    {
1372 33
        $this->match(Lexer::T_GROUP);
1373 33
        $this->match(Lexer::T_BY);
1374
1375 33
        $groupByItems = [$this->GroupByItem()];
1376
1377 32
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1378 8
            $this->match(Lexer::T_COMMA);
1379
1380 8
            $groupByItems[] = $this->GroupByItem();
1381
        }
1382
1383 32
        return new AST\GroupByClause($groupByItems);
1384
    }
1385
1386
    /**
1387
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1388
     *
1389
     * @return AST\OrderByClause
1390
     */
1391 180
    public function OrderByClause()
1392
    {
1393 180
        $this->match(Lexer::T_ORDER);
1394 180
        $this->match(Lexer::T_BY);
1395
1396 180
        $orderByItems   = [];
1397 180
        $orderByItems[] = $this->OrderByItem();
1398
1399 180
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1400 15
            $this->match(Lexer::T_COMMA);
1401
1402 15
            $orderByItems[] = $this->OrderByItem();
1403
        }
1404
1405 180
        return new AST\OrderByClause($orderByItems);
1406
    }
1407
1408
    /**
1409
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1410
     *
1411
     * @return AST\Subselect
1412
     */
1413 49
    public function Subselect()
1414
    {
1415
        // Increase query nesting level
1416 49
        $this->nestingLevel++;
1417
1418 49
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1419
1420 48
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1421 48
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1422 48
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1423 48
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1424
1425
        // Decrease query nesting level
1426 48
        $this->nestingLevel--;
1427
1428 48
        return $subselect;
1429
    }
1430
1431
    /**
1432
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1433
     *
1434
     * @return AST\UpdateItem
1435
     */
1436 31
    public function UpdateItem()
1437
    {
1438 31
        $pathExpr = $this->SingleValuedPathExpression();
1439
1440 31
        $this->match(Lexer::T_EQUALS);
1441
1442 31
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1443
1444 31
        return $updateItem;
1445
    }
1446
1447
    /**
1448
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1449
     *
1450
     * @return string|AST\PathExpression
1451
     */
1452 33
    public function GroupByItem()
1453
    {
1454
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1455 33
        $glimpse = $this->lexer->glimpse();
1456
1457 33
        if ($glimpse['type'] === Lexer::T_DOT) {
1458 14
            return $this->SingleValuedPathExpression();
1459
        }
1460
1461
        // Still need to decide between IdentificationVariable or ResultVariable
1462 19
        $lookaheadValue = $this->lexer->lookahead['value'];
1463
1464 19
        if (! isset($this->queryComponents[$lookaheadValue])) {
1465 1
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1466
        }
1467
1468 18
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1469 16
            ? $this->IdentificationVariable()
1470 18
            : $this->ResultVariable();
1471
    }
1472
1473
    /**
1474
     * OrderByItem ::= (
1475
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1476
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1477
     * ) ["ASC" | "DESC"]
1478
     *
1479
     * @return AST\OrderByItem
1480
     */
1481 180
    public function OrderByItem()
1482
    {
1483 180
        $this->lexer->peek(); // lookahead => '.'
1484 180
        $this->lexer->peek(); // lookahead => token after '.'
1485
1486 180
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1487
1488 180
        $this->lexer->resetPeek();
1489
1490 180
        $glimpse = $this->lexer->glimpse();
1491
1492
        switch (true) {
1493 180
            case ($this->isFunction()):
1494 2
                $expr = $this->FunctionDeclaration();
1495 2
                break;
1496
1497 178
            case ($this->isMathOperator($peek)):
1498 25
                $expr = $this->SimpleArithmeticExpression();
1499 25
                break;
1500
1501 154
            case ($glimpse['type'] === Lexer::T_DOT):
1502 139
                $expr = $this->SingleValuedPathExpression();
1503 139
                break;
1504
1505 19
            case ($this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis())):
1506 2
                $expr = $this->ScalarExpression();
1507 2
                break;
1508
1509
            default:
1510 17
                $expr = $this->ResultVariable();
1511 17
                break;
1512
        }
1513
1514 180
        $type = 'ASC';
1515 180
        $item = new AST\OrderByItem($expr);
1516
1517
        switch (true) {
1518 180
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1519 95
                $this->match(Lexer::T_DESC);
1520 95
                $type = 'DESC';
1521 95
                break;
1522
1523 152
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1524 97
                $this->match(Lexer::T_ASC);
1525 97
                break;
1526
1527
            default:
1528
                // Do nothing
1529
        }
1530
1531 180
        $item->type = $type;
1532
1533 180
        return $item;
1534
    }
1535
1536
    /**
1537
     * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1538
     *      EnumPrimary | SimpleEntityExpression | "NULL"
1539
     *
1540
     * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1541
     * grammar that needs to be supported:
1542
     *
1543
     * NewValue ::= SimpleArithmeticExpression | "NULL"
1544
     *
1545
     * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1546
     *
1547
     * @return AST\ArithmeticExpression
1548
     */
1549 31
    public function NewValue()
1550
    {
1551 31
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1552 1
            $this->match(Lexer::T_NULL);
1553
1554 1
            return null;
1555
        }
1556
1557 30
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1558 17
            $this->match(Lexer::T_INPUT_PARAMETER);
1559
1560 17
            return new AST\InputParameter($this->lexer->token['value']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return new Doctrine\ORM\...>lexer->token['value']) returns the type Doctrine\ORM\Query\AST\InputParameter which is incompatible with the documented return type Doctrine\ORM\Query\AST\ArithmeticExpression.
Loading history...
1561
        }
1562
1563 13
        return $this->ArithmeticExpression();
1564
    }
1565
1566
    /**
1567
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1568
     *
1569
     * @return AST\IdentificationVariableDeclaration
1570
     */
1571 763
    public function IdentificationVariableDeclaration()
1572
    {
1573 763
        $joins                    = [];
1574 763
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1575 753
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1576 7
            ? $this->IndexBy()
1577 753
            : null;
1578
1579 753
        $rangeVariableDeclaration->isRoot = true;
1580
1581 753
        while ($this->lexer->isNextToken(Lexer::T_LEFT) ||
1582 753
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1583 753
            $this->lexer->isNextToken(Lexer::T_JOIN)
1584
        ) {
1585 278
            $joins[] = $this->Join();
1586
        }
1587
1588 750
        return new AST\IdentificationVariableDeclaration(
1589 750
            $rangeVariableDeclaration,
1590 750
            $indexBy,
1591 750
            $joins
1592
        );
1593
    }
1594
1595
    /**
1596
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1597
     *
1598
     * {@internal WARNING: Solution is harder than a bare implementation.
1599
     * Desired EBNF support:
1600
     *
1601
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1602
     *
1603
     * It demands that entire SQL generation to become programmatical. This is
1604
     * needed because association based subselect requires "WHERE" conditional
1605
     * expressions to be injected, but there is no scope to do that. Only scope
1606
     * accessible is "FROM", prohibiting an easy implementation without larger
1607
     * changes. }}
1608
     *
1609
     * @return AST\SubselectIdentificationVariableDeclaration|AST\IdentificationVariableDeclaration
1610
     */
1611 49
    public function SubselectIdentificationVariableDeclaration()
1612
    {
1613
        /*
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...
1614
        NOT YET IMPLEMENTED!
1615
1616
        $glimpse = $this->lexer->glimpse();
1617
1618
        if ($glimpse['type'] == Lexer::T_DOT) {
1619
            $associationPathExpression = $this->AssociationPathExpression();
1620
1621
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1622
                $this->match(Lexer::T_AS);
1623
            }
1624
1625
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1626
            $identificationVariable      = $associationPathExpression->identificationVariable;
1627
            $field                       = $associationPathExpression->associationField;
1628
1629
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1630
            $association = $class->getProperty($field);
1631
            $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
1632
1633
            // Building queryComponent
1634
            $joinQueryComponent = array(
1635
                'metadata'     => $targetClass,
1636
                'parent'       => $identificationVariable,
1637
                'relation'     => $association,
1638
                'map'          => null,
1639
                'nestingLevel' => $this->nestingLevel,
1640
                'token'        => $this->lexer->lookahead
1641
            );
1642
1643
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1644
1645
            return new AST\SubselectIdentificationVariableDeclaration(
1646
                $associationPathExpression, $aliasIdentificationVariable
1647
            );
1648
        }
1649
        */
1650
1651 49
        return $this->IdentificationVariableDeclaration();
1652
    }
1653
1654
    /**
1655
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1656
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1657
     *          ["WITH" ConditionalExpression]
1658
     *
1659
     * @return AST\Join
1660
     */
1661 278
    public function Join()
1662
    {
1663
        // Check Join type
1664 278
        $joinType = AST\Join::JOIN_TYPE_INNER;
1665
1666
        switch (true) {
1667 278
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1668 68
                $this->match(Lexer::T_LEFT);
1669
1670 68
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1671
1672
                // Possible LEFT OUTER join
1673 68
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1674
                    $this->match(Lexer::T_OUTER);
1675
1676
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1677
                }
1678 68
                break;
1679
1680 213
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1681 21
                $this->match(Lexer::T_INNER);
1682 21
                break;
1683
1684
            default:
1685
                // Do nothing
1686
        }
1687
1688 278
        $this->match(Lexer::T_JOIN);
1689
1690 278
        $next            = $this->lexer->glimpse();
1691 278
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1692 275
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1693 275
        $join            = new AST\Join($joinType, $joinDeclaration);
1694
1695
        // Describe non-root join declaration
1696 275
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1697 22
            $joinDeclaration->isRoot = false;
1698
        }
1699
1700
        // Check for ad-hoc Join conditions
1701 275
        if ($adhocConditions) {
1702 25
            $this->match(Lexer::T_WITH);
1703
1704 25
            $join->conditionalExpression = $this->ConditionalExpression();
1705
        }
1706
1707 275
        return $join;
1708
    }
1709
1710
    /**
1711
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1712
     *
1713
     * @return AST\RangeVariableDeclaration
1714
     *
1715
     * @throws QueryException
1716
     */
1717 763
    public function RangeVariableDeclaration()
1718
    {
1719 763
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()['type'] === Lexer::T_SELECT) {
1720 2
            $this->semanticalError('Subquery is not supported here', $this->lexer->token);
1721
        }
1722
1723 762
        $abstractSchemaName = $this->AbstractSchemaName();
1724
1725 761
        $this->validateAbstractSchemaName($abstractSchemaName);
1726
1727 753
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1728 6
            $this->match(Lexer::T_AS);
1729
        }
1730
1731 753
        $token                       = $this->lexer->lookahead;
1732 753
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1733 753
        $classMetadata               = $this->em->getClassMetadata($abstractSchemaName);
1734
1735
        // Building queryComponent
1736
        $queryComponent = [
1737 753
            'metadata'     => $classMetadata,
1738
            'parent'       => null,
1739
            'relation'     => null,
1740
            'map'          => null,
1741 753
            'nestingLevel' => $this->nestingLevel,
1742 753
            'token'        => $token,
1743
        ];
1744
1745 753
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1746
1747 753
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1748
    }
1749
1750
    /**
1751
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1752
     *
1753
     * @return AST\JoinAssociationPathExpression
1754
     */
1755 256
    public function JoinAssociationDeclaration()
1756
    {
1757 256
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1758
1759 256
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1760 5
            $this->match(Lexer::T_AS);
1761
        }
1762
1763 256
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1764 254
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1765
1766 254
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1767 254
        $field                  = $joinAssociationPathExpression->associationField;
1768
1769 254
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1770 254
        $association = $class->getProperty($field);
1771 254
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
1772
1773
        // Building queryComponent
1774
        $joinQueryComponent = [
1775 254
            'metadata'     => $targetClass,
1776 254
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1777 254
            'relation'     => $association,
1778
            'map'          => null,
1779 254
            'nestingLevel' => $this->nestingLevel,
1780 254
            'token'        => $this->lexer->lookahead,
1781
        ];
1782
1783 254
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1784
1785 254
        return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
0 ignored issues
show
Bug Best Practice introduced by
The expression return new Doctrine\ORM\...tionVariable, $indexBy) returns the type Doctrine\ORM\Query\AST\JoinAssociationDeclaration which is incompatible with the documented return type Doctrine\ORM\Query\AST\J...sociationPathExpression.
Loading history...
1786
    }
1787
1788
    /**
1789
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1790
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1791
     *
1792
     * @return AST\PartialObjectExpression
1793
     */
1794 9
    public function PartialObjectExpression()
1795
    {
1796 9
        $this->match(Lexer::T_PARTIAL);
1797
1798 9
        $partialFieldSet = [];
1799
1800 9
        $identificationVariable = $this->IdentificationVariable();
1801
1802 9
        $this->match(Lexer::T_DOT);
1803 9
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1804 9
        $this->match(Lexer::T_IDENTIFIER);
1805
1806 9
        $field = $this->lexer->token['value'];
1807
1808
        // First field in partial expression might be embeddable property
1809 9
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1810
            $this->match(Lexer::T_DOT);
1811
            $this->match(Lexer::T_IDENTIFIER);
1812
            $field .= '.' . $this->lexer->token['value'];
1813
        }
1814
1815 9
        $partialFieldSet[] = $field;
1816
1817 9
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1818 7
            $this->match(Lexer::T_COMMA);
1819 7
            $this->match(Lexer::T_IDENTIFIER);
1820
1821 7
            $field = $this->lexer->token['value'];
1822
1823 7
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1824
                $this->match(Lexer::T_DOT);
1825
                $this->match(Lexer::T_IDENTIFIER);
1826
                $field .= '.' . $this->lexer->token['value'];
1827
            }
1828
1829 7
            $partialFieldSet[] = $field;
1830
        }
1831
1832 9
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1833
1834 9
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1835
1836
        // Defer PartialObjectExpression validation
1837 9
        $this->deferredPartialObjectExpressions[] = [
1838 9
            'expression'   => $partialObjectExpression,
1839 9
            'nestingLevel' => $this->nestingLevel,
1840 9
            'token'        => $this->lexer->token,
1841
        ];
1842
1843 9
        return $partialObjectExpression;
1844
    }
1845
1846
    /**
1847
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1848
     *
1849
     * @return AST\NewObjectExpression
1850
     */
1851 26
    public function NewObjectExpression()
1852
    {
1853 26
        $this->match(Lexer::T_NEW);
1854
1855 26
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1856 26
        $token     = $this->lexer->token;
1857
1858 26
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1859
1860 26
        $args[] = $this->NewObjectArg();
0 ignored issues
show
Comprehensibility Best Practice introduced by
$args was never initialized. Although not strictly required by PHP, it is generally a good practice to add $args = array(); before regardless.
Loading history...
1861
1862 26
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1863 22
            $this->match(Lexer::T_COMMA);
1864
1865 22
            $args[] = $this->NewObjectArg();
1866
        }
1867
1868 26
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1869
1870 26
        $expression = new AST\NewObjectExpression($className, $args);
1871
1872
        // Defer NewObjectExpression validation
1873 26
        $this->deferredNewObjectExpressions[] = [
1874 26
            'token'        => $token,
1875 26
            'expression'   => $expression,
1876 26
            'nestingLevel' => $this->nestingLevel,
1877
        ];
1878
1879 26
        return $expression;
1880
    }
1881
1882
    /**
1883
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1884
     *
1885
     * @return mixed
1886
     */
1887 26
    public function NewObjectArg()
1888
    {
1889 26
        $token = $this->lexer->lookahead;
1890 26
        $peek  = $this->lexer->glimpse();
1891
1892 26
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1893 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1894 2
            $expression = $this->Subselect();
1895 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1896
1897 2
            return $expression;
1898
        }
1899
1900 26
        return $this->ScalarExpression();
1901
    }
1902
1903
    /**
1904
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1905
     *
1906
     * @return AST\IndexBy
1907
     */
1908 11
    public function IndexBy()
1909
    {
1910 11
        $this->match(Lexer::T_INDEX);
1911 11
        $this->match(Lexer::T_BY);
1912 11
        $pathExpr = $this->StateFieldPathExpression();
1913
1914
        // Add the INDEX BY info to the query component
1915 11
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1916
1917 11
        return new AST\IndexBy($pathExpr);
1918
    }
1919
1920
    /**
1921
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1922
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1923
     *                      InstanceOfExpression
1924
     *
1925
     * @return mixed One of the possible expressions or subexpressions.
1926
     */
1927 163
    public function ScalarExpression()
1928
    {
1929 163
        $lookahead = $this->lexer->lookahead['type'];
1930 163
        $peek      = $this->lexer->glimpse();
1931
1932 163
        switch (true) {
1933
            case ($lookahead === Lexer::T_INTEGER):
1934 160
            case ($lookahead === Lexer::T_FLOAT):
1935
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1936 160
            case ($lookahead === Lexer::T_MINUS):
1937 160
            case ($lookahead === Lexer::T_PLUS):
1938 17
                return $this->SimpleArithmeticExpression();
1939
1940 160
            case ($lookahead === Lexer::T_STRING):
1941 13
                return $this->StringPrimary();
1942
1943 158
            case ($lookahead === Lexer::T_TRUE):
1944 158
            case ($lookahead === Lexer::T_FALSE):
1945 3
                $this->match($lookahead);
1946
1947 3
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1948
1949 158
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
1950
                switch (true) {
1951 1
                    case $this->isMathOperator($peek):
1952
                        // :param + u.value
1953 1
                        return $this->SimpleArithmeticExpression();
1954
                    default:
1955
                        return $this->InputParameter();
1956
                }
1957
                // cannot get here
1958
1959 158
            case ($lookahead === Lexer::T_CASE):
1960 154
            case ($lookahead === Lexer::T_COALESCE):
1961 154
            case ($lookahead === Lexer::T_NULLIF):
1962
                // Since NULLIF and COALESCE can be identified as a function,
1963
                // we need to check these before checking for FunctionDeclaration
1964 8
                return $this->CaseExpression();
1965
1966 154
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1967 4
                return $this->SimpleArithmeticExpression();
1968
1969
            // this check must be done before checking for a filed path expression
1970 151
            case ($this->isFunction()):
1971 27
                $this->lexer->peek(); // "("
1972
1973
                switch (true) {
1974 27
                    case ($this->isMathOperator($this->peekBeyondClosingParenthesis())):
1975
                        // SUM(u.id) + COUNT(u.id)
1976 7
                        return $this->SimpleArithmeticExpression();
1977
1978
                    default:
1979
                        // IDENTITY(u)
1980 22
                        return $this->FunctionDeclaration();
1981
                }
1982
1983
                break;
1984
            // it is no function, so it must be a field path
1985 132
            case ($lookahead === Lexer::T_IDENTIFIER):
1986 132
                $this->lexer->peek(); // lookahead => '.'
1987 132
                $this->lexer->peek(); // lookahead => token after '.'
1988 132
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1989 132
                $this->lexer->resetPeek();
1990
1991 132
                if ($this->isMathOperator($peek)) {
1992 7
                    return $this->SimpleArithmeticExpression();
1993
                }
1994
1995 127
                return $this->StateFieldPathExpression();
1996
1997
            default:
1998
                $this->syntaxError();
1999
        }
2000
    }
2001
2002
    /**
2003
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
2004
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2005
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2006
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2007
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2008
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2009
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2010
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2011
     *
2012
     * @return mixed One of the possible expressions or subexpressions.
2013
     */
2014 19
    public function CaseExpression()
2015
    {
2016 19
        $lookahead = $this->lexer->lookahead['type'];
2017
2018 19
        switch ($lookahead) {
2019
            case Lexer::T_NULLIF:
2020 5
                return $this->NullIfExpression();
2021
2022
            case Lexer::T_COALESCE:
2023 2
                return $this->CoalesceExpression();
2024
2025
            case Lexer::T_CASE:
2026 14
                $this->lexer->resetPeek();
2027 14
                $peek = $this->lexer->peek();
2028
2029 14
                if ($peek['type'] === Lexer::T_WHEN) {
2030 9
                    return $this->GeneralCaseExpression();
2031
                }
2032
2033 5
                return $this->SimpleCaseExpression();
2034
2035
            default:
2036
                // Do nothing
2037
                break;
2038
        }
2039
2040
        $this->syntaxError();
2041
    }
2042
2043
    /**
2044
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2045
     *
2046
     * @return AST\CoalesceExpression
2047
     */
2048 3
    public function CoalesceExpression()
2049
    {
2050 3
        $this->match(Lexer::T_COALESCE);
2051 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2052
2053
        // Process ScalarExpressions (1..N)
2054 3
        $scalarExpressions   = [];
2055 3
        $scalarExpressions[] = $this->ScalarExpression();
2056
2057 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2058 3
            $this->match(Lexer::T_COMMA);
2059
2060 3
            $scalarExpressions[] = $this->ScalarExpression();
2061
        }
2062
2063 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2064
2065 3
        return new AST\CoalesceExpression($scalarExpressions);
2066
    }
2067
2068
    /**
2069
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2070
     *
2071
     * @return AST\NullIfExpression
2072
     */
2073 5
    public function NullIfExpression()
2074
    {
2075 5
        $this->match(Lexer::T_NULLIF);
2076 5
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2077
2078 5
        $firstExpression = $this->ScalarExpression();
2079 5
        $this->match(Lexer::T_COMMA);
2080 5
        $secondExpression = $this->ScalarExpression();
2081
2082 5
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2083
2084 5
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2085
    }
2086
2087
    /**
2088
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2089
     *
2090
     * @return AST\GeneralCaseExpression
2091
     */
2092 9
    public function GeneralCaseExpression()
2093
    {
2094 9
        $this->match(Lexer::T_CASE);
2095
2096
        // Process WhenClause (1..N)
2097 9
        $whenClauses = [];
2098
2099
        do {
2100 9
            $whenClauses[] = $this->WhenClause();
2101 9
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2102
2103 9
        $this->match(Lexer::T_ELSE);
2104 9
        $scalarExpression = $this->ScalarExpression();
2105 9
        $this->match(Lexer::T_END);
2106
2107 9
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2108
    }
2109
2110
    /**
2111
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2112
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2113
     *
2114
     * @return AST\SimpleCaseExpression
2115
     */
2116 5
    public function SimpleCaseExpression()
2117
    {
2118 5
        $this->match(Lexer::T_CASE);
2119 5
        $caseOperand = $this->StateFieldPathExpression();
2120
2121
        // Process SimpleWhenClause (1..N)
2122 5
        $simpleWhenClauses = [];
2123
2124
        do {
2125 5
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2126 5
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2127
2128 5
        $this->match(Lexer::T_ELSE);
2129 5
        $scalarExpression = $this->ScalarExpression();
2130 5
        $this->match(Lexer::T_END);
2131
2132 5
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2133
    }
2134
2135
    /**
2136
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2137
     *
2138
     * @return AST\WhenClause
2139
     */
2140 9
    public function WhenClause()
2141
    {
2142 9
        $this->match(Lexer::T_WHEN);
2143 9
        $conditionalExpression = $this->ConditionalExpression();
2144 9
        $this->match(Lexer::T_THEN);
2145
2146 9
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2147
    }
2148
2149
    /**
2150
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2151
     *
2152
     * @return AST\SimpleWhenClause
2153
     */
2154 5
    public function SimpleWhenClause()
2155
    {
2156 5
        $this->match(Lexer::T_WHEN);
2157 5
        $conditionalExpression = $this->ScalarExpression();
2158 5
        $this->match(Lexer::T_THEN);
2159
2160 5
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2161
    }
2162
2163
    /**
2164
     * SelectExpression ::= (
2165
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2166
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2167
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2168
     *
2169
     * @return AST\SelectExpression
2170
     */
2171 775
    public function SelectExpression()
2172
    {
2173 775
        $expression    = null;
2174 775
        $identVariable = null;
2175 775
        $peek          = $this->lexer->glimpse();
2176 775
        $lookaheadType = $this->lexer->lookahead['type'];
2177
2178 775
        switch (true) {
2179
            // ScalarExpression (u.name)
2180 699
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2181 106
                $expression = $this->ScalarExpression();
2182 106
                break;
2183
2184
            // IdentificationVariable (u)
2185 715
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2186 588
                $expression = $identVariable = $this->IdentificationVariable();
2187 588
                break;
2188
2189
            // 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...
2190 189
            case ($lookaheadType === Lexer::T_CASE):
2191 184
            case ($lookaheadType === Lexer::T_COALESCE):
2192 182
            case ($lookaheadType === Lexer::T_NULLIF):
2193 9
                $expression = $this->CaseExpression();
2194 9
                break;
2195
2196
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2197 180
            case ($this->isFunction()):
2198 104
                $this->lexer->peek(); // "("
2199
2200
                switch (true) {
2201 104
                    case ($this->isMathOperator($this->peekBeyondClosingParenthesis())):
2202
                        // SUM(u.id) + COUNT(u.id)
2203 2
                        $expression = $this->ScalarExpression();
2204 2
                        break;
2205
2206
                    default:
2207
                        // IDENTITY(u)
2208 102
                        $expression = $this->FunctionDeclaration();
2209 102
                        break;
2210
                }
2211
2212 104
                break;
2213
2214
            // PartialObjectExpression (PARTIAL u.{id, name})
2215 77
            case ($lookaheadType === Lexer::T_PARTIAL):
2216 9
                $expression    = $this->PartialObjectExpression();
2217 9
                $identVariable = $expression->identificationVariable;
2218 9
                break;
2219
2220
            // Subselect
2221 68
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2222 23
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2223 23
                $expression = $this->Subselect();
2224 23
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2225 23
                break;
2226
2227
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2228 45
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2229 41
            case ($lookaheadType === Lexer::T_INTEGER):
2230 39
            case ($lookaheadType === Lexer::T_STRING):
2231 30
            case ($lookaheadType === Lexer::T_FLOAT):
2232
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2233 30
            case ($lookaheadType === Lexer::T_MINUS):
2234 30
            case ($lookaheadType === Lexer::T_PLUS):
2235 16
                $expression = $this->SimpleArithmeticExpression();
2236 16
                break;
2237
2238
            // 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...
2239 29
            case ($lookaheadType === Lexer::T_NEW):
2240 26
                $expression = $this->NewObjectExpression();
2241 26
                break;
2242
2243
            default:
2244 3
                $this->syntaxError(
2245 3
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2246 3
                    $this->lexer->lookahead
2247
                );
2248
        }
2249
2250
        // [["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...
2251 772
        $mustHaveAliasResultVariable = false;
2252
2253 772
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2254 123
            $this->match(Lexer::T_AS);
2255
2256 123
            $mustHaveAliasResultVariable = true;
2257
        }
2258
2259 772
        $hiddenAliasResultVariable = false;
2260
2261 772
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2262 10
            $this->match(Lexer::T_HIDDEN);
2263
2264 10
            $hiddenAliasResultVariable = true;
2265
        }
2266
2267 772
        $aliasResultVariable = null;
2268
2269 772
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2270 132
            $token               = $this->lexer->lookahead;
2271 132
            $aliasResultVariable = $this->AliasResultVariable();
2272
2273
            // Include AliasResultVariable in query components.
2274 127
            $this->queryComponents[$aliasResultVariable] = [
2275 127
                'resultVariable' => $expression,
2276 127
                'nestingLevel'   => $this->nestingLevel,
2277 127
                'token'          => $token,
2278
            ];
2279
        }
2280
2281
        // AST
2282
2283 767
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2284
2285 767
        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...
2286 595
            $this->identVariableExpressions[$identVariable] = $expr;
2287
        }
2288
2289 767
        return $expr;
2290
    }
2291
2292
    /**
2293
     * SimpleSelectExpression ::= (
2294
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2295
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2296
     * ) [["AS"] AliasResultVariable]
2297
     *
2298
     * @return AST\SimpleSelectExpression
2299
     */
2300 49
    public function SimpleSelectExpression()
2301
    {
2302 49
        $peek = $this->lexer->glimpse();
2303
2304 49
        switch ($this->lexer->lookahead['type']) {
2305
            case Lexer::T_IDENTIFIER:
2306 19
                switch (true) {
2307 19
                    case ($peek['type'] === Lexer::T_DOT):
2308 16
                        $expression = $this->StateFieldPathExpression();
2309
2310 16
                        return new AST\SimpleSelectExpression($expression);
2311
2312 3
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2313 2
                        $expression = $this->IdentificationVariable();
2314
2315 2
                        return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
$expression of type string is incompatible with the type Doctrine\ORM\Query\AST\Node expected by parameter $expression of Doctrine\ORM\Query\AST\S...pression::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2315
                        return new AST\SimpleSelectExpression(/** @scrutinizer ignore-type */ $expression);
Loading history...
2316
2317 1
                    case ($this->isFunction()):
2318
                        // SUM(u.id) + COUNT(u.id)
2319 1
                        if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) {
2320
                            return new AST\SimpleSelectExpression($this->ScalarExpression());
2321
                        }
2322
                        // COUNT(u.id)
2323 1
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2324
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2325
                        }
2326
                        // IDENTITY(u)
2327 1
                        return new AST\SimpleSelectExpression($this->FunctionDeclaration());
2328
2329
                    default:
2330
                        // Do nothing
2331
                }
2332
                break;
2333
2334
            case Lexer::T_OPEN_PARENTHESIS:
2335 3
                if ($peek['type'] !== Lexer::T_SELECT) {
2336
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2337 3
                    $expression = $this->SimpleArithmeticExpression();
2338
2339 3
                    return new AST\SimpleSelectExpression($expression);
2340
                }
2341
2342
                // Subselect
2343
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2344
                $expression = $this->Subselect();
2345
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2346
2347
                return new AST\SimpleSelectExpression($expression);
2348
2349
            default:
2350
                // Do nothing
2351
        }
2352
2353 28
        $this->lexer->peek();
2354
2355 28
        $expression = $this->ScalarExpression();
2356 28
        $expr       = new AST\SimpleSelectExpression($expression);
2357
2358 28
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2359 1
            $this->match(Lexer::T_AS);
2360
        }
2361
2362 28
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2363 2
            $token                             = $this->lexer->lookahead;
2364 2
            $resultVariable                    = $this->AliasResultVariable();
2365 2
            $expr->fieldIdentificationVariable = $resultVariable;
2366
2367
            // Include AliasResultVariable in query components.
2368 2
            $this->queryComponents[$resultVariable] = [
2369 2
                'resultvariable' => $expr,
2370 2
                'nestingLevel'   => $this->nestingLevel,
2371 2
                'token'          => $token,
2372
            ];
2373
        }
2374
2375 28
        return $expr;
2376
    }
2377
2378
    /**
2379
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2380
     *
2381
     * @return AST\ConditionalExpression
2382
     */
2383 384
    public function ConditionalExpression()
2384
    {
2385 384
        $conditionalTerms   = [];
2386 384
        $conditionalTerms[] = $this->ConditionalTerm();
2387
2388 381
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2389 16
            $this->match(Lexer::T_OR);
2390
2391 16
            $conditionalTerms[] = $this->ConditionalTerm();
2392
        }
2393
2394
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2395
        // if only one AST\ConditionalTerm is defined
2396 381
        if (count($conditionalTerms) === 1) {
2397 373
            return $conditionalTerms[0];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $conditionalTerms[0] returns the type Doctrine\ORM\Query\AST\ConditionalTerm which is incompatible with the documented return type Doctrine\ORM\Query\AST\ConditionalExpression.
Loading history...
2398
        }
2399
2400 16
        return new AST\ConditionalExpression($conditionalTerms);
2401
    }
2402
2403
    /**
2404
     * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2405
     *
2406
     * @return AST\ConditionalTerm
2407
     */
2408 384
    public function ConditionalTerm()
2409
    {
2410 384
        $conditionalFactors   = [];
2411 384
        $conditionalFactors[] = $this->ConditionalFactor();
2412
2413 381
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2414 32
            $this->match(Lexer::T_AND);
2415
2416 32
            $conditionalFactors[] = $this->ConditionalFactor();
2417
        }
2418
2419
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2420
        // if only one AST\ConditionalFactor is defined
2421 381
        if (count($conditionalFactors) === 1) {
2422 362
            return $conditionalFactors[0];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $conditionalFactors[0] returns the type Doctrine\ORM\Query\AST\ConditionalFactor which is incompatible with the documented return type Doctrine\ORM\Query\AST\ConditionalTerm.
Loading history...
2423
        }
2424
2425 32
        return new AST\ConditionalTerm($conditionalFactors);
2426
    }
2427
2428
    /**
2429
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2430
     *
2431
     * @return AST\ConditionalFactor
2432
     */
2433 384
    public function ConditionalFactor()
2434
    {
2435 384
        $not = false;
2436
2437 384
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2438 6
            $this->match(Lexer::T_NOT);
2439
2440 6
            $not = true;
2441
        }
2442
2443 384
        $conditionalPrimary = $this->ConditionalPrimary();
2444
2445
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2446
        // if only one AST\ConditionalPrimary is defined
2447 381
        if (! $not) {
2448 379
            return $conditionalPrimary;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $conditionalPrimary returns the type Doctrine\ORM\Query\AST\ConditionalPrimary which is incompatible with the documented return type Doctrine\ORM\Query\AST\ConditionalFactor.
Loading history...
2449
        }
2450
2451 6
        $conditionalFactor      = new AST\ConditionalFactor($conditionalPrimary);
2452 6
        $conditionalFactor->not = $not;
2453
2454 6
        return $conditionalFactor;
2455
    }
2456
2457
    /**
2458
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2459
     *
2460
     * @return AST\ConditionalPrimary
2461
     */
2462 384
    public function ConditionalPrimary()
2463
    {
2464 384
        $condPrimary = new AST\ConditionalPrimary();
2465
2466 384
        if (! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2467 375
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2468
2469 372
            return $condPrimary;
2470
        }
2471
2472
        // Peek beyond the matching closing parenthesis ')'
2473 25
        $peek = $this->peekBeyondClosingParenthesis();
2474
2475 25
        if (in_array($peek['value'], ['=', '<', '<=', '<>', '>', '>=', '!='], true) ||
2476 22
            in_array($peek['type'], [Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS], true) ||
2477 25
            $this->isMathOperator($peek)) {
2478 15
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2479
2480 15
            return $condPrimary;
2481
        }
2482
2483 21
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2484 21
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2485 21
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2486
2487 21
        return $condPrimary;
2488
    }
2489
2490
    /**
2491
     * SimpleConditionalExpression ::=
2492
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2493
     *      InExpression | NullComparisonExpression | ExistsExpression |
2494
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2495
     *      InstanceOfExpression
2496
     */
2497 384
    public function SimpleConditionalExpression()
2498
    {
2499 384
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2500 7
            return $this->ExistsExpression();
2501
        }
2502
2503 384
        $token     = $this->lexer->lookahead;
2504 384
        $peek      = $this->lexer->glimpse();
2505 384
        $lookahead = $token;
2506
2507 384
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2508
            $token = $this->lexer->glimpse();
2509
        }
2510
2511 384
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2512
            // Peek beyond the matching closing parenthesis.
2513 360
            $beyond = $this->lexer->peek();
2514
2515 360
            switch ($peek['value']) {
2516 360
                case '(':
2517
                    // Peeks beyond the matched closing parenthesis.
2518 37
                    $token = $this->peekBeyondClosingParenthesis(false);
2519
2520 37
                    if ($token['type'] === Lexer::T_NOT) {
2521 3
                        $token = $this->lexer->peek();
2522
                    }
2523
2524 37
                    if ($token['type'] === Lexer::T_IS) {
2525 2
                        $lookahead = $this->lexer->peek();
2526
                    }
2527 37
                    break;
2528
2529
                default:
2530
                    // Peek beyond the PathExpression or InputParameter.
2531 332
                    $token = $beyond;
2532
2533 332
                    while ($token['value'] === '.') {
2534 287
                        $this->lexer->peek();
2535
2536 287
                        $token = $this->lexer->peek();
2537
                    }
2538
2539
                    // Also peek beyond a NOT if there is one.
2540 332
                    if ($token['type'] === Lexer::T_NOT) {
2541 11
                        $token = $this->lexer->peek();
2542
                    }
2543
2544
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2545 332
                    $lookahead = $this->lexer->peek();
2546
            }
2547
2548
            // Also peek beyond a NOT if there is one.
2549 360
            if ($lookahead['type'] === Lexer::T_NOT) {
2550 7
                $lookahead = $this->lexer->peek();
2551
            }
2552
2553 360
            $this->lexer->resetPeek();
2554
        }
2555
2556 384
        if ($token['type'] === Lexer::T_BETWEEN) {
2557 8
            return $this->BetweenExpression();
2558
        }
2559
2560 378
        if ($token['type'] === Lexer::T_LIKE) {
2561 14
            return $this->LikeExpression();
2562
        }
2563
2564 365
        if ($token['type'] === Lexer::T_IN) {
2565 35
            return $this->InExpression();
2566
        }
2567
2568 339
        if ($token['type'] === Lexer::T_INSTANCE) {
2569 17
            return $this->InstanceOfExpression();
2570
        }
2571
2572 322
        if ($token['type'] === Lexer::T_MEMBER) {
2573 8
            return $this->CollectionMemberExpression();
2574
        }
2575
2576 314
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2577 13
            return $this->NullComparisonExpression();
2578
        }
2579
2580 304
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_EMPTY) {
2581 4
            return $this->EmptyCollectionComparisonExpression();
2582
        }
2583
2584 300
        return $this->ComparisonExpression();
2585
    }
2586
2587
    /**
2588
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2589
     *
2590
     * @return AST\EmptyCollectionComparisonExpression
2591
     */
2592 4
    public function EmptyCollectionComparisonExpression()
2593
    {
2594 4
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2595 4
            $this->CollectionValuedPathExpression()
2596
        );
2597 4
        $this->match(Lexer::T_IS);
2598
2599 4
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2600 2
            $this->match(Lexer::T_NOT);
2601 2
            $emptyCollectionCompExpr->not = true;
2602
        }
2603
2604 4
        $this->match(Lexer::T_EMPTY);
2605
2606 4
        return $emptyCollectionCompExpr;
2607
    }
2608
2609
    /**
2610
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2611
     *
2612
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2613
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2614
     *
2615
     * @return AST\CollectionMemberExpression
2616
     */
2617 8
    public function CollectionMemberExpression()
2618
    {
2619 8
        $not        = false;
2620 8
        $entityExpr = $this->EntityExpression();
2621
2622 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2623
            $this->match(Lexer::T_NOT);
2624
2625
            $not = true;
2626
        }
2627
2628 8
        $this->match(Lexer::T_MEMBER);
2629
2630 8
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2631 8
            $this->match(Lexer::T_OF);
2632
        }
2633
2634 8
        $collMemberExpr      = new AST\CollectionMemberExpression(
2635 8
            $entityExpr,
2636 8
            $this->CollectionValuedPathExpression()
2637
        );
2638 8
        $collMemberExpr->not = $not;
2639
2640 8
        return $collMemberExpr;
2641
    }
2642
2643
    /**
2644
     * Literal ::= string | char | integer | float | boolean
2645
     *
2646
     * @return AST\Literal
2647
     */
2648 187
    public function Literal()
2649
    {
2650 187
        switch ($this->lexer->lookahead['type']) {
2651
            case Lexer::T_STRING:
2652 48
                $this->match(Lexer::T_STRING);
2653
2654 48
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2655
            case Lexer::T_INTEGER:
2656
            case Lexer::T_FLOAT:
2657 140
                $this->match(
2658 140
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2659
                );
2660
2661 140
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2662
            case Lexer::T_TRUE:
2663
            case Lexer::T_FALSE:
2664 8
                $this->match(
2665 8
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2666
                );
2667
2668 8
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2669
            default:
2670
                $this->syntaxError('Literal');
2671
        }
2672
    }
2673
2674
    /**
2675
     * InParameter ::= Literal | InputParameter
2676
     *
2677
     * @return string|AST\InputParameter
2678
     */
2679 26
    public function InParameter()
2680
    {
2681 26
        if ($this->lexer->lookahead['type'] === Lexer::T_INPUT_PARAMETER) {
2682 14
            return $this->InputParameter();
2683
        }
2684
2685 12
        return $this->Literal();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->Literal() returns the type Doctrine\ORM\Query\AST\Literal which is incompatible with the documented return type string|Doctrine\ORM\Query\AST\InputParameter.
Loading history...
2686
    }
2687
2688
    /**
2689
     * InputParameter ::= PositionalParameter | NamedParameter
2690
     *
2691
     * @return AST\InputParameter
2692
     */
2693 168
    public function InputParameter()
2694
    {
2695 168
        $this->match(Lexer::T_INPUT_PARAMETER);
2696
2697 168
        return new AST\InputParameter($this->lexer->token['value']);
2698
    }
2699
2700
    /**
2701
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2702
     *
2703
     * @return AST\ArithmeticExpression
2704
     */
2705 334
    public function ArithmeticExpression()
2706
    {
2707 334
        $expr = new AST\ArithmeticExpression();
2708
2709 334
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2710 19
            $peek = $this->lexer->glimpse();
2711
2712 19
            if ($peek['type'] === Lexer::T_SELECT) {
2713 7
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2714 7
                $expr->subselect = $this->Subselect();
2715 7
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2716
2717 7
                return $expr;
2718
            }
2719
        }
2720
2721 334
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2722
2723 334
        return $expr;
2724
    }
2725
2726
    /**
2727
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2728
     *
2729
     * @return AST\SimpleArithmeticExpression
2730
     */
2731 439
    public function SimpleArithmeticExpression()
2732
    {
2733 439
        $terms   = [];
2734 439
        $terms[] = $this->ArithmeticTerm();
2735
2736 439
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2737 21
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2738
2739 21
            $terms[] = $this->lexer->token['value'];
2740 21
            $terms[] = $this->ArithmeticTerm();
2741
        }
2742
2743
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2744
        // if only one AST\ArithmeticTerm is defined
2745 439
        if (count($terms) === 1) {
2746 434
            return $terms[0];
2747
        }
2748
2749 21
        return new AST\SimpleArithmeticExpression($terms);
2750
    }
2751
2752
    /**
2753
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2754
     *
2755
     * @return AST\ArithmeticTerm
2756
     */
2757 439
    public function ArithmeticTerm()
2758
    {
2759 439
        $factors   = [];
2760 439
        $factors[] = $this->ArithmeticFactor();
2761
2762 439
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2763 53
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2764
2765 53
            $factors[] = $this->lexer->token['value'];
2766 53
            $factors[] = $this->ArithmeticFactor();
2767
        }
2768
2769
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2770
        // if only one AST\ArithmeticFactor is defined
2771 439
        if (count($factors) === 1) {
2772 411
            return $factors[0];
2773
        }
2774
2775 53
        return new AST\ArithmeticTerm($factors);
2776
    }
2777
2778
    /**
2779
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2780
     *
2781
     * @return AST\ArithmeticFactor
2782
     */
2783 439
    public function ArithmeticFactor()
2784
    {
2785 439
        $sign   = null;
2786 439
        $isPlus = $this->lexer->isNextToken(Lexer::T_PLUS);
2787
2788 439
        if ($isPlus || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2789 3
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2790 3
            $sign = $isPlus;
2791
        }
2792
2793 439
        $primary = $this->ArithmeticPrimary();
2794
2795
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2796
        // if only one AST\ArithmeticPrimary is defined
2797 439
        if ($sign === null) {
2798 438
            return $primary;
2799
        }
2800
2801 3
        return new AST\ArithmeticFactor($primary, $sign);
2802
    }
2803
2804
    /**
2805
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2806
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2807
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2808
     *          | InputParameter | CaseExpression
2809
     */
2810 454
    public function ArithmeticPrimary()
2811
    {
2812 454
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2813 25
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2814
2815 25
            $expr = $this->SimpleArithmeticExpression();
2816
2817 25
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2818
2819 25
            return new AST\ParenthesisExpression($expr);
2820
        }
2821
2822 454
        switch ($this->lexer->lookahead['type']) {
2823
            case Lexer::T_COALESCE:
2824
            case Lexer::T_NULLIF:
2825
            case Lexer::T_CASE:
2826 4
                return $this->CaseExpression();
2827
2828
            case Lexer::T_IDENTIFIER:
2829 424
                $peek = $this->lexer->glimpse();
2830
2831 424
                if ($peek['value'] === '(') {
2832 43
                    return $this->FunctionDeclaration();
2833
                }
2834
2835 393
                if ($peek['value'] === '.') {
2836 382
                    return $this->SingleValuedPathExpression();
2837
                }
2838
2839 46
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2840 10
                    return $this->ResultVariable();
2841
                }
2842
2843 38
                return $this->StateFieldPathExpression();
2844
2845
            case Lexer::T_INPUT_PARAMETER:
2846 149
                return $this->InputParameter();
2847
2848
            default:
2849 181
                $peek = $this->lexer->glimpse();
2850
2851 181
                if ($peek['value'] === '(') {
2852 18
                    return $this->FunctionDeclaration();
2853
                }
2854
2855 177
                return $this->Literal();
2856
        }
2857
    }
2858
2859
    /**
2860
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2861
     *
2862
     * @return AST\Subselect|string
2863
     */
2864 14
    public function StringExpression()
2865
    {
2866 14
        $peek = $this->lexer->glimpse();
2867
2868
        // Subselect
2869 14
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
2870
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2871
            $expr = $this->Subselect();
2872
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2873
2874
            return $expr;
2875
        }
2876
2877
        // ResultVariable (string)
2878 14
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2879 14
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2880 2
            return $this->ResultVariable();
2881
        }
2882
2883 12
        return $this->StringPrimary();
2884
    }
2885
2886
    /**
2887
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2888
     */
2889 65
    public function StringPrimary()
2890
    {
2891 65
        $lookaheadType = $this->lexer->lookahead['type'];
2892
2893 65
        switch ($lookaheadType) {
2894
            case Lexer::T_IDENTIFIER:
2895 35
                $peek = $this->lexer->glimpse();
2896
2897 35
                if ($peek['value'] === '.') {
2898 35
                    return $this->StateFieldPathExpression();
2899
                }
2900
2901 8
                if ($peek['value'] === '(') {
2902
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2903 8
                    return $this->FunctionDeclaration();
2904
                }
2905
2906
                $this->syntaxError("'.' or '('");
2907
                break;
2908
2909
            case Lexer::T_STRING:
2910 47
                $this->match(Lexer::T_STRING);
2911
2912 47
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2913
2914
            case Lexer::T_INPUT_PARAMETER:
2915 2
                return $this->InputParameter();
2916
2917
            case Lexer::T_CASE:
2918
            case Lexer::T_COALESCE:
2919
            case Lexer::T_NULLIF:
2920
                return $this->CaseExpression();
2921
        }
2922
2923
        $this->syntaxError(
2924
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2925
        );
2926
    }
2927
2928
    /**
2929
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2930
     *
2931
     * @return AST\PathExpression
2932
     */
2933 8
    public function EntityExpression()
2934
    {
2935 8
        $glimpse = $this->lexer->glimpse();
2936
2937 8
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2938 1
            return $this->SingleValuedAssociationPathExpression();
2939
        }
2940
2941 7
        return $this->SimpleEntityExpression();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->SimpleEntityExpression() returns the type Doctrine\ORM\Query\AST\InputParameter which is incompatible with the documented return type Doctrine\ORM\Query\AST\PathExpression.
Loading history...
2942
    }
2943
2944
    /**
2945
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2946
     *
2947
     * @return string|AST\InputParameter
2948
     */
2949 7
    public function SimpleEntityExpression()
2950
    {
2951 7
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2952 5
            return $this->InputParameter();
2953
        }
2954
2955 2
        return $this->StateFieldPathExpression();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->StateFieldPathExpression() returns the type Doctrine\ORM\Query\AST\PathExpression which is incompatible with the documented return type string|Doctrine\ORM\Query\AST\InputParameter.
Loading history...
2956
    }
2957
2958
    /**
2959
     * AggregateExpression ::=
2960
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2961
     *
2962
     * @return AST\AggregateExpression
2963
     */
2964 88
    public function AggregateExpression()
2965
    {
2966 88
        $lookaheadType = $this->lexer->lookahead['type'];
2967 88
        $isDistinct    = false;
2968
2969 88
        if (! in_array($lookaheadType, [Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM], true)) {
2970
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2971
        }
2972
2973 88
        $this->match($lookaheadType);
2974 88
        $functionName = $this->lexer->token['value'];
2975 88
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2976
2977 88
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
2978 3
            $this->match(Lexer::T_DISTINCT);
2979 3
            $isDistinct = true;
2980
        }
2981
2982 88
        $pathExp = $this->SimpleArithmeticExpression();
2983
2984 88
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2985
2986 88
        return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
2987
    }
2988
2989
    /**
2990
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
2991
     *
2992
     * @return AST\QuantifiedExpression
2993
     */
2994 3
    public function QuantifiedExpression()
2995
    {
2996 3
        $lookaheadType = $this->lexer->lookahead['type'];
2997 3
        $value         = $this->lexer->lookahead['value'];
2998
2999 3
        if (! in_array($lookaheadType, [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME], true)) {
3000
            $this->syntaxError('ALL, ANY or SOME');
3001
        }
3002
3003 3
        $this->match($lookaheadType);
3004 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3005
3006 3
        $qExpr       = new AST\QuantifiedExpression($this->Subselect());
3007 3
        $qExpr->type = $value;
3008
3009 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3010
3011 3
        return $qExpr;
3012
    }
3013
3014
    /**
3015
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3016
     *
3017
     * @return AST\BetweenExpression
3018
     */
3019 8
    public function BetweenExpression()
3020
    {
3021 8
        $not        = false;
3022 8
        $arithExpr1 = $this->ArithmeticExpression();
3023
3024 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3025 3
            $this->match(Lexer::T_NOT);
3026 3
            $not = true;
3027
        }
3028
3029 8
        $this->match(Lexer::T_BETWEEN);
3030 8
        $arithExpr2 = $this->ArithmeticExpression();
3031 8
        $this->match(Lexer::T_AND);
3032 8
        $arithExpr3 = $this->ArithmeticExpression();
3033
3034 8
        $betweenExpr      = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3035 8
        $betweenExpr->not = $not;
3036
3037 8
        return $betweenExpr;
3038
    }
3039
3040
    /**
3041
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3042
     *
3043
     * @return AST\ComparisonExpression
3044
     */
3045 300
    public function ComparisonExpression()
3046
    {
3047 300
        $this->lexer->glimpse();
3048
3049 300
        $leftExpr  = $this->ArithmeticExpression();
3050 300
        $operator  = $this->ComparisonOperator();
3051 300
        $rightExpr = ($this->isNextAllAnySome())
3052 3
            ? $this->QuantifiedExpression()
3053 300
            : $this->ArithmeticExpression();
3054
3055 298
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3056
    }
3057
3058
    /**
3059
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3060
     *
3061
     * @return AST\InExpression
3062
     */
3063 35
    public function InExpression()
3064
    {
3065 35
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3066
3067 35
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3068 6
            $this->match(Lexer::T_NOT);
3069 6
            $inExpression->not = true;
3070
        }
3071
3072 35
        $this->match(Lexer::T_IN);
3073 35
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3074
3075 35
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3076 9
            $inExpression->subselect = $this->Subselect();
3077
        } else {
3078 26
            $literals   = [];
3079 26
            $literals[] = $this->InParameter();
3080
3081 26
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3082 16
                $this->match(Lexer::T_COMMA);
3083 16
                $literals[] = $this->InParameter();
3084
            }
3085
3086 26
            $inExpression->literals = $literals;
0 ignored issues
show
Documentation Bug introduced by
It seems like $literals of type Doctrine\ORM\Query\AST\InputParameter[] is incompatible with the declared type Doctrine\ORM\Query\AST\Literal[] of property $literals.

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...
3087
        }
3088
3089 34
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3090
3091 34
        return $inExpression;
3092
    }
3093
3094
    /**
3095
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3096
     *
3097
     * @return AST\InstanceOfExpression
3098
     */
3099 17
    public function InstanceOfExpression()
3100
    {
3101 17
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3102
3103 17
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3104 1
            $this->match(Lexer::T_NOT);
3105 1
            $instanceOfExpression->not = true;
3106
        }
3107
3108 17
        $this->match(Lexer::T_INSTANCE);
3109 17
        $this->match(Lexer::T_OF);
3110
3111 17
        $exprValues = [];
3112
3113 17
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3114 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3115
3116 2
            $exprValues[] = $this->InstanceOfParameter();
3117
3118 2
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3119 2
                $this->match(Lexer::T_COMMA);
3120
3121 2
                $exprValues[] = $this->InstanceOfParameter();
3122
            }
3123
3124 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3125
3126 2
            $instanceOfExpression->value = $exprValues;
3127
3128 2
            return $instanceOfExpression;
3129
        }
3130
3131 15
        $exprValues[] = $this->InstanceOfParameter();
3132
3133 15
        $instanceOfExpression->value = $exprValues;
3134
3135 15
        return $instanceOfExpression;
3136
    }
3137
3138
    /**
3139
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3140
     *
3141
     * @return mixed
3142
     */
3143 17
    public function InstanceOfParameter()
3144
    {
3145 17
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3146 6
            $this->match(Lexer::T_INPUT_PARAMETER);
3147
3148 6
            return new AST\InputParameter($this->lexer->token['value']);
3149
        }
3150
3151 11
        $abstractSchemaName = $this->AbstractSchemaName();
3152
3153 11
        $this->validateAbstractSchemaName($abstractSchemaName);
3154
3155 11
        return $abstractSchemaName;
3156
    }
3157
3158
    /**
3159
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3160
     *
3161
     * @return AST\LikeExpression
3162
     */
3163 14
    public function LikeExpression()
3164
    {
3165 14
        $stringExpr = $this->StringExpression();
3166 14
        $not        = false;
3167
3168 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3169 3
            $this->match(Lexer::T_NOT);
3170 3
            $not = true;
3171
        }
3172
3173 14
        $this->match(Lexer::T_LIKE);
3174
3175 14
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3176 3
            $this->match(Lexer::T_INPUT_PARAMETER);
3177 3
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3178
        } else {
3179 12
            $stringPattern = $this->StringPrimary();
3180
        }
3181
3182 14
        $escapeChar = null;
3183
3184 14
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3185 2
            $this->match(Lexer::T_ESCAPE);
3186 2
            $this->match(Lexer::T_STRING);
3187
3188 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3189
        }
3190
3191 14
        $likeExpr      = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $stringExpr can also be of type string; however, parameter $stringExpression of Doctrine\ORM\Query\AST\L...pression::__construct() does only seem to accept Doctrine\ORM\Query\AST\Node, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3191
        $likeExpr      = new AST\LikeExpression(/** @scrutinizer ignore-type */ $stringExpr, $stringPattern, $escapeChar);
Loading history...
3192 14
        $likeExpr->not = $not;
3193
3194 14
        return $likeExpr;
3195
    }
3196
3197
    /**
3198
     * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
3199
     *
3200
     * @return AST\NullComparisonExpression
3201
     */
3202 13
    public function NullComparisonExpression()
3203
    {
3204
        switch (true) {
3205 13
            case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER):
3206
                $this->match(Lexer::T_INPUT_PARAMETER);
3207
3208
                $expr = new AST\InputParameter($this->lexer->token['value']);
3209
                break;
3210
3211 13
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3212 1
                $expr = $this->NullIfExpression();
3213 1
                break;
3214
3215 13
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3216 1
                $expr = $this->CoalesceExpression();
3217 1
                break;
3218
3219 13
            case $this->isFunction():
3220 2
                $expr = $this->FunctionDeclaration();
3221 2
                break;
3222
3223
            default:
3224
                // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
3225 12
                $glimpse = $this->lexer->glimpse();
3226
3227 12
                if ($glimpse['type'] === Lexer::T_DOT) {
3228 8
                    $expr = $this->SingleValuedPathExpression();
3229
3230
                    // Leave switch statement
3231 8
                    break;
3232
                }
3233
3234 4
                $lookaheadValue = $this->lexer->lookahead['value'];
3235
3236
                // Validate existing component
3237 4
                if (! isset($this->queryComponents[$lookaheadValue])) {
3238
                    $this->semanticalError('Cannot add having condition on undefined result variable.');
3239
                }
3240
3241
                // Validate SingleValuedPathExpression (ie.: "product")
3242 4
                if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {
3243 1
                    $expr = $this->SingleValuedPathExpression();
3244 1
                    break;
3245
                }
3246
3247
                // Validating ResultVariable
3248 3
                if (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3249
                    $this->semanticalError('Cannot add having condition on a non result variable.');
3250
                }
3251
3252 3
                $expr = $this->ResultVariable();
3253 3
                break;
3254
        }
3255
3256 13
        $nullCompExpr = new AST\NullComparisonExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr can also be of type string; however, parameter $expression of Doctrine\ORM\Query\AST\N...pression::__construct() does only seem to accept Doctrine\ORM\Query\AST\Node, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3256
        $nullCompExpr = new AST\NullComparisonExpression(/** @scrutinizer ignore-type */ $expr);
Loading history...
3257
3258 13
        $this->match(Lexer::T_IS);
3259
3260 13
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3261 5
            $this->match(Lexer::T_NOT);
3262
3263 5
            $nullCompExpr->not = true;
3264
        }
3265
3266 13
        $this->match(Lexer::T_NULL);
3267
3268 13
        return $nullCompExpr;
3269
    }
3270
3271
    /**
3272
     * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"
3273
     *
3274
     * @return AST\ExistsExpression
3275
     */
3276 7
    public function ExistsExpression()
3277
    {
3278 7
        $not = false;
3279
3280 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3281
            $this->match(Lexer::T_NOT);
3282
            $not = true;
3283
        }
3284
3285 7
        $this->match(Lexer::T_EXISTS);
3286 7
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3287
3288 7
        $existsExpression      = new AST\ExistsExpression($this->Subselect());
3289 7
        $existsExpression->not = $not;
3290
3291 7
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3292
3293 7
        return $existsExpression;
3294
    }
3295
3296
    /**
3297
     * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!="
3298
     *
3299
     * @return string
3300
     */
3301 300
    public function ComparisonOperator()
3302
    {
3303 300
        switch ($this->lexer->lookahead['value']) {
3304 300
            case '=':
3305 249
                $this->match(Lexer::T_EQUALS);
3306
3307 249
                return '=';
3308
3309 62
            case '<':
3310 17
                $this->match(Lexer::T_LOWER_THAN);
3311 17
                $operator = '<';
3312
3313 17
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3314 5
                    $this->match(Lexer::T_EQUALS);
3315 5
                    $operator .= '=';
3316 12
                } elseif ($this->lexer->isNextToken(Lexer::T_GREATER_THAN)) {
3317 3
                    $this->match(Lexer::T_GREATER_THAN);
3318 3
                    $operator .= '>';
3319
                }
3320
3321 17
                return $operator;
3322
3323 53
            case '>':
3324 47
                $this->match(Lexer::T_GREATER_THAN);
3325 47
                $operator = '>';
3326
3327 47
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3328 6
                    $this->match(Lexer::T_EQUALS);
3329 6
                    $operator .= '=';
3330
                }
3331
3332 47
                return $operator;
3333
3334 6
            case '!':
3335 6
                $this->match(Lexer::T_NEGATE);
3336 6
                $this->match(Lexer::T_EQUALS);
3337
3338 6
                return '<>';
3339
3340
            default:
3341
                $this->syntaxError('=, <, <=, <>, >, >=, !=');
3342
        }
3343
    }
3344
3345
    /**
3346
     * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
3347
     *
3348
     * @return Functions\FunctionNode
3349
     */
3350 158
    public function FunctionDeclaration()
3351
    {
3352 158
        $token    = $this->lexer->lookahead;
3353 158
        $funcName = strtolower($token['value']);
3354
3355 158
        $customFunctionDeclaration = $this->CustomFunctionDeclaration();
3356
3357
        // Check for custom functions functions first!
3358 158
        switch (true) {
3359
            case $customFunctionDeclaration !== null:
3360 4
                return $customFunctionDeclaration;
3361
3362 154
            case (isset(self::$_STRING_FUNCTIONS[$funcName])):
3363 33
                return $this->FunctionsReturningStrings();
3364
3365 126
            case (isset(self::$_NUMERIC_FUNCTIONS[$funcName])):
3366 109
                return $this->FunctionsReturningNumerics();
3367
3368 18
            case (isset(self::$_DATETIME_FUNCTIONS[$funcName])):
3369 18
                return $this->FunctionsReturningDatetime();
3370
3371
            default:
3372
                $this->syntaxError('known function', $token);
3373
        }
3374
    }
3375
3376
    /**
3377
     * Helper function for FunctionDeclaration grammar rule.
3378
     *
3379
     * @return Functions\FunctionNode
3380
     */
3381 158
    private function CustomFunctionDeclaration()
3382
    {
3383 158
        $token    = $this->lexer->lookahead;
3384 158
        $funcName = strtolower($token['value']);
3385
3386
        // Check for custom functions afterwards
3387 158
        $config = $this->em->getConfiguration();
3388
3389 158
        switch (true) {
3390 158
            case ($config->getCustomStringFunction($funcName) !== null):
3391 3
                return $this->CustomFunctionsReturningStrings();
3392
3393 156
            case ($config->getCustomNumericFunction($funcName) !== null):
3394 2
                return $this->CustomFunctionsReturningNumerics();
3395
3396 154
            case ($config->getCustomDatetimeFunction($funcName) !== null):
3397
                return $this->CustomFunctionsReturningDatetime();
3398
3399
            default:
3400 154
                return null;
3401
        }
3402
    }
3403
3404
    /**
3405
     * FunctionsReturningNumerics ::=
3406
     *      "LENGTH" "(" StringPrimary ")" |
3407
     *      "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" |
3408
     *      "ABS" "(" SimpleArithmeticExpression ")" |
3409
     *      "SQRT" "(" SimpleArithmeticExpression ")" |
3410
     *      "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3411
     *      "SIZE" "(" CollectionValuedPathExpression ")" |
3412
     *      "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3413
     *      "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3414
     *      "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
3415
     *
3416
     * @return Functions\FunctionNode
3417
     */
3418 109
    public function FunctionsReturningNumerics()
3419
    {
3420 109
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3421 109
        $funcClass     = self::$_NUMERIC_FUNCTIONS[$funcNameLower];
3422
3423 109
        $function = new $funcClass($funcNameLower);
3424 109
        $function->parse($this);
3425
3426 109
        return $function;
3427
    }
3428
3429
    /**
3430
     * @return Functions\FunctionNode
3431
     */
3432 2
    public function CustomFunctionsReturningNumerics()
3433
    {
3434
        // getCustomNumericFunction is case-insensitive
3435 2
        $functionName  = strtolower($this->lexer->lookahead['value']);
3436 2
        $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName);
3437
3438 2
        $function = is_string($functionClass)
3439 1
            ? new $functionClass($functionName)
3440 2
            : call_user_func($functionClass, $functionName);
3441
3442 2
        $function->parse($this);
3443
3444 2
        return $function;
3445
    }
3446
3447
    /**
3448
     * FunctionsReturningDateTime ::=
3449
     *     "CURRENT_DATE" |
3450
     *     "CURRENT_TIME" |
3451
     *     "CURRENT_TIMESTAMP" |
3452
     *     "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" |
3453
     *     "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"
3454
     *
3455
     * @return Functions\FunctionNode
3456
     */
3457 18
    public function FunctionsReturningDatetime()
3458
    {
3459 18
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3460 18
        $funcClass     = self::$_DATETIME_FUNCTIONS[$funcNameLower];
3461
3462 18
        $function = new $funcClass($funcNameLower);
3463 18
        $function->parse($this);
3464
3465 18
        return $function;
3466
    }
3467
3468
    /**
3469
     * @return Functions\FunctionNode
3470
     */
3471
    public function CustomFunctionsReturningDatetime()
3472
    {
3473
        // getCustomDatetimeFunction is case-insensitive
3474
        $functionName  = $this->lexer->lookahead['value'];
3475
        $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName);
3476
3477
        $function = is_string($functionClass)
3478
            ? new $functionClass($functionName)
3479
            : call_user_func($functionClass, $functionName);
3480
3481
        $function->parse($this);
3482
3483
        return $function;
3484
    }
3485
3486
    /**
3487
     * FunctionsReturningStrings ::=
3488
     *   "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" |
3489
     *   "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3490
     *   "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" |
3491
     *   "LOWER" "(" StringPrimary ")" |
3492
     *   "UPPER" "(" StringPrimary ")" |
3493
     *   "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"
3494
     *
3495
     * @return Functions\FunctionNode
3496
     */
3497 33
    public function FunctionsReturningStrings()
3498
    {
3499 33
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3500 33
        $funcClass     = self::$_STRING_FUNCTIONS[$funcNameLower];
3501
3502 33
        $function = new $funcClass($funcNameLower);
3503 33
        $function->parse($this);
3504
3505 33
        return $function;
3506
    }
3507
3508
    /**
3509
     * @return Functions\FunctionNode
3510
     */
3511 3
    public function CustomFunctionsReturningStrings()
3512
    {
3513
        // getCustomStringFunction is case-insensitive
3514 3
        $functionName  = $this->lexer->lookahead['value'];
3515 3
        $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName);
3516
3517 3
        $function = is_string($functionClass)
3518 2
            ? new $functionClass($functionName)
3519 3
            : call_user_func($functionClass, $functionName);
3520
3521 3
        $function->parse($this);
3522
3523 3
        return $function;
3524
    }
3525
}
3526