Completed
Push — master ( e80cd7...256091 )
by Marco
21:52 queued 14:28
created

Parser   F

Complexity

Total Complexity 435

Size/Duplication

Total Lines 3482
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 62

Test Coverage

Coverage 92.83%

Importance

Changes 0
Metric Value
wmc 435
lcom 1
cbo 62
dl 0
loc 3482
ccs 1256
cts 1353
cp 0.9283
rs 0.5217
c 0
b 0
f 0

113 Methods

Rating   Name   Duplication   Size   Complexity  
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
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 73 18
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 120 25
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 48 11
B StringExpression() 0 21 5
D StringPrimary() 0 38 9
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
C NullComparisonExpression() 0 68 10
A ExistsExpression() 0 19 2
C ComparisonOperator() 0 43 8
B FunctionDeclaration() 0 25 5
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
B processDeferredResultVariables() 0 29 5

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
        // Aggregate functions
70
        'min'       => Functions\MinFunction::class,
71
        'max'       => Functions\MaxFunction::class,
72
        'avg'       => Functions\AvgFunction::class,
73
        'sum'       => Functions\SumFunction::class,
74
        'count'     => Functions\CountFunction::class,
75
    ];
76
77
    /**
78
     * READ-ONLY: Maps BUILT-IN datetime function names to AST class names.
79
     *
80
     * @var array
81
     */
82
    private static $_DATETIME_FUNCTIONS = [
83
        'current_date'      => Functions\CurrentDateFunction::class,
84
        'current_time'      => Functions\CurrentTimeFunction::class,
85
        'current_timestamp' => Functions\CurrentTimestampFunction::class,
86
        'date_add'          => Functions\DateAddFunction::class,
87
        'date_sub'          => Functions\DateSubFunction::class,
88
    ];
89
90
    /*
91
     * Expressions that were encountered during parsing of identifiers and expressions
92
     * and still need to be validated.
93
     */
94
95
    /**
96
     * @var array
97
     */
98
    private $deferredIdentificationVariables = [];
99
100
    /**
101
     * @var array
102
     */
103
    private $deferredPartialObjectExpressions = [];
104
105
    /**
106
     * @var array
107
     */
108
    private $deferredPathExpressions = [];
109
110
    /**
111
     * @var array
112
     */
113
    private $deferredResultVariables = [];
114
115
    /**
116
     * @var array
117
     */
118
    private $deferredNewObjectExpressions = [];
119
120
    /**
121
     * The lexer.
122
     *
123
     * @var \Doctrine\ORM\Query\Lexer
124
     */
125
    private $lexer;
126
127
    /**
128
     * The parser result.
129
     *
130
     * @var \Doctrine\ORM\Query\ParserResult
131
     */
132
    private $parserResult;
133
134
    /**
135
     * The EntityManager.
136
     *
137
     * @var \Doctrine\ORM\EntityManager
138
     */
139
    private $em;
140
141
    /**
142
     * The Query to parse.
143
     *
144
     * @var Query
145
     */
146
    private $query;
147
148
    /**
149
     * Map of declared query components in the parsed query.
150
     *
151
     * @var array
152
     */
153
    private $queryComponents = [];
154
155
    /**
156
     * Keeps the nesting level of defined ResultVariables.
157
     *
158
     * @var integer
159
     */
160
    private $nestingLevel = 0;
161
162
    /**
163
     * Any additional custom tree walkers that modify the AST.
164
     *
165
     * @var array
166
     */
167
    private $customTreeWalkers = [];
168
169
    /**
170
     * The custom last tree walker, if any, that is responsible for producing the output.
171
     *
172
     * @var TreeWalker
173
     */
174
    private $customOutputWalker;
175
176
    /**
177
     * @var array
178
     */
179
    private $identVariableExpressions = [];
180
181
    /**
182
     * Creates a new query parser object.
183
     *
184
     * @param Query $query The Query to parse.
185
     */
186 827
    public function __construct(Query $query)
187
    {
188 827
        $this->query        = $query;
189 827
        $this->em           = $query->getEntityManager();
190 827
        $this->lexer        = new Lexer($query->getDQL());
191 827
        $this->parserResult = new ParserResult();
192 827
    }
193
194
    /**
195
     * Sets a custom tree walker that produces output.
196
     * This tree walker will be run last over the AST, after any other walkers.
197
     *
198
     * @param string $className
199
     *
200
     * @return void
201
     */
202 127
    public function setCustomOutputTreeWalker($className)
203
    {
204 127
        $this->customOutputWalker = $className;
0 ignored issues
show
Documentation Bug introduced by
It seems like $className of type string is incompatible with the declared type object<Doctrine\ORM\Query\TreeWalker> of property $customOutputWalker.

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

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

Loading history...
205 127
    }
206
207
    /**
208
     * Adds a custom tree walker for modifying the AST.
209
     *
210
     * @param string $className
211
     *
212
     * @return void
213
     */
214
    public function addCustomTreeWalker($className)
215
    {
216
        $this->customTreeWalkers[] = $className;
217
    }
218
219
    /**
220
     * Gets the lexer used by the parser.
221
     *
222
     * @return \Doctrine\ORM\Query\Lexer
223
     */
224 28
    public function getLexer()
225
    {
226 28
        return $this->lexer;
227
    }
228
229
    /**
230
     * Gets the ParserResult that is being filled with information during parsing.
231
     *
232
     * @return \Doctrine\ORM\Query\ParserResult
233
     */
234
    public function getParserResult()
235
    {
236
        return $this->parserResult;
237
    }
238
239
    /**
240
     * Gets the EntityManager used by the parser.
241
     *
242
     * @return \Doctrine\ORM\EntityManager
243
     */
244
    public function getEntityManager()
245
    {
246
        return $this->em;
247
    }
248
249
    /**
250
     * Parses and builds AST for the given Query.
251
     *
252
     * @return \Doctrine\ORM\Query\AST\SelectStatement |
253
     *         \Doctrine\ORM\Query\AST\UpdateStatement |
254
     *         \Doctrine\ORM\Query\AST\DeleteStatement
255
     */
256 827
    public function getAST()
257
    {
258
        // Parse & build AST
259 827
        $AST = $this->QueryLanguage();
260
261
        // Process any deferred validations of some nodes in the AST.
262
        // This also allows post-processing of the AST for modification purposes.
263 787
        $this->processDeferredIdentificationVariables();
264
265 785
        if ($this->deferredPartialObjectExpressions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->deferredPartialObjectExpressions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

Loading history...
278 28
            $this->processDeferredNewObjectExpressions($AST);
0 ignored issues
show
Documentation introduced by
$AST is of type object<Doctrine\ORM\Query\AST\SelectStatement>, but the function expects a object<Doctrine\ORM\Query\AST\SelectClause>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

Loading history...
345
        $this->lexer->lookahead = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $lookahead.

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

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

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

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

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

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

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

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

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

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

    return array();
}

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

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

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

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

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

An additional type check may prevent trouble.

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

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
1563
        }
1564
1565 12
        return $this->ArithmeticExpression();
1566
    }
1567
1568
    /**
1569
     * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*
1570
     *
1571
     * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1572
     */
1573 750
    public function IdentificationVariableDeclaration()
1574
    {
1575 750
        $joins                    = [];
1576 750
        $rangeVariableDeclaration = $this->RangeVariableDeclaration();
1577 734
        $indexBy                  = $this->lexer->isNextToken(Lexer::T_INDEX)
1578 7
            ? $this->IndexBy()
1579 734
            : null;
1580
1581 734
        $rangeVariableDeclaration->isRoot = true;
1582
1583
        while (
1584 734
            $this->lexer->isNextToken(Lexer::T_LEFT) ||
1585 734
            $this->lexer->isNextToken(Lexer::T_INNER) ||
1586 733
            $this->lexer->isNextToken(Lexer::T_JOIN)
1587
        ) {
1588 276
            $joins[] = $this->Join();
1589
        }
1590
1591 732
        return new AST\IdentificationVariableDeclaration(
1592 732
            $rangeVariableDeclaration, $indexBy, $joins
1593
        );
1594
    }
1595
1596
    /**
1597
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
1598
     *
1599
     * {Internal note: WARNING: Solution is harder than a bare implementation.
1600
     * Desired EBNF support:
1601
     *
1602
     * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)
1603
     *
1604
     * It demands that entire SQL generation to become programmatical. This is
1605
     * needed because association based subselect requires "WHERE" conditional
1606
     * expressions to be injected, but there is no scope to do that. Only scope
1607
     * accessible is "FROM", prohibiting an easy implementation without larger
1608
     * changes.}
1609
     *
1610
     * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration |
1611
     *         \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration
1612
     */
1613 48
    public function SubselectIdentificationVariableDeclaration()
