Failed Conditions
Pull Request — master (#6143)
by Luís
10:34
created

Parser::SimpleWhenClause()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
ccs 5
cts 5
cp 1
rs 9.4285
cc 1
eloc 5
nc 1
nop 0
crap 1
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Query;
21
22
use Doctrine\ORM\Mapping\ClassMetadata;
23
use Doctrine\ORM\Query;
24
use Doctrine\ORM\Query\AST\Functions;
25
26
/**
27
 * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
28
 * Parses a DQL query, reports any errors in it, and generates an AST.
29
 *
30
 * @since   2.0
31
 * @author  Guilherme Blanco <[email protected]>
32
 * @author  Jonathan Wage <[email protected]>
33
 * @author  Roman Borschel <[email protected]>
34
 * @author  Janne Vanhala <[email protected]>
35
 * @author  Fabio B. Silva <[email protected]>
36
 */
37
class Parser
38
{
39
    /**
40
     * READ-ONLY: Maps BUILT-IN string function names to AST class names.
41
     *
42
     * @var array
43
     */
44
    private static $_STRING_FUNCTIONS = [
45
        'concat'    => Functions\ConcatFunction::class,
46
        'substring' => Functions\SubstringFunction::class,
47
        'trim'      => Functions\TrimFunction::class,
48
        'lower'     => Functions\LowerFunction::class,
49
        'upper'     => Functions\UpperFunction::class,
50
        'identity'  => Functions\IdentityFunction::class,
51
    ];
52
53
    /**
54
     * READ-ONLY: Maps BUILT-IN numeric function names to AST class names.
55
     *
56
     * @var array
57
     */
58
    private static $_NUMERIC_FUNCTIONS = [
59
        'length'    => Functions\LengthFunction::class,
60
        'locate'    => Functions\LocateFunction::class,
61
        'abs'       => Functions\AbsFunction::class,
62
        'sqrt'      => Functions\SqrtFunction::class,
63
        'mod'       => Functions\ModFunction::class,
64
        'size'      => Functions\SizeFunction::class,
65
        'date_diff' => Functions\DateDiffFunction::class,
66
        'bit_and'   => Functions\BitAndFunction::class,
67
        'bit_or'    => Functions\BitOrFunction::class,
68
69
        // Aggregate functions
70
        'min'       => Functions\MinFunction::class,
71
        'max'       => Functions\MaxFunction::class,
72
        'avg'       => Functions\AvgFunction::class,
73
        'sum'       => Functions\SumFunction::class,
74
        'count'     => Functions\CountFunction::class,
75
    ];
76
77
    /**
78
     * READ-ONLY: Maps BUILT-IN datetime function names to AST class names.
79
     *
80
     * @var array
81
     */
82
    private static $_DATETIME_FUNCTIONS = [
83
        'current_date'      => Functions\CurrentDateFunction::class,
84
        'current_time'      => Functions\CurrentTimeFunction::class,
85
        'current_timestamp' => Functions\CurrentTimestampFunction::class,
86
        'date_add'          => Functions\DateAddFunction::class,
87
        'date_sub'          => Functions\DateSubFunction::class,
88
    ];
89
90
    /*
91
     * Expressions that were encountered during parsing of identifiers and expressions
92
     * and still need to be validated.
93
     */
94
95
    /**
96
     * @var array
97
     */
98
    private $deferredIdentificationVariables = [];
99
100
    /**
101
     * @var array
102
     */
103
    private $deferredPartialObjectExpressions = [];
104
105
    /**
106
     * @var array
107
     */
108
    private $deferredPathExpressions = [];
109
110
    /**
111
     * @var array
112
     */
113
    private $deferredResultVariables = [];
114
115
    /**
116
     * @var array
117
     */
118
    private $deferredNewObjectExpressions = [];
119
120
    /**
121
     * The lexer.
122
     *
123
     * @var \Doctrine\ORM\Query\Lexer
124
     */
125
    private $lexer;
126
127
    /**
128
     * The parser result.
129
     *
130
     * @var \Doctrine\ORM\Query\ParserResult
131
     */
132
    private $parserResult;
133
134
    /**
135
     * The EntityManager.
136
     *
137
     * @var \Doctrine\ORM\EntityManager
138
     */
139
    private $em;
140
141
    /**
142
     * The Query to parse.
143
     *
144
     * @var Query
145
     */
146
    private $query;
147
148
    /**
149
     * Map of declared query components in the parsed query.
150
     *
151
     * @var array
152
     */
153
    private $queryComponents = [];
154
155
    /**
156
     * Keeps the nesting level of defined ResultVariables.
157
     *
158
     * @var integer
159
     */
160
    private $nestingLevel = 0;
161
162
    /**
163
     * Any additional custom tree walkers that modify the AST.
164
     *
165
     * @var array
166
     */
167
    private $customTreeWalkers = [];
168
169
    /**
170
     * The custom last tree walker, if any, that is responsible for producing the output.
171
     *
172
     * @var TreeWalker
173
     */
174
    private $customOutputWalker;
175
176
    /**
177
     * @var array
178
     */
179
    private $identVariableExpressions = [];
180
181
    /**
182
     * Creates a new query parser object.
183
     *
184
     * @param Query $query The Query to parse.
185
     */
186 830
    public function __construct(Query $query)
187
    {
188 830
        $this->query        = $query;
189 830
        $this->em           = $query->getEntityManager();
190 830
        $this->lexer        = new Lexer($query->getDQL());
191 830
        $this->parserResult = new ParserResult();
192 830
    }
193
194
    /**
195
     * Sets a custom tree walker that produces output.
196
     * This tree walker will be run last over the AST, after any other walkers.
197
     *
198
     * @param string $className
199
     *
200
     * @return void
201
     */
202 127
    public function setCustomOutputTreeWalker($className)
203
    {
204 127
        $this->customOutputWalker = $className;
0 ignored issues
show
Documentation Bug introduced by
It seems like $className of type string is incompatible with the declared type object<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...
205 127
    }
206
207
    /**
208
     * Adds a custom tree walker for modifying the AST.
209
     *
210
     * @param string $className
211
     *
212
     * @return void
213
     */
214
    public function addCustomTreeWalker($className)
215
    {
216
        $this->customTreeWalkers[] = $className;
217
    }
218
219
    /**
220
     * Gets the lexer used by the parser.
221
     *
222
     * @return \Doctrine\ORM\Query\Lexer
223
     */
224 28
    public function getLexer()
225
    {
226 28
        return $this->lexer;
227
    }
228
229
    /**
230
     * Gets the ParserResult that is being filled with information during parsing.
231
     *
232
     * @return \Doctrine\ORM\Query\ParserResult
233
     */
234
    public function getParserResult()
235
    {
236
        return $this->parserResult;
237
    }
238
239
    /**
240
     * Gets the EntityManager used by the parser.
241
     *
242
     * @return \Doctrine\ORM\EntityManager
243
     */
244
    public function getEntityManager()
245
    {
246
        return $this->em;
247
    }
248
249
    /**
250
     * Parses and builds AST for the given Query.
251
     *
252
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
253
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
254
     *         \Doctrine\ORM\Query\AST\DeleteStatement
255
     */
256 830
    public function getAST()
257
    {
258
        // Parse & build AST
259 830
        $AST = $this->QueryLanguage();
260
261
        // Process any deferred validations of some nodes in the AST.
262
        // This also allows post-processing of the AST for modification purposes.
263 788
        $this->processDeferredIdentificationVariables();
264
265 786
        if ($this->deferredPartialObjectExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredPartialObjectExpressions of type array 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...
266 11
            $this->processDeferredPartialObjectExpressions();
267
        }
268
269 784
        if ($this->deferredPathExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredPathExpressions of type array 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...
270 590
            $this->processDeferredPathExpressions();
271
        }
272
273 781
        if ($this->deferredResultVariables) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredResultVariables of type array 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...
274 30
            $this->processDeferredResultVariables();
275
        }
276
277 781
        if ($this->deferredNewObjectExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredNewObjectExpressions of type array 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...
278 28
            $this->processDeferredNewObjectExpressions($AST);
0 ignored issues
show
Documentation introduced by
$AST is of type object<Doctrine\ORM\Query\AST\SelectStatement>, but the function expects a object<Doctrine\ORM\Query\AST\SelectClause>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
279
        }
280
281 777
        $this->processRootEntityAliasSelected();
282
283
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
284 776
        $this->fixIdentificationVariableOrder($AST);
285
286 776
        return $AST;
287
    }
288
289
    /**
290
     * Attempts to match the given token with the current lookahead token.
291
     *
292
     * If they match, updates the lookahead token; otherwise raises a syntax
293
     * error.
294
     *
295
     * @param int $token The token type.
296
     *
297
     * @return void
298
     *
299
     * @throws QueryException If the tokens don't match.
300
     */
301 841
    public function match($token)
302
    {
303 841
        $lookaheadType = $this->lexer->lookahead['type'];
304
305
        // Short-circuit on first condition, usually types match
306 841
        if ($lookaheadType !== $token) {
307
            // If parameter is not identifier (1-99) must be exact match
308 21
            if ($token < Lexer::T_IDENTIFIER) {
309 3
                $this->syntaxError($this->lexer->getLiteral($token));
310
            }
311
312
            // If parameter is keyword (200+) must be exact match
313 18
            if ($token > Lexer::T_IDENTIFIER) {
314 7
                $this->syntaxError($this->lexer->getLiteral($token));
315
            }
316
317
            // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
318 11
            if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
319 8
                $this->syntaxError($this->lexer->getLiteral($token));
320
            }
321
        }
322
323 834
        $this->lexer->moveNext();
324 834
    }
325
326
    /**
327
     * Frees this parser, enabling it to be reused.
328
     *
329
     * @param boolean $deep     Whether to clean peek and reset errors.
330
     * @param integer $position Position to reset.
331
     *
332
     * @return void
333
     */
334
    public function free($deep = false, $position = 0)
335
    {
336
        // WARNING! Use this method with care. It resets the scanner!
337
        $this->lexer->resetPosition($position);
338
339
        // Deep = true cleans peek and also any previously defined errors
340
        if ($deep) {
341
            $this->lexer->resetPeek();
342
        }
343
344
        $this->lexer->token = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $token.

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...
345
        $this->lexer->lookahead = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $lookahead.

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...
346
    }
347
348
    /**
349
     * Parses a query string.
350
     *
351
     * @return ParserResult
352
     */
353 830
    public function parse()
354
    {
355 830
        $AST = $this->getAST();
356
357 776
        if (($customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) !== false) {
358 93
            $this->customTreeWalkers = $customWalkers;
0 ignored issues
show
Documentation Bug introduced by
It seems like $customWalkers of type * is incompatible with the declared type array of property $customTreeWalkers.

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...
359
        }
360
361 776
        if (($customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER)) !== false) {
362 79
            $this->customOutputWalker = $customOutputWalker;
363
        }
364
365
        // Run any custom tree walkers over the AST
366 776
        if ($this->customTreeWalkers) {
367 92
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
368
369 92
            foreach ($this->customTreeWalkers as $walker) {
370 92
                $treeWalkerChain->addTreeWalker($walker);
371
            }
372
373
            switch (true) {
374 92
                case ($AST instanceof AST\UpdateStatement):
375
                    $treeWalkerChain->walkUpdateStatement($AST);
376
                    break;
377
378 92
                case ($AST instanceof AST\DeleteStatement):
379
                    $treeWalkerChain->walkDeleteStatement($AST);
380
                    break;
381
382 92
                case ($AST instanceof AST\SelectStatement):
383
                default:
384 92
                    $treeWalkerChain->walkSelectStatement($AST);
385
            }
386
387 86
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
388
        }
389
390 770
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
391 770
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
392
393
        // Assign an SQL executor to the parser result
394 770
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
395
396 762
        return $this->parserResult;
397
    }
398
399
    /**
400
     * Fixes order of identification variables.
401
     *
402
     * They have to appear in the select clause in the same order as the
403
     * declarations (from ... x join ... y join ... z ...) appear in the query
404
     * as the hydration process relies on that order for proper operation.
405
     *
406
     * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
407
     *
408
     * @return void
409
     */
410 776
    private function fixIdentificationVariableOrder($AST)
