Completed
Pull Request — master (#8105)
by
unknown
10:29
created

Parser::RangeVariableDeclaration()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 31
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4.0047

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 31
ccs 14
cts 15
cp 0.9333
rs 9.6666
c 0
b 0
f 0
cc 4
nc 4
nop 0
crap 4.0047
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 462
    public function __construct(Query $query)
176
    {
177 462
        $this->query        = $query;
178 462
        $this->em           = $query->getEntityManager();
179 462
        $this->lexer        = new Lexer($query->getDQL());
180 462
        $this->parserResult = new ParserResult();
181 462
    }
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 28
    public function getLexer()
210
    {
211 28
        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 9
    public function getEntityManager()
230
    {
231 9
        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 462
    public function getAST()
240
    {
241
        // Parse & build AST
242 462
        $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 427
        $this->processDeferredIdentificationVariables();
247
248 425
        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 5
            $this->processDeferredPartialObjectExpressions();
250
        }
251
252 424
        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 331
            $this->processDeferredPathExpressions();
254
        }
255
256 422
        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 27
            $this->processDeferredResultVariables();
258
        }
259
260 422
        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 3
            $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 422
        $this->processRootEntityAliasSelected();
265
266
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
267 421
        $this->fixIdentificationVariableOrder($AST);
268
269 421
        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 473
    public function match($token)
283
    {
284 473
        $lookaheadType = $this->lexer->lookahead['type'];
285
286
        // Short-circuit on first condition, usually types match
287 473
        if ($lookaheadType === $token) {
288 465
            $this->lexer->moveNext();
289
290 465
            return;
291
        }
292
293
        // If parameter is not identifier (1-99) must be exact match
294 21
        if ($token < Lexer::T_IDENTIFIER) {
295 3
            $this->syntaxError($this->lexer->getLiteral($token));
296
        }
297
298
        // If parameter is keyword (200+) must be exact match
299 18
        if ($token > Lexer::T_IDENTIFIER) {
300 7
            $this->syntaxError($this->lexer->getLiteral($token));
301
        }
302
303
        // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
304 11
        if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
305 8
            $this->syntaxError($this->lexer->getLiteral($token));
306
        }
307
308 3
        $this->lexer->moveNext();
309 3
    }
310
311
    /**
312
     * Frees this parser, enabling it to be reused.
313
     *
314
     * @param bool $deep     Whether to clean peek and reset errors.
315
     * @param int  $position Position to reset.
316
     */
317
    public function free($deep = false, $position = 0)
318
    {
319
        // WARNING! Use this method with care. It resets the scanner!
320
        $this->lexer->resetPosition($position);
321
322
        // Deep = true cleans peek and also any previously defined errors
323
        if ($deep) {
324
            $this->lexer->resetPeek();
325
        }
326
327
        $this->lexer->token     = null;
328
        $this->lexer->lookahead = null;
329
    }
330
331
    /**
332
     * Parses a query string.
333
     *
334
     * @return ParserResult
335
     */
336 462
    public function parse()
337
    {
338 462
        $AST = $this->getAST();
339
340 421
        $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
341 421
        if ($customWalkers !== false) {
342 35
            $this->customTreeWalkers = $customWalkers;
343
        }
344
345 421
        $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER);
346
347 421
        if ($customOutputWalker !== false) {
348 30
            $this->customOutputWalker = $customOutputWalker;
349
        }
350
351
        // Run any custom tree walkers over the AST
352 421
        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...
353 34
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
354
355 34
            foreach ($this->customTreeWalkers as $walker) {
356 34
                $treeWalkerChain->addTreeWalker($walker);
357
            }
358
359
            switch (true) {
360 34
                case $AST instanceof AST\UpdateStatement:
361
                    $treeWalkerChain->walkUpdateStatement($AST);
362
                    break;
363
364 34
                case $AST instanceof AST\DeleteStatement:
365
                    $treeWalkerChain->walkDeleteStatement($AST);
366
                    break;
367
368 34
                case $AST instanceof AST\SelectStatement:
369
                default:
370 34
                    $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

370
                    $treeWalkerChain->walkSelectStatement(/** @scrutinizer ignore-type */ $AST);
Loading history...
371
            }
372
373 33
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
374
        }
375
376 420
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
377 420
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
378
379
        // Assign an SQL executor to the parser result
380 420
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
381
382 413
        return $this->parserResult;
383
    }
384
385
    /**
386
     * Fixes order of identification variables.
387
     *
388
     * They have to appear in the select clause in the same order as the
389
     * declarations (from ... x join ... y join ... z ...) appear in the query
390
     * as the hydration process relies on that order for proper operation.
391
     *
392
     * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
393
     */
394 421
    private function fixIdentificationVariableOrder($AST)
