Completed
Push — master ( e4704b...bd1efa )
by Luís
11:15
created

Parser   F

Complexity

Total Complexity 442

Size/Duplication

Total Lines 3510
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 62

Test Coverage

Coverage 92.53%

Importance

Changes 0
Metric Value
wmc 442
lcom 1
cbo 62
dl 0
loc 3510
ccs 1264
cts 1366
cp 0.9253
rs 0.5217
c 0
b 0
f 0

114 Methods

Rating   Name   Duplication   Size   Complexity  
B StringExpression() 0 21 5
D StringPrimary() 0 43 10
A EntityExpression() 0 10 3
A SimpleEntityExpression() 0 8 2
B AggregateExpression() 0 24 3
A QuantifiedExpression() 0 19 2
A BetweenExpression() 0 20 2
A ComparisonExpression() 0 12 2
B InExpression() 0 30 4
B InstanceOfExpression() 0 38 4
A InstanceOfParameter() 0 14 2
B LikeExpression() 0 33 4
A isInternalFunction() 0 8 3
A __construct() 0 7 1
A setCustomOutputTreeWalker() 0 4 1
A addCustomTreeWalker() 0 4 1
A getLexer() 0 4 1
A getParserResult() 0 4 1
A getEntityManager() 0 4 1
B getAST() 0 32 5
B match() 0 24 6
A free() 0 13 2
D parse() 0 45 9
A fixIdentificationVariableOrder() 0 19 4
B syntaxError() 0 14 5
B semanticalError() 0 24 6
B peekBeyondClosingParenthesis() 0 28 6
A isMathOperator() 0 4 1
A isFunction() 0 9 2
A isAggregateFunction() 0 4 1
A isNextAllAnySome() 0 4 1
B processDeferredIdentificationVariables() 0 29 5
C processDeferredNewObjectExpressions() 0 41 11
C processDeferredPartialObjectExpressions() 0 30 8
B processDeferredResultVariables() 0 29 5
C processDeferredPathExpressions() 0 65 12
B processRootEntityAliasSelected() 0 14 5
B QueryLanguage() 0 29 5
B SelectStatement() 0 11 5
A UpdateStatement() 0 8 2
A DeleteStatement() 0 8 2
A IdentificationVariable() 0 14 1
A AliasIdentificationVariable() 0 13 2
A AbstractSchemaName() 0 20 3
A validateAbstractSchemaName() 0 6 3
A AliasResultVariable() 0 13 2
A ResultVariable() 0 15 1
B JoinAssociationPathExpression() 0 25 3
B PathExpression() 0 30 3
A AssociationPathExpression() 0 7 1
A SingleValuedPathExpression() 0 7 1
A StateFieldPathExpression() 0 4 1
A SingleValuedAssociationPathExpression() 0 4 1
A CollectionValuedPathExpression() 0 4 1
B SelectClause() 0 24 3
A SimpleSelectClause() 0 13 2
B UpdateClause() 0 45 3
B DeleteClause() 0 38 3
A FromClause() 0 15 2
A SubselectFromClause() 0 15 2
A WhereClause() 0 6 1
A HavingClause() 0 6 1
A GroupByClause() 0 15 2
A OrderByClause() 0 16 2
B Subselect() 0 17 5
A UpdateItem() 0 10 1
A GroupByItem() 0 20 4
B OrderByItem() 0 54 8
A NewValue() 0 16 3
B IdentificationVariableDeclaration() 0 22 5
B SubselectIdentificationVariableDeclaration() 0 41 1
C Join() 0 48 7
B RangeVariableDeclaration() 0 28 2
B JoinAssociationDeclaration() 0 31 3
B PartialObjectExpression() 0 51 4
B NewObjectExpression() 0 30 2
A NewObjectArg() 0 15 3
A IndexBy() 0 11 1
C ScalarExpression() 0 76 19
B CaseExpression() 0 28 5
A CoalesceExpression() 0 19 2
A NullIfExpression() 0 13 1
A GeneralCaseExpression() 0 17 2
A SimpleCaseExpression() 0 18 2
A WhenClause() 0 8 1
A SimpleWhenClause() 0 8 1
F SelectExpression() 0 125 26
C SimpleSelectExpression() 0 77 11
A ConditionalExpression() 0 19 3
A ConditionalTerm() 0 19 3
A ConditionalFactor() 0 23 3
B ConditionalPrimary() 0 27 5
D SimpleConditionalExpression() 0 89 21
A EmptyCollectionComparisonExpression() 0 16 2
B CollectionMemberExpression() 0 24 3
C Literal() 0 25 8
A InParameter() 0 8 2
A InputParameter() 0 6 1
A ArithmeticExpression() 0 20 3
B SimpleArithmeticExpression() 0 20 5
B ArithmeticTerm() 0 20 5
B ArithmeticFactor() 0 19 5
C ArithmeticPrimary() 0 52 12
C NullComparisonExpression() 0 72 11
A ExistsExpression() 0 19 2
C ComparisonOperator() 0 43 8
A FunctionDeclaration() 0 20 4
B CustomFunctionDeclaration() 0 22 4
A FunctionsReturningNumerics() 0 10 1
A CustomFunctionsReturningNumerics() 0 14 2
A FunctionsReturningDatetime() 0 10 1
A CustomFunctionsReturningDatetime() 0 14 2
A FunctionsReturningStrings() 0 10 1
A CustomFunctionsReturningStrings() 0 14 2

How to fix   Complexity   

Complex Class

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

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
70
    /**
71
     * READ-ONLY: Maps BUILT-IN datetime function names to AST class names.
72
     *
73
     * @var array
74
     */
75
    private static $_DATETIME_FUNCTIONS = [
76
        'current_date'      => Functions\CurrentDateFunction::class,
77
        'current_time'      => Functions\CurrentTimeFunction::class,
78
        'current_timestamp' => Functions\CurrentTimestampFunction::class,
79
        'date_add'          => Functions\DateAddFunction::class,
80
        'date_sub'          => Functions\DateSubFunction::class,
81
    ];
82
83
    /*
84
     * Expressions that were encountered during parsing of identifiers and expressions
85
     * and still need to be validated.
86
     */
87
88
    /**
89
     * @var array
90
     */
91
    private $deferredIdentificationVariables = [];
92
93
    /**
94
     * @var array
95
     */
96
    private $deferredPartialObjectExpressions = [];
97
98
    /**
99
     * @var array
100
     */
101
    private $deferredPathExpressions = [];
102
103
    /**
104
     * @var array
105
     */
106
    private $deferredResultVariables = [];
107
108
    /**
109
     * @var array
110
     */
111
    private $deferredNewObjectExpressions = [];
112
113
    /**
114
     * The lexer.
115
     *
116
     * @var \Doctrine\ORM\Query\Lexer
117
     */
118
    private $lexer;
119
120
    /**
121
     * The parser result.
122
     *
123
     * @var \Doctrine\ORM\Query\ParserResult
124
     */
125
    private $parserResult;
126
127
    /**
128
     * The EntityManager.
129
     *
130
     * @var \Doctrine\ORM\EntityManager
131
     */
132
    private $em;
133
134
    /**
135
     * The Query to parse.
136
     *
137
     * @var Query
138
     */
139
    private $query;
140
141
    /**
142
     * Map of declared query components in the parsed query.
143
     *
144
     * @var array
145
     */
146
    private $queryComponents = [];
147
148
    /**
149
     * Keeps the nesting level of defined ResultVariables.
150
     *
151
     * @var integer
152
     */
153
    private $nestingLevel = 0;
154
155
    /**
156
     * Any additional custom tree walkers that modify the AST.
157
     *
158
     * @var array
159
     */
160
    private $customTreeWalkers = [];
161
162
    /**
163
     * The custom last tree walker, if any, that is responsible for producing the output.
164
     *
165
     * @var TreeWalker
166
     */
167
    private $customOutputWalker;
168
169
    /**
170
     * @var array
171
     */
172
    private $identVariableExpressions = [];
173
174
    /**
175
     * Checks if a function is internally defined. Used to prevent overwriting
176
     * of built-in functions through user-defined functions.
177
     *
178
     * @param string $functionName
179
     *
180
     * @return bool
181
     */
182 6
    static public function isInternalFunction($functionName)
183
    {
184 6
        $functionName = strtolower($functionName);
185
186 6
        return isset(self::$_STRING_FUNCTIONS[$functionName])
187 6
            || isset(self::$_DATETIME_FUNCTIONS[$functionName])
188 6
            || isset(self::$_NUMERIC_FUNCTIONS[$functionName]);
189
    }
190
191
    /**
192
     * Creates a new query parser object.
193
     *
194
     * @param Query $query The Query to parse.
195
     */
196 822
    public function __construct(Query $query)
197
    {
198 822
        $this->query        = $query;
199 822
        $this->em           = $query->getEntityManager();
200 822
        $this->lexer        = new Lexer($query->getDql());
201 822
        $this->parserResult = new ParserResult();
202 822
    }
203
204
    /**
205
     * Sets a custom tree walker that produces output.
206
     * This tree walker will be run last over the AST, after any other walkers.
207
     *
208
     * @param string $className
209
     *
210
     * @return void
211
     */
212 127
    public function setCustomOutputTreeWalker($className)
213
    {
214 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...
215 127
    }
216
217
    /**
218
     * Adds a custom tree walker for modifying the AST.
219
     *
220
     * @param string $className
221
     *
222
     * @return void
223
     */
224
    public function addCustomTreeWalker($className)
225
    {
226
        $this->customTreeWalkers[] = $className;
227
    }
228
229
    /**
230
     * Gets the lexer used by the parser.
231
     *
232
     * @return \Doctrine\ORM\Query\Lexer
233
     */
234 28
    public function getLexer()
235
    {
236 28
        return $this->lexer;
237
    }
238
239
    /**
240
     * Gets the ParserResult that is being filled with information during parsing.
241
     *
242
     * @return \Doctrine\ORM\Query\ParserResult
243
     */
244
    public function getParserResult()
245
    {
246
        return $this->parserResult;
247
    }
248
249
    /**
250
     * Gets the EntityManager used by the parser.
251
     *
252
     * @return \Doctrine\ORM\EntityManager
253
     */
254
    public function getEntityManager()
255
    {
256
        return $this->em;
257
    }
258
259
    /**
260
     * Parses and builds AST for the given Query.
261
     *
262
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
263
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
264
     *         \Doctrine\ORM\Query\AST\DeleteStatement
265
     */
266 822
    public function getAST()
267
    {
268
        // Parse & build AST
269 822
        $AST = $this->QueryLanguage();
270
271
        // Process any deferred validations of some nodes in the AST.
272
        // This also allows post-processing of the AST for modification purposes.
273 783
        $this->processDeferredIdentificationVariables();
274
275 781
        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...
276 11
            $this->processDeferredPartialObjectExpressions();
277
        }
278
279 779
        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...
280 585
            $this->processDeferredPathExpressions($AST);
281
        }
282
283 776
        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...
284 30
            $this->processDeferredResultVariables();
285
        }
286
287 776
        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...
288 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...
289
        }
290
291 772
        $this->processRootEntityAliasSelected();
292
293
        // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!
294 771
        $this->fixIdentificationVariableOrder($AST);
295
296 771
        return $AST;
297
    }
298
299
    /**
300
     * Attempts to match the given token with the current lookahead token.
301
     *
302
     * If they match, updates the lookahead token; otherwise raises a syntax
303
     * error.
304
     *
305
     * @param int $token The token type.
306
     *
307
     * @return void
308
     *
309
     * @throws QueryException If the tokens don't match.
310
     */
311 833
    public function match($token)
312
    {
313 833
        $lookaheadType = $this->lexer->lookahead['type'];
314
315
        // Short-circuit on first condition, usually types match
316 833
        if ($lookaheadType !== $token) {
317
            // If parameter is not identifier (1-99) must be exact match
318 20
            if ($token < Lexer::T_IDENTIFIER) {
319 2
                $this->syntaxError($this->lexer->getLiteral($token));
320
            }
321
322
            // If parameter is keyword (200+) must be exact match
323 18
            if ($token > Lexer::T_IDENTIFIER) {
324 7
                $this->syntaxError($this->lexer->getLiteral($token));
325
            }
326
327
            // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
328 11
            if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
329 8
                $this->syntaxError($this->lexer->getLiteral($token));
330
            }
331
        }
332
333 826
        $this->lexer->moveNext();
334 826
    }
335
336
    /**
337
     * Frees this parser, enabling it to be reused.
338
     *
339
     * @param boolean $deep     Whether to clean peek and reset errors.
340
     * @param integer $position Position to reset.
341
     *
342
     * @return void
343
     */
344
    public function free($deep = false, $position = 0)
345
    {
346
        // WARNING! Use this method with care. It resets the scanner!
347
        $this->lexer->resetPosition($position);
348
349
        // Deep = true cleans peek and also any previously defined errors
350
        if ($deep) {
351
            $this->lexer->resetPeek();
352
        }
353
354
        $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...
355
        $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...
356
    }
357
358
    /**
359
     * Parses a query string.
360
     *
361
     * @return ParserResult
362
     */
363 822
    public function parse()
364
    {
365 822
        $AST = $this->getAST();
366
367 771
        if (($customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) !== false) {
368 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...
369
        }
370
371 771
        if (($customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER)) !== false) {
372 78
            $this->customOutputWalker = $customOutputWalker;
373
        }
374
375
        // Run any custom tree walkers over the AST
376 771
        if ($this->customTreeWalkers) {
377 92
            $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);
378
379 92
            foreach ($this->customTreeWalkers as $walker) {
380 92
                $treeWalkerChain->addTreeWalker($walker);
381
            }
