Passed
Pull Request — master (#7065)
by Michael
11:41
created

Parser::ComparisonOperator()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 41
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 8.0032

Importance

Changes 0
Metric Value
cc 8
eloc 27
nc 8
nop 0
dl 0
loc 41
ccs 26
cts 27
cp 0.963
crap 8.0032
rs 5.3846
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 839
    public function __construct(Query $query)
185
    {
186 839
        $this->query        = $query;
187 839
        $this->em           = $query->getEntityManager();
188 839
        $this->lexer        = new Lexer($query->getDQL());
189 839
        $this->parserResult = new ParserResult();
190 839
    }
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 839
    public function getAST()
249
    {
250
        // Parse & build AST
251 839
        $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 804
        $this->processDeferredIdentificationVariables();
256
257 802
        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 801
        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 799
        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 799
        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 795
        $this->processRootEntityAliasSelected();
274
275
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
276 794
        $this->fixIdentificationVariableOrder($AST);
277
278 794
        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 850
    public function match($token)
292
    {
293 850
        $lookaheadType = $this->lexer->lookahead['type'];
294
295
        // Short-circuit on first condition, usually types match
296 850
        if ($lookaheadType !== $token) {
297
            // If parameter is not identifier (1-99) must be exact match
298 21
            if ($token < Lexer::T_IDENTIFIER) {
299 2
                $this->syntaxError($this->lexer->getLiteral($token));
300
            }
301
302
            // If parameter is keyword (200+) must be exact match
303 19
            if ($token > Lexer::T_IDENTIFIER) {
304 7
                $this->syntaxError($this->lexer->getLiteral($token));
305
            }
306
307
            // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
308 12
            if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
309 9
                $this->syntaxError($this->lexer->getLiteral($token));
310
            }
311
        }
312
313 843
        $this->lexer->moveNext();
314 843
    }
315
316
    /**
317
     * Frees this parser, enabling it to be reused.
318
     *
319
     * @param bool $deep     Whether to clean peek and reset errors.
320
     * @param int  $position Position to reset.
321
     */
322
    public function free($deep = false, $position = 0)
323
    {
324
        // WARNING! Use this method with care. It resets the scanner!
325
        $this->lexer->resetPosition($position);
326
327
        // Deep = true cleans peek and also any previously defined errors
328
        if ($deep) {
329
            $this->lexer->resetPeek();
330
        }
331
332
        $this->lexer->token     = null;
333
        $this->lexer->lookahead = null;
334
    }
335
336
    /**
337
     * Parses a query string.
338
     *
339
     * @return ParserResult
340
     */
341 839
    public function parse()
342
    {
343 839
        $AST = $this->getAST();
344
345 794
        $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
346 794
        if ($customWalkers !== false) {
347 96
            $this->customTreeWalkers = $customWalkers;
348
        }
349
350 794
        $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER);
351
352 794
        if ($customOutputWalker !== false) {
353 79
            $this->customOutputWalker = $customOutputWalker;
354
        }
355
356
        // Run any custom tree walkers over the AST
357 794
        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...
358 95
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
359
360 95
            foreach ($this->customTreeWalkers as $walker) {
361 95
                $treeWalkerChain->addTreeWalker($walker);
362
            }
363
364
            switch (true) {
365 95
                case ($AST instanceof AST\UpdateStatement):
366
                    $treeWalkerChain->walkUpdateStatement($AST);
367
                    break;
368
369 95
                case ($AST instanceof AST\DeleteStatement):
370
                    $treeWalkerChain->walkDeleteStatement($AST);
371
                    break;
372
373 95
                case ($AST instanceof AST\SelectStatement):
374
                default:
375 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

375
                    $treeWalkerChain->walkSelectStatement(/** @scrutinizer ignore-type */ $AST);
Loading history...
376
            }
377
378 89
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
379
        }
380
381 788
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
382 788
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
383
384
        // Assign an SQL executor to the parser result
385 788
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
386
387 780
        return $this->parserResult;
388
    }
389
390
    /**
391
     * Fixes order of identification variables.
392
     *
393
     * They have to appear in the select clause in the same order as the
394
     * declarations (from ... x join ... y join ... z ...) appear in the query
395
     * as the hydration process relies on that order for proper operation.
396
     *
397
     * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
398
     */
399 794
    private function fixIdentificationVariableOrder($AST)
400
    {
401 794
        if (count($this->identVariableExpressions) <= 1) {
402 619
            return;
403
        }
404
405 180
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
406 180
            if (! isset($this->identVariableExpressions[$dqlAlias])) {
407 8
                continue;
408
            }
409
410 180
            $expr = $this->identVariableExpressions[$dqlAlias];
411 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...
412
413 180
            unset($AST->selectClause->selectExpressions[$key]);
414
415 180
            $AST->selectClause->selectExpressions[] = $expr;
416
        }
417 180
    }
418
419
    /**
420
     * Generates a new syntax error.
421
     *
422
     * @param string       $expected Expected string.
423
     * @param mixed[]|null $token    Got token.
424
     *
425
     * @throws QueryException
426
     */
427 18
    public function syntaxError($expected = '', $token = null)
428
    {
429 18
        if ($token === null) {
430 15
            $token = $this->lexer->lookahead;
431
        }
432
433 18
        $tokenPos = $token['position'] ?? '-1';
434
435 18
        $message  = sprintf('line 0, col %d: Error: ', $tokenPos);
436 18
        $message .= ($expected !== '') ? sprintf('Expected %s, got ', $expected) : 'Unexpected ';
437 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...
438
439 18
        throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL()));
440
    }
441
442
    /**
443
     * Generates a new semantical error.
444
     *
445
     * @param string       $message Optional message.
446
     * @param mixed[]|null $token   Optional token.
447
     *
448
     * @throws QueryException
449
     */
450 26
    public function semanticalError($message = '', $token = null, ?\Throwable $previousFailure = null)
451
    {
452 26
        if ($token === null) {
453 2
            $token = $this->lexer->lookahead;
454
        }
455
456
        // Minimum exposed chars ahead of token
457 26
        $distance = 12;
458
459
        // Find a position of a final word to display in error string
460 26
        $dql    = $this->query->getDQL();
461 26
        $length = strlen($dql);
462 26
        $pos    = $token['position'] + $distance;
463 26
        $pos    = strpos($dql, ' ', ($length > $pos) ? $pos : $length);
464 26
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
0 ignored issues
show
introduced by
The condition $pos !== false can never be false.
Loading history...
465
466 26
        $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1';
467 26
        $tokenStr = substr($dql, (int) $token['position'], $length);
468
469
        // Building informative message
470 26
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
471
472 26
        throw QueryException::semanticalError(
473 26
            $message,
474 26
            QueryException::dqlError($this->query->getDQL(), $previousFailure)
475
        );
476
    }
477
478
    /**
479
     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
480
     *
481
     * @param bool $resetPeek Reset peek after finding the closing parenthesis.
482
     *
483
     * @return mixed[]
484
     */
485 173
    private function peekBeyondClosingParenthesis($resetPeek = true)
486
    {
487 173
        $token        = $this->lexer->peek();
488 173
        $numUnmatched = 1;
489
490 173
        while ($numUnmatched > 0 && $token !== null) {
491 172
            switch ($token['type']) {
492
                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...
493 44
                    ++$numUnmatched;
494 44
                    break;
495
496
                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...
497 172
                    --$numUnmatched;
498 172
                    break;
499
500
                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...
501
                    // Do nothing
502
            }
503
504 172
            $token = $this->lexer->peek();
505
        }
506
507 173
        if ($resetPeek) {
508 152
            $this->lexer->resetPeek();
509
        }
510
511 173
        return $token;
512
    }
