Failed Conditions
Pull Request — master (#7242)
by Gabriel
08:46
created

Parser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 6
ccs 5
cts 5
cp 1
crap 1
rs 9.4285
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 501
    public function __construct(Query $query)
174
    {
175 501
        $this->query        = $query;
176 501
        $this->em           = $query->getEntityManager();
177 501
        $this->lexer        = new Lexer($query->getDQL());
178 501
        $this->parserResult = new ParserResult();
179 501
    }
180
181
    /**
182
     * Sets a custom tree walker that produces output.
183
     * This tree walker will be run last over the AST, after any other walkers.
184
     *
185
     * @param string $className
186
     */
187 120
    public function setCustomOutputTreeWalker($className)
188
    {
189 120
        $this->customOutputWalker = $className;
0 ignored issues
show
Documentation Bug introduced by
It seems like $className of type string is incompatible with the declared type Doctrine\ORM\Query\TreeWalker of property $customOutputWalker.

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

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

Loading history...
190 120
    }
191
192
    /**
193
     * Adds a custom tree walker for modifying the AST.
194
     *
195
     * @param string $className
196
     */
197
    public function addCustomTreeWalker($className)
198
    {
199
        $this->customTreeWalkers[] = $className;
200
    }
201
202
    /**
203
     * Gets the lexer used by the parser.
204
     *
205
     * @return Lexer
206
     */
207 24
    public function getLexer()
208
    {
209 24
        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 9
    public function getEntityManager()
228
    {
229 9
        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 501
    public function getAST()
238
    {
239
        // Parse & build AST
240 501
        $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 466
        $this->processDeferredIdentificationVariables();
245
246 464
        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 5
            $this->processDeferredPartialObjectExpressions();
248
        }
249
250 463
        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 353
            $this->processDeferredPathExpressions();
252
        }
253
254 461
        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 27
            $this->processDeferredResultVariables();
256
        }
257
258 461
        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 3
            $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 461
        $this->processRootEntityAliasSelected();
263
264
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
265 460
        $this->fixIdentificationVariableOrder($AST);
266
267 460
        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 512
    public function match($token)
281
    {
282 512
        $lookaheadType = $this->lexer->lookahead['type'];
283
284
        // Short-circuit on first condition, usually types match
285 512
        if ($lookaheadType === $token) {
286 504
            $this->lexer->moveNext();
287 504
            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 501
    public function parse()
334
    {
335 501
        $AST = $this->getAST();
336
337 460
        $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
338 460
        if ($customWalkers !== false) {
339 32
            $this->customTreeWalkers = $customWalkers;
340
        }
341
342 460
        $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER);
343
344 460
        if ($customOutputWalker !== false) {
345 30
            $this->customOutputWalker = $customOutputWalker;
346
        }
347
348
        // Run any custom tree walkers over the AST
349 460
        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 31
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
351
352 31
            foreach ($this->customTreeWalkers as $walker) {
353 31
                $treeWalkerChain->addTreeWalker($walker);
354
            }
355
356
            switch (true) {
357 31
                case ($AST instanceof AST\UpdateStatement):
358
                    $treeWalkerChain->walkUpdateStatement($AST);
359
                    break;
360
361 31
                case ($AST instanceof AST\DeleteStatement):
362
                    $treeWalkerChain->walkDeleteStatement($AST);
363
                    break;
364
365 31
                case ($AST instanceof AST\SelectStatement):
366
                default:
367 31
                    $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 30
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
371
        }
372
373 459
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
374 459
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
375
376
        // Assign an SQL executor to the parser result
377 459
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
378
379 452
        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 460
    private function fixIdentificationVariableOrder($AST)
392
    {
393 460
        if (count($this->identVariableExpressions) <= 1) {
394 394
            return;
395
        }
396
397 66
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
398 66
            if (! isset($this->identVariableExpressions[$dqlAlias])) {
399 7
                continue;
400
            }
401
402 66
            $expr = $this->identVariableExpressions[$dqlAlias];
403 66
            $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 66
            unset($AST->selectClause->selectExpressions[$key]);
406
407 66
            $AST->selectClause->selectExpressions[] = $expr;
408
        }
409 66
    }
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 22
    public function semanticalError($message = '', $token = null, ?\Throwable $previousFailure = null)
443
    {
444 22
        if ($token === null) {
445 2
            $token = $this->lexer->lookahead;
446
        }
447
448
        // Minimum exposed chars ahead of token
449 22
        $distance = 12;
450
451
        // Find a position of a final word to display in error string
452 22
        $dql    = $this->query->getDQL();
453 22
        $length = strlen($dql);
454 22
        $pos    = $token['position'] + $distance;
455 22
        $pos    = strpos($dql, ' ', ($length > $pos) ? $pos : $length);
456 22
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
457
458 22
        $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1';
459 22
        $tokenStr = substr($dql, (int) $token['position'], $length);
460
461
        // Building informative message
462 22
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
463
464 22
        throw QueryException::semanticalError(
465 22
            $message,
466 22
            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 114
    private function peekBeyondClosingParenthesis($resetPeek = true)
478
    {
479 114
        $token        = $this->lexer->peek();
480 114
        $numUnmatched = 1;
481
482 114
        while ($numUnmatched > 0 && $token !== null) {
483 113
            switch ($token['type']) {
484
                case Lexer::T_OPEN_PARENTHESIS:
485 22
                    ++$numUnmatched;
486 22
                    break;
487
488
                case Lexer::T_CLOSE_PARENTHESIS:
489 113
                    --$numUnmatched;
490 113
                    break;
491
492
                default:
493
                    // Do nothing
494
            }
495
496 113
            $token = $this->lexer->peek();
497
        }
498
499 114
        if ($resetPeek) {
500 94
            $this->lexer->resetPeek();
501
        }
502
503 114
        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 191
    private function isMathOperator($token)
514
    {
515 191
        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 226
    private function isFunction()
524
    {
525 226
        $lookaheadType = $this->lexer->lookahead['type'];
526 226
        $peek          = $this->lexer->peek();
527
528 226
        $this->lexer->resetPeek();
529
530 226
        return $lookaheadType >= Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_OPEN_PARENTHESIS;
531
    }
532
533
    /**
534
     * Checks whether the given token type indicates an aggregate function.
535
     *
536
     * @param int $tokenType
537
     *
538
     * @return bool TRUE if the token type is an aggregate function, FALSE otherwise.
539
     */
540 1
    private function isAggregateFunction($tokenType)
541
    {
542 1
        return in_array($tokenType, [Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT], true);
543
    }
544
545
    /**
546
     * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
547
     *
548
     * @return bool
549
     */
550 195
    private function isNextAllAnySome()
551
    {
552 195
        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 466
    private function processDeferredIdentificationVariables()
560
    {
561 466
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
562 459
            $identVariable = $deferredItem['expression'];
563
564
            // Check if IdentificationVariable exists in queryComponents
565 459
            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 459
            $qComp = $this->queryComponents[$identVariable];
573
574
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
575 459
            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 459
            if ($qComp['nestingLevel'] <= $deferredItem['nestingLevel']) {
584 459
                continue;
585
            }
586
587 1
            $this->semanticalError(
588 1
                sprintf("'%s' is used outside the scope of its declaration.", $identVariable),
589 1
                $deferredItem['token']
590
            );
591
        }
592 464
    }
593
594
    /**
595
     * Validates that the given <tt>NewObjectExpression</tt>.
596
     *
597
     * @param AST\SelectClause $AST
598
     */
599 3
    private function processDeferredNewObjectExpressions($AST)
600
    {
601 3
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
602 3
            $expression    = $deferredItem['expression'];
603 3
            $token         = $deferredItem['token'];
604 3
            $className     = $expression->className;
605 3
            $args          = $expression->args;
606 3
            $fromClassName = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null;
0 ignored issues
show
Bug introduced by
The property fromClause does not seem to exist on Doctrine\ORM\Query\AST\SelectClause.
Loading history...
607
608
            // If the namespace is not given then assumes the first FROM entity namespace
609 3
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
610
                $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
611
                $fqcn      = $namespace . '\\' . $className;
612
613
                if (class_exists($fqcn)) {
614
                    $expression->className = $fqcn;
615
                    $className             = $fqcn;
616
                }
617
            }
618
619 3
            if (! class_exists($className)) {
620
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
621
            }
622
623 3
            $class = new \ReflectionClass($className);
624
625 3
            if (! $class->isInstantiable()) {
626
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
627
            }
628
629 3
            if ($class->getConstructor() === null) {
630
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
631
            }
632
633 3
            if ($class->getConstructor()->getNumberOfRequiredParameters() <= count($args)) {
634 3
                continue;
635
            }
636
637
            $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
638
        }
639 3
    }
640
641
    /**
642
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
643
     * It must exist in query components list.
644
     */
645 5
    private function processDeferredPartialObjectExpressions()
646
    {
647 5
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
648 5
            $expr  = $deferredItem['expression'];
649 5
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
650
651 5
            foreach ($expr->partialFieldSet as $field) {
652 5
                $property = $class->getProperty($field);
653
654 5
                if ($property instanceof FieldMetadata ||
655 5
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
656 5
                    continue;
657
                }
658
659
                $this->semanticalError(
660
                    sprintf("There is no mapped field named '%s' on class %s.", $field, $class->getClassName()),
661
                    $deferredItem['token']
662
                );
663
            }
664
665 5
            if (array_intersect($class->identifier, $expr->partialFieldSet) === $class->identifier) {
666 4
                continue;
667
            }
668
669 1
            $this->semanticalError(
670 1
                sprintf('The partial field selection of class %s must contain the identifier.', $class->getClassName()),
671 1
                $deferredItem['token']
672
            );
673
        }
674 4
    }
675
676
    /**
677
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
678
     * It must exist in query components list.
679
     */
680 27
    private function processDeferredResultVariables()
681
    {
682 27
        foreach ($this->deferredResultVariables as $deferredItem) {
683 27
            $resultVariable = $deferredItem['expression'];
684
685
            // Check if ResultVariable exists in queryComponents
686 27
            if (! isset($this->queryComponents[$resultVariable])) {
687
                $this->semanticalError(
688
                    sprintf("'%s' is not defined.", $resultVariable),
689
                    $deferredItem['token']
690
                );
691
            }
692
693 27
            $qComp = $this->queryComponents[$resultVariable];
694
695
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
696 27
            if (! isset($qComp['resultVariable'])) {
697
                $this->semanticalError(
698
                    sprintf("'%s' does not point to a ResultVariable.", $resultVariable),
699
                    $deferredItem['token']
700
                );
701
            }
702
703
            // Validate if identification variable nesting level is lower or equal than the current one
704 27
            if ($qComp['nestingLevel'] <= $deferredItem['nestingLevel']) {
705 27
                continue;
706
            }
707
708
            $this->semanticalError(
709
                sprintf("'%s' is used outside the scope of its declaration.", $resultVariable),
710
                $deferredItem['token']
711
            );
712
        }
713 27
    }
714
715
    /**
716
     * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
717
     *
718
     * AssociationPathExpression             ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
719
     * SingleValuedPathExpression            ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
720
     * StateFieldPathExpression              ::= IdentificationVariable "." StateField
721
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
722
     * CollectionValuedPathExpression        ::= IdentificationVariable "." CollectionValuedAssociationField
723
     */
724 353
    private function processDeferredPathExpressions()
725
    {
726 353
        foreach ($this->deferredPathExpressions as $deferredItem) {
727 353
            $pathExpression = $deferredItem['expression'];
728
729 353
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
730 353
            $class = $qComp['metadata'];
731 353
            $field = $pathExpression->field;
732
733 353
            if ($field === null) {
734 24
                $field = $pathExpression->field = $class->identifier[0];
735
            }
736
737 353
            $property = $class->getProperty($field);
738
739
            // Check if field or association exists
740 353
            if (! $property) {
741
                $this->semanticalError(
742
                    'Class ' . $class->getClassName() . ' has no field or association named ' . $field,
743
                    $deferredItem['token']
744
                );
745
            }
746
747 353
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
748
749 353
            if ($property instanceof AssociationMetadata) {
750 66
                $fieldType = $property instanceof ToOneAssociationMetadata
751 45
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
752 66
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
753
                ;
754
            }
755
756
            // Validate if PathExpression is one of the expected types
757 353
            $expectedType = $pathExpression->expectedType;
758
759 353
            if (! ($expectedType & $fieldType)) {
760
                // We need to recognize which was expected type(s)
761 2
                $expectedStringTypes = [];
762
763
                // Validate state field type
764 2
                if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
765 1
                    $expectedStringTypes[] = 'StateFieldPathExpression';
766
                }
767
768
                // Validate single valued association (*-to-one)
769 2
                if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
770 2
                    $expectedStringTypes[] = 'SingleValuedAssociationField';
771
                }
772
773
                // Validate single valued association (*-to-many)
774 2
                if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
775
                    $expectedStringTypes[] = 'CollectionValuedAssociationField';
776
                }
777
778
                // Build the error message
779 2
                $semanticalError  = 'Invalid PathExpression. ';
780 2
                $semanticalError .= count($expectedStringTypes) === 1
781 1
                    ? 'Must be a ' . $expectedStringTypes[0] . '.'
782 2
                    : implode(' or ', $expectedStringTypes) . ' expected.';
783
784 2
                $this->semanticalError($semanticalError, $deferredItem['token']);
785
            }
786
787
            // We need to force the type in PathExpression
788 351
            $pathExpression->type = $fieldType;
789
        }
790 351
    }