382
383
            switch (true) {
384 92
                case ($AST instanceof AST\UpdateStatement):
385
                    $treeWalkerChain->walkUpdateStatement($AST);
386
                    break;
387
388
                case ($AST instanceof AST\DeleteStatement):
389
                    $treeWalkerChain->walkDeleteStatement($AST);
390
                    break;
391
392
                case ($AST instanceof AST\SelectStatement):
393
                default:
394 92
                    $treeWalkerChain->walkSelectStatement($AST);
395
            }
396
397 86
            $this->queryComponents = $treeWalkerChain->getQueryComponents();
398
        }
399
400 765
        $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;
401 765
        $outputWalker      = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);
402
403
        // Assign an SQL executor to the parser result
404 765
        $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));
405
406 756
        return $this->parserResult;
407
    }
408
409
    /**
410
     * Fixes order of identification variables.
411
     *
412
     * They have to appear in the select clause in the same order as the
413
     * declarations (from ... x join ... y join ... z ...) appear in the query
414
     * as the hydration process relies on that order for proper operation.
415
     *
416
     * @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST
417
     *
418
     * @return void
419
     */
420 771
    private function fixIdentificationVariableOrder($AST)
421
    {
422 771
        if (count($this->identVariableExpressions) <= 1) {
423 595
            return;
424
        }
425
426 181
        foreach ($this->queryComponents as $dqlAlias => $qComp) {
427 181
            if ( ! isset($this->identVariableExpressions[$dqlAlias])) {
428 8
                continue;
429
            }
430
431 181
            $expr = $this->identVariableExpressions[$dqlAlias];
432 181
            $key  = array_search($expr, $AST->selectClause->selectExpressions);
433
434 181
            unset($AST->selectClause->selectExpressions[$key]);
435
436 181
            $AST->selectClause->selectExpressions[] = $expr;
437
        }
438 181
    }
439
440
    /**
441
     * Generates a new syntax error.
442
     *
443
     * @param string     $expected Expected string.
444
     * @param array|null $token    Got token.
445
     *
446
     * @return void
447
     *
448
     * @throws \Doctrine\ORM\Query\QueryException
449
     */
450 17
    public function syntaxError($expected = '', $token = null)
451
    {
452 17
        if ($token === null) {
453 14
            $token = $this->lexer->lookahead;
454
        }
455
456 17
        $tokenPos = (isset($token['position'])) ? $token['position'] : '-1';
457
458 17
        $message  = "line 0, col {$tokenPos}: Error: ";
459 17
        $message .= ($expected !== '') ? "Expected {$expected}, got " : 'Unexpected ';
460 17
        $message .= ($this->lexer->lookahead === null) ? 'end of string.' : "'{$token['value']}'";
461
462 17
        throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL()));
463
    }
464
465
    /**
466
     * Generates a new semantical error.
467
     *
468
     * @param string     $message Optional message.
469
     * @param array|null $token   Optional token.
470
     *
471
     * @return void
472
     *
473
     * @throws \Doctrine\ORM\Query\QueryException
474
     */
475 33
    public function semanticalError($message = '', $token = null)
476
    {
477 33
        if ($token === null) {
478 2
            $token = $this->lexer->lookahead;
479
        }
480
481
        // Minimum exposed chars ahead of token
482 33
        $distance = 12;
483
484
        // Find a position of a final word to display in error string
485 33
        $dql    = $this->query->getDql();
486 33
        $length = strlen($dql);
487 33
        $pos    = $token['position'] + $distance;
488 33
        $pos    = strpos($dql, ' ', ($length > $pos) ? $pos : $length);
489 33
        $length = ($pos !== false) ? $pos - $token['position'] : $distance;
490
491 33
        $tokenPos = (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1';
492 33
        $tokenStr = substr($dql, $token['position'], $length);
493
494
        // Building informative message
495 33
        $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;
496
497 33
        throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL()));
498
    }
499
500
    /**
501
     * Peeks beyond the matched closing parenthesis and returns the first token after that one.
502
     *
503
     * @param boolean $resetPeek Reset peek after finding the closing parenthesis.
504
     *
505
     * @return array
506
     */
507 157
    private function peekBeyondClosingParenthesis($resetPeek = true)
508
    {
509 157
        $token = $this->lexer->peek();
510 157
        $numUnmatched = 1;
511
512 157
        while ($numUnmatched > 0 && $token !== null) {
513 156
            switch ($token['type']) {
514 156
                case Lexer::T_OPEN_PARENTHESIS:
515 33
                    ++$numUnmatched;
516 33
                    break;
517
518 156
                case Lexer::T_CLOSE_PARENTHESIS:
519 156
                    --$numUnmatched;
520 156
                    break;
521
522
                default:
523
                    // Do nothing
524
            }
525
526 156
            $token = $this->lexer->peek();
527
        }
528
529 157
        if ($resetPeek) {
530 135
            $this->lexer->resetPeek();
531
        }
532
533 157
        return $token;
534
    }
535
536
    /**
537
     * Checks if the given token indicates a mathematical operator.
538
     *
539
     * @param array $token
540
     *
541
     * @return boolean TRUE if the token is a mathematical operator, FALSE otherwise.
542
     */
543 343
    private function isMathOperator($token)
544
    {
545 343
        return in_array($token['type'], [Lexer::T_PLUS, Lexer::T_MINUS, Lexer::T_DIVIDE, Lexer::T_MULTIPLY]);
546
    }
547
548
    /**
549
     * Checks if the next-next (after lookahead) token starts a function.
550
     *
551
     * @return boolean TRUE if the next-next tokens start a function, FALSE otherwise.
552
     */
553 388
    private function isFunction()
554
    {
555 388
        $lookaheadType = $this->lexer->lookahead['type'];
556 388
        $peek          = $this->lexer->peek();
557
558 388
        $this->lexer->resetPeek();
559
560 388
        return ($lookaheadType >= Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_OPEN_PARENTHESIS);
561
    }
562
563
    /**
564
     * Checks whether the given token type indicates an aggregate function.
565
     *
566
     * @param int $tokenType
567
     *
568
     * @return boolean TRUE if the token type is an aggregate function, FALSE otherwise.
569
     */
570 126
    private function isAggregateFunction($tokenType)
571
    {
572 126
        return in_array($tokenType, [Lexer::T_AVG, Lexer::T_MIN, Lexer::T_MAX, Lexer::T_SUM, Lexer::T_COUNT]);
573
    }
574
575
    /**
576
     * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.
577
     *
578
     * @return boolean
579
     */
580 294
    private function isNextAllAnySome()
581
    {
582 294
        return in_array($this->lexer->lookahead['type'], [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME]);
583
    }
584
585
    /**
586
     * Validates that the given <tt>IdentificationVariable</tt> is semantically correct.
587
     * It must exist in query components list.
588
     *
589
     * @return void
590
     */
591 783
    private function processDeferredIdentificationVariables()
592
    {
593 783
        foreach ($this->deferredIdentificationVariables as $deferredItem) {
594 771
            $identVariable = $deferredItem['expression'];
595
596
            // Check if IdentificationVariable exists in queryComponents
597 771
            if ( ! isset($this->queryComponents[$identVariable])) {
598 1
                $this->semanticalError(
599 1
                    "'$identVariable' is not defined.", $deferredItem['token']
600
                );
601
            }
602
603 771
            $qComp = $this->queryComponents[$identVariable];
604
605
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
606 771
            if ( ! isset($qComp['metadata'])) {
607
                $this->semanticalError(
608
                    "'$identVariable' does not point to a Class.", $deferredItem['token']
609
                );
610
            }
611
612
            // Validate if identification variable nesting level is lower or equal than the current one
613 771
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
614 1
                $this->semanticalError(
615 771
                    "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token']
616
                );
617
            }
618
        }
619 781
    }
620
621
    /**
622
     * Validates that the given <tt>NewObjectExpression</tt>.
623
     *
624
     * @param \Doctrine\ORM\Query\AST\SelectClause $AST
625
     *
626
     * @return void
627
     */
628 28
    private function processDeferredNewObjectExpressions($AST)
629
    {
630 28
        foreach ($this->deferredNewObjectExpressions as $deferredItem) {
631 28
            $expression     = $deferredItem['expression'];
632 28
            $token          = $deferredItem['token'];
633 28
            $className      = $expression->className;
634 28
            $args           = $expression->args;
635 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...
636 28
                ? $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName
637 28
                : null;
638
639
            // If the namespace is not given then assumes the first FROM entity namespace
640 28
            if (strpos($className, '\\') === false && ! class_exists($className) && strpos($fromClassName, '\\') !== false) {
641 11
                $namespace  = substr($fromClassName, 0, strrpos($fromClassName, '\\'));
642 11
                $fqcn       = $namespace . '\\' . $className;
643
644 11
                if (class_exists($fqcn)) {
645 11
                    $expression->className  = $fqcn;
646 11
                    $className              = $fqcn;
647
                }
648
            }
649
650 28
            if ( ! class_exists($className)) {
651 1
                $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);
652
            }
653
654 27
            $class = new \ReflectionClass($className);
655
656 27
            if ( ! $class->isInstantiable()) {
657 1
                $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);
658
            }
659
660 26
            if ($class->getConstructor() === null) {
661 1
                $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);
662
            }
663
664 25
            if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {
665 25
                $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);
666
            }
667
        }
668 24
    }
669
670
    /**
671
     * Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.
672
     * It must exist in query components list.
673
     *
674
     * @return void
675
     */
676 11
    private function processDeferredPartialObjectExpressions()
677
    {
678 11
        foreach ($this->deferredPartialObjectExpressions as $deferredItem) {
679 11
            $expr = $deferredItem['expression'];
680 11
            $class = $this->queryComponents[$expr->identificationVariable]['metadata'];
681
682 11
            foreach ($expr->partialFieldSet as $field) {
683 11
                if (isset($class->fieldMappings[$field])) {
684 10
                    continue;
685
                }
686
687 3
                if (isset($class->associationMappings[$field]) &&
688 3
                    $class->associationMappings[$field]['isOwningSide'] &&
689 3
                    $class->associationMappings[$field]['type'] & ClassMetadata::TO_ONE) {
690 2
                    continue;
691
                }
692
693 1
                $this->semanticalError(
694 1
                    "There is no mapped field named '$field' on class " . $class->name . ".", $deferredItem['token']
695
                );
696
            }
697
698 10
            if (array_intersect($class->identifier, $expr->partialFieldSet) != $class->identifier) {
699 1
                $this->semanticalError(
700 1
                    "The partial field selection of class " . $class->name . " must contain the identifier.",
701 10
                    $deferredItem['token']
702
                );
703
            }
704
        }
705 9
    }
706
707
    /**
708
     * Validates that the given <tt>ResultVariable</tt> is semantically correct.
709
     * It must exist in query components list.
710
     *
711
     * @return void
712
     */
713 30
    private function processDeferredResultVariables()
714
    {
715 30
        foreach ($this->deferredResultVariables as $deferredItem) {
716 30
            $resultVariable = $deferredItem['expression'];
717
718
            // Check if ResultVariable exists in queryComponents
719 30
            if ( ! isset($this->queryComponents[$resultVariable])) {
720
                $this->semanticalError(
721
                    "'$resultVariable' is not defined.", $deferredItem['token']
722
                );
723
            }
724
725 30
            $qComp = $this->queryComponents[$resultVariable];
726
727
            // Check if queryComponent points to an AbstractSchemaName or a ResultVariable
728 30
            if ( ! isset($qComp['resultVariable'])) {
729
                $this->semanticalError(
730
                    "'$resultVariable' does not point to a ResultVariable.", $deferredItem['token']
731
                );
732
            }
733
734
            // Validate if identification variable nesting level is lower or equal than the current one
735 30
            if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {
736
                $this->semanticalError(
737 30
                    "'$resultVariable' is used outside the scope of its declaration.", $deferredItem['token']
738
                );
739
            }
740
        }
741 30
    }
742
743
    /**
744
     * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:
745
     *
746
     * AssociationPathExpression             ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
747
     * SingleValuedPathExpression            ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
748
     * StateFieldPathExpression              ::= IdentificationVariable "." StateField
749
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
750
     * CollectionValuedPathExpression        ::= IdentificationVariable "." CollectionValuedAssociationField
751
     *
752
     * @param mixed $AST
753
     *
754
     * @return void
755
     */
756 585
    private function processDeferredPathExpressions($AST)
0 ignored issues
show
Unused Code introduced by
The parameter $AST is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
757
    {
758 585
        foreach ($this->deferredPathExpressions as $deferredItem) {
759 585
            $pathExpression = $deferredItem['expression'];
760
761 585
            $qComp = $this->queryComponents[$pathExpression->identificationVariable];
762 585
            $class = $qComp['metadata'];
763
764 585
            if (($field = $pathExpression->field) === null) {
765 39
                $field = $pathExpression->field = $class->identifier[0];
766
            }
767
768
            // Check if field or association exists
769 585
            if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) {
770 1
                $this->semanticalError(
771 1
                    'Class ' . $class->name . ' has no field or association named ' . $field,
772 1
                    $deferredItem['token']
773
                );
774
            }
775
776 584
            $fieldType = AST\PathExpression::TYPE_STATE_FIELD;
777
778 584
            if (isset($class->associationMappings[$field])) {
779 88
                $assoc = $class->associationMappings[$field];
780
781 88
                $fieldType = ($assoc['type'] & ClassMetadata::TO_ONE)
782 66
                    ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
783 88
                    : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;
784
            }
785
786
            // Validate if PathExpression is one of the expected types
787 584
            $expectedType = $pathExpression->expectedType;
788
789 584
            if ( ! ($expectedType & $fieldType)) {
790
                // We need to recognize which was expected type(s)
791 2
                $expectedStringTypes = [];
792
793
                // Validate state field type
794 2
                if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {
795 1
                    $expectedStringTypes[] = 'StateFieldPathExpression';
796
                }
797
798
                // Validate single valued association (*-to-one)
799 2
                if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
800 2
                    $expectedStringTypes[] = 'SingleValuedAssociationField';
801
                }
802
803
                // Validate single valued association (*-to-many)
804 2
                if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {
805
                    $expectedStringTypes[] = 'CollectionValuedAssociationField';
806
                }
807
808
                // Build the error message
809 2
                $semanticalError  = 'Invalid PathExpression. ';
810 2
                $semanticalError .= (count($expectedStringTypes) == 1)
811 1
                    ? 'Must be a ' . $expectedStringTypes[0] . '.'
812 2
                    : implode(' or ', $expectedStringTypes) . ' expected.';
813
814 2
                $this->semanticalError($semanticalError, $deferredItem['token']);
815
            }
