Parser::SimpleConditionalExpression()   F
last analyzed

Complexity

Conditions 21
Paths 209

Size

Total Lines 88
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 21.0055

Importance

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

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

Loading history...
192 121
    }
193
194
    /**
195
     * Adds a custom tree walker for modifying the AST.
196
     *
197
     * @param string $className
198
     */
199
    public function addCustomTreeWalker($className)
200
    {
201
        $this->customTreeWalkers[] = $className;
202
    }
203
204
    /**
205
     * Gets the lexer used by the parser.
206
     *
207
     * @return Lexer
208
     */
209 35
    public function getLexer()
210
    {
211 35
        return $this->lexer;
212
    }
213
214
    /**
215
     * Gets the ParserResult that is being filled with information during parsing.
216
     *
217
     * @return ParserResult
218
     */
219
    public function getParserResult()
220
    {
221
        return $this->parserResult;
222
    }
223
224
    /**
225
     * Gets the EntityManager used by the parser.
226
     *
227
     * @return EntityManagerInterface
228
     */
229 10
    public function getEntityManager()
230
    {
231 10
        return $this->em;
232
    }
233
234
    /**
235
     * Parses and builds AST for the given Query.
236
     *
237
     * @return AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement
238
     */
239 853
    public function getAST()
240
    {
241
        // Parse & build AST
242 853
        $AST = $this->QueryLanguage();
243
244
        // Process any deferred validations of some nodes in the AST.
245
        // This also allows post-processing of the AST for modification purposes.
246 818
        $this->processDeferredIdentificationVariables();
247
248 816
        if ($this->deferredPartialObjectExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredPartialObjectExpressions of type array<mixed,array<mixed,mixed>> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
249 9
            $this->processDeferredPartialObjectExpressions();
250
        }
251
252 815
        if ($this->deferredPathExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredPathExpressions of type array<mixed,array<mixed,mixed>> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
253 603
            $this->processDeferredPathExpressions();
254
        }
255
256 813
        if ($this->deferredResultVariables) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredResultVariables of type array<mixed,array<mixed,mixed>> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
257 32
            $this->processDeferredResultVariables();
258
        }
259
260 813
        if ($this->deferredNewObjectExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredNewObjectExpressions of type array<mixed,array<mixed,mixed>> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
261 26
            $this->processDeferredNewObjectExpressions($AST);
0 ignored issues
show
Bug introduced by
$AST of type Doctrine\ORM\Query\AST\D...ery\AST\UpdateStatement is incompatible with the type Doctrine\ORM\Query\AST\SelectClause expected by parameter $AST of Doctrine\ORM\Query\Parse...dNewObjectExpressions(). ( Ignorable by Annotation )

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

261
            $this->processDeferredNewObjectExpressions(/** @scrutinizer ignore-type */ $AST);
Loading history...
262
        }
263
264 809
        $this->processRootEntityAliasSelected();
265
266
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
267 808
        $this->fixIdentificationVariableOrder($AST);
268
269 808
        return $AST;
270
    }
271
272
    /**
273
     * Attempts to match the given token with the current lookahead token.
274
     *
275
     * If they match, updates the lookahead token; otherwise raises a syntax
276
     * error.
277
     *
278
     * @param int $token The token type.
279
     *
280
     * @throws QueryException If the tokens don't match.
281
     */
282 864
    public function match($token)
283
    {
284 864
        $lookaheadType = $this->lexer->lookahead['type'];
285
286
        // Short-circuit on first condition, usually types match
287 864
        if ($lookaheadType === $token) {
288 856
            $this->lexer->moveNext();
289
290 856
            return;
291
        }
292
293
        // If parameter is not identifier (1-99) must be exact match
294 21
        if ($token < Lexer::T_IDENTIFIER) {
295 3
            $this->syntaxError($this->lexer->getLiteral($token));
296
        }
297
298
        // If parameter is keyword (200+) must be exact match
299 18
        if ($token > Lexer::T_IDENTIFIER) {
300 7
            $this->syntaxError($this->lexer->getLiteral($token));
301
        }
302
303
        // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
304 11
        if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
305 8
            $this->syntaxError($this->lexer->getLiteral($token));
306
        }
307
308 3
        $this->lexer->moveNext();
309 3
    }
310
311
    /**
312
     * Frees this parser, enabling it to be reused.
313
     *
314
     * @param bool $deep     Whether to clean peek and reset errors.
315
     * @param int  $position Position to reset.
316
     */
317
    public function free($deep = false, $position = 0)
318
    {
319
        // WARNING! Use this method with care. It resets the scanner!
320
        $this->lexer->resetPosition($position);
321
322
        // Deep = true cleans peek and also any previously defined errors
323
        if ($deep) {
324
            $this->lexer->resetPeek();
325
        }
326
327
        $this->lexer->token     = null;
328
        $this->lexer->lookahead = null;
329
    }
330
331
    /**
332
     * Parses a query string.
333
     *
334
     * @return ParserResult
335
     */
336 853
    public function parse()
337
    {
338 853
        $AST = $this->getAST();
339
340 808
        $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);
341 808
        if ($customWalkers !== false) {
342 99
            $this->customTreeWalkers = $customWalkers;
343
        }
344
345 808
        $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER);
346
347 808
        if ($customOutputWalker !== false) {
348 78
            $this->customOutputWalker = $customOutputWalker;
349
        }
350
351
        // Run any custom tree walkers over the AST
352 808
        if ($this->customTreeWalkers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->customTreeWalkers of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
353 98
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
354
355 98
            foreach ($this->customTreeWalkers as $walker) {
356 98
                $treeWalkerChain->addTreeWalker($walker);
357
            }
358
359
            switch (true) {
360 98
                case $AST instanceof AST\UpdateStatement:
361
                    $treeWalkerChain->walkUpdateStatement($AST);
362
                    break;
363
364 98
                case $AST instanceof AST\DeleteStatement:
365
                    $treeWalkerChain->walkDeleteStatement($AST);
366
                    break;
367
368 98
                case $AST instanceof AST\SelectStatement:
369
                default:
370 98
                    $treeWalkerChain->walkSelectStatement($AST);
0 ignored issues
show
Bug introduced by
It seems like $AST can also be of type Doctrine\ORM\Query\AST\DeleteStatement and Doctrine\ORM\Query\AST\UpdateStatement; however, parameter $AST of Doctrine\ORM\Query\TreeW...::walkSelectStatement() does only seem to accept Doctrine\ORM\Query\AST\SelectStatement, maybe add an additional type check? ( Ignorable by Annotation )

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

370
                    $treeWalkerChain->walkSelectStatement(/** @scrutinizer ignore-type */ $AST);
Loading history...
371
            }
372
373 92
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
374
        }