395
    {
396 421
        if (count($this->identVariableExpressions) <= 1) {
397 368
            return;
398
        }
399
400 53
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
401 53
            if (! isset($this->identVariableExpressions[$dqlAlias])) {
402 7
                continue;
403
            }
404
405 53
            $expr = $this->identVariableExpressions[$dqlAlias];
406 53
            $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\DeleteStatement.
Loading history...
Bug introduced by
The property selectClause does not seem to exist on Doctrine\ORM\Query\AST\UpdateStatement.
Loading history...
407
408 53
            unset($AST->selectClause->selectExpressions[$key]);
409
410 53
            $AST->selectClause->selectExpressions[] = $expr;
411
        }
412 53
    }
413
414
    /**
415
     * Generates a new syntax error.
416
     *
417
     * @param string       $expected Expected string.
418
     * @param mixed[]|null $token    Got token.
419
     *
420
     * @throws QueryException
421
     */
422 18
    public function syntaxError($expected = '', $token = null)
423
    {
424 18
        if ($token === null) {
425 15
            $token = $this->lexer->lookahead;
426
        }
427
428 18
        $tokenPos = $token['position'] ?? '-1';
429
430 18
        $message  = sprintf('line 0, col %d: Error: ', $tokenPos);
431 18
        $message .= $expected !== '' ? sprintf('Expected %s, got ', $expected) : 'Unexpected ';
432 18
        $message .= $this->lexer->lookahead === null ? 'end of string.' : sprintf("'%s'", $token['value']);
433
434 18
        throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL()));
435
    }
436
437
    /**
438
     * Generates a new semantical error.
439
     *
440
     * @param string       $message Optional message.
441
     * @param mixed[]|null $token   Optional token.
442
     *
443
     * @throws QueryException
444
     */
445 22
    public function semanticalError($message = '', $token = null, ?Throwable $previousFailure = null)