1614
    {
1615
        /*
1616
        NOT YET IMPLEMENTED!
1617
1618
        $glimpse = $this->lexer->glimpse();
1619
1620
        if ($glimpse['type'] == Lexer::T_DOT) {
1621
            $associationPathExpression = $this->AssociationPathExpression();
1622
1623
            if ($this->lexer->isNextToken(Lexer::T_AS)) {
1624
                $this->match(Lexer::T_AS);
1625
            }
1626
1627
            $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1628
            $identificationVariable      = $associationPathExpression->identificationVariable;
1629
            $field                       = $associationPathExpression->associationField;
1630
1631
            $class       = $this->queryComponents[$identificationVariable]['metadata'];
1632
            $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1633
1634
            // Building queryComponent
1635
            $joinQueryComponent = array(
1636
                'metadata'     => $targetClass,
1637
                'parent'       => $identificationVariable,
1638
                'relation'     => $class->getAssociationMapping($field),
1639
                'map'          => null,
1640
                'nestingLevel' => $this->nestingLevel,
1641
                'token'        => $this->lexer->lookahead
1642
            );
1643
1644
            $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1645
1646
            return new AST\SubselectIdentificationVariableDeclaration(
1647
                $associationPathExpression, $aliasIdentificationVariable
1648
            );
1649
        }
1650
        */
1651
1652 48
        return $this->IdentificationVariableDeclaration();
1653
    }
1654
1655
    /**
1656
     * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
1657
     *          (JoinAssociationDeclaration | RangeVariableDeclaration)
1658
     *          ["WITH" ConditionalExpression]
1659
     *
1660
     * @return \Doctrine\ORM\Query\AST\Join
1661
     */
1662 276
    public function Join()
1663
    {
1664
        // Check Join type
1665 276
        $joinType = AST\Join::JOIN_TYPE_INNER;
1666
1667
        switch (true) {
1668 276
            case ($this->lexer->isNextToken(Lexer::T_LEFT)):
1669 67
                $this->match(Lexer::T_LEFT);
1670
1671 67
                $joinType = AST\Join::JOIN_TYPE_LEFT;
1672
1673
                // Possible LEFT OUTER join
1674 67
                if ($this->lexer->isNextToken(Lexer::T_OUTER)) {
1675
                    $this->match(Lexer::T_OUTER);
1676
1677
                    $joinType = AST\Join::JOIN_TYPE_LEFTOUTER;
1678
                }
1679 67
                break;
1680
1681 213
            case ($this->lexer->isNextToken(Lexer::T_INNER)):
1682 20
                $this->match(Lexer::T_INNER);
1683 20
                break;
1684
1685
            default:
1686
                // Do nothing
1687
        }
1688
1689 276
        $this->match(Lexer::T_JOIN);
1690
1691 276
        $next            = $this->lexer->glimpse();
1692 276
        $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
1693 274
        $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH);
1694 274
        $join            = new AST\Join($joinType, $joinDeclaration);
1695
1696
        // Describe non-root join declaration
1697 274
        if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
1698 21
            $joinDeclaration->isRoot = false;
1699
        }
1700
1701
        // Check for ad-hoc Join conditions
1702 274
        if ($adhocConditions) {
1703 23
            $this->match(Lexer::T_WITH);
1704
1705 23
            $join->conditionalExpression = $this->ConditionalExpression();
1706
        }
1707
1708 274
        return $join;
1709
    }
1710
1711
    /**
1712
     * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
1713
     *
1714
     * @return \Doctrine\ORM\Query\AST\RangeVariableDeclaration
1715
     */
1716 750
    public function RangeVariableDeclaration()
1717
    {
1718 750
        $abstractSchemaName = $this->AbstractSchemaName();
1719
1720 749
        $this->validateAbstractSchemaName($abstractSchemaName);
1721
1722 734
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1723 5
            $this->match(Lexer::T_AS);
1724
        }
1725
1726 734
        $token = $this->lexer->lookahead;
1727 734
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1728 734
        $classMetadata = $this->em->getClassMetadata($abstractSchemaName);
1729
1730
        // Building queryComponent
1731
        $queryComponent = [
1732 734
            'metadata'     => $classMetadata,
1733
            'parent'       => null,
1734
            'relation'     => null,
1735
            'map'          => null,
1736 734
            'nestingLevel' => $this->nestingLevel,
1737 734
            'token'        => $token
1738
        ];
1739
1740 734
        $this->queryComponents[$aliasIdentificationVariable] = $queryComponent;
1741
1742 734
        return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);
1743
    }
1744
1745
    /**
1746
     * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
1747
     *
1748
     * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression
1749
     */
1750 255
    public function JoinAssociationDeclaration()
1751
    {
1752 255
        $joinAssociationPathExpression = $this->JoinAssociationPathExpression();
1753
1754 255
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
1755 4
            $this->match(Lexer::T_AS);
1756
        }
1757
1758 255
        $aliasIdentificationVariable = $this->AliasIdentificationVariable();
1759 253
        $indexBy                     = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null;
1760
1761 253
        $identificationVariable = $joinAssociationPathExpression->identificationVariable;
1762 253
        $field                  = $joinAssociationPathExpression->associationField;
1763
1764 253
        $class       = $this->queryComponents[$identificationVariable]['metadata'];
1765 253
        $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);
1766
1767
        // Building queryComponent
1768
        $joinQueryComponent = [
1769 253
            'metadata'     => $targetClass,
1770 253
            'parent'       => $joinAssociationPathExpression->identificationVariable,
1771 253
            'relation'     => $class->getAssociationMapping($field),
1772
            'map'          => null,
1773 253
            'nestingLevel' => $this->nestingLevel,
1774 253
            'token'        => $this->lexer->lookahead
1775
        ];
1776
1777 253
        $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
1778
1779 253
        return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);
1780
    }
1781
1782
    /**
1783
     * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1784
     * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1785
     *
1786
     * @return \Doctrine\ORM\Query\AST\PartialObjectExpression
1787
     */
1788 11
    public function PartialObjectExpression()
1789
    {
1790 11
        $this->match(Lexer::T_PARTIAL);
1791
1792 11
        $partialFieldSet = [];
1793
1794 11
        $identificationVariable = $this->IdentificationVariable();
1795
1796 11
        $this->match(Lexer::T_DOT);
1797 11
        $this->match(Lexer::T_OPEN_CURLY_BRACE);
1798 11
        $this->match(Lexer::T_IDENTIFIER);
1799
1800 11
        $field = $this->lexer->token['value'];
1801
1802
        // First field in partial expression might be embeddable property
1803 11
        while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1804 1
            $this->match(Lexer::T_DOT);
1805 1
            $this->match(Lexer::T_IDENTIFIER);
1806 1
            $field .= '.'.$this->lexer->token['value'];
1807
        }
1808
1809 11
        $partialFieldSet[] = $field;
1810
1811 11
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1812 9
            $this->match(Lexer::T_COMMA);
1813 9
            $this->match(Lexer::T_IDENTIFIER);
1814
1815 9
            $field = $this->lexer->token['value'];
1816
1817 9
            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
1818 2
                $this->match(Lexer::T_DOT);
1819 2
                $this->match(Lexer::T_IDENTIFIER);
1820 2
                $field .= '.'.$this->lexer->token['value'];
1821
            }
1822
1823 9
            $partialFieldSet[] = $field;
1824
        }
1825
1826 11
        $this->match(Lexer::T_CLOSE_CURLY_BRACE);
1827
1828 11
        $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);
1829
1830
        // Defer PartialObjectExpression validation
1831 11
        $this->deferredPartialObjectExpressions[] = [
1832 11
            'expression'   => $partialObjectExpression,
1833 11
            'nestingLevel' => $this->nestingLevel,
1834 11
            'token'        => $this->lexer->token,
1835
        ];
1836
1837 11
        return $partialObjectExpression;
1838
    }
1839
1840
    /**
1841
     * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1842
     *
1843
     * @return \Doctrine\ORM\Query\AST\NewObjectExpression
1844
     */
1845 28
    public function NewObjectExpression()
1846
    {
1847 28
        $this->match(Lexer::T_NEW);
1848
1849 28
        $className = $this->AbstractSchemaName(); // note that this is not yet validated
1850 28
        $token = $this->lexer->token;
1851
1852 28
        $this->match(Lexer::T_OPEN_PARENTHESIS);
1853
1854 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...
1855
1856 28
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
1857 24
            $this->match(Lexer::T_COMMA);
1858
1859 24
            $args[] = $this->NewObjectArg();
1860
        }
1861
1862 28
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
1863
1864 28
        $expression = new AST\NewObjectExpression($className, $args);
1865
1866
        // Defer NewObjectExpression validation
1867 28
        $this->deferredNewObjectExpressions[] = [
1868 28
            'token'        => $token,
1869 28
            'expression'   => $expression,
1870 28
            'nestingLevel' => $this->nestingLevel,
1871
        ];
1872
1873 28
        return $expression;
1874
    }
1875
1876
    /**
1877
     * NewObjectArg ::= ScalarExpression | "(" Subselect ")"
1878
     *
1879
     * @return mixed
1880
     */
1881 28
    public function NewObjectArg()
1882
    {
1883 28
        $token = $this->lexer->lookahead;
1884 28
        $peek  = $this->lexer->glimpse();
1885
1886 28
        if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) {
1887 2
            $this->match(Lexer::T_OPEN_PARENTHESIS);
1888 2
            $expression = $this->Subselect();
1889 2
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
1890
1891 2
            return $expression;
1892
        }