411
    {
412 776
        if (count($this->identVariableExpressions) <= 1) {
413 601
            return;
414
        }
415
416 180
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
417 180
            if ( ! isset($this->identVariableExpressions[$dqlAlias])) {
418 8
                continue;
419
            }
420
421 180
            $expr = $this->identVariableExpressions[$dqlAlias];
422 180
            $key  = array_search($expr, $AST->selectClause->selectExpressions);
423
424 180
            unset($AST->selectClause->selectExpressions[$key]);
425
426 180
            $AST->selectClause->selectExpressions[] = $expr;
427
        }
428 180
    }
429
430
    /**
431
     * Generates a new syntax error.
432
     *
433
     * @param string     $expected Expected string.
434
     * @param array|null $token    Got token.
435
     *
436
     * @return void
437
     *
438
     * @throws \Doctrine\ORM\Query\QueryException
439
     */
440 18
    public function syntaxError($expected = '', $token = null)
441
    {
442 18
        if ($token === null) {
443 15
            $token = $this->lexer->lookahead;
444
        }
445
446 18
        $tokenPos = (isset($token['position'])) ? $token['position'] : '-1';
447
448 18
        $message  = "line 0, col {$tokenPos}: Error: ";
449 18
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
450 18
        $message .= ($this->lexer->lookahead === null) ? 'end of string.' : "'{$token['value']}'";
451
452 18
        throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL()));
453
    }
454
455
    /**
456
     * Generates a new semantical error.
457
     *
458
     * @param string     $message Optional message.
459
     * @param array|null $token   Optional token.
460
     *
461
     * @return void
462
     *
463
     * @throws \Doctrine\ORM\Query\QueryException
464
     */
465 35
    public function semanticalError($message = '', $token = null)
466
    {
467 35
        if ($token === null) {
468 2
            $token = $this->lexer->lookahead;
469
        }
470
471
        // Minimum exposed chars ahead of token
472 35
        $distance = 12;
473
474
        // Find a position of a final word to display in error string
475 35
        $dql    = $this->query->getDQL();
476 35
        $length = strlen($dql);
477 35
        $pos    = $token['position'] + $distance;
478 35
        $pos    = strpos($dql, ' ', ($length > $pos) ? $pos : $length);
479 35
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
480
481 35
        $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1';
482 35
        $tokenStr = substr($dql, $token['position'], $length);
483
484
        // Building informative message
485 35
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
486
487 35
        throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL()));
488
    }
489
490
    /**
491
     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
492
     *
493
     * @param boolean $resetPeek Reset peek after finding the closing parenthesis.
494
     *
495
     * @return array
496
     */
497 157
    private function peekBeyondClosingParenthesis($resetPeek = true)
498
    {
499 157
        $token = $this->lexer->peek();
500 157
        $numUnmatched = 1;
501
502 157
        while ($numUnmatched > 0 && $token !== null) {
503 156
            switch ($token['type']) {
504 156
                case Lexer::T_OPEN_PARENTHESIS:
505 33
                    ++$numUnmatched;
506 33
                    break;
507
508 156
                case Lexer::T_CLOSE_PARENTHESIS:
509 156
                    --$numUnmatched;
510 156
                    break;
511
512
                default:
513
                    // Do nothing
514
            }
515
516 156
            $token = $this->lexer->peek();
517
        }
518
519 157
        if ($resetPeek) {
520 136
            $this->lexer->resetPeek();
521
        }
522
523 157
        return $token;
524
    }
525
526
    /**
527
     * Checks if the given token indicates a mathematical operator.
528
     *
529
     * @param array $token
530
     *
531
     * @return boolean TRUE if the token is a mathematical operator, FALSE otherwise.
532
     */
533 345
    private function isMathOperator($token)
534
    {
535 345
        return in_array($token['type'], [Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY]);
536
    }
537
538
    /**
539
     * Checks if the next-next (after lookahead) token starts a function.
540
     *
541
     * @return boolean TRUE if the next-next tokens start a function, FALSE otherwise.
542
     */
543 390
    private function isFunction()
544
    {
545 390
        $lookaheadType = $this->lexer->lookahead['type'];
546 390
        $peek          = $this->lexer->peek();
547
548 390
        $this->lexer->resetPeek();
549
550 390
        return ($lookaheadType >= Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_OPEN_PARENTHESIS);
551
    }
552
553
    /**
554
     * Checks whether the given token type indicates an aggregate function.
555
     *
556
     * @param int $tokenType
557
     *
558
     * @return boolean TRUE if the token type is an aggregate function, FALSE otherwise.
559
     */
560 1
    private function isAggregateFunction($tokenType)
561
    {
562 1
        return in_array($tokenType, [Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT]);
563
    }
564
565
    /**
566
     * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
567
     *
568
     * @return boolean
569
     */
570 297
    private function isNextAllAnySome()
571
    {
572 297
        return in_array($this->lexer->lookahead['type'], [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME]);
573
    }
574
575
    /**
576
     * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
577
     * It must exist in query components list.
578
     *
579
     * @return void
580
     */
581 788 View Code Duplication
    private function processDeferredIdentificationVariables()
582
    {
583 788
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
584 776
            $identVariable = $deferredItem['expression'];
585
586
            // Check if IdentificationVariable exists in queryComponents
587 776
            if ( ! isset($this->queryComponents[$identVariable])) {
588 1
                $this->semanticalError(
589 1
                    "'$identVariable' is not defined.", $deferredItem['token']
590
                );
591
            }
592
593 776
            $qComp = $this->queryComponents[$identVariable];
594
595
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
596 776
            if ( ! isset($qComp['metadata'])) {
597
                $this->semanticalError(
598
                    "'$identVariable' does not point to a Class.", $deferredItem['token']
599
                );
600
            }
601
602
            // Validate if identification variable nesting level is lower or equal than the current one
603 776
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
604 1
                $this->semanticalError(
605 1
                    "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token']
606
                );
607
            }
608
        }
609 786
    }
610
611
    /**
612
     * Validates that the given <tt>NewObjectExpression</tt>.
613
     *
614
     * @param \Doctrine\ORM\Query\AST\SelectClause $AST
615
     *
616
     * @return void
617
     */
618 28
    private function processDeferredNewObjectExpressions($AST)
619
    {
620 28
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
621 28
            $expression     = $deferredItem['expression'];
622 28
            $token          = $deferredItem['token'];
623 28
            $className      = $expression->className;
624 28
            $args           = $expression->args;
625 28
            $fromClassName  = isset($AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName)
0 ignored issues
show
Bug introduced by
The property fromClause does not seem to exist in Doctrine\ORM\Query\AST\SelectClause.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
626 28
                ? $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName
627 28
                : null;
628
629
            // If the namespace is not given then assumes the first FROM entity namespace
630 28
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
631 11
                $namespace  = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
632 11
                $fqcn       = $namespace . '\\' . $className;
633
634 11
                if (class_exists($fqcn)) {
635 11
                    $expression->className  = $fqcn;
636 11
                    $className              = $fqcn;
637
                }
638
            }
639
640 28
            if ( ! class_exists($className)) {
641 1
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
642
            }
643
644 27
            $class = new \ReflectionClass($className);
645
646 27
            if ( ! $class->isInstantiable()) {
647 1
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
648
            }
649
650 26
            if ($class->getConstructor() === null) {
651 1
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
652
            }
653
654 25
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
655 1
                $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
656
            }
657
        }
658 24
    }
659
660
    /**
661
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
662
     * It must exist in query components list.
663
     *
664
     * @return void
665
     */
666 11
    private function processDeferredPartialObjectExpressions()
667
    {
668 11
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
669 11
            $expr = $deferredItem['expression'];
670 11
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
671
672 11
            foreach ($expr->partialFieldSet as $field) {
673 11
                if (isset($class->fieldMappings[$field])) {
674 10
                    continue;
675
                }
676
677 3
                if (isset($class->associationMappings[$field]) &&
678 2
                    $class->associationMappings[$field]['isOwningSide'] &&
679 2
                    $class->associationMappings[$field]['type'] & ClassMetadata::TO_ONE) {
680 2
                    continue;
681
                }
682
683 1
                $this->semanticalError(
684 1
                    "There is no mapped field named '$field' on class " . $class->name . ".", $deferredItem['token']
685
                );
686
            }
687
688 10
            if (array_intersect($class->identifier, $expr->partialFieldSet) != $class->identifier) {
689 1
                $this->semanticalError(
690 1
                    "The partial field selection of class " . $class->name . " must contain the identifier.",
691 1
                    $deferredItem['token']
692
                );
693
            }
694
        }
695 9
    }
696
697
    /**
698
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
699
     * It must exist in query components list.
700
     *
701
     * @return void
702
     */
703 30 View Code Duplication
    private function processDeferredResultVariables()
704
    {
705 30
        foreach ($this->deferredResultVariables as $deferredItem) {
706 30
            $resultVariable = $deferredItem['expression'];
707
708
            // Check if ResultVariable exists in queryComponents
709 30
            if ( ! isset($this->queryComponents[$resultVariable])) {
710
                $this->semanticalError(
711
                    "'$resultVariable' is not defined.", $deferredItem['token']
712
                );
713
            }
714
715 30
            $qComp = $this->queryComponents[$resultVariable];
716
717
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
718 30
            if ( ! isset($qComp['resultVariable'])) {
719
                $this->semanticalError(
720
                    "'$resultVariable' does not point to a ResultVariable.", $deferredItem['token']
721
                );
722
            }
723
724
            // Validate if identification variable nesting level is lower or equal than the current one
725 30
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
726
                $this->semanticalError(
727
                    "'$resultVariable' is used outside the scope of its declaration.", $deferredItem['token']
728
                );
729
            }
730
        }
731 30
    }
732
733
    /**
734
     * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
735
     *
736
     * AssociationPathExpression             ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
737
     * SingleValuedPathExpression            ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
738
     * StateFieldPathExpression              ::= IdentificationVariable "." StateField
739
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
740
     * CollectionValuedPathExpression        ::= IdentificationVariable "." CollectionValuedAssociationField
741
     *
742
     * @return void
743
     */
744 590
    private function processDeferredPathExpressions()
745
    {
746 590
        foreach ($this->deferredPathExpressions as $deferredItem) {
747 590
            $pathExpression = $deferredItem['expression'];
748
749 590
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
750 590
            $class = $qComp['metadata'];
751
752 590
            if (($field = $pathExpression->field) === null) {
753 39
                $field = $pathExpression->field = $class->identifier[0];
754
            }
755
756
            // Check if field or association exists
757 590
            if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) {
758 1
                $this->semanticalError(
759 1
                    'Class ' . $class->name . ' has no field or association named ' . $field,
760 1
                    $deferredItem['token']
761
                );
762
            }
763
764 589
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
765
766 589
            if (isset($class->associationMappings[$field])) {
767 87
                $assoc = $class->associationMappings[$field];
768
769 87
                $fieldType = ($assoc['type'] & ClassMetadata::TO_ONE)
770 66
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
771 87
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
772
            }
773
774
            // Validate if PathExpression is one of the expected types
775 589
            $expectedType = $pathExpression->expectedType;
776
777 589
            if ( ! ($expectedType & $fieldType)) {
778
                // We need to recognize which was expected type(s)
779 2
                $expectedStringTypes = [];
780
781
                // Validate state field type
782 2
                if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
783 1
                    $expectedStringTypes[] = 'StateFieldPathExpression';
784
                }
785
786
                // Validate single valued association (*-to-one)
787 2
                if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
788 2
                    $expectedStringTypes[] = 'SingleValuedAssociationField';
789
                }
790
791
                // Validate single valued association (*-to-many)
792 2
                if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
793
                    $expectedStringTypes[] = 'CollectionValuedAssociationField';
794
                }
795
796
                // Build the error message
797 2
                $semanticalError  = 'Invalid PathExpression. ';
798 2
                $semanticalError .= (count($expectedStringTypes) == 1)
799 1
                    ? 'Must be a ' . $expectedStringTypes[0] . '.'
800 2
                    : implode(' or ', $expectedStringTypes) . ' expected.';
801
802 2
                $this->semanticalError($semanticalError, $deferredItem['token']);
803
            }
804
805
            // We need to force the type in PathExpression
806 587
            $pathExpression->type = $fieldType;
807
        }
808 587
    }
809
810
    /**
811
     * @return void
812
     */
813 777
    private function processRootEntityAliasSelected()
814
    {
815 777
        if ( ! count($this->identVariableExpressions)) {
816 225
            return;
817
        }
818
819 563
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
820 563
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
821 562
                return;
822
            }