375
376 802
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
377 802
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
378
379
        // Assign an SQL executor to the parser result
380 802
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
381
382 794
        return $this->parserResult;
383
    }
384
385
    /**
386
     * Fixes order of identification variables.
387
     *
388
     * They have to appear in the select clause in the same order as the
389
     * declarations (from ... x join ... y join ... z ...) appear in the query
390
     * as the hydration process relies on that order for proper operation.
391
     *
392
     * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
393
     */
394 808
    private function fixIdentificationVariableOrder($AST)
395
    {
396 808
        if (count($this->identVariableExpressions) <= 1) {
397 633
            return;
398
        }
399
400 180
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
401 180
            if (! isset($this->identVariableExpressions[$dqlAlias])) {
402 8
                continue;
403
            }
404
405 180
            $expr = $this->identVariableExpressions[$dqlAlias];
406 180
            $key  = array_search($expr, $AST->selectClause->selectExpressions, true);
0 ignored issues
show
Bug introduced by
The property selectClause does not seem to exist on Doctrine\ORM\Query\AST\DeleteStatement.
Loading history...
Bug introduced by
The property selectClause does not seem to exist on Doctrine\ORM\Query\AST\UpdateStatement.
Loading history...
407
408 180
            unset($AST->selectClause->selectExpressions[$key]);
409
410 180
            $AST->selectClause->selectExpressions[] = $expr;
411
        }
412 180
    }
413
414
    /**
415
     * Generates a new syntax error.
416
     *
417
     * @param string       $expected Expected string.
418
     * @param mixed[]|null $token    Got token.
419
     *
420
     * @throws QueryException
421
     */
422 18
    public function syntaxError($expected = '', $token = null)
423
    {
424 18
        if ($token === null) {
425 15
            $token = $this->lexer->lookahead;
426
        }
427
428 18
        $tokenPos = $token['position'] ?? '-1';
429
430 18
        $message  = sprintf('line 0, col %d: Error: ', $tokenPos);
431 18
        $message .= $expected !== '' ? sprintf('Expected %s, got ', $expected) : 'Unexpected ';
432 18
        $message .= $this->lexer->lookahead === null ? 'end of string.' : sprintf("'%s'", $token['value']);
433
434 18
        throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL()));
435
    }
436
437
    /**
438
     * Generates a new semantical error.
439
     *
440
     * @param string       $message Optional message.
441
     * @param mixed[]|null $token   Optional token.
442
     *
443
     * @throws QueryException
444
     */
445 26
    public function semanticalError($message = '', $token = null, ?Throwable $previousFailure = null)
446
    {
447 26
        if ($token === null) {
448 2
            $token = $this->lexer->lookahead;
449
        }
450
451
        // Minimum exposed chars ahead of token
452 26
        $distance = 12;
453
454
        // Find a position of a final word to display in error string
455 26
        $dql      = $this->query->getDQL();
456 26
        $length   = strlen($dql);
457 26
        $tokenPos = $token && isset($token['position']) && $token['position'] > 0 ? $token['position'] : 0;
458 26
        $pos      = $tokenPos + $distance;
459 26
        $pos      = strpos($dql, ' ', $length > $pos ? $pos : $length);
460 26
        $length   = $pos !== false ? $pos - $tokenPos : $distance;
461 26
        $tokenStr = substr($dql, $tokenPos, $length);
462
463
        // Building informative message
464 26
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
465
466 26
        throw QueryException::semanticalError(
467 26
            $message,
468 26
            QueryException::dqlError($this->query->getDQL(), $previousFailure)
469
        );
470
    }
471
472
    /**
473
     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
474
     *
475
     * @param bool $resetPeek Reset peek after finding the closing parenthesis.
476
     *
477
     * @return mixed[]
478
     */
479 179
    private function peekBeyondClosingParenthesis($resetPeek = true)
480
    {
481 179
        $token        = $this->lexer->peek();
482 179
        $numUnmatched = 1;
483
484 179
        while ($numUnmatched > 0 && $token !== null) {
485 178
            switch ($token['type']) {
486
                case Lexer::T_OPEN_PARENTHESIS:
487 47
                    ++$numUnmatched;
488 47
                    break;
489
490
                case Lexer::T_CLOSE_PARENTHESIS:
491 178
                    --$numUnmatched;
492 178
                    break;
493
494
                default:
495
                    // Do nothing
496
            }
497
498 178
            $token = $this->lexer->peek();
499
        }
500
501 179
        if ($resetPeek) {
502 158
            $this->lexer->resetPeek();
503
        }
504
505 179
        return $token;
506
    }
507
508
    /**
509
     * Checks if the given token indicates a mathematical operator.
510
     *
511
     * @param mixed[] $token
512
     *
513
     * @return bool TRUE if the token is a mathematical operator, FALSE otherwise.
514
     */
515 253
    private function isMathOperator($token)
516
    {
517 253
        return $token && in_array($token['type'], [Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY], true);
0 ignored issues
show
Bug Best Practice introduced by
The expression $token of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
518
    }
519
520
    /**
521
     * Checks if the next-next (after lookahead) token starts a function.
522
     *
523
     * @return bool TRUE if the next-next tokens start a function, FALSE otherwise.
524
     */
525 408
    private function isFunction()
526
    {
527 408
        $lookaheadType = $this->lexer->lookahead['type'];
528 408
        $peek          = $this->lexer->peek();
529
530 408
        $this->lexer->resetPeek();
531
532 408
        return $lookaheadType >= Lexer::T_IDENTIFIER && $peek && $peek['type'] === Lexer::T_OPEN_PARENTHESIS;
533
    }
534
535
    /**
536
     * Checks whether the given token type indicates an aggregate function.
537
     *
538
     * @param int $tokenType
539
     *
540
     * @return bool TRUE if the token type is an aggregate function, FALSE otherwise.
541
     */
542 4
    private function isAggregateFunction($tokenType)