1893
1894 28
        return $this->ScalarExpression();
1895
    }
1896
1897
    /**
1898
     * IndexBy ::= "INDEX" "BY" StateFieldPathExpression
1899
     *
1900
     * @return \Doctrine\ORM\Query\AST\IndexBy
1901
     */
1902 11
    public function IndexBy()
1903
    {
1904 11
        $this->match(Lexer::T_INDEX);
1905 11
        $this->match(Lexer::T_BY);
1906 11
        $pathExpr = $this->StateFieldPathExpression();
1907
1908
        // Add the INDEX BY info to the query component
1909 11
        $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;
1910
1911 11
        return new AST\IndexBy($pathExpr);
1912
    }
1913
1914
    /**
1915
     * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
1916
     *                      StateFieldPathExpression | BooleanPrimary | CaseExpression |
1917
     *                      InstanceOfExpression
1918
     *
1919
     * @return mixed One of the possible expressions or subexpressions.
1920
     */
1921 160
    public function ScalarExpression()
1922
    {
1923 160
        $lookahead = $this->lexer->lookahead['type'];
1924 160
        $peek      = $this->lexer->glimpse();
1925
1926
        switch (true) {
1927 160
            case ($lookahead === Lexer::T_INTEGER):
1928 157
            case ($lookahead === Lexer::T_FLOAT):
1929
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )  or ( - 1 ) or ( + 1 )
1930 157
            case ($lookahead === Lexer::T_MINUS):
1931 157
            case ($lookahead === Lexer::T_PLUS):
1932 17
                return $this->SimpleArithmeticExpression();
1933
1934 157
            case ($lookahead === Lexer::T_STRING):
1935 13
                return $this->StringPrimary();
1936
1937 155
            case ($lookahead === Lexer::T_TRUE):
1938 155
            case ($lookahead === Lexer::T_FALSE):
1939 3
                $this->match($lookahead);
1940
1941 3
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
1942
1943 155
            case ($lookahead === Lexer::T_INPUT_PARAMETER):
1944
                switch (true) {
1945 1
                    case $this->isMathOperator($peek):
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->glimpse() on line 1924 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...
1946
                        // :param + u.value
1947 1
                        return $this->SimpleArithmeticExpression();
1948
                    default:
1949
                        return $this->InputParameter();
1950
                }
1951
1952 155
            case ($lookahead === Lexer::T_CASE):
1953 151
            case ($lookahead === Lexer::T_COALESCE):
1954 151
            case ($lookahead === Lexer::T_NULLIF):
1955
                // Since NULLIF and COALESCE can be identified as a function,
1956
                // we need to check these before checking for FunctionDeclaration
1957 8
                return $this->CaseExpression();
1958
1959 151
            case ($lookahead === Lexer::T_OPEN_PARENTHESIS):
1960 4
                return $this->SimpleArithmeticExpression();
1961
1962
            // this check must be done before checking for a filed path expression
1963 148
            case ($this->isFunction()):
1964 26
                $this->lexer->peek(); // "("
1965
1966
                switch (true) {
1967 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...
1968
                        // SUM(u.id) + COUNT(u.id)
1969 7
                        return $this->SimpleArithmeticExpression();
1970
1971
                    default:
1972
                        // IDENTITY(u)
1973 21
                        return $this->FunctionDeclaration();
1974
                }
1975
1976
                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...
1977
            // it is no function, so it must be a field path
1978 130
            case ($lookahead === Lexer::T_IDENTIFIER):
1979 130
                $this->lexer->peek(); // lookahead => '.'
1980 130
                $this->lexer->peek(); // lookahead => token after '.'
1981 130
                $peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1982 130
                $this->lexer->resetPeek();
1983
1984 130
                if ($this->isMathOperator($peek)) {
0 ignored issues
show
Bug introduced by
It seems like $peek defined by $this->lexer->peek() on line 1981 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...
1985 7
                    return $this->SimpleArithmeticExpression();
1986
                }
1987
1988 125
                return $this->StateFieldPathExpression();
1989
1990
            default:
1991
                $this->syntaxError();
1992
        }
1993
    }
1994
1995
    /**
1996
     * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
1997
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
1998
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
1999
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2000
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2001
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2002
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2003
     * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2004
     *
2005
     * @return mixed One of the possible expressions or subexpressions.
2006
     */
2007 19
    public function CaseExpression()
2008
    {
2009 19
        $lookahead = $this->lexer->lookahead['type'];
2010
2011
        switch ($lookahead) {
2012 19
            case Lexer::T_NULLIF:
2013 5
                return $this->NullIfExpression();
2014
2015 16
            case Lexer::T_COALESCE:
2016 2
                return $this->CoalesceExpression();
2017
2018 14
            case Lexer::T_CASE:
2019 14
                $this->lexer->resetPeek();
2020 14
                $peek = $this->lexer->peek();
2021
2022 14
                if ($peek['type'] === Lexer::T_WHEN) {
2023 9
                    return $this->GeneralCaseExpression();
2024
                }
2025
2026 5
                return $this->SimpleCaseExpression();
2027
2028
            default:
2029
                // Do nothing
2030
                break;
2031
        }
2032
2033
        $this->syntaxError();
2034
    }
2035
2036
    /**
2037
     * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
2038
     *
2039
     * @return \Doctrine\ORM\Query\AST\CoalesceExpression
2040
     */
2041 3
    public function CoalesceExpression()
2042
    {
2043 3
        $this->match(Lexer::T_COALESCE);
2044 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2045
2046
        // Process ScalarExpressions (1..N)
2047 3
        $scalarExpressions = [];
2048 3
        $scalarExpressions[] = $this->ScalarExpression();
2049
2050 3
        while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
2051 3
            $this->match(Lexer::T_COMMA);
2052
2053 3
            $scalarExpressions[] = $this->ScalarExpression();
2054
        }
2055
2056 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2057
2058 3
        return new AST\CoalesceExpression($scalarExpressions);
2059
    }
2060
2061
    /**
2062
     * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
2063
     *
2064
     * @return \Doctrine\ORM\Query\AST\NullIfExpression
2065
     */
2066 5
    public function NullIfExpression()
2067
    {
2068 5
        $this->match(Lexer::T_NULLIF);
2069 5
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2070
2071 5
        $firstExpression = $this->ScalarExpression();
2072 5
        $this->match(Lexer::T_COMMA);
2073 5
        $secondExpression = $this->ScalarExpression();
2074
2075 5
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2076
2077 5
        return new AST\NullIfExpression($firstExpression, $secondExpression);
2078
    }
2079
2080
    /**
2081
     * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
2082
     *
2083
     * @return \Doctrine\ORM\Query\AST\GeneralCaseExpression
2084
     */
2085 9
    public function GeneralCaseExpression()
2086
    {
2087 9
        $this->match(Lexer::T_CASE);
2088
2089
        // Process WhenClause (1..N)
2090 9
        $whenClauses = [];
2091
2092
        do {
2093 9
            $whenClauses[] = $this->WhenClause();
2094 9
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2095
2096 9
        $this->match(Lexer::T_ELSE);
2097 9
        $scalarExpression = $this->ScalarExpression();
2098 9
        $this->match(Lexer::T_END);
2099
2100 9
        return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);
2101
    }
2102
2103
    /**
2104
     * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
2105
     * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
2106
     *
2107
     * @return AST\SimpleCaseExpression
2108
     */
2109 5
    public function SimpleCaseExpression()
2110
    {
2111 5
        $this->match(Lexer::T_CASE);
2112 5
        $caseOperand = $this->StateFieldPathExpression();
2113
2114
        // Process SimpleWhenClause (1..N)
2115 5
        $simpleWhenClauses = [];
2116
2117
        do {
2118 5
            $simpleWhenClauses[] = $this->SimpleWhenClause();
2119 5
        } while ($this->lexer->isNextToken(Lexer::T_WHEN));
2120
2121 5
        $this->match(Lexer::T_ELSE);
2122 5
        $scalarExpression = $this->ScalarExpression();
2123 5
        $this->match(Lexer::T_END);
2124
2125 5
        return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);
2126
    }
2127
2128
    /**
2129
     * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
2130
     *
2131
     * @return \Doctrine\ORM\Query\AST\WhenClause
2132
     */
2133 9
    public function WhenClause()
2134
    {
2135 9
        $this->match(Lexer::T_WHEN);
2136 9
        $conditionalExpression = $this->ConditionalExpression();
2137 9
        $this->match(Lexer::T_THEN);
2138
2139 9
        return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());
2140
    }
2141
2142
    /**
2143
     * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
2144
     *
2145
     * @return \Doctrine\ORM\Query\AST\SimpleWhenClause
2146
     */
2147 5
    public function SimpleWhenClause()
2148
    {
2149 5
        $this->match(Lexer::T_WHEN);
2150 5
        $conditionalExpression = $this->ScalarExpression();
2151 5
        $this->match(Lexer::T_THEN);
2152
2153 5
        return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());
2154
    }