823
        }
824
825 1
        $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
826
    }
827
828
    /**
829
     * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
830
     *
831
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
832
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
833
     *         \Doctrine\ORM\Query\AST\DeleteStatement
834
     */
835 830
    public function QueryLanguage()
836
    {
837 830
        $this->lexer->moveNext();
838
839 830
        switch ($this->lexer->lookahead['type']) {
840 830
            case Lexer::T_SELECT:
841 765
                $statement = $this->SelectStatement();
842 727
                break;
843
844 72
            case Lexer::T_UPDATE:
845 32
                $statement = $this->UpdateStatement();
846 32
                break;
847
848 42
            case Lexer::T_DELETE:
849 41
                $statement = $this->DeleteStatement();
850 40
                break;
851
852
            default:
853 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
854
                break;
855
        }
856
857
        // Check for end of string
858 791
        if ($this->lexer->lookahead !== null) {
859 3
            $this->syntaxError('end of string');
860
        }
861
862 788
        return $statement;
0 ignored issues
show
Bug introduced by
The variable $statement does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug Best Practice introduced by
The return type of return $statement; (Doctrine\ORM\Query\AST\S...ery\AST\DeleteStatement) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::QueryLanguage of type Doctrine\ORM\Query\AST\SelectStatement.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
863
    }
864
865
    /**
866
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
867
     *
868
     * @return \Doctrine\ORM\Query\AST\SelectStatement
869
     */
870 765
    public function SelectStatement()
871
    {
872 765
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
873
874 731
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
875 728
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
876 727
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
877 727
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
878
879 727
        return $selectStatement;
880
    }
881
882
    /**
883
     * UpdateStatement ::= UpdateClause [WhereClause]
884
     *
885
     * @return \Doctrine\ORM\Query\AST\UpdateStatement
886
     */
887 32 View Code Duplication
    public function UpdateStatement()
888
    {
889 32
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
890
891 32
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
892
893 32
        return $updateStatement;
894
    }
895
896
    /**
897
     * DeleteStatement ::= DeleteClause [WhereClause]
898
     *
899
     * @return \Doctrine\ORM\Query\AST\DeleteStatement
900
     */
901 41 View Code Duplication
    public function DeleteStatement()
902
    {
903 41
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
904
905 40
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
906
907 40
        return $deleteStatement;
908
    }
909
910
    /**
911
     * IdentificationVariable ::= identifier
912
     *
913
     * @return string
914
     */
915 807 View Code Duplication
    public function IdentificationVariable()
916
    {
917 807
        $this->match(Lexer::T_IDENTIFIER);
918
919 807
        $identVariable = $this->lexer->token['value'];
920
921 807
        $this->deferredIdentificationVariables[] = [
922 807
            'expression'   => $identVariable,
923 807
            'nestingLevel' => $this->nestingLevel,
924 807
            'token'        => $this->lexer->token,
925
        ];
926
927 807
        return $identVariable;
928
    }
929
930
    /**
931
     * AliasIdentificationVariable = identifier
932
     *
933
     * @return string
934
     */
935 799 View Code Duplication
    public function AliasIdentificationVariable()
936
    {
937 799
        $this->match(Lexer::T_IDENTIFIER);
938
939 799
        $aliasIdentVariable = $this->lexer->token['value'];
940 799
        $exists = isset($this->queryComponents[$aliasIdentVariable]);
941
942 799
        if ($exists) {
943 2
            $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->lexer->token);
944
        }
945
946 799
        return $aliasIdentVariable;
947
    }
948
949
    /**
950
     * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier
951
     *
952
     * @return string
953
     */
954 820
    public function AbstractSchemaName()
955
    {
956 820
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
957 802
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
958
959 802
            $schemaName = $this->lexer->token['value'];
960 29
        } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
961 19
            $this->match(Lexer::T_IDENTIFIER);
962
963 19
            $schemaName = $this->lexer->token['value'];
964
        } else {
965 11
            $this->match(Lexer::T_ALIASED_NAME);
966
967 10
            list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']);
968
969 10
            $schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
970
        }
971
972 819
        return $schemaName;
973
    }
974
975
    /**
976
     * Validates an AbstractSchemaName, making sure the class exists.
977
     *
978
     * @param string $schemaName The name to validate.
979
     *
980
     * @throws QueryException if the name does not exist.
981
     */
982 814
    private function validateAbstractSchemaName($schemaName)
983
    {
984 814
        if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {
985 16
            $this->semanticalError("Class '$schemaName' is not defined.", $this->lexer->token);
986
        }
987 799
    }
988
989
    /**
990
     * AliasResultVariable ::= identifier
991
     *
992
     * @return string
993
     */
994 119 View Code Duplication
    public function AliasResultVariable()
995
    {
996 119
        $this->match(Lexer::T_IDENTIFIER);
997
998 115
        $resultVariable = $this->lexer->token['value'];
999 115
        $exists = isset($this->queryComponents[$resultVariable]);
1000
1001 115
        if ($exists) {
1002 2
            $this->semanticalError("'$resultVariable' is already defined.", $this->lexer->token);
1003
        }
1004
1005 115
        return $resultVariable;
1006
    }
1007
1008
    /**
1009
     * ResultVariable ::= identifier
1010
     *
1011
     * @return string
1012
     */
1013 30 View Code Duplication
    public function ResultVariable()
1014
    {
1015 30
        $this->match(Lexer::T_IDENTIFIER);
1016
1017 30
        $resultVariable = $this->lexer->token['value'];
1018
1019
        // Defer ResultVariable validation
1020 30
        $this->deferredResultVariables[] = [
1021 30
            'expression'   => $resultVariable,
1022 30
            'nestingLevel' => $this->nestingLevel,
1023 30
            'token'        => $this->lexer->token,
1024
        ];
1025
1026 30
        return $resultVariable;
1027
    }
1028
1029
    /**
1030
     * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
1031
     *
1032
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1033
     */
1034 255
    public function JoinAssociationPathExpression()
1035
    {
1036 255
        $identVariable = $this->IdentificationVariable();
1037
1038 255
        if ( ! isset($this->queryComponents[$identVariable])) {
1039
            $this->semanticalError(
1040
                'Identification Variable ' . $identVariable .' used in join path expression but was not defined before.'
1041
            );
1042
        }
1043
1044 255
        $this->match(Lexer::T_DOT);
1045 255
        $this->match(Lexer::T_IDENTIFIER);
1046
1047 255
        $field = $this->lexer->token['value'];
1048
1049
        // Validate association field
1050 255
        $qComp = $this->queryComponents[$identVariable];
1051 255
        $class = $qComp['metadata'];
1052
1053 255
        if ( ! $class->hasAssociation($field)) {
1054
            $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
1055
        }
1056
1057 255
        return new AST\JoinAssociationPathExpression($identVariable, $field);
1058
    }
1059
1060
    /**
1061
     * Parses an arbitrary path expression and defers semantical validation
1062
     * based on expected types.
1063
     *
1064
     * PathExpression ::= IdentificationVariable {"." identifier}*
1065
     *
1066
     * @param integer $expectedTypes
1067
     *
1068
     * @return \Doctrine\ORM\Query\AST\PathExpression
1069
     */
1070 600
    public function PathExpression($expectedTypes)
1071
    {
1072 600
        $identVariable = $this->IdentificationVariable();
1073 600
        $field = null;
1074
1075 600
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1076 593
            $this->match(Lexer::T_DOT);
1077 593
            $this->match(Lexer::T_IDENTIFIER);
1078
1079 593
            $field = $this->lexer->token['value'];
1080
1081 593 View Code Duplication
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1082 2
                $this->match(Lexer::T_DOT);
1083 2
                $this->match(Lexer::T_IDENTIFIER);
1084 2
                $field .= '.'.$this->lexer->token['value'];
1085
            }
1086
        }
1087
1088
        // Creating AST node
1089 600
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1090
1091
        // Defer PathExpression validation if requested to be deferred
1092 600
        $this->deferredPathExpressions[] = [
1093 600
            'expression'   => $pathExpr,
1094 600
            'nestingLevel' => $this->nestingLevel,
1095 600
            'token'        => $this->lexer->token,
1096
        ];
1097
1098 600
        return $pathExpr;
1099
    }
1100
1101
    /**
1102
     * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
1103
     *
1104
     * @return \Doctrine\ORM\Query\AST\PathExpression
1105
     */
1106
    public function AssociationPathExpression()
1107
    {
1108
        return $this->PathExpression(
1109
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
1110
            AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
1111
        );
1112
    }
1113
1114
    /**
1115
     * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
1116
     *
1117
     * @return \Doctrine\ORM\Query\AST\PathExpression
1118
     */
1119 509
    public function SingleValuedPathExpression()
1120
    {
1121 509
        return $this->PathExpression(
1122 509
            AST\PathExpression::TYPE_STATE_FIELD |
1123 509
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1124
        );
1125
    }
1126
1127
    /**
1128
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1129
     *
1130
     * @return \Doctrine\ORM\Query\AST\PathExpression
1131
     */
1132 203
    public function StateFieldPathExpression()
1133
    {
1134 203
        return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
1135
    }
1136
1137
    /**
1138
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
1139
     *
1140
     * @return \Doctrine\ORM\Query\AST\PathExpression
1141
     */
1142 9
    public function SingleValuedAssociationPathExpression()
1143
    {
1144 9
        return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
1145
    }
1146
1147
    /**
1148
     * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
1149
     *
1150
     * @return \Doctrine\ORM\Query\AST\PathExpression
1151
     */
1152 21
    public function CollectionValuedPathExpression()
1153
    {
1154 21
        return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
1155
    }
1156
1157
    /**
1158
     * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
1159
     *
1160
     * @return \Doctrine\ORM\Query\AST\SelectClause
1161
     */
1162 765
    public function SelectClause()
1163
    {
1164 765
        $isDistinct = false;
1165 765
        $this->match(Lexer::T_SELECT);
1166
1167
        // Check for DISTINCT
1168 765
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1169 6
            $this->match(Lexer::T_DISTINCT);
1170
1171 6
            $isDistinct = true;
1172
        }
1173
1174
        // Process SelectExpressions (1..N)
1175 765
        $selectExpressions = [];
1176 765
        $selectExpressions[] = $this->SelectExpression();
1177
1178 757
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1179 283
            $this->match(Lexer::T_COMMA);
1180
1181 283
            $selectExpressions[] = $this->SelectExpression();
1182
        }
1183
1184 756
        return new AST\SelectClause($selectExpressions, $isDistinct);
1185
    }
1186
1187
    /**
1188
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1189
     *
1190
     * @return \Doctrine\ORM\Query\AST\SimpleSelectClause
1191
     */
1192 48 View Code Duplication
    public function SimpleSelectClause()
1193
    {
1194 48
        $isDistinct = false;
1195 48
        $this->match(Lexer::T_SELECT);
1196
1197 48
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1198
            $this->match(Lexer::T_DISTINCT);
1199
1200
            $isDistinct = true;
1201
        }
1202
1203 48
        return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1204
    }
1205
1206
    /**
1207
     * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1208
     *
1209
     * @return \Doctrine\ORM\Query\AST\UpdateClause
1210
     */
1211 32
    public function UpdateClause()
1212
    {
1213 32
        $this->match(Lexer::T_UPDATE);
1214
1215 32
        $token = $this->lexer->lookahead;
1216 32
        $abstractSchemaName = $this->AbstractSchemaName();
1217
1218 32
        $this->validateAbstractSchemaName($abstractSchemaName);
1219
1220 32
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1221 2
            $this->match(Lexer::T_AS);
1222
        }
1223
1224 32
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1225
1226 32
        $class = $this->em->getClassMetadata($abstractSchemaName);
1227
1228
        // Building queryComponent
1229
        $queryComponent = [
1230 32
            'metadata'     => $class,
1231
            'parent'       => null,
1232
            'relation'     => null,
1233
            'map'          => null,
1234 32
            'nestingLevel' => $this->nestingLevel,
1235 32
            'token'        => $token,
1236
        ];
1237
1238 32
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1239
1240 32
        $this->match(Lexer::T_SET);
1241
1242 32
        $updateItems = [];
1243 32
        $updateItems[] = $this->UpdateItem();
1244
1245 32
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1246 5
            $this->match(Lexer::T_COMMA);
1247
1248 5
            $updateItems[] = $this->UpdateItem();
1249
        }