816
817
            // We need to force the type in PathExpression
818 582
            $pathExpression->type = $fieldType;
819
        }
820 582
    }
821
822
    /**
823
     * @return void
824
     */
825 772
    private function processRootEntityAliasSelected()
826
    {
827 772
        if ( ! count($this->identVariableExpressions)) {
828 223
            return;
829
        }
830
831 559
        foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
832 559
            if (isset($this->queryComponents[$dqlAlias]) && $this->queryComponents[$dqlAlias]['parent'] === null) {
833 559
                return;
834
            }
835
        }
836
837 1
        $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');
838
    }
839
840
    /**
841
     * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement
842
     *
843
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
844
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
845
     *         \Doctrine\ORM\Query\AST\DeleteStatement
846
     */
847 822
    public function QueryLanguage()
848
    {
849 822
        $this->lexer->moveNext();
850
851 822
        switch ($this->lexer->lookahead['type']) {
852 822
            case Lexer::T_SELECT:
853 756
                $statement = $this->SelectStatement();
854 721
                break;
855
856 72
            case Lexer::T_UPDATE:
857 32
                $statement = $this->UpdateStatement();
858 32
                break;
859
860 43
            case Lexer::T_DELETE:
861 42
                $statement = $this->DeleteStatement();
862 41
                break;
863
864
            default:
865 2
                $this->syntaxError('SELECT, UPDATE or DELETE');
866
                break;
867
        }
868
869
        // Check for end of string
870 786
        if ($this->lexer->lookahead !== null) {
871 3
            $this->syntaxError('end of string');
872
        }
873
874 783
        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...
875
    }
876
877
    /**
878
     * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
879
     *
880
     * @return \Doctrine\ORM\Query\AST\SelectStatement
881
     */
882 756
    public function SelectStatement()
883
    {
884 756
        $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());
885
886 725
        $selectStatement->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
887 722
        $selectStatement->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
888 721
        $selectStatement->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
889 721
        $selectStatement->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
890
891 721
        return $selectStatement;
892
    }
893
894
    /**
895
     * UpdateStatement ::= UpdateClause [WhereClause]
896
     *
897
     * @return \Doctrine\ORM\Query\AST\UpdateStatement
898
     */
899 32
    public function UpdateStatement()
900
    {
901 32
        $updateStatement = new AST\UpdateStatement($this->UpdateClause());
902
903 32
        $updateStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
904
905 32
        return $updateStatement;
906
    }
907
908
    /**
909
     * DeleteStatement ::= DeleteClause [WhereClause]
910
     *
911
     * @return \Doctrine\ORM\Query\AST\DeleteStatement
912
     */
913 42
    public function DeleteStatement()
914
    {
915 42
        $deleteStatement = new AST\DeleteStatement($this->DeleteClause());
916
917 41
        $deleteStatement->whereClause = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
918
919 41
        return $deleteStatement;
920
    }
921
922
    /**
923
     * IdentificationVariable ::= identifier
924
     *
925
     * @return string
926
     */
927 799
    public function IdentificationVariable()
928
    {
929 799
        $this->match(Lexer::T_IDENTIFIER);
930
931 799
        $identVariable = $this->lexer->token['value'];
932
933 799
        $this->deferredIdentificationVariables[] = [
934 799
            'expression'   => $identVariable,
935 799
            'nestingLevel' => $this->nestingLevel,
936 799
            'token'        => $this->lexer->token,
937
        ];
938
939 799
        return $identVariable;
940
    }
941
942
    /**
943
     * AliasIdentificationVariable = identifier
944
     *
945
     * @return string
946
     */
947 793
    public function AliasIdentificationVariable()
948
    {
949 793
        $this->match(Lexer::T_IDENTIFIER);
950
951 793
        $aliasIdentVariable = $this->lexer->token['value'];
952 793
        $exists = isset($this->queryComponents[$aliasIdentVariable]);
953
954 793
        if ($exists) {
955 2
            $this->semanticalError("'$aliasIdentVariable' is already defined.", $this->lexer->token);
956
        }
957
958 793
        return $aliasIdentVariable;
959
    }
960
961
    /**
962
     * AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier
963
     *
964
     * @return string
965
     */
966 813
    public function AbstractSchemaName()
967
    {
968 813
        if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
969 796
            $this->match(Lexer::T_FULLY_QUALIFIED_NAME);
970
971 796
            $schemaName = $this->lexer->token['value'];
972 28
        } else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
973 19
            $this->match(Lexer::T_IDENTIFIER);
974
975 19
            $schemaName = $this->lexer->token['value'];
976
        } else {
977 10
            $this->match(Lexer::T_ALIASED_NAME);
978
979 10
            list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']);
980
981 10
            $schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
982
        }
983
984 813
        return $schemaName;
985
    }
986
987
    /**
988
     * Validates an AbstractSchemaName, making sure the class exists.
989
     *
990
     * @param string $schemaName The name to validate.
991
     *
992
     * @throws QueryException if the name does not exist.
993
     */
994 808
    private function validateAbstractSchemaName($schemaName)
995
    {
996 808
        if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {
997 16
            $this->semanticalError("Class '$schemaName' is not defined.", $this->lexer->token);
998
        }
999 793
    }
1000
1001
    /**
1002
     * AliasResultVariable ::= identifier
1003
     *
1004
     * @return string
1005
     */
1006 119
    public function AliasResultVariable()
1007
    {
1008 119
        $this->match(Lexer::T_IDENTIFIER);
1009
1010 115
        $resultVariable = $this->lexer->token['value'];
1011 115
        $exists = isset($this->queryComponents[$resultVariable]);
1012
1013 115
        if ($exists) {
1014 2
            $this->semanticalError("'$resultVariable' is already defined.", $this->lexer->token);
1015
        }
1016
1017 115
        return $resultVariable;
1018
    }
1019
1020
    /**
1021
     * ResultVariable ::= identifier
1022
     *
1023
     * @return string
1024
     */
1025 30
    public function ResultVariable()
1026
    {
1027 30
        $this->match(Lexer::T_IDENTIFIER);
1028
1029 30
        $resultVariable = $this->lexer->token['value'];
1030
1031
        // Defer ResultVariable validation
1032 30
        $this->deferredResultVariables[] = [
1033 30
            'expression'   => $resultVariable,
1034 30
            'nestingLevel' => $this->nestingLevel,
1035 30
            'token'        => $this->lexer->token,
1036
        ];
1037
1038 30
        return $resultVariable;
1039
    }
1040
1041
    /**
1042
     * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)
1043
     *
1044
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1045
     */
1046 256
    public function JoinAssociationPathExpression()
1047
    {
1048 256
        $identVariable = $this->IdentificationVariable();
1049
1050 256
        if ( ! isset($this->queryComponents[$identVariable])) {
1051
            $this->semanticalError(
1052
                'Identification Variable ' . $identVariable .' used in join path expression but was not defined before.'
1053
            );
1054
        }
1055
1056 256
        $this->match(Lexer::T_DOT);
1057 256
        $this->match(Lexer::T_IDENTIFIER);
1058
1059 256
        $field = $this->lexer->token['value'];
1060
1061
        // Validate association field
1062 256
        $qComp = $this->queryComponents[$identVariable];
1063 256
        $class = $qComp['metadata'];
1064
1065 256
        if ( ! $class->hasAssociation($field)) {
1066
            $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
1067
        }
1068
1069 256
        return new AST\JoinAssociationPathExpression($identVariable, $field);
1070
    }
1071
1072
    /**
1073
     * Parses an arbitrary path expression and defers semantical validation
1074
     * based on expected types.
1075
     *
1076
     * PathExpression ::= IdentificationVariable {"." identifier}*
1077
     *
1078
     * @param integer $expectedTypes
1079
     *
1080
     * @return \Doctrine\ORM\Query\AST\PathExpression
1081
     */
1082 595
    public function PathExpression($expectedTypes)
1083
    {
1084 595
        $identVariable = $this->IdentificationVariable();
1085 595
        $field = null;
1086
1087 595
        if ($this->lexer->isNextToken(Lexer::T_DOT)) {
1088 588
            $this->match(Lexer::T_DOT);
1089 588
            $this->match(Lexer::T_IDENTIFIER);
1090
1091 588
            $field = $this->lexer->token['value'];
1092
1093 588
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1094 2
                $this->match(Lexer::T_DOT);
1095 2
                $this->match(Lexer::T_IDENTIFIER);
1096 2
                $field .= '.'.$this->lexer->token['value'];
1097
            }
1098
        }
1099
1100
        // Creating AST node
1101 595
        $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);
1102
1103
        // Defer PathExpression validation if requested to be deferred
1104 595
        $this->deferredPathExpressions[] = [
1105 595
            'expression'   => $pathExpr,
1106 595
            'nestingLevel' => $this->nestingLevel,
1107 595
            'token'        => $this->lexer->token,
1108
        ];
1109
1110 595
        return $pathExpr;
1111
    }
1112
1113
    /**
1114
     * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression
1115
     *
1116
     * @return \Doctrine\ORM\Query\AST\PathExpression
1117
     */
1118
    public function AssociationPathExpression()
1119
    {
1120
        return $this->PathExpression(
1121
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |
1122
            AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION
1123
        );
1124
    }
1125
1126
    /**
1127
     * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression
1128
     *
1129
     * @return \Doctrine\ORM\Query\AST\PathExpression
1130
     */
1131 504
    public function SingleValuedPathExpression()
1132
    {
1133 504
        return $this->PathExpression(
1134 504
            AST\PathExpression::TYPE_STATE_FIELD |
1135 504
            AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION
1136
        );
1137
    }
1138
1139
    /**
1140
     * StateFieldPathExpression ::= IdentificationVariable "." StateField
1141
     *
1142
     * @return \Doctrine\ORM\Query\AST\PathExpression
1143
     */
1144 202
    public function StateFieldPathExpression()
1145
    {
1146 202
        return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);
1147
    }
1148
1149
    /**
1150
     * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField
1151
     *
1152
     * @return \Doctrine\ORM\Query\AST\PathExpression
1153
     */
1154 9
    public function SingleValuedAssociationPathExpression()
1155
    {
1156 9
        return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);
1157
    }
1158
1159
    /**
1160
     * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField
1161
     *
1162
     * @return \Doctrine\ORM\Query\AST\PathExpression
1163
     */
1164 22
    public function CollectionValuedPathExpression()
1165
    {
1166 22
        return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
1167
    }
1168
1169
    /**
1170
     * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
1171
     *
1172
     * @return \Doctrine\ORM\Query\AST\SelectClause
1173
     */
1174 756
    public function SelectClause()
1175
    {
1176 756
        $isDistinct = false;
1177 756
        $this->match(Lexer::T_SELECT);
1178
1179
        // Check for DISTINCT
1180 756
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1181 6
            $this->match(Lexer::T_DISTINCT);
1182
1183 6
            $isDistinct = true;
1184
        }
1185
1186
        // Process SelectExpressions (1..N)
1187 756
        $selectExpressions = [];
1188 756
        $selectExpressions[] = $this->SelectExpression();
1189
1190 748
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1191 283
            $this->match(Lexer::T_COMMA);
1192
1193 283
            $selectExpressions[] = $this->SelectExpression();
1194
        }
1195
1196 747
        return new AST\SelectClause($selectExpressions, $isDistinct);
1197
    }
1198
1199
    /**
1200
     * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression
1201
     *
1202
     * @return \Doctrine\ORM\Query\AST\SimpleSelectClause
1203
     */
1204 48
    public function SimpleSelectClause()
1205
    {
1206 48
        $isDistinct = false;
1207 48
        $this->match(Lexer::T_SELECT);
1208
1209 48
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
1210
            $this->match(Lexer::T_DISTINCT);
1211
1212
            $isDistinct = true;
1213
        }
1214
1215 48
        return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);
1216
    }
1217
1218
    /**
1219
     * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*
1220
     *
1221
     * @return \Doctrine\ORM\Query\AST\UpdateClause
1222
     */
1223 32
    public function UpdateClause()
1224
    {
1225 32
        $this->match(Lexer::T_UPDATE);
1226
1227 32
        $token = $this->lexer->lookahead;
1228 32
        $abstractSchemaName = $this->AbstractSchemaName();
1229
1230 32
        $this->validateAbstractSchemaName($abstractSchemaName);
1231
1232 32
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1233 2
            $this->match(Lexer::T_AS);
1234
        }
1235
1236 32
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1237
1238 32
        $class = $this->em->getClassMetadata($abstractSchemaName);
1239
1240
        // Building queryComponent
1241
        $queryComponent = [
1242 32
            'metadata'     => $class,
1243
            'parent'       => null,
1244
            'relation'     => null,
1245
            'map'          => null,
1246 32
            'nestingLevel' => $this->nestingLevel,
1247 32
            'token'        => $token,
1248
        ];
1249
1250 32
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1251
1252 32
        $this->match(Lexer::T_SET);
1253
1254 32
        $updateItems = [];
1255 32
        $updateItems[] = $this->UpdateItem();
1256
1257 32
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1258 5
            $this->match(Lexer::T_COMMA);