2155
2156
    /**
2157
     * SelectExpression ::= (
2158
     *     IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |
2159
     *     PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression
2160
     * ) [["AS"] ["HIDDEN"] AliasResultVariable]
2161
     *
2162
     * @return \Doctrine\ORM\Query\AST\SelectExpression
2163
     */
2164 762
    public function SelectExpression()
2165
    {
2166 762
        $expression    = null;
2167 762
        $identVariable = null;
2168 762
        $peek          = $this->lexer->glimpse();
2169 762
        $lookaheadType = $this->lexer->lookahead['type'];
2170
2171
        switch (true) {
2172
            // ScalarExpression (u.name)
2173 762
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT):
2174 102
                $expression = $this->ScalarExpression();
2175 102
                break;
2176
2177
            // IdentificationVariable (u)
2178 703
            case ($lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2179 587
                $expression = $identVariable = $this->IdentificationVariable();
2180 587
                break;
2181
2182
            // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))
2183 177
            case ($lookaheadType === Lexer::T_CASE):
2184 172
            case ($lookaheadType === Lexer::T_COALESCE):
2185 170
            case ($lookaheadType === Lexer::T_NULLIF):
2186 9
                $expression = $this->CaseExpression();
2187 9
                break;
2188
2189
            // DQL Function (SUM(u.value) or SUM(u.value) + 1)
2190 168
            case ($this->isFunction()):
2191 88
                $this->lexer->peek(); // "("
2192
2193
                switch (true) {
2194 88
                    case ($this->isMathOperator($this->peekBeyondClosingParenthesis())):
0 ignored issues
show
Bug introduced by
It seems like $this->peekBeyondClosingParenthesis() targeting Doctrine\ORM\Query\Parse...ondClosingParenthesis() can also be of type null; however, Doctrine\ORM\Query\Parser::isMathOperator() does only seem to accept array, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

Loading history...
2195
                        // SUM(u.id) + COUNT(u.id)
2196 2
                        $expression = $this->ScalarExpression();
2197 2
                        break;
2198
2199
                    default:
2200
                        // IDENTITY(u)
2201 86
                        $expression = $this->FunctionDeclaration();
2202 86
                        break;
2203
                }
2204
2205 88
                break;
2206
2207
            // PartialObjectExpression (PARTIAL u.{id, name})
2208 80
            case ($lookaheadType === Lexer::T_PARTIAL):
2209 11
                $expression    = $this->PartialObjectExpression();
2210 11
                $identVariable = $expression->identificationVariable;
2211 11
                break;
2212
2213
            // Subselect
2214 69
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT):
2215 22
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2216 22
                $expression = $this->Subselect();
2217 22
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2218 22
                break;
2219
2220
            // Shortcut: ScalarExpression => SimpleArithmeticExpression
2221 47
            case ($lookaheadType === Lexer::T_OPEN_PARENTHESIS):
2222 43
            case ($lookaheadType === Lexer::T_INTEGER):
2223 41
            case ($lookaheadType === Lexer::T_STRING):
2224 32
            case ($lookaheadType === Lexer::T_FLOAT):
2225
            // SimpleArithmeticExpression : (- u.value ) or ( + u.value )
2226 32
            case ($lookaheadType === Lexer::T_MINUS):
2227 32
            case ($lookaheadType === Lexer::T_PLUS):
2228 16
                $expression = $this->SimpleArithmeticExpression();
2229 16
                break;
2230
2231
            // NewObjectExpression (New ClassName(id, name))
2232 31
            case ($lookaheadType === Lexer::T_NEW):
2233 28
                $expression = $this->NewObjectExpression();
2234 28
                break;
2235
2236
            default:
2237 3
                $this->syntaxError(
2238 3
                    'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',
2239 3
                    $this->lexer->lookahead
2240
                );
2241
        }
2242
2243
        // [["AS"] ["HIDDEN"] AliasResultVariable]
2244 759
        $mustHaveAliasResultVariable = false;
2245
2246 759
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2247 110
            $this->match(Lexer::T_AS);
2248
2249 110
            $mustHaveAliasResultVariable = true;
2250
        }
2251
2252 759
        $hiddenAliasResultVariable = false;
2253
2254 759
        if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) {
2255 10
            $this->match(Lexer::T_HIDDEN);
2256
2257 10
            $hiddenAliasResultVariable = true;
2258
        }
2259
2260 759
        $aliasResultVariable = null;
2261
2262 759
        if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2263 119
            $token = $this->lexer->lookahead;
2264 119
            $aliasResultVariable = $this->AliasResultVariable();
2265
2266
            // Include AliasResultVariable in query components.
2267 114
            $this->queryComponents[$aliasResultVariable] = [
2268 114
                'resultVariable' => $expression,
2269 114
                'nestingLevel'   => $this->nestingLevel,
2270 114
                'token'          => $token,
2271
            ];
2272
        }
2273
2274
        // AST
2275
2276 754
        $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
2277
2278 754
        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...
2279 595
            $this->identVariableExpressions[$identVariable] = $expr;
2280
        }
2281
2282 754
        return $expr;
2283
    }
2284
2285
    /**
2286
     * SimpleSelectExpression ::= (
2287
     *      StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |
2288
     *      AggregateExpression | "(" Subselect ")" | ScalarExpression
2289
     * ) [["AS"] AliasResultVariable]
2290
     *
2291
     * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression
2292
     */
2293 48
    public function SimpleSelectExpression()
2294
    {
2295 48
        $peek = $this->lexer->glimpse();
2296
2297 48
        switch ($this->lexer->lookahead['type']) {
2298 48
            case Lexer::T_IDENTIFIER:
2299
                switch (true) {
2300 19
                    case ($peek['type'] === Lexer::T_DOT):
2301 16
                        $expression = $this->StateFieldPathExpression();
2302
2303 16
                        return new AST\SimpleSelectExpression($expression);
2304
2305 3
                    case ($peek['type'] !== Lexer::T_OPEN_PARENTHESIS):
2306 2
                        $expression = $this->IdentificationVariable();
2307
2308 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...
2309
2310 1
                    case ($this->isFunction()):
2311
                        // SUM(u.id) + COUNT(u.id)
2312 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...
2313
                            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...
2314
                        }
2315
                        // COUNT(u.id)
2316 1
                        if ($this->isAggregateFunction($this->lexer->lookahead['type'])) {
2317
                            return new AST\SimpleSelectExpression($this->AggregateExpression());
2318
                        }
2319
                        // IDENTITY(u)
2320 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...
2321
2322
                    default:
2323
                        // Do nothing
2324
                }
2325
                break;
2326
2327 30
            case Lexer::T_OPEN_PARENTHESIS:
2328 3
                if ($peek['type'] !== Lexer::T_SELECT) {
2329
                    // Shortcut: ScalarExpression => SimpleArithmeticExpression
2330 3
                    $expression = $this->SimpleArithmeticExpression();
2331
2332 3
                    return new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->SimpleArithmeticExpression() on line 2330 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...
2333
                }
2334
2335
                // Subselect
2336
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2337
                $expression = $this->Subselect();
2338
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2339
2340
                return new AST\SimpleSelectExpression($expression);
2341
2342
            default:
2343
                // Do nothing
2344
        }
2345
2346 27
        $this->lexer->peek();
2347
2348 27
        $expression = $this->ScalarExpression();
2349 27
        $expr       = new AST\SimpleSelectExpression($expression);
0 ignored issues
show
Bug introduced by
It seems like $expression defined by $this->ScalarExpression() on line 2348 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...
2350
2351 27
        if ($this->lexer->isNextToken(Lexer::T_AS)) {
2352 1
            $this->match(Lexer::T_AS);
2353
        }
2354
2355 27
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
2356 2
            $token = $this->lexer->lookahead;
2357 2
            $resultVariable = $this->AliasResultVariable();
2358 2
            $expr->fieldIdentificationVariable = $resultVariable;
2359
2360
            // Include AliasResultVariable in query components.
2361 2
            $this->queryComponents[$resultVariable] = [
2362 2
                'resultvariable' => $expr,
2363 2
                'nestingLevel'   => $this->nestingLevel,
2364 2
                'token'          => $token,
2365
            ];
2366
        }
2367
2368 27
        return $expr;
2369
    }
2370
2371
    /**
2372
     * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*
2373
     *
2374
     * @return \Doctrine\ORM\Query\AST\ConditionalExpression
2375
     */
2376 375
    public function ConditionalExpression()
2377
    {
2378 375
        $conditionalTerms = [];
2379 375
        $conditionalTerms[] = $this->ConditionalTerm();
2380
2381 372
        while ($this->lexer->isNextToken(Lexer::T_OR)) {
2382 16
            $this->match(Lexer::T_OR);
2383
2384 16
            $conditionalTerms[] = $this->ConditionalTerm();
2385
        }
2386
2387
        // Phase 1 AST optimization: Prevent AST\ConditionalExpression
2388
        // if only one AST\ConditionalTerm is defined
2389 372
        if (count($conditionalTerms) == 1) {
2390 364
            return $conditionalTerms[0];
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $conditionalTerms[0]; (Doctrine\ORM\Query\AST\ConditionalTerm) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ConditionalExpression of type Doctrine\ORM\Query\AST\ConditionalExpression.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

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

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

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

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
2442
        }
2443
2444 6
        $conditionalFactor = new AST\ConditionalFactor($conditionalPrimary);
2445 6
        $conditionalFactor->not = $not;
2446
2447 6
        return $conditionalFactor;
2448
    }