543
    {
544 4
        return in_array($tokenType, [Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT], true);
545
    }
546
547
    /**
548
     * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
549
     *
550
     * @return bool
551
     */
552 309
    private function isNextAllAnySome()
553
    {
554 309
        return in_array($this->lexer->lookahead['type'], [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME], true);
555
    }
556
557
    /**
558
     * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
559
     * It must exist in query components list.
560
     */
561 818
    private function processDeferredIdentificationVariables()
562
    {
563 818
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
564 794
            $identVariable = $deferredItem['expression'];
565
566
            // Check if IdentificationVariable exists in queryComponents
567 794
            if (! isset($this->queryComponents[$identVariable])) {
568 1
                $this->semanticalError(
569 1
                    sprintf("'%s' is not defined.", $identVariable),
570 1
                    $deferredItem['token']
571
                );
572
            }
573
574 794
            $qComp = $this->queryComponents[$identVariable];
575
576
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
577 794
            if (! isset($qComp['metadata'])) {
578
                $this->semanticalError(
579
                    sprintf("'%s' does not point to a Class.", $identVariable),
580
                    $deferredItem['token']
581
                );
582
            }
583
584
            // Validate if identification variable nesting level is lower or equal than the current one
585 794
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
586 1
                $this->semanticalError(
587 1
                    sprintf("'%s' is used outside the scope of its declaration.", $identVariable),
588 1
                    $deferredItem['token']
589
                );
590
            }
591
        }
592 816
    }
593
594
    /**
595
     * Validates that the given <tt>NewObjectExpression</tt>.
596
     *
597
     * @param AST\SelectClause $AST
598
     */
599 26
    private function processDeferredNewObjectExpressions($AST)
600
    {
601 26
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
602 26
            $expression    = $deferredItem['expression'];
603 26
            $token         = $deferredItem['token'];
604 26
            $className     = $expression->className;
605 26
            $args          = $expression->args;
606 26
            $fromClassName = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null;
0 ignored issues
show
Bug introduced by
The property fromClause does not seem to exist on Doctrine\ORM\Query\AST\SelectClause.
Loading history...
607
608
            // If the namespace is not given then assumes the first FROM entity namespace
609 26
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
610 10
                $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
611 10
                $fqcn      = $namespace . '\\' . $className;
612
613 10
                if (class_exists($fqcn)) {
614 10
                    $expression->className = $fqcn;
615 10
                    $className             = $fqcn;
616
                }
617
            }
618
619 26
            if (! class_exists($className)) {
620 1
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
621
            }
622
623 25
            $class = new ReflectionClass($className);
624
625 25
            if (! $class->isInstantiable()) {
626 1
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
627
            }
628
629 24
            if ($class->getConstructor() === null) {
630 1
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
631
            }
632
633 23
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
634 1
                $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
635
            }
636
        }
637 22
    }
638
639
    /**
640
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
641
     * It must exist in query components list.
642
     */
643 9
    private function processDeferredPartialObjectExpressions()
644
    {
645 9
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
646 9
            $expr  = $deferredItem['expression'];
647 9
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
648
649 9
            foreach ($expr->partialFieldSet as $field) {
650 9
                $property = $class->getProperty($field);
651
652 9
                if ($property instanceof FieldMetadata ||
653 9
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
654 9
                    continue;
655
                }
656
657
                $this->semanticalError(
658
                    sprintf("There is no mapped field named '%s' on class %s.", $field, $class->getClassName()),
659
                    $deferredItem['token']
660
                );
661
            }
662
663 9
            if (array_intersect($class->identifier, $expr->partialFieldSet) !== $class->identifier) {
664 1
                $this->semanticalError(
665 1
                    sprintf('The partial field selection of class %s must contain the identifier.', $class->getClassName()),
666 1
                    $deferredItem['token']
667
                );
668
            }
669
        }
670 8
    }
671
672
    /**
673
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
674
     * It must exist in query components list.
675
     */
676 32
    private function processDeferredResultVariables()
677
    {
678 32
        foreach ($this->deferredResultVariables as $deferredItem) {
679 32
            $resultVariable = $deferredItem['expression'];
680
681
            // Check if ResultVariable exists in queryComponents
682 32
            if (! isset($this->queryComponents[$resultVariable])) {
683
                $this->semanticalError(
684
                    sprintf("'%s' is not defined.", $resultVariable),
685
                    $deferredItem['token']
686
                );
687
            }
688
689 32
            $qComp = $this->queryComponents[$resultVariable];
690
691
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
692 32
            if (! isset($qComp['resultVariable'])) {
693
                $this->semanticalError(
694
                    sprintf("'%s' does not point to a ResultVariable.", $resultVariable),
695
                    $deferredItem['token']
696
                );
697
            }
698
699
            // Validate if identification variable nesting level is lower or equal than the current one
700 32
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
701
                $this->semanticalError(
702
                    sprintf("'%s' is used outside the scope of its declaration.", $resultVariable),
703
                    $deferredItem['token']
704
                );
705
            }
706
        }
707 32
    }
708
709
    /**
710
     * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
711
     *
712
     * AssociationPathExpression             ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
713
     * SingleValuedPathExpression            ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
714
     * StateFieldPathExpression              ::= IdentificationVariable "." StateField
715
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
716
     * CollectionValuedPathExpression        ::= IdentificationVariable "." CollectionValuedAssociationField
717
     */
718 603
    private function processDeferredPathExpressions()
719
    {
720 603
        foreach ($this->deferredPathExpressions as $deferredItem) {
721 603
            $pathExpression = $deferredItem['expression'];
722
723 603
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
724 603
            $class = $qComp['metadata'];
725 603
            $field = $pathExpression->field;
726
727 603
            if ($field === null) {
728 41
                $field = $pathExpression->field = $class->identifier[0];
729
            }
730
731 603
            $property = $class->getProperty($field);
732
733
            // Check if field or association exists
734 603
            if (! $property) {
735
                $this->semanticalError(
736
                    'Class ' . $class->getClassName() . ' has no field or association named ' . $field,
737
                    $deferredItem['token']
738
                );
739
            }
740
741 603
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
742
743 603
            if ($property instanceof AssociationMetadata) {
744 90
                $fieldType = $property instanceof ToOneAssociationMetadata
745 67
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
746 90
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
747
            }
748
749
            // Validate if PathExpression is one of the expected types
750 603
            $expectedType = $pathExpression->expectedType;
751
752 603
            if (! ($expectedType & $fieldType)) {
753
                // We need to recognize which was expected type(s)
754 2
                $expectedStringTypes = [];
755
756
                // Validate state field type
757 2
                if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
758 1
                    $expectedStringTypes[] = 'StateFieldPathExpression';
759
                }
760
761
                // Validate single valued association (*-to-one)
762 2
                if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
763 2
                    $expectedStringTypes[] = 'SingleValuedAssociationField';
764
                }
765
766
                // Validate single valued association (*-to-many)
767 2
                if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
768
                    $expectedStringTypes[] = 'CollectionValuedAssociationField';
769
                }