1259
1260 5
            $updateItems[] = $this->UpdateItem();
1261
        }
1262
1263 32
        $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);
1264 32
        $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1265
1266 32
        return $updateClause;
1267
    }
1268
1269
    /**
1270
     * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable
1271
     *
1272
     * @return \Doctrine\ORM\Query\AST\DeleteClause
1273
     */
1274 42
    public function DeleteClause()
1275
    {
1276 42
        $this->match(Lexer::T_DELETE);
1277
1278 42
        if ($this->lexer->isNextToken(Lexer::T_FROM)) {
1279 8
            $this->match(Lexer::T_FROM);
1280
        }
1281
1282 42
        $token = $this->lexer->lookahead;
1283 42
        $abstractSchemaName = $this->AbstractSchemaName();
1284
1285 42
        $this->validateAbstractSchemaName($abstractSchemaName);
1286
1287 42
        $deleteClause = new AST\DeleteClause($abstractSchemaName);
1288
1289 42
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1290 1
            $this->match(Lexer::T_AS);
1291
        }
1292
1293 42
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1294
1295 41
        $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
1296 41
        $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);
1297
1298
        // Building queryComponent
1299
        $queryComponent = [
1300 41
            'metadata'     => $class,
1301
            'parent'       => null,
1302
            'relation'     => null,
1303
            'map'          => null,
1304 41
            'nestingLevel' => $this->nestingLevel,
1305 41
            'token'        => $token,
1306
        ];
1307
1308 41
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1309
1310 41
        return $deleteClause;
1311
    }
1312
1313
    /**
1314
     * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*
1315
     *
1316
     * @return \Doctrine\ORM\Query\AST\FromClause
1317
     */
1318 747
    public function FromClause()
1319
    {
1320 747
        $this->match(Lexer::T_FROM);
1321
1322 742
        $identificationVariableDeclarations = [];
1323 742
        $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1324
1325 725
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1326 6
            $this->match(Lexer::T_COMMA);
1327
1328 6
            $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();
1329
        }
1330
1331 725
        return new AST\FromClause($identificationVariableDeclarations);
1332
    }
1333
1334
    /**
1335
     * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*
1336
     *
1337
     * @return \Doctrine\ORM\Query\AST\SubselectFromClause
1338
     */
1339 48
    public function SubselectFromClause()
1340
    {
1341 48
        $this->match(Lexer::T_FROM);
1342
1343 48
        $identificationVariables = [];
1344 48
        $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1345
1346 47
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1347
            $this->match(Lexer::T_COMMA);
1348
1349
            $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();
1350
        }
1351
1352 47
        return new AST\SubselectFromClause($identificationVariables);
1353
    }
1354
1355
    /**
1356
     * WhereClause ::= "WHERE" ConditionalExpression
1357
     *
1358
     * @return \Doctrine\ORM\Query\AST\WhereClause
1359
     */
1360 330
    public function WhereClause()
1361
    {
1362 330
        $this->match(Lexer::T_WHERE);
1363
1364 330
        return new AST\WhereClause($this->ConditionalExpression());
1365
    }
1366
1367
    /**
1368
     * HavingClause ::= "HAVING" ConditionalExpression
1369
     *
1370
     * @return \Doctrine\ORM\Query\AST\HavingClause
1371
     */
1372 21
    public function HavingClause()
1373
    {
1374 21
        $this->match(Lexer::T_HAVING);
1375
1376 21
        return new AST\HavingClause($this->ConditionalExpression());
1377
    }
1378
1379
    /**
1380
     * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*
1381
     *
1382
     * @return \Doctrine\ORM\Query\AST\GroupByClause
1383
     */
1384 31
    public function GroupByClause()
1385
    {
1386 31
        $this->match(Lexer::T_GROUP);
1387 31
        $this->match(Lexer::T_BY);
1388
1389 31
        $groupByItems = [$this->GroupByItem()];
1390
1391 30
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1392 8
            $this->match(Lexer::T_COMMA);
1393
1394 8
            $groupByItems[] = $this->GroupByItem();
1395
        }
1396
1397 30
        return new AST\GroupByClause($groupByItems);
1398
    }
1399
1400
    /**
1401
     * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
1402
     *
1403
     * @return \Doctrine\ORM\Query\AST\OrderByClause
1404
     */
1405 179
    public function OrderByClause()
1406
    {
1407 179
        $this->match(Lexer::T_ORDER);
1408 179
        $this->match(Lexer::T_BY);
1409
1410 179
        $orderByItems = [];
1411 179
        $orderByItems[] = $this->OrderByItem();
1412
1413 179
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1414 14
            $this->match(Lexer::T_COMMA);
1415
1416 14
            $orderByItems[] = $this->OrderByItem();
1417
        }
1418
1419 179
        return new AST\OrderByClause($orderByItems);
1420
    }
1421
1422
    /**
1423
     * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]
1424
     *
1425
     * @return \Doctrine\ORM\Query\AST\Subselect
1426
     */
1427 48
    public function Subselect()
1428
    {
1429
        // Increase query nesting level
1430 48
        $this->nestingLevel++;
1431
1432 48
        $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());
1433
1434 47
        $subselect->whereClause   = $this->lexer->isNextToken(Lexer::T_WHERE) ? $this->WhereClause() : null;
1435 47
        $subselect->groupByClause = $this->lexer->isNextToken(Lexer::T_GROUP) ? $this->GroupByClause() : null;
1436 47
        $subselect->havingClause  = $this->lexer->isNextToken(Lexer::T_HAVING) ? $this->HavingClause() : null;
1437 47
        $subselect->orderByClause = $this->lexer->isNextToken(Lexer::T_ORDER) ? $this->OrderByClause() : null;
1438
1439
        // Decrease query nesting level
1440 47
        $this->nestingLevel--;
1441
1442 47
        return $subselect;
1443
    }
1444
1445
    /**
1446
     * UpdateItem ::= SingleValuedPathExpression "=" NewValue
1447
     *
1448
     * @return \Doctrine\ORM\Query\AST\UpdateItem
1449
     */
1450 32
    public function UpdateItem()
1451
    {
1452 32
        $pathExpr = $this->SingleValuedPathExpression();
1453
1454 32
        $this->match(Lexer::T_EQUALS);
1455
1456 32
        $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue());
1457
1458 32
        return $updateItem;
1459
    }
1460
1461
    /**
1462
     * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression
1463
     *
1464
     * @return string | \Doctrine\ORM\Query\AST\PathExpression
1465
     */
1466 31
    public function GroupByItem()
1467
    {
1468
        // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
1469 31
        $glimpse = $this->lexer->glimpse();
1470
1471 31
        if ($glimpse['type'] === Lexer::T_DOT) {
1472 12
            return $this->SingleValuedPathExpression();
1473
        }
1474
1475
        // Still need to decide between IdentificationVariable or ResultVariable
1476 19
        $lookaheadValue = $this->lexer->lookahead['value'];
1477
1478 19
        if ( ! isset($this->queryComponents[$lookaheadValue])) {
1479 1
            $this->semanticalError('Cannot group by undefined identification or result variable.');
1480
        }
1481
1482 18
        return (isset($this->queryComponents[$lookaheadValue]['metadata']))
1483 16
            ? $this->IdentificationVariable()
1484 18
            : $this->ResultVariable();
1485
    }
1486
1487
    /**
1488
     * OrderByItem ::= (
1489
     *      SimpleArithmeticExpression | SingleValuedPathExpression |
1490
     *      ScalarExpression | ResultVariable | FunctionDeclaration
1491
     * ) ["ASC" | "DESC"]
1492
     *
1493
     * @return \Doctrine\ORM\Query\AST\OrderByItem
1494
     */
1495 179
    public function OrderByItem()
1496
    {
1497 179
        $this->lexer->peek(); // lookahead => '.'
1498 179
        $this->lexer->peek(); // lookahead => token after '.'
1499
1500 179
        $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1501
1502 179
        $this->lexer->resetPeek();
1503
1504 179
        $glimpse = $this->lexer->glimpse();
1505
1506
        switch (true) {
1507 179
            case ($this->isFunction($peek)):
0 ignored issues
show
Unused Code introduced by
The call to Parser::isFunction() has too many arguments starting with $peek.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1508 1
                $expr = $this->FunctionDeclaration();
1509 1
                break;
1510
1511 178
            case ($this->isMathOperator($peek)):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1500 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...
1512 25
                $expr = $this->SimpleArithmeticExpression();
1513 25
                break;
1514
1515 154
            case ($glimpse['type'] === Lexer::T_DOT):
1516 141
                $expr = $this->SingleValuedPathExpression();
1517 141
                break;
1518
1519 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...
1520 2
                $expr = $this->ScalarExpression();
1521 2
                break;
1522
1523
            default:
1524 15
                $expr = $this->ResultVariable();
1525 15
                break;
1526
        }
1527
1528 179
        $type = 'ASC';
1529 179
        $item = new AST\OrderByItem($expr);
1530
1531
        switch (true) {
1532 179
            case ($this->lexer->isNextToken(Lexer::T_DESC)):
1533 93
                $this->match(Lexer::T_DESC);
1534 93
                $type = 'DESC';
1535 93
                break;
1536
1537 152
            case ($this->lexer->isNextToken(Lexer::T_ASC)):
1538 96
                $this->match(Lexer::T_ASC);
1539 96
                break;
1540
1541
            default:
1542
                // Do nothing
1543
        }
1544
1545 179
        $item->type = $type;
1546
1547 179
        return $item;
1548
    }
1549
1550
    /**
1551
     * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |
1552
     *      EnumPrimary | SimpleEntityExpression | "NULL"
1553
     *
1554
     * NOTE: Since it is not possible to correctly recognize individual types, here is the full
1555
     * grammar that needs to be supported:
1556
     *
1557
     * NewValue ::= SimpleArithmeticExpression | "NULL"
1558
     *
1559
     * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression
1560
     *
1561
     * @return AST\ArithmeticExpression
1562
     */
1563 32
    public function NewValue()
1564
    {
1565 32
        if ($this->lexer->isNextToken(Lexer::T_NULL)) {
1566 1
            $this->match(Lexer::T_NULL);
1567
1568 1
            return null;
1569
        }
1570
1571 31
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
1572 18
            $this->match(Lexer::T_INPUT_PARAMETER);
1573
1574 18
            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...
1575
        }
1576
1577 13
        return $this->ArithmeticExpression();
1578
    }
1579
1580
    /**
1581
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1582
     *
1583
     * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1584
     */
1585 744
    public function IdentificationVariableDeclaration()
1586
    {
1587 744
        $joins                    = [];
1588 744
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1589 729
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1590 7
            ? $this->IndexBy()
1591 729
            : null;
1592
1593 729
        $rangeVariableDeclaration->isRoot = true;
1594
1595
        while (
1596 729
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1597 729
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1598 729
            $this->lexer->isNextToken(Lexer::T_JOIN)
1599
        ) {
1600 277
            $joins[] = $this->Join();
1601
        }
1602
1603 727
        return new AST\IdentificationVariableDeclaration(
1604
            $rangeVariableDeclaration, $indexBy, $joins
1605
        );
1606
    }
1607
1608
    /**
1609
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1610
     *
1611
     * {Internal note: WARNING: Solution is harder than a bare implementation.
1612
     * Desired EBNF support:
1613
     *
1614
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1615
     *
1616
     * It demands that entire SQL generation to become programmatical. This is
1617
     * needed because association based subselect requires "WHERE" conditional
1618
     * expressions to be injected, but there is no scope to do that. Only scope
1619
     * accessible is "FROM", prohibiting an easy implementation without larger
1620
     * changes.}
1621
     *
1622
     * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration |
1623
     *         \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1624
     */
1625 48
    public function SubselectIdentificationVariableDeclaration()
1626
    {
1627
        /*
1628
        NOT YET IMPLEMENTED!
1629
1630
        $glimpse = $this->lexer->glimpse();
1631
1632
        if ($glimpse['type'] == Lexer::T_DOT) {
1633
            $associationPathExpression = $this->AssociationPathExpression();
1634
1635
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1636
                $this->match(Lexer::T_AS);
1637
            }
1638
1639
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1640
            $identificationVariable      = $associationPathExpression->identificationVariable;
1641
            $field                       = $associationPathExpression->associationField;
1642
1643
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1644
            $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1645
1646
            // Building queryComponent
1647
            $joinQueryComponent = array(
1648
                'metadata'     => $targetClass,
1649
                'parent'       => $identificationVariable,
1650
                'relation'     => $class->getAssociationMapping($field),
1651
                'map'          => null,
1652
                'nestingLevel' => $this->nestingLevel,
1653
                'token'        => $this->lexer->lookahead
1654
            );
1655
1656
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1657
1658
            return new AST\SubselectIdentificationVariableDeclaration(
1659
                $associationPathExpression, $aliasIdentificationVariable
1660
            );
1661
        }
1662
        */
1663
1664 48
        return $this->IdentificationVariableDeclaration();
1665
    }
1666
1667
    /**
1668
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1669
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1670
     *          ["WITH" ConditionalExpression]
1671
     *
1672
     * @return \Doctrine\ORM\Query\AST\Join
1673
     */
1674 277
    public function Join()
1675
    {
1676
        // Check Join type
1677 277
        $joinType = AST\Join::JOIN_TYPE_INNER;
1678
1679
        switch (true) {
1680 277
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1681 67
                $this->match(Lexer::T_LEFT);
1682
1683 67
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1684
1685
                // Possible LEFT OUTER join
1686 67
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1687
                    $this->match(Lexer::T_OUTER);
1688
1689
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1690
                }
1691 67
                break;
1692
1693 214
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1694 20
                $this->match(Lexer::T_INNER);
1695 20
                break;
1696
1697
            default:
1698
                // Do nothing
1699
        }
1700
1701 277
        $this->match(Lexer::T_JOIN);