1250
1251 32
        $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1252 32
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1253
1254 32
        return $updateClause;
1255
    }
1256
1257
    /**
1258
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1259
     *
1260
     * @return \Doctrine\ORM\Query\AST\DeleteClause
1261
     */
1262 41
    public function DeleteClause()
1263
    {
1264 41
        $this->match(Lexer::T_DELETE);
1265
1266 41
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1267 8
            $this->match(Lexer::T_FROM);
1268
        }
1269
1270 41
        $token = $this->lexer->lookahead;
1271 41
        $abstractSchemaName = $this->AbstractSchemaName();
1272
1273 41
        $this->validateAbstractSchemaName($abstractSchemaName);
1274
1275 41
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1276
1277 41
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1278 1
            $this->match(Lexer::T_AS);
1279
        }
1280
1281 41
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1282
1283 40
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1284 40
        $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1285
1286
        // Building queryComponent
1287
        $queryComponent = [
1288 40
            'metadata'     => $class,
1289
            'parent'       => null,
1290
            'relation'     => null,
1291
            'map'          => null,
1292 40
            'nestingLevel' => $this->nestingLevel,
1293 40
            'token'        => $token,
1294
        ];
1295
1296 40
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1297
1298 40
        return $deleteClause;
1299
    }
1300
1301
    /**
1302
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1303
     *
1304
     * @return \Doctrine\ORM\Query\AST\FromClause
1305
     */
1306 756
    public function FromClause()
1307
    {
1308 756
        $this->match(Lexer::T_FROM);
1309
1310 751
        $identificationVariableDeclarations = [];
1311 751
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1312
1313 731
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1314 6
            $this->match(Lexer::T_COMMA);
1315
1316 6
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1317
        }
1318
1319 731
        return new AST\FromClause($identificationVariableDeclarations);
1320
    }
1321
1322
    /**
1323
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1324
     *
1325
     * @return \Doctrine\ORM\Query\AST\SubselectFromClause
1326
     */
1327 48
    public function SubselectFromClause()
1328
    {
1329 48
        $this->match(Lexer::T_FROM);
1330
1331 48
        $identificationVariables = [];
1332 48
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1333
1334 47
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1335
            $this->match(Lexer::T_COMMA);
1336
1337
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1338
        }
1339
1340 47
        return new AST\SubselectFromClause($identificationVariables);
1341
    }
1342
1343
    /**
1344
     * WhereClause ::= "WHERE" ConditionalExpression
1345
     *
1346
     * @return \Doctrine\ORM\Query\AST\WhereClause
1347
     */
1348 333
    public function WhereClause()
1349
    {
1350 333
        $this->match(Lexer::T_WHERE);
1351
1352 333
        return new AST\WhereClause($this->ConditionalExpression());
1353
    }
1354
1355
    /**
1356
     * HavingClause ::= "HAVING" ConditionalExpression
1357
     *
1358
     * @return \Doctrine\ORM\Query\AST\HavingClause
1359
     */
1360 21
    public function HavingClause()
1361
    {
1362 21
        $this->match(Lexer::T_HAVING);
1363
1364 21
        return new AST\HavingClause($this->ConditionalExpression());
1365
    }
1366
1367
    /**
1368
     * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1369
     *
1370
     * @return \Doctrine\ORM\Query\AST\GroupByClause
1371
     */
1372 32
    public function GroupByClause()
1373
    {
1374 32
        $this->match(Lexer::T_GROUP);
1375 32
        $this->match(Lexer::T_BY);
1376
1377 32
        $groupByItems = [$this->GroupByItem()];
1378
1379 31
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1380 8
            $this->match(Lexer::T_COMMA);
1381
1382 8
            $groupByItems[] = $this->GroupByItem();
1383
        }
1384
1385 31
        return new AST\GroupByClause($groupByItems);
1386
    }
1387
1388
    /**
1389
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1390
     *
1391
     * @return \Doctrine\ORM\Query\AST\OrderByClause
1392
     */
1393 179
    public function OrderByClause()
1394
    {
1395 179
        $this->match(Lexer::T_ORDER);
1396 179
        $this->match(Lexer::T_BY);
1397
1398 179
        $orderByItems = [];
1399 179
        $orderByItems[] = $this->OrderByItem();
1400
1401 179
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1402 14
            $this->match(Lexer::T_COMMA);
1403
1404 14
            $orderByItems[] = $this->OrderByItem();
1405
        }
1406
1407 179
        return new AST\OrderByClause($orderByItems);
1408
    }
1409
1410
    /**
1411
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1412
     *
1413
     * @return \Doctrine\ORM\Query\AST\Subselect
1414
     */
1415 48
    public function Subselect()
1416
    {
1417
        // Increase query nesting level
1418 48
        $this->nestingLevel++;
1419
1420 48
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1421
1422 47
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1423 47
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1424 47
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1425 47
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1426
1427
        // Decrease query nesting level
1428 47
        $this->nestingLevel--;
1429
1430 47
        return $subselect;
1431
    }
1432
1433
    /**
1434
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1435
     *
1436
     * @return \Doctrine\ORM\Query\AST\UpdateItem
1437
     */
1438 32
    public function UpdateItem()
1439
    {
1440 32
        $pathExpr = $this->SingleValuedPathExpression();
1441
1442 32
        $this->match(Lexer::T_EQUALS);
1443
1444 32
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1445
1446 32
        return $updateItem;
1447
    }
1448
1449
    /**
1450
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1451
     *
1452
     * @return string | \Doctrine\ORM\Query\AST\PathExpression
1453
     */
1454 32
    public function GroupByItem()
1455
    {
1456
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1457 32
        $glimpse = $this->lexer->glimpse();
1458
1459 32
        if ($glimpse['type'] === Lexer::T_DOT) {
1460 13
            return $this->SingleValuedPathExpression();
1461
        }
1462
1463
        // Still need to decide between IdentificationVariable or ResultVariable
1464 19
        $lookaheadValue = $this->lexer->lookahead['value'];
1465
1466 19
        if ( ! isset($this->queryComponents[$lookaheadValue])) {
1467 1
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1468
        }
1469
1470 18
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1471 16
            ? $this->IdentificationVariable()
1472 18
            : $this->ResultVariable();
1473
    }
1474
1475
    /**
1476
     * OrderByItem ::= (
1477
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1478
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1479
     * ) ["ASC" | "DESC"]
1480
     *
1481
     * @return \Doctrine\ORM\Query\AST\OrderByItem
1482
     */
1483 179
    public function OrderByItem()
1484
    {
1485 179
        $this->lexer->peek(); // lookahead => '.'
1486 179
        $this->lexer->peek(); // lookahead => token after '.'
1487
1488 179
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1489
1490 179
        $this->lexer->resetPeek();
1491
1492 179
        $glimpse = $this->lexer->glimpse();
1493
1494
        switch (true) {
1495 179
            case ($this->isFunction()):
1496 1
                $expr = $this->FunctionDeclaration();
1497 1
                break;
1498
1499 178
            case ($this->isMathOperator($peek)):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1488 can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1500 25
                $expr = $this->SimpleArithmeticExpression();
1501 25
                break;
1502
1503 154
            case ($glimpse['type'] === Lexer::T_DOT):
1504 141
                $expr = $this->SingleValuedPathExpression();
1505 141
                break;
1506
1507 17
            case ($this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis())):
0 ignored issues
show
Bug introduced by
It seems like $this->peekBeyondClosingParenthesis() targeting Doctrine\ORM\Query\Parse...ondClosingParenthesis() can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1508 2
                $expr = $this->ScalarExpression();
1509 2
                break;
1510
1511
            default:
1512 15
                $expr = $this->ResultVariable();
1513 15
                break;
1514
        }
1515
1516 179
        $type = 'ASC';
1517 179
        $item = new AST\OrderByItem($expr);
1518
1519
        switch (true) {
1520 179
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1521 93
                $this->match(Lexer::T_DESC);
1522 93
                $type = 'DESC';
1523 93
                break;
1524
1525 152
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1526 96
                $this->match(Lexer::T_ASC);
1527 96
                break;
1528
1529
            default:
1530
                // Do nothing
1531
        }
1532
1533 179
        $item->type = $type;
1534
1535 179
        return $item;
1536
    }
1537
1538
    /**
1539
     * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1540
     *      EnumPrimary | SimpleEntityExpression | "NULL"
1541
     *
1542
     * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1543
     * grammar that needs to be supported:
1544
     *
1545
     * NewValue ::= SimpleArithmeticExpression | "NULL"
1546
     *
1547
     * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1548
     *
1549
     * @return AST\ArithmeticExpression
1550
     */
1551 32
    public function NewValue()
1552
    {
1553 32
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1554 1
            $this->match(Lexer::T_NULL);
1555
1556 1
            return null;
1557
        }
1558
1559 31 View Code Duplication
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1560 19
            $this->match(Lexer::T_INPUT_PARAMETER);
1561
1562 19
            return new AST\InputParameter($this->lexer->token['value']);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \Doctrine\ORM...lexer->token['value']); (Doctrine\ORM\Query\AST\InputParameter) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::NewValue of type Doctrine\ORM\Query\AST\ArithmeticExpression|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
1563
        }
1564
1565 12
        return $this->ArithmeticExpression();
1566
    }
1567
1568
    /**
1569
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1570
     *
1571
     * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1572
     */
1573 753
    public function IdentificationVariableDeclaration()
1574
    {
1575 753
        $joins                    = [];
1576 753
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1577 736
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1578 7
            ? $this->IndexBy()
1579 736
            : null;
1580
1581 736
        $rangeVariableDeclaration->isRoot = true;
1582
1583
        while (
1584 736
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1585 736
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1586 735
            $this->lexer->isNextToken(Lexer::T_JOIN)
1587
        ) {
1588 277
            $joins[] = $this->Join();
1589
        }
1590
1591 733
        return new AST\IdentificationVariableDeclaration(
1592 733
            $rangeVariableDeclaration, $indexBy, $joins
1593
        );
1594
    }
1595
1596
    /**
1597
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1598
     *
1599
     * {Internal note: WARNING: Solution is harder than a bare implementation.
1600
     * Desired EBNF support:
1601
     *
1602
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1603
     *
1604
     * It demands that entire SQL generation to become programmatical. This is
1605
     * needed because association based subselect requires "WHERE" conditional
1606
     * expressions to be injected, but there is no scope to do that. Only scope
1607
     * accessible is "FROM", prohibiting an easy implementation without larger
1608
     * changes.}
1609
     *
1610
     * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration |
1611
     *         \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1612
     */
1613 48
    public function SubselectIdentificationVariableDeclaration()
1614
    {
1615
        /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1616
        NOT YET IMPLEMENTED!
1617
1618
        $glimpse = $this->lexer->glimpse();
1619
1620
        if ($glimpse['type'] == Lexer::T_DOT) {
1621
            $associationPathExpression = $this->AssociationPathExpression();
1622
1623
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1624
                $this->match(Lexer::T_AS);
1625
            }
1626
1627
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1628
            $identificationVariable      = $associationPathExpression->identificationVariable;
1629
            $field                       = $associationPathExpression->associationField;
1630
1631
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1632
            $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1633
1634
            // Building queryComponent
1635
            $joinQueryComponent = array(
1636
                'metadata'     => $targetClass,
1637
                'parent'       => $identificationVariable,
1638
                'relation'     => $class->getAssociationMapping($field),
1639
                'map'          => null,
1640
                'nestingLevel' => $this->nestingLevel,
1641
                'token'        => $this->lexer->lookahead
1642
            );
1643
1644
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1645
1646
            return new AST\SubselectIdentificationVariableDeclaration(
1647
                $associationPathExpression, $aliasIdentificationVariable
1648
            );
1649
        }
1650
        */
1651
1652 48
        return $this->IdentificationVariableDeclaration();
1653
    }
1654
1655
    /**
1656
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1657
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1658
     *          ["WITH" ConditionalExpression]
1659
     *
1660
     * @return \Doctrine\ORM\Query\AST\Join
1661
     */
1662 277
    public function Join()
1663
    {
1664
        // Check Join type
1665 277
        $joinType = AST\Join::JOIN_TYPE_INNER;
1666
1667
        switch (true) {
1668 277
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1669 67
                $this->match(Lexer::T_LEFT);
1670
1671 67
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1672
1673
                // Possible LEFT OUTER join
1674 67
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1675
                    $this->match(Lexer::T_OUTER);
1676
1677
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1678
                }
1679 67
                break;
1680
1681 214
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1682 20
                $this->match(Lexer::T_INNER);
1683 20
                break;
1684
1685
            default:
1686
                // Do nothing
1687
        }