513
514
    /**
515
     * Checks if the given token indicates a mathematical operator.
516
     *
517
     * @param mixed[] $token
518
     *
519
     * @return bool TRUE if the token is a mathematical operator, FALSE otherwise.
520
     */
521 359
    private function isMathOperator($token)
522
    {
523 359
        return in_array($token['type'], [Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY], true);
524
    }
525
526
    /**
527
     * Checks if the next-next (after lookahead) token starts a function.
528
     *
529
     * @return bool TRUE if the next-next tokens start a function, FALSE otherwise.
530
     */
531 402
    private function isFunction()
532
    {
533 402
        $lookaheadType = $this->lexer->lookahead['type'];
534 402
        $peek          = $this->lexer->peek();
535
536 402
        $this->lexer->resetPeek();
537
538 402
        return $lookaheadType >= Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_OPEN_PARENTHESIS;
539
    }
540
541
    /**
542
     * Checks whether the given token type indicates an aggregate function.
543
     *
544
     * @param int $tokenType
545
     *
546
     * @return bool TRUE if the token type is an aggregate function, FALSE otherwise.
547
     */
548 1
    private function isAggregateFunction($tokenType)
549
    {
550 1
        return in_array($tokenType, [Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT], true);
551
    }
552
553
    /**
554
     * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
555
     *
556
     * @return bool
557
     */
558 300
    private function isNextAllAnySome()
559
    {
560 300
        return in_array($this->lexer->lookahead['type'], [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME], true);
561
    }
562
563
    /**
564
     * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
565
     * It must exist in query components list.
566
     */
567 804
    private function processDeferredIdentificationVariables()
568
    {
569 804
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
570 781
            $identVariable = $deferredItem['expression'];
571
572
            // Check if IdentificationVariable exists in queryComponents
573 781
            if (! isset($this->queryComponents[$identVariable])) {
574 1
                $this->semanticalError(
575 1
                    sprintf("'%s' is not defined.", $identVariable),
576 1
                    $deferredItem['token']
577
                );
578
            }
579
580 781
            $qComp = $this->queryComponents[$identVariable];
581
582
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
583 781
            if (! isset($qComp['metadata'])) {
584
                $this->semanticalError(
585
                    sprintf("'%s' does not point to a Class.", $identVariable),
586
                    $deferredItem['token']
587
                );
588
            }
589
590
            // Validate if identification variable nesting level is lower or equal than the current one
591 781
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
592 1
                $this->semanticalError(
593 1
                    sprintf("'%s' is used outside the scope of its declaration.", $identVariable),
594 781
                    $deferredItem['token']
595
                );
596
            }
597
        }
598 802
    }
599
600
    /**
601
     * Validates that the given <tt>NewObjectExpression</tt>.
602
     *
603
     * @param AST\SelectClause $AST
604
     */
605 26
    private function processDeferredNewObjectExpressions($AST)
606
    {
607 26
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
608 26
            $expression    = $deferredItem['expression'];
609 26
            $token         = $deferredItem['token'];
610 26
            $className     = $expression->className;
611 26
            $args          = $expression->args;
612 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...
613
614
            // If the namespace is not given then assumes the first FROM entity namespace
615 26
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
616 10
                $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
617 10
                $fqcn      = $namespace . '\\' . $className;
618
619 10
                if (class_exists($fqcn)) {
620 10
                    $expression->className = $fqcn;
621 10
                    $className             = $fqcn;
622
                }
623
            }
624
625 26
            if (! class_exists($className)) {
626 1
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
627
            }
628
629 25
            $class = new \ReflectionClass($className);
630
631 25
            if (! $class->isInstantiable()) {
632 1
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
633
            }
634
635 24
            if ($class->getConstructor() === null) {
636 1
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
637
            }
638
639 23
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
640 23
                $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
641
            }
642
        }
643 22
    }
644
645
    /**
646
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
647
     * It must exist in query components list.
648
     */
649 9
    private function processDeferredPartialObjectExpressions()
650
    {
651 9
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
652 9
            $expr  = $deferredItem['expression'];
653 9
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
654
655 9
            foreach ($expr->partialFieldSet as $field) {
656 9
                $property = $class->getProperty($field);
657
658 9
                if ($property instanceof FieldMetadata ||
659 9
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
660 9
                    continue;
661
                }
662
663
                $this->semanticalError(
664
                    sprintf("There is no mapped field named '%s' on class %s.", $field, $class->getClassName()),
665
                    $deferredItem['token']
666
                );
667
            }
668
669 9
            if (array_intersect($class->identifier, $expr->partialFieldSet) !== $class->identifier) {
670 1
                $this->semanticalError(
671 1
                    sprintf('The partial field selection of class %s must contain the identifier.', $class->getClassName()),
672 9
                    $deferredItem['token']
673
                );
674
            }
675
        }
676 8
    }
677
678
    /**
679
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
680
     * It must exist in query components list.
681
     */
682 32
    private function processDeferredResultVariables()
683
    {
684 32
        foreach ($this->deferredResultVariables as $deferredItem) {
685 32
            $resultVariable = $deferredItem['expression'];
686
687
            // Check if ResultVariable exists in queryComponents
688 32
            if (! isset($this->queryComponents[$resultVariable])) {
689
                $this->semanticalError(
690
                    sprintf("'%s' is not defined.", $resultVariable),
691
                    $deferredItem['token']
692
                );
693
            }
694
695 32
            $qComp = $this->queryComponents[$resultVariable];
696
697
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
698 32
            if (! isset($qComp['resultVariable'])) {
699
                $this->semanticalError(
700
                    sprintf("'%s' does not point to a ResultVariable.", $resultVariable),
701
                    $deferredItem['token']
702
                );
703
            }
704
705
            // Validate if identification variable nesting level is lower or equal than the current one
706 32
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
707
                $this->semanticalError(
708
                    sprintf("'%s' is used outside the scope of its declaration.", $resultVariable),
709 32
                    $deferredItem['token']
710
                );
711
            }
712
        }
713 32
    }
714
715
    /**
716
     * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
717
     *
718
     * AssociationPathExpression             ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
719
     * SingleValuedPathExpression            ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
720
     * StateFieldPathExpression              ::= IdentificationVariable "." StateField
721
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
722
     * CollectionValuedPathExpression        ::= IdentificationVariable "." CollectionValuedAssociationField
723
     */
724 592
    private function processDeferredPathExpressions()
725
    {
726 592
        foreach ($this->deferredPathExpressions as $deferredItem) {
727 592
            $pathExpression = $deferredItem['expression'];
728
729 592
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
730 592
            $class = $qComp['metadata'];
731 592
            $field = $pathExpression->field;
732
733 592
            if ($field === null) {
734 40
                $field = $pathExpression->field = $class->identifier[0];
735
            }
736
737 592
            $property = $class->getProperty($field);
738
739
            // Check if field or association exists
740 592
            if (! $property) {
741
                $this->semanticalError(
742
                    'Class ' . $class->getClassName() . ' has no field or association named ' . $field,
743
                    $deferredItem['token']
744
                );
745
            }
746
747 592
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
748
749 592
            if ($property instanceof AssociationMetadata) {
750 89
                $fieldType = $property instanceof ToOneAssociationMetadata
751 66
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
752 89
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
753
                ;
754
            }
755
756
            // Validate if PathExpression is one of the expected types
757 592
            $expectedType = $pathExpression->expectedType;
758
759 592
            if (! ($expectedType & $fieldType)) {
760
                // We need to recognize which was expected type(s)
761 2
                $expectedStringTypes = [];
762
763
                // Validate state field type
764 2
                if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
765 1
                    $expectedStringTypes[] = 'StateFieldPathExpression';
766
                }
767
768
                // Validate single valued association (*-to-one)
769 2
                if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
770 2
                    $expectedStringTypes[] = 'SingleValuedAssociationField';
771
                }
772
773
                // Validate single valued association (*-to-many)
774 2
                if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
775
                    $expectedStringTypes[] = 'CollectionValuedAssociationField';
776
                }