770
771
                // Build the error message
772 2
                $semanticalError  = 'Invalid PathExpression. ';
773 2
                $semanticalError .= count($expectedStringTypes) === 1
774 1
                    ? 'Must be a ' . $expectedStringTypes[0] . '.'
775 2
                    : implode(' or ', $expectedStringTypes) . ' expected.';
776
777 2
                $this->semanticalError($semanticalError, $deferredItem['token']);
778
            }
779
780
            // We need to force the type in PathExpression
781 601
            $pathExpression->type = $fieldType;
782
        }
783 601
    }
784
785 809
    private function processRootEntityAliasSelected()
786
    {
787 809
        if (! $this->identVariableExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->identVariableExpressions of type Doctrine\ORM\Query\AST\Node[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

2281
                        return new AST\SimpleSelectExpression(/** @scrutinizer ignore-type */ $expression);
Loading history...
2282 1
                    case $this->isFunction():
2283
                        // SUM(u.id) + COUNT(u.id)
2284 1
                        if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) {
2285
                            return new AST\SimpleSelectExpression($this->ScalarExpression());
2286
                        }
2287
                        // COUNT(u.id)
2288 1
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2289
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2290
                        }
2291
2292
                        // IDENTITY(u)
2293 1
                        return new AST\SimpleSelectExpression($this->FunctionDeclaration());
2294
                    default:
2295
                        // Do nothing
2296
                }
2297
                break;
2298
2299
            case Lexer::T_OPEN_PARENTHESIS:
2300 3
                if ($peek['type'] !== Lexer::T_SELECT) {
2301
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2302 3
                    $expression = $this->SimpleArithmeticExpression();
2303
2304 3
                    return new AST\SimpleSelectExpression($expression);
2305
                }
2306
2307
                // Subselect
2308
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2309
                $expression = $this->Subselect();
2310
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2311
2312
                return new AST\SimpleSelectExpression($expression);
2313
            default:
2314
                // Do nothing
2315
        }
2316
2317 29
        $this->lexer->peek();
2318
2319 29
        $expression = $this->ScalarExpression();
2320 29
        $expr       = new AST\SimpleSelectExpression($expression);
2321
2322 29
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2323 1
            $this->match(Lexer::T_AS);
2324
        }
2325
2326 29
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2327 2
            $token                             = $this->lexer->lookahead;
2328 2
            $resultVariable                    = $this->AliasResultVariable();
2329 2
            $expr->fieldIdentificationVariable = $resultVariable;
2330
2331
            // Include AliasResultVariable in query components.
2332 2
            $this->queryComponents[$resultVariable] = [
2333 2
                'resultvariable' => $expr,
2334 2
                'nestingLevel'   => $this->nestingLevel,
2335 2
                'token'          => $token,
2336
            ];
2337
        }
2338
2339 29
        return $expr;
2340
    }
2341
2342
    /**
2343
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2344
     *
2345
     * @return AST\ConditionalExpression
2346
     */
2347 394
    public function ConditionalExpression()
2348
    {
2349 394
        $conditionalTerms   = [];
2350 394
        $conditionalTerms[] = $this->ConditionalTerm();
2351
2352 391
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2353 16
            $this->match(Lexer::T_OR);
2354
2355 16
            $conditionalTerms[] = $this->ConditionalTerm();
2356
        }
2357
2358
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2359
        // if only one AST\ConditionalTerm is defined
2360 391
        if (count($conditionalTerms) === 1) {
2361 383
            return $conditionalTerms[0];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $conditionalTerms[0] returns the type Doctrine\ORM\Query\AST\ConditionalTerm which is incompatible with the documented return type Doctrine\ORM\Query\AST\ConditionalExpression.
Loading history...
2362
        }
2363
2364 16
        return new AST\ConditionalExpression($conditionalTerms);
2365
    }
2366
2367
    /**
2368
     * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2369
     *
2370
     * @return AST\ConditionalTerm
2371
     */
2372 394
    public function ConditionalTerm()
2373
    {
2374 394
        $conditionalFactors   = [];
2375 394
        $conditionalFactors[] = $this->ConditionalFactor();
2376
2377 391
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2378 34
            $this->match(Lexer::T_AND);
2379
2380 34
            $conditionalFactors[] = $this->ConditionalFactor();
2381
        }
2382
2383
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2384
        // if only one AST\ConditionalFactor is defined
2385 391
        if (count($conditionalFactors) === 1) {
2386 371
            return $conditionalFactors[0];
0 ignored issues
show
Bug Best Practice introduced by
The expression return $conditionalFactors[0] returns the type Doctrine\ORM\Query\AST\ConditionalFactor which is incompatible with the documented return type Doctrine\ORM\Query\AST\ConditionalTerm.
Loading history...
2387
        }
2388
2389 34
        return new AST\ConditionalTerm($conditionalFactors);
2390
    }
2391
2392
    /**
2393
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2394
     *
2395
     * @return AST\ConditionalFactor
2396
     */
2397 394
    public function ConditionalFactor()
2398
    {
2399 394
        $not = false;
2400
2401 394
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2402 6
            $this->match(Lexer::T_NOT);
2403
2404 6
            $not = true;
2405
        }
2406
2407 394
        $conditionalPrimary = $this->ConditionalPrimary();
2408
2409
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2410
        // if only one AST\ConditionalPrimary is defined
2411 391
        if (! $not) {
2412 389
            return $conditionalPrimary;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $conditionalPrimary returns the type Doctrine\ORM\Query\AST\ConditionalPrimary which is incompatible with the documented return type Doctrine\ORM\Query\AST\ConditionalFactor.
Loading history...
2413
        }