2449
2450
    /**
2451
     * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"
2452
     *
2453
     * @return \Doctrine\ORM\Query\AST\ConditionalPrimary
2454
     */
2455 375
    public function ConditionalPrimary()
2456
    {
2457 375
        $condPrimary = new AST\ConditionalPrimary;
2458
2459 375
        if ( ! $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2460 366
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2461
2462 363
            return $condPrimary;
2463
        }
2464
2465
        // Peek beyond the matching closing parenthesis ')'
2466 25
        $peek = $this->peekBeyondClosingParenthesis();
2467
2468 25
        if (in_array($peek['value'], ["=",  "<", "<=", "<>", ">", ">=", "!="]) ||
2469 22
            in_array($peek['type'], [Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS]) ||
2470 22
            $this->isMathOperator($peek)) {
2471 15
            $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();
2472
2473 15
            return $condPrimary;
2474
        }
2475
2476 21
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2477 21
        $condPrimary->conditionalExpression = $this->ConditionalExpression();
2478 21
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2479
2480 21
        return $condPrimary;
2481
    }
2482
2483
    /**
2484
     * SimpleConditionalExpression ::=
2485
     *      ComparisonExpression | BetweenExpression | LikeExpression |
2486
     *      InExpression | NullComparisonExpression | ExistsExpression |
2487
     *      EmptyCollectionComparisonExpression | CollectionMemberExpression |
2488
     *      InstanceOfExpression
2489
     */
2490 375
    public function SimpleConditionalExpression()
2491
    {
2492 375
        if ($this->lexer->isNextToken(Lexer::T_EXISTS)) {
2493 7
            return $this->ExistsExpression();
2494
        }
2495
2496 375
        $token      = $this->lexer->lookahead;
2497 375
        $peek       = $this->lexer->glimpse();
2498 375
        $lookahead  = $token;
2499
2500 375
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2501
            $token = $this->lexer->glimpse();
2502
        }
2503
2504 375
        if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) {
2505
            // Peek beyond the matching closing parenthesis.
2506 351
            $beyond = $this->lexer->peek();
2507
2508 351
            switch ($peek['value']) {
2509 351
                case '(':
2510
                    // Peeks beyond the matched closing parenthesis.
2511 33
                    $token = $this->peekBeyondClosingParenthesis(false);
2512
2513 33
                    if ($token['type'] === Lexer::T_NOT) {
2514 3
                        $token = $this->lexer->peek();
2515
                    }
2516
2517 33
                    if ($token['type'] === Lexer::T_IS) {
2518 2
                        $lookahead = $this->lexer->peek();
2519
                    }
2520 33
                    break;
2521
2522
                default:
2523
                    // Peek beyond the PathExpression or InputParameter.
2524 324
                    $token = $beyond;
2525
2526 324
                    while ($token['value'] === '.') {
2527 285
                        $this->lexer->peek();
2528
2529 285
                        $token = $this->lexer->peek();
2530
                    }
2531
2532
                    // Also peek beyond a NOT if there is one.
2533 324
                    if ($token['type'] === Lexer::T_NOT) {
2534 11
                        $token = $this->lexer->peek();
2535
                    }
2536
2537
                    // We need to go even further in case of IS (differentiate between NULL and EMPTY)
2538 324
                    $lookahead = $this->lexer->peek();
2539
            }
2540
2541
            // Also peek beyond a NOT if there is one.
2542 351
            if ($lookahead['type'] === Lexer::T_NOT) {
2543 7
                $lookahead = $this->lexer->peek();
2544
            }
2545
2546 351
            $this->lexer->resetPeek();
2547
        }
2548
2549 375
        if ($token['type'] === Lexer::T_BETWEEN) {
2550 8
            return $this->BetweenExpression();
2551
        }
2552
2553 369
        if ($token['type'] === Lexer::T_LIKE) {
2554 14
            return $this->LikeExpression();
2555
        }
2556
2557 356
        if ($token['type'] === Lexer::T_IN) {
2558 34
            return $this->InExpression();
2559
        }
2560
2561 331
        if ($token['type'] === Lexer::T_INSTANCE) {
2562 12
            return $this->InstanceOfExpression();
2563
        }
2564
2565 319
        if ($token['type'] === Lexer::T_MEMBER) {
2566 7
            return $this->CollectionMemberExpression();
2567
        }
2568
2569 312
        if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) {
2570 14
            return $this->NullComparisonExpression();
2571
        }
2572
2573 301
        if ($token['type'] === Lexer::T_IS  && $lookahead['type'] === Lexer::T_EMPTY) {
2574 4
            return $this->EmptyCollectionComparisonExpression();
2575
        }
2576
2577 297
        return $this->ComparisonExpression();
2578
    }
2579
2580
    /**
2581
     * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"
2582
     *
2583
     * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression
2584
     */
2585 4
    public function EmptyCollectionComparisonExpression()
2586
    {
2587 4
        $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression(
2588 4
            $this->CollectionValuedPathExpression()
2589
        );
2590 4
        $this->match(Lexer::T_IS);
2591
2592 4
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2593 2
            $this->match(Lexer::T_NOT);
2594 2
            $emptyCollectionCompExpr->not = true;
2595
        }
2596
2597 4
        $this->match(Lexer::T_EMPTY);
2598
2599 4
        return $emptyCollectionCompExpr;
2600
    }
2601
2602
    /**
2603
     * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression
2604
     *
2605
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2606
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2607
     *
2608
     * @return \Doctrine\ORM\Query\AST\CollectionMemberExpression
2609
     */
2610 7
    public function CollectionMemberExpression()
2611
    {
2612 7
        $not        = false;
2613 7
        $entityExpr = $this->EntityExpression();
2614
2615 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
2616
            $this->match(Lexer::T_NOT);
2617
2618
            $not = true;
2619
        }
2620
2621 7
        $this->match(Lexer::T_MEMBER);
2622
2623 7
        if ($this->lexer->isNextToken(Lexer::T_OF)) {
2624 7
            $this->match(Lexer::T_OF);
2625
        }
2626
2627 7
        $collMemberExpr = new AST\CollectionMemberExpression(
2628 7
            $entityExpr, $this->CollectionValuedPathExpression()
2629
        );
2630 7
        $collMemberExpr->not = $not;
2631
2632 7
        return $collMemberExpr;
2633
    }
2634
2635
    /**
2636
     * Literal ::= string | char | integer | float | boolean
2637
     *
2638
     * @return \Doctrine\ORM\Query\AST\Literal
2639
     */
2640 177
    public function Literal()
2641
    {
2642 177
        switch ($this->lexer->lookahead['type']) {
2643 177
            case Lexer::T_STRING:
2644 48
                $this->match(Lexer::T_STRING);
2645
2646 48
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2647 137
            case Lexer::T_INTEGER:
2648 9
            case Lexer::T_FLOAT:
2649 129
                $this->match(
2650 129
                    $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT
2651
                );
2652
2653 129
                return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']);
2654 8
            case Lexer::T_TRUE:
2655 4
            case Lexer::T_FALSE:
2656 8
                $this->match(
2657 8
                    $this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE
2658
                );
2659
2660 8
                return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']);
2661
            default:
2662
                $this->syntaxError('Literal');
2663
        }
2664
    }
2665
2666
    /**
2667
     * InParameter ::= Literal | InputParameter
2668
     *
2669
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2670
     */
2671 25
    public function InParameter()
2672
    {
2673 25
        if ($this->lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) {
2674 13
            return $this->InputParameter();
2675
        }
2676
2677 12
        return $this->Literal();
2678
    }
2679
2680
    /**
2681
     * InputParameter ::= PositionalParameter | NamedParameter
2682
     *
2683
     * @return \Doctrine\ORM\Query\AST\InputParameter
2684
     */
2685 165
    public function InputParameter()
2686
    {
2687 165
        $this->match(Lexer::T_INPUT_PARAMETER);
2688
2689 165
        return new AST\InputParameter($this->lexer->token['value']);
2690
    }
2691
2692
    /**
2693
     * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"
2694
     *
2695
     * @return \Doctrine\ORM\Query\AST\ArithmeticExpression
2696
     */
2697 330
    public function ArithmeticExpression()
2698
    {
2699 330
        $expr = new AST\ArithmeticExpression;
2700
2701 330
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2702 19
            $peek = $this->lexer->glimpse();
2703
2704 19
            if ($peek['type'] === Lexer::T_SELECT) {
2705 7
                $this->match(Lexer::T_OPEN_PARENTHESIS);
2706 7
                $expr->subselect = $this->Subselect();
2707 7
                $this->match(Lexer::T_CLOSE_PARENTHESIS);
2708
2709 7
                return $expr;
2710
            }
2711
        }
2712
2713 330
        $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();
2714
2715 330
        return $expr;
2716
    }
2717
2718
    /**
2719
     * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*
2720
     *
2721
     * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression
2722
     */
