Passed
Pull Request — master (#7607)
by
unknown
09:28
created

Parser::GroupByItem()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

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

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

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

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

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

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

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

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

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

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

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