2414
2415 6
        $conditionalFactor      = new AST\ConditionalFactor($conditionalPrimary);
2416 6
        $conditionalFactor->not = $not;
2417
2418 6
        return $conditionalFactor;
2419
    }
2420
2421
    /**
2422
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2423
     *
2424
     * @return AST\ConditionalPrimary
2425
     */
2426 394
    public function ConditionalPrimary()
2427
    {
2428 394
        $condPrimary = new AST\ConditionalPrimary();
2429
2430 394
        if (! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2431 385
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2432
2433 382
            return $condPrimary;
2434
        }
2435
2436
        // Peek beyond the matching closing parenthesis ')'
2437 25
        $peek = $this->peekBeyondClosingParenthesis();
2438
2439 25
        if ($peek && (
0 ignored issues
show
Bug Best Practice introduced by
The expression $peek of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2440 21
            in_array($peek['value'], ['=', '<', '<=', '<>', '>', '>=', '!='], true) ||
2441 17
            in_array($peek['type'], [Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS], true) ||
2442 25
            $this->isMathOperator($peek))) {
2443 15
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2444
2445 15
            return $condPrimary;
2446
        }
2447
2448 21
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2449 21
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2450 21
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2451
2452 21
        return $condPrimary;
2453
    }
2454
2455
    /**
2456
     * SimpleConditionalExpression ::=
2457
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2458
     *      InExpression | NullComparisonExpression | ExistsExpression |
2459
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2460
     *      InstanceOfExpression
2461
     */
2462 394
    public function SimpleConditionalExpression()
2463
    {
2464 394
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2465 7
            return $this->ExistsExpression();
2466
        }
2467
2468 394
        $token     = $this->lexer->lookahead;
2469 394
        $peek      = $this->lexer->glimpse();
2470 394
        $lookahead = $token;
2471
2472 394
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2473
            $token = $this->lexer->glimpse();
2474
        }
2475
2476 394
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2477
            // Peek beyond the matching closing parenthesis.
2478 370
            $beyond = $this->lexer->peek();
2479
2480 370
            switch ($peek['value']) {
2481 370
                case '(':
2482
                    // Peeks beyond the matched closing parenthesis.
2483 39
                    $token = $this->peekBeyondClosingParenthesis(false);
2484
2485 39
                    if ($token['type'] === Lexer::T_NOT) {
2486 3
                        $token = $this->lexer->peek();
2487
                    }
2488
2489 39
                    if ($token['type'] === Lexer::T_IS) {
2490 2
                        $lookahead = $this->lexer->peek();
2491
                    }
2492 39
                    break;
2493
2494
                default:
2495
                    // Peek beyond the PathExpression or InputParameter.
2496 342
                    $token = $beyond;
2497
2498 342
                    while ($token['value'] === '.') {
2499 297
                        $this->lexer->peek();
2500
2501 297
                        $token = $this->lexer->peek();
2502
                    }
2503
2504
                    // Also peek beyond a NOT if there is one.
2505 342
                    if ($token['type'] === Lexer::T_NOT) {
2506 11
                        $token = $this->lexer->peek();
2507
                    }
2508
2509
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2510 342
                    $lookahead = $this->lexer->peek();
2511
            }
2512
2513
            // Also peek beyond a NOT if there is one.
2514 370
            if ($lookahead['type'] === Lexer::T_NOT) {
2515 9
                $lookahead = $this->lexer->peek();
2516
            }
2517
2518 370
            $this->lexer->resetPeek();
2519
        }
2520
2521 394
        if ($token['type'] === Lexer::T_BETWEEN) {
2522 8
            return $this->BetweenExpression();
2523
        }
2524
2525 388
        if ($token['type'] === Lexer::T_LIKE) {
2526 14
            return $this->LikeExpression();
2527
        }
2528
2529 375
        if ($token['type'] === Lexer::T_IN) {
2530 35
            return $this->InExpression();
2531
        }
2532
2533 349
        if ($token['type'] === Lexer::T_INSTANCE) {
2534 17
            return $this->InstanceOfExpression();
2535
        }
2536
2537 332
        if ($token['type'] === Lexer::T_MEMBER) {
2538 8
            return $this->CollectionMemberExpression();
2539
        }
2540
2541 324
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2542 15
            return $this->NullComparisonExpression();
2543
        }
2544
2545 313
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_EMPTY) {
2546 4
            return $this->EmptyCollectionComparisonExpression();
2547
        }
2548
2549 309
        return $this->ComparisonExpression();
2550
    }
2551
2552
    /**
2553
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2554
     *
2555
     * @return AST\EmptyCollectionComparisonExpression
2556
     */
2557 4
    public function EmptyCollectionComparisonExpression()
2558
    {
2559 4
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2560 4
            $this->CollectionValuedPathExpression()
2561
        );
2562 4
        $this->match(Lexer::T_IS);
2563
2564 4
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2565 2
            $this->match(Lexer::T_NOT);
2566 2
            $emptyCollectionCompExpr->not = true;
2567
        }
2568
2569 4
        $this->match(Lexer::T_EMPTY);
2570
2571 4
        return $emptyCollectionCompExpr;
2572
    }
2573
2574
    /**
2575
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2576
     *
2577
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2578
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2579
     *
2580
     * @return AST\CollectionMemberExpression
2581
     */
2582 8
    public function CollectionMemberExpression()
2583
    {
2584 8
        $not        = false;
2585 8
        $entityExpr = $this->EntityExpression();
2586
2587 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2588
            $this->match(Lexer::T_NOT);
2589
2590
            $not = true;
2591
        }
2592
2593 8
        $this->match(Lexer::T_MEMBER);
2594
2595 8
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2596 8
            $this->match(Lexer::T_OF);
2597
        }
2598
2599 8
        $collMemberExpr      = new AST\CollectionMemberExpression(
2600 8
            $entityExpr,
2601 8
            $this->CollectionValuedPathExpression()
2602
        );
2603 8
        $collMemberExpr->not = $not;
2604
2605 8
        return $collMemberExpr;
2606
    }
2607
2608
    /**
2609
     * Literal ::= string | char | integer | float | boolean
2610
     *
2611
     * @return AST\Literal
2612
     */
2613 190
    public function Literal()