1702
1703 277
        $next            = $this->lexer->glimpse();
1704 277
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1705 275
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1706 275
        $join            = new AST\Join($joinType, $joinDeclaration);
1707
1708
        // Describe non-root join declaration
1709 275
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1710 21
            $joinDeclaration->isRoot = false;
1711
        }
1712
1713
        // Check for ad-hoc Join conditions
1714 275
        if ($adhocConditions) {
1715 23
            $this->match(Lexer::T_WITH);
1716
1717 23
            $join->conditionalExpression = $this->ConditionalExpression();
1718
        }
1719
1720 275
        return $join;
1721
    }
1722
1723
    /**
1724
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1725
     *
1726
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1727
     */
1728 744
    public function RangeVariableDeclaration()
1729
    {
1730 744
        $abstractSchemaName = $this->AbstractSchemaName();
1731
1732 744
        $this->validateAbstractSchemaName($abstractSchemaName);
1733
1734 729
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1735 5
            $this->match(Lexer::T_AS);
1736
        }
1737
1738 729
        $token = $this->lexer->lookahead;
1739 729
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1740 729
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1741
1742
        // Building queryComponent
1743
        $queryComponent = [
1744 729
            'metadata'     => $classMetadata,
1745
            'parent'       => null,
1746
            'relation'     => null,
1747
            'map'          => null,
1748 729
            'nestingLevel' => $this->nestingLevel,
1749 729
            'token'        => $token
1750
        ];
1751
1752 729
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1753
1754 729
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1755
    }
1756
1757
    /**
1758
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1759
     *
1760
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1761
     */
1762 256
    public function JoinAssociationDeclaration()
1763
    {
1764 256
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1765
1766 256
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1767 4
            $this->match(Lexer::T_AS);
1768
        }
1769
1770 256
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1771 254
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1772
1773 254
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1774 254
        $field                  = $joinAssociationPathExpression->associationField;
1775
1776 254
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1777 254
        $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1778
1779
        // Building queryComponent
1780
        $joinQueryComponent = [
1781 254
            'metadata'     => $targetClass,
1782 254
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1783 254
            'relation'     => $class->getAssociationMapping($field),
1784
            'map'          => null,
1785 254
            'nestingLevel' => $this->nestingLevel,
1786 254
            'token'        => $this->lexer->lookahead
1787
        ];
1788
1789 254
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1790
1791 254
        return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
1792
    }
1793
1794
    /**
1795
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1796
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1797
     *
1798
     * @return array
1799
     */
1800 11
    public function PartialObjectExpression()
1801
    {
1802 11
        $this->match(Lexer::T_PARTIAL);
1803
1804 11
        $partialFieldSet = [];
1805
1806 11
        $identificationVariable = $this->IdentificationVariable();
1807
1808 11
        $this->match(Lexer::T_DOT);
1809 11
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1810 11
        $this->match(Lexer::T_IDENTIFIER);
1811
1812 11
        $field = $this->lexer->token['value'];
1813
1814
        // First field in partial expression might be embeddable property
1815 11
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1816 1
            $this->match(Lexer::T_DOT);
1817 1
            $this->match(Lexer::T_IDENTIFIER);
1818 1
            $field .= '.'.$this->lexer->token['value'];
1819
        }
1820
1821 11
        $partialFieldSet[] = $field;
1822
1823 11
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1824 9
            $this->match(Lexer::T_COMMA);
1825 9
            $this->match(Lexer::T_IDENTIFIER);
1826
1827 9
            $field = $this->lexer->token['value'];
1828
1829 9
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1830 2
                $this->match(Lexer::T_DOT);
1831 2
                $this->match(Lexer::T_IDENTIFIER);
1832 2
                $field .= '.'.$this->lexer->token['value'];
1833
            }
1834
1835 9
            $partialFieldSet[] = $field;
1836
        }
1837
1838 11
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1839
1840 11
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1841
1842
        // Defer PartialObjectExpression validation
1843 11
        $this->deferredPartialObjectExpressions[] = [
1844 11
            'expression'   => $partialObjectExpression,
1845 11
            'nestingLevel' => $this->nestingLevel,
1846 11
            'token'        => $this->lexer->token,
1847
        ];
1848
1849 11
        return $partialObjectExpression;
1850
    }
1851
1852
    /**
1853
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1854
     *
1855
     * @return \Doctrine\ORM\Query\AST\NewObjectExpression
1856
     */
1857 28
    public function NewObjectExpression()
1858
    {
1859 28
        $this->match(Lexer::T_NEW);
1860
1861 28
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1862 28
        $token = $this->lexer->token;
1863
1864 28
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1865
1866 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...
1867
1868 28
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1869 24
            $this->match(Lexer::T_COMMA);
1870
1871 24
            $args[] = $this->NewObjectArg();
1872
        }
1873
1874 28
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1875
1876 28
        $expression = new AST\NewObjectExpression($className, $args);
1877
1878
        // Defer NewObjectExpression validation
1879 28
        $this->deferredNewObjectExpressions[] = [
1880 28
            'token'        => $token,
1881 28
            'expression'   => $expression,
1882 28
            'nestingLevel' => $this->nestingLevel,
1883
        ];
1884
1885 28
        return $expression;
1886
    }
1887
1888
    /**
1889
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1890
     *
1891
     * @return mixed
1892
     */
1893 28
    public function NewObjectArg()
1894
    {
1895 28
        $token = $this->lexer->lookahead;
1896 28
        $peek  = $this->lexer->glimpse();
1897
1898 28
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1899 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1900 2
            $expression = $this->Subselect();
1901 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1902
1903 2
            return $expression;
1904
        }
1905
1906 28
        return $this->ScalarExpression();
1907
    }
1908
1909
    /**
1910
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1911
     *
1912
     * @return \Doctrine\ORM\Query\AST\IndexBy
1913
     */
1914 11
    public function IndexBy()
1915
    {
1916 11
        $this->match(Lexer::T_INDEX);
1917 11
        $this->match(Lexer::T_BY);
1918 11
        $pathExpr = $this->StateFieldPathExpression();
1919
1920
        // Add the INDEX BY info to the query component
1921 11
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1922
1923 11
        return new AST\IndexBy($pathExpr);
1924
    }
1925
1926
    /**
1927
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1928
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1929
     *                      InstanceOfExpression
1930
     *
1931
     * @return mixed One of the possible expressions or subexpressions.
1932
     */
1933 160
    public function ScalarExpression()
1934
    {
1935 160
        $lookahead = $this->lexer->lookahead['type'];
1936 160
        $peek      = $this->lexer->glimpse();
1937
1938
        switch (true) {
1939 160
            case ($lookahead === Lexer::T_INTEGER):
1940 157
            case ($lookahead === Lexer::T_FLOAT):
1941
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1942 157
            case ($lookahead === Lexer::T_MINUS):
1943 157
            case ($lookahead === Lexer::T_PLUS):
1944 17
                return $this->SimpleArithmeticExpression();
1945
1946 157
            case ($lookahead === Lexer::T_STRING):
1947 13
                return $this->StringPrimary();
1948
1949 155
            case ($lookahead === Lexer::T_TRUE):
1950 155
            case ($lookahead === Lexer::T_FALSE):
1951 3
                $this->match($lookahead);
1952
1953 3
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1954
1955 155
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
1956
                switch (true) {
1957 1
                    case $this->isMathOperator($peek):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->glimpse() on line 1936 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...
1958
                        // :param + u.value
1959 1
                        return $this->SimpleArithmeticExpression();
1960
                    default:
1961
                        return $this->InputParameter();
1962
                }
1963
1964 155
            case ($lookahead === Lexer::T_CASE):
1965 151
            case ($lookahead === Lexer::T_COALESCE):
1966 151
            case ($lookahead === Lexer::T_NULLIF):
1967
                // Since NULLIF and COALESCE can be identified as a function,
1968
                // we need to check these before checking for FunctionDeclaration
1969 8
                return $this->CaseExpression();
1970
1971 151
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1972 4
                return $this->SimpleArithmeticExpression();
1973
1974
            // this check must be done before checking for a filed path expression
1975 148
            case ($this->isFunction()):
1976 26
                $this->lexer->peek(); // "("
1977
1978
                switch (true) {
1979 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...
1980
                        // SUM(u.id) + COUNT(u.id)
1981 7
                        return $this->SimpleArithmeticExpression();
1982
1983 21
                    case ($this->isAggregateFunction($this->lexer->lookahead['type'])):
1984 19
                        return $this->AggregateExpression();
1985
1986
                    default:
1987
                        // IDENTITY(u)
1988 2
                        return $this->FunctionDeclaration();
1989
                }
1990
1991
                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...
1992
            // it is no function, so it must be a field path
1993 130
            case ($lookahead === Lexer::T_IDENTIFIER):
1994 130
                $this->lexer->peek(); // lookahead => '.'
1995 130
                $this->lexer->peek(); // lookahead => token after '.'
1996 130
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1997 130
                $this->lexer->resetPeek();
1998
1999 130
                if ($this->isMathOperator($peek)) {
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1996 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...
2000 7
                    return $this->SimpleArithmeticExpression();
2001
                }
2002
2003 125
                return $this->StateFieldPathExpression();
2004
2005
            default:
2006
                $this->syntaxError();
2007
        }
2008
    }
2009
2010
    /**
2011
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
2012
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2013
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2014
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2015
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2016
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2017
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2018
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2019
     *
2020
     * @return mixed One of the possible expressions or subexpressions.
2021
     */
2022 19
    public function CaseExpression()
2023
    {
2024 19
        $lookahead = $this->lexer->lookahead['type'];
2025
2026
        switch ($lookahead) {
2027 19
            case Lexer::T_NULLIF:
2028 5
                return $this->NullIfExpression();
2029
2030 16
            case Lexer::T_COALESCE:
2031 2
                return $this->CoalesceExpression();
2032
2033 14
            case Lexer::T_CASE:
2034 14
                $this->lexer->resetPeek();
2035 14
                $peek = $this->lexer->peek();
2036
2037 14
                if ($peek['type'] === Lexer::T_WHEN) {
2038 9
                    return $this->GeneralCaseExpression();
2039
                }
2040
2041 5
                return $this->SimpleCaseExpression();
2042
2043
            default:
2044
                // Do nothing
2045
                break;
2046
        }
2047
2048
        $this->syntaxError();
2049
    }
2050
2051
    /**
2052
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2053
     *
2054
     * @return \Doctrine\ORM\Query\AST\CoalesceExpression
2055
     */
2056 3
    public function CoalesceExpression()
2057
    {
2058 3
        $this->match(Lexer::T_COALESCE);
2059 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2060
2061
        // Process ScalarExpressions (1..N)
2062 3
        $scalarExpressions = [];
2063 3
        $scalarExpressions[] = $this->ScalarExpression();
2064
2065 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2066 3
            $this->match(Lexer::T_COMMA);
2067
2068 3
            $scalarExpressions[] = $this->ScalarExpression();
2069
        }
2070
2071 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2072
2073 3
        return new AST\CoalesceExpression($scalarExpressions);
2074
    }
2075
2076
    /**
2077
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2078
     *
2079
     * @return \Doctrine\ORM\Query\AST\NullIfExpression
2080
     */
2081 5
    public function NullIfExpression()
2082
    {
2083 5
        $this->match(Lexer::T_NULLIF);
2084 5
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2085
2086 5
        $firstExpression = $this->ScalarExpression();
2087 5
        $this->match(Lexer::T_COMMA);
2088 5
        $secondExpression = $this->ScalarExpression();
2089
2090 5
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2091
2092 5
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2093
    }
2094
2095
    /**
2096
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2097
     *
2098
     * @return \Doctrine\ORM\Query\AST\GeneralCaseExpression
2099
     */
2100 9
    public function GeneralCaseExpression()