791
792 461
    private function processRootEntityAliasSelected()
793
    {
794 461
        if (! $this->identVariableExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->identVariableExpressions of type Doctrine\ORM\Query\AST\Node[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
795 170
            return;
796
        }
797
798 297
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
799 297
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
800 297
                return;
801
            }
802
        }
803
804 1
        $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
805
    }
806
807
    /**
808
     * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
809
     *
810
     * @return AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement
811
     */
812 501
    public function QueryLanguage()
813
    {
814 501
        $statement = null;
815
816 501
        $this->lexer->moveNext();
817
818 501
        switch ($this->lexer->lookahead['type']) {
819
            case Lexer::T_SELECT:
820 445
                $statement = $this->SelectStatement();
821 414
                break;
822
823
            case Lexer::T_UPDATE:
824 25
                $statement = $this->UpdateStatement();
825 25
                break;
826
827
            case Lexer::T_DELETE:
828 34
                $statement = $this->DeleteStatement();
829 34
                break;
830
831
            default:
832 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
833
                break;
834
        }
835
836
        // Check for end of string
837 470
        if ($this->lexer->lookahead !== null) {
838 4
            $this->syntaxError('end of string');
839
        }
840
841 466
        return $statement;
842
    }
843
844
    /**
845
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
846
     *
847
     * @return AST\SelectStatement
848
     */