2614
    {
2615 190
        switch ($this->lexer->lookahead['type']) {
2616
            case Lexer::T_STRING:
2617 49
                $this->match(Lexer::T_STRING);
2618
2619 49
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2620
            case Lexer::T_INTEGER:
2621
            case Lexer::T_FLOAT:
2622 141
                $this->match(
2623 141
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2624
                );
2625
2626 141
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2627
            case Lexer::T_TRUE:
2628
            case Lexer::T_FALSE:
2629 9
                $this->match(
2630 9
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2631
                );
2632
2633 9
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2634
            default:
2635
                $this->syntaxError('Literal');
2636
        }
2637
    }
2638
2639
    /**
2640
     * InParameter ::= Literal | InputParameter
2641
     *
2642
     * @return string|AST\InputParameter
2643
     */
2644 26
    public function InParameter()
2645
    {
2646 26
        if ($this->lexer->lookahead['type'] === Lexer::T_INPUT_PARAMETER) {
2647 14
            return $this->InputParameter();
2648
        }
2649
2650 12
        return $this->Literal();
2651
    }
2652
2653
    /**
2654
     * InputParameter ::= PositionalParameter | NamedParameter
2655
     *
2656
     * @return AST\InputParameter
2657
     */
2658 175
    public function InputParameter()
2659
    {
2660 175
        $this->match(Lexer::T_INPUT_PARAMETER);
2661
2662 175
        return new AST\InputParameter($this->lexer->token['value']);
2663
    }
2664
2665
    /**
2666
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2667
     *
2668
     * @return AST\ArithmeticExpression
2669
     */
2670 343
    public function ArithmeticExpression()
2671
    {
2672 343
        $expr = new AST\ArithmeticExpression();
2673
2674 343
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2675 19
            $peek = $this->lexer->glimpse();
2676
2677 19
            if ($peek['type'] === Lexer::T_SELECT) {
2678 7
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2679 7
                $expr->subselect = $this->Subselect();
2680 7
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2681
2682 7
                return $expr;
2683
            }
2684
        }
2685
2686 343
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2687
2688 343
        return $expr;
2689
    }
2690
2691
    /**
2692
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2693
     *
2694
     * @return AST\SimpleArithmeticExpression
2695
     */
2696 558
    public function SimpleArithmeticExpression()
2697
    {
2698 558
        $terms   = [];
2699 558
        $terms[] = $this->ArithmeticTerm();
2700
2701 558
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2702 21
            $this->match($isPlus ? Lexer::T_PLUS : Lexer::T_MINUS);
2703
2704 21
            $terms[] = $this->lexer->token['value'];
2705 21
            $terms[] = $this->ArithmeticTerm();
2706
        }
2707
2708
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2709
        // if only one AST\ArithmeticTerm is defined
2710 558
        if (count($terms) === 1) {
2711 556
            return $terms[0];
2712
        }
2713
2714 21
        return new AST\SimpleArithmeticExpression($terms);
2715
    }
2716
2717
    /**
2718
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2719
     *
2720
     * @return AST\ArithmeticTerm
2721
     */
2722 558
    public function ArithmeticTerm()
2723
    {
2724 558
        $factors   = [];
2725 558
        $factors[] = $this->ArithmeticFactor();
2726
2727 558
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2728 53
            $this->match($isMult ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2729
2730 53
            $factors[] = $this->lexer->token['value'];
2731 53
            $factors[] = $this->ArithmeticFactor();
2732
        }
2733
2734
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2735
        // if only one AST\ArithmeticFactor is defined
2736 558
        if (count($factors) === 1) {
2737 534
            return $factors[0];
2738
        }
2739
2740 53
        return new AST\ArithmeticTerm($factors);
2741
    }
2742
2743
    /**
2744
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2745
     *
2746
     * @return AST\ArithmeticFactor
2747
     */
2748 558
    public function ArithmeticFactor()
2749
    {
2750 558
        $sign   = null;
2751 558
        $isPlus = $this->lexer->isNextToken(Lexer::T_PLUS);
2752
2753 558
        if ($isPlus || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2754 3
            $this->match($isPlus ? Lexer::T_PLUS : Lexer::T_MINUS);
2755 3
            $sign = $isPlus;
2756
        }
2757
2758 558
        $primary = $this->ArithmeticPrimary();
2759
2760
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2761
        // if only one AST\ArithmeticPrimary is defined
2762 558
        if ($sign === null) {
2763 557
            return $primary;
2764
        }
2765
2766 3
        return new AST\ArithmeticFactor($primary, $sign);
2767
    }
2768
2769
    /**
2770
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2771
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2772
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2773
     *          | InputParameter | CaseExpression
2774
     */
2775 573
    public function ArithmeticPrimary()
2776
    {
2777 573
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2778 25
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2779
2780 25
            $expr = $this->SimpleArithmeticExpression();
2781
2782 25
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2783
2784 25
            return new AST\ParenthesisExpression($expr);
2785
        }
2786
2787 573
        switch ($this->lexer->lookahead['type']) {
2788
            case Lexer::T_COALESCE:
2789
            case Lexer::T_NULLIF:
2790
            case Lexer::T_CASE:
2791 4
                return $this->CaseExpression();
2792
            case Lexer::T_IDENTIFIER:
2793 546
                $peek = $this->lexer->glimpse();
2794
2795 546
                if ($peek && $peek['value'] === '(') {
2796 46
                    return $this->FunctionDeclaration();
2797
                }
2798
2799 514
                if ($peek && $peek['value'] === '.') {
2800 505
                    return $this->SingleValuedPathExpression();
2801
                }
2802
2803 47
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2804 10
                    return $this->ResultVariable();
2805
                }
2806
2807 39
                return $this->StateFieldPathExpression();
2808
            case Lexer::T_INPUT_PARAMETER:
2809 156
                return $this->InputParameter();
2810
            default:
2811 185
                $peek = $this->lexer->glimpse();
2812
2813 185
                if ($peek && $peek['value'] === '(') {
2814 19
                    return $this->FunctionDeclaration();
2815
                }
2816
2817 180
                return $this->Literal();
2818
        }
2819
    }
2820
2821
    /**
2822
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2823
     *
2824
     * @return AST\Subselect|string
2825
     */
2826 14
    public function StringExpression()