777
778
                // Build the error message
779 2
                $semanticalError  = 'Invalid PathExpression. ';
780 2
                $semanticalError .= count($expectedStringTypes) === 1
781 1
                    ? 'Must be a ' . $expectedStringTypes[0] . '.'
782 2
                    : implode(' or ', $expectedStringTypes) . ' expected.';
783
784 2
                $this->semanticalError($semanticalError, $deferredItem['token']);
785
            }
786
787
            // We need to force the type in PathExpression
788 590
            $pathExpression->type = $fieldType;
789
        }
790 590
    }
791
792 795
    private function processRootEntityAliasSelected()
793
    {
794 795
        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...
795 235
            return;
796
        }
797
798 570
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
799 570
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
800 570
                return;
801
            }
802
        }
803
804 1
        $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
805
    }
806
807
    /**
808
     * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
809
     *
810
     * @return AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement
811
     */
812 839
    public function QueryLanguage()
813
    {
814 839
        $statement = null;
815
816 839
        $this->lexer->moveNext();
817
818 839
        switch ($this->lexer->lookahead['type']) {
819
            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...
820 775
                $statement = $this->SelectStatement();
821 744
                break;
822
823
            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...
824 31
                $statement = $this->UpdateStatement();
825 31
                break;
826
827
            case Lexer::T_DELETE:
828 41
                $statement = $this->DeleteStatement();
829 40
                break;
830
831
            default:
832 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
833
                break;
834
        }
835
836
        // Check for end of string
837 807
        if ($this->lexer->lookahead !== null) {
838 3
            $this->syntaxError('end of string');
839
        }
840
841 804
        return $statement;
842
    }
843
844
    /**
845
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
846
     *
847
     * @return AST\SelectStatement
848
     */
849 775
    public function SelectStatement()
850
    {
851 775
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
852
853 748
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
854 745
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
855 744
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
856 744
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
857
858 744
        return $selectStatement;
859
    }
860
861
    /**
862
     * UpdateStatement ::= UpdateClause [WhereClause]
863
     *
864
     * @return AST\UpdateStatement
865
     */
866 31
    public function UpdateStatement()
867
    {
868 31
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
869
870 31
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
871
872 31
        return $updateStatement;
873
    }
874
875
    /**
876
     * DeleteStatement ::= DeleteClause [WhereClause]
877
     *
878
     * @return AST\DeleteStatement
879
     */
880 41
    public function DeleteStatement()
881
    {
882 41
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
883
884 40
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
885
886 40
        return $deleteStatement;
887
    }
888
889
    /**
890
     * IdentificationVariable ::= identifier
891
     *
892
     * @return string
893
     */
894 805
    public function IdentificationVariable()
895
    {
896 805
        $this->match(Lexer::T_IDENTIFIER);
897
898 805
        $identVariable = $this->lexer->token['value'];
899
900 805
        $this->deferredIdentificationVariables[] = [
901 805
            'expression'   => $identVariable,
902 805
            'nestingLevel' => $this->nestingLevel,
903 805
            'token'        => $this->lexer->token,
904
        ];
905
906 805
        return $identVariable;
907
    }
908
909
    /**
910
     * AliasIdentificationVariable = identifier
911
     *
912
     * @return string
913
     */
914 815
    public function AliasIdentificationVariable()
915
    {
916 815
        $this->match(Lexer::T_IDENTIFIER);
917
918 815
        $aliasIdentVariable = $this->lexer->token['value'];
919 815
        $exists             = isset($this->queryComponents[$aliasIdentVariable]);
920
921 815
        if ($exists) {
922 2
            $this->semanticalError(sprintf("'%s' is already defined.", $aliasIdentVariable), $this->lexer->token);
923
        }
924
925 815
        return $aliasIdentVariable;
926
    }
927
928
    /**
929
     * AbstractSchemaName ::= fully_qualified_name | identifier
930
     *
931
     * @return string
932
     */
933 827
    public function AbstractSchemaName()
934
    {
935 827
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
936 820
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
937
938 820
            return $this->lexer->token['value'];
939
        }
940
941 18
        $this->match(Lexer::T_IDENTIFIER);
942
943 17
        return $this->lexer->token['value'];
944
    }
945
946
    /**
947
     * Validates an AbstractSchemaName, making sure the class exists.
948
     *
949
     * @param string $schemaName The name to validate.
950
     *
951
     * @throws QueryException If the name does not exist.
952
     */
953 823
    private function validateAbstractSchemaName($schemaName) : void
954
    {
955 823
        if (class_exists($schemaName, true) || interface_exists($schemaName, true)) {
956 814
            return;
957
        }
958
959
        try {
960 10
            $this->getEntityManager()->getClassMetadata($schemaName);
961
962 1
            return;
963 9
        } catch (MappingException $mappingException) {
964 9
            $this->semanticalError(
965 9
                sprintf('Class %s could not be mapped', $schemaName),
966 9
                $this->lexer->token
967
            );
968
        }
969
970
        $this->semanticalError(sprintf("Class '%s' is not defined.", $schemaName), $this->lexer->token);
971
    }
972
973
    /**
974
     * AliasResultVariable ::= identifier
975
     *
976
     * @return string
977
     */
978 132
    public function AliasResultVariable()
979
    {
980 132
        $this->match(Lexer::T_IDENTIFIER);
981
982 128
        $resultVariable = $this->lexer->token['value'];
983 128
        $exists         = isset($this->queryComponents[$resultVariable]);
984
985 128
        if ($exists) {
986 2
            $this->semanticalError(sprintf("'%s' is already defined.", $resultVariable), $this->lexer->token);
987
        }
988
989 128
        return $resultVariable;
990
    }
991
992
    /**
993
     * ResultVariable ::= identifier
994
     *
995
     * @return string
996
     */
997 32
    public function ResultVariable()
998
    {
999 32
        $this->match(Lexer::T_IDENTIFIER);
1000
1001 32
        $resultVariable = $this->lexer->token['value'];
1002
1003
        // Defer ResultVariable validation
1004 32
        $this->deferredResultVariables[] = [
1005 32
            'expression'   => $resultVariable,
1006 32
            'nestingLevel' => $this->nestingLevel,
1007 32
            'token'        => $this->lexer->token,
1008
        ];
1009
1010 32
        return $resultVariable;
1011
    }
1012
1013
    /**
1014
     * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
1015
     *
1016
     * @return AST\JoinAssociationPathExpression
1017
     */
1018 256
    public function JoinAssociationPathExpression()
1019
    {
1020 256
        $identVariable = $this->IdentificationVariable();
1021
1022 256
        if (! isset($this->queryComponents[$identVariable])) {
1023
            $this->semanticalError(
1024
                'Identification Variable ' . $identVariable . ' used in join path expression but was not defined before.'
1025
            );
1026
        }
1027
1028 256
        $this->match(Lexer::T_DOT);
1029 256
        $this->match(Lexer::T_IDENTIFIER);
1030
1031 256
        $field = $this->lexer->token['value'];
1032
1033
        // Validate association field
1034 256
        $qComp    = $this->queryComponents[$identVariable];
1035 256
        $class    = $qComp['metadata'];
1036 256
        $property = $class->getProperty($field);
1037
1038 256
        if (! ($property !== null && $property instanceof AssociationMetadata)) {
1039
            $this->semanticalError('Class ' . $class->getClassName() . ' has no association named ' . $field);
1040
        }
1041
1042 256
        return new AST\JoinAssociationPathExpression($identVariable, $field);
1043
    }