2723 434
    public function SimpleArithmeticExpression()
2724
    {
2725 434
        $terms = [];
2726 434
        $terms[] = $this->ArithmeticTerm();
2727
2728 434
        while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2729 21
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2730
2731 21
            $terms[] = $this->lexer->token['value'];
2732 21
            $terms[] = $this->ArithmeticTerm();
2733
        }
2734
2735
        // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression
2736
        // if only one AST\ArithmeticTerm is defined
2737 434
        if (count($terms) == 1) {
2738 429
            return $terms[0];
0 ignored issues
show
Bug Compatibility introduced by
The expression $terms[0]; of type Doctrine\ORM\Query\AST\ArithmeticTerm|null adds the type Doctrine\ORM\Query\AST\ArithmeticTerm to the return on line 2738 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...
2739
        }
2740
2741 21
        return new AST\SimpleArithmeticExpression($terms);
2742
    }
2743
2744
    /**
2745
     * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*
2746
     *
2747
     * @return \Doctrine\ORM\Query\AST\ArithmeticTerm
2748
     */
2749 434
    public function ArithmeticTerm()
2750
    {
2751 434
        $factors = [];
2752 434
        $factors[] = $this->ArithmeticFactor();
2753
2754 434
        while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) {
2755 53
            $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE);
2756
2757 53
            $factors[] = $this->lexer->token['value'];
2758 53
            $factors[] = $this->ArithmeticFactor();
2759
        }
2760
2761
        // Phase 1 AST optimization: Prevent AST\ArithmeticTerm
2762
        // if only one AST\ArithmeticFactor is defined
2763 434
        if (count($factors) == 1) {
2764 406
            return $factors[0];
0 ignored issues
show
Bug Compatibility introduced by
The expression $factors[0]; of type Doctrine\ORM\Query\AST\ArithmeticFactor|null adds the type Doctrine\ORM\Query\AST\ArithmeticFactor to the return on line 2764 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticTerm of type Doctrine\ORM\Query\AST\ArithmeticTerm|null.
Loading history...
2765
        }
2766
2767 53
        return new AST\ArithmeticTerm($factors);
2768
    }
2769
2770
    /**
2771
     * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary
2772
     *
2773
     * @return \Doctrine\ORM\Query\AST\ArithmeticFactor
2774
     */
2775 434
    public function ArithmeticFactor()
2776
    {
2777 434
        $sign = null;
2778
2779 434
        if (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) {
2780 3
            $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS);
2781 3
            $sign = $isPlus;
2782
        }
2783
2784 434
        $primary = $this->ArithmeticPrimary();
2785
2786
        // Phase 1 AST optimization: Prevent AST\ArithmeticFactor
2787
        // if only one AST\ArithmeticPrimary is defined
2788 434
        if ($sign === null) {
2789 433
            return $primary;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $primary; (Doctrine\ORM\Query\AST\P...e\ORM\Query\AST\Literal) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::ArithmeticFactor of type Doctrine\ORM\Query\AST\ArithmeticFactor|null.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
2790
        }
2791
2792 3
        return new AST\ArithmeticFactor($primary, $sign);
2793
    }
2794
2795
    /**
2796
     * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression
2797
     *          | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings
2798
     *          | FunctionsReturningDatetime | IdentificationVariable | ResultVariable
2799
     *          | InputParameter | CaseExpression
2800
     */
2801 438
    public function ArithmeticPrimary()
2802
    {
2803 438
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
2804 25
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2805
2806 25
            $expr = $this->SimpleArithmeticExpression();
2807
2808 25
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2809
2810 25
            return new AST\ParenthesisExpression($expr);
0 ignored issues
show
Bug introduced by
It seems like $expr defined by $this->SimpleArithmeticExpression() on line 2806 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...
2811
        }
2812
2813 438
        switch ($this->lexer->lookahead['type']) {
2814 438
            case Lexer::T_COALESCE:
2815 438
            case Lexer::T_NULLIF:
2816 438
            case Lexer::T_CASE:
2817 4
                return $this->CaseExpression();
2818
2819 438
            case Lexer::T_IDENTIFIER:
2820 408
                $peek = $this->lexer->glimpse();
2821
2822 408
                if ($peek['value'] == '(') {
2823 28
                    return $this->FunctionDeclaration();
2824
                }
2825
2826 388
                if ($peek['value'] == '.') {
2827 377
                    return $this->SingleValuedPathExpression();
2828
                }
2829
2830 46
                if (isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2831 10
                    return $this->ResultVariable();
2832
                }
2833
2834 38
                return $this->StateFieldPathExpression();
2835
2836 310
            case Lexer::T_INPUT_PARAMETER:
2837 147
                return $this->InputParameter();
2838
2839
            default:
2840 171
                $peek = $this->lexer->glimpse();
2841
2842 171
                if ($peek['value'] == '(') {
2843 18
                    return $this->FunctionDeclaration();
2844
                }
2845
2846 167
                return $this->Literal();
2847
        }
2848
    }
2849
2850
    /**
2851
     * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
2852
     *
2853
     * @return \Doctrine\ORM\Query\AST\Subselect |
2854
     *         string
2855
     */
2856 14
    public function StringExpression()
2857
    {
2858 14
        $peek = $this->lexer->glimpse();
2859
2860
        // Subselect
2861 14
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) {
2862
            $this->match(Lexer::T_OPEN_PARENTHESIS);
2863
            $expr = $this->Subselect();
2864
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
2865
2866
            return $expr;
2867
        }
2868
2869
        // ResultVariable (string)
2870 14
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) &&
2871 14
            isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) {
2872 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...
2873
        }
2874
2875 12
        return $this->StringPrimary();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->StringPrimary(); (Doctrine\ORM\Query\AST\P...mpleCaseExpression|null) is incompatible with the return type documented by Doctrine\ORM\Query\Parser::StringExpression of type Doctrine\ORM\Query\AST\Subselect|null.

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

Let’s take a look at an example:

class Author {
    private $name;

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

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

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

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

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

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

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

Loading history...
2876
    }
2877
2878
    /**
2879
     * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
2880
     */
2881 51
    public function StringPrimary()
2882
    {
2883 51
        $lookaheadType = $this->lexer->lookahead['type'];
2884
2885
        switch ($lookaheadType) {
2886 51
            case Lexer::T_IDENTIFIER:
2887 32
                $peek = $this->lexer->glimpse();
2888
2889 32
                if ($peek['value'] == '.') {
2890 32
                    return $this->StateFieldPathExpression();
2891
                }
2892
2893 8
                if ($peek['value'] == '(') {
2894
                    // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.
2895 8
                    return $this->FunctionDeclaration();
2896
                }
2897
2898
                $this->syntaxError("'.' or '('");
2899
                break;
2900
2901 32
            case Lexer::T_STRING:
2902 32
                $this->match(Lexer::T_STRING);
2903
2904 32
                return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
2905
2906 2
            case Lexer::T_INPUT_PARAMETER:
2907 2
                return $this->InputParameter();
2908
2909
            case Lexer::T_CASE:
2910
            case Lexer::T_COALESCE:
2911
            case Lexer::T_NULLIF:
2912
                return $this->CaseExpression();
2913
        }
2914
2915
        $this->syntaxError(
2916
            'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'
2917
        );
2918
    }
2919
2920
    /**
2921
     * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression
2922
     *
2923
     * @return \Doctrine\ORM\Query\AST\PathExpression |
2924
     *         \Doctrine\ORM\Query\AST\SimpleEntityExpression
2925
     */
2926 7
    public function EntityExpression()
2927
    {
2928 7
        $glimpse = $this->lexer->glimpse();
2929
2930 7
        if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') {
2931 1
            return $this->SingleValuedAssociationPathExpression();
2932
        }
2933
2934 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 2934 which is incompatible with the return type documented by Doctrine\ORM\Query\Parser::EntityExpression of type Doctrine\ORM\Query\AST\PathExpression.
Loading history...
2935
    }
2936
2937
    /**
2938
     * SimpleEntityExpression ::= IdentificationVariable | InputParameter
2939
     *
2940
     * @return string | \Doctrine\ORM\Query\AST\InputParameter
2941
     */
2942 6
    public function SimpleEntityExpression()
2943
    {
2944 6
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
2945 5
            return $this->InputParameter();
2946
        }
2947
2948 1
        return $this->StateFieldPathExpression();
2949
    }
2950
2951
    /**
2952
     * AggregateExpression ::=
2953
     *  ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"
2954
     *
2955
     * @return \Doctrine\ORM\Query\AST\AggregateExpression
2956
     */
2957 85
    public function AggregateExpression()
2958
    {
2959 85
        $lookaheadType = $this->lexer->lookahead['type'];
2960 85
        $isDistinct = false;
2961
2962 85
        if ( ! in_array($lookaheadType, [Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::T_SUM])) {
2963
            $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');
2964
        }
2965
2966 85
        $this->match($lookaheadType);
2967 85
        $functionName = $this->lexer->token['value'];
2968 85
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2969
2970 85
        if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) {
2971 3
            $this->match(Lexer::T_DISTINCT);
2972 3
            $isDistinct = true;
2973
        }