2827
    {
2828 14
        $peek = $this->lexer->glimpse();
2829
2830
        // Subselect
2831 14
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
2832
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2833
            $expr = $this->Subselect();
2834
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2835
2836
            return $expr;
2837
        }
2838
2839
        // ResultVariable (string)
2840 14
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2841 14
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2842 2
            return $this->ResultVariable();
2843
        }
2844
2845 12
        return $this->StringPrimary();
2846
    }
2847
2848
    /**
2849
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2850
     */
2851 70
    public function StringPrimary()
2852
    {
2853 70
        $lookaheadType = $this->lexer->lookahead['type'];
2854
2855 70
        switch ($lookaheadType) {
2856
            case Lexer::T_IDENTIFIER:
2857 40
                $peek = $this->lexer->glimpse();
2858
2859 40
                if ($peek['value'] === '.') {
2860 40
                    return $this->StateFieldPathExpression();
2861
                }
2862
2863 8
                if ($peek['value'] === '(') {
2864
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2865 8
                    return $this->FunctionDeclaration();
2866
                }
2867
2868
                $this->syntaxError("'.' or '('");
2869
                break;
2870
2871
            case Lexer::T_STRING:
2872 49
                $this->match(Lexer::T_STRING);
2873
2874 49
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2875
            case Lexer::T_INPUT_PARAMETER:
2876 2
                return $this->InputParameter();
2877
            case Lexer::T_CASE:
2878
            case Lexer::T_COALESCE:
2879
            case Lexer::T_NULLIF:
2880
                return $this->CaseExpression();
2881
            default:
2882 3
                if ($this->isAggregateFunction($lookaheadType)) {
2883 3
                    return $this->AggregateExpression();
2884
                }
2885
        }
2886
2887
        $this->syntaxError(
2888
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2889
        );
2890
    }
2891
2892
    /**
2893
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2894
     *
2895
     * @return AST\PathExpression
2896
     */
2897 8
    public function EntityExpression()
2898
    {
2899 8
        $glimpse = $this->lexer->glimpse();
2900
2901 8
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2902 1
            return $this->SingleValuedAssociationPathExpression();
2903
        }
2904
2905 7
        return $this->SimpleEntityExpression();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->SimpleEntityExpression() returns the type Doctrine\ORM\Query\AST\InputParameter which is incompatible with the documented return type Doctrine\ORM\Query\AST\PathExpression.
Loading history...
2906
    }
2907
2908
    /**
2909
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2910
     *
2911
     * @return string|AST\InputParameter
2912
     */
2913 7
    public function SimpleEntityExpression()
2914
    {
2915 7
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2916 5
            return $this->InputParameter();
2917
        }
2918
2919 2
        return $this->StateFieldPathExpression();
2920
    }
2921
2922
    /**
2923
     * AggregateExpression ::=
2924
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2925
     *
2926
     * @return AST\AggregateExpression
2927
     */
2928 92
    public function AggregateExpression()
2929
    {
2930 92
        $lookaheadType = $this->lexer->lookahead['type'];
2931 92
        $isDistinct    = false;
2932
2933 92
        if (! in_array($lookaheadType, [Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM], true)) {
2934
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2935
        }
2936
2937 92
        $this->match($lookaheadType);
2938 92
        $functionName = $this->lexer->token['value'];
2939 92
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2940
2941 92
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
2942 3
            $this->match(Lexer::T_DISTINCT);
2943 3
            $isDistinct = true;
2944
        }
2945
2946 92
        $pathExp = $this->SimpleArithmeticExpression();
2947
2948 92
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2949
2950 92
        return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
2951
    }
2952
2953
    /**
2954
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
2955
     *
2956
     * @return AST\QuantifiedExpression
2957
     */
2958 3
    public function QuantifiedExpression()
2959
    {
2960 3
        $lookaheadType = $this->lexer->lookahead['type'];
2961 3
        $value         = $this->lexer->lookahead['value'];
2962
2963 3
        if (! in_array($lookaheadType, [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME], true)) {
2964
            $this->syntaxError('ALL, ANY or SOME');
2965
        }
2966
2967 3
        $this->match($lookaheadType);
2968 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2969
2970 3
        $qExpr       = new AST\QuantifiedExpression($this->Subselect());
2971 3
        $qExpr->type = $value;
2972
2973 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2974
2975 3
        return $qExpr;
2976
    }
2977
2978
    /**
2979
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
2980
     *
2981
     * @return AST\BetweenExpression
2982
     */
2983 8
    public function BetweenExpression()
2984
    {
2985 8
        $not        = false;
2986 8
        $arithExpr1 = $this->ArithmeticExpression();
2987
2988 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2989 3
            $this->match(Lexer::T_NOT);
2990 3
            $not = true;
2991
        }
2992
2993 8
        $this->match(Lexer::T_BETWEEN);
2994 8
        $arithExpr2 = $this->ArithmeticExpression();
2995 8
        $this->match(Lexer::T_AND);
2996 8
        $arithExpr3 = $this->ArithmeticExpression();
2997
2998 8
        $betweenExpr      = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
2999 8
        $betweenExpr->not = $not;
3000
3001 8
        return $betweenExpr;
3002
    }
3003
3004
    /**
3005
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3006
     *
3007
     * @return AST\ComparisonExpression
3008
     */
3009 309
    public function ComparisonExpression()
3010
    {
3011 309
        $this->lexer->glimpse();
3012
3013 309
        $leftExpr  = $this->ArithmeticExpression();
3014 309
        $operator  = $this->ComparisonOperator();
3015 309
        $rightExpr = $this->isNextAllAnySome()
3016 3
            ? $this->QuantifiedExpression()
3017 309
            : $this->ArithmeticExpression();
3018
3019 307
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3020
    }
3021
3022
    /**
3023
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3024
     *
3025
     * @return AST\InExpression
3026
     */
3027 35
    public function InExpression()
3028
    {
3029 35
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3030
3031 35
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3032 6
            $this->match(Lexer::T_NOT);
3033 6
            $inExpression->not = true;
3034
        }
3035
3036 35
        $this->match(Lexer::T_IN);
3037 35
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3038
3039 35
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3040 9
            $inExpression->subselect = $this->Subselect();
3041
        } else {
3042 26
            $literals   = [];
3043 26
            $literals[] = $this->InParameter();
3044
3045 26
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3046 16
                $this->match(Lexer::T_COMMA);
3047 16
                $literals[] = $this->InParameter();
3048
            }