2101
    {
2102 9
        $this->match(Lexer::T_CASE);
2103
2104
        // Process WhenClause (1..N)
2105 9
        $whenClauses = [];
2106
2107
        do {
2108 9
            $whenClauses[] = $this->WhenClause();
2109 9
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2110
2111 9
        $this->match(Lexer::T_ELSE);
2112 9
        $scalarExpression = $this->ScalarExpression();
2113 9
        $this->match(Lexer::T_END);
2114
2115 9
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2116
    }
2117
2118
    /**
2119
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2120
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2121
     *
2122
     * @return AST\SimpleCaseExpression
2123
     */
2124 5
    public function SimpleCaseExpression()
2125
    {
2126 5
        $this->match(Lexer::T_CASE);
2127 5
        $caseOperand = $this->StateFieldPathExpression();
2128
2129
        // Process SimpleWhenClause (1..N)
2130 5
        $simpleWhenClauses = [];
2131
2132
        do {
2133 5
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2134 5
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2135
2136 5
        $this->match(Lexer::T_ELSE);
2137 5
        $scalarExpression = $this->ScalarExpression();
2138 5
        $this->match(Lexer::T_END);
2139
2140 5
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2141
    }
2142
2143
    /**
2144
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2145
     *
2146
     * @return \Doctrine\ORM\Query\AST\WhenClause
2147
     */
2148 9
    public function WhenClause()
2149
    {
2150 9
        $this->match(Lexer::T_WHEN);
2151 9
        $conditionalExpression = $this->ConditionalExpression();
2152 9
        $this->match(Lexer::T_THEN);
2153
2154 9
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2155
    }
2156
2157
    /**
2158
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2159
     *
2160
     * @return \Doctrine\ORM\Query\AST\SimpleWhenClause
2161
     */
2162 5
    public function SimpleWhenClause()
2163
    {
2164 5
        $this->match(Lexer::T_WHEN);
2165 5
        $conditionalExpression = $this->ScalarExpression();
2166 5
        $this->match(Lexer::T_THEN);
2167
2168 5
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2169
    }
2170
2171
    /**
2172
     * SelectExpression ::= (
2173
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2174
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2175
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2176
     *
2177
     * @return \Doctrine\ORM\Query\AST\SelectExpression
2178
     */
2179 756
    public function SelectExpression()
2180
    {
2181 756
        $expression    = null;
2182 756
        $identVariable = null;
2183 756
        $peek          = $this->lexer->glimpse();
2184 756
        $lookaheadType = $this->lexer->lookahead['type'];
2185
2186
        switch (true) {
2187
            // ScalarExpression (u.name)
2188 756
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2189 102
                $expression = $this->ScalarExpression();
2190 102
                break;
2191
2192
            // IdentificationVariable (u)
2193 697
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2194 582
                $expression = $identVariable = $this->IdentificationVariable();
2195 582
                break;
2196
2197
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
2198 176
            case ($lookaheadType === Lexer::T_CASE):
2199 171
            case ($lookaheadType === Lexer::T_COALESCE):
2200 169
            case ($lookaheadType === Lexer::T_NULLIF):
2201 9
                $expression = $this->CaseExpression();
2202 9
                break;
2203
2204
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2205 167
            case ($this->isFunction()):
2206 87
                $this->lexer->peek(); // "("
2207
2208
                switch (true) {
2209 87
                    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...
2210
                        // SUM(u.id) + COUNT(u.id)
2211 2
                        $expression = $this->ScalarExpression();
2212 2
                        break;
2213
2214 85
                    case ($this->isAggregateFunction($lookaheadType)):
2215
                        // COUNT(u.id)
2216 54
                        $expression = $this->AggregateExpression();
2217 54
                        break;
2218
2219
                    default:
2220
                        // IDENTITY(u)
2221 31
                        $expression = $this->FunctionDeclaration();
2222 31
                        break;
2223
                }
2224
2225 87
                break;
2226
2227
            // PartialObjectExpression (PARTIAL u.{id, name})
2228 80
            case ($lookaheadType === Lexer::T_PARTIAL):
2229 11
                $expression    = $this->PartialObjectExpression();
2230 11
                $identVariable = $expression->identificationVariable;
2231 11
                break;
2232
2233
            // Subselect
2234 69
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2235 22
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2236 22
                $expression = $this->Subselect();
2237 22
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2238 22
                break;
2239
2240
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2241 47
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2242 43
            case ($lookaheadType === Lexer::T_INTEGER):
2243 41
            case ($lookaheadType === Lexer::T_STRING):
2244 32
            case ($lookaheadType === Lexer::T_FLOAT):
2245
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2246 32
            case ($lookaheadType === Lexer::T_MINUS):
2247 32
            case ($lookaheadType === Lexer::T_PLUS):
2248 16
                $expression = $this->SimpleArithmeticExpression();
2249 16
                break;
2250
2251
            // NewObjectExpression (New ClassName(id, name))
2252 31
            case ($lookaheadType === Lexer::T_NEW):
2253 28
                $expression = $this->NewObjectExpression();
2254 28
                break;
2255
2256
            default:
2257 3
                $this->syntaxError(
2258 3
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2259 3
                    $this->lexer->lookahead
2260
                );
2261
        }
2262
2263
        // [["AS"] ["HIDDEN"] AliasResultVariable]
2264 753
        $mustHaveAliasResultVariable = false;
2265
2266 753
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2267 110
            $this->match(Lexer::T_AS);
2268
2269 110
            $mustHaveAliasResultVariable = true;
2270
        }
2271
2272 753
        $hiddenAliasResultVariable = false;
2273
2274 753
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2275 10
            $this->match(Lexer::T_HIDDEN);
2276
2277 10
            $hiddenAliasResultVariable = true;
2278
        }
2279
2280 753
        $aliasResultVariable = null;
2281
2282 753
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2283 119
            $token = $this->lexer->lookahead;
2284 119
            $aliasResultVariable = $this->AliasResultVariable();
2285
2286
            // Include AliasResultVariable in query components.
2287 114
            $this->queryComponents[$aliasResultVariable] = [
2288 114
                'resultVariable' => $expression,
2289 114
                'nestingLevel'   => $this->nestingLevel,
2290 114
                'token'          => $token,
2291
            ];
2292
        }
2293
2294
        // AST
2295
2296 748
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2297
2298 748
        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...
2299 590
            $this->identVariableExpressions[$identVariable] = $expr;
2300
        }
2301
2302 748
        return $expr;
2303
    }
2304
2305
    /**
2306
     * SimpleSelectExpression ::= (
2307
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2308
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2309
     * ) [["AS"] AliasResultVariable]
2310
     *
2311
     * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
2312
     */
2313 48
    public function SimpleSelectExpression()
2314
    {
2315 48
        $peek = $this->lexer->glimpse();
2316
2317 48
        switch ($this->lexer->lookahead['type']) {
2318 48
            case Lexer::T_IDENTIFIER:
2319
                switch (true) {
2320 19
                    case ($peek['type'] === Lexer::T_DOT):
2321 16
                        $expression = $this->StateFieldPathExpression();
2322
2323 16
                        return new AST\SimpleSelectExpression($expression);
2324
2325 3
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2326 2
                        $expression = $this->IdentificationVariable();
2327
2328 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...
2329
2330 1
                    case ($this->isFunction()):
2331
                        // SUM(u.id) + COUNT(u.id)
2332 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...
2333
                            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...
2334
                        }
2335
                        // COUNT(u.id)
2336 1
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2337
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2338
                        }
2339
                        // IDENTITY(u)
2340 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...
2341
2342
                    default:
2343
                        // Do nothing
2344
                }
2345
                break;
2346
2347 30
            case Lexer::T_OPEN_PARENTHESIS:
2348 3
                if ($peek['type'] !== Lexer::T_SELECT) {
2349
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2350 3
                    $expression = $this->SimpleArithmeticExpression();
2351
2352 3
                    return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->SimpleArithmeticExpression() on line 2350 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...
2353
                }
2354
2355
                // Subselect
2356
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2357
                $expression = $this->Subselect();
2358
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2359
2360
                return new AST\SimpleSelectExpression($expression);
2361
2362
            default:
2363
                // Do nothing
2364
        }
2365
2366 27
        $this->lexer->peek();
2367
2368 27
        $expression = $this->ScalarExpression();
2369 27
        $expr       = new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->ScalarExpression() on line 2368 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...
2370
2371 27
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2372 1
            $this->match(Lexer::T_AS);
2373
        }
2374
2375 27
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2376 2
            $token = $this->lexer->lookahead;
2377 2
            $resultVariable = $this->AliasResultVariable();
2378 2
            $expr->fieldIdentificationVariable = $resultVariable;
2379
2380
            // Include AliasResultVariable in query components.
2381 2
            $this->queryComponents[$resultVariable] = [
2382 2
                'resultvariable' => $expr,
2383 2
                'nestingLevel'   => $this->nestingLevel,
2384 2
                'token'          => $token,
2385
            ];
2386
        }
2387
2388 27
        return $expr;
2389
    }
2390
2391
    /**
2392
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2393
     *
2394
     * @return \Doctrine\ORM\Query\AST\ConditionalExpression
2395
     */
2396 372
    public function ConditionalExpression()
2397
    {
2398 372
        $conditionalTerms = [];
2399 372
        $conditionalTerms[] = $this->ConditionalTerm();
2400
2401 369
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2402 16
            $this->match(Lexer::T_OR);
2403
2404 16
            $conditionalTerms[] = $this->ConditionalTerm();
2405
        }
2406
2407
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2408
        // if only one AST\ConditionalTerm is defined
2409 369
        if (count($conditionalTerms) == 1) {
2410 361
            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...
2411
        }
2412
2413 16
        return new AST\ConditionalExpression($conditionalTerms);
2414
    }
2415
2416
    /**
2417
     * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*
2418
     *
2419
     * @return \Doctrine\ORM\Query\AST\ConditionalTerm
2420
     */
2421 372
    public function ConditionalTerm()
2422
    {
2423 372
        $conditionalFactors = [];
2424 372
        $conditionalFactors[] = $this->ConditionalFactor();
2425
2426 369
        while ($this->lexer->isNextToken(Lexer::T_AND)) {
2427 32
            $this->match(Lexer::T_AND);
2428
2429 32
            $conditionalFactors[] = $this->ConditionalFactor();
2430
        }
2431
2432
        // Phase 1 AST optimization: Prevent AST\ConditionalTerm
2433
        // if only one AST\ConditionalFactor is defined
2434 369
        if (count($conditionalFactors) == 1) {
2435 351
            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...
2436
        }
2437
2438 32
        return new AST\ConditionalTerm($conditionalFactors);
2439
    }
2440
2441
    /**
2442
     * ConditionalFactor ::= ["NOT"] ConditionalPrimary
2443
     *
2444
     * @return \Doctrine\ORM\Query\AST\ConditionalFactor
2445
     */
2446 372
    public function ConditionalFactor()
2447
    {
2448 372
        $not = false;
2449
2450 372
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2451 6
            $this->match(Lexer::T_NOT);
2452
2453 6
            $not = true;
2454
        }
2455
2456 372
        $conditionalPrimary = $this->ConditionalPrimary();
2457
2458
        // Phase 1 AST optimization: Prevent AST\ConditionalFactor
2459
        // if only one AST\ConditionalPrimary is defined
2460 369
        if ( ! $not) {
2461 367
            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...
2462
        }
2463
2464 6
        $conditionalFactor = new AST\ConditionalFactor($conditionalPrimary);
2465 6
        $conditionalFactor->not = $not;
2466
2467 6
        return $conditionalFactor;
2468
    }
2469
2470
    /**
2471
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2472
     *
2473
     * @return \Doctrine\ORM\Query\AST\ConditionalPrimary
2474
     */
2475 372
    public function ConditionalPrimary()
2476
    {
2477 372
        $condPrimary = new AST\ConditionalPrimary;
2478
2479 372
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2480 363
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2481
2482 360
            return $condPrimary;
2483
        }
2484
2485
        // Peek beyond the matching closing parenthesis ')'
2486 25
        $peek = $this->peekBeyondClosingParenthesis();
2487
2488 25
        if (in_array($peek['value'], ["=",  "<", "<=", "<>", ">", ">=", "!="]) ||
2489 22
            in_array($peek['type'], [Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS]) ||
2490 25
            $this->isMathOperator($peek)) {
2491 15
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2492
2493 15
            return $condPrimary;
2494
        }
2495
2496 21
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2497 21
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2498 21
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2499
2500 21
        return $condPrimary;
2501
    }
2502
2503
    /**
2504
     * SimpleConditionalExpression ::=
2505
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2506
     *      InExpression | NullComparisonExpression | ExistsExpression |
2507
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2508
     *      InstanceOfExpression
2509
     */
2510 372
    public function SimpleConditionalExpression()
2511
    {
2512 372
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2513 7
            return $this->ExistsExpression();
2514
        }
2515
2516 372
        $token      = $this->lexer->lookahead;
2517 372
        $peek       = $this->lexer->glimpse();
2518 372
        $lookahead  = $token;
2519
2520 372
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2521
            $token = $this->lexer->glimpse();
2522
        }
2523
2524 372
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2525
            // Peek beyond the matching closing parenthesis.
2526 348
            $beyond = $this->lexer->peek();
2527
2528 348
            switch ($peek['value']) {
2529 348
                case '(':
2530
                    // Peeks beyond the matched closing parenthesis.
2531 34
                    $token = $this->peekBeyondClosingParenthesis(false);
2532
2533 34
                    if ($token['type'] === Lexer::T_NOT) {
2534 3
                        $token = $this->lexer->peek();
2535
                    }
2536
2537 34
                    if ($token['type'] === Lexer::T_IS) {
2538 2
                        $lookahead = $this->lexer->peek();
2539
                    }
2540 34
                    break;
2541
2542
                default:
2543
                    // Peek beyond the PathExpression or InputParameter.
2544 320
                    $token = $beyond;
2545
2546 320
                    while ($token['value'] === '.') {
2547 281
                        $this->lexer->peek();
2548
2549 281
                        $token = $this->lexer->peek();
2550
                    }
2551
2552
                    // Also peek beyond a NOT if there is one.
2553 320
                    if ($token['type'] === Lexer::T_NOT) {
2554 11
                        $token = $this->lexer->peek();
2555
                    }
2556
2557
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2558 320
                    $lookahead = $this->lexer->peek();
2559
            }
2560
2561
            // Also peek beyond a NOT if there is one.
2562 348
            if ($lookahead['type'] === Lexer::T_NOT) {
2563 7
                $lookahead = $this->lexer->peek();
2564
            }
2565
2566 348
            $this->lexer->resetPeek();
2567
        }
2568
2569 372
        if ($token['type'] === Lexer::T_BETWEEN) {
2570 8
            return $this->BetweenExpression();
2571
        }
2572
2573 366
        if ($token['type'] === Lexer::T_LIKE) {
2574 14
            return $this->LikeExpression();
2575
        }
2576
2577 353
        if ($token['type'] === Lexer::T_IN) {
2578 34
            return $this->InExpression();
2579
        }
2580
2581 328
        if ($token['type'] === Lexer::T_INSTANCE) {
2582 12
            return $this->InstanceOfExpression();
2583
        }
2584
2585 316
        if ($token['type'] === Lexer::T_MEMBER) {
2586 7
            return $this->CollectionMemberExpression();
2587
        }
2588
2589 309
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2590 14
            return $this->NullComparisonExpression();
2591
        }
2592
2593 298
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2594 4
            return $this->EmptyCollectionComparisonExpression();
2595
        }
2596
2597 294
        return $this->ComparisonExpression();
2598
    }
2599
2600
    /**
2601
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2602
     *
2603
     * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression
2604
     */
2605 4
    public function EmptyCollectionComparisonExpression()