1688
1689 277
        $this->match(Lexer::T_JOIN);
1690
1691 277
        $next            = $this->lexer->glimpse();
1692 277
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1693 274
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1694 274
        $join            = new AST\Join($joinType, $joinDeclaration);
1695
1696
        // Describe non-root join declaration
1697 274
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1698 21
            $joinDeclaration->isRoot = false;
1699
        }
1700
1701
        // Check for ad-hoc Join conditions
1702 274
        if ($adhocConditions) {
1703 23
            $this->match(Lexer::T_WITH);
1704
1705 23
            $join->conditionalExpression = $this->ConditionalExpression();
1706
        }
1707
1708 274
        return $join;
1709
    }
1710
1711
    /**
1712
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1713
     *
1714
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1715
     *
1716
     * @throws QueryException
1717
     */
1718 753
    public function RangeVariableDeclaration()
1719
    {
1720 753
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()['type'] === Lexer::T_SELECT) {
1721 2
            $this->semanticalError('Subquery is not supported here', $this->lexer->token);
1722
        }
1723
1724 752
        $abstractSchemaName = $this->AbstractSchemaName();
1725
1726 751
        $this->validateAbstractSchemaName($abstractSchemaName);
1727
1728 736
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1729 5
            $this->match(Lexer::T_AS);
1730
        }
1731
1732 736
        $token = $this->lexer->lookahead;
1733 736
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1734 736
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1735
1736
        // Building queryComponent
1737
        $queryComponent = [
1738 736
            'metadata'     => $classMetadata,
1739
            'parent'       => null,
1740
            'relation'     => null,
1741
            'map'          => null,
1742 736
            'nestingLevel' => $this->nestingLevel,
1743 736
            'token'        => $token
1744
        ];
1745
1746 736
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1747
1748 736
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1749
    }
1750
1751
    /**
1752
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1753
     *
1754
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1755
     */
1756 255
    public function JoinAssociationDeclaration()
1757
    {
1758 255
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1759
1760 255
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1761 4
            $this->match(Lexer::T_AS);
1762
        }
1763
1764 255
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1765 253
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1766
1767 253
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1768 253
        $field                  = $joinAssociationPathExpression->associationField;
1769
1770 253
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1771 253
        $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1772
1773
        // Building queryComponent
1774
        $joinQueryComponent = [
1775 253
            'metadata'     => $targetClass,
1776 253
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1777 253
            'relation'     => $class->getAssociationMapping($field),
1778
            'map'          => null,
1779 253
            'nestingLevel' => $this->nestingLevel,
1780 253
            'token'        => $this->lexer->lookahead
1781
        ];
1782
1783 253
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1784
1785 253
        return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
1786
    }
1787
1788
    /**
1789
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1790
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1791
     *
1792
     * @return \Doctrine\ORM\Query\AST\PartialObjectExpression
1793
     */
1794 11
    public function PartialObjectExpression()
1795
    {
1796 11
        $this->match(Lexer::T_PARTIAL);
1797
1798 11
        $partialFieldSet = [];
1799
1800 11
        $identificationVariable = $this->IdentificationVariable();
1801
1802 11
        $this->match(Lexer::T_DOT);
1803 11
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1804 11
        $this->match(Lexer::T_IDENTIFIER);
1805
1806 11
        $field = $this->lexer->token['value'];
1807
1808
        // First field in partial expression might be embeddable property
1809 11 View Code Duplication
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1810 1
            $this->match(Lexer::T_DOT);
1811 1
            $this->match(Lexer::T_IDENTIFIER);
1812 1
            $field .= '.'.$this->lexer->token['value'];
1813
        }
1814
1815 11
        $partialFieldSet[] = $field;
1816
1817 11
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1818 9
            $this->match(Lexer::T_COMMA);
1819 9
            $this->match(Lexer::T_IDENTIFIER);
1820
1821 9
            $field = $this->lexer->token['value'];
1822
1823 9 View Code Duplication
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1824 2
                $this->match(Lexer::T_DOT);
1825 2
                $this->match(Lexer::T_IDENTIFIER);
1826 2
                $field .= '.'.$this->lexer->token['value'];
1827
            }
1828
1829 9
            $partialFieldSet[] = $field;
1830
        }
1831
1832 11
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1833
1834 11
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1835
1836
        // Defer PartialObjectExpression validation
1837 11
        $this->deferredPartialObjectExpressions[] = [
1838 11
            'expression'   => $partialObjectExpression,
1839 11
            'nestingLevel' => $this->nestingLevel,
1840 11
            'token'        => $this->lexer->token,
1841
        ];
1842
1843 11
        return $partialObjectExpression;
1844
    }
1845
1846
    /**
1847
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1848
     *
1849
     * @return \Doctrine\ORM\Query\AST\NewObjectExpression
1850
     */
1851 28
    public function NewObjectExpression()
1852
    {
1853 28
        $this->match(Lexer::T_NEW);
1854
1855 28
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1856 28
        $token = $this->lexer->token;
1857
1858 28
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1859
1860 28
        $args[] = $this->NewObjectArg();
0 ignored issues
show
Coding Style Comprehensibility introduced by
$args was never initialized. Although not strictly required by PHP, it is generally a good practice to add $args = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1861
1862 28
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1863 24
            $this->match(Lexer::T_COMMA);
1864
1865 24
            $args[] = $this->NewObjectArg();
1866
        }
1867
1868 28
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1869
1870 28
        $expression = new AST\NewObjectExpression($className, $args);
1871
1872
        // Defer NewObjectExpression validation
1873 28
        $this->deferredNewObjectExpressions[] = [
1874 28
            'token'        => $token,
1875 28
            'expression'   => $expression,
1876 28
            'nestingLevel' => $this->nestingLevel,
1877
        ];
1878
1879 28
        return $expression;
1880
    }
1881
1882
    /**
1883
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1884
     *
1885
     * @return mixed
1886
     */
1887 28
    public function NewObjectArg()
1888
    {
1889 28
        $token = $this->lexer->lookahead;
1890 28
        $peek  = $this->lexer->glimpse();
1891
1892 28
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1893 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1894 2
            $expression = $this->Subselect();
1895 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1896
1897 2
            return $expression;
1898
        }
1899
1900 28
        return $this->ScalarExpression();
1901
    }
1902
1903
    /**
1904
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1905
     *
1906
     * @return \Doctrine\ORM\Query\AST\IndexBy
1907
     */
1908 11
    public function IndexBy()
1909
    {
1910 11
        $this->match(Lexer::T_INDEX);
1911 11
        $this->match(Lexer::T_BY);
1912 11
        $pathExpr = $this->StateFieldPathExpression();
1913
1914
        // Add the INDEX BY info to the query component
1915 11
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1916
1917 11
        return new AST\IndexBy($pathExpr);
1918
    }
1919
1920
    /**
1921
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1922
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1923
     *                      InstanceOfExpression
1924
     *
1925
     * @return mixed One of the possible expressions or subexpressions.
1926
     */
1927 161
    public function ScalarExpression()
1928
    {
1929 161
        $lookahead = $this->lexer->lookahead['type'];
1930 161
        $peek      = $this->lexer->glimpse();
1931
1932
        switch (true) {
1933 161
            case ($lookahead === Lexer::T_INTEGER):
1934 158
            case ($lookahead === Lexer::T_FLOAT):
1935
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1936 158
            case ($lookahead === Lexer::T_MINUS):
1937 158
            case ($lookahead === Lexer::T_PLUS):
1938 17
                return $this->SimpleArithmeticExpression();
1939
1940 158
            case ($lookahead === Lexer::T_STRING):
1941 13
                return $this->StringPrimary();
1942
1943 156
            case ($lookahead === Lexer::T_TRUE):
1944 156 View Code Duplication
            case ($lookahead === Lexer::T_FALSE):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1945 3
                $this->match($lookahead);
1946
1947 3
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1948
1949 156
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
1950
                switch (true) {
1951 1
                    case $this->isMathOperator($peek):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->glimpse() on line 1930 can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1952
                        // :param + u.value
1953 1
                        return $this->SimpleArithmeticExpression();
1954
                    default:
1955
                        return $this->InputParameter();
1956
                }
1957
1958 156
            case ($lookahead === Lexer::T_CASE):
1959 152
            case ($lookahead === Lexer::T_COALESCE):
1960 152
            case ($lookahead === Lexer::T_NULLIF):
1961
                // Since NULLIF and COALESCE can be identified as a function,
1962
                // we need to check these before checking for FunctionDeclaration
1963 8
                return $this->CaseExpression();
1964
1965 152
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1966 4
                return $this->SimpleArithmeticExpression();
1967
1968
            // this check must be done before checking for a filed path expression
1969 149
            case ($this->isFunction()):
1970 26
                $this->lexer->peek(); // "("
1971
1972
                switch (true) {
1973 26
                    case ($this->isMathOperator($this->peekBeyondClosingParenthesis())):
0 ignored issues
show
Bug introduced by
It seems like $this->peekBeyondClosingParenthesis() targeting Doctrine\ORM\Query\Parse...ondClosingParenthesis() can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1974
                        // SUM(u.id) + COUNT(u.id)
1975 7
                        return $this->SimpleArithmeticExpression();
1976
1977
                    default:
1978
                        // IDENTITY(u)
1979 21
                        return $this->FunctionDeclaration();
1980
                }
1981
1982
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1983
            // it is no function, so it must be a field path
1984 131
            case ($lookahead === Lexer::T_IDENTIFIER):
1985 131
                $this->lexer->peek(); // lookahead => '.'
1986 131
                $this->lexer->peek(); // lookahead => token after '.'
1987 131
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1988 131
                $this->lexer->resetPeek();
1989
1990 131
                if ($this->isMathOperator($peek)) {
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1987 can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1991 7
                    return $this->SimpleArithmeticExpression();
1992
                }
1993
1994 126
                return $this->StateFieldPathExpression();
1995
1996
            default:
1997
                $this->syntaxError();
1998
        }
1999
    }
2000
2001
    /**
2002
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
2003
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2004
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2005
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2006
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2007
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2008
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2009
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2010
     *
2011
     * @return mixed One of the possible expressions or subexpressions.
2012
     */
2013 19
    public function CaseExpression()
2014
    {
2015 19
        $lookahead = $this->lexer->lookahead['type'];
2016
2017
        switch ($lookahead) {
2018 19
            case Lexer::T_NULLIF:
2019 5
                return $this->NullIfExpression();
2020
2021 16
            case Lexer::T_COALESCE:
2022 2
                return $this->CoalesceExpression();
2023
2024 14
            case Lexer::T_CASE:
2025 14
                $this->lexer->resetPeek();
2026 14
                $peek = $this->lexer->peek();
2027
2028 14
                if ($peek['type'] === Lexer::T_WHEN) {
2029 9
                    return $this->GeneralCaseExpression();
2030
                }
2031
2032 5
                return $this->SimpleCaseExpression();
2033
2034
            default:
2035
                // Do nothing
2036
                break;
2037
        }
2038
2039
        $this->syntaxError();
2040
    }
2041
2042
    /**
2043
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2044
     *
2045
     * @return \Doctrine\ORM\Query\AST\CoalesceExpression
2046
     */
2047 3
    public function CoalesceExpression()
2048
    {
2049 3
        $this->match(Lexer::T_COALESCE);
2050 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2051
2052
        // Process ScalarExpressions (1..N)
2053 3
        $scalarExpressions = [];
2054 3
        $scalarExpressions[] = $this->ScalarExpression();
2055
2056 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2057 3
            $this->match(Lexer::T_COMMA);
2058
2059 3
            $scalarExpressions[] = $this->ScalarExpression();
2060
        }
2061
2062 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2063
2064 3
        return new AST\CoalesceExpression($scalarExpressions);
2065
    }
2066
2067
    /**
2068
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2069
     *
2070
     * @return \Doctrine\ORM\Query\AST\NullIfExpression
2071
     */
2072 5
    public function NullIfExpression()
2073
    {
2074 5
        $this->match(Lexer::T_NULLIF);
2075 5
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2076
2077 5
        $firstExpression = $this->ScalarExpression();
2078 5
        $this->match(Lexer::T_COMMA);
2079 5
        $secondExpression = $this->ScalarExpression();
2080
2081 5
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2082
2083 5
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2084
    }