1044
1045
    /**
1046
     * Parses an arbitrary path expression and defers semantical validation
1047
     * based on expected types.
1048
     *
1049
     * PathExpression ::= IdentificationVariable {"." identifier}*
1050
     *
1051
     * @param int $expectedTypes
1052
     *
1053
     * @return AST\PathExpression
1054
     */
1055 602
    public function PathExpression($expectedTypes)
1056
    {
1057 602
        $identVariable = $this->IdentificationVariable();
1058 602
        $field         = null;
1059
1060 602
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1061 596
            $this->match(Lexer::T_DOT);
1062 596
            $this->match(Lexer::T_IDENTIFIER);
1063
1064 596
            $field = $this->lexer->token['value'];
1065
1066 596
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1067
                $this->match(Lexer::T_DOT);
1068
                $this->match(Lexer::T_IDENTIFIER);
1069
                $field .= '.' . $this->lexer->token['value'];
1070
            }
1071
        }
1072
1073
        // Creating AST node
1074 602
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1075
1076
        // Defer PathExpression validation if requested to be deferred
1077 602
        $this->deferredPathExpressions[] = [
1078 602
            'expression'   => $pathExpr,
1079 602
            'nestingLevel' => $this->nestingLevel,
1080 602
            'token'        => $this->lexer->token,
1081
        ];
1082
1083 602
        return $pathExpr;
1084
    }
1085
1086
    /**
1087
     * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
1088
     *
1089
     * @return AST\PathExpression
1090
     */
1091
    public function AssociationPathExpression()
1092
    {
1093
        return $this->PathExpression(
1094
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
1095
            AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
1096
        );
1097
    }
1098
1099
    /**
1100
     * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
1101
     *
1102
     * @return AST\PathExpression
1103
     */
1104 511
    public function SingleValuedPathExpression()
1105
    {
1106 511
        return $this->PathExpression(
1107 511
            AST\PathExpression::TYPE_STATE_FIELD |
1108 511
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1109
        );
1110
    }
1111
1112
    /**
1113
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1114
     *
1115
     * @return AST\PathExpression
1116
     */
1117 205
    public function StateFieldPathExpression()
1118
    {
1119 205
        return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
1120
    }
1121
1122
    /**
1123
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
1124
     *
1125
     * @return AST\PathExpression
1126
     */
1127 9
    public function SingleValuedAssociationPathExpression()
1128
    {
1129 9
        return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
1130
    }
1131
1132
    /**
1133
     * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
1134
     *
1135
     * @return AST\PathExpression
1136
     */
1137 23
    public function CollectionValuedPathExpression()
1138
    {
1139 23
        return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
1140
    }
1141
1142
    /**
1143
     * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
1144
     *
1145
     * @return AST\SelectClause
1146
     */
1147 775
    public function SelectClause()
1148
    {
1149 775
        $isDistinct = false;
1150 775
        $this->match(Lexer::T_SELECT);
1151
1152
        // Check for DISTINCT
1153 775
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1154 6
            $this->match(Lexer::T_DISTINCT);
1155
1156 6
            $isDistinct = true;
1157
        }
1158
1159
        // Process SelectExpressions (1..N)
1160 775
        $selectExpressions   = [];
1161 775
        $selectExpressions[] = $this->SelectExpression();
1162
1163 767
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1164 298
            $this->match(Lexer::T_COMMA);
1165
1166 298
            $selectExpressions[] = $this->SelectExpression();
1167
        }
1168
1169 766
        return new AST\SelectClause($selectExpressions, $isDistinct);
1170
    }
1171
1172
    /**
1173
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1174
     *
1175
     * @return AST\SimpleSelectClause
1176
     */
1177 49
    public function SimpleSelectClause()
1178
    {
1179 49
        $isDistinct = false;
1180 49
        $this->match(Lexer::T_SELECT);
1181
1182 49
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1183
            $this->match(Lexer::T_DISTINCT);
1184
1185
            $isDistinct = true;
1186
        }
1187
1188 49
        return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1189
    }
1190
1191
    /**
1192
     * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1193
     *
1194
     * @return AST\UpdateClause
1195
     */
1196 31
    public function UpdateClause()
1197
    {
1198 31
        $this->match(Lexer::T_UPDATE);
1199
1200 31
        $token              = $this->lexer->lookahead;
1201 31
        $abstractSchemaName = $this->AbstractSchemaName();
1202
1203 31
        $this->validateAbstractSchemaName($abstractSchemaName);
1204
1205 31
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1206 2
            $this->match(Lexer::T_AS);
1207
        }
1208
1209 31
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1210
1211 31
        $class = $this->em->getClassMetadata($abstractSchemaName);
1212
1213
        // Building queryComponent
1214
        $queryComponent = [
1215 31
            'metadata'     => $class,
1216
            'parent'       => null,
1217
            'relation'     => null,
1218
            'map'          => null,
1219 31
            'nestingLevel' => $this->nestingLevel,
1220 31
            'token'        => $token,
1221
        ];
1222
1223 31
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1224
1225 31
        $this->match(Lexer::T_SET);
1226
1227 31
        $updateItems   = [];
1228 31
        $updateItems[] = $this->UpdateItem();
1229
1230 31
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1231 4
            $this->match(Lexer::T_COMMA);
1232
1233 4
            $updateItems[] = $this->UpdateItem();
1234
        }
1235
1236 31
        $updateClause                              = new AST\UpdateClause($abstractSchemaName, $updateItems);
1237 31
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1238
1239 31
        return $updateClause;
1240
    }
1241
1242
    /**
1243
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1244
     *
1245
     * @return AST\DeleteClause
1246
     */
1247 41
    public function DeleteClause()
1248
    {
1249 41
        $this->match(Lexer::T_DELETE);
1250
1251 41
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1252 8
            $this->match(Lexer::T_FROM);
1253
        }
1254
1255 41
        $token              = $this->lexer->lookahead;
1256 41
        $abstractSchemaName = $this->AbstractSchemaName();
1257
1258 41
        $this->validateAbstractSchemaName($abstractSchemaName);
1259
1260 41
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1261
1262 41
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1263 1
            $this->match(Lexer::T_AS);
1264
        }
1265
1266 41
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1267
1268 40
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1269 40
        $class                                     = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1270
1271
        // Building queryComponent
1272
        $queryComponent = [
1273 40
            'metadata'     => $class,
1274
            'parent'       => null,
1275
            'relation'     => null,
1276
            'map'          => null,
1277 40
            'nestingLevel' => $this->nestingLevel,
1278 40
            'token'        => $token,
1279
        ];
1280
1281 40
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1282
1283 40
        return $deleteClause;
1284
    }
1285
1286
    /**
1287
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1288
     *
1289
     * @return AST\FromClause
1290
     */
1291 766
    public function FromClause()
1292
    {
1293 766
        $this->match(Lexer::T_FROM);
1294
1295 761
        $identificationVariableDeclarations   = [];
1296 761
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1297
1298 748
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1299 7
            $this->match(Lexer::T_COMMA);
1300
1301 7
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1302
        }
1303
1304 748
        return new AST\FromClause($identificationVariableDeclarations);
1305
    }
1306
1307
    /**
1308
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1309
     *
1310
     * @return AST\SubselectFromClause
1311
     */
1312 49
    public function SubselectFromClause()
1313
    {
1314 49
        $this->match(Lexer::T_FROM);
1315
1316 49
        $identificationVariables   = [];
1317 49
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1318
1319 48
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1320
            $this->match(Lexer::T_COMMA);
1321
1322
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1323
        }
1324
1325 48
        return new AST\SubselectFromClause($identificationVariables);
1326
    }
1327
1328
    /**
1329
     * WhereClause ::= "WHERE" ConditionalExpression
1330
     *
1331
     * @return AST\WhereClause
1332
     */