2606
    {
2607 4
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2608 4
            $this->CollectionValuedPathExpression()
2609
        );
2610 4
        $this->match(Lexer::T_IS);
2611
2612 4
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2613 2
            $this->match(Lexer::T_NOT);
2614 2
            $emptyCollectionCompExpr->not = true;
2615
        }
2616
2617 4
        $this->match(Lexer::T_EMPTY);
2618
2619 4
        return $emptyCollectionCompExpr;
2620
    }
2621
2622
    /**
2623
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2624
     *
2625
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2626
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2627
     *
2628
     * @return \Doctrine\ORM\Query\AST\CollectionMemberExpression
2629
     */
2630 7
    public function CollectionMemberExpression()
2631
    {
2632 7
        $not        = false;
2633 7
        $entityExpr = $this->EntityExpression();
2634
2635 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2636
            $this->match(Lexer::T_NOT);
2637
2638
            $not = true;
2639
        }
2640
2641 7
        $this->match(Lexer::T_MEMBER);
2642
2643 7
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2644 7
            $this->match(Lexer::T_OF);
2645
        }
2646
2647 7
        $collMemberExpr = new AST\CollectionMemberExpression(
2648 7
            $entityExpr, $this->CollectionValuedPathExpression()
2649
        );
2650 7
        $collMemberExpr->not = $not;
2651
2652 7
        return $collMemberExpr;
2653
    }
2654
2655
    /**
2656
     * Literal ::= string | char | integer | float | boolean
2657
     *
2658
     * @return \Doctrine\ORM\Query\AST\Literal
2659
     */
2660 178
    public function Literal()
2661
    {
2662 178
        switch ($this->lexer->lookahead['type']) {
2663 178
            case Lexer::T_STRING:
2664 49
                $this->match(Lexer::T_STRING);
2665
2666 49
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2667 138
            case Lexer::T_INTEGER:
2668 9
            case Lexer::T_FLOAT:
2669 130
                $this->match(
2670 130
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2671
                );
2672
2673 130
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2674 8
            case Lexer::T_TRUE:
2675 4
            case Lexer::T_FALSE:
2676 8
                $this->match(
2677 8
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2678
                );
2679
2680 8
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2681
            default:
2682
                $this->syntaxError('Literal');
2683
        }
2684
    }
2685
2686
    /**
2687
     * InParameter ::= Literal | InputParameter
2688
     *
2689
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2690
     */
2691 25
    public function InParameter()
2692
    {
2693 25
        if ($this->lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) {
2694 13
            return $this->InputParameter();
2695
        }
2696
2697 12
        return $this->Literal();
2698
    }
2699
2700
    /**
2701
     * InputParameter ::= PositionalParameter | NamedParameter
2702
     *
2703
     * @return \Doctrine\ORM\Query\AST\InputParameter
2704
     */
2705 161
    public function InputParameter()
2706
    {
2707 161
        $this->match(Lexer::T_INPUT_PARAMETER);
2708
2709 161
        return new AST\InputParameter($this->lexer->token['value']);
2710
    }
2711
2712
    /**
2713
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2714
     *
2715
     * @return \Doctrine\ORM\Query\AST\ArithmeticExpression
2716
     */
2717 327
    public function ArithmeticExpression()
2718
    {
2719 327
        $expr = new AST\ArithmeticExpression;
2720
2721 327
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2722 19
            $peek = $this->lexer->glimpse();
2723
2724 19
            if ($peek['type'] === Lexer::T_SELECT) {
2725 7
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2726 7
                $expr->subselect = $this->Subselect();
2727 7
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2728
2729 7
                return $expr;
2730
            }
2731
        }
2732
2733 327
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2734
2735 327
        return $expr;
2736
    }
2737
2738
    /**
2739
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2740
     *
2741
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2742
     */
2743 430
    public function SimpleArithmeticExpression()
2744
    {
2745 430
        $terms = [];
2746 430
        $terms[] = $this->ArithmeticTerm();
2747
2748 430
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2749 21
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2750
2751 21
            $terms[] = $this->lexer->token['value'];
2752 21
            $terms[] = $this->ArithmeticTerm();
2753
        }
2754
2755
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2756
        // if only one AST\ArithmeticTerm is defined
2757 430
        if (count($terms) == 1) {
2758 425
            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 2758 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...
2759
        }
2760
2761 21
        return new AST\SimpleArithmeticExpression($terms);
2762
    }
2763
2764
    /**
2765
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2766
     *
2767
     * @return \Doctrine\ORM\Query\AST\ArithmeticTerm
2768
     */
2769 430
    public function ArithmeticTerm()
2770
    {
2771 430
        $factors = [];
2772 430
        $factors[] = $this->ArithmeticFactor();
2773
2774 430
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2775 53
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2776
2777 53
            $factors[] = $this->lexer->token['value'];
2778 53
            $factors[] = $this->ArithmeticFactor();
2779
        }
2780
2781
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2782
        // if only one AST\ArithmeticFactor is defined
2783 430
        if (count($factors) == 1) {
2784 402
            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 2784 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticTerm of type Doctrine\ORM\Query\AST\ArithmeticTerm|null.
Loading history...
2785
        }
2786
2787 53
        return new AST\ArithmeticTerm($factors);
2788
    }
2789
2790
    /**
2791
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2792
     *
2793
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2794
     */
2795 430
    public function ArithmeticFactor()
2796
    {
2797 430
        $sign = null;
2798
2799 430
        if (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2800 3
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2801 3
            $sign = $isPlus;
2802
        }
2803
2804 430
        $primary = $this->ArithmeticPrimary();
2805
2806
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2807
        // if only one AST\ArithmeticPrimary is defined
2808 430
        if ($sign === null) {
2809 429
            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...
2810
        }
2811
2812 3
        return new AST\ArithmeticFactor($primary, $sign);
2813
    }
2814
2815
    /**
2816
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2817
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2818
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2819
     *          | InputParameter | CaseExpression
2820
     */
2821 434
    public function ArithmeticPrimary()
2822
    {
2823 434
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2824 25
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2825
2826 25
            $expr = $this->SimpleArithmeticExpression();
2827
2828 25
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2829
2830 25
            return new AST\ParenthesisExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr defined by $this->SimpleArithmeticExpression() on line 2826 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...
2831
        }
2832
2833 434
        switch ($this->lexer->lookahead['type']) {
2834 434
            case Lexer::T_COALESCE:
2835 434
            case Lexer::T_NULLIF:
2836 434
            case Lexer::T_CASE:
2837 4
                return $this->CaseExpression();
2838
2839 434
            case Lexer::T_IDENTIFIER:
2840 404
                $peek = $this->lexer->glimpse();
2841
2842 404
                if ($peek['value'] == '(') {
2843 29
                    return $this->FunctionDeclaration();
2844
                }
2845
2846 383
                if ($peek['value'] == '.') {
2847 372
                    return $this->SingleValuedPathExpression();
2848
                }
2849
2850 46
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2851 10
                    return $this->ResultVariable();
2852
                }
2853
2854 38
                return $this->StateFieldPathExpression();
2855
2856 307
            case Lexer::T_INPUT_PARAMETER:
2857 143
                return $this->InputParameter();
2858
2859
            default:
2860 172
                $peek = $this->lexer->glimpse();
2861
2862 172
                if ($peek['value'] == '(') {
2863 18
                    if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2864 18
                        return $this->AggregateExpression();
2865
                    }
2866
2867
                    return $this->FunctionDeclaration();
2868
                }
2869
2870 168
                return $this->Literal();
2871
        }
2872
    }
2873
2874
    /**
2875
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2876
     *
2877
     * @return \Doctrine\ORM\Query\AST\Subselect |
2878
     *         string
2879
     */
2880 14
    public function StringExpression()
2881
    {
2882 14
        $peek = $this->lexer->glimpse();
2883
2884
        // Subselect
2885 14
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
2886
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2887
            $expr = $this->Subselect();
2888
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2889
2890
            return $expr;
2891
        }
2892
2893
        // ResultVariable (string)
2894 14
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2895 14
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2896 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...
2897
        }
2898
2899 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...AST\AggregateExpression) 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...
2900
    }
2901
2902
    /**
2903
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2904
     */
2905 51
    public function StringPrimary()
2906
    {
2907 51
        $lookaheadType = $this->lexer->lookahead['type'];
2908
2909
        switch ($lookaheadType) {
2910 51
            case Lexer::T_IDENTIFIER:
2911 32
                $peek = $this->lexer->glimpse();
2912
2913 32
                if ($peek['value'] == '.') {
2914 32
                    return $this->StateFieldPathExpression();
2915
                }
2916
2917 8
                if ($peek['value'] == '(') {
2918
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2919 8
                    return $this->FunctionDeclaration();
2920
                }
2921
2922
                $this->syntaxError("'.' or '('");
2923
                break;
2924
2925 32
            case Lexer::T_STRING:
2926 32
                $this->match(Lexer::T_STRING);
2927
2928 32
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2929
2930 2
            case Lexer::T_INPUT_PARAMETER:
2931 2
                return $this->InputParameter();
2932
2933
            case Lexer::T_CASE:
2934
            case Lexer::T_COALESCE:
2935
            case Lexer::T_NULLIF:
2936
                return $this->CaseExpression();
2937
2938
            default:
2939
                if ($this->isAggregateFunction($lookaheadType)) {
2940
                    return $this->AggregateExpression();
2941
                }
2942
        }
2943
2944
        $this->syntaxError(
2945
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2946
        );
2947
    }
2948
2949
    /**
2950
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2951
     *
2952
     * @return \Doctrine\ORM\Query\AST\PathExpression |
2953
     *         \Doctrine\ORM\Query\AST\SimpleEntityExpression
2954
     */
2955 7
    public function EntityExpression()
2956
    {
2957 7
        $glimpse = $this->lexer->glimpse();
2958
2959 7
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2960 1
            return $this->SingleValuedAssociationPathExpression();
2961
        }
2962
2963 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 2963 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::EntityExpression of type Doctrine\ORM\Query\AST\PathExpression.
Loading history...
2964
    }
2965
2966
    /**
2967
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2968
     *
2969
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2970
     */
2971 6
    public function SimpleEntityExpression()
2972
    {
2973 6
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2974 5
            return $this->InputParameter();
2975
        }
2976
2977 1
        return $this->StateFieldPathExpression();
2978
    }
2979
2980
    /**
2981
     * AggregateExpression ::=
2982
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2983
     *
2984
     * @return \Doctrine\ORM\Query\AST\AggregateExpression
2985
     */
2986 84
    public function AggregateExpression()
2987
    {
2988 84
        $lookaheadType = $this->lexer->lookahead['type'];
2989 84
        $isDistinct = false;
2990
2991 84
        if ( ! in_array($lookaheadType, [Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM])) {
2992
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2993
        }
2994
2995 84
        $this->match($lookaheadType);
2996 84
        $functionName = $this->lexer->token['value'];
2997 84
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2998
2999 84
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
3000 2
            $this->match(Lexer::T_DISTINCT);
3001 2
            $isDistinct = true;
3002
        }
3003
3004 84
        $pathExp = $this->SimpleArithmeticExpression();
3005
3006 84
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3007
3008 84
        return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
0 ignored issues
show
Bug introduced by
It seems like $pathExp defined by $this->SimpleArithmeticExpression() on line 3004 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...
3009
    }
3010
3011
    /**
3012
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
3013
     *
3014
     * @return \Doctrine\ORM\Query\AST\QuantifiedExpression
3015
     */
3016 3
    public function QuantifiedExpression()
3017
    {
3018 3
        $lookaheadType = $this->lexer->lookahead['type'];
3019 3
        $value = $this->lexer->lookahead['value'];
3020
3021 3
        if ( ! in_array($lookaheadType, [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME])) {
3022
            $this->syntaxError('ALL, ANY or SOME');
3023
        }
3024
3025 3
        $this->match($lookaheadType);
3026 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3027
3028 3
        $qExpr = new AST\QuantifiedExpression($this->Subselect());
3029 3
        $qExpr->type = $value;
3030
3031 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3032
3033 3
        return $qExpr;
3034
    }
3035
3036
    /**
3037
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3038
     *
3039
     * @return \Doctrine\ORM\Query\AST\BetweenExpression
3040
     */
3041 8
    public function BetweenExpression()
3042
    {
3043 8
        $not = false;
3044 8
        $arithExpr1 = $this->ArithmeticExpression();
3045
3046 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3047 3
            $this->match(Lexer::T_NOT);
3048 3
            $not = true;
3049
        }
3050
3051 8
        $this->match(Lexer::T_BETWEEN);
3052 8
        $arithExpr2 = $this->ArithmeticExpression();
3053 8
        $this->match(Lexer::T_AND);
3054 8
        $arithExpr3 = $this->ArithmeticExpression();
3055
3056 8
        $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3057 8
        $betweenExpr->not = $not;
3058
3059 8
        return $betweenExpr;
3060
    }
3061
3062
    /**
3063
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3064
     *
3065
     * @return \Doctrine\ORM\Query\AST\ComparisonExpression
3066
     */
3067 294
    public function ComparisonExpression()
3068
    {
3069 294
        $this->lexer->glimpse();
3070
3071 294
        $leftExpr  = $this->ArithmeticExpression();
3072 294
        $operator  = $this->ComparisonOperator();
3073 294
        $rightExpr = ($this->isNextAllAnySome())
3074 3
            ? $this->QuantifiedExpression()
3075 294
            : $this->ArithmeticExpression();
3076
3077 292
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3078
    }
3079
3080
    /**
3081
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3082
     *
3083
     * @return \Doctrine\ORM\Query\AST\InExpression
3084
     */
3085 34
    public function InExpression()
3086
    {
3087 34
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3088
3089 34
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3090 6
            $this->match(Lexer::T_NOT);
3091 6
            $inExpression->not = true;
3092
        }