2085
2086
    /**
2087
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2088
     *
2089
     * @return \Doctrine\ORM\Query\AST\GeneralCaseExpression
2090
     */
2091 9 View Code Duplication
    public function GeneralCaseExpression()
2092
    {
2093 9
        $this->match(Lexer::T_CASE);
2094
2095
        // Process WhenClause (1..N)
2096 9
        $whenClauses = [];
2097
2098
        do {
2099 9
            $whenClauses[] = $this->WhenClause();
2100 9
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2101
2102 9
        $this->match(Lexer::T_ELSE);
2103 9
        $scalarExpression = $this->ScalarExpression();
2104 9
        $this->match(Lexer::T_END);
2105
2106 9
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2107
    }
2108
2109
    /**
2110
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2111
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2112
     *
2113
     * @return AST\SimpleCaseExpression
2114
     */
2115 5 View Code Duplication
    public function SimpleCaseExpression()
2116
    {
2117 5
        $this->match(Lexer::T_CASE);
2118 5
        $caseOperand = $this->StateFieldPathExpression();
2119
2120
        // Process SimpleWhenClause (1..N)
2121 5
        $simpleWhenClauses = [];
2122
2123
        do {
2124 5
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2125 5
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2126
2127 5
        $this->match(Lexer::T_ELSE);
2128 5
        $scalarExpression = $this->ScalarExpression();
2129 5
        $this->match(Lexer::T_END);
2130
2131 5
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2132
    }
2133
2134
    /**
2135
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2136
     *
2137
     * @return \Doctrine\ORM\Query\AST\WhenClause
2138
     */
2139 9
    public function WhenClause()
2140
    {
2141 9
        $this->match(Lexer::T_WHEN);
2142 9
        $conditionalExpression = $this->ConditionalExpression();
2143 9
        $this->match(Lexer::T_THEN);
2144
2145 9
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2146
    }
2147
2148
    /**
2149
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2150
     *
2151
     * @return \Doctrine\ORM\Query\AST\SimpleWhenClause
2152
     */
2153 5
    public function SimpleWhenClause()
2154
    {
2155 5
        $this->match(Lexer::T_WHEN);
2156 5
        $conditionalExpression = $this->ScalarExpression();
2157 5
        $this->match(Lexer::T_THEN);
2158
2159 5
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2160
    }
2161
2162
    /**
2163
     * SelectExpression ::= (
2164
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2165
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2166
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2167
     *
2168
     * @return \Doctrine\ORM\Query\AST\SelectExpression
2169
     */
2170 765
    public function SelectExpression()
2171
    {
2172 765
        $expression    = null;
2173 765
        $identVariable = null;
2174 765
        $peek          = $this->lexer->glimpse();
2175 765
        $lookaheadType = $this->lexer->lookahead['type'];
2176
2177
        switch (true) {
2178
            // ScalarExpression (u.name)
2179 765
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2180 103
                $expression = $this->ScalarExpression();
2181 103
                break;
2182
2183
            // IdentificationVariable (u)
2184 705
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2185 589
                $expression = $identVariable = $this->IdentificationVariable();
2186 589
                break;
2187
2188
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2189 177
            case ($lookaheadType === Lexer::T_CASE):
2190 172
            case ($lookaheadType === Lexer::T_COALESCE):
2191 170
            case ($lookaheadType === Lexer::T_NULLIF):
2192 9
                $expression = $this->CaseExpression();
2193 9
                break;
2194
2195
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2196 168
            case ($this->isFunction()):
2197 88
                $this->lexer->peek(); // "("
2198
2199
                switch (true) {
2200 88
                    case ($this->isMathOperator($this->peekBeyondClosingParenthesis())):
0 ignored issues
show
Bug introduced by
It seems like $this->peekBeyondClosingParenthesis() targeting Doctrine\ORM\Query\Parse...ondClosingParenthesis() can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2201
                        // SUM(u.id) + COUNT(u.id)
2202 2
                        $expression = $this->ScalarExpression();
2203 2
                        break;
2204
2205
                    default:
2206
                        // IDENTITY(u)
2207 86
                        $expression = $this->FunctionDeclaration();
2208 86
                        break;
2209
                }
2210
2211 88
                break;
2212
2213
            // PartialObjectExpression (PARTIAL u.{id, name})
2214 80
            case ($lookaheadType === Lexer::T_PARTIAL):
2215 11
                $expression    = $this->PartialObjectExpression();
2216 11
                $identVariable = $expression->identificationVariable;
2217 11
                break;
2218
2219
            // Subselect
2220 69
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2221 22
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2222 22
                $expression = $this->Subselect();
2223 22
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2224 22
                break;
2225
2226
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2227 47
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2228 43
            case ($lookaheadType === Lexer::T_INTEGER):
2229 41
            case ($lookaheadType === Lexer::T_STRING):
2230 32
            case ($lookaheadType === Lexer::T_FLOAT):
2231
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2232 32
            case ($lookaheadType === Lexer::T_MINUS):
2233 32
            case ($lookaheadType === Lexer::T_PLUS):
2234 16
                $expression = $this->SimpleArithmeticExpression();
2235 16
                break;
2236
2237
            // NewObjectExpression (New ClassName(id, name))
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2238 31
            case ($lookaheadType === Lexer::T_NEW):
2239 28
                $expression = $this->NewObjectExpression();
2240 28
                break;
2241
2242
            default:
2243 3
                $this->syntaxError(
2244 3
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2245 3
                    $this->lexer->lookahead
2246
                );
2247
        }
2248
2249
        // [["AS"] ["HIDDEN"] AliasResultVariable]
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2250 762
        $mustHaveAliasResultVariable = false;
2251
2252 762
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2253 110
            $this->match(Lexer::T_AS);
2254
2255 110
            $mustHaveAliasResultVariable = true;
2256
        }
2257
2258 762
        $hiddenAliasResultVariable = false;
2259
2260 762
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2261 10
            $this->match(Lexer::T_HIDDEN);
2262
2263 10
            $hiddenAliasResultVariable = true;
2264
        }
2265
2266 762
        $aliasResultVariable = null;
2267
2268 762 View Code Duplication
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2269 119
            $token = $this->lexer->lookahead;
2270 119
            $aliasResultVariable = $this->AliasResultVariable();
2271
2272
            // Include AliasResultVariable in query components.
2273 114
            $this->queryComponents[$aliasResultVariable] = [
2274 114
                'resultVariable' => $expression,
2275 114
                'nestingLevel'   => $this->nestingLevel,
2276 114
                'token'          => $token,
2277
            ];
2278
        }
2279
2280
        // AST
2281
2282 757
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2283
2284 757
        if ($identVariable) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $identVariable of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2285 597
            $this->identVariableExpressions[$identVariable] = $expr;
2286
        }
2287
2288 757
        return $expr;
2289
    }
2290
2291
    /**
2292
     * SimpleSelectExpression ::= (
2293
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2294
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2295
     * ) [["AS"] AliasResultVariable]
2296
     *
2297
     * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
2298
     */
2299 48
    public function SimpleSelectExpression()
2300
    {
2301 48
        $peek = $this->lexer->glimpse();
2302
2303 48
        switch ($this->lexer->lookahead['type']) {
2304 48
            case Lexer::T_IDENTIFIER:
2305
                switch (true) {
2306 19
                    case ($peek['type'] === Lexer::T_DOT):
2307 16
                        $expression = $this->StateFieldPathExpression();
2308
2309 16
                        return new AST\SimpleSelectExpression($expression);
2310
2311 3
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2312 2
                        $expression = $this->IdentificationVariable();
2313
2314 2
                        return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Documentation introduced by
$expression is of type string, but the function expects a object<Doctrine\ORM\Query\AST\Node>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
2315
2316 1
                    case ($this->isFunction()):
2317
                        // SUM(u.id) + COUNT(u.id)
2318 1
                        if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) {
0 ignored issues
show
Bug introduced by
It seems like $this->peekBeyondClosingParenthesis() targeting Doctrine\ORM\Query\Parse...ondClosingParenthesis() can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2319
                            return new AST\SimpleSelectExpression($this->ScalarExpression());
0 ignored issues
show
Bug introduced by
It seems like $this->ScalarExpression() targeting Doctrine\ORM\Query\Parser::ScalarExpression() can also be of type null; however, Doctrine\ORM\Query\AST\S...pression::__construct() does only seem to accept object<Doctrine\ORM\Query\AST\Node>, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2320
                        }
2321
                        // COUNT(u.id)
2322 1
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2323
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2324
                        }
2325
                        // IDENTITY(u)
2326 1
                        return new AST\SimpleSelectExpression($this->FunctionDeclaration());
0 ignored issues
show
Bug introduced by
It seems like $this->FunctionDeclaration() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2327
2328
                    default:
2329
                        // Do nothing
2330
                }
2331
                break;
2332
2333 30
            case Lexer::T_OPEN_PARENTHESIS:
2334 3
                if ($peek['type'] !== Lexer::T_SELECT) {
2335
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2336 3
                    $expression = $this->SimpleArithmeticExpression();
2337
2338 3
                    return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->SimpleArithmeticExpression() on line 2336 can be null; however, Doctrine\ORM\Query\AST\S...pression::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2339
                }
2340
2341
                // Subselect
2342
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2343
                $expression = $this->Subselect();
2344
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2345
2346
                return new AST\SimpleSelectExpression($expression);
2347
2348
            default:
2349
                // Do nothing
2350
        }
2351
2352 27
        $this->lexer->peek();
2353
2354 27
        $expression = $this->ScalarExpression();
2355 27
        $expr       = new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->ScalarExpression() on line 2354 can also be of type null; however, Doctrine\ORM\Query\AST\S...pression::__construct() does only seem to accept object<Doctrine\ORM\Query\AST\Node>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2356
2357 27
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2358 1
            $this->match(Lexer::T_AS);
2359
        }
2360
2361 27 View Code Duplication
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2362 2
            $token = $this->lexer->lookahead;
2363 2
            $resultVariable = $this->AliasResultVariable();
2364 2
            $expr->fieldIdentificationVariable = $resultVariable;
2365
2366
            // Include AliasResultVariable in query components.
2367 2
            $this->queryComponents[$resultVariable] = [
2368 2
                'resultvariable' => $expr,
2369 2
                'nestingLevel'   => $this->nestingLevel,
2370 2
                'token'          => $token,
2371
            ];
2372
        }
2373
2374 27
        return $expr;
2375
    }
2376
2377
    /**
2378
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2379
     *
2380
     * @return \Doctrine\ORM\Query\AST\ConditionalExpression
2381
     */
2382 375 View Code Duplication
    public function ConditionalExpression()
2383
    {
2384 375
        $conditionalTerms = [];
2385 375
        $conditionalTerms[] = $this->ConditionalTerm();
2386
2387 372
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2388 16
            $this->match(Lexer::T_OR);
2389
2390 16
            $conditionalTerms[] = $this->ConditionalTerm();
2391
        }
2392
2393
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2394
        // if only one AST\ConditionalTerm is defined
2395 372
        if (count($conditionalTerms) == 1) {
2396 364
            return $conditionalTerms[0];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $conditionalTerms[0]; (Doctrine\ORM\Query\AST\ConditionalTerm) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ConditionalExpression of type Doctrine\ORM\Query\AST\ConditionalExpression.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2397
        }
2398
2399 16
        return new AST\ConditionalExpression($conditionalTerms);
2400
    }
2401
2402
    /**
2403
     * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2404
     *
2405
     * @return \Doctrine\ORM\Query\AST\ConditionalTerm
2406
     */
2407 375 View Code Duplication
    public function ConditionalTerm()
2408
    {
2409 375
        $conditionalFactors = [];
2410 375
        $conditionalFactors[] = $this->ConditionalFactor();
2411
2412 372
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2413 32
            $this->match(Lexer::T_AND);
2414
2415 32
            $conditionalFactors[] = $this->ConditionalFactor();
2416
        }
2417
2418
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2419
        // if only one AST\ConditionalFactor is defined
2420 372
        if (count($conditionalFactors) == 1) {
2421 354
            return $conditionalFactors[0];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $conditionalFactors[0]; (Doctrine\ORM\Query\AST\ConditionalFactor) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ConditionalTerm of type Doctrine\ORM\Query\AST\ConditionalTerm.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2422
        }
2423
2424 32
        return new AST\ConditionalTerm($conditionalFactors);
