Parser::SimpleConditionalExpression()   F
last analyzed

Complexity

Conditions 21
Paths 209

Size

Total Lines 88
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 21.0055

Importance

Changes 0
Metric Value
cc 21
eloc 43
nc 209
nop 0
dl 0
loc 88
ccs 42
cts 43
cp 0.9767
crap 21.0055
rs 3.2208
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 function array_intersect;
16
use function array_search;
17
use function call_user_func;
18
use function class_exists;
19
use function count;
20
use function explode;
21
use function implode;
22
use function in_array;
23
use function interface_exists;
24
use function is_string;
25
use function sprintf;
26
use function strlen;
27
use function strpos;
28
use function strrpos;
29
use function strtolower;
30
use function substr;
31
32
/**
33
 * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
34
 * Parses a DQL query, reports any errors in it, and generates an AST.
35
 */
36
class Parser
37
{
38
    /**
39
     * READ-ONLY: Maps BUILT-IN string function names to AST class names.
40
     *
41
     * @var string[]
42
     */
43
    private static $_STRING_FUNCTIONS = [
44
        'concat'    => Functions\ConcatFunction::class,
45
        'substring' => Functions\SubstringFunction::class,
46
        'trim'      => Functions\TrimFunction::class,
47
        'lower'     => Functions\LowerFunction::class,
48
        'upper'     => Functions\UpperFunction::class,
49
        'identity'  => Functions\IdentityFunction::class,
50
    ];
51
52
    /**
53
     * READ-ONLY: Maps BUILT-IN numeric function names to AST class names.
54
     *
55
     * @var string[]
56
     */
57
    private static $_NUMERIC_FUNCTIONS = [
58
        'length'    => Functions\LengthFunction::class,
59
        'locate'    => Functions\LocateFunction::class,
60
        'abs'       => Functions\AbsFunction::class,
61
        'sqrt'      => Functions\SqrtFunction::class,
62
        'mod'       => Functions\ModFunction::class,
63
        'size'      => Functions\SizeFunction::class,
64
        'date_diff' => Functions\DateDiffFunction::class,
65
        'bit_and'   => Functions\BitAndFunction::class,
66
        'bit_or'    => Functions\BitOrFunction::class,
67
68
        // Aggregate functions
69
        'min'       => Functions\MinFunction::class,
70
        'max'       => Functions\MaxFunction::class,
71
        'avg'       => Functions\AvgFunction::class,
72
        'sum'       => Functions\SumFunction::class,
73
        'count'     => Functions\CountFunction::class,
74
    ];
75
76
    /**
77
     * READ-ONLY: Maps BUILT-IN datetime function names to AST class names.
78
     *
79
     * @var string[]
80
     */
81
    private static $_DATETIME_FUNCTIONS = [
82
        'current_date'      => Functions\CurrentDateFunction::class,
83
        'current_time'      => Functions\CurrentTimeFunction::class,
84
        'current_timestamp' => Functions\CurrentTimestampFunction::class,
85
        'date_add'          => Functions\DateAddFunction::class,
86
        'date_sub'          => Functions\DateSubFunction::class,
87
    ];
88
89
    /*
90
     * Expressions that were encountered during parsing of identifiers and expressions
91
     * and still need to be validated.
92
     */
93
94
    /** @var mixed[][] */
95
    private $deferredIdentificationVariables = [];
96
97
    /** @var mixed[][] */
98
    private $deferredPartialObjectExpressions = [];
99
100
    /** @var mixed[][] */
101
    private $deferredPathExpressions = [];
102
103
    /** @var mixed[][] */
104
    private $deferredResultVariables = [];
105
106
    /** @var mixed[][] */
107
    private $deferredNewObjectExpressions = [];
108
109
    /**
110
     * The lexer.
111
     *
112
     * @var Lexer
113
     */
114
    private $lexer;
115
116
    /**
117
     * The parser result.
118
     *
119
     * @var ParserResult
120
     */
121
    private $parserResult;
122
123
    /**
124
     * The EntityManager.
125
     *
126
     * @var EntityManagerInterface
127
     */
128
    private $em;
129
130
    /**
131
     * The Query to parse.
132
     *
133
     * @var Query
134
     */
135
    private $query;
136
137
    /**
138
     * Map of declared query components in the parsed query.
139
     *
140
     * @var mixed[][]
141
     */
142
    private $queryComponents = [];
143
144
    /**
145
     * Keeps the nesting level of defined ResultVariables.
146
     *
147
     * @var int
148
     */
149
    private $nestingLevel = 0;
150
151
    /**
152
     * Any additional custom tree walkers that modify the AST.
153
     *
154
     * @var string[]
155
     */
156
    private $customTreeWalkers = [];
157
158
    /**
159
     * The custom last tree walker, if any, that is responsible for producing the output.
160
     *
161
     * @var TreeWalker
162
     */
163
    private $customOutputWalker;
164
165
    /** @var Node[] */
166
    private $identVariableExpressions = [];
167
168
    /**
169
     * Creates a new query parser object.
170
     *
171
     * @param Query $query The Query to parse.
172
     */
173 842
    public function __construct(Query $query)
174
    {
175 842
        $this->query        = $query;
176 842
        $this->em           = $query->getEntityManager();
177 842
        $this->lexer        = new Lexer($query->getDQL());
178 842
        $this->parserResult = new ParserResult();
179 842
    }
180
181
    /**
182
     * Sets a custom tree walker that produces output.
183
     * This tree walker will be run last over the AST, after any other walkers.
184
     *
185
     * @param string $className
186
     */
187 120
    public function setCustomOutputTreeWalker($className)
188
    {
189 120
        $this->customOutputWalker = $className;
0 ignored issues
show
Documentation Bug introduced by
It seems like $className of type string is incompatible with the declared type Doctrine\ORM\Query\TreeWalker of property $customOutputWalker.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
190 120
    }
191
192
    /**
193
     * Adds a custom tree walker for modifying the AST.
194
     *
195
     * @param string $className
196
     */
197
    public function addCustomTreeWalker($className)
198
    {
199
        $this->customTreeWalkers[] = $className;
200
    }
201
202
    /**
203
     * Gets the lexer used by the parser.
204
     *
205
     * @return Lexer
206
     */
207 31
    public function getLexer()
208
    {
209 31
        return $this->lexer;
210
    }
211
212
    /**
213
     * Gets the ParserResult that is being filled with information during parsing.
214
     *
215
     * @return ParserResult
216
     */
217
    public function getParserResult()
218
    {
219
        return $this->parserResult;
220
    }
221
222
    /**
223
     * Gets the EntityManager used by the parser.
224
     *
225
     * @return EntityManagerInterface
226
     */
227 10
    public function getEntityManager()
228
    {
229 10
        return $this->em;
230
    }
231
232
    /**
233
     * Parses and builds AST for the given Query.
234
     *
235
     * @return AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement
236
     */
237 842
    public function getAST()
238
    {
239
        // Parse & build AST
240 842
        $AST = $this->QueryLanguage();
241
242
        // Process any deferred validations of some nodes in the AST.
243
        // This also allows post-processing of the AST for modification purposes.
244 807
        $this->processDeferredIdentificationVariables();
245
246 805
        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...
247 9
            $this->processDeferredPartialObjectExpressions();
248
        }
249
250 804
        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...
251 594
            $this->processDeferredPathExpressions();
252
        }
253
254 802
        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...
255 32
            $this->processDeferredResultVariables();
256
        }
257
258 802
        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...
259 26
            $this->processDeferredNewObjectExpressions($AST);
0 ignored issues
show
Bug introduced by
$AST of type Doctrine\ORM\Query\AST\U...ery\AST\DeleteStatement is incompatible with the type Doctrine\ORM\Query\AST\SelectClause expected by parameter $AST of Doctrine\ORM\Query\Parse...dNewObjectExpressions(). ( Ignorable by Annotation )

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

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

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

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

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

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

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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

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

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

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

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