1333 340
    public function WhereClause()
1334
    {
1335 340
        $this->match(Lexer::T_WHERE);
1336
1337 340
        return new AST\WhereClause($this->ConditionalExpression());
1338
    }
1339
1340
    /**
1341
     * HavingClause ::= "HAVING" ConditionalExpression
1342
     *
1343
     * @return AST\HavingClause
1344
     */
1345 21
    public function HavingClause()
1346
    {
1347 21
        $this->match(Lexer::T_HAVING);
1348
1349 21
        return new AST\HavingClause($this->ConditionalExpression());
1350
    }
1351
1352
    /**
1353
     * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1354
     *
1355
     * @return AST\GroupByClause
1356
     */
1357 33
    public function GroupByClause()
1358
    {
1359 33
        $this->match(Lexer::T_GROUP);
1360 33
        $this->match(Lexer::T_BY);
1361
1362 33
        $groupByItems = [$this->GroupByItem()];
1363
1364 32
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1365 8
            $this->match(Lexer::T_COMMA);
1366
1367 8
            $groupByItems[] = $this->GroupByItem();
1368
        }
1369
1370 32
        return new AST\GroupByClause($groupByItems);
1371
    }
1372
1373
    /**
1374
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1375
     *
1376
     * @return AST\OrderByClause
1377
     */
1378 180
    public function OrderByClause()
1379
    {
1380 180
        $this->match(Lexer::T_ORDER);
1381 180
        $this->match(Lexer::T_BY);
1382
1383 180
        $orderByItems   = [];
1384 180
        $orderByItems[] = $this->OrderByItem();
1385
1386 180
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1387 15
            $this->match(Lexer::T_COMMA);
1388
1389 15
            $orderByItems[] = $this->OrderByItem();
1390
        }
1391
1392 180
        return new AST\OrderByClause($orderByItems);
1393
    }
1394
1395
    /**
1396
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1397
     *
1398
     * @return AST\Subselect
1399
     */
1400 49
    public function Subselect()
1401
    {
1402
        // Increase query nesting level
1403 49
        $this->nestingLevel++;
1404
1405 49
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1406
1407 48
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1408 48
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1409 48
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1410 48
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1411
1412
        // Decrease query nesting level
1413 48
        $this->nestingLevel--;
1414
1415 48
        return $subselect;
1416
    }
1417
1418
    /**
1419
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1420
     *
1421
     * @return AST\UpdateItem
1422
     */
1423 31
    public function UpdateItem()
1424
    {
1425 31
        $pathExpr = $this->SingleValuedPathExpression();
1426
1427 31
        $this->match(Lexer::T_EQUALS);
1428
1429 31
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1430
1431 31
        return $updateItem;
1432
    }
1433
1434
    /**
1435
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1436
     *
1437
     * @return string|AST\PathExpression
1438
     */
1439 33
    public function GroupByItem()
1440
    {
1441
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1442 33
        $glimpse = $this->lexer->glimpse();
1443
1444 33
        if ($glimpse['type'] === Lexer::T_DOT) {
1445 14
            return $this->SingleValuedPathExpression();
1446
        }
1447
1448
        // Still need to decide between IdentificationVariable or ResultVariable
1449 19
        $lookaheadValue = $this->lexer->lookahead['value'];
1450
1451 19
        if (! isset($this->queryComponents[$lookaheadValue])) {
1452 1
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1453
        }
1454
1455 18
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1456 16
            ? $this->IdentificationVariable()
1457 18
            : $this->ResultVariable();
1458
    }
1459
1460
    /**
1461
     * OrderByItem ::= (
1462
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1463
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1464
     * ) ["ASC" | "DESC"]
1465
     *
1466
     * @return AST\OrderByItem
1467
     */
1468 180
    public function OrderByItem()
1469
    {
1470 180
        $this->lexer->peek(); // lookahead => '.'
1471 180
        $this->lexer->peek(); // lookahead => token after '.'
1472
1473 180
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1474
1475 180
        $this->lexer->resetPeek();
1476
1477 180
        $glimpse = $this->lexer->glimpse();
1478
1479
        switch (true) {
1480 180
            case ($this->isFunction()):
1481 2
                $expr = $this->FunctionDeclaration();
1482 2
                break;
1483
1484 178
            case ($this->isMathOperator($peek)):
1485 25
                $expr = $this->SimpleArithmeticExpression();
1486 25
                break;
1487
1488 154
            case ($glimpse['type'] === Lexer::T_DOT):
1489 139
                $expr = $this->SingleValuedPathExpression();
1490 139
                break;
1491
1492 19
            case ($this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis())):
1493 2
                $expr = $this->ScalarExpression();
1494 2
                break;
1495
1496
            default:
1497 17
                $expr = $this->ResultVariable();
1498 17
                break;
1499
        }
1500
1501 180
        $type = 'ASC';
1502 180
        $item = new AST\OrderByItem($expr);
1503
1504
        switch (true) {
1505 180
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1506 95
                $this->match(Lexer::T_DESC);
1507 95
                $type = 'DESC';
1508 95
                break;
1509
1510 152
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1511 97
                $this->match(Lexer::T_ASC);
1512 97
                break;
1513
1514
            default:
1515
                // Do nothing
1516
        }
1517
1518 180
        $item->type = $type;
1519
1520 180
        return $item;
1521
    }
1522
1523
    /**
1524
     * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1525
     *      EnumPrimary | SimpleEntityExpression | "NULL"
1526
     *
1527
     * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1528
     * grammar that needs to be supported:
1529
     *
1530
     * NewValue ::= SimpleArithmeticExpression | "NULL"
1531
     *
1532
     * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1533
     *
1534
     * @return AST\ArithmeticExpression
1535
     */
1536 31
    public function NewValue()
1537
    {
1538 31
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1539 1
            $this->match(Lexer::T_NULL);
1540
1541 1
            return null;
1542
        }
1543
1544 30
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1545 17
            $this->match(Lexer::T_INPUT_PARAMETER);
1546
1547 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...
1548
        }
1549
1550 13
        return $this->ArithmeticExpression();
1551
    }
1552
1553
    /**
1554
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1555
     *
1556
     * @return AST\IdentificationVariableDeclaration
1557
     */
1558 763
    public function IdentificationVariableDeclaration()
1559
    {
1560 763
        $joins                    = [];
1561 763
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1562 753
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1563 7
            ? $this->IndexBy()
1564 753
            : null;
1565
1566 753
        $rangeVariableDeclaration->isRoot = true;
1567
1568 753
        while ($this->lexer->isNextToken(Lexer::T_LEFT) ||
1569 753
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1570 753
            $this->lexer->isNextToken(Lexer::T_JOIN)
1571
        ) {
1572 278
            $joins[] = $this->Join();
1573
        }
1574
1575 750
        return new AST\IdentificationVariableDeclaration(
1576 750
            $rangeVariableDeclaration,
1577 750
            $indexBy,
1578 750
            $joins
1579
        );
1580
    }
1581
1582
    /**
1583
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1584
     *
1585
     * {@internal WARNING: Solution is harder than a bare implementation.
1586
     * Desired EBNF support:
1587
     *
1588
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1589
     *
1590
     * It demands that entire SQL generation to become programmatical. This is
1591
     * needed because association based subselect requires "WHERE" conditional
1592
     * expressions to be injected, but there is no scope to do that. Only scope
1593
     * accessible is "FROM", prohibiting an easy implementation without larger
1594
     * changes. }}
1595
     *
1596
     * @return AST\SubselectIdentificationVariableDeclaration|AST\IdentificationVariableDeclaration
1597
     */
1598 49
    public function SubselectIdentificationVariableDeclaration()