2425
    }
2426
2427
    /**
2428
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2429
     *
2430
     * @return \Doctrine\ORM\Query\AST\ConditionalFactor
2431
     */
2432 375
    public function ConditionalFactor()
2433
    {
2434 375
        $not = false;
2435
2436 375
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2437 6
            $this->match(Lexer::T_NOT);
2438
2439 6
            $not = true;
2440
        }
2441
2442 375
        $conditionalPrimary = $this->ConditionalPrimary();
2443
2444
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2445
        // if only one AST\ConditionalPrimary is defined
2446 372
        if ( ! $not) {
2447 370
            return $conditionalPrimary;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $conditionalPrimary; (Doctrine\ORM\Query\AST\ConditionalPrimary) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ConditionalFactor of type Doctrine\ORM\Query\AST\ConditionalFactor.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2448
        }
2449
2450 6
        $conditionalFactor = new AST\ConditionalFactor($conditionalPrimary);
2451 6
        $conditionalFactor->not = $not;
2452
2453 6
        return $conditionalFactor;
2454
    }
2455
2456
    /**
2457
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2458
     *
2459
     * @return \Doctrine\ORM\Query\AST\ConditionalPrimary
2460
     */
2461 375
    public function ConditionalPrimary()
2462
    {
2463 375
        $condPrimary = new AST\ConditionalPrimary;
2464
2465 375
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2466 366
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2467
2468 363
            return $condPrimary;
2469
        }
2470
2471
        // Peek beyond the matching closing parenthesis ')'
2472 25
        $peek = $this->peekBeyondClosingParenthesis();
2473
2474 25
        if (in_array($peek['value'], ["=",  "<", "<=", "<>", ">", ">=", "!="]) ||
2475 22
            in_array($peek['type'], [Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS]) ||
2476 22
            $this->isMathOperator($peek)) {
2477 15
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2478
2479 15
            return $condPrimary;
2480
        }
2481
2482 21
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2483 21
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2484 21
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2485
2486 21
        return $condPrimary;
2487
    }
2488
2489
    /**
2490
     * SimpleConditionalExpression ::=
2491
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2492
     *      InExpression | NullComparisonExpression | ExistsExpression |
2493
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2494
     *      InstanceOfExpression
2495
     */
2496 375
    public function SimpleConditionalExpression()
2497
    {
2498 375
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2499 7
            return $this->ExistsExpression();
2500
        }
2501
2502 375
        $token      = $this->lexer->lookahead;
2503 375
        $peek       = $this->lexer->glimpse();
2504 375
        $lookahead  = $token;
2505
2506 375
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2507
            $token = $this->lexer->glimpse();
2508
        }
2509
2510 375
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2511
            // Peek beyond the matching closing parenthesis.
2512 351
            $beyond = $this->lexer->peek();
2513
2514 351
            switch ($peek['value']) {
2515 351
                case '(':
2516
                    // Peeks beyond the matched closing parenthesis.
2517 33
                    $token = $this->peekBeyondClosingParenthesis(false);
2518
2519 33
                    if ($token['type'] === Lexer::T_NOT) {
2520 3
                        $token = $this->lexer->peek();
2521
                    }
2522
2523 33
                    if ($token['type'] === Lexer::T_IS) {
2524 2
                        $lookahead = $this->lexer->peek();
2525
                    }
2526 33
                    break;
2527
2528
                default:
2529
                    // Peek beyond the PathExpression or InputParameter.
2530 324
                    $token = $beyond;
2531
2532 324
                    while ($token['value'] === '.') {
2533 285
                        $this->lexer->peek();
2534
2535 285
                        $token = $this->lexer->peek();
2536
                    }
2537
2538
                    // Also peek beyond a NOT if there is one.
2539 324
                    if ($token['type'] === Lexer::T_NOT) {
2540 11
                        $token = $this->lexer->peek();
2541
                    }
2542
2543
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2544 324
                    $lookahead = $this->lexer->peek();
2545
            }
2546
2547
            // Also peek beyond a NOT if there is one.
2548 351
            if ($lookahead['type'] === Lexer::T_NOT) {
2549 7
                $lookahead = $this->lexer->peek();
2550
            }
2551
2552 351
            $this->lexer->resetPeek();
2553
        }
2554
2555 375
        if ($token['type'] === Lexer::T_BETWEEN) {
2556 8
            return $this->BetweenExpression();
2557
        }
2558
2559 369
        if ($token['type'] === Lexer::T_LIKE) {
2560 14
            return $this->LikeExpression();
2561
        }
2562
2563 356
        if ($token['type'] === Lexer::T_IN) {
2564 34
            return $this->InExpression();
2565
        }
2566
2567 331
        if ($token['type'] === Lexer::T_INSTANCE) {
2568 12
            return $this->InstanceOfExpression();
2569
        }
2570
2571 319
        if ($token['type'] === Lexer::T_MEMBER) {
2572 7
            return $this->CollectionMemberExpression();
2573
        }
2574
2575 312
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2576 14
            return $this->NullComparisonExpression();
2577
        }
2578
2579 301
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2580 4
            return $this->EmptyCollectionComparisonExpression();
2581
        }
2582
2583 297
        return $this->ComparisonExpression();
2584
    }
2585
2586
    /**
2587
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2588
     *
2589
     * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression
2590
     */
2591 4 View Code Duplication
    public function EmptyCollectionComparisonExpression()
2592
    {
2593 4
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2594 4
            $this->CollectionValuedPathExpression()
2595
        );
2596 4
        $this->match(Lexer::T_IS);
2597
2598 4
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2599 2
            $this->match(Lexer::T_NOT);
2600 2
            $emptyCollectionCompExpr->not = true;
2601
        }
2602
2603 4
        $this->match(Lexer::T_EMPTY);
2604
2605 4
        return $emptyCollectionCompExpr;
2606
    }
2607
2608
    /**
2609
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2610
     *
2611
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2612
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2613
     *
2614
     * @return \Doctrine\ORM\Query\AST\CollectionMemberExpression
2615
     */
2616 7 View Code Duplication
    public function CollectionMemberExpression()
2617
    {
2618 7
        $not        = false;
2619 7
        $entityExpr = $this->EntityExpression();
2620
2621 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2622
            $this->match(Lexer::T_NOT);
2623
2624
            $not = true;
2625
        }
2626
2627 7
        $this->match(Lexer::T_MEMBER);
2628
2629 7
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2630 7
            $this->match(Lexer::T_OF);
2631
        }
2632
2633 7
        $collMemberExpr = new AST\CollectionMemberExpression(
2634 7
            $entityExpr, $this->CollectionValuedPathExpression()
2635
        );
2636 7
        $collMemberExpr->not = $not;
2637
2638 7
        return $collMemberExpr;
2639
    }
2640
2641
    /**
2642
     * Literal ::= string | char | integer | float | boolean
2643
     *
2644
     * @return \Doctrine\ORM\Query\AST\Literal
2645
     */
2646 177
    public function Literal()
2647
    {
2648 177
        switch ($this->lexer->lookahead['type']) {
2649 177 View Code Duplication
            case Lexer::T_STRING:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2650 48
                $this->match(Lexer::T_STRING);
2651
2652 48
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2653 137
            case Lexer::T_INTEGER:
2654 9 View Code Duplication
            case Lexer::T_FLOAT:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2655 129
                $this->match(
2656 129
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2657
                );
2658
2659 129
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2660 8
            case Lexer::T_TRUE:
2661 4 View Code Duplication
            case Lexer::T_FALSE:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2662 8
                $this->match(
2663 8
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2664
                );
2665
2666 8
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2667
            default:
2668
                $this->syntaxError('Literal');
2669
        }
2670
    }
2671
2672
    /**
2673
     * InParameter ::= Literal | InputParameter
2674
     *
2675
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2676
     */
2677 25
    public function InParameter()
2678
    {
2679 25
        if ($this->lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) {
2680 13
            return $this->InputParameter();
2681
        }
2682
2683 12
        return $this->Literal();
2684
    }
2685
2686
    /**
2687
     * InputParameter ::= PositionalParameter | NamedParameter
2688
     *
2689
     * @return \Doctrine\ORM\Query\AST\InputParameter
2690
     */
2691 165
    public function InputParameter()
2692
    {
2693 165
        $this->match(Lexer::T_INPUT_PARAMETER);
2694
2695 165
        return new AST\InputParameter($this->lexer->token['value']);
2696
    }
2697
2698
    /**
2699
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2700
     *
2701
     * @return \Doctrine\ORM\Query\AST\ArithmeticExpression
2702
     */
2703 330
    public function ArithmeticExpression()
2704
    {
2705 330
        $expr = new AST\ArithmeticExpression;
2706
2707 330
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2708 19
            $peek = $this->lexer->glimpse();
2709
2710 19
            if ($peek['type'] === Lexer::T_SELECT) {
2711 7
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2712 7
                $expr->subselect = $this->Subselect();
2713 7
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2714
2715 7
                return $expr;
2716
            }
2717
        }
2718
2719 330
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2720
2721 330
        return $expr;
2722
    }
2723
2724
    /**
2725
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2726
     *
2727
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2728
     */
2729 434 View Code Duplication
    public function SimpleArithmeticExpression()
2730
    {
2731 434
        $terms = [];
2732 434
        $terms[] = $this->ArithmeticTerm();
2733
2734 434
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2735 21
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2736
2737 21
            $terms[] = $this->lexer->token['value'];
2738 21
            $terms[] = $this->ArithmeticTerm();
2739
        }
2740
2741
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2742
        // if only one AST\ArithmeticTerm is defined
2743 434
        if (count($terms) == 1) {
2744 429
            return $terms[0];
0 ignored issues
show
Bug Compatibility introduced by
The expression $terms[0]; of type Doctrine\ORM\Query\AST\ArithmeticTerm|null adds the type Doctrine\ORM\Query\AST\ArithmeticTerm to the return on line 2744 which is incompatible with the return type documented by Doctrine\ORM\Query\Parse...pleArithmeticExpression of type Doctrine\ORM\Query\AST\S...ithmeticExpression|null.
Loading history...
2745
        }
2746
2747 21
        return new AST\SimpleArithmeticExpression($terms);
2748
    }
2749
2750
    /**
2751
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2752
     *
2753
     * @return \Doctrine\ORM\Query\AST\ArithmeticTerm
2754
     */
2755 434 View Code Duplication
    public function ArithmeticTerm()
2756
    {
2757 434
        $factors = [];
2758 434
        $factors[] = $this->ArithmeticFactor();
2759
2760 434
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2761 53
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2762
2763 53
            $factors[] = $this->lexer->token['value'];
2764 53
            $factors[] = $this->ArithmeticFactor();
2765
        }
2766
2767
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2768
        // if only one AST\ArithmeticFactor is defined
2769 434
        if (count($factors) == 1) {
2770 406
            return $factors[0];
0 ignored issues
show
Bug Compatibility introduced by
The expression $factors[0]; of type Doctrine\ORM\Query\AST\ArithmeticFactor|null adds the type Doctrine\ORM\Query\AST\ArithmeticFactor to the return on line 2770 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticTerm of type Doctrine\ORM\Query\AST\ArithmeticTerm|null.
Loading history...
2771
        }
2772
2773 53
        return new AST\ArithmeticTerm($factors);
2774
    }
2775
2776
    /**
2777
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2778
     *
2779
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2780
     */
2781 434
    public function ArithmeticFactor()
2782
    {
2783 434
        $sign = null;
2784
2785 434
        if (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2786 3
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2787 3
            $sign = $isPlus;
2788
        }
2789
2790 434
        $primary = $this->ArithmeticPrimary();
2791
2792
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2793
        // if only one AST\ArithmeticPrimary is defined
2794 434
        if ($sign === null) {
2795 433
            return $primary;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $primary; (Doctrine\ORM\Query\AST\P...e\ORM\Query\AST\Literal) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticFactor of type Doctrine\ORM\Query\AST\ArithmeticFactor|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2796
        }
2797
2798 3
        return new AST\ArithmeticFactor($primary, $sign);
2799
    }
2800
2801
    /**
2802
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2803
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2804
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2805
     *          | InputParameter | CaseExpression
2806
     */
2807 438
    public function ArithmeticPrimary()
