Completed
Pull Request — master (#7405)
by Michael
68:49 queued 63:08
created

Parser::NewObjectExpression()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 15
nc 2
nop 0
dl 0
loc 29
ccs 16
cts 16
cp 1
crap 2
rs 9.7666
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Query;
6
7
use Doctrine\Common\Persistence\Mapping\MappingException;
8
use Doctrine\ORM\EntityManagerInterface;
9
use Doctrine\ORM\Mapping\AssociationMetadata;
10
use Doctrine\ORM\Mapping\FieldMetadata;
11
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
12
use Doctrine\ORM\Query;
13
use Doctrine\ORM\Query\AST\Functions;
14
use Doctrine\ORM\Query\AST\Node;
15
use 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 848
    public function __construct(Query $query)
174
    {
175 848
        $this->query        = $query;
176 848
        $this->em           = $query->getEntityManager();
177 848
        $this->lexer        = new Lexer($query->getDQL());
178 848
        $this->parserResult = new ParserResult();
179 848
    }
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 121
    public function setCustomOutputTreeWalker($className)
188
    {
189 121
        $this->customOutputWalker = $className;
0 ignored issues
show
Documentation Bug introduced by
It seems like $className of type string is incompatible with the declared type Doctrine\ORM\Query\TreeWalker of property $customOutputWalker.

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

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

Loading history...
190 121
    }
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 33
    public function getLexer()
208
    {
209 33
        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 848
    public function getAST()
238
    {
239
        // Parse & build AST
240 848
        $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 813
        $this->processDeferredIdentificationVariables();
245
246 811
        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 810
        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 598
            $this->processDeferredPathExpressions();
252
        }
253
254 808
        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 808
        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 804
        $this->processRootEntityAliasSelected();
263
264
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
265 803
        $this->fixIdentificationVariableOrder($AST);
266
267 803
        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 859
    public function match($token)
281
    {
282 859
        $lookaheadType = $this->lexer->lookahead['type'];
283
284
        // Short-circuit on first condition, usually types match
285 859
        if ($lookaheadType === $token) {
286 851
            $this->lexer->moveNext();
287 851
            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 848
    public function parse()
334
    {
335 848
        $AST = $this->getAST();
336
337 803
        $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
338 803
        if ($customWalkers !== false) {
339 99
            $this->customTreeWalkers = $customWalkers;
340
        }
341
342 803
        $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER);
343
344 803
        if ($customOutputWalker !== false) {
345 78
            $this->customOutputWalker = $customOutputWalker;
346
        }
347
348
        // Run any custom tree walkers over the AST
349 803
        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 98
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
351
352 98
            foreach ($this->customTreeWalkers as $walker) {
353 98
                $treeWalkerChain->addTreeWalker($walker);
354
            }
355
356
            switch (true) {
357 98
                case ($AST instanceof AST\UpdateStatement):
358
                    $treeWalkerChain->walkUpdateStatement($AST);
359
                    break;
360
361 98
                case ($AST instanceof AST\DeleteStatement):
362
                    $treeWalkerChain->walkDeleteStatement($AST);
363
                    break;
364
365 98
                case ($AST instanceof AST\SelectStatement):
366
                default:
367 98
                    $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 92
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
371
        }
372
373 797
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
374 797
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
375
376
        // Assign an SQL executor to the parser result
377 797
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
378
379 789
        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 803
    private function fixIdentificationVariableOrder($AST)
392
    {
393 803
        if (count($this->identVariableExpressions) <= 1) {
394 628
            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 177
    private function peekBeyondClosingParenthesis($resetPeek = true)
478
    {
479 177
        $token        = $this->lexer->peek();
480 177
        $numUnmatched = 1;
481
482 177
        while ($numUnmatched > 0 && $token !== null) {
483 176
            switch ($token['type']) {
484
                case Lexer::T_OPEN_PARENTHESIS:
485 47
                    ++$numUnmatched;
486 47
                    break;
487
488
                case Lexer::T_CLOSE_PARENTHESIS:
489 176
                    --$numUnmatched;
490 176
                    break;
491
492
                default:
493
                    // Do nothing
494
            }
495
496 176
            $token = $this->lexer->peek();
497
        }
498
499 177
        if ($resetPeek) {
500 156
            $this->lexer->resetPeek();
501
        }
502
503 177
        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 363
    private function isMathOperator($token)
514
    {
515 363
        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 406
    private function isFunction()
524
    {
525 406
        $lookaheadType = $this->lexer->lookahead['type'];
526 406
        $peek          = $this->lexer->peek();
527
528 406
        $this->lexer->resetPeek();
529
530 406
        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 4
    private function isAggregateFunction($tokenType)
541
    {
542 4
        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 304
    private function isNextAllAnySome()
551
    {
552 304
        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 813
    private function processDeferredIdentificationVariables()
560
    {
561 813
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
562 789
            $identVariable = $deferredItem['expression'];
563
564
            // Check if IdentificationVariable exists in queryComponents
565 789
            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 789
            $qComp = $this->queryComponents[$identVariable];
573
574
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
575 789
            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 789
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
584 1
                $this->semanticalError(
585 1
                    sprintf("'%s' is used outside the scope of its declaration.", $identVariable),
586 789
                    $deferredItem['token']
587
                );
588
            }
589
        }
590 811
    }
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 598
    private function processDeferredPathExpressions()
717
    {
718 598
        foreach ($this->deferredPathExpressions as $deferredItem) {
719 598
            $pathExpression = $deferredItem['expression'];
720
721 598
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
722 598
            $class = $qComp['metadata'];
723 598
            $field = $pathExpression->field;
724
725 598
            if ($field === null) {
726 41
                $field = $pathExpression->field = $class->identifier[0];
727
            }
728
729 598
            $property = $class->getProperty($field);
730
731
            // Check if field or association exists
732 598
            if (! $property) {
733
                $this->semanticalError(
734
                    'Class ' . $class->getClassName() . ' has no field or association named ' . $field,
735
                    $deferredItem['token']
736
                );
737
            }
738
739 598
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
740
741 598
            if ($property instanceof AssociationMetadata) {
742 90
                $fieldType = $property instanceof ToOneAssociationMetadata
743 67
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
744 90
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
745
                ;
746
            }
747
748
            // Validate if PathExpression is one of the expected types
749 598
            $expectedType = $pathExpression->expectedType;
750
751 598
            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 596
            $pathExpression->type = $fieldType;
781
        }
782 596
    }
783
784 804
    private function processRootEntityAliasSelected()
785
    {
786 804
        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 239
            return;
788
        }
789
790 575
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
791 575
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
792 575
                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 848
    public function QueryLanguage()
805
    {
806 848
        $statement = null;
807
808 848
        $this->lexer->moveNext();
809
810 848
        switch ($this->lexer->lookahead['type']) {
811
            case Lexer::T_SELECT:
812 783
                $statement = $this->SelectStatement();
813 752
                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 817
        if ($this->lexer->lookahead !== null) {
830 4
            $this->syntaxError('end of string');
831
        }
832
833 813
        return $statement;
834
    }
835
836
    /**
837
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
838
     *
839
     * @return AST\SelectStatement
840
     */
841 783
    public function SelectStatement()
842
    {
843 783
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
844
845 756
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
846 753
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
847 752
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
848 752
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
849
850 752
        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 813
    public function IdentificationVariable()
887
    {
888 813
        $this->match(Lexer::T_IDENTIFIER);
889
890 813
        $identVariable = $this->lexer->token['value'];
891
892 813
        $this->deferredIdentificationVariables[] = [
893 813
            'expression'   => $identVariable,
894 813
            'nestingLevel' => $this->nestingLevel,
895 813
            'token'        => $this->lexer->token,
896
        ];
897
898 813
        return $identVariable;
899
    }
900
901
    /**
902
     * AliasIdentificationVariable = identifier
903
     *
904
     * @return string
905
     */
906 823
    public function AliasIdentificationVariable()
907
    {
908 823
        $this->match(Lexer::T_IDENTIFIER);
909
910 823
        $aliasIdentVariable = $this->lexer->token['value'];
911 823
        $exists             = isset($this->queryComponents[$aliasIdentVariable]);
912
913 823
        if ($exists) {
914 2
            $this->semanticalError(sprintf("'%s' is already defined.", $aliasIdentVariable), $this->lexer->token);
915
        }
916
917 823
        return $aliasIdentVariable;
918
    }
919
920
    /**
921
     * AbstractSchemaName ::= fully_qualified_name | identifier
922
     *
923
     * @return string
924
     */
925 836
    public function AbstractSchemaName()
926
    {
927 836
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
928 829
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
929
930 829
            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 832
    private function validateAbstractSchemaName($schemaName) : void
954
    {
955 832
        if (class_exists($schemaName, true) || interface_exists($schemaName, true)) {
956 823
            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 136
    public function AliasResultVariable()
979
    {
980 136
        $this->match(Lexer::T_IDENTIFIER);
981
982 132
        $resultVariable = $this->lexer->token['value'];
983 132
        $exists         = isset($this->queryComponents[$resultVariable]);
984
985 132
        if ($exists) {
986 2
            $this->semanticalError(sprintf("'%s' is already defined.", $resultVariable), $this->lexer->token);
987
        }
988
989 132
        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 608
    public function PathExpression($expectedTypes)
1056
    {
1057 608
        $identVariable = $this->IdentificationVariable();
1058 608
        $field         = null;
1059
1060 608
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1061 602
            $this->match(Lexer::T_DOT);
1062 602
            $this->match(Lexer::T_IDENTIFIER);
1063
1064 602
            $field = $this->lexer->token['value'];
1065
1066 602
            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 608
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1075
1076
        // Defer PathExpression validation if requested to be deferred
1077 608
        $this->deferredPathExpressions[] = [
1078 608
            'expression'   => $pathExpr,
1079 608
            'nestingLevel' => $this->nestingLevel,
1080 608
            'token'        => $this->lexer->token,
1081
        ];
1082
1083 608
        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 517
    public function SingleValuedPathExpression()
1105
    {
1106 517
        return $this->PathExpression(
1107 517
            AST\PathExpression::TYPE_STATE_FIELD |
1108 517
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1109
        );
1110
    }
1111
1112
    /**
1113
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1114
     *
1115
     * @return AST\PathExpression
1116
     */
1117 209
    public function StateFieldPathExpression()
1118
    {
1119 209
        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 783
    public function SelectClause()
1148
    {
1149 783
        $isDistinct = false;
1150 783
        $this->match(Lexer::T_SELECT);
1151
1152
        // Check for DISTINCT
1153 783
        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 783
        $selectExpressions   = [];
1161 783
        $selectExpressions[] = $this->SelectExpression();
1162
1163 775
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1164 299
            $this->match(Lexer::T_COMMA);
1165
1166 299
            $selectExpressions[] = $this->SelectExpression();
1167
        }
1168
1169 774
        return new AST\SelectClause($selectExpressions, $isDistinct);
1170
    }
1171
1172
    /**
1173
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1174
     *
1175
     * @return AST\SimpleSelectClause
1176
     */
1177 50
    public function SimpleSelectClause()
1178
    {
1179 50
        $isDistinct = false;
1180 50
        $this->match(Lexer::T_SELECT);
1181
1182 50
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1183
            $this->match(Lexer::T_DISTINCT);
1184
1185
            $isDistinct = true;
1186
        }
1187
1188 50
        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 774
    public function FromClause()
1294
    {
1295 774
        $this->match(Lexer::T_FROM);
1296
1297 769
        $identificationVariableDeclarations   = [];
1298 769
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1299
1300 756
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1301 7
            $this->match(Lexer::T_COMMA);
1302
1303 7
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1304
        }
1305
1306 756
        return new AST\FromClause($identificationVariableDeclarations);
1307
    }
1308
1309
    /**
1310
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1311
     *
1312
     * @return AST\SubselectFromClause
1313
     */
1314 50
    public function SubselectFromClause()
1315
    {
1316 50
        $this->match(Lexer::T_FROM);
1317
1318 50
        $identificationVariables   = [];
1319 50
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1320
1321 49
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1322
            $this->match(Lexer::T_COMMA);
1323
1324
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1325
        }
1326
1327 49
        return new AST\SubselectFromClause($identificationVariables);
1328
    }
1329
1330
    /**
1331
     * WhereClause ::= "WHERE" ConditionalExpression
1332
     *
1333
     * @return AST\WhereClause
1334
     */
1335 345
    public function WhereClause()
1336
    {
1337 345
        $this->match(Lexer::T_WHERE);
1338
1339 345
        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 36
    public function GroupByClause()
1360
    {
1361 36
        $this->match(Lexer::T_GROUP);
1362 36
        $this->match(Lexer::T_BY);
1363
1364 36
        $groupByItems = [$this->GroupByItem()];
1365
1366 35
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1367 8
            $this->match(Lexer::T_COMMA);
1368
1369 8
            $groupByItems[] = $this->GroupByItem();
1370
        }
1371
1372 35
        return new AST\GroupByClause($groupByItems);
1373
    }
1374
1375
    /**
1376
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1377
     *
1378
     * @return AST\OrderByClause
1379
     */
1380 181
    public function OrderByClause()
1381
    {
1382 181
        $this->match(Lexer::T_ORDER);
1383 181
        $this->match(Lexer::T_BY);
1384
1385 181
        $orderByItems   = [];
1386 181
        $orderByItems[] = $this->OrderByItem();
1387
1388 181
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1389 15
            $this->match(Lexer::T_COMMA);
1390
1391 15
            $orderByItems[] = $this->OrderByItem();
1392
        }
1393
1394 181
        return new AST\OrderByClause($orderByItems);
1395
    }
1396
1397
    /**
1398
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1399
     *
1400
     * @return AST\Subselect
1401
     */
1402 50
    public function Subselect()
1403
    {
1404
        // Increase query nesting level
1405 50
        $this->nestingLevel++;
1406
1407 50
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1408
1409 49
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1410 49
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1411 49
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1412 49
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1413
1414
        // Decrease query nesting level
1415 49
        $this->nestingLevel--;
1416
1417 49
        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 36
    public function GroupByItem()
1442
    {
1443
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1444 36
        $glimpse = $this->lexer->glimpse();
1445
1446 36
        if ($glimpse['type'] === Lexer::T_DOT) {
1447 16
            return $this->SingleValuedPathExpression();
1448
        }
1449
1450
        // Still need to decide between IdentificationVariable or ResultVariable
1451 20
        $lookaheadValue = $this->lexer->lookahead['value'];
1452
1453 20
        if (! isset($this->queryComponents[$lookaheadValue])) {
1454 1
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1455
        }
1456
1457 19
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1458 17
            ? $this->IdentificationVariable()
1459 19
            : $this->ResultVariable();
1460
    }
1461
1462
    /**
1463
     * OrderByItem ::= (
1464
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1465
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1466
     * ) ["ASC" | "DESC"]
1467
     *
1468
     * @return AST\OrderByItem
1469
     */
1470 181
    public function OrderByItem()
1471
    {
1472 181
        $this->lexer->peek(); // lookahead => '.'
1473 181
        $this->lexer->peek(); // lookahead => token after '.'
1474
1475 181
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1476
1477 181
        $this->lexer->resetPeek();
1478
1479 181
        $glimpse = $this->lexer->glimpse();
1480
1481
        switch (true) {
1482 181
            case ($this->isFunction()):
1483 2
                $expr = $this->FunctionDeclaration();
1484 2
                break;
1485
1486 179
            case ($this->isMathOperator($peek)):
1487 25
                $expr = $this->SimpleArithmeticExpression();
1488 25
                break;
1489
1490 155
            case ($glimpse['type'] === Lexer::T_DOT):
1491 140
                $expr = $this->SingleValuedPathExpression();
1492 140
                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 181
        $type = 'ASC';
1504 181
        $item = new AST\OrderByItem($expr);
1505
1506
        switch (true) {
1507 181
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1508 95
                $this->match(Lexer::T_DESC);
1509 95
                $type = 'DESC';
1510 95
                break;
1511
1512 153
            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 181
        $item->type = $type;
1521
1522 181
        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 771
    public function IdentificationVariableDeclaration()
1561
    {
1562 771
        $joins                    = [];
1563 771
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1564 761
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1565 7
            ? $this->IndexBy()
1566 761
            : null;
1567
1568 761
        $rangeVariableDeclaration->isRoot = true;
1569
1570 761
        while ($this->lexer->isNextToken(Lexer::T_LEFT) ||
1571 761
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1572 761
            $this->lexer->isNextToken(Lexer::T_JOIN)
1573
        ) {
1574 278
            $joins[] = $this->Join();
1575
        }
1576
1577 758
        return new AST\IdentificationVariableDeclaration(
1578 758
            $rangeVariableDeclaration,
1579 758
            $indexBy,
1580 758
            $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 50
    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 50
        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 771
    public function RangeVariableDeclaration()
1707
    {
1708 771
        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 770
        $abstractSchemaName = $this->AbstractSchemaName();
1713
1714 769
        $this->validateAbstractSchemaName($abstractSchemaName);
1715
1716 761
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1717 6
            $this->match(Lexer::T_AS);
1718
        }
1719
1720 761
        $token                       = $this->lexer->lookahead;
1721 761
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1722 761
        $classMetadata               = $this->em->getClassMetadata($abstractSchemaName);
1723
1724
        // Building queryComponent
1725
        $queryComponent = [
1726 761
            'metadata'     => $classMetadata,
1727
            'parent'       => null,
1728
            'relation'     => null,
1729
            'map'          => null,
1730 761
            'nestingLevel' => $this->nestingLevel,
1731 761
            'token'        => $token,
1732
        ];
1733
1734 761
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1735
1736 761
        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 164
    public function ScalarExpression()
1917
    {
1918 164
        $lookahead = $this->lexer->lookahead['type'];
1919 164
        $peek      = $this->lexer->glimpse();
1920
1921 164
        switch (true) {
1922
            case ($lookahead === Lexer::T_INTEGER):
1923 161
            case ($lookahead === Lexer::T_FLOAT):
1924
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1925 161
            case ($lookahead === Lexer::T_MINUS):
1926 161
            case ($lookahead === Lexer::T_PLUS):
1927 17
                return $this->SimpleArithmeticExpression();
1928
1929 161
            case ($lookahead === Lexer::T_STRING):
1930 13
                return $this->StringPrimary();
1931
1932 159
            case ($lookahead === Lexer::T_TRUE):
1933 159
            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 159
            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 159
            case ($lookahead === Lexer::T_CASE):
1949 155
            case ($lookahead === Lexer::T_COALESCE):
1950 155
            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 155
            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 152
            case ($this->isFunction()):
1960 28
                $this->lexer->peek(); // "("
1961
1962
                switch (true) {
1963 28
                    case ($this->isMathOperator($this->peekBeyondClosingParenthesis())):
1964
                        // SUM(u.id) + COUNT(u.id)
1965 7
                        return $this->SimpleArithmeticExpression();
1966
1967
                    default:
1968
                        // IDENTITY(u)
1969 23
                        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 783
    public function SelectExpression()
2161
    {
2162 783
        $expression    = null;
2163 783
        $identVariable = null;
2164 783
        $peek          = $this->lexer->glimpse();
2165 783
        $lookaheadType = $this->lexer->lookahead['type'];
2166
2167 783
        switch (true) {
2168
            // ScalarExpression (u.name)
2169 707
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2170 106
                $expression = $this->ScalarExpression();
2171 106
                break;
2172
2173
            // IdentificationVariable (u)
2174 723
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2175 593
                $expression = $identVariable = $this->IdentificationVariable();
2176 593
                break;
2177
2178
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
2179 193
            case ($lookaheadType === Lexer::T_CASE):
2180 188
            case ($lookaheadType === Lexer::T_COALESCE):
2181 186
            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 184
            case ($this->isFunction()):
2187 107
                $this->lexer->peek(); // "("
2188
2189
                switch (true) {
2190 107
                    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 105
                        $expression = $this->FunctionDeclaration();
2198 105
                        break;
2199
                }
2200
2201 107
                break;
2202
2203
            // PartialObjectExpression (PARTIAL u.{id, name})
2204 78
            case ($lookaheadType === Lexer::T_PARTIAL):
2205 9
                $expression    = $this->PartialObjectExpression();
2206 9
                $identVariable = $expression->identificationVariable;
2207 9
                break;
2208
2209
            // Subselect
2210 69
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2211 24
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2212 24
                $expression = $this->Subselect();
2213 24
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2214 24
                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 780
        $mustHaveAliasResultVariable = false;
2241
2242 780
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2243 124
            $this->match(Lexer::T_AS);
2244
2245 124
            $mustHaveAliasResultVariable = true;
2246
        }
2247
2248 780
        $hiddenAliasResultVariable = false;
2249
2250 780
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2251 11
            $this->match(Lexer::T_HIDDEN);
2252
2253 11
            $hiddenAliasResultVariable = true;
2254
        }
2255
2256 780
        $aliasResultVariable = null;
2257
2258 780
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2259 136
            $token               = $this->lexer->lookahead;
2260 136
            $aliasResultVariable = $this->AliasResultVariable();
2261
2262
            // Include AliasResultVariable in query components.
2263 131
            $this->queryComponents[$aliasResultVariable] = [
2264 131
                'resultVariable' => $expression,
2265 131
                'nestingLevel'   => $this->nestingLevel,
2266 131
                'token'          => $token,
2267
            ];
2268
        }
2269
2270
        // AST
2271
2272 775
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2273
2274 775
        if ($identVariable) {
2275 600
            $this->identVariableExpressions[$identVariable] = $expr;
2276
        }
2277
2278 775
        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 50
    public function SimpleSelectExpression()
2290
    {
2291 50
        $peek = $this->lexer->glimpse();
2292
2293 50
        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 29
        $this->lexer->peek();
2343
2344 29
        $expression = $this->ScalarExpression();
2345 29
        $expr       = new AST\SimpleSelectExpression($expression);
2346
2347 29
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2348 1
            $this->match(Lexer::T_AS);
2349
        }
2350
2351 29
        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 29
        return $expr;
2365
    }
2366
2367
    /**
2368
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2369
     *
2370
     * @return AST\ConditionalExpression
2371
     */
2372 389
    public function ConditionalExpression()
2373
    {
2374 389
        $conditionalTerms   = [];
2375 389
        $conditionalTerms[] = $this->ConditionalTerm();
2376
2377 386
        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 386
        if (count($conditionalTerms) === 1) {
2386 378
            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 389
    public function ConditionalTerm()
2398
    {
2399 389
        $conditionalFactors   = [];
2400 389
        $conditionalFactors[] = $this->ConditionalFactor();
2401
2402 386
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2403 34
            $this->match(Lexer::T_AND);
2404
2405 34
            $conditionalFactors[] = $this->ConditionalFactor();
2406
        }
2407
2408
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2409
        // if only one AST\ConditionalFactor is defined
2410 386
        if (count($conditionalFactors) === 1) {
2411 366
            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 34
        return new AST\ConditionalTerm($conditionalFactors);
2415
    }
2416
2417
    /**
2418
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2419
     *
2420
     * @return AST\ConditionalFactor
2421
     */
2422 389
    public function ConditionalFactor()
2423
    {
2424 389
        $not = false;
2425
2426 389
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2427 6
            $this->match(Lexer::T_NOT);
2428
2429 6
            $not = true;
2430
        }
2431
2432 389
        $conditionalPrimary = $this->ConditionalPrimary();
2433
2434
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2435
        // if only one AST\ConditionalPrimary is defined
2436 386
        if (! $not) {
2437 384
            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 389
    public function ConditionalPrimary()
2452
    {
2453 389
        $condPrimary = new AST\ConditionalPrimary();
2454
2455 389
        if (! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2456 380
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2457
2458 377
            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 389
    public function SimpleConditionalExpression()
2487
    {
2488 389
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2489 7
            return $this->ExistsExpression();
2490
        }
2491
2492 389
        $token     = $this->lexer->lookahead;
2493 389
        $peek      = $this->lexer->glimpse();
2494 389
        $lookahead = $token;
2495
2496 389
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2497
            $token = $this->lexer->glimpse();
2498
        }
2499
2500 389
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2501
            // Peek beyond the matching closing parenthesis.
2502 365
            $beyond = $this->lexer->peek();
2503
2504 365
            switch ($peek['value']) {
2505 365
                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 337
                    $token = $beyond;
2521
2522 337
                    while ($token['value'] === '.') {
2523 292
                        $this->lexer->peek();
2524
2525 292
                        $token = $this->lexer->peek();
2526
                    }
2527
2528
                    // Also peek beyond a NOT if there is one.
2529 337
                    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 337
                    $lookahead = $this->lexer->peek();
2535
            }
2536
2537
            // Also peek beyond a NOT if there is one.
2538 365
            if ($lookahead['type'] === Lexer::T_NOT) {
2539 9
                $lookahead = $this->lexer->peek();
2540
            }
2541
2542 365
            $this->lexer->resetPeek();
2543
        }
2544
2545 389
        if ($token['type'] === Lexer::T_BETWEEN) {
2546 8
            return $this->BetweenExpression();
2547
        }
2548
2549 383
        if ($token['type'] === Lexer::T_LIKE) {
2550 14
            return $this->LikeExpression();
2551
        }
2552
2553 370
        if ($token['type'] === Lexer::T_IN) {
2554 35
            return $this->InExpression();
2555
        }
2556
2557 344
        if ($token['type'] === Lexer::T_INSTANCE) {
2558 17
            return $this->InstanceOfExpression();
2559
        }
2560
2561 327
        if ($token['type'] === Lexer::T_MEMBER) {
2562 8
            return $this->CollectionMemberExpression();
2563
        }
2564
2565 319
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2566 15
            return $this->NullComparisonExpression();
2567
        }
2568
2569 308
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_EMPTY) {
2570 4
            return $this->EmptyCollectionComparisonExpression();
2571
        }
2572
2573 304
        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 188
    public function Literal()
2638
    {
2639 188
        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 141
                $this->match(
2647 141
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2648
                );
2649
2650 141
                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 172
    public function InputParameter()
2683
    {
2684 172
        $this->match(Lexer::T_INPUT_PARAMETER);
2685
2686 172
        return new AST\InputParameter($this->lexer->token['value']);
2687
    }
2688
2689
    /**
2690
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2691
     *
2692
     * @return AST\ArithmeticExpression
2693
     */
2694 338
    public function ArithmeticExpression()
2695
    {
2696 338
        $expr = new AST\ArithmeticExpression();
2697
2698 338
        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 338
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2711
2712 338
        return $expr;
2713
    }
2714
2715
    /**
2716
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2717
     *
2718
     * @return AST\SimpleArithmeticExpression
2719
     */
2720 445
    public function SimpleArithmeticExpression()
2721
    {
2722 445
        $terms   = [];
2723 445
        $terms[] = $this->ArithmeticTerm();
2724
2725 445
        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 445
        if (count($terms) === 1) {
2735 440
            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 445
    public function ArithmeticTerm()
2747
    {
2748 445
        $factors   = [];
2749 445
        $factors[] = $this->ArithmeticFactor();
2750
2751 445
        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 445
        if (count($factors) === 1) {
2761 417
            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 445
    public function ArithmeticFactor()
2773
    {
2774 445
        $sign   = null;
2775 445
        $isPlus = $this->lexer->isNextToken(Lexer::T_PLUS);
2776
2777 445
        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 445
        $primary = $this->ArithmeticPrimary();
2783
2784
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2785
        // if only one AST\ArithmeticPrimary is defined
2786 445
        if ($sign === null) {
2787 444
            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 460
    public function ArithmeticPrimary()
2800
    {
2801 460
        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 460
        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 430
                $peek = $this->lexer->glimpse();
2819
2820 430
                if ($peek['value'] === '(') {
2821 43
                    return $this->FunctionDeclaration();
2822
                }
2823
2824 399
                if ($peek['value'] === '.') {
2825 388
                    return $this->SingleValuedPathExpression();
2826
                }
2827
2828 47
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2829 10
                    return $this->ResultVariable();
2830
                }
2831
2832 39
                return $this->StateFieldPathExpression();
2833
2834
            case Lexer::T_INPUT_PARAMETER:
2835 153
                return $this->InputParameter();
2836
2837
            default:
2838 182
                $peek = $this->lexer->glimpse();
2839
2840 182
                if ($peek['value'] === '(') {
2841 18
                    return $this->FunctionDeclaration();
2842
                }
2843
2844 178
                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 68
    public function StringPrimary()
2879
    {
2880 68
        $lookaheadType = $this->lexer->lookahead['type'];
2881
2882 68
        switch ($lookaheadType) {
2883
            case Lexer::T_IDENTIFIER:
2884 38
                $peek = $this->lexer->glimpse();
2885
2886 38
                if ($peek['value'] === '.') {
2887 38
                    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
            default:
2911 3
                if ($this->isAggregateFunction($lookaheadType)) {
2912 3
                    return $this->AggregateExpression();
2913
                }
2914
        }
2915
2916
        $this->syntaxError(
2917
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2918
        );
2919
    }
2920
2921
    /**
2922
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2923
     *
2924
     * @return AST\PathExpression
2925
     */
2926 8
    public function EntityExpression()
2927
    {
2928 8
        $glimpse = $this->lexer->glimpse();
2929
2930 8
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2931 1
            return $this->SingleValuedAssociationPathExpression();
2932
        }
2933
2934 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...
2935
    }
2936
2937
    /**
2938
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2939
     *
2940
     * @return string|AST\InputParameter
2941
     */
2942 7
    public function SimpleEntityExpression()
2943
    {
2944 7
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2945 5
            return $this->InputParameter();
2946
        }
2947
2948 2
        return $this->StateFieldPathExpression();
2949
    }
2950
2951
    /**
2952
     * AggregateExpression ::=
2953
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2954
     *
2955
     * @return AST\AggregateExpression
2956
     */
2957 92
    public function AggregateExpression()
2958
    {
2959 92
        $lookaheadType = $this->lexer->lookahead['type'];
2960 92
        $isDistinct    = false;
2961
2962 92
        if (! in_array($lookaheadType, [Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM], true)) {
2963
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2964
        }
2965
2966 92
        $this->match($lookaheadType);
2967 92
        $functionName = $this->lexer->token['value'];
2968 92
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2969
2970 92
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
2971 3
            $this->match(Lexer::T_DISTINCT);
2972 3
            $isDistinct = true;
2973
        }
2974
2975 92
        $pathExp = $this->SimpleArithmeticExpression();
2976
2977 92
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2978
2979 92
        return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
2980
    }
2981
2982
    /**
2983
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
2984
     *
2985
     * @return AST\QuantifiedExpression
2986
     */
2987 3
    public function QuantifiedExpression()
2988
    {
2989 3
        $lookaheadType = $this->lexer->lookahead['type'];
2990 3
        $value         = $this->lexer->lookahead['value'];
2991
2992 3
        if (! in_array($lookaheadType, [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME], true)) {
2993
            $this->syntaxError('ALL, ANY or SOME');
2994
        }
2995
2996 3
        $this->match($lookaheadType);
2997 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2998
2999 3
        $qExpr       = new AST\QuantifiedExpression($this->Subselect());
3000 3
        $qExpr->type = $value;
3001
3002 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3003
3004 3
        return $qExpr;
3005
    }
3006
3007
    /**
3008
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3009
     *
3010
     * @return AST\BetweenExpression
3011
     */
3012 8
    public function BetweenExpression()
3013
    {
3014 8
        $not        = false;
3015 8
        $arithExpr1 = $this->ArithmeticExpression();
3016
3017 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3018 3
            $this->match(Lexer::T_NOT);
3019 3
            $not = true;
3020
        }
3021
3022 8
        $this->match(Lexer::T_BETWEEN);
3023 8
        $arithExpr2 = $this->ArithmeticExpression();
3024 8
        $this->match(Lexer::T_AND);
3025 8
        $arithExpr3 = $this->ArithmeticExpression();
3026
3027 8
        $betweenExpr      = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3028 8
        $betweenExpr->not = $not;
3029
3030 8
        return $betweenExpr;
3031
    }
3032
3033
    /**
3034
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3035
     *
3036
     * @return AST\ComparisonExpression
3037
     */
3038 304
    public function ComparisonExpression()
3039
    {
3040 304
        $this->lexer->glimpse();
3041
3042 304
        $leftExpr  = $this->ArithmeticExpression();
3043 304
        $operator  = $this->ComparisonOperator();
3044 304
        $rightExpr = ($this->isNextAllAnySome())
3045 3
            ? $this->QuantifiedExpression()
3046 304
            : $this->ArithmeticExpression();
3047
3048 302
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3049
    }
3050
3051
    /**
3052
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3053
     *
3054
     * @return AST\InExpression
3055
     */
3056 35
    public function InExpression()
3057
    {
3058 35
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3059
3060 35
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3061 6
            $this->match(Lexer::T_NOT);
3062 6
            $inExpression->not = true;
3063
        }
3064
3065 35
        $this->match(Lexer::T_IN);
3066 35
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3067
3068 35
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3069 9
            $inExpression->subselect = $this->Subselect();
3070
        } else {
3071 26
            $literals   = [];
3072 26
            $literals[] = $this->InParameter();
3073
3074 26
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3075 16
                $this->match(Lexer::T_COMMA);
3076 16
                $literals[] = $this->InParameter();
3077
            }
3078
3079 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...
3080
        }
3081
3082 34
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3083
3084 34
        return $inExpression;
3085
    }
3086
3087
    /**
3088
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3089
     *
3090
     * @return AST\InstanceOfExpression
3091
     */
3092 17
    public function InstanceOfExpression()
3093
    {
3094 17
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3095
3096 17
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3097 1
            $this->match(Lexer::T_NOT);
3098 1
            $instanceOfExpression->not = true;
3099
        }
3100
3101 17
        $this->match(Lexer::T_INSTANCE);
3102 17
        $this->match(Lexer::T_OF);
3103
3104 17
        $exprValues = [];
3105
3106 17
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3107 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3108
3109 2
            $exprValues[] = $this->InstanceOfParameter();
3110
3111 2
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3112 2
                $this->match(Lexer::T_COMMA);
3113
3114 2
                $exprValues[] = $this->InstanceOfParameter();
3115
            }
3116
3117 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3118
3119 2
            $instanceOfExpression->value = $exprValues;
3120
3121 2
            return $instanceOfExpression;
3122
        }
3123
3124 15
        $exprValues[] = $this->InstanceOfParameter();
3125
3126 15
        $instanceOfExpression->value = $exprValues;
3127
3128 15
        return $instanceOfExpression;
3129
    }
3130
3131
    /**
3132
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3133
     *
3134
     * @return mixed
3135
     */
3136 17
    public function InstanceOfParameter()
3137
    {
3138 17
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3139 6
            $this->match(Lexer::T_INPUT_PARAMETER);
3140
3141 6
            return new AST\InputParameter($this->lexer->token['value']);
3142
        }
3143
3144 11
        $abstractSchemaName = $this->AbstractSchemaName();
3145
3146 11
        $this->validateAbstractSchemaName($abstractSchemaName);
3147
3148 11
        return $abstractSchemaName;
3149
    }
3150
3151
    /**
3152
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3153
     *
3154
     * @return AST\LikeExpression
3155
     */
3156 14
    public function LikeExpression()
3157
    {
3158 14
        $stringExpr = $this->StringExpression();
3159 14
        $not        = false;
3160
3161 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3162 3
            $this->match(Lexer::T_NOT);
3163 3
            $not = true;
3164
        }
3165
3166 14
        $this->match(Lexer::T_LIKE);
3167
3168 14
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3169 3
            $this->match(Lexer::T_INPUT_PARAMETER);
3170 3
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3171
        } else {
3172 12
            $stringPattern = $this->StringPrimary();
3173
        }
3174
3175 14
        $escapeChar = null;
3176
3177 14
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3178 2
            $this->match(Lexer::T_ESCAPE);
3179 2
            $this->match(Lexer::T_STRING);
3180
3181 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3182
        }
3183
3184 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

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

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

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