3093
3094 34
        $this->match(Lexer::T_IN);
3095 34
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3096
3097 34
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3098 9
            $inExpression->subselect = $this->Subselect();
3099
        } else {
3100 25
            $literals = [];
3101 25
            $literals[] = $this->InParameter();
3102
3103 25
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3104 16
                $this->match(Lexer::T_COMMA);
3105 16
                $literals[] = $this->InParameter();
3106
            }
3107
3108 25
            $inExpression->literals = $literals;
3109
        }
3110
3111 33
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3112
3113 33
        return $inExpression;
3114
    }
3115
3116
    /**
3117
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3118
     *
3119
     * @return \Doctrine\ORM\Query\AST\InstanceOfExpression
3120
     */
3121 12
    public function InstanceOfExpression()
3122
    {
3123 12
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3124
3125 12
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3126 1
            $this->match(Lexer::T_NOT);
3127 1
            $instanceOfExpression->not = true;
3128
        }
3129
3130 12
        $this->match(Lexer::T_INSTANCE);
3131 12
        $this->match(Lexer::T_OF);
3132
3133 12
        $exprValues = [];
3134
3135 12
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3136 1
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3137
3138 1
            $exprValues[] = $this->InstanceOfParameter();
3139
3140 1
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3141 1
                $this->match(Lexer::T_COMMA);
3142
3143 1
                $exprValues[] = $this->InstanceOfParameter();
3144
            }
3145
3146 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3147
3148 1
            $instanceOfExpression->value = $exprValues;
3149
3150 1
            return $instanceOfExpression;
3151
        }
3152
3153 11
        $exprValues[] = $this->InstanceOfParameter();
3154
3155 11
        $instanceOfExpression->value = $exprValues;
3156
3157 11
        return $instanceOfExpression;
3158
    }
3159
3160
    /**
3161
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3162
     *
3163
     * @return mixed
3164
     */
3165 12
    public function InstanceOfParameter()
3166
    {
3167 12
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3168 5
            $this->match(Lexer::T_INPUT_PARAMETER);
3169
3170 5
            return new AST\InputParameter($this->lexer->token['value']);
3171
        }
3172
3173 7
        $abstractSchemaName = $this->AbstractSchemaName();
3174
3175 7
        $this->validateAbstractSchemaName($abstractSchemaName);
3176
3177 7
        return $abstractSchemaName;
3178
    }
3179
3180
    /**
3181
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3182
     *
3183
     * @return \Doctrine\ORM\Query\AST\LikeExpression
3184
     */
3185 14
    public function LikeExpression()
3186
    {
3187 14
        $stringExpr = $this->StringExpression();
3188 14
        $not = false;
3189
3190 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3191 3
            $this->match(Lexer::T_NOT);
3192 3
            $not = true;
3193
        }
3194
3195 14
        $this->match(Lexer::T_LIKE);
3196
3197 14
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3198 4
            $this->match(Lexer::T_INPUT_PARAMETER);
3199 4
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3200
        } else {
3201 11
            $stringPattern = $this->StringPrimary();
3202
        }
3203
3204 14
        $escapeChar = null;
3205
3206 14
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3207 2
            $this->match(Lexer::T_ESCAPE);
3208 2
            $this->match(Lexer::T_STRING);
3209
3210 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3211
        }
3212
3213 14
        $likeExpr = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $stringPattern defined by $this->StringPrimary() on line 3201 can also be of type null or object<Doctrine\ORM\Quer...ST\AggregateExpression> 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...
Bug introduced by
It seems like $stringExpr defined by $this->StringExpression() on line 3187 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...
3214 14
        $likeExpr->not = $not;
3215
3216 14
        return $likeExpr;
3217
    }
3218
3219
    /**
3220
     * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
3221
     *
3222
     * @return \Doctrine\ORM\Query\AST\NullComparisonExpression
3223
     */
3224 14
    public function NullComparisonExpression()
3225
    {
3226
        switch (true) {
3227 14
            case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER):
3228
                $this->match(Lexer::T_INPUT_PARAMETER);
3229
3230
                $expr = new AST\InputParameter($this->lexer->token['value']);
3231
                break;
3232
3233 14
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3234 1
                $expr = $this->NullIfExpression();
3235 1
                break;
3236
3237 14
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3238 1
                $expr = $this->CoalesceExpression();
3239 1
                break;
3240
3241 14
            case $this->isAggregateFunction($this->lexer->lookahead['type']):
3242 1
                $expr = $this->AggregateExpression();
3243 1
                break;
3244
3245 14
            case $this->isFunction():
3246 1
                $expr = $this->FunctionDeclaration();
3247 1
                break;
3248
3249
            default:
3250
                // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
3251 13
                $glimpse = $this->lexer->glimpse();
3252
3253 13
                if ($glimpse['type'] === Lexer::T_DOT) {
3254 9
                    $expr = $this->SingleValuedPathExpression();
3255
3256
                    // Leave switch statement
3257 9
                    break;
3258
                }
3259
3260 4
                $lookaheadValue = $this->lexer->lookahead['value'];
3261
3262
                // Validate existing component
3263 4
                if ( ! isset($this->queryComponents[$lookaheadValue])) {
3264
                    $this->semanticalError('Cannot add having condition on undefined result variable.');
3265
                }
3266
3267
                // Validate SingleValuedPathExpression (ie.: "product")
3268 4
                if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {
3269 1
                    $expr = $this->SingleValuedPathExpression();
3270 1
                    break;
3271
                }
3272
3273
                // Validating ResultVariable
3274 3
                if ( ! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3275
                    $this->semanticalError('Cannot add having condition on a non result variable.');
3276
                }
3277
3278 3
                $expr = $this->ResultVariable();
3279 3
                break;
3280
        }
3281
3282 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...
3283
3284 14
        $this->match(Lexer::T_IS);
3285
3286 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3287 5
            $this->match(Lexer::T_NOT);
3288
3289 5
            $nullCompExpr->not = true;
3290
        }
3291
3292 14
        $this->match(Lexer::T_NULL);
3293
3294 14
        return $nullCompExpr;
3295
    }
3296
3297
    /**
3298
     * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"
3299
     *
3300
     * @return \Doctrine\ORM\Query\AST\ExistsExpression
3301
     */
3302 7
    public function ExistsExpression()
3303
    {
3304 7
        $not = false;
3305
3306 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3307
            $this->match(Lexer::T_NOT);
3308
            $not = true;
3309
        }
3310
3311 7
        $this->match(Lexer::T_EXISTS);
3312 7
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3313
3314 7
        $existsExpression = new AST\ExistsExpression($this->Subselect());
3315 7
        $existsExpression->not = $not;
3316
3317 7
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3318
3319 7
        return $existsExpression;
3320
    }
3321
3322
    /**
3323
     * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!="
3324
     *
3325
     * @return string
3326
     */
3327 294
    public function ComparisonOperator()
3328
    {
3329 294
        switch ($this->lexer->lookahead['value']) {
3330 294
            case '=':
3331 243
                $this->match(Lexer::T_EQUALS);
3332
3333 243
                return '=';
3334
3335 62
            case '<':
3336 17
                $this->match(Lexer::T_LOWER_THAN);
3337 17
                $operator = '<';
3338
3339 17
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3340 5
                    $this->match(Lexer::T_EQUALS);
3341 5
                    $operator .= '=';
3342 12
                } else if ($this->lexer->isNextToken(Lexer::T_GREATER_THAN)) {
3343 3
                    $this->match(Lexer::T_GREATER_THAN);
3344 3
                    $operator .= '>';
3345
                }
3346
3347 17
                return $operator;
3348
3349 53
            case '>':
3350 47
                $this->match(Lexer::T_GREATER_THAN);
3351 47
                $operator = '>';
3352
3353 47
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3354 6
                    $this->match(Lexer::T_EQUALS);
3355 6
                    $operator .= '=';
3356
                }
3357
3358 47
                return $operator;
3359
3360 6
            case '!':
3361 6
                $this->match(Lexer::T_NEGATE);
3362 6
                $this->match(Lexer::T_EQUALS);
3363
3364 6
                return '<>';
3365
3366
            default:
3367
                $this->syntaxError('=, <, <=, <>, >, >=, !=');
3368
        }
3369
    }
3370
3371
    /**
3372
     * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
3373
     *
3374
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3375
     */
3376 59
    public function FunctionDeclaration()
3377
    {
3378 59
        $token = $this->lexer->lookahead;
3379 59
        $funcName = strtolower($token['value']);
3380
3381
        // Check for built-in functions first!
3382
        switch (true) {
3383 59
            case (isset(self::$_STRING_FUNCTIONS[$funcName])):
3384 30
                return $this->FunctionsReturningStrings();
3385
3386 32
            case (isset(self::$_NUMERIC_FUNCTIONS[$funcName])):
3387 23
                return $this->FunctionsReturningNumerics();
3388
3389 10
            case (isset(self::$_DATETIME_FUNCTIONS[$funcName])):
3390 7
                return $this->FunctionsReturningDatetime();
3391
3392
            default:
3393 3
                return $this->CustomFunctionDeclaration();
3394
        }
3395
    }
3396
3397
    /**
3398
     * Helper function for FunctionDeclaration grammar rule.
3399
     *
3400
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3401
     */
3402 3
    private function CustomFunctionDeclaration()
3403
    {
3404 3
        $token = $this->lexer->lookahead;
3405 3
        $funcName = strtolower($token['value']);
3406
3407
        // Check for custom functions afterwards
3408 3
        $config = $this->em->getConfiguration();
3409
3410
        switch (true) {
3411 3
            case ($config->getCustomStringFunction($funcName) !== null):
3412 2
                return $this->CustomFunctionsReturningStrings();
3413
3414 2
            case ($config->getCustomNumericFunction($funcName) !== null):
3415 2
                return $this->CustomFunctionsReturningNumerics();
3416
3417
            case ($config->getCustomDatetimeFunction($funcName) !== null):
3418
                return $this->CustomFunctionsReturningDatetime();
3419
3420
            default:
3421
                $this->syntaxError('known function', $token);
3422
        }
3423
    }
3424
3425
    /**
3426
     * FunctionsReturningNumerics ::=
3427
     *      "LENGTH" "(" StringPrimary ")" |
3428
     *      "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" |
3429
     *      "ABS" "(" SimpleArithmeticExpression ")" |
3430
     *      "SQRT" "(" SimpleArithmeticExpression ")" |
3431
     *      "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3432
     *      "SIZE" "(" CollectionValuedPathExpression ")" |
3433
     *      "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3434
     *      "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3435
     *      "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
3436
     *
3437
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3438
     */
3439 23
    public function FunctionsReturningNumerics()
3440
    {
3441 23
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3442 23
        $funcClass     = self::$_NUMERIC_FUNCTIONS[$funcNameLower];
3443
3444 23
        $function = new $funcClass($funcNameLower);
3445 23
        $function->parse($this);
3446
3447 23
        return $function;
3448
    }
3449
3450
    /**
3451
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3452
     */
3453 2
    public function CustomFunctionsReturningNumerics()
3454
    {
3455
        // getCustomNumericFunction is case-insensitive
3456 2
        $functionName  = strtolower($this->lexer->lookahead['value']);
3457 2
        $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName);
3458
3459 2
        $function = is_string($functionClass)
3460 1
            ? new $functionClass($functionName)
3461 2
            : call_user_func($functionClass, $functionName);
3462
3463 2
        $function->parse($this);
3464
3465 2
        return $function;
3466
    }
3467
3468
    /**
3469
     * FunctionsReturningDateTime ::=
3470
     *     "CURRENT_DATE" |
3471
     *     "CURRENT_TIME" |
3472
     *     "CURRENT_TIMESTAMP" |
3473
     *     "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" |
3474
     *     "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"
3475
     *
3476
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3477
     */
3478 7
    public function FunctionsReturningDatetime()
3479
    {
3480 7
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3481 7
        $funcClass     = self::$_DATETIME_FUNCTIONS[$funcNameLower];
3482
3483 7
        $function = new $funcClass($funcNameLower);
3484 7
        $function->parse($this);
3485
3486 7
        return $function;
3487
    }
3488
3489
    /**
3490
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3491
     */
3492
    public function CustomFunctionsReturningDatetime()
3493
    {
3494
        // getCustomDatetimeFunction is case-insensitive
3495
        $functionName  = $this->lexer->lookahead['value'];
3496
        $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName);
3497
3498
        $function = is_string($functionClass)
3499
            ? new $functionClass($functionName)
3500
            : call_user_func($functionClass, $functionName);
3501
3502
        $function->parse($this);
3503
3504
        return $function;
3505
    }
3506
3507
    /**
3508
     * FunctionsReturningStrings ::=
3509
     *   "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" |
3510
     *   "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3511
     *   "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" |
3512
     *   "LOWER" "(" StringPrimary ")" |
3513
     *   "UPPER" "(" StringPrimary ")" |
3514
     *   "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"
3515
     *
3516
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3517
     */
3518 30
    public function FunctionsReturningStrings()
3519
    {
3520 30
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3521 30
        $funcClass     = self::$_STRING_FUNCTIONS[$funcNameLower];
3522
3523 30
        $function = new $funcClass($funcNameLower);
3524 30
        $function->parse($this);
3525
3526 30
        return $function;
3527
    }
3528
3529
    /**
3530
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3531
     */
3532 2
    public function CustomFunctionsReturningStrings()
3533
    {
3534
        // getCustomStringFunction is case-insensitive
3535 2
        $functionName  = $this->lexer->lookahead['value'];
3536 2
        $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName);
3537
3538 2
        $function = is_string($functionClass)
3539 1
            ? new $functionClass($functionName)
3540 2
            : call_user_func($functionClass, $functionName);
3541
3542 2
        $function->parse($this);
3543
3544 2
        return $function;
3545
    }
3546
}
3547