2808
    {
2809 438 View Code Duplication
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2810 25
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2811
2812 25
            $expr = $this->SimpleArithmeticExpression();
2813
2814 25
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2815
2816 25
            return new AST\ParenthesisExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr defined by $this->SimpleArithmeticExpression() on line 2812 can be null; however, Doctrine\ORM\Query\AST\P...pression::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2817
        }
2818
2819 438
        switch ($this->lexer->lookahead['type']) {
2820 438
            case Lexer::T_COALESCE:
2821 438
            case Lexer::T_NULLIF:
2822 438
            case Lexer::T_CASE:
2823 4
                return $this->CaseExpression();
2824
2825 438
            case Lexer::T_IDENTIFIER:
2826 408
                $peek = $this->lexer->glimpse();
2827
2828 408
                if ($peek['value'] == '(') {
2829 28
                    return $this->FunctionDeclaration();
2830
                }
2831
2832 388
                if ($peek['value'] == '.') {
2833 377
                    return $this->SingleValuedPathExpression();
2834
                }
2835
2836 46
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2837 10
                    return $this->ResultVariable();
2838
                }
2839
2840 38
                return $this->StateFieldPathExpression();
2841
2842 310
            case Lexer::T_INPUT_PARAMETER:
2843 147
                return $this->InputParameter();
2844
2845
            default:
2846 171
                $peek = $this->lexer->glimpse();
2847
2848 171
                if ($peek['value'] == '(') {
2849 18
                    return $this->FunctionDeclaration();
2850
                }
2851
2852 167
                return $this->Literal();
2853
        }
2854
    }
2855
2856
    /**
2857
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2858
     *
2859
     * @return \Doctrine\ORM\Query\AST\Subselect |
2860
     *         string
2861
     */
2862 14
    public function StringExpression()
2863
    {
2864 14
        $peek = $this->lexer->glimpse();
2865
2866
        // Subselect
2867 14 View Code Duplication
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2868
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2869
            $expr = $this->Subselect();
2870
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2871
2872
            return $expr;
2873
        }
2874
2875
        // ResultVariable (string)
2876 14
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2877 14
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2878 2
            return $this->ResultVariable();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->ResultVariable(); (string) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::StringExpression of type Doctrine\ORM\Query\AST\Subselect|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2879
        }
2880
2881 12
        return $this->StringPrimary();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->StringPrimary(); (Doctrine\ORM\Query\AST\P...mpleCaseExpression|null) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::StringExpression of type Doctrine\ORM\Query\AST\Subselect|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
2882
    }
2883
2884
    /**
2885
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2886
     */
2887 51
    public function StringPrimary()
2888
    {
2889 51
        $lookaheadType = $this->lexer->lookahead['type'];
2890
2891
        switch ($lookaheadType) {
2892 51
            case Lexer::T_IDENTIFIER:
2893 32
                $peek = $this->lexer->glimpse();
2894
2895 32
                if ($peek['value'] == '.') {
2896 32
                    return $this->StateFieldPathExpression();
2897
                }
2898
2899 8
                if ($peek['value'] == '(') {
2900
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2901 8
                    return $this->FunctionDeclaration();
2902
                }
2903
2904
                $this->syntaxError("'.' or '('");
2905
                break;
2906
2907 32 View Code Duplication
            case Lexer::T_STRING:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2908 32
                $this->match(Lexer::T_STRING);
2909
2910 32
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2911
2912 2
            case Lexer::T_INPUT_PARAMETER:
2913 2
                return $this->InputParameter();
2914
2915
            case Lexer::T_CASE:
2916
            case Lexer::T_COALESCE:
2917
            case Lexer::T_NULLIF:
2918
                return $this->CaseExpression();
2919
        }
2920
2921
        $this->syntaxError(
2922
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2923
        );
2924
    }
2925
2926
    /**
2927
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2928
     *
2929
     * @return \Doctrine\ORM\Query\AST\PathExpression |
2930
     *         \Doctrine\ORM\Query\AST\SimpleEntityExpression
2931
     */
2932 7
    public function EntityExpression()
2933
    {
2934 7
        $glimpse = $this->lexer->glimpse();
2935
2936 7
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2937 1
            return $this->SingleValuedAssociationPathExpression();
2938
        }
2939
2940 6
        return $this->SimpleEntityExpression();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->SimpleEntityExpression(); of type Doctrine\ORM\Query\AST\I...uery\AST\PathExpression adds the type Doctrine\ORM\Query\AST\InputParameter to the return on line 2940 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::EntityExpression of type Doctrine\ORM\Query\AST\PathExpression.
Loading history...
2941
    }
2942
2943
    /**
2944
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2945
     *
2946
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2947
     */
2948 6
    public function SimpleEntityExpression()
2949
    {
2950 6
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2951 5
            return $this->InputParameter();
2952
        }
2953
2954 1
        return $this->StateFieldPathExpression();
2955
    }
2956
2957
    /**
2958
     * AggregateExpression ::=
2959
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2960
     *
2961
     * @return \Doctrine\ORM\Query\AST\AggregateExpression
2962
     */
2963 85
    public function AggregateExpression()
2964
    {
2965 85
        $lookaheadType = $this->lexer->lookahead['type'];
2966 85
        $isDistinct = false;
2967
2968 85
        if ( ! in_array($lookaheadType, [Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM])) {
2969
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2970
        }
2971
2972 85
        $this->match($lookaheadType);
2973 85
        $functionName = $this->lexer->token['value'];
2974 85
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2975
2976 85
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
2977 3
            $this->match(Lexer::T_DISTINCT);
2978 3
            $isDistinct = true;
2979
        }
2980
2981 85
        $pathExp = $this->SimpleArithmeticExpression();
2982
2983 85
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2984
2985 85
        return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
0 ignored issues
show
Bug introduced by
It seems like $pathExp defined by $this->SimpleArithmeticExpression() on line 2981 can be null; however, Doctrine\ORM\Query\AST\A...pression::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2986
    }
2987
2988
    /**
2989
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
2990
     *
2991
     * @return \Doctrine\ORM\Query\AST\QuantifiedExpression
2992
     */
2993 3
    public function QuantifiedExpression()
2994
    {
2995 3
        $lookaheadType = $this->lexer->lookahead['type'];
2996 3
        $value = $this->lexer->lookahead['value'];
2997
2998 3
        if ( ! in_array($lookaheadType, [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME])) {
2999
            $this->syntaxError('ALL, ANY or SOME');
3000
        }
3001
3002 3
        $this->match($lookaheadType);
3003 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3004
3005 3
        $qExpr = new AST\QuantifiedExpression($this->Subselect());
3006 3
        $qExpr->type = $value;
3007
3008 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3009
3010 3
        return $qExpr;
3011
    }
3012
3013
    /**
3014
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3015
     *
3016
     * @return \Doctrine\ORM\Query\AST\BetweenExpression
3017
     */
3018 8
    public function BetweenExpression()
3019
    {
3020 8
        $not = false;
3021 8
        $arithExpr1 = $this->ArithmeticExpression();
3022
3023 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3024 3
            $this->match(Lexer::T_NOT);
3025 3
            $not = true;
3026
        }
3027
3028 8
        $this->match(Lexer::T_BETWEEN);
3029 8
        $arithExpr2 = $this->ArithmeticExpression();
3030 8
        $this->match(Lexer::T_AND);
3031 8
        $arithExpr3 = $this->ArithmeticExpression();
3032
3033 8
        $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3034 8
        $betweenExpr->not = $not;
3035
3036 8
        return $betweenExpr;
3037
    }
3038
3039
    /**
3040
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3041
     *
3042
     * @return \Doctrine\ORM\Query\AST\ComparisonExpression
3043
     */
3044 297
    public function ComparisonExpression()
3045
    {
3046 297
        $this->lexer->glimpse();
3047
3048 297
        $leftExpr  = $this->ArithmeticExpression();
3049 297
        $operator  = $this->ComparisonOperator();
3050 297
        $rightExpr = ($this->isNextAllAnySome())
3051 3
            ? $this->QuantifiedExpression()
3052 297
            : $this->ArithmeticExpression();
3053
3054 295
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3055
    }
3056
3057
    /**
3058
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3059
     *
3060
     * @return \Doctrine\ORM\Query\AST\InExpression
3061
     */
3062 34
    public function InExpression()
3063
    {
3064 34
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3065
3066 34
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3067 6
            $this->match(Lexer::T_NOT);
3068 6
            $inExpression->not = true;
3069
        }
3070
3071 34
        $this->match(Lexer::T_IN);
3072 34
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3073
3074 34
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3075 9
            $inExpression->subselect = $this->Subselect();
3076
        } else {
3077 25
            $literals = [];
3078 25
            $literals[] = $this->InParameter();
3079
3080 25
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3081 16
                $this->match(Lexer::T_COMMA);
3082 16
                $literals[] = $this->InParameter();
3083
            }
3084
3085 25
            $inExpression->literals = $literals;
3086
        }
3087
3088 33
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3089
3090 33
        return $inExpression;
3091
    }
3092
3093
    /**
3094
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3095
     *
3096
     * @return \Doctrine\ORM\Query\AST\InstanceOfExpression
3097
     */
3098 12
    public function InstanceOfExpression()
3099
    {
3100 12
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3101
3102 12
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3103 1
            $this->match(Lexer::T_NOT);
3104 1
            $instanceOfExpression->not = true;
3105
        }
3106
3107 12
        $this->match(Lexer::T_INSTANCE);
3108 12
        $this->match(Lexer::T_OF);
3109
3110 12
        $exprValues = [];
3111
3112 12
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3113 1
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3114
3115 1
            $exprValues[] = $this->InstanceOfParameter();
3116
3117 1
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3118 1
                $this->match(Lexer::T_COMMA);
3119
3120 1
                $exprValues[] = $this->InstanceOfParameter();
3121
            }
3122
3123 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3124
3125 1
            $instanceOfExpression->value = $exprValues;
3126
3127 1
            return $instanceOfExpression;
3128
        }
3129
3130 11
        $exprValues[] = $this->InstanceOfParameter();
3131
3132 11
        $instanceOfExpression->value = $exprValues;
3133
3134 11
        return $instanceOfExpression;
3135
    }
3136
3137
    /**
3138
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3139
     *
3140
     * @return mixed
3141
     */
3142 12
    public function InstanceOfParameter()
3143
    {
3144 12 View Code Duplication
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3145 5
            $this->match(Lexer::T_INPUT_PARAMETER);
3146
3147 5
            return new AST\InputParameter($this->lexer->token['value']);
3148
        }
3149
3150 7
        $abstractSchemaName = $this->AbstractSchemaName();
3151
3152 7
        $this->validateAbstractSchemaName($abstractSchemaName);
3153
3154 7
        return $abstractSchemaName;
3155
    }
3156
3157
    /**
3158
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3159
     *
3160
     * @return \Doctrine\ORM\Query\AST\LikeExpression
3161
     */
3162 14
    public function LikeExpression()
3163
    {
3164 14
        $stringExpr = $this->StringExpression();
3165 14
        $not = false;
3166
3167 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3168 3
            $this->match(Lexer::T_NOT);
3169 3
            $not = true;
3170
        }
3171
3172 14
        $this->match(Lexer::T_LIKE);
3173
3174 14
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3175 4
            $this->match(Lexer::T_INPUT_PARAMETER);
3176 4
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3177
        } else {
3178 11
            $stringPattern = $this->StringPrimary();
3179
        }
3180
3181 14
        $escapeChar = null;
3182
3183 14
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3184 2
            $this->match(Lexer::T_ESCAPE);
3185 2
            $this->match(Lexer::T_STRING);
3186
3187 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3188
        }
3189
3190 14
        $likeExpr = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $stringExpr defined by $this->StringExpression() on line 3164 can be null; however, Doctrine\ORM\Query\AST\L...pression::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
Bug introduced by
It seems like $stringPattern defined by $this->StringPrimary() on line 3178 can also be of type null or object<Doctrine\ORM\Query\AST\CoalesceExpression> or object<Doctrine\ORM\Quer...Functions\FunctionNode> or object<Doctrine\ORM\Quer...\GeneralCaseExpression> or object<Doctrine\ORM\Query\AST\Literal> or object<Doctrine\ORM\Query\AST\NullIfExpression> or object<Doctrine\ORM\Query\AST\PathExpression> or object<Doctrine\ORM\Quer...T\SimpleCaseExpression>; however, Doctrine\ORM\Query\AST\L...pression::__construct() does only seem to accept object<Doctrine\ORM\Query\AST\InputParameter>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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