1599
    {
1600
        /*
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...
1601
        NOT YET IMPLEMENTED!
1602
1603
        $glimpse = $this->lexer->glimpse();
1604
1605
        if ($glimpse['type'] == Lexer::T_DOT) {
1606
            $associationPathExpression = $this->AssociationPathExpression();
1607
1608
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1609
                $this->match(Lexer::T_AS);
1610
            }
1611
1612
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1613
            $identificationVariable      = $associationPathExpression->identificationVariable;
1614
            $field                       = $associationPathExpression->associationField;
1615
1616
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1617
            $association = $class->getProperty($field);
1618
            $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
1619
1620
            // Building queryComponent
1621
            $joinQueryComponent = array(
1622
                'metadata'     => $targetClass,
1623
                'parent'       => $identificationVariable,
1624
                'relation'     => $association,
1625
                'map'          => null,
1626
                'nestingLevel' => $this->nestingLevel,
1627
                'token'        => $this->lexer->lookahead
1628
            );
1629
1630
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1631
1632
            return new AST\SubselectIdentificationVariableDeclaration(
1633
                $associationPathExpression, $aliasIdentificationVariable
1634
            );
1635
        }
1636
        */
1637
1638 49
        return $this->IdentificationVariableDeclaration();
1639
    }
1640
1641
    /**
1642
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1643
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1644
     *          ["WITH" ConditionalExpression]
1645
     *
1646
     * @return AST\Join
1647
     */
1648 278
    public function Join()
1649
    {
1650
        // Check Join type
1651 278
        $joinType = AST\Join::JOIN_TYPE_INNER;
1652
1653
        switch (true) {
1654 278
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1655 68
                $this->match(Lexer::T_LEFT);
1656
1657 68
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1658
1659
                // Possible LEFT OUTER join
1660 68
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1661
                    $this->match(Lexer::T_OUTER);
1662
1663
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1664
                }
1665 68
                break;
1666
1667 213
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1668 21
                $this->match(Lexer::T_INNER);
1669 21
                break;
1670
1671
            default:
1672
                // Do nothing
1673
        }
1674
1675 278
        $this->match(Lexer::T_JOIN);
1676
1677 278
        $next            = $this->lexer->glimpse();
1678 278
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1679 275
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1680 275
        $join            = new AST\Join($joinType, $joinDeclaration);
1681
1682
        // Describe non-root join declaration
1683 275
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1684 22
            $joinDeclaration->isRoot = false;
1685
        }
1686
1687
        // Check for ad-hoc Join conditions
1688 275
        if ($adhocConditions) {
1689 25
            $this->match(Lexer::T_WITH);
1690
1691 25
            $join->conditionalExpression = $this->ConditionalExpression();
1692
        }
1693
1694 275
        return $join;
1695
    }
1696
1697
    /**
1698
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1699
     *
1700
     * @return AST\RangeVariableDeclaration
1701
     *
1702
     * @throws QueryException
1703
     */
1704 763
    public function RangeVariableDeclaration()
1705
    {
1706 763
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()['type'] === Lexer::T_SELECT) {
1707 2
            $this->semanticalError('Subquery is not supported here', $this->lexer->token);
1708
        }
1709
1710 762
        $abstractSchemaName = $this->AbstractSchemaName();
1711
1712 761
        $this->validateAbstractSchemaName($abstractSchemaName);
1713
1714 753
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1715 6
            $this->match(Lexer::T_AS);
1716
        }
1717
1718 753
        $token                       = $this->lexer->lookahead;
1719 753
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1720 753
        $classMetadata               = $this->em->getClassMetadata($abstractSchemaName);
1721
1722
        // Building queryComponent
1723
        $queryComponent = [
1724 753
            'metadata'     => $classMetadata,
1725
            'parent'       => null,
1726
            'relation'     => null,
1727
            'map'          => null,
1728 753
            'nestingLevel' => $this->nestingLevel,
1729 753
            'token'        => $token,
1730
        ];
1731
1732 753
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1733
1734 753
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1735
    }
1736
1737
    /**
1738
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1739
     *
1740
     * @return AST\JoinAssociationPathExpression
1741
     */
1742 256
    public function JoinAssociationDeclaration()
1743
    {
1744 256
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1745
1746 256
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1747 5
            $this->match(Lexer::T_AS);
1748
        }
1749
1750 256
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1751 254
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1752
1753 254
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1754 254
        $field                  = $joinAssociationPathExpression->associationField;
1755
1756 254
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1757 254
        $association = $class->getProperty($field);
1758 254
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
1759
1760
        // Building queryComponent
1761
        $joinQueryComponent = [
1762 254
            'metadata'     => $targetClass,
1763 254
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1764 254
            'relation'     => $association,
1765
            'map'          => null,
1766 254
            'nestingLevel' => $this->nestingLevel,
1767 254
            'token'        => $this->lexer->lookahead,
1768
        ];
1769
1770 254
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1771
1772 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...
1773
    }
1774
1775
    /**
1776
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1777
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1778
     *
1779
     * @return AST\PartialObjectExpression
1780
     */
1781 9
    public function PartialObjectExpression()
1782
    {
1783 9
        $this->match(Lexer::T_PARTIAL);
1784
1785 9
        $partialFieldSet = [];
1786
1787 9
        $identificationVariable = $this->IdentificationVariable();
1788
1789 9
        $this->match(Lexer::T_DOT);
1790 9
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1791 9
        $this->match(Lexer::T_IDENTIFIER);
1792
1793 9
        $field = $this->lexer->token['value'];
1794
1795
        // First field in partial expression might be embeddable property
1796 9
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1797
            $this->match(Lexer::T_DOT);
1798
            $this->match(Lexer::T_IDENTIFIER);
1799
            $field .= '.' . $this->lexer->token['value'];
1800
        }
1801
1802 9
        $partialFieldSet[] = $field;
1803
1804 9
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1805 7
            $this->match(Lexer::T_COMMA);
1806 7
            $this->match(Lexer::T_IDENTIFIER);
1807
1808 7
            $field = $this->lexer->token['value'];
1809
1810 7
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1811
                $this->match(Lexer::T_DOT);
1812
                $this->match(Lexer::T_IDENTIFIER);
1813
                $field .= '.' . $this->lexer->token['value'];
1814
            }
1815
1816 7
            $partialFieldSet[] = $field;
1817
        }
1818
1819 9
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1820
1821 9
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1822
1823
        // Defer PartialObjectExpression validation
1824 9
        $this->deferredPartialObjectExpressions[] = [
1825 9
            'expression'   => $partialObjectExpression,
1826 9
            'nestingLevel' => $this->nestingLevel,
1827 9
            'token'        => $this->lexer->token,
1828
        ];
1829
1830 9
        return $partialObjectExpression;
1831
    }
1832
1833
    /**
1834
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1835
     *
1836
     * @return AST\NewObjectExpression
1837
     */
1838 26
    public function NewObjectExpression()
1839
    {
1840 26
        $this->match(Lexer::T_NEW);
1841
1842 26
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1843 26
        $token     = $this->lexer->token;
1844
1845 26
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1846
1847 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...
1848
1849 26
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1850 22
            $this->match(Lexer::T_COMMA);
1851
1852 22
            $args[] = $this->NewObjectArg();
1853
        }
1854
1855 26
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1856
1857 26
        $expression = new AST\NewObjectExpression($className, $args);
1858
1859
        // Defer NewObjectExpression validation
1860 26
        $this->deferredNewObjectExpressions[] = [
1861 26
            'token'        => $token,
1862 26
            'expression'   => $expression,
1863 26
            'nestingLevel' => $this->nestingLevel,
1864
        ];
1865
1866 26
        return $expression;
1867
    }
1868
1869
    /**
1870
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1871
     *
1872
     * @return mixed
1873
     */