446
    {
447 22
        if ($token === null) {
448 2
            $token = $this->lexer->lookahead;
449
        }
450
451
        // Minimum exposed chars ahead of token
452 22
        $distance = 12;
453
454
        // Find a position of a final word to display in error string
455 22
        $dql      = $this->query->getDQL();
456 22
        $length   = strlen($dql);
457 22
        $tokenPos = $token && isset($token['position']) && $token['position'] > 0 ? $token['position'] : 0;
458 22
        $pos      = $tokenPos + $distance;
459 22
        $pos      = strpos($dql, ' ', $length > $pos ? $pos : $length);
460 22
        $length   = $pos !== false ? $pos - $tokenPos : $distance;
461 22
        $tokenStr = substr($dql, $tokenPos, $length);
462
463
        // Building informative message
464 22
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
465
466 22
        throw QueryException::semanticalError(
467 22
            $message,
468 22
            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 117
    private function peekBeyondClosingParenthesis($resetPeek = true)
480
    {
481 117
        $token        = $this->lexer->peek();
482 117
        $numUnmatched = 1;
483
484 117
        while ($numUnmatched > 0 && $token !== null) {
485 116
            switch ($token['type']) {
486
                case Lexer::T_OPEN_PARENTHESIS:
487 25
                    ++$numUnmatched;
488 25
                    break;
489
490
                case Lexer::T_CLOSE_PARENTHESIS:
491 116
                    --$numUnmatched;
492 116
                    break;
493
494
                default:
495
                    // Do nothing
496
            }
497
498 116
            $token = $this->lexer->peek();
499
        }
500
501 117
        if ($resetPeek) {
502 97
            $this->lexer->resetPeek();
503
        }
504
505 117
        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 161
    private function isMathOperator($token)
516
    {
517 161
        return $token && in_array($token['type'], [Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY], true);
0 ignored issues
show
Bug Best Practice introduced by
The expression $token of type 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...
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 220
    private function isFunction()
526
    {
527 220
        $lookaheadType = $this->lexer->lookahead['type'];
528 220
        $peek          = $this->lexer->peek();
529
530 220
        $this->lexer->resetPeek();
531
532 220
        return $lookaheadType >= Lexer::T_IDENTIFIER && $peek && $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 185
    private function isNextAllAnySome()
553
    {
554 185
        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 427
    private function processDeferredIdentificationVariables()
562
    {
563 427
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
564 420
            $identVariable = $deferredItem['expression'];
565
566
            // Check if IdentificationVariable exists in queryComponents
567 420
            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 420
            $qComp = $this->queryComponents[$identVariable];
575
576
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
577 420
            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 420
            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 425
    }
593
594
    /**
595
     * Validates that the given <tt>NewObjectExpression</tt>.
596
     *
597
     * @param AST\SelectClause $AST
598
     */
599 3
    private function processDeferredNewObjectExpressions($AST)
600
    {
601 3
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
602 3
            $expression    = $deferredItem['expression'];
603 3
            $token         = $deferredItem['token'];
604 3
            $className     = $expression->className;
605 3
            $args          = $expression->args;
606 3
            $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 3
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
610
                $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
611
                $fqcn      = $namespace . '\\' . $className;
612
613
                if (class_exists($fqcn)) {
614
                    $expression->className = $fqcn;
615
                    $className             = $fqcn;
616
                }
617
            }
618
619 3
            if (! class_exists($className)) {
620
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
621
            }
622
623 3
            $class = new ReflectionClass($className);
624
625 3
            if (! $class->isInstantiable()) {
626
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
627
            }
628
629 3
            if ($class->getConstructor() === null) {
630
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
631
            }
632
633 3
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
634
                $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
635
            }
636
        }
637 3
    }
638
639
    /**
640
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
641
     * It must exist in query components list.
642
     */
643 5
    private function processDeferredPartialObjectExpressions()
644
    {
645 5
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
646 5
            $expr  = $deferredItem['expression'];
647 5
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
648
649 5
            foreach ($expr->partialFieldSet as $field) {
650 5
                $property = $class->getProperty($field);
651
652 5
                if ($property instanceof FieldMetadata ||
653 5
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
654 5
                    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 5
            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 4
    }
671
672
    /**
673
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
674
     * It must exist in query components list.
675
     */
676 27
    private function processDeferredResultVariables()
677
    {
678 27
        foreach ($this->deferredResultVariables as $deferredItem) {
679 27
            $resultVariable = $deferredItem['expression'];
680
681
            // Check if ResultVariable exists in queryComponents
682 27
            if (! isset($this->queryComponents[$resultVariable])) {
683
                $this->semanticalError(
684
                    sprintf("'%s' is not defined.", $resultVariable),
685
                    $deferredItem['token']
686
                );
687
            }
688
689 27
            $qComp = $this->queryComponents[$resultVariable];
690
691
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
692 27
            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 27
            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 27
    }
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 331
    private function processDeferredPathExpressions()
719
    {
720 331
        foreach ($this->deferredPathExpressions as $deferredItem) {
721 331
            $pathExpression = $deferredItem['expression'];
722
723 331
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
724 331
            $class = $qComp['metadata'];
725 331
            $field = $pathExpression->field;
726
727 331
            if ($field === null) {
728 23
                $field = $pathExpression->field = $class->identifier[0];
729
            }
730
731 331
            $property = $class->getProperty($field);
732
733
            // Check if field or association exists
734 331
            if (! $property) {
735
                $this->semanticalError(
736
                    'Class ' . $class->getClassName() . ' has no field or association named ' . $field,
737
                    $deferredItem['token']
738
                );
739
            }
740
741 331
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
742
743 331
            if ($property instanceof AssociationMetadata) {
744 63
                $fieldType = $property instanceof ToOneAssociationMetadata
745 44
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
746 63
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
747
            }
748
749
            // Validate if PathExpression is one of the expected types
750 331
            $expectedType = $pathExpression->expectedType;
751
752 331
            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 329
            $pathExpression->type = $fieldType;
782
        }
783 329
    }
784
785 422
    private function processRootEntityAliasSelected()
786
    {
787 422
        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 171
            return;
789
        }
790
791 257
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
792 257
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
793 256
                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 462
    public function QueryLanguage()
806
    {
807 462
        $statement = null;
808
809 462
        $this->lexer->moveNext();
810
811
        // Check if we do have something to be parsed
812 462
        if ($this->lexer->lookahead === null) {
813 1
            $this->syntaxError('SELECT, UPDATE or DELETE');
814
        }
815
816 461
        switch ($this->lexer->lookahead['type']) {
817
            case Lexer::T_SELECT:
818 407
                $statement = $this->SelectStatement();
819 376
                break;
820
821
            case Lexer::T_UPDATE:
822 23
                $statement = $this->UpdateStatement();
823 23
                break;
824
825
            case Lexer::T_DELETE:
826 33
                $statement = $this->DeleteStatement();
827 33
                break;
828
829
            default:
830 1
                $this->syntaxError('SELECT, UPDATE or DELETE');
831
                break;
832
        }
833
834
        // Check for end of string
835 431
        if ($this->lexer->lookahead !== null) {
836 4
            $this->syntaxError('end of string');
837
        }
838
839 427
        return $statement;
840
    }
841
842
    /**
843
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
844
     *
845
     * @return AST\SelectStatement
846
     */
847 407
    public function SelectStatement()
848
    {
849 407
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
850
851 380
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
852 377
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
853 376
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
854 376
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
855
856 376
        return $selectStatement;
857
    }
858
859
    /**
860
     * UpdateStatement ::= UpdateClause [WhereClause]
861
     *
862
     * @return AST\UpdateStatement
863
     */
864 23
    public function UpdateStatement()
865
    {
866 23
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
867
868 23
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
869
870 23
        return $updateStatement;
871
    }
872
873
    /**
874
     * DeleteStatement ::= DeleteClause [WhereClause]
875
     *
876
     * @return AST\DeleteStatement
877
     */
878 33
    public function DeleteStatement()
879
    {
880 33
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
881
882 33
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
883
884 33
        return $deleteStatement;
885
    }
886
887
    /**
888
     * IdentificationVariable ::= identifier
889
     *
890
     * @return string
891
     */
892 444
    public function IdentificationVariable()
893
    {
894 444
        $this->match(Lexer::T_IDENTIFIER);
895
896 444
        $identVariable = $this->lexer->token['value'];
897
898 444
        $this->deferredIdentificationVariables[] = [
899 444
            'expression'   => $identVariable,
900 444
            'nestingLevel' => $this->nestingLevel,
901 444
            'token'        => $this->lexer->token,
902
        ];
903
904 444
        return $identVariable;
905
    }
906
907
    /**
908
     * AliasIdentificationVariable = identifier
909
     *
910
     * @return string
911
     */
912 437
    public function AliasIdentificationVariable()
913
    {
914 437
        $this->match(Lexer::T_IDENTIFIER);
915
916 437
        $aliasIdentVariable = $this->lexer->token['value'];
917 437
        $exists             = isset($this->queryComponents[$aliasIdentVariable]);
918
919 437
        if ($exists) {
920 2
            $this->semanticalError(sprintf("'%s' is already defined.", $aliasIdentVariable), $this->lexer->token);
921
        }
922
923 437
        return $aliasIdentVariable;
924
    }
925
926
    /**
927
     * AbstractSchemaName ::= fully_qualified_name | identifier
928
     *
929
     * @return string
930
     */
931 450
    public function AbstractSchemaName()
932
    {
933 450
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
934 443
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
935
936 443
            return $this->lexer->token['value'];
937
        }
938
939 8
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
940 7
            $this->match(Lexer::T_IDENTIFIER);
941
942 7
            return $this->lexer->token['value'];
943
        }
944
945 1
        $this->match(Lexer::T_ALIASED_NAME);
946
947
        [$namespaceAlias, $simpleClassName] = explode(':', $this->lexer->token['value']);
948
949
        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

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

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

3155
        $likeExpr      = new AST\LikeExpression($stringExpr, /** @scrutinizer ignore-type */ $stringPattern, $escapeChar);
Loading history...
3156 14
        $likeExpr->not = $not;
3157
3158 14
        return $likeExpr;
3159
    }
3160
3161
    /**
3162
     * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
3163
     *
3164
     * @return AST\NullComparisonExpression
3165
     */
3166 12
    public function NullComparisonExpression()
3167
    {
3168
        switch (true) {
3169 12
            case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER):
3170
                $this->match(Lexer::T_INPUT_PARAMETER);
3171
3172
                $expr = new AST\InputParameter($this->lexer->token['value']);
3173
                break;
3174
3175 12
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3176 1
                $expr = $this->NullIfExpression();
3177 1
                break;
3178
3179 12
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3180 1
                $expr = $this->CoalesceExpression();
3181 1
                break;
3182
3183 12
            case $this->isFunction():
3184 2
                $expr = $this->FunctionDeclaration();
3185 2
                break;
3186
3187
            default:
3188
                // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
3189 11
                $glimpse = $this->lexer->glimpse();
3190
3191 11
                if ($glimpse['type'] === Lexer::T_DOT) {
3192 8
                    $expr = $this->SingleValuedPathExpression();
3193
3194
                    // Leave switch statement
3195 8
                    break;
3196
                }
3197
3198 3
                $lookaheadValue = $this->lexer->lookahead['value'];
3199
3200
                // Validate existing component
3201 3
                if (! isset($this->queryComponents[$lookaheadValue])) {
3202
                    $this->semanticalError('Cannot add having condition on undefined result variable.');
3203
                }
3204
3205
                // Validate SingleValuedPathExpression (ie.: "product")
3206 3
                if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {
3207
                    $expr = $this->SingleValuedPathExpression();
3208
                    break;
3209
                }
3210
3211
                // Validating ResultVariable
3212 3
                if (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3213
                    $this->semanticalError('Cannot add having condition on a non result variable.');
3214
                }
3215
3216 3
                $expr = $this->ResultVariable();
3217 3
                break;
3218
        }
3219
3220 12
        $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

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