849 445
    public function SelectStatement()
850
    {
851 445
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
852
853 418
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
854 415
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
855 414
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
856 414
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
857
858 414
        return $selectStatement;
859
    }
860
861
    /**
862
     * UpdateStatement ::= UpdateClause [WhereClause]
863
     *
864
     * @return AST\UpdateStatement
865
     */
866 25
    public function UpdateStatement()
867
    {
868 25
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
869
870 25
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
871
872 25
        return $updateStatement;
873
    }
874
875
    /**
876
     * DeleteStatement ::= DeleteClause [WhereClause]
877
     *
878
     * @return AST\DeleteStatement
879
     */
880 34
    public function DeleteStatement()
881
    {
882 34
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
883
884 34
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
885
886 34
        return $deleteStatement;
887
    }
888
889
    /**
890
     * IdentificationVariable ::= identifier
891
     *
892
     * @return string
893
     */
894 483
    public function IdentificationVariable()
895
    {
896 483
        $this->match(Lexer::T_IDENTIFIER);
897
898 483
        $identVariable = $this->lexer->token['value'];
899
900 483
        $this->deferredIdentificationVariables[] = [
901 483
            'expression'   => $identVariable,
902 483
            'nestingLevel' => $this->nestingLevel,
903 483
            'token'        => $this->lexer->token,
904
        ];
905
906 483
        return $identVariable;
907
    }