2974
2975 85
        $pathExp = $this->SimpleArithmeticExpression();
2976
2977 85
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
2978
2979 85
        return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);
0 ignored issues
show
Bug introduced by
It seems like $pathExp defined by $this->SimpleArithmeticExpression() on line 2975 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...
2980
    }
2981
2982
    /**
2983
     * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"
2984
     *
2985
     * @return \Doctrine\ORM\Query\AST\QuantifiedExpression
2986
     */
2987 3
    public function QuantifiedExpression()
2988
    {
2989 3
        $lookaheadType = $this->lexer->lookahead['type'];
2990 3
        $value = $this->lexer->lookahead['value'];
2991
2992 3
        if ( ! in_array($lookaheadType, [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME])) {
2993
            $this->syntaxError('ALL, ANY or SOME');
2994
        }
2995
2996 3
        $this->match($lookaheadType);
2997 3
        $this->match(Lexer::T_OPEN_PARENTHESIS);
2998
2999 3
        $qExpr = new AST\QuantifiedExpression($this->Subselect());
3000 3
        $qExpr->type = $value;
3001
3002 3
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3003
3004 3
        return $qExpr;
3005
    }
3006
3007
    /**
3008
     * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression
3009
     *
3010
     * @return \Doctrine\ORM\Query\AST\BetweenExpression
3011
     */
3012 8
    public function BetweenExpression()
3013
    {
3014 8
        $not = false;
3015 8
        $arithExpr1 = $this->ArithmeticExpression();
3016
3017 8
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3018 3
            $this->match(Lexer::T_NOT);
3019 3
            $not = true;
3020
        }
3021
3022 8
        $this->match(Lexer::T_BETWEEN);
3023 8
        $arithExpr2 = $this->ArithmeticExpression();
3024 8
        $this->match(Lexer::T_AND);
3025 8
        $arithExpr3 = $this->ArithmeticExpression();
3026
3027 8
        $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3);
3028 8
        $betweenExpr->not = $not;
3029
3030 8
        return $betweenExpr;
3031
    }
3032
3033
    /**
3034
     * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )
3035
     *
3036
     * @return \Doctrine\ORM\Query\AST\ComparisonExpression
3037
     */
3038 297
    public function ComparisonExpression()
3039
    {
3040 297
        $this->lexer->glimpse();
3041
3042 297
        $leftExpr  = $this->ArithmeticExpression();
3043 297
        $operator  = $this->ComparisonOperator();
3044 297
        $rightExpr = ($this->isNextAllAnySome())
3045 3
            ? $this->QuantifiedExpression()
3046 297
            : $this->ArithmeticExpression();
3047
3048 295
        return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);
3049
    }
3050
3051
    /**
3052
     * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"
3053
     *
3054
     * @return \Doctrine\ORM\Query\AST\InExpression
3055
     */
3056 34
    public function InExpression()
3057
    {
3058 34
        $inExpression = new AST\InExpression($this->ArithmeticExpression());
3059
3060 34
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3061 6
            $this->match(Lexer::T_NOT);
3062 6
            $inExpression->not = true;
3063
        }
3064
3065 34
        $this->match(Lexer::T_IN);
3066 34
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3067
3068 34
        if ($this->lexer->isNextToken(Lexer::T_SELECT)) {
3069 9
            $inExpression->subselect = $this->Subselect();
3070
        } else {
3071 25
            $literals = [];
3072 25
            $literals[] = $this->InParameter();
3073
3074 25
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3075 16
                $this->match(Lexer::T_COMMA);
3076 16
                $literals[] = $this->InParameter();
3077
            }
3078
3079 25
            $inExpression->literals = $literals;
3080
        }
3081
3082 33
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3083
3084 33
        return $inExpression;
3085
    }
3086
3087
    /**
3088
     * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")
3089
     *
3090
     * @return \Doctrine\ORM\Query\AST\InstanceOfExpression
3091
     */
3092 12
    public function InstanceOfExpression()
3093
    {
3094 12
        $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable());
3095
3096 12
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3097 1
            $this->match(Lexer::T_NOT);
3098 1
            $instanceOfExpression->not = true;
3099
        }
3100
3101 12
        $this->match(Lexer::T_INSTANCE);
3102 12
        $this->match(Lexer::T_OF);
3103
3104 12
        $exprValues = [];
3105
3106 12
        if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) {
3107 1
            $this->match(Lexer::T_OPEN_PARENTHESIS);
3108
3109 1
            $exprValues[] = $this->InstanceOfParameter();
3110
3111 1
            while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
3112 1
                $this->match(Lexer::T_COMMA);
3113
3114 1
                $exprValues[] = $this->InstanceOfParameter();
3115
            }
3116
3117 1
            $this->match(Lexer::T_CLOSE_PARENTHESIS);
3118
3119 1
            $instanceOfExpression->value = $exprValues;
3120
3121 1
            return $instanceOfExpression;
3122
        }
3123
3124 11
        $exprValues[] = $this->InstanceOfParameter();
3125
3126 11
        $instanceOfExpression->value = $exprValues;
3127
3128 11
        return $instanceOfExpression;
3129
    }
3130
3131
    /**
3132
     * InstanceOfParameter ::= AbstractSchemaName | InputParameter
3133
     *
3134
     * @return mixed
3135
     */
3136 12
    public function InstanceOfParameter()
3137
    {
3138 12
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3139 5
            $this->match(Lexer::T_INPUT_PARAMETER);
3140
3141 5
            return new AST\InputParameter($this->lexer->token['value']);
3142
        }
3143
3144 7
        $abstractSchemaName = $this->AbstractSchemaName();
3145
3146 7
        $this->validateAbstractSchemaName($abstractSchemaName);
3147
3148 7
        return $abstractSchemaName;
3149
    }
3150
3151
    /**
3152
     * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]
3153
     *
3154
     * @return \Doctrine\ORM\Query\AST\LikeExpression
3155
     */
3156 14
    public function LikeExpression()
3157
    {
3158 14
        $stringExpr = $this->StringExpression();
3159 14
        $not = false;
3160
3161 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3162 3
            $this->match(Lexer::T_NOT);
3163 3
            $not = true;
3164
        }
3165
3166 14
        $this->match(Lexer::T_LIKE);
3167
3168 14
        if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) {
3169 4
            $this->match(Lexer::T_INPUT_PARAMETER);
3170 4
            $stringPattern = new AST\InputParameter($this->lexer->token['value']);
3171
        } else {
3172 11
            $stringPattern = $this->StringPrimary();
3173
        }
3174
3175 14
        $escapeChar = null;
3176
3177 14
        if ($this->lexer->lookahead['type'] === Lexer::T_ESCAPE) {
3178 2
            $this->match(Lexer::T_ESCAPE);
3179 2
            $this->match(Lexer::T_STRING);
3180
3181 2
            $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']);
3182
        }
3183
3184 14
        $likeExpr = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar);
0 ignored issues
show
Bug introduced by
It seems like $stringExpr defined by $this->StringExpression() on line 3158 can be null; however, Doctrine\ORM\Query\AST\L...pression::__construct() does not accept null, maybe add an additional type check?

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

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

function doesNotAcceptNull(stdClass $x) { }

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

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

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

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

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

    return array();
}

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

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

Loading history...
3185 14
        $likeExpr->not = $not;
3186
3187 14
        return $likeExpr;
3188
    }
3189
3190
    /**
3191
     * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"
3192
     *
3193
     * @return \Doctrine\ORM\Query\AST\NullComparisonExpression
3194
     */
3195 14
    public function NullComparisonExpression()
3196
    {
3197
        switch (true) {
3198 14
            case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER):
3199
                $this->match(Lexer::T_INPUT_PARAMETER);
3200
3201
                $expr = new AST\InputParameter($this->lexer->token['value']);
3202
                break;
3203
3204 14
            case $this->lexer->isNextToken(Lexer::T_NULLIF):
3205 1
                $expr = $this->NullIfExpression();
3206 1
                break;
3207
3208 14
            case $this->lexer->isNextToken(Lexer::T_COALESCE):
3209 1
                $expr = $this->CoalesceExpression();
3210 1
                break;
3211
3212 14
            case $this->isFunction():
3213 2
                $expr = $this->FunctionDeclaration();
3214 2
                break;
3215
3216
            default:
3217
                // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression
3218 13
                $glimpse = $this->lexer->glimpse();
3219
3220 13
                if ($glimpse['type'] === Lexer::T_DOT) {
3221 9
                    $expr = $this->SingleValuedPathExpression();
3222
3223
                    // Leave switch statement
3224 9
                    break;
3225
                }
3226
3227 4
                $lookaheadValue = $this->lexer->lookahead['value'];
3228
3229
                // Validate existing component
3230 4
                if ( ! isset($this->queryComponents[$lookaheadValue])) {
3231
                    $this->semanticalError('Cannot add having condition on undefined result variable.');
3232
                }
3233
3234
                // Validate SingleValuedPathExpression (ie.: "product")
3235 4
                if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {
3236 1
                    $expr = $this->SingleValuedPathExpression();
3237 1
                    break;
3238
                }
3239
3240
                // Validating ResultVariable
3241 3
                if ( ! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {
3242
                    $this->semanticalError('Cannot add having condition on a non result variable.');
3243
                }
3244
3245 3
                $expr = $this->ResultVariable();
3246 3
                break;
3247
        }
3248
3249 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...
3250
3251 14
        $this->match(Lexer::T_IS);
3252
3253 14
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3254 5
            $this->match(Lexer::T_NOT);
3255
3256 5
            $nullCompExpr->not = true;
3257
        }