1874 26
    public function NewObjectArg()
1875
    {
1876 26
        $token = $this->lexer->lookahead;
1877 26
        $peek  = $this->lexer->glimpse();
1878
1879 26
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1880 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1881 2
            $expression = $this->Subselect();
1882 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1883
1884 2
            return $expression;
1885
        }
1886
1887 26
        return $this->ScalarExpression();
1888
    }
1889
1890
    /**
1891
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1892
     *
1893
     * @return AST\IndexBy
1894
     */
1895 11
    public function IndexBy()
1896
    {
1897 11
        $this->match(Lexer::T_INDEX);
1898 11
        $this->match(Lexer::T_BY);
1899 11
        $pathExpr = $this->StateFieldPathExpression();
1900
1901
        // Add the INDEX BY info to the query component
1902 11
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1903
1904 11
        return new AST\IndexBy($pathExpr);
1905
    }
1906
1907
    /**
1908
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1909
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1910
     *                      InstanceOfExpression
1911
     *
1912
     * @return mixed One of the possible expressions or subexpressions.
1913
     */
1914 163
    public function ScalarExpression()
1915
    {
1916 163
        $lookahead = $this->lexer->lookahead['type'];
1917 163
        $peek      = $this->lexer->glimpse();
1918
1919 163
        switch (true) {
1920
            case ($lookahead === Lexer::T_INTEGER):
1921 160
            case ($lookahead === Lexer::T_FLOAT):
1922
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1923 160
            case ($lookahead === Lexer::T_MINUS):
1924 160
            case ($lookahead === Lexer::T_PLUS):
1925 17
                return $this->SimpleArithmeticExpression();
1926
1927 160
            case ($lookahead === Lexer::T_STRING):
1928 13
                return $this->StringPrimary();
1929
1930 158
            case ($lookahead === Lexer::T_TRUE):
1931 158
            case ($lookahead === Lexer::T_FALSE):
1932 3
                $this->match($lookahead);
1933
1934 3
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1935
1936 158
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
1937
                switch (true) {
1938 1
                    case $this->isMathOperator($peek):
1939
                        // :param + u.value
1940 1
                        return $this->SimpleArithmeticExpression();
1941
                    default:
1942
                        return $this->InputParameter();
1943
                }
1944
                // cannot get here
1945
1946 158
            case ($lookahead === Lexer::T_CASE):
1947 154
            case ($lookahead === Lexer::T_COALESCE):
1948 154
            case ($lookahead === Lexer::T_NULLIF):
1949
                // Since NULLIF and COALESCE can be identified as a function,
1950
                // we need to check these before checking for FunctionDeclaration
1951 8
                return $this->CaseExpression();
1952
1953 154
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1954 4
                return $this->SimpleArithmeticExpression();
1955
1956
            // this check must be done before checking for a filed path expression
1957 151
            case ($this->isFunction()):
1958 27
                $this->lexer->peek(); // "("
1959
1960
                switch (true) {
1961 27
                    case ($this->isMathOperator($this->peekBeyondClosingParenthesis())):
1962
                        // SUM(u.id) + COUNT(u.id)
1963 7
                        return $this->SimpleArithmeticExpression();
1964
1965
                    default:
1966
                        // IDENTITY(u)
1967 22
                        return $this->FunctionDeclaration();
1968
                }
1969
1970
                break;
1971
            // it is no function, so it must be a field path
1972 132
            case ($lookahead === Lexer::T_IDENTIFIER):
1973 132
                $this->lexer->peek(); // lookahead => '.'
1974 132
                $this->lexer->peek(); // lookahead => token after '.'
1975 132
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1976 132
                $this->lexer->resetPeek();
1977
1978 132
                if ($this->isMathOperator($peek)) {
1979 7
                    return $this->SimpleArithmeticExpression();
1980
                }
1981
1982 127
                return $this->StateFieldPathExpression();
1983
1984
            default:
1985
                $this->syntaxError();
1986
        }
1987
    }
1988
1989
    /**
1990
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
1991
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
1992
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
1993
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
1994
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
1995
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
1996
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
1997
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
1998
     *
1999
     * @return mixed One of the possible expressions or subexpressions.
2000
     */
2001 19
    public function CaseExpression()
2002
    {
2003 19
        $lookahead = $this->lexer->lookahead['type'];
2004
2005 19
        switch ($lookahead) {
2006
            case Lexer::T_NULLIF:
2007 5
                return $this->NullIfExpression();
2008
2009
            case Lexer::T_COALESCE:
2010 2
                return $this->CoalesceExpression();
2011
2012
            case Lexer::T_CASE:
2013 14
                $this->lexer->resetPeek();
2014 14
                $peek = $this->lexer->peek();
2015
2016 14
                if ($peek['type'] === Lexer::T_WHEN) {
2017 9
                    return $this->GeneralCaseExpression();
2018
                }
2019
2020 5
                return $this->SimpleCaseExpression();
2021
2022
            default:
2023
                // Do nothing
2024
                break;
2025
        }
2026
2027
        $this->syntaxError();
2028
    }
2029
2030
    /**
2031
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2032
     *
2033
     * @return AST\CoalesceExpression
2034
     */
2035 3
    public function CoalesceExpression()
2036
    {
2037 3
        $this->match(Lexer::T_COALESCE);
2038 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2039
2040
        // Process ScalarExpressions (1..N)
2041 3
        $scalarExpressions   = [];
2042 3
        $scalarExpressions[] = $this->ScalarExpression();
2043
2044 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2045 3
            $this->match(Lexer::T_COMMA);
2046
2047 3
            $scalarExpressions[] = $this->ScalarExpression();
2048
        }
2049
2050 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2051
2052 3
        return new AST\CoalesceExpression($scalarExpressions);
2053
    }
2054
2055
    /**
2056
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2057
     *
2058
     * @return AST\NullIfExpression
2059
     */
2060 5
    public function NullIfExpression()
2061
    {
2062 5
        $this->match(Lexer::T_NULLIF);
2063 5
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2064
2065 5
        $firstExpression = $this->ScalarExpression();
2066 5
        $this->match(Lexer::T_COMMA);
2067 5
        $secondExpression = $this->ScalarExpression();
2068
2069 5
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2070
2071 5
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2072
    }
2073
2074
    /**
2075
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2076
     *
2077
     * @return AST\GeneralCaseExpression
2078
     */
2079 9
    public function GeneralCaseExpression()