908
909
    /**
910
     * AliasIdentificationVariable = identifier
911
     *
912
     * @return string
913
     */
914 476
    public function AliasIdentificationVariable()
915
    {
916 476
        $this->match(Lexer::T_IDENTIFIER);
917
918 476
        $aliasIdentVariable = $this->lexer->token['value'];
919 476
        $exists             = isset($this->queryComponents[$aliasIdentVariable]);
920
921 476
        if ($exists) {
922 2
            $this->semanticalError(sprintf("'%s' is already defined.", $aliasIdentVariable), $this->lexer->token);
923
        }
924
925 476
        return $aliasIdentVariable;
926
    }
927
928
    /**
929
     * AbstractSchemaName ::= fully_qualified_name | identifier
930
     *
931
     * @return string
932
     */
933 489
    public function AbstractSchemaName()
934
    {
935 489
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
936 482
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
937
938 482
            return $this->lexer->token['value'];
939
        }
940
941 8
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
942 7
            $this->match(Lexer::T_IDENTIFIER);
943
944 7
            return $this->lexer->token['value'];
945
        }
946
947 1
        $this->match(Lexer::T_ALIASED_NAME);
948
949
        [$namespaceAlias, $simpleClassName] = explode(':', $this->lexer->token['value']);
950
951
        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

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

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

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

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

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