3049
3050 26
            $inExpression->literals = $literals;
0 ignored issues
show
Documentation Bug introduced by
It seems like $literals of type Doctrine\ORM\Query\AST\InputParameter[] is incompatible with the declared type Doctrine\ORM\Query\AST\Literal[] of property $literals.

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

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

Loading history...
3051
        }
3052
3053 34
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3054
3055 34
        return $inExpression;
3056
    }
3057
3058
    /**
3059
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3060
     *
3061
     * @return AST\InstanceOfExpression
3062
     */
3063 17
    public function InstanceOfExpression()
3064
    {
3065 17
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3066
3067 17
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3068 1
            $this->match(Lexer::T_NOT);
3069 1
            $instanceOfExpression->not = true;
3070
        }
3071
3072 17
        $this->match(Lexer::T_INSTANCE);
3073 17
        $this->match(Lexer::T_OF);
3074
3075 17
        $exprValues = [];
3076
3077 17
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3078 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3079
3080 2
            $exprValues[] = $this->InstanceOfParameter();
3081
3082 2
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3083 2
                $this->match(Lexer::T_COMMA);
3084
3085 2
                $exprValues[] = $this->InstanceOfParameter();
3086
            }
3087
3088 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3089
3090 2
            $instanceOfExpression->value = $exprValues;
3091
3092 2
            return $instanceOfExpression;
3093
        }
3094
3095 15
        $exprValues[] = $this->InstanceOfParameter();
3096
3097 15
        $instanceOfExpression->value = $exprValues;
3098
3099 15
        return $instanceOfExpression;
3100
    }
3101
3102
    /**
3103
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3104
     *
3105
     * @return mixed
3106
     */
3107 17
    public function InstanceOfParameter()
3108
    {
3109 17
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3110 6
            $this->match(Lexer::T_INPUT_PARAMETER);
3111
3112 6
            return new AST\InputParameter($this->lexer->token['value']);
3113
        }
3114
3115 11
        $abstractSchemaName = $this->AbstractSchemaName();
3116
3117 11
        $this->validateAbstractSchemaName($abstractSchemaName);
3118
3119 11
        return $abstractSchemaName;
3120
    }
3121
3122
    /**
3123
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3124
     *
3125
     * @return AST\LikeExpression
3126
     */
3127 14
    public function LikeExpression()
3128
    {
3129 14
        $stringExpr = $this->StringExpression();
3130 14
        $not        = false;
3131
3132 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3133 3
            $this->match(Lexer::T_NOT);
3134 3
            $not = true;
3135
        }
3136
3137 14
        $this->match(Lexer::T_LIKE);
3138
3139 14
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3140 3
            $this->match(Lexer::T_INPUT_PARAMETER);
3141 3
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3142
        } else {
3143 12
            $stringPattern = $this->StringPrimary();
3144
        }
3145
3146 14
        $escapeChar = null;
3147
3148 14
        if ($this->lexer->isNextToken(Lexer::T_ESCAPE)) {
3149 2
            $this->match(Lexer::T_ESCAPE);
3150 2
            $this->match(Lexer::T_STRING);
3151
3152 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3153
        }
3154
3155 14
        $likeExpr      = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $stringPattern can also be of type Doctrine\ORM\Query\AST\AggregateExpression and Doctrine\ORM\Query\AST\CoalesceExpression and Doctrine\ORM\Query\AST\Functions\FunctionNode and Doctrine\ORM\Query\AST\Literal and Doctrine\ORM\Query\AST\NullIfExpression and Doctrine\ORM\Query\AST\PathExpression and Doctrine\ORM\Query\AST\SimpleCaseExpression; however, parameter $stringPattern of Doctrine\ORM\Query\AST\L...pression::__construct() does only seem to accept Doctrine\ORM\Query\AST\InputParameter, maybe add an additional type check? ( Ignorable by Annotation )

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

3155
        $likeExpr      = new AST\LikeExpression($stringExpr, /** @scrutinizer ignore-type */ $stringPattern, $escapeChar);
Loading history...
3156 14
        $likeExpr->not = $not;
3157
3158 14
        return $likeExpr;
3159
    }
3160
3161
    /**
3162
     * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
3163
     *
3164
     * @return AST\NullComparisonExpression
3165
     */
3166 15
    public function NullComparisonExpression()
3167
    {
3168
        switch (true) {
3169 15
            case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER):
3170
                $this->match(Lexer::T_INPUT_PARAMETER);
3171
3172
                $expr = new AST\InputParameter($this->lexer->token['value']);
3173
                break;
3174
3175 15
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3176 1
                $expr = $this->NullIfExpression();
3177 1
                break;
3178
3179 15
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3180 1
                $expr = $this->CoalesceExpression();
3181 1
                break;
3182
3183 15
            case $this->isFunction():
3184 2
                $expr = $this->FunctionDeclaration();
3185 2
                break;
3186
3187
            default:
3188
                // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
3189 14
                $glimpse = $this->lexer->glimpse();
3190
3191 14
                if ($glimpse['type'] === Lexer::T_DOT) {
3192 10
                    $expr = $this->SingleValuedPathExpression();
3193
3194
                    // Leave switch statement
3195 10
                    break;
3196
                }
3197
3198 4
                $lookaheadValue = $this->lexer->lookahead['value'];
3199
3200
                // Validate existing component
3201 4
                if (! isset($this->queryComponents[$lookaheadValue])) {
3202
                    $this->semanticalError('Cannot add having condition on undefined result variable.');
3203
                }
3204
3205
                // Validate SingleValuedPathExpression (ie.: "product")
3206 4
                if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {
3207 1
                    $expr = $this->SingleValuedPathExpression();
3208 1
                    break;
3209
                }
3210
3211
                // Validating ResultVariable
3212 3
                if (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3213
                    $this->semanticalError('Cannot add having condition on a non result variable.');
3214
                }
3215
3216 3
                $expr = $this->ResultVariable();
3217 3
                break;
3218
        }
3219
3220 15
        $nullCompExpr = new AST\NullComparisonExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr can also be of type string; however, parameter $expression of Doctrine\ORM\Query\AST\N...pression::__construct() does only seem to accept Doctrine\ORM\Query\AST\Node, maybe add an additional type check? ( Ignorable by Annotation )

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

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