2080
    {
2081 9
        $this->match(Lexer::T_CASE);
2082
2083
        // Process WhenClause (1..N)
2084 9
        $whenClauses = [];
2085
2086
        do {
2087 9
            $whenClauses[] = $this->WhenClause();
2088 9
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2089
2090 9
        $this->match(Lexer::T_ELSE);
2091 9
        $scalarExpression = $this->ScalarExpression();
2092 9
        $this->match(Lexer::T_END);
2093
2094 9
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2095
    }
2096
2097
    /**
2098
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2099
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2100
     *
2101
     * @return AST\SimpleCaseExpression
2102
     */
2103 5
    public function SimpleCaseExpression()
2104
    {
2105 5
        $this->match(Lexer::T_CASE);
2106 5
        $caseOperand = $this->StateFieldPathExpression();
2107
2108
        // Process SimpleWhenClause (1..N)
2109 5
        $simpleWhenClauses = [];
2110
2111
        do {
2112 5
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2113 5
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2114
2115 5
        $this->match(Lexer::T_ELSE);
2116 5
        $scalarExpression = $this->ScalarExpression();
2117 5
        $this->match(Lexer::T_END);
2118
2119 5
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2120
    }
2121
2122
    /**
2123
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2124
     *
2125
     * @return AST\WhenClause
2126
     */
2127 9
    public function WhenClause()
2128
    {
2129 9
        $this->match(Lexer::T_WHEN);
2130 9
        $conditionalExpression = $this->ConditionalExpression();
2131 9
        $this->match(Lexer::T_THEN);
2132
2133 9
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2134
    }
2135
2136
    /**
2137
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2138
     *
2139
     * @return AST\SimpleWhenClause
2140
     */
2141 5
    public function SimpleWhenClause()
2142
    {
2143 5
        $this->match(Lexer::T_WHEN);
2144 5
        $conditionalExpression = $this->ScalarExpression();
2145 5
        $this->match(Lexer::T_THEN);
2146
2147 5
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2148
    }
2149
2150
    /**
2151
     * SelectExpression ::= (
2152
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2153
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2154
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2155
     *
2156
     * @return AST\SelectExpression
2157
     */
2158 775
    public function SelectExpression()
2159
    {
2160 775
        $expression    = null;
2161 775
        $identVariable = null;
2162 775
        $peek          = $this->lexer->glimpse();
2163 775
        $lookaheadType = $this->lexer->lookahead['type'];
2164
2165 775
        switch (true) {
2166
            // ScalarExpression (u.name)
2167 699
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2168 106
                $expression = $this->ScalarExpression();
2169 106
                break;
2170
2171
            // IdentificationVariable (u)
2172 715
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2173 588
                $expression = $identVariable = $this->IdentificationVariable();
2174 588
                break;
2175
2176
            // 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...
2177 189
            case ($lookaheadType === Lexer::T_CASE):
2178 184
            case ($lookaheadType === Lexer::T_COALESCE):
2179 182
            case ($lookaheadType === Lexer::T_NULLIF):
2180 9
                $expression = $this->CaseExpression();
2181 9
                break;
2182
2183
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2184 180
            case ($this->isFunction()):
2185 104
                $this->lexer->peek(); // "("
2186
2187
                switch (true) {
2188 104
                    case ($this->isMathOperator($this->peekBeyondClosingParenthesis())):
2189
                        // SUM(u.id) + COUNT(u.id)
2190 2
                        $expression = $this->ScalarExpression();
2191 2
                        break;
2192
2193
                    default:
2194
                        // IDENTITY(u)
2195 102
                        $expression = $this->FunctionDeclaration();
2196 102
                        break;
2197
                }
2198
2199 104
                break;
2200
2201
            // PartialObjectExpression (PARTIAL u.{id, name})
2202 77
            case ($lookaheadType === Lexer::T_PARTIAL):
2203 9
                $expression    = $this->PartialObjectExpression();
2204 9
                $identVariable = $expression->identificationVariable;
2205 9
                break;
2206
2207
            // Subselect
2208 68
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2209 23
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2210 23
                $expression = $this->Subselect();
2211 23
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2212 23
                break;
2213
2214
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2215 45
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2216 41
            case ($lookaheadType === Lexer::T_INTEGER):
2217 39
            case ($lookaheadType === Lexer::T_STRING):
2218 30
            case ($lookaheadType === Lexer::T_FLOAT):
2219
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2220 30
            case ($lookaheadType === Lexer::T_MINUS):
2221 30
            case ($lookaheadType === Lexer::T_PLUS):
2222 16
                $expression = $this->SimpleArithmeticExpression();
2223 16
                break;
2224
2225
            // 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...
2226 29
            case ($lookaheadType === Lexer::T_NEW):
2227 26
                $expression = $this->NewObjectExpression();
2228 26
                break;
2229
2230
            default:
2231 3
                $this->syntaxError(
2232 3
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2233 3
                    $this->lexer->lookahead
2234
                );
2235
        }
2236
2237
        // [["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...
2238 772
        $mustHaveAliasResultVariable = false;
2239
2240 772
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2241 123
            $this->match(Lexer::T_AS);
2242
2243 123
            $mustHaveAliasResultVariable = true;
2244
        }
2245
2246 772
        $hiddenAliasResultVariable = false;
2247
2248 772
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2249 10
            $this->match(Lexer::T_HIDDEN);
2250
2251 10
            $hiddenAliasResultVariable = true;
2252
        }
2253
2254 772
        $aliasResultVariable = null;
2255
2256 772
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2257 132
            $token               = $this->lexer->lookahead;
2258 132
            $aliasResultVariable = $this->AliasResultVariable();
2259
2260
            // Include AliasResultVariable in query components.
2261 127
            $this->queryComponents[$aliasResultVariable] = [
2262 127
                'resultVariable' => $expression,
2263 127
                'nestingLevel'   => $this->nestingLevel,
2264 127
                'token'          => $token,
2265
            ];
2266
        }
2267
2268
        // AST
2269
2270 767
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2271
2272 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...
2273 595
            $this->identVariableExpressions[$identVariable] = $expr;
2274
        }
2275
2276 767
        return $expr;
2277
    }
2278
2279
    /**
2280
     * SimpleSelectExpression ::= (
2281
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2282
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2283
     * ) [["AS"] AliasResultVariable]
2284
     *
2285
     * @return AST\SimpleSelectExpression
2286
     */
2287 49
    public function SimpleSelectExpression()
2288
    {
2289 49
        $peek = $this->lexer->glimpse();
2290
2291 49
        switch ($this->lexer->lookahead['type']) {
2292
            case Lexer::T_IDENTIFIER:
2293 19
                switch (true) {
2294 19
                    case ($peek['type'] === Lexer::T_DOT):
2295 16
                        $expression = $this->StateFieldPathExpression();
2296
2297 16
                        return new AST\SimpleSelectExpression($expression);
2298
2299 3
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2300 2
                        $expression = $this->IdentificationVariable();
2301
2302 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

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

3178
        $likeExpr      = new AST\LikeExpression(/** @scrutinizer ignore-type */ $stringExpr, $stringPattern, $escapeChar);
Loading history...
3179 14
        $likeExpr->not = $not;
3180
3181 14
        return $likeExpr;
3182
    }
3183
3184
    /**
3185
     * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
3186
     *
3187
     * @return AST\NullComparisonExpression
3188
     */
3189 13
    public function NullComparisonExpression()
3190
    {
3191
        switch (true) {
3192 13
            case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER):
3193
                $this->match(Lexer::T_INPUT_PARAMETER);
3194
3195
                $expr = new AST\InputParameter($this->lexer->token['value']);
3196
                break;
3197
3198 13
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3199 1
                $expr = $this->NullIfExpression();
3200 1
                break;
3201
3202 13
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3203 1
                $expr = $this->CoalesceExpression();
3204 1
                break;
3205
3206 13
            case $this->isFunction():
3207 2
                $expr = $this->FunctionDeclaration();
3208 2
                break;
3209
3210
            default:
3211
                // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
3212 12
                $glimpse = $this->lexer->glimpse();
3213
3214 12
                if ($glimpse['type'] === Lexer::T_DOT) {
3215 8
                    $expr = $this->SingleValuedPathExpression();
3216
3217
                    // Leave switch statement
3218 8
                    break;
3219
                }
3220
3221 4
                $lookaheadValue = $this->lexer->lookahead['value'];
3222
3223
                // Validate existing component
3224 4
                if (! isset($this->queryComponents[$lookaheadValue])) {
3225
                    $this->semanticalError('Cannot add having condition on undefined result variable.');
3226
                }
3227
3228
                // Validate SingleValuedPathExpression (ie.: "product")
3229 4
                if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {
3230 1
                    $expr = $this->SingleValuedPathExpression();
3231 1
                    break;
3232
                }
3233
3234
                // Validating ResultVariable
3235 3
                if (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3236
                    $this->semanticalError('Cannot add having condition on a non result variable.');
3237
                }
3238
3239 3
                $expr = $this->ResultVariable();
3240 3
                break;
3241
        }
3242
3243 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

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