3258
3259 14
        $this->match(Lexer::T_NULL);
3260
3261 14
        return $nullCompExpr;
3262
    }
3263
3264
    /**
3265
     * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"
3266
     *
3267
     * @return \Doctrine\ORM\Query\AST\ExistsExpression
3268
     */
3269 7
    public function ExistsExpression()
3270
    {
3271 7
        $not = false;
3272
3273 7
        if ($this->lexer->isNextToken(Lexer::T_NOT)) {
3274
            $this->match(Lexer::T_NOT);
3275
            $not = true;
3276
        }
3277
3278 7
        $this->match(Lexer::T_EXISTS);
3279 7
        $this->match(Lexer::T_OPEN_PARENTHESIS);
3280
3281 7
        $existsExpression = new AST\ExistsExpression($this->Subselect());
3282 7
        $existsExpression->not = $not;
3283
3284 7
        $this->match(Lexer::T_CLOSE_PARENTHESIS);
3285
3286 7
        return $existsExpression;
3287
    }
3288
3289
    /**
3290
     * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!="
3291
     *
3292
     * @return string
3293
     */
3294 297
    public function ComparisonOperator()
3295
    {
3296 297
        switch ($this->lexer->lookahead['value']) {
3297 297
            case '=':
3298 246
                $this->match(Lexer::T_EQUALS);
3299
3300 246
                return '=';
3301
3302 62
            case '<':
3303 17
                $this->match(Lexer::T_LOWER_THAN);
3304 17
                $operator = '<';
3305
3306 17
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3307 5
                    $this->match(Lexer::T_EQUALS);
3308 5
                    $operator .= '=';
3309 12
                } else if ($this->lexer->isNextToken(Lexer::T_GREATER_THAN)) {
3310 3
                    $this->match(Lexer::T_GREATER_THAN);
3311 3
                    $operator .= '>';
3312
                }
3313
3314 17
                return $operator;
3315
3316 53
            case '>':
3317 47
                $this->match(Lexer::T_GREATER_THAN);
3318 47
                $operator = '>';
3319
3320 47
                if ($this->lexer->isNextToken(Lexer::T_EQUALS)) {
3321 6
                    $this->match(Lexer::T_EQUALS);
3322 6
                    $operator .= '=';
3323
                }
3324
3325 47
                return $operator;
3326
3327 6
            case '!':
3328 6
                $this->match(Lexer::T_NEGATE);
3329 6
                $this->match(Lexer::T_EQUALS);
3330
3331 6
                return '<>';
3332
3333
            default:
3334
                $this->syntaxError('=, <, <=, <>, >, >=, !=');
3335
        }
3336
    }
3337
3338
    /**
3339
     * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
3340
     *
3341
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3342
     */
3343 141
    public function FunctionDeclaration()
3344
    {
3345 141
        $token = $this->lexer->lookahead;
3346 141
        $funcName = strtolower($token['value']);
3347
3348 141
        $customFunctionDeclaration = $this->CustomFunctionDeclaration();
3349
3350
        // Check for custom functions functions first!
3351
        switch (true) {
3352 141
            case $customFunctionDeclaration !== null:
3353 4
                return $customFunctionDeclaration;
3354
            
3355 137
            case (isset(self::$_STRING_FUNCTIONS[$funcName])):
3356 30
                return $this->FunctionsReturningStrings();
3357
3358 112
            case (isset(self::$_NUMERIC_FUNCTIONS[$funcName])):
3359 106
                return $this->FunctionsReturningNumerics();
3360
3361 7
            case (isset(self::$_DATETIME_FUNCTIONS[$funcName])):
3362 7
                return $this->FunctionsReturningDatetime();
3363
3364
            default:
3365
                $this->syntaxError('known function', $token);
3366
        }
3367
    }
3368
3369
    /**
3370
     * Helper function for FunctionDeclaration grammar rule.
3371
     *
3372
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3373
     */
3374 141
    private function CustomFunctionDeclaration()
3375
    {
3376 141
        $token = $this->lexer->lookahead;
3377 141
        $funcName = strtolower($token['value']);
3378
3379
        // Check for custom functions afterwards
3380 141
        $config = $this->em->getConfiguration();
3381
3382
        switch (true) {
3383 141
            case ($config->getCustomStringFunction($funcName) !== null):
3384 3
                return $this->CustomFunctionsReturningStrings();
3385
3386 139
            case ($config->getCustomNumericFunction($funcName) !== null):
3387 2
                return $this->CustomFunctionsReturningNumerics();
3388
3389 137
            case ($config->getCustomDatetimeFunction($funcName) !== null):
3390
                return $this->CustomFunctionsReturningDatetime();
3391
3392
            default:
3393 137
                return null;
3394
        }
3395
    }
3396
3397
    /**
3398
     * FunctionsReturningNumerics ::=
3399
     *      "LENGTH" "(" StringPrimary ")" |
3400
     *      "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" |
3401
     *      "ABS" "(" SimpleArithmeticExpression ")" |
3402
     *      "SQRT" "(" SimpleArithmeticExpression ")" |
3403
     *      "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3404
     *      "SIZE" "(" CollectionValuedPathExpression ")" |
3405
     *      "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3406
     *      "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
3407
     *      "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
3408
     *
3409
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3410
     */
3411 106
    public function FunctionsReturningNumerics()
3412
    {
3413 106
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3414 106
        $funcClass     = self::$_NUMERIC_FUNCTIONS[$funcNameLower];
3415
3416 106
        $function = new $funcClass($funcNameLower);
3417 106
        $function->parse($this);
3418
3419 106
        return $function;
3420
    }
3421
3422
    /**
3423
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3424
     */
3425 2
    public function CustomFunctionsReturningNumerics()
3426
    {
3427
        // getCustomNumericFunction is case-insensitive
3428 2
        $functionName  = strtolower($this->lexer->lookahead['value']);
3429 2
        $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName);
3430
3431 2
        $function = is_string($functionClass)
3432 1
            ? new $functionClass($functionName)
3433 2
            : call_user_func($functionClass, $functionName);
3434
3435 2
        $function->parse($this);
3436
3437 2
        return $function;
3438
    }
3439
3440
    /**
3441
     * FunctionsReturningDateTime ::=
3442
     *     "CURRENT_DATE" |
3443
     *     "CURRENT_TIME" |
3444
     *     "CURRENT_TIMESTAMP" |
3445
     *     "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" |
3446
     *     "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"
3447
     *
3448
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3449
     */
3450 7
    public function FunctionsReturningDatetime()
3451
    {
3452 7
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3453 7
        $funcClass     = self::$_DATETIME_FUNCTIONS[$funcNameLower];
3454
3455 7
        $function = new $funcClass($funcNameLower);
3456 7
        $function->parse($this);
3457
3458 7
        return $function;
3459
    }
3460
3461
    /**
3462
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3463
     */
3464
    public function CustomFunctionsReturningDatetime()
3465
    {
3466
        // getCustomDatetimeFunction is case-insensitive
3467
        $functionName  = $this->lexer->lookahead['value'];
3468
        $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName);
3469
3470
        $function = is_string($functionClass)
3471
            ? new $functionClass($functionName)
3472
            : call_user_func($functionClass, $functionName);
3473
3474
        $function->parse($this);
3475
3476
        return $function;
3477
    }
3478
3479
    /**
3480
     * FunctionsReturningStrings ::=
3481
     *   "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" |
3482
     *   "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |
3483
     *   "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" |
3484
     *   "LOWER" "(" StringPrimary ")" |
3485
     *   "UPPER" "(" StringPrimary ")" |
3486
     *   "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"
3487
     *
3488
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3489
     */
3490 30
    public function FunctionsReturningStrings()
3491
    {
3492 30
        $funcNameLower = strtolower($this->lexer->lookahead['value']);
3493 30
        $funcClass     = self::$_STRING_FUNCTIONS[$funcNameLower];
3494
3495 30
        $function = new $funcClass($funcNameLower);
3496 30
        $function->parse($this);
3497
3498 30
        return $function;
3499
    }
3500
3501
    /**
3502
     * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode
3503
     */
3504 3
    public function CustomFunctionsReturningStrings()
3505
    {
3506
        // getCustomStringFunction is case-insensitive
3507 3
        $functionName  = $this->lexer->lookahead['value'];
3508 3
        $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName);
3509
3510 3
        $function = is_string($functionClass)
3511 2
            ? new $functionClass($functionName)
3512 3
            : call_user_func($functionClass, $functionName);
3513
3514 3
        $function->parse($this);
3515
3516 3
        return $function;
3517
    }
3518
}
3519