diff options
Diffstat (limited to 'vendor/doctrine/orm/src/Query')
125 files changed, 12640 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Query/AST/ASTException.php b/vendor/doctrine/orm/src/Query/AST/ASTException.php new file mode 100644 index 0000000..1ef890a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ASTException.php | |||
@@ -0,0 +1,20 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\QueryException; | ||
8 | |||
9 | use function get_debug_type; | ||
10 | |||
11 | /** | ||
12 | * Base exception class for AST exceptions. | ||
13 | */ | ||
14 | class ASTException extends QueryException | ||
15 | { | ||
16 | public static function noDispatchForNode(Node $node): self | ||
17 | { | ||
18 | return new self('Double-dispatch for node ' . get_debug_type($node) . ' is not supported.'); | ||
19 | } | ||
20 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/AggregateExpression.php b/vendor/doctrine/orm/src/Query/AST/AggregateExpression.php new file mode 100644 index 0000000..468c65c --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/AggregateExpression.php | |||
@@ -0,0 +1,23 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | class AggregateExpression extends Node | ||
10 | { | ||
11 | /** @param bool $isDistinct Some aggregate expressions support distinct, eg COUNT. */ | ||
12 | public function __construct( | ||
13 | public string $functionName, | ||
14 | public Node|string $pathExpression, | ||
15 | public bool $isDistinct, | ||
16 | ) { | ||
17 | } | ||
18 | |||
19 | public function dispatch(SqlWalker $walker): string | ||
20 | { | ||
21 | return $walker->walkAggregateExpression($this); | ||
22 | } | ||
23 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php b/vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php new file mode 100644 index 0000000..a819e05 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ArithmeticExpression.php | |||
@@ -0,0 +1,34 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class ArithmeticExpression extends Node | ||
15 | { | ||
16 | public Node|string|null $simpleArithmeticExpression = null; | ||
17 | |||
18 | public Subselect|null $subselect = null; | ||
19 | |||
20 | public function isSimpleArithmeticExpression(): bool | ||
21 | { | ||
22 | return (bool) $this->simpleArithmeticExpression; | ||
23 | } | ||
24 | |||
25 | public function isSubselect(): bool | ||
26 | { | ||
27 | return (bool) $this->subselect; | ||
28 | } | ||
29 | |||
30 | public function dispatch(SqlWalker $walker): string | ||
31 | { | ||
32 | return $walker->walkArithmeticExpression($this); | ||
33 | } | ||
34 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php b/vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php new file mode 100644 index 0000000..278a921 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ArithmeticFactor.php | |||
@@ -0,0 +1,36 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class ArithmeticFactor extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public mixed $arithmeticPrimary, | ||
18 | public bool|null $sign = null, | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | public function isPositiveSigned(): bool | ||
23 | { | ||
24 | return $this->sign === true; | ||
25 | } | ||
26 | |||
27 | public function isNegativeSigned(): bool | ||
28 | { | ||
29 | return $this->sign === false; | ||
30 | } | ||
31 | |||
32 | public function dispatch(SqlWalker $walker): string | ||
33 | { | ||
34 | return $walker->walkArithmeticFactor($this); | ||
35 | } | ||
36 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php b/vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php new file mode 100644 index 0000000..b233612 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ArithmeticTerm.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class ArithmeticTerm extends Node | ||
15 | { | ||
16 | /** @param mixed[] $arithmeticFactors */ | ||
17 | public function __construct(public array $arithmeticFactors) | ||
18 | { | ||
19 | } | ||
20 | |||
21 | public function dispatch(SqlWalker $walker): string | ||
22 | { | ||
23 | return $walker->walkArithmeticTerm($this); | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/BetweenExpression.php b/vendor/doctrine/orm/src/Query/AST/BetweenExpression.php new file mode 100644 index 0000000..c13292b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/BetweenExpression.php | |||
@@ -0,0 +1,23 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | class BetweenExpression extends Node | ||
10 | { | ||
11 | public function __construct( | ||
12 | public ArithmeticExpression $expression, | ||
13 | public ArithmeticExpression $leftBetweenExpression, | ||
14 | public ArithmeticExpression $rightBetweenExpression, | ||
15 | public bool $not = false, | ||
16 | ) { | ||
17 | } | ||
18 | |||
19 | public function dispatch(SqlWalker $walker): string | ||
20 | { | ||
21 | return $walker->walkBetweenExpression($this); | ||
22 | } | ||
23 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php b/vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php new file mode 100644 index 0000000..89f025f --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/CoalesceExpression.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class CoalesceExpression extends Node | ||
15 | { | ||
16 | /** @param mixed[] $scalarExpressions */ | ||
17 | public function __construct(public array $scalarExpressions) | ||
18 | { | ||
19 | } | ||
20 | |||
21 | public function dispatch(SqlWalker $walker): string | ||
22 | { | ||
23 | return $walker->walkCoalesceExpression($this); | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php b/vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php new file mode 100644 index 0000000..a62a191 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/CollectionMemberExpression.php | |||
@@ -0,0 +1,27 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class CollectionMemberExpression extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public mixed $entityExpression, | ||
18 | public PathExpression $collectionValuedPathExpression, | ||
19 | public bool $not = false, | ||
20 | ) { | ||
21 | } | ||
22 | |||
23 | public function dispatch(SqlWalker $walker): string | ||
24 | { | ||
25 | return $walker->walkCollectionMemberExpression($this); | ||
26 | } | ||
27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php b/vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php new file mode 100644 index 0000000..a7d91f9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ComparisonExpression.php | |||
@@ -0,0 +1,32 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) | | ||
11 | * StringExpression ComparisonOperator (StringExpression | QuantifiedExpression) | | ||
12 | * BooleanExpression ("=" | "<>" | "!=") (BooleanExpression | QuantifiedExpression) | | ||
13 | * EnumExpression ("=" | "<>" | "!=") (EnumExpression | QuantifiedExpression) | | ||
14 | * DatetimeExpression ComparisonOperator (DatetimeExpression | QuantifiedExpression) | | ||
15 | * EntityExpression ("=" | "<>") (EntityExpression | QuantifiedExpression) | ||
16 | * | ||
17 | * @link www.doctrine-project.org | ||
18 | */ | ||
19 | class ComparisonExpression extends Node | ||
20 | { | ||
21 | public function __construct( | ||
22 | public Node|string $leftExpression, | ||
23 | public string $operator, | ||
24 | public Node|string $rightExpression, | ||
25 | ) { | ||
26 | } | ||
27 | |||
28 | public function dispatch(SqlWalker $walker): string | ||
29 | { | ||
30 | return $walker->walkComparisonExpression($this); | ||
31 | } | ||
32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php b/vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php new file mode 100644 index 0000000..26a98e5 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ConditionalExpression.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class ConditionalExpression extends Node | ||
15 | { | ||
16 | /** @param mixed[] $conditionalTerms */ | ||
17 | public function __construct(public array $conditionalTerms) | ||
18 | { | ||
19 | } | ||
20 | |||
21 | public function dispatch(SqlWalker $walker): string | ||
22 | { | ||
23 | return $walker->walkConditionalExpression($this); | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php b/vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php new file mode 100644 index 0000000..7881743 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ConditionalFactor.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * ConditionalFactor ::= ["NOT"] ConditionalPrimary | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class ConditionalFactor extends Node implements Phase2OptimizableConditional | ||
15 | { | ||
16 | public function __construct( | ||
17 | public ConditionalPrimary $conditionalPrimary, | ||
18 | public bool $not = false, | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkConditionalFactor($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php b/vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php new file mode 100644 index 0000000..9344cd9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ConditionalPrimary.php | |||
@@ -0,0 +1,34 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class ConditionalPrimary extends Node implements Phase2OptimizableConditional | ||
15 | { | ||
16 | public Node|null $simpleConditionalExpression = null; | ||
17 | |||
18 | public ConditionalExpression|Phase2OptimizableConditional|null $conditionalExpression = null; | ||
19 | |||
20 | public function isSimpleConditionalExpression(): bool | ||
21 | { | ||
22 | return (bool) $this->simpleConditionalExpression; | ||
23 | } | ||
24 | |||
25 | public function isConditionalExpression(): bool | ||
26 | { | ||
27 | return (bool) $this->conditionalExpression; | ||
28 | } | ||
29 | |||
30 | public function dispatch(SqlWalker $walker): string | ||
31 | { | ||
32 | return $walker->walkConditionalPrimary($this); | ||
33 | } | ||
34 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php b/vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php new file mode 100644 index 0000000..dcea50b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ConditionalTerm.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class ConditionalTerm extends Node implements Phase2OptimizableConditional | ||
15 | { | ||
16 | /** @param mixed[] $conditionalFactors */ | ||
17 | public function __construct(public array $conditionalFactors) | ||
18 | { | ||
19 | } | ||
20 | |||
21 | public function dispatch(SqlWalker $walker): string | ||
22 | { | ||
23 | return $walker->walkConditionalTerm($this); | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/DeleteClause.php b/vendor/doctrine/orm/src/Query/AST/DeleteClause.php new file mode 100644 index 0000000..25e9085 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/DeleteClause.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName [["AS"] AliasIdentificationVariable] | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class DeleteClause extends Node | ||
15 | { | ||
16 | public string $aliasIdentificationVariable; | ||
17 | |||
18 | public function __construct(public string $abstractSchemaName) | ||
19 | { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkDeleteClause($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/DeleteStatement.php b/vendor/doctrine/orm/src/Query/AST/DeleteStatement.php new file mode 100644 index 0000000..f367d09 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/DeleteStatement.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * DeleteStatement = DeleteClause [WhereClause] | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class DeleteStatement extends Node | ||
15 | { | ||
16 | public WhereClause|null $whereClause = null; | ||
17 | |||
18 | public function __construct(public DeleteClause $deleteClause) | ||
19 | { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkDeleteStatement($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php b/vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php new file mode 100644 index 0000000..9978800 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/EmptyCollectionComparisonExpression.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY" | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class EmptyCollectionComparisonExpression extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public PathExpression $expression, | ||
18 | public bool $not = false, | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkEmptyCollectionComparisonExpression($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ExistsExpression.php b/vendor/doctrine/orm/src/Query/AST/ExistsExpression.php new file mode 100644 index 0000000..72757f4 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ExistsExpression.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")" | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class ExistsExpression extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public Subselect $subselect, | ||
18 | public bool $not = false, | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkExistsExpression($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/FromClause.php b/vendor/doctrine/orm/src/Query/AST/FromClause.php new file mode 100644 index 0000000..0b74393 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/FromClause.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration} | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class FromClause extends Node | ||
15 | { | ||
16 | /** @param mixed[] $identificationVariableDeclarations */ | ||
17 | public function __construct(public array $identificationVariableDeclarations) | ||
18 | { | ||
19 | } | ||
20 | |||
21 | public function dispatch(SqlWalker $walker): string | ||
22 | { | ||
23 | return $walker->walkFromClause($this); | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php new file mode 100644 index 0000000..4edff06 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/AbsFunction.php | |||
@@ -0,0 +1,37 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | /** | ||
13 | * "ABS" "(" SimpleArithmeticExpression ")" | ||
14 | * | ||
15 | * @link www.doctrine-project.org | ||
16 | */ | ||
17 | class AbsFunction extends FunctionNode | ||
18 | { | ||
19 | public Node|string $simpleArithmeticExpression; | ||
20 | |||
21 | public function getSql(SqlWalker $sqlWalker): string | ||
22 | { | ||
23 | return 'ABS(' . $sqlWalker->walkSimpleArithmeticExpression( | ||
24 | $this->simpleArithmeticExpression, | ||
25 | ) . ')'; | ||
26 | } | ||
27 | |||
28 | public function parse(Parser $parser): void | ||
29 | { | ||
30 | $parser->match(TokenType::T_IDENTIFIER); | ||
31 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
32 | |||
33 | $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
34 | |||
35 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
36 | } | ||
37 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php new file mode 100644 index 0000000..ba7b7f3 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/AvgFunction.php | |||
@@ -0,0 +1,27 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | |||
11 | /** | ||
12 | * "AVG" "(" ["DISTINCT"] StringPrimary ")" | ||
13 | */ | ||
14 | final class AvgFunction extends FunctionNode | ||
15 | { | ||
16 | private AggregateExpression $aggregateExpression; | ||
17 | |||
18 | public function getSql(SqlWalker $sqlWalker): string | ||
19 | { | ||
20 | return $this->aggregateExpression->dispatch($sqlWalker); | ||
21 | } | ||
22 | |||
23 | public function parse(Parser $parser): void | ||
24 | { | ||
25 | $this->aggregateExpression = $parser->AggregateExpression(); | ||
26 | } | ||
27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php new file mode 100644 index 0000000..f2d3146 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/BitAndFunction.php | |||
@@ -0,0 +1,43 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | /** | ||
13 | * "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | ||
14 | * | ||
15 | * @link www.doctrine-project.org | ||
16 | */ | ||
17 | class BitAndFunction extends FunctionNode | ||
18 | { | ||
19 | public Node $firstArithmetic; | ||
20 | public Node $secondArithmetic; | ||
21 | |||
22 | public function getSql(SqlWalker $sqlWalker): string | ||
23 | { | ||
24 | $platform = $sqlWalker->getConnection()->getDatabasePlatform(); | ||
25 | |||
26 | return $platform->getBitAndComparisonExpression( | ||
27 | $this->firstArithmetic->dispatch($sqlWalker), | ||
28 | $this->secondArithmetic->dispatch($sqlWalker), | ||
29 | ); | ||
30 | } | ||
31 | |||
32 | public function parse(Parser $parser): void | ||
33 | { | ||
34 | $parser->match(TokenType::T_IDENTIFIER); | ||
35 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
36 | |||
37 | $this->firstArithmetic = $parser->ArithmeticPrimary(); | ||
38 | $parser->match(TokenType::T_COMMA); | ||
39 | $this->secondArithmetic = $parser->ArithmeticPrimary(); | ||
40 | |||
41 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
42 | } | ||
43 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php new file mode 100644 index 0000000..f3f84da --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/BitOrFunction.php | |||
@@ -0,0 +1,43 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | /** | ||
13 | * "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | ||
14 | * | ||
15 | * @link www.doctrine-project.org | ||
16 | */ | ||
17 | class BitOrFunction extends FunctionNode | ||
18 | { | ||
19 | public Node $firstArithmetic; | ||
20 | public Node $secondArithmetic; | ||
21 | |||
22 | public function getSql(SqlWalker $sqlWalker): string | ||
23 | { | ||
24 | $platform = $sqlWalker->getConnection()->getDatabasePlatform(); | ||
25 | |||
26 | return $platform->getBitOrComparisonExpression( | ||
27 | $this->firstArithmetic->dispatch($sqlWalker), | ||
28 | $this->secondArithmetic->dispatch($sqlWalker), | ||
29 | ); | ||
30 | } | ||
31 | |||
32 | public function parse(Parser $parser): void | ||
33 | { | ||
34 | $parser->match(TokenType::T_IDENTIFIER); | ||
35 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
36 | |||
37 | $this->firstArithmetic = $parser->ArithmeticPrimary(); | ||
38 | $parser->match(TokenType::T_COMMA); | ||
39 | $this->secondArithmetic = $parser->ArithmeticPrimary(); | ||
40 | |||
41 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
42 | } | ||
43 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php new file mode 100644 index 0000000..5b8d696 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/ConcatFunction.php | |||
@@ -0,0 +1,58 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | /** | ||
13 | * "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary }* ")" | ||
14 | * | ||
15 | * @link www.doctrine-project.org | ||
16 | */ | ||
17 | class ConcatFunction extends FunctionNode | ||
18 | { | ||
19 | public Node $firstStringPrimary; | ||
20 | public Node $secondStringPrimary; | ||
21 | |||
22 | /** @psalm-var list<Node> */ | ||
23 | public array $concatExpressions = []; | ||
24 | |||
25 | public function getSql(SqlWalker $sqlWalker): string | ||
26 | { | ||
27 | $platform = $sqlWalker->getConnection()->getDatabasePlatform(); | ||
28 | |||
29 | $args = []; | ||
30 | |||
31 | foreach ($this->concatExpressions as $expression) { | ||
32 | $args[] = $sqlWalker->walkStringPrimary($expression); | ||
33 | } | ||
34 | |||
35 | return $platform->getConcatExpression(...$args); | ||
36 | } | ||
37 | |||
38 | public function parse(Parser $parser): void | ||
39 | { | ||
40 | $parser->match(TokenType::T_IDENTIFIER); | ||
41 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
42 | |||
43 | $this->firstStringPrimary = $parser->StringPrimary(); | ||
44 | $this->concatExpressions[] = $this->firstStringPrimary; | ||
45 | |||
46 | $parser->match(TokenType::T_COMMA); | ||
47 | |||
48 | $this->secondStringPrimary = $parser->StringPrimary(); | ||
49 | $this->concatExpressions[] = $this->secondStringPrimary; | ||
50 | |||
51 | while ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) { | ||
52 | $parser->match(TokenType::T_COMMA); | ||
53 | $this->concatExpressions[] = $parser->StringPrimary(); | ||
54 | } | ||
55 | |||
56 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
57 | } | ||
58 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php new file mode 100644 index 0000000..dc926a5 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/CountFunction.php | |||
@@ -0,0 +1,35 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\DBAL\Types\Type; | ||
8 | use Doctrine\DBAL\Types\Types; | ||
9 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
10 | use Doctrine\ORM\Query\AST\TypedExpression; | ||
11 | use Doctrine\ORM\Query\Parser; | ||
12 | use Doctrine\ORM\Query\SqlWalker; | ||
13 | |||
14 | /** | ||
15 | * "COUNT" "(" ["DISTINCT"] StringPrimary ")" | ||
16 | */ | ||
17 | final class CountFunction extends FunctionNode implements TypedExpression | ||
18 | { | ||
19 | private AggregateExpression $aggregateExpression; | ||
20 | |||
21 | public function getSql(SqlWalker $sqlWalker): string | ||
22 | { | ||
23 | return $this->aggregateExpression->dispatch($sqlWalker); | ||
24 | } | ||
25 | |||
26 | public function parse(Parser $parser): void | ||
27 | { | ||
28 | $this->aggregateExpression = $parser->AggregateExpression(); | ||
29 | } | ||
30 | |||
31 | public function getReturnType(): Type | ||
32 | { | ||
33 | return Type::getType(Types::INTEGER); | ||
34 | } | ||
35 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php new file mode 100644 index 0000000..cec9632 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentDateFunction.php | |||
@@ -0,0 +1,29 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\Parser; | ||
8 | use Doctrine\ORM\Query\SqlWalker; | ||
9 | use Doctrine\ORM\Query\TokenType; | ||
10 | |||
11 | /** | ||
12 | * "CURRENT_DATE" | ||
13 | * | ||
14 | * @link www.doctrine-project.org | ||
15 | */ | ||
16 | class CurrentDateFunction extends FunctionNode | ||
17 | { | ||
18 | public function getSql(SqlWalker $sqlWalker): string | ||
19 | { | ||
20 | return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentDateSQL(); | ||
21 | } | ||
22 | |||
23 | public function parse(Parser $parser): void | ||
24 | { | ||
25 | $parser->match(TokenType::T_IDENTIFIER); | ||
26 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
27 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
28 | } | ||
29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php new file mode 100644 index 0000000..6473fce --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimeFunction.php | |||
@@ -0,0 +1,29 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\Parser; | ||
8 | use Doctrine\ORM\Query\SqlWalker; | ||
9 | use Doctrine\ORM\Query\TokenType; | ||
10 | |||
11 | /** | ||
12 | * "CURRENT_TIME" | ||
13 | * | ||
14 | * @link www.doctrine-project.org | ||
15 | */ | ||
16 | class CurrentTimeFunction extends FunctionNode | ||
17 | { | ||
18 | public function getSql(SqlWalker $sqlWalker): string | ||
19 | { | ||
20 | return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentTimeSQL(); | ||
21 | } | ||
22 | |||
23 | public function parse(Parser $parser): void | ||
24 | { | ||
25 | $parser->match(TokenType::T_IDENTIFIER); | ||
26 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
27 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
28 | } | ||
29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php new file mode 100644 index 0000000..edcd27c --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/CurrentTimestampFunction.php | |||
@@ -0,0 +1,29 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\Parser; | ||
8 | use Doctrine\ORM\Query\SqlWalker; | ||
9 | use Doctrine\ORM\Query\TokenType; | ||
10 | |||
11 | /** | ||
12 | * "CURRENT_TIMESTAMP" | ||
13 | * | ||
14 | * @link www.doctrine-project.org | ||
15 | */ | ||
16 | class CurrentTimestampFunction extends FunctionNode | ||
17 | { | ||
18 | public function getSql(SqlWalker $sqlWalker): string | ||
19 | { | ||
20 | return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentTimestampSQL(); | ||
21 | } | ||
22 | |||
23 | public function parse(Parser $parser): void | ||
24 | { | ||
25 | $parser->match(TokenType::T_IDENTIFIER); | ||
26 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
27 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
28 | } | ||
29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php new file mode 100644 index 0000000..12920dc --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/DateAddFunction.php | |||
@@ -0,0 +1,83 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\ASTException; | ||
8 | use Doctrine\ORM\Query\AST\Node; | ||
9 | use Doctrine\ORM\Query\Parser; | ||
10 | use Doctrine\ORM\Query\QueryException; | ||
11 | use Doctrine\ORM\Query\SqlWalker; | ||
12 | use Doctrine\ORM\Query\TokenType; | ||
13 | |||
14 | use function strtolower; | ||
15 | |||
16 | /** | ||
17 | * "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" | ||
18 | * | ||
19 | * @link www.doctrine-project.org | ||
20 | */ | ||
21 | class DateAddFunction extends FunctionNode | ||
22 | { | ||
23 | public Node $firstDateExpression; | ||
24 | public Node $intervalExpression; | ||
25 | public Node $unit; | ||
26 | |||
27 | public function getSql(SqlWalker $sqlWalker): string | ||
28 | { | ||
29 | return match (strtolower((string) $this->unit->value)) { | ||
30 | 'second' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddSecondsExpression( | ||
31 | $this->firstDateExpression->dispatch($sqlWalker), | ||
32 | $this->dispatchIntervalExpression($sqlWalker), | ||
33 | ), | ||
34 | 'minute' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddMinutesExpression( | ||
35 | $this->firstDateExpression->dispatch($sqlWalker), | ||
36 | $this->dispatchIntervalExpression($sqlWalker), | ||
37 | ), | ||
38 | 'hour' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddHourExpression( | ||
39 | $this->firstDateExpression->dispatch($sqlWalker), | ||
40 | $this->dispatchIntervalExpression($sqlWalker), | ||
41 | ), | ||
42 | 'day' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddDaysExpression( | ||
43 | $this->firstDateExpression->dispatch($sqlWalker), | ||
44 | $this->dispatchIntervalExpression($sqlWalker), | ||
45 | ), | ||
46 | 'week' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddWeeksExpression( | ||
47 | $this->firstDateExpression->dispatch($sqlWalker), | ||
48 | $this->dispatchIntervalExpression($sqlWalker), | ||
49 | ), | ||
50 | 'month' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddMonthExpression( | ||
51 | $this->firstDateExpression->dispatch($sqlWalker), | ||
52 | $this->dispatchIntervalExpression($sqlWalker), | ||
53 | ), | ||
54 | 'year' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddYearsExpression( | ||
55 | $this->firstDateExpression->dispatch($sqlWalker), | ||
56 | $this->dispatchIntervalExpression($sqlWalker), | ||
57 | ), | ||
58 | default => throw QueryException::semanticalError( | ||
59 | 'DATE_ADD() only supports units of type second, minute, hour, day, week, month and year.', | ||
60 | ), | ||
61 | }; | ||
62 | } | ||
63 | |||
64 | /** @throws ASTException */ | ||
65 | private function dispatchIntervalExpression(SqlWalker $sqlWalker): string | ||
66 | { | ||
67 | return $this->intervalExpression->dispatch($sqlWalker); | ||
68 | } | ||
69 | |||
70 | public function parse(Parser $parser): void | ||
71 | { | ||
72 | $parser->match(TokenType::T_IDENTIFIER); | ||
73 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
74 | |||
75 | $this->firstDateExpression = $parser->ArithmeticPrimary(); | ||
76 | $parser->match(TokenType::T_COMMA); | ||
77 | $this->intervalExpression = $parser->ArithmeticPrimary(); | ||
78 | $parser->match(TokenType::T_COMMA); | ||
79 | $this->unit = $parser->StringPrimary(); | ||
80 | |||
81 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
82 | } | ||
83 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php new file mode 100644 index 0000000..55598c0 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/DateDiffFunction.php | |||
@@ -0,0 +1,41 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | /** | ||
13 | * "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | ||
14 | * | ||
15 | * @link www.doctrine-project.org | ||
16 | */ | ||
17 | class DateDiffFunction extends FunctionNode | ||
18 | { | ||
19 | public Node $date1; | ||
20 | public Node $date2; | ||
21 | |||
22 | public function getSql(SqlWalker $sqlWalker): string | ||
23 | { | ||
24 | return $sqlWalker->getConnection()->getDatabasePlatform()->getDateDiffExpression( | ||
25 | $this->date1->dispatch($sqlWalker), | ||
26 | $this->date2->dispatch($sqlWalker), | ||
27 | ); | ||
28 | } | ||
29 | |||
30 | public function parse(Parser $parser): void | ||
31 | { | ||
32 | $parser->match(TokenType::T_IDENTIFIER); | ||
33 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
34 | |||
35 | $this->date1 = $parser->ArithmeticPrimary(); | ||
36 | $parser->match(TokenType::T_COMMA); | ||
37 | $this->date2 = $parser->ArithmeticPrimary(); | ||
38 | |||
39 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
40 | } | ||
41 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php new file mode 100644 index 0000000..5363680 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/DateSubFunction.php | |||
@@ -0,0 +1,62 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\ASTException; | ||
8 | use Doctrine\ORM\Query\QueryException; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | |||
11 | use function strtolower; | ||
12 | |||
13 | /** | ||
14 | * "DATE_SUB(date1, interval, unit)" | ||
15 | * | ||
16 | * @link www.doctrine-project.org | ||
17 | */ | ||
18 | class DateSubFunction extends DateAddFunction | ||
19 | { | ||
20 | public function getSql(SqlWalker $sqlWalker): string | ||
21 | { | ||
22 | return match (strtolower((string) $this->unit->value)) { | ||
23 | 'second' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubSecondsExpression( | ||
24 | $this->firstDateExpression->dispatch($sqlWalker), | ||
25 | $this->dispatchIntervalExpression($sqlWalker), | ||
26 | ), | ||
27 | 'minute' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubMinutesExpression( | ||
28 | $this->firstDateExpression->dispatch($sqlWalker), | ||
29 | $this->dispatchIntervalExpression($sqlWalker), | ||
30 | ), | ||
31 | 'hour' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubHourExpression( | ||
32 | $this->firstDateExpression->dispatch($sqlWalker), | ||
33 | $this->dispatchIntervalExpression($sqlWalker), | ||
34 | ), | ||
35 | 'day' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubDaysExpression( | ||
36 | $this->firstDateExpression->dispatch($sqlWalker), | ||
37 | $this->dispatchIntervalExpression($sqlWalker), | ||
38 | ), | ||
39 | 'week' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubWeeksExpression( | ||
40 | $this->firstDateExpression->dispatch($sqlWalker), | ||
41 | $this->dispatchIntervalExpression($sqlWalker), | ||
42 | ), | ||
43 | 'month' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubMonthExpression( | ||
44 | $this->firstDateExpression->dispatch($sqlWalker), | ||
45 | $this->dispatchIntervalExpression($sqlWalker), | ||
46 | ), | ||
47 | 'year' => $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubYearsExpression( | ||
48 | $this->firstDateExpression->dispatch($sqlWalker), | ||
49 | $this->dispatchIntervalExpression($sqlWalker), | ||
50 | ), | ||
51 | default => throw QueryException::semanticalError( | ||
52 | 'DATE_SUB() only supports units of type second, minute, hour, day, week, month and year.', | ||
53 | ), | ||
54 | }; | ||
55 | } | ||
56 | |||
57 | /** @throws ASTException */ | ||
58 | private function dispatchIntervalExpression(SqlWalker $sqlWalker): string | ||
59 | { | ||
60 | return $this->intervalExpression->dispatch($sqlWalker); | ||
61 | } | ||
62 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php b/vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php new file mode 100644 index 0000000..4cc549e --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/FunctionNode.php | |||
@@ -0,0 +1,32 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | |||
11 | /** | ||
12 | * Abstract Function Node. | ||
13 | * | ||
14 | * @link www.doctrine-project.org | ||
15 | * | ||
16 | * @psalm-consistent-constructor | ||
17 | */ | ||
18 | abstract class FunctionNode extends Node | ||
19 | { | ||
20 | public function __construct(public string $name) | ||
21 | { | ||
22 | } | ||
23 | |||
24 | abstract public function getSql(SqlWalker $sqlWalker): string; | ||
25 | |||
26 | public function dispatch(SqlWalker $sqlWalker): string | ||
27 | { | ||
28 | return $sqlWalker->walkFunction($this); | ||
29 | } | ||
30 | |||
31 | abstract public function parse(Parser $parser): void; | ||
32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php new file mode 100644 index 0000000..1dd1bf5 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/IdentityFunction.php | |||
@@ -0,0 +1,90 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\PathExpression; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\QueryException; | ||
10 | use Doctrine\ORM\Query\SqlWalker; | ||
11 | use Doctrine\ORM\Query\TokenType; | ||
12 | |||
13 | use function assert; | ||
14 | use function reset; | ||
15 | use function sprintf; | ||
16 | |||
17 | /** | ||
18 | * "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")" | ||
19 | * | ||
20 | * @link www.doctrine-project.org | ||
21 | */ | ||
22 | class IdentityFunction extends FunctionNode | ||
23 | { | ||
24 | public PathExpression $pathExpression; | ||
25 | |||
26 | public string|null $fieldMapping = null; | ||
27 | |||
28 | public function getSql(SqlWalker $sqlWalker): string | ||
29 | { | ||
30 | assert($this->pathExpression->field !== null); | ||
31 | $entityManager = $sqlWalker->getEntityManager(); | ||
32 | $platform = $entityManager->getConnection()->getDatabasePlatform(); | ||
33 | $quoteStrategy = $entityManager->getConfiguration()->getQuoteStrategy(); | ||
34 | $dqlAlias = $this->pathExpression->identificationVariable; | ||
35 | $assocField = $this->pathExpression->field; | ||
36 | $assoc = $sqlWalker->getMetadataForDqlAlias($dqlAlias)->associationMappings[$assocField]; | ||
37 | $targetEntity = $entityManager->getClassMetadata($assoc->targetEntity); | ||
38 | |||
39 | assert($assoc->isToOneOwningSide()); | ||
40 | $joinColumn = reset($assoc->joinColumns); | ||
41 | |||
42 | if ($this->fieldMapping !== null) { | ||
43 | if (! isset($targetEntity->fieldMappings[$this->fieldMapping])) { | ||
44 | throw new QueryException(sprintf('Undefined reference field mapping "%s"', $this->fieldMapping)); | ||
45 | } | ||
46 | |||
47 | $field = $targetEntity->fieldMappings[$this->fieldMapping]; | ||
48 | $joinColumn = null; | ||
49 | |||
50 | foreach ($assoc->joinColumns as $mapping) { | ||
51 | if ($mapping->referencedColumnName === $field->columnName) { | ||
52 | $joinColumn = $mapping; | ||
53 | |||
54 | break; | ||
55 | } | ||
56 | } | ||
57 | |||
58 | if ($joinColumn === null) { | ||
59 | throw new QueryException(sprintf('Unable to resolve the reference field mapping "%s"', $this->fieldMapping)); | ||
60 | } | ||
61 | } | ||
62 | |||
63 | // The table with the relation may be a subclass, so get the table name from the association definition | ||
64 | $tableName = $entityManager->getClassMetadata($assoc->sourceEntity)->getTableName(); | ||
65 | |||
66 | $tableAlias = $sqlWalker->getSQLTableAlias($tableName, $dqlAlias); | ||
67 | $columnName = $quoteStrategy->getJoinColumnName($joinColumn, $targetEntity, $platform); | ||
68 | |||
69 | return $tableAlias . '.' . $columnName; | ||
70 | } | ||
71 | |||
72 | public function parse(Parser $parser): void | ||
73 | { | ||
74 | $parser->match(TokenType::T_IDENTIFIER); | ||
75 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
76 | |||
77 | $this->pathExpression = $parser->SingleValuedAssociationPathExpression(); | ||
78 | |||
79 | if ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) { | ||
80 | $parser->match(TokenType::T_COMMA); | ||
81 | $parser->match(TokenType::T_STRING); | ||
82 | |||
83 | $token = $parser->getLexer()->token; | ||
84 | assert($token !== null); | ||
85 | $this->fieldMapping = $token->value; | ||
86 | } | ||
87 | |||
88 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
89 | } | ||
90 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php new file mode 100644 index 0000000..3994918 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/LengthFunction.php | |||
@@ -0,0 +1,45 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\DBAL\Types\Type; | ||
8 | use Doctrine\DBAL\Types\Types; | ||
9 | use Doctrine\ORM\Query\AST\Node; | ||
10 | use Doctrine\ORM\Query\AST\TypedExpression; | ||
11 | use Doctrine\ORM\Query\Parser; | ||
12 | use Doctrine\ORM\Query\SqlWalker; | ||
13 | use Doctrine\ORM\Query\TokenType; | ||
14 | |||
15 | /** | ||
16 | * "LENGTH" "(" StringPrimary ")" | ||
17 | * | ||
18 | * @link www.doctrine-project.org | ||
19 | */ | ||
20 | class LengthFunction extends FunctionNode implements TypedExpression | ||
21 | { | ||
22 | public Node $stringPrimary; | ||
23 | |||
24 | public function getSql(SqlWalker $sqlWalker): string | ||
25 | { | ||
26 | return $sqlWalker->getConnection()->getDatabasePlatform()->getLengthExpression( | ||
27 | $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary), | ||
28 | ); | ||
29 | } | ||
30 | |||
31 | public function parse(Parser $parser): void | ||
32 | { | ||
33 | $parser->match(TokenType::T_IDENTIFIER); | ||
34 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
35 | |||
36 | $this->stringPrimary = $parser->StringPrimary(); | ||
37 | |||
38 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
39 | } | ||
40 | |||
41 | public function getReturnType(): Type | ||
42 | { | ||
43 | return Type::getType(Types::INTEGER); | ||
44 | } | ||
45 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php new file mode 100644 index 0000000..c0d3b4a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/LocateFunction.php | |||
@@ -0,0 +1,62 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | /** | ||
13 | * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" | ||
14 | * | ||
15 | * @link www.doctrine-project.org | ||
16 | */ | ||
17 | class LocateFunction extends FunctionNode | ||
18 | { | ||
19 | public Node|string $firstStringPrimary; | ||
20 | public Node|string $secondStringPrimary; | ||
21 | |||
22 | public Node|string|bool $simpleArithmeticExpression = false; | ||
23 | |||
24 | public function getSql(SqlWalker $sqlWalker): string | ||
25 | { | ||
26 | $platform = $sqlWalker->getConnection()->getDatabasePlatform(); | ||
27 | |||
28 | $firstString = $sqlWalker->walkStringPrimary($this->firstStringPrimary); | ||
29 | $secondString = $sqlWalker->walkStringPrimary($this->secondStringPrimary); | ||
30 | |||
31 | if ($this->simpleArithmeticExpression) { | ||
32 | return $platform->getLocateExpression( | ||
33 | $secondString, | ||
34 | $firstString, | ||
35 | $sqlWalker->walkSimpleArithmeticExpression($this->simpleArithmeticExpression), | ||
36 | ); | ||
37 | } | ||
38 | |||
39 | return $platform->getLocateExpression($secondString, $firstString); | ||
40 | } | ||
41 | |||
42 | public function parse(Parser $parser): void | ||
43 | { | ||
44 | $parser->match(TokenType::T_IDENTIFIER); | ||
45 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
46 | |||
47 | $this->firstStringPrimary = $parser->StringPrimary(); | ||
48 | |||
49 | $parser->match(TokenType::T_COMMA); | ||
50 | |||
51 | $this->secondStringPrimary = $parser->StringPrimary(); | ||
52 | |||
53 | $lexer = $parser->getLexer(); | ||
54 | if ($lexer->isNextToken(TokenType::T_COMMA)) { | ||
55 | $parser->match(TokenType::T_COMMA); | ||
56 | |||
57 | $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
58 | } | ||
59 | |||
60 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
61 | } | ||
62 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php new file mode 100644 index 0000000..8ae337a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/LowerFunction.php | |||
@@ -0,0 +1,40 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | use function sprintf; | ||
13 | |||
14 | /** | ||
15 | * "LOWER" "(" StringPrimary ")" | ||
16 | * | ||
17 | * @link www.doctrine-project.org | ||
18 | */ | ||
19 | class LowerFunction extends FunctionNode | ||
20 | { | ||
21 | public Node $stringPrimary; | ||
22 | |||
23 | public function getSql(SqlWalker $sqlWalker): string | ||
24 | { | ||
25 | return sprintf( | ||
26 | 'LOWER(%s)', | ||
27 | $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary), | ||
28 | ); | ||
29 | } | ||
30 | |||
31 | public function parse(Parser $parser): void | ||
32 | { | ||
33 | $parser->match(TokenType::T_IDENTIFIER); | ||
34 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
35 | |||
36 | $this->stringPrimary = $parser->StringPrimary(); | ||
37 | |||
38 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
39 | } | ||
40 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php new file mode 100644 index 0000000..8a6eecf --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/MaxFunction.php | |||
@@ -0,0 +1,27 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | |||
11 | /** | ||
12 | * "MAX" "(" ["DISTINCT"] StringPrimary ")" | ||
13 | */ | ||
14 | final class MaxFunction extends FunctionNode | ||
15 | { | ||
16 | private AggregateExpression $aggregateExpression; | ||
17 | |||
18 | public function getSql(SqlWalker $sqlWalker): string | ||
19 | { | ||
20 | return $this->aggregateExpression->dispatch($sqlWalker); | ||
21 | } | ||
22 | |||
23 | public function parse(Parser $parser): void | ||
24 | { | ||
25 | $this->aggregateExpression = $parser->AggregateExpression(); | ||
26 | } | ||
27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php new file mode 100644 index 0000000..98d73a2 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/MinFunction.php | |||
@@ -0,0 +1,27 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | |||
11 | /** | ||
12 | * "MIN" "(" ["DISTINCT"] StringPrimary ")" | ||
13 | */ | ||
14 | final class MinFunction extends FunctionNode | ||
15 | { | ||
16 | private AggregateExpression $aggregateExpression; | ||
17 | |||
18 | public function getSql(SqlWalker $sqlWalker): string | ||
19 | { | ||
20 | return $this->aggregateExpression->dispatch($sqlWalker); | ||
21 | } | ||
22 | |||
23 | public function parse(Parser $parser): void | ||
24 | { | ||
25 | $this->aggregateExpression = $parser->AggregateExpression(); | ||
26 | } | ||
27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php new file mode 100644 index 0000000..7c1af0b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/ModFunction.php | |||
@@ -0,0 +1,43 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | /** | ||
13 | * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | ||
14 | * | ||
15 | * @link www.doctrine-project.org | ||
16 | */ | ||
17 | class ModFunction extends FunctionNode | ||
18 | { | ||
19 | public Node|string $firstSimpleArithmeticExpression; | ||
20 | public Node|string $secondSimpleArithmeticExpression; | ||
21 | |||
22 | public function getSql(SqlWalker $sqlWalker): string | ||
23 | { | ||
24 | return $sqlWalker->getConnection()->getDatabasePlatform()->getModExpression( | ||
25 | $sqlWalker->walkSimpleArithmeticExpression($this->firstSimpleArithmeticExpression), | ||
26 | $sqlWalker->walkSimpleArithmeticExpression($this->secondSimpleArithmeticExpression), | ||
27 | ); | ||
28 | } | ||
29 | |||
30 | public function parse(Parser $parser): void | ||
31 | { | ||
32 | $parser->match(TokenType::T_IDENTIFIER); | ||
33 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
34 | |||
35 | $this->firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
36 | |||
37 | $parser->match(TokenType::T_COMMA); | ||
38 | |||
39 | $this->secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
40 | |||
41 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
42 | } | ||
43 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php new file mode 100644 index 0000000..87ee713 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/SizeFunction.php | |||
@@ -0,0 +1,113 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\PathExpression; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | use function assert; | ||
13 | |||
14 | /** | ||
15 | * "SIZE" "(" CollectionValuedPathExpression ")" | ||
16 | * | ||
17 | * @link www.doctrine-project.org | ||
18 | */ | ||
19 | class SizeFunction extends FunctionNode | ||
20 | { | ||
21 | public PathExpression $collectionPathExpression; | ||
22 | |||
23 | /** | ||
24 | * @inheritdoc | ||
25 | * @todo If the collection being counted is already joined, the SQL can be simpler (more efficient). | ||
26 | */ | ||
27 | public function getSql(SqlWalker $sqlWalker): string | ||
28 | { | ||
29 | assert($this->collectionPathExpression->field !== null); | ||
30 | $entityManager = $sqlWalker->getEntityManager(); | ||
31 | $platform = $entityManager->getConnection()->getDatabasePlatform(); | ||
32 | $quoteStrategy = $entityManager->getConfiguration()->getQuoteStrategy(); | ||
33 | $dqlAlias = $this->collectionPathExpression->identificationVariable; | ||
34 | $assocField = $this->collectionPathExpression->field; | ||
35 | |||
36 | $class = $sqlWalker->getMetadataForDqlAlias($dqlAlias); | ||
37 | $assoc = $class->associationMappings[$assocField]; | ||
38 | $sql = 'SELECT COUNT(*) FROM '; | ||
39 | |||
40 | if ($assoc->isOneToMany()) { | ||
41 | $targetClass = $entityManager->getClassMetadata($assoc->targetEntity); | ||
42 | $targetTableAlias = $sqlWalker->getSQLTableAlias($targetClass->getTableName()); | ||
43 | $sourceTableAlias = $sqlWalker->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
44 | |||
45 | $sql .= $quoteStrategy->getTableName($targetClass, $platform) . ' ' . $targetTableAlias . ' WHERE '; | ||
46 | |||
47 | $owningAssoc = $targetClass->associationMappings[$assoc->mappedBy]; | ||
48 | assert($owningAssoc->isManyToOne()); | ||
49 | |||
50 | $first = true; | ||
51 | |||
52 | foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) { | ||
53 | if ($first) { | ||
54 | $first = false; | ||
55 | } else { | ||
56 | $sql .= ' AND '; | ||
57 | } | ||
58 | |||
59 | $sql .= $targetTableAlias . '.' . $sourceColumn | ||
60 | . ' = ' | ||
61 | . $sourceTableAlias . '.' . $quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $platform); | ||
62 | } | ||
63 | } else { // many-to-many | ||
64 | assert($assoc->isManyToMany()); | ||
65 | $owningAssoc = $entityManager->getMetadataFactory()->getOwningSide($assoc); | ||
66 | $joinTable = $owningAssoc->joinTable; | ||
67 | |||
68 | // SQL table aliases | ||
69 | $joinTableAlias = $sqlWalker->getSQLTableAlias($joinTable->name); | ||
70 | $sourceTableAlias = $sqlWalker->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
71 | |||
72 | // join to target table | ||
73 | $targetClass = $entityManager->getClassMetadata($assoc->targetEntity); | ||
74 | $sql .= $quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $platform) . ' ' . $joinTableAlias . ' WHERE '; | ||
75 | |||
76 | $joinColumns = $assoc->isOwningSide() | ||
77 | ? $joinTable->joinColumns | ||
78 | : $joinTable->inverseJoinColumns; | ||
79 | |||
80 | $first = true; | ||
81 | |||
82 | foreach ($joinColumns as $joinColumn) { | ||
83 | if ($first) { | ||
84 | $first = false; | ||
85 | } else { | ||
86 | $sql .= ' AND '; | ||
87 | } | ||
88 | |||
89 | $sourceColumnName = $quoteStrategy->getColumnName( | ||
90 | $class->fieldNames[$joinColumn->referencedColumnName], | ||
91 | $class, | ||
92 | $platform, | ||
93 | ); | ||
94 | |||
95 | $sql .= $joinTableAlias . '.' . $joinColumn->name | ||
96 | . ' = ' | ||
97 | . $sourceTableAlias . '.' . $sourceColumnName; | ||
98 | } | ||
99 | } | ||
100 | |||
101 | return '(' . $sql . ')'; | ||
102 | } | ||
103 | |||
104 | public function parse(Parser $parser): void | ||
105 | { | ||
106 | $parser->match(TokenType::T_IDENTIFIER); | ||
107 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
108 | |||
109 | $this->collectionPathExpression = $parser->CollectionValuedPathExpression(); | ||
110 | |||
111 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
112 | } | ||
113 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php new file mode 100644 index 0000000..e643663 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/SqrtFunction.php | |||
@@ -0,0 +1,40 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | use function sprintf; | ||
13 | |||
14 | /** | ||
15 | * "SQRT" "(" SimpleArithmeticExpression ")" | ||
16 | * | ||
17 | * @link www.doctrine-project.org | ||
18 | */ | ||
19 | class SqrtFunction extends FunctionNode | ||
20 | { | ||
21 | public Node|string $simpleArithmeticExpression; | ||
22 | |||
23 | public function getSql(SqlWalker $sqlWalker): string | ||
24 | { | ||
25 | return sprintf( | ||
26 | 'SQRT(%s)', | ||
27 | $sqlWalker->walkSimpleArithmeticExpression($this->simpleArithmeticExpression), | ||
28 | ); | ||
29 | } | ||
30 | |||
31 | public function parse(Parser $parser): void | ||
32 | { | ||
33 | $parser->match(TokenType::T_IDENTIFIER); | ||
34 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
35 | |||
36 | $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
37 | |||
38 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
39 | } | ||
40 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php new file mode 100644 index 0000000..5744f08 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/SubstringFunction.php | |||
@@ -0,0 +1,58 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | /** | ||
13 | * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | ||
14 | * | ||
15 | * @link www.doctrine-project.org | ||
16 | */ | ||
17 | class SubstringFunction extends FunctionNode | ||
18 | { | ||
19 | public Node $stringPrimary; | ||
20 | |||
21 | public Node|string $firstSimpleArithmeticExpression; | ||
22 | public Node|string|null $secondSimpleArithmeticExpression = null; | ||
23 | |||
24 | public function getSql(SqlWalker $sqlWalker): string | ||
25 | { | ||
26 | $optionalSecondSimpleArithmeticExpression = null; | ||
27 | if ($this->secondSimpleArithmeticExpression !== null) { | ||
28 | $optionalSecondSimpleArithmeticExpression = $sqlWalker->walkSimpleArithmeticExpression($this->secondSimpleArithmeticExpression); | ||
29 | } | ||
30 | |||
31 | return $sqlWalker->getConnection()->getDatabasePlatform()->getSubstringExpression( | ||
32 | $sqlWalker->walkStringPrimary($this->stringPrimary), | ||
33 | $sqlWalker->walkSimpleArithmeticExpression($this->firstSimpleArithmeticExpression), | ||
34 | $optionalSecondSimpleArithmeticExpression, | ||
35 | ); | ||
36 | } | ||
37 | |||
38 | public function parse(Parser $parser): void | ||
39 | { | ||
40 | $parser->match(TokenType::T_IDENTIFIER); | ||
41 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
42 | |||
43 | $this->stringPrimary = $parser->StringPrimary(); | ||
44 | |||
45 | $parser->match(TokenType::T_COMMA); | ||
46 | |||
47 | $this->firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
48 | |||
49 | $lexer = $parser->getLexer(); | ||
50 | if ($lexer->isNextToken(TokenType::T_COMMA)) { | ||
51 | $parser->match(TokenType::T_COMMA); | ||
52 | |||
53 | $this->secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); | ||
54 | } | ||
55 | |||
56 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
57 | } | ||
58 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php new file mode 100644 index 0000000..588dce9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/SumFunction.php | |||
@@ -0,0 +1,27 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\AggregateExpression; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | |||
11 | /** | ||
12 | * "SUM" "(" ["DISTINCT"] StringPrimary ")" | ||
13 | */ | ||
14 | final class SumFunction extends FunctionNode | ||
15 | { | ||
16 | private AggregateExpression $aggregateExpression; | ||
17 | |||
18 | public function getSql(SqlWalker $sqlWalker): string | ||
19 | { | ||
20 | return $this->aggregateExpression->dispatch($sqlWalker); | ||
21 | } | ||
22 | |||
23 | public function parse(Parser $parser): void | ||
24 | { | ||
25 | $this->aggregateExpression = $parser->AggregateExpression(); | ||
26 | } | ||
27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php new file mode 100644 index 0000000..e0a3e99 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/TrimFunction.php | |||
@@ -0,0 +1,119 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\DBAL\Platforms\TrimMode; | ||
8 | use Doctrine\ORM\Query\AST\Node; | ||
9 | use Doctrine\ORM\Query\Parser; | ||
10 | use Doctrine\ORM\Query\SqlWalker; | ||
11 | use Doctrine\ORM\Query\TokenType; | ||
12 | |||
13 | use function assert; | ||
14 | use function strcasecmp; | ||
15 | |||
16 | /** | ||
17 | * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" | ||
18 | * | ||
19 | * @link www.doctrine-project.org | ||
20 | */ | ||
21 | class TrimFunction extends FunctionNode | ||
22 | { | ||
23 | public bool $leading = false; | ||
24 | public bool $trailing = false; | ||
25 | public bool $both = false; | ||
26 | public string|false $trimChar = false; | ||
27 | public Node $stringPrimary; | ||
28 | |||
29 | public function getSql(SqlWalker $sqlWalker): string | ||
30 | { | ||
31 | $stringPrimary = $sqlWalker->walkStringPrimary($this->stringPrimary); | ||
32 | $platform = $sqlWalker->getConnection()->getDatabasePlatform(); | ||
33 | $trimMode = $this->getTrimMode(); | ||
34 | |||
35 | if ($this->trimChar !== false) { | ||
36 | return $platform->getTrimExpression( | ||
37 | $stringPrimary, | ||
38 | $trimMode, | ||
39 | $platform->quoteStringLiteral($this->trimChar), | ||
40 | ); | ||
41 | } | ||
42 | |||
43 | return $platform->getTrimExpression($stringPrimary, $trimMode); | ||
44 | } | ||
45 | |||
46 | public function parse(Parser $parser): void | ||
47 | { | ||
48 | $lexer = $parser->getLexer(); | ||
49 | |||
50 | $parser->match(TokenType::T_IDENTIFIER); | ||
51 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
52 | |||
53 | $this->parseTrimMode($parser); | ||
54 | |||
55 | if ($lexer->isNextToken(TokenType::T_STRING)) { | ||
56 | $parser->match(TokenType::T_STRING); | ||
57 | |||
58 | assert($lexer->token !== null); | ||
59 | $this->trimChar = $lexer->token->value; | ||
60 | } | ||
61 | |||
62 | if ($this->leading || $this->trailing || $this->both || ($this->trimChar !== false)) { | ||
63 | $parser->match(TokenType::T_FROM); | ||
64 | } | ||
65 | |||
66 | $this->stringPrimary = $parser->StringPrimary(); | ||
67 | |||
68 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
69 | } | ||
70 | |||
71 | /** @psalm-return TrimMode::* */ | ||
72 | private function getTrimMode(): TrimMode|int | ||
73 | { | ||
74 | if ($this->leading) { | ||
75 | return TrimMode::LEADING; | ||
76 | } | ||
77 | |||
78 | if ($this->trailing) { | ||
79 | return TrimMode::TRAILING; | ||
80 | } | ||
81 | |||
82 | if ($this->both) { | ||
83 | return TrimMode::BOTH; | ||
84 | } | ||
85 | |||
86 | return TrimMode::UNSPECIFIED; | ||
87 | } | ||
88 | |||
89 | private function parseTrimMode(Parser $parser): void | ||
90 | { | ||
91 | $lexer = $parser->getLexer(); | ||
92 | assert($lexer->lookahead !== null); | ||
93 | $value = $lexer->lookahead->value; | ||
94 | |||
95 | if (strcasecmp('leading', $value) === 0) { | ||
96 | $parser->match(TokenType::T_LEADING); | ||
97 | |||
98 | $this->leading = true; | ||
99 | |||
100 | return; | ||
101 | } | ||
102 | |||
103 | if (strcasecmp('trailing', $value) === 0) { | ||
104 | $parser->match(TokenType::T_TRAILING); | ||
105 | |||
106 | $this->trailing = true; | ||
107 | |||
108 | return; | ||
109 | } | ||
110 | |||
111 | if (strcasecmp('both', $value) === 0) { | ||
112 | $parser->match(TokenType::T_BOTH); | ||
113 | |||
114 | $this->both = true; | ||
115 | |||
116 | return; | ||
117 | } | ||
118 | } | ||
119 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php b/vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php new file mode 100644 index 0000000..1ecef66 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Functions/UpperFunction.php | |||
@@ -0,0 +1,40 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST\Functions; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Node; | ||
8 | use Doctrine\ORM\Query\Parser; | ||
9 | use Doctrine\ORM\Query\SqlWalker; | ||
10 | use Doctrine\ORM\Query\TokenType; | ||
11 | |||
12 | use function sprintf; | ||
13 | |||
14 | /** | ||
15 | * "UPPER" "(" StringPrimary ")" | ||
16 | * | ||
17 | * @link www.doctrine-project.org | ||
18 | */ | ||
19 | class UpperFunction extends FunctionNode | ||
20 | { | ||
21 | public Node $stringPrimary; | ||
22 | |||
23 | public function getSql(SqlWalker $sqlWalker): string | ||
24 | { | ||
25 | return sprintf( | ||
26 | 'UPPER(%s)', | ||
27 | $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary), | ||
28 | ); | ||
29 | } | ||
30 | |||
31 | public function parse(Parser $parser): void | ||
32 | { | ||
33 | $parser->match(TokenType::T_IDENTIFIER); | ||
34 | $parser->match(TokenType::T_OPEN_PARENTHESIS); | ||
35 | |||
36 | $this->stringPrimary = $parser->StringPrimary(); | ||
37 | |||
38 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); | ||
39 | } | ||
40 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php b/vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php new file mode 100644 index 0000000..39d760a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/GeneralCaseExpression.php | |||
@@ -0,0 +1,27 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class GeneralCaseExpression extends Node | ||
15 | { | ||
16 | /** @param mixed[] $whenClauses */ | ||
17 | public function __construct( | ||
18 | public array $whenClauses, | ||
19 | public mixed $elseScalarExpression = null, | ||
20 | ) { | ||
21 | } | ||
22 | |||
23 | public function dispatch(SqlWalker $walker): string | ||
24 | { | ||
25 | return $walker->walkGeneralCaseExpression($this); | ||
26 | } | ||
27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/GroupByClause.php b/vendor/doctrine/orm/src/Query/AST/GroupByClause.php new file mode 100644 index 0000000..eb0f1b9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/GroupByClause.php | |||
@@ -0,0 +1,20 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | class GroupByClause extends Node | ||
10 | { | ||
11 | /** @param mixed[] $groupByItems */ | ||
12 | public function __construct(public array $groupByItems) | ||
13 | { | ||
14 | } | ||
15 | |||
16 | public function dispatch(SqlWalker $walker): string | ||
17 | { | ||
18 | return $walker->walkGroupByClause($this); | ||
19 | } | ||
20 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/HavingClause.php b/vendor/doctrine/orm/src/Query/AST/HavingClause.php new file mode 100644 index 0000000..0d4d821 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/HavingClause.php | |||
@@ -0,0 +1,19 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | class HavingClause extends Node | ||
10 | { | ||
11 | public function __construct(public ConditionalExpression|Phase2OptimizableConditional $conditionalExpression) | ||
12 | { | ||
13 | } | ||
14 | |||
15 | public function dispatch(SqlWalker $walker): string | ||
16 | { | ||
17 | return $walker->walkHavingClause($this); | ||
18 | } | ||
19 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php new file mode 100644 index 0000000..c4c7cca --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/IdentificationVariableDeclaration.php | |||
@@ -0,0 +1,28 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}* | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class IdentificationVariableDeclaration extends Node | ||
15 | { | ||
16 | /** @param mixed[] $joins */ | ||
17 | public function __construct( | ||
18 | public RangeVariableDeclaration|null $rangeVariableDeclaration = null, | ||
19 | public IndexBy|null $indexBy = null, | ||
20 | public array $joins = [], | ||
21 | ) { | ||
22 | } | ||
23 | |||
24 | public function dispatch(SqlWalker $walker): string | ||
25 | { | ||
26 | return $walker->walkIdentificationVariableDeclaration($this); | ||
27 | } | ||
28 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/InListExpression.php b/vendor/doctrine/orm/src/Query/AST/InListExpression.php new file mode 100644 index 0000000..dc0f32b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/InListExpression.php | |||
@@ -0,0 +1,23 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | class InListExpression extends Node | ||
10 | { | ||
11 | /** @param non-empty-list<mixed> $literals */ | ||
12 | public function __construct( | ||
13 | public ArithmeticExpression $expression, | ||
14 | public array $literals, | ||
15 | public bool $not = false, | ||
16 | ) { | ||
17 | } | ||
18 | |||
19 | public function dispatch(SqlWalker $walker): string | ||
20 | { | ||
21 | return $walker->walkInListExpression($this); | ||
22 | } | ||
23 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php b/vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php new file mode 100644 index 0000000..1128285 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/InSubselectExpression.php | |||
@@ -0,0 +1,22 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | class InSubselectExpression extends Node | ||
10 | { | ||
11 | public function __construct( | ||
12 | public ArithmeticExpression $expression, | ||
13 | public Subselect $subselect, | ||
14 | public bool $not = false, | ||
15 | ) { | ||
16 | } | ||
17 | |||
18 | public function dispatch(SqlWalker $walker): string | ||
19 | { | ||
20 | return $walker->walkInSubselectExpression($this); | ||
21 | } | ||
22 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/IndexBy.php b/vendor/doctrine/orm/src/Query/AST/IndexBy.php new file mode 100644 index 0000000..3d90265 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/IndexBy.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * IndexBy ::= "INDEX" "BY" SingleValuedPathExpression | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class IndexBy extends Node | ||
15 | { | ||
16 | public function __construct(public PathExpression $singleValuedPathExpression) | ||
17 | { | ||
18 | } | ||
19 | |||
20 | public function dispatch(SqlWalker $walker): string | ||
21 | { | ||
22 | $walker->walkIndexBy($this); | ||
23 | |||
24 | return ''; | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/InputParameter.php b/vendor/doctrine/orm/src/Query/AST/InputParameter.php new file mode 100644 index 0000000..a8e0a3b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/InputParameter.php | |||
@@ -0,0 +1,35 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\QueryException; | ||
8 | use Doctrine\ORM\Query\SqlWalker; | ||
9 | |||
10 | use function is_numeric; | ||
11 | use function strlen; | ||
12 | use function substr; | ||
13 | |||
14 | class InputParameter extends Node | ||
15 | { | ||
16 | public bool $isNamed; | ||
17 | public string $name; | ||
18 | |||
19 | /** @throws QueryException */ | ||
20 | public function __construct(string $value) | ||
21 | { | ||
22 | if (strlen($value) === 1) { | ||
23 | throw QueryException::invalidParameterFormat($value); | ||
24 | } | ||
25 | |||
26 | $param = substr($value, 1); | ||
27 | $this->isNamed = ! is_numeric($param); | ||
28 | $this->name = $param; | ||
29 | } | ||
30 | |||
31 | public function dispatch(SqlWalker $walker): string | ||
32 | { | ||
33 | return $walker->walkInputParameter($this); | ||
34 | } | ||
35 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php b/vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php new file mode 100644 index 0000000..3a4e75f --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/InstanceOfExpression.php | |||
@@ -0,0 +1,29 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")") | ||
11 | * InstanceOfParameter ::= AbstractSchemaName | InputParameter | ||
12 | * | ||
13 | * @link www.doctrine-project.org | ||
14 | */ | ||
15 | class InstanceOfExpression extends Node | ||
16 | { | ||
17 | /** @param non-empty-list<InputParameter|string> $value */ | ||
18 | public function __construct( | ||
19 | public string $identificationVariable, | ||
20 | public array $value, | ||
21 | public bool $not = false, | ||
22 | ) { | ||
23 | } | ||
24 | |||
25 | public function dispatch(SqlWalker $walker): string | ||
26 | { | ||
27 | return $walker->walkInstanceOfExpression($this); | ||
28 | } | ||
29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Join.php b/vendor/doctrine/orm/src/Query/AST/Join.php new file mode 100644 index 0000000..34ce830 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Join.php | |||
@@ -0,0 +1,34 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression | ||
11 | * ["AS"] AliasIdentificationVariable [("ON" | "WITH") ConditionalExpression] | ||
12 | * | ||
13 | * @link www.doctrine-project.org | ||
14 | */ | ||
15 | class Join extends Node | ||
16 | { | ||
17 | final public const JOIN_TYPE_LEFT = 1; | ||
18 | final public const JOIN_TYPE_LEFTOUTER = 2; | ||
19 | final public const JOIN_TYPE_INNER = 3; | ||
20 | |||
21 | public ConditionalExpression|Phase2OptimizableConditional|null $conditionalExpression = null; | ||
22 | |||
23 | /** @psalm-param self::JOIN_TYPE_* $joinType */ | ||
24 | public function __construct( | ||
25 | public int $joinType, | ||
26 | public Node|null $joinAssociationDeclaration = null, | ||
27 | ) { | ||
28 | } | ||
29 | |||
30 | public function dispatch(SqlWalker $walker): string | ||
31 | { | ||
32 | return $walker->walkJoin($this); | ||
33 | } | ||
34 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php b/vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php new file mode 100644 index 0000000..e08d7f5 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/JoinAssociationDeclaration.php | |||
@@ -0,0 +1,27 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class JoinAssociationDeclaration extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public JoinAssociationPathExpression $joinAssociationPathExpression, | ||
18 | public string $aliasIdentificationVariable, | ||
19 | public IndexBy|null $indexBy, | ||
20 | ) { | ||
21 | } | ||
22 | |||
23 | public function dispatch(SqlWalker $walker): string | ||
24 | { | ||
25 | return $walker->walkJoinAssociationDeclaration($this); | ||
26 | } | ||
27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php b/vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php new file mode 100644 index 0000000..230be36 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/JoinAssociationPathExpression.php | |||
@@ -0,0 +1,19 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | /** | ||
8 | * JoinAssociationPathExpression ::= IdentificationVariable "." (SingleValuedAssociationField | CollectionValuedAssociationField) | ||
9 | * | ||
10 | * @link www.doctrine-project.org | ||
11 | */ | ||
12 | class JoinAssociationPathExpression extends Node | ||
13 | { | ||
14 | public function __construct( | ||
15 | public string $identificationVariable, | ||
16 | public string $associationField, | ||
17 | ) { | ||
18 | } | ||
19 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php b/vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php new file mode 100644 index 0000000..cc92782 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/JoinClassPathExpression.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * JoinClassPathExpression ::= AbstractSchemaName ["AS"] AliasIdentificationVariable | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class JoinClassPathExpression extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public mixed $abstractSchemaName, | ||
18 | public mixed $aliasIdentificationVariable, | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkJoinPathExpression($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php new file mode 100644 index 0000000..bf76695 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/JoinVariableDeclaration.php | |||
@@ -0,0 +1,24 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * JoinVariableDeclaration ::= Join [IndexBy] | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class JoinVariableDeclaration extends Node | ||
15 | { | ||
16 | public function __construct(public Join $join, public IndexBy|null $indexBy) | ||
17 | { | ||
18 | } | ||
19 | |||
20 | public function dispatch(SqlWalker $walker): string | ||
21 | { | ||
22 | return $walker->walkJoinVariableDeclaration($this); | ||
23 | } | ||
24 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/LikeExpression.php b/vendor/doctrine/orm/src/Query/AST/LikeExpression.php new file mode 100644 index 0000000..e3f67f8 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/LikeExpression.php | |||
@@ -0,0 +1,29 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\AST\Functions\FunctionNode; | ||
8 | use Doctrine\ORM\Query\SqlWalker; | ||
9 | |||
10 | /** | ||
11 | * LikeExpression ::= StringExpression ["NOT"] "LIKE" string ["ESCAPE" char] | ||
12 | * | ||
13 | * @link www.doctrine-project.org | ||
14 | */ | ||
15 | class LikeExpression extends Node | ||
16 | { | ||
17 | public function __construct( | ||
18 | public Node|string $stringExpression, | ||
19 | public InputParameter|FunctionNode|PathExpression|Literal $stringPattern, | ||
20 | public Literal|null $escapeChar = null, | ||
21 | public bool $not = false, | ||
22 | ) { | ||
23 | } | ||
24 | |||
25 | public function dispatch(SqlWalker $walker): string | ||
26 | { | ||
27 | return $walker->walkLikeExpression($this); | ||
28 | } | ||
29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Literal.php b/vendor/doctrine/orm/src/Query/AST/Literal.php new file mode 100644 index 0000000..9ec2036 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Literal.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | class Literal extends Node | ||
10 | { | ||
11 | final public const STRING = 1; | ||
12 | final public const BOOLEAN = 2; | ||
13 | final public const NUMERIC = 3; | ||
14 | |||
15 | /** @psalm-param self::* $type */ | ||
16 | public function __construct( | ||
17 | public int $type, | ||
18 | public mixed $value, | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkLiteral($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php b/vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php new file mode 100644 index 0000000..7383c48 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/NewObjectExpression.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * NewObjectExpression ::= "NEW" IdentificationVariable "(" NewObjectArg {"," NewObjectArg}* ")" | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class NewObjectExpression extends Node | ||
15 | { | ||
16 | /** @param mixed[] $args */ | ||
17 | public function __construct(public string $className, public array $args) | ||
18 | { | ||
19 | } | ||
20 | |||
21 | public function dispatch(SqlWalker $walker): string | ||
22 | { | ||
23 | return $walker->walkNewObject($this); | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Node.php b/vendor/doctrine/orm/src/Query/AST/Node.php new file mode 100644 index 0000000..cdb5855 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Node.php | |||
@@ -0,0 +1,85 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | use Stringable; | ||
9 | |||
10 | use function get_debug_type; | ||
11 | use function get_object_vars; | ||
12 | use function is_array; | ||
13 | use function is_object; | ||
14 | use function str_repeat; | ||
15 | use function var_export; | ||
16 | |||
17 | use const PHP_EOL; | ||
18 | |||
19 | /** | ||
20 | * Abstract class of an AST node. | ||
21 | * | ||
22 | * @link www.doctrine-project.org | ||
23 | */ | ||
24 | abstract class Node implements Stringable | ||
25 | { | ||
26 | /** | ||
27 | * Double-dispatch method, supposed to dispatch back to the walker. | ||
28 | * | ||
29 | * Implementation is not mandatory for all nodes. | ||
30 | * | ||
31 | * @throws ASTException | ||
32 | */ | ||
33 | public function dispatch(SqlWalker $walker): string | ||
34 | { | ||
35 | throw ASTException::noDispatchForNode($this); | ||
36 | } | ||
37 | |||
38 | /** | ||
39 | * Dumps the AST Node into a string representation for information purpose only. | ||
40 | */ | ||
41 | public function __toString(): string | ||
42 | { | ||
43 | return $this->dump($this); | ||
44 | } | ||
45 | |||
46 | public function dump(mixed $value): string | ||
47 | { | ||
48 | static $ident = 0; | ||
49 | |||
50 | $str = ''; | ||
51 | |||
52 | if ($value instanceof Node) { | ||
53 | $str .= get_debug_type($value) . '(' . PHP_EOL; | ||
54 | $props = get_object_vars($value); | ||
55 | |||
56 | foreach ($props as $name => $prop) { | ||
57 | $ident += 4; | ||
58 | $str .= str_repeat(' ', $ident) . '"' . $name . '": ' | ||
59 | . $this->dump($prop) . ',' . PHP_EOL; | ||
60 | $ident -= 4; | ||
61 | } | ||
62 | |||
63 | $str .= str_repeat(' ', $ident) . ')'; | ||
64 | } elseif (is_array($value)) { | ||
65 | $ident += 4; | ||
66 | $str .= 'array('; | ||
67 | $some = false; | ||
68 | |||
69 | foreach ($value as $k => $v) { | ||
70 | $str .= PHP_EOL . str_repeat(' ', $ident) . '"' | ||
71 | . $k . '" => ' . $this->dump($v) . ','; | ||
72 | $some = true; | ||
73 | } | ||
74 | |||
75 | $ident -= 4; | ||
76 | $str .= ($some ? PHP_EOL . str_repeat(' ', $ident) : '') . ')'; | ||
77 | } elseif (is_object($value)) { | ||
78 | $str .= 'instanceof(' . get_debug_type($value) . ')'; | ||
79 | } else { | ||
80 | $str .= var_export($value, true); | ||
81 | } | ||
82 | |||
83 | return $str; | ||
84 | } | ||
85 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php b/vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php new file mode 100644 index 0000000..e60cb04 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/NullComparisonExpression.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * NullComparisonExpression ::= (SingleValuedPathExpression | InputParameter) "IS" ["NOT"] "NULL" | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class NullComparisonExpression extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public Node|string $expression, | ||
18 | public bool $not = false, | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkNullComparisonExpression($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/NullIfExpression.php b/vendor/doctrine/orm/src/Query/AST/NullIfExpression.php new file mode 100644 index 0000000..6fffeeb --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/NullIfExpression.php | |||
@@ -0,0 +1,24 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class NullIfExpression extends Node | ||
15 | { | ||
16 | public function __construct(public mixed $firstExpression, public mixed $secondExpression) | ||
17 | { | ||
18 | } | ||
19 | |||
20 | public function dispatch(SqlWalker $walker): string | ||
21 | { | ||
22 | return $walker->walkNullIfExpression($this); | ||
23 | } | ||
24 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/OrderByClause.php b/vendor/doctrine/orm/src/Query/AST/OrderByClause.php new file mode 100644 index 0000000..f6d7a67 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/OrderByClause.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class OrderByClause extends Node | ||
15 | { | ||
16 | /** @param OrderByItem[] $orderByItems */ | ||
17 | public function __construct(public array $orderByItems) | ||
18 | { | ||
19 | } | ||
20 | |||
21 | public function dispatch(SqlWalker $walker): string | ||
22 | { | ||
23 | return $walker->walkOrderByClause($this); | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/OrderByItem.php b/vendor/doctrine/orm/src/Query/AST/OrderByItem.php new file mode 100644 index 0000000..64b3f40 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/OrderByItem.php | |||
@@ -0,0 +1,38 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | use function strtoupper; | ||
10 | |||
11 | /** | ||
12 | * OrderByItem ::= (ResultVariable | StateFieldPathExpression) ["ASC" | "DESC"] | ||
13 | * | ||
14 | * @link www.doctrine-project.org | ||
15 | */ | ||
16 | class OrderByItem extends Node | ||
17 | { | ||
18 | public string $type; | ||
19 | |||
20 | public function __construct(public mixed $expression) | ||
21 | { | ||
22 | } | ||
23 | |||
24 | public function isAsc(): bool | ||
25 | { | ||
26 | return strtoupper($this->type) === 'ASC'; | ||
27 | } | ||
28 | |||
29 | public function isDesc(): bool | ||
30 | { | ||
31 | return strtoupper($this->type) === 'DESC'; | ||
32 | } | ||
33 | |||
34 | public function dispatch(SqlWalker $walker): string | ||
35 | { | ||
36 | return $walker->walkOrderByItem($this); | ||
37 | } | ||
38 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php b/vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php new file mode 100644 index 0000000..cda6d19 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/ParenthesisExpression.php | |||
@@ -0,0 +1,22 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * ParenthesisExpression ::= "(" ArithmeticPrimary ")" | ||
11 | */ | ||
12 | class ParenthesisExpression extends Node | ||
13 | { | ||
14 | public function __construct(public Node $expression) | ||
15 | { | ||
16 | } | ||
17 | |||
18 | public function dispatch(SqlWalker $walker): string | ||
19 | { | ||
20 | return $walker->walkParenthesisExpression($this); | ||
21 | } | ||
22 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/PathExpression.php b/vendor/doctrine/orm/src/Query/AST/PathExpression.php new file mode 100644 index 0000000..4a56fcd --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/PathExpression.php | |||
@@ -0,0 +1,39 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression | ||
11 | * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression | ||
12 | * StateFieldPathExpression ::= SimpleStateFieldPathExpression | SimpleStateFieldAssociationPathExpression | ||
13 | * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField | ||
14 | * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField | ||
15 | * StateField ::= {EmbeddedClassStateField "."}* SimpleStateField | ||
16 | * SimpleStateFieldPathExpression ::= IdentificationVariable "." StateField | ||
17 | */ | ||
18 | class PathExpression extends Node | ||
19 | { | ||
20 | final public const TYPE_COLLECTION_VALUED_ASSOCIATION = 2; | ||
21 | final public const TYPE_SINGLE_VALUED_ASSOCIATION = 4; | ||
22 | final public const TYPE_STATE_FIELD = 8; | ||
23 | |||
24 | /** @psalm-var self::TYPE_*|null */ | ||
25 | public int|null $type = null; | ||
26 | |||
27 | /** @psalm-param int-mask-of<self::TYPE_*> $expectedType */ | ||
28 | public function __construct( | ||
29 | public int $expectedType, | ||
30 | public string $identificationVariable, | ||
31 | public string|null $field = null, | ||
32 | ) { | ||
33 | } | ||
34 | |||
35 | public function dispatch(SqlWalker $walker): string | ||
36 | { | ||
37 | return $walker->walkPathExpression($this); | ||
38 | } | ||
39 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php b/vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php new file mode 100644 index 0000000..276f8f8 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Phase2OptimizableConditional.php | |||
@@ -0,0 +1,17 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | /** | ||
8 | * Marks types that can be used in place of a ConditionalExpression as a phase | ||
9 | * 2 optimization. | ||
10 | * | ||
11 | * @internal | ||
12 | * | ||
13 | * @psalm-inheritors ConditionalPrimary|ConditionalFactor|ConditionalTerm | ||
14 | */ | ||
15 | interface Phase2OptimizableConditional | ||
16 | { | ||
17 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php b/vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php new file mode 100644 index 0000000..90331cd --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/QuantifiedExpression.php | |||
@@ -0,0 +1,43 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | use function strtoupper; | ||
10 | |||
11 | /** | ||
12 | * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")" | ||
13 | * | ||
14 | * @link www.doctrine-project.org | ||
15 | */ | ||
16 | class QuantifiedExpression extends Node | ||
17 | { | ||
18 | public string $type; | ||
19 | |||
20 | public function __construct(public Subselect $subselect) | ||
21 | { | ||
22 | } | ||
23 | |||
24 | public function isAll(): bool | ||
25 | { | ||
26 | return strtoupper($this->type) === 'ALL'; | ||
27 | } | ||
28 | |||
29 | public function isAny(): bool | ||
30 | { | ||
31 | return strtoupper($this->type) === 'ANY'; | ||
32 | } | ||
33 | |||
34 | public function isSome(): bool | ||
35 | { | ||
36 | return strtoupper($this->type) === 'SOME'; | ||
37 | } | ||
38 | |||
39 | public function dispatch(SqlWalker $walker): string | ||
40 | { | ||
41 | return $walker->walkQuantifiedExpression($this); | ||
42 | } | ||
43 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php new file mode 100644 index 0000000..59bd5c8 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/RangeVariableDeclaration.php | |||
@@ -0,0 +1,27 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class RangeVariableDeclaration extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public string $abstractSchemaName, | ||
18 | public string $aliasIdentificationVariable, | ||
19 | public bool $isRoot = true, | ||
20 | ) { | ||
21 | } | ||
22 | |||
23 | public function dispatch(SqlWalker $walker): string | ||
24 | { | ||
25 | return $walker->walkRangeVariableDeclaration($this); | ||
26 | } | ||
27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SelectClause.php b/vendor/doctrine/orm/src/Query/AST/SelectClause.php new file mode 100644 index 0000000..ad50e67 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SelectClause.php | |||
@@ -0,0 +1,27 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * SelectClause = "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class SelectClause extends Node | ||
15 | { | ||
16 | /** @param mixed[] $selectExpressions */ | ||
17 | public function __construct( | ||
18 | public array $selectExpressions, | ||
19 | public bool $isDistinct, | ||
20 | ) { | ||
21 | } | ||
22 | |||
23 | public function dispatch(SqlWalker $walker): string | ||
24 | { | ||
25 | return $walker->walkSelectClause($this); | ||
26 | } | ||
27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SelectExpression.php b/vendor/doctrine/orm/src/Query/AST/SelectExpression.php new file mode 100644 index 0000000..f09f3cd --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SelectExpression.php | |||
@@ -0,0 +1,28 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * SelectExpression ::= IdentificationVariable ["." "*"] | StateFieldPathExpression | | ||
11 | * (AggregateExpression | "(" Subselect ")") [["AS"] ["HIDDEN"] FieldAliasIdentificationVariable] | ||
12 | * | ||
13 | * @link www.doctrine-project.org | ||
14 | */ | ||
15 | class SelectExpression extends Node | ||
16 | { | ||
17 | public function __construct( | ||
18 | public mixed $expression, | ||
19 | public string|null $fieldIdentificationVariable, | ||
20 | public bool $hiddenAliasResultVariable = false, | ||
21 | ) { | ||
22 | } | ||
23 | |||
24 | public function dispatch(SqlWalker $walker): string | ||
25 | { | ||
26 | return $walker->walkSelectExpression($this); | ||
27 | } | ||
28 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SelectStatement.php b/vendor/doctrine/orm/src/Query/AST/SelectStatement.php new file mode 100644 index 0000000..399462f --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SelectStatement.php | |||
@@ -0,0 +1,32 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * SelectStatement = SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class SelectStatement extends Node | ||
15 | { | ||
16 | public WhereClause|null $whereClause = null; | ||
17 | |||
18 | public GroupByClause|null $groupByClause = null; | ||
19 | |||
20 | public HavingClause|null $havingClause = null; | ||
21 | |||
22 | public OrderByClause|null $orderByClause = null; | ||
23 | |||
24 | public function __construct(public SelectClause $selectClause, public FromClause $fromClause) | ||
25 | { | ||
26 | } | ||
27 | |||
28 | public function dispatch(SqlWalker $walker): string | ||
29 | { | ||
30 | return $walker->walkSelectStatement($this); | ||
31 | } | ||
32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php b/vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php new file mode 100644 index 0000000..ae7ca44 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SimpleArithmeticExpression.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}* | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class SimpleArithmeticExpression extends Node | ||
15 | { | ||
16 | /** @param mixed[] $arithmeticTerms */ | ||
17 | public function __construct(public array $arithmeticTerms) | ||
18 | { | ||
19 | } | ||
20 | |||
21 | public function dispatch(SqlWalker $walker): string | ||
22 | { | ||
23 | return $walker->walkSimpleArithmeticExpression($this); | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php b/vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php new file mode 100644 index 0000000..b3764ba --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SimpleCaseExpression.php | |||
@@ -0,0 +1,28 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class SimpleCaseExpression extends Node | ||
15 | { | ||
16 | /** @param mixed[] $simpleWhenClauses */ | ||
17 | public function __construct( | ||
18 | public PathExpression|null $caseOperand = null, | ||
19 | public array $simpleWhenClauses = [], | ||
20 | public mixed $elseScalarExpression = null, | ||
21 | ) { | ||
22 | } | ||
23 | |||
24 | public function dispatch(SqlWalker $walker): string | ||
25 | { | ||
26 | return $walker->walkSimpleCaseExpression($this); | ||
27 | } | ||
28 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php b/vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php new file mode 100644 index 0000000..0259e3b --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SimpleSelectClause.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class SimpleSelectClause extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public SimpleSelectExpression $simpleSelectExpression, | ||
18 | public bool $isDistinct = false, | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkSimpleSelectClause($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php b/vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php new file mode 100644 index 0000000..97e8f08 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SimpleSelectExpression.php | |||
@@ -0,0 +1,27 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * SimpleSelectExpression ::= StateFieldPathExpression | IdentificationVariable | ||
11 | * | (AggregateExpression [["AS"] FieldAliasIdentificationVariable]) | ||
12 | * | ||
13 | * @link www.doctrine-project.org | ||
14 | */ | ||
15 | class SimpleSelectExpression extends Node | ||
16 | { | ||
17 | public string|null $fieldIdentificationVariable = null; | ||
18 | |||
19 | public function __construct(public Node|string $expression) | ||
20 | { | ||
21 | } | ||
22 | |||
23 | public function dispatch(SqlWalker $walker): string | ||
24 | { | ||
25 | return $walker->walkSimpleSelectExpression($this); | ||
26 | } | ||
27 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php b/vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php new file mode 100644 index 0000000..892165a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SimpleWhenClause.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class SimpleWhenClause extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public mixed $caseScalarExpression = null, | ||
18 | public mixed $thenScalarExpression = null, | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkWhenClauseExpression($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/Subselect.php b/vendor/doctrine/orm/src/Query/AST/Subselect.php new file mode 100644 index 0000000..8ff8595 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/Subselect.php | |||
@@ -0,0 +1,32 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class Subselect extends Node | ||
15 | { | ||
16 | public WhereClause|null $whereClause = null; | ||
17 | |||
18 | public GroupByClause|null $groupByClause = null; | ||
19 | |||
20 | public HavingClause|null $havingClause = null; | ||
21 | |||
22 | public OrderByClause|null $orderByClause = null; | ||
23 | |||
24 | public function __construct(public SimpleSelectClause $simpleSelectClause, public SubselectFromClause $subselectFromClause) | ||
25 | { | ||
26 | } | ||
27 | |||
28 | public function dispatch(SqlWalker $walker): string | ||
29 | { | ||
30 | return $walker->walkSubselect($this); | ||
31 | } | ||
32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php b/vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php new file mode 100644 index 0000000..7cf01e2 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SubselectFromClause.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}* | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class SubselectFromClause extends Node | ||
15 | { | ||
16 | /** @param mixed[] $identificationVariableDeclarations */ | ||
17 | public function __construct(public array $identificationVariableDeclarations) | ||
18 | { | ||
19 | } | ||
20 | |||
21 | public function dispatch(SqlWalker $walker): string | ||
22 | { | ||
23 | return $walker->walkSubselectFromClause($this); | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php b/vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php new file mode 100644 index 0000000..eadf6bc --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/SubselectIdentificationVariableDeclaration.php | |||
@@ -0,0 +1,19 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | /** | ||
8 | * SubselectIdentificationVariableDeclaration ::= AssociationPathExpression ["AS"] AliasIdentificationVariable | ||
9 | * | ||
10 | * @link www.doctrine-project.org | ||
11 | */ | ||
12 | class SubselectIdentificationVariableDeclaration | ||
13 | { | ||
14 | public function __construct( | ||
15 | public PathExpression $associationPathExpression, | ||
16 | public string $aliasIdentificationVariable, | ||
17 | ) { | ||
18 | } | ||
19 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/TypedExpression.php b/vendor/doctrine/orm/src/Query/AST/TypedExpression.php new file mode 100644 index 0000000..d7cf76f --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/TypedExpression.php | |||
@@ -0,0 +1,15 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\DBAL\Types\Type; | ||
8 | |||
9 | /** | ||
10 | * Provides an API for resolving the type of a Node | ||
11 | */ | ||
12 | interface TypedExpression | ||
13 | { | ||
14 | public function getReturnType(): Type; | ||
15 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/UpdateClause.php b/vendor/doctrine/orm/src/Query/AST/UpdateClause.php new file mode 100644 index 0000000..cafcff0 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/UpdateClause.php | |||
@@ -0,0 +1,29 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * UpdateClause ::= "UPDATE" AbstractSchemaName [["AS"] AliasIdentificationVariable] "SET" UpdateItem {"," UpdateItem}* | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class UpdateClause extends Node | ||
15 | { | ||
16 | public string $aliasIdentificationVariable; | ||
17 | |||
18 | /** @param mixed[] $updateItems */ | ||
19 | public function __construct( | ||
20 | public string $abstractSchemaName, | ||
21 | public array $updateItems, | ||
22 | ) { | ||
23 | } | ||
24 | |||
25 | public function dispatch(SqlWalker $walker): string | ||
26 | { | ||
27 | return $walker->walkUpdateClause($this); | ||
28 | } | ||
29 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/UpdateItem.php b/vendor/doctrine/orm/src/Query/AST/UpdateItem.php new file mode 100644 index 0000000..b540593 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/UpdateItem.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * UpdateItem ::= [IdentificationVariable "."] {StateField | SingleValuedAssociationField} "=" NewValue | ||
11 | * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary | | ||
12 | * EnumPrimary | SimpleEntityExpression | "NULL" | ||
13 | * | ||
14 | * @link www.doctrine-project.org | ||
15 | */ | ||
16 | class UpdateItem extends Node | ||
17 | { | ||
18 | public function __construct(public PathExpression $pathExpression, public InputParameter|ArithmeticExpression|null $newValue) | ||
19 | { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkUpdateItem($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/UpdateStatement.php b/vendor/doctrine/orm/src/Query/AST/UpdateStatement.php new file mode 100644 index 0000000..7ea5076 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/UpdateStatement.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * UpdateStatement = UpdateClause [WhereClause] | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class UpdateStatement extends Node | ||
15 | { | ||
16 | public WhereClause|null $whereClause = null; | ||
17 | |||
18 | public function __construct(public UpdateClause $updateClause) | ||
19 | { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkUpdateStatement($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/WhenClause.php b/vendor/doctrine/orm/src/Query/AST/WhenClause.php new file mode 100644 index 0000000..9bf194e --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/WhenClause.php | |||
@@ -0,0 +1,26 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class WhenClause extends Node | ||
15 | { | ||
16 | public function __construct( | ||
17 | public ConditionalExpression|Phase2OptimizableConditional $caseConditionExpression, | ||
18 | public mixed $thenScalarExpression = null, | ||
19 | ) { | ||
20 | } | ||
21 | |||
22 | public function dispatch(SqlWalker $walker): string | ||
23 | { | ||
24 | return $walker->walkWhenClauseExpression($this); | ||
25 | } | ||
26 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/AST/WhereClause.php b/vendor/doctrine/orm/src/Query/AST/WhereClause.php new file mode 100644 index 0000000..e4d7b66 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/AST/WhereClause.php | |||
@@ -0,0 +1,24 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\AST; | ||
6 | |||
7 | use Doctrine\ORM\Query\SqlWalker; | ||
8 | |||
9 | /** | ||
10 | * WhereClause ::= "WHERE" ConditionalExpression | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class WhereClause extends Node | ||
15 | { | ||
16 | public function __construct(public ConditionalExpression|Phase2OptimizableConditional $conditionalExpression) | ||
17 | { | ||
18 | } | ||
19 | |||
20 | public function dispatch(SqlWalker $walker): string | ||
21 | { | ||
22 | return $walker->walkWhereClause($this); | ||
23 | } | ||
24 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php b/vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php new file mode 100644 index 0000000..101bf26 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Exec/AbstractSqlExecutor.php | |||
@@ -0,0 +1,61 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Exec; | ||
6 | |||
7 | use Doctrine\DBAL\ArrayParameterType; | ||
8 | use Doctrine\DBAL\Cache\QueryCacheProfile; | ||
9 | use Doctrine\DBAL\Connection; | ||
10 | use Doctrine\DBAL\ParameterType; | ||
11 | use Doctrine\DBAL\Result; | ||
12 | use Doctrine\DBAL\Types\Type; | ||
13 | |||
14 | /** | ||
15 | * Base class for SQL statement executors. | ||
16 | * | ||
17 | * @link http://www.doctrine-project.org | ||
18 | * | ||
19 | * @todo Rename: AbstractSQLExecutor | ||
20 | * @psalm-type WrapperParameterType = string|Type|ParameterType::*|ArrayParameterType::* | ||
21 | * @psalm-type WrapperParameterTypeArray = array<int<0, max>, WrapperParameterType>|array<string, WrapperParameterType> | ||
22 | */ | ||
23 | abstract class AbstractSqlExecutor | ||
24 | { | ||
25 | /** @var list<string>|string */ | ||
26 | protected array|string $sqlStatements; | ||
27 | |||
28 | protected QueryCacheProfile|null $queryCacheProfile = null; | ||
29 | |||
30 | /** | ||
31 | * Gets the SQL statements that are executed by the executor. | ||
32 | * | ||
33 | * @return list<string>|string All the SQL update statements. | ||
34 | */ | ||
35 | public function getSqlStatements(): array|string | ||
36 | { | ||
37 | return $this->sqlStatements; | ||
38 | } | ||
39 | |||
40 | public function setQueryCacheProfile(QueryCacheProfile $qcp): void | ||
41 | { | ||
42 | $this->queryCacheProfile = $qcp; | ||
43 | } | ||
44 | |||
45 | /** | ||
46 | * Do not use query cache | ||
47 | */ | ||
48 | public function removeQueryCacheProfile(): void | ||
49 | { | ||
50 | $this->queryCacheProfile = null; | ||
51 | } | ||
52 | |||
53 | /** | ||
54 | * Executes all sql statements. | ||
55 | * | ||
56 | * @param Connection $conn The database connection that is used to execute the queries. | ||
57 | * @param list<mixed>|array<string, mixed> $params The parameters. | ||
58 | * @psalm-param WrapperParameterTypeArray $types The parameter types. | ||
59 | */ | ||
60 | abstract public function execute(Connection $conn, array $params, array $types): Result|int; | ||
61 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php b/vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php new file mode 100644 index 0000000..6096462 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Exec/MultiTableDeleteExecutor.php | |||
@@ -0,0 +1,131 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Exec; | ||
6 | |||
7 | use Doctrine\DBAL\Connection; | ||
8 | use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; | ||
9 | use Doctrine\DBAL\Types\Type; | ||
10 | use Doctrine\ORM\Query\AST; | ||
11 | use Doctrine\ORM\Query\AST\DeleteStatement; | ||
12 | use Doctrine\ORM\Query\SqlWalker; | ||
13 | use Doctrine\ORM\Utility\PersisterHelper; | ||
14 | use Throwable; | ||
15 | |||
16 | use function array_reverse; | ||
17 | use function implode; | ||
18 | |||
19 | /** | ||
20 | * Executes the SQL statements for bulk DQL DELETE statements on classes in | ||
21 | * Class Table Inheritance (JOINED). | ||
22 | * | ||
23 | * @link http://www.doctrine-project.org | ||
24 | */ | ||
25 | class MultiTableDeleteExecutor extends AbstractSqlExecutor | ||
26 | { | ||
27 | private readonly string $createTempTableSql; | ||
28 | private readonly string $dropTempTableSql; | ||
29 | private readonly string $insertSql; | ||
30 | |||
31 | /** | ||
32 | * Initializes a new <tt>MultiTableDeleteExecutor</tt>. | ||
33 | * | ||
34 | * Internal note: Any SQL construction and preparation takes place in the constructor for | ||
35 | * best performance. With a query cache the executor will be cached. | ||
36 | * | ||
37 | * @param DeleteStatement $AST The root AST node of the DQL query. | ||
38 | * @param SqlWalker $sqlWalker The walker used for SQL generation from the AST. | ||
39 | */ | ||
40 | public function __construct(AST\Node $AST, SqlWalker $sqlWalker) | ||
41 | { | ||
42 | $em = $sqlWalker->getEntityManager(); | ||
43 | $conn = $em->getConnection(); | ||
44 | $platform = $conn->getDatabasePlatform(); | ||
45 | $quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); | ||
46 | |||
47 | if ($conn instanceof PrimaryReadReplicaConnection) { | ||
48 | $conn->ensureConnectedToPrimary(); | ||
49 | } | ||
50 | |||
51 | $primaryClass = $em->getClassMetadata($AST->deleteClause->abstractSchemaName); | ||
52 | $primaryDqlAlias = $AST->deleteClause->aliasIdentificationVariable; | ||
53 | $rootClass = $em->getClassMetadata($primaryClass->rootEntityName); | ||
54 | |||
55 | $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); | ||
56 | $idColumnNames = $rootClass->getIdentifierColumnNames(); | ||
57 | $idColumnList = implode(', ', $idColumnNames); | ||
58 | |||
59 | // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause() | ||
60 | $sqlWalker->setSQLTableAlias($primaryClass->getTableName(), 't0', $primaryDqlAlias); | ||
61 | |||
62 | $insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')' | ||
63 | . ' SELECT t0.' . implode(', t0.', $idColumnNames); | ||
64 | |||
65 | $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $primaryDqlAlias); | ||
66 | $fromClause = new AST\FromClause([new AST\IdentificationVariableDeclaration($rangeDecl, null, [])]); | ||
67 | $insertSql .= $sqlWalker->walkFromClause($fromClause); | ||
68 | |||
69 | // Append WHERE clause, if there is one. | ||
70 | if ($AST->whereClause) { | ||
71 | $insertSql .= $sqlWalker->walkWhereClause($AST->whereClause); | ||
72 | } | ||
73 | |||
74 | $this->insertSql = $insertSql; | ||
75 | |||
76 | // 2. Create ID subselect statement used in DELETE ... WHERE ... IN (subselect) | ||
77 | $idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable; | ||
78 | |||
79 | // 3. Create and store DELETE statements | ||
80 | $classNames = [...$primaryClass->parentClasses, ...[$primaryClass->name], ...$primaryClass->subClasses]; | ||
81 | foreach (array_reverse($classNames) as $className) { | ||
82 | $tableName = $quoteStrategy->getTableName($em->getClassMetadata($className), $platform); | ||
83 | $this->sqlStatements[] = 'DELETE FROM ' . $tableName | ||
84 | . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')'; | ||
85 | } | ||
86 | |||
87 | // 4. Store DDL for temporary identifier table. | ||
88 | $columnDefinitions = []; | ||
89 | foreach ($idColumnNames as $idColumnName) { | ||
90 | $columnDefinitions[$idColumnName] = [ | ||
91 | 'name' => $idColumnName, | ||
92 | 'notnull' => true, | ||
93 | 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $em)), | ||
94 | ]; | ||
95 | } | ||
96 | |||
97 | $this->createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' | ||
98 | . $platform->getColumnDeclarationListSQL($columnDefinitions) . ', PRIMARY KEY(' . implode(',', $idColumnNames) . '))'; | ||
99 | $this->dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable); | ||
100 | } | ||
101 | |||
102 | /** | ||
103 | * {@inheritDoc} | ||
104 | */ | ||
105 | public function execute(Connection $conn, array $params, array $types): int | ||
106 | { | ||
107 | // Create temporary id table | ||
108 | $conn->executeStatement($this->createTempTableSql); | ||
109 | |||
110 | try { | ||
111 | // Insert identifiers | ||
112 | $numDeleted = $conn->executeStatement($this->insertSql, $params, $types); | ||
113 | |||
114 | // Execute DELETE statements | ||
115 | foreach ($this->sqlStatements as $sql) { | ||
116 | $conn->executeStatement($sql); | ||
117 | } | ||
118 | } catch (Throwable $exception) { | ||
119 | // FAILURE! Drop temporary table to avoid possible collisions | ||
120 | $conn->executeStatement($this->dropTempTableSql); | ||
121 | |||
122 | // Re-throw exception | ||
123 | throw $exception; | ||
124 | } | ||
125 | |||
126 | // Drop temporary table | ||
127 | $conn->executeStatement($this->dropTempTableSql); | ||
128 | |||
129 | return $numDeleted; | ||
130 | } | ||
131 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php b/vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php new file mode 100644 index 0000000..dab1b61 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Exec/MultiTableUpdateExecutor.php | |||
@@ -0,0 +1,180 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Exec; | ||
6 | |||
7 | use Doctrine\DBAL\Connection; | ||
8 | use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; | ||
9 | use Doctrine\DBAL\Types\Type; | ||
10 | use Doctrine\ORM\Query\AST; | ||
11 | use Doctrine\ORM\Query\AST\UpdateStatement; | ||
12 | use Doctrine\ORM\Query\ParameterTypeInferer; | ||
13 | use Doctrine\ORM\Query\SqlWalker; | ||
14 | use Doctrine\ORM\Utility\PersisterHelper; | ||
15 | |||
16 | use function array_reverse; | ||
17 | use function array_slice; | ||
18 | use function implode; | ||
19 | |||
20 | /** | ||
21 | * Executes the SQL statements for bulk DQL UPDATE statements on classes in | ||
22 | * Class Table Inheritance (JOINED). | ||
23 | */ | ||
24 | class MultiTableUpdateExecutor extends AbstractSqlExecutor | ||
25 | { | ||
26 | private readonly string $createTempTableSql; | ||
27 | private readonly string $dropTempTableSql; | ||
28 | private readonly string $insertSql; | ||
29 | |||
30 | /** @var mixed[] */ | ||
31 | private array $sqlParameters = []; | ||
32 | private int $numParametersInUpdateClause = 0; | ||
33 | |||
34 | /** | ||
35 | * Initializes a new <tt>MultiTableUpdateExecutor</tt>. | ||
36 | * | ||
37 | * Internal note: Any SQL construction and preparation takes place in the constructor for | ||
38 | * best performance. With a query cache the executor will be cached. | ||
39 | * | ||
40 | * @param UpdateStatement $AST The root AST node of the DQL query. | ||
41 | * @param SqlWalker $sqlWalker The walker used for SQL generation from the AST. | ||
42 | */ | ||
43 | public function __construct(AST\Node $AST, SqlWalker $sqlWalker) | ||
44 | { | ||
45 | $em = $sqlWalker->getEntityManager(); | ||
46 | $conn = $em->getConnection(); | ||
47 | $platform = $conn->getDatabasePlatform(); | ||
48 | $quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); | ||
49 | $this->sqlStatements = []; | ||
50 | |||
51 | if ($conn instanceof PrimaryReadReplicaConnection) { | ||
52 | $conn->ensureConnectedToPrimary(); | ||
53 | } | ||
54 | |||
55 | $updateClause = $AST->updateClause; | ||
56 | $primaryClass = $sqlWalker->getEntityManager()->getClassMetadata($updateClause->abstractSchemaName); | ||
57 | $rootClass = $em->getClassMetadata($primaryClass->rootEntityName); | ||
58 | |||
59 | $updateItems = $updateClause->updateItems; | ||
60 | |||
61 | $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); | ||
62 | $idColumnNames = $rootClass->getIdentifierColumnNames(); | ||
63 | $idColumnList = implode(', ', $idColumnNames); | ||
64 | |||
65 | // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause() | ||
66 | $sqlWalker->setSQLTableAlias($primaryClass->getTableName(), 't0', $updateClause->aliasIdentificationVariable); | ||
67 | |||
68 | $insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')' | ||
69 | . ' SELECT t0.' . implode(', t0.', $idColumnNames); | ||
70 | |||
71 | $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $updateClause->aliasIdentificationVariable); | ||
72 | $fromClause = new AST\FromClause([new AST\IdentificationVariableDeclaration($rangeDecl, null, [])]); | ||
73 | |||
74 | $insertSql .= $sqlWalker->walkFromClause($fromClause); | ||
75 | |||
76 | // 2. Create ID subselect statement used in UPDATE ... WHERE ... IN (subselect) | ||
77 | $idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable; | ||
78 | |||
79 | // 3. Create and store UPDATE statements | ||
80 | $classNames = [...$primaryClass->parentClasses, ...[$primaryClass->name], ...$primaryClass->subClasses]; | ||
81 | |||
82 | foreach (array_reverse($classNames) as $className) { | ||
83 | $affected = false; | ||
84 | $class = $em->getClassMetadata($className); | ||
85 | $updateSql = 'UPDATE ' . $quoteStrategy->getTableName($class, $platform) . ' SET '; | ||
86 | |||
87 | $sqlParameters = []; | ||
88 | foreach ($updateItems as $updateItem) { | ||
89 | $field = $updateItem->pathExpression->field; | ||
90 | |||
91 | if ( | ||
92 | (isset($class->fieldMappings[$field]) && ! isset($class->fieldMappings[$field]->inherited)) || | ||
93 | (isset($class->associationMappings[$field]) && ! isset($class->associationMappings[$field]->inherited)) | ||
94 | ) { | ||
95 | $newValue = $updateItem->newValue; | ||
96 | |||
97 | if (! $affected) { | ||
98 | $affected = true; | ||
99 | } else { | ||
100 | $updateSql .= ', '; | ||
101 | } | ||
102 | |||
103 | $updateSql .= $sqlWalker->walkUpdateItem($updateItem); | ||
104 | |||
105 | if ($newValue instanceof AST\InputParameter) { | ||
106 | $sqlParameters[] = $newValue->name; | ||
107 | |||
108 | ++$this->numParametersInUpdateClause; | ||
109 | } | ||
110 | } | ||
111 | } | ||
112 | |||
113 | if ($affected) { | ||
114 | $this->sqlParameters[] = $sqlParameters; | ||
115 | $this->sqlStatements[] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')'; | ||
116 | } | ||
117 | } | ||
118 | |||
119 | // Append WHERE clause to insertSql, if there is one. | ||
120 | if ($AST->whereClause) { | ||
121 | $insertSql .= $sqlWalker->walkWhereClause($AST->whereClause); | ||
122 | } | ||
123 | |||
124 | $this->insertSql = $insertSql; | ||
125 | |||
126 | // 4. Store DDL for temporary identifier table. | ||
127 | $columnDefinitions = []; | ||
128 | |||
129 | foreach ($idColumnNames as $idColumnName) { | ||
130 | $columnDefinitions[$idColumnName] = [ | ||
131 | 'name' => $idColumnName, | ||
132 | 'notnull' => true, | ||
133 | 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $em)), | ||
134 | ]; | ||
135 | } | ||
136 | |||
137 | $this->createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' | ||
138 | . $platform->getColumnDeclarationListSQL($columnDefinitions) . ', PRIMARY KEY(' . implode(',', $idColumnNames) . '))'; | ||
139 | |||
140 | $this->dropTempTableSql = $platform->getDropTemporaryTableSQL($tempTable); | ||
141 | } | ||
142 | |||
143 | /** | ||
144 | * {@inheritDoc} | ||
145 | */ | ||
146 | public function execute(Connection $conn, array $params, array $types): int | ||
147 | { | ||
148 | // Create temporary id table | ||
149 | $conn->executeStatement($this->createTempTableSql); | ||
150 | |||
151 | try { | ||
152 | // Insert identifiers. Parameters from the update clause are cut off. | ||
153 | $numUpdated = $conn->executeStatement( | ||
154 | $this->insertSql, | ||
155 | array_slice($params, $this->numParametersInUpdateClause), | ||
156 | array_slice($types, $this->numParametersInUpdateClause), | ||
157 | ); | ||
158 | |||
159 | // Execute UPDATE statements | ||
160 | foreach ($this->sqlStatements as $key => $statement) { | ||
161 | $paramValues = []; | ||
162 | $paramTypes = []; | ||
163 | |||
164 | if (isset($this->sqlParameters[$key])) { | ||
165 | foreach ($this->sqlParameters[$key] as $parameterKey => $parameterName) { | ||
166 | $paramValues[] = $params[$parameterKey]; | ||
167 | $paramTypes[] = $types[$parameterKey] ?? ParameterTypeInferer::inferType($params[$parameterKey]); | ||
168 | } | ||
169 | } | ||
170 | |||
171 | $conn->executeStatement($statement, $paramValues, $paramTypes); | ||
172 | } | ||
173 | } finally { | ||
174 | // Drop temporary table | ||
175 | $conn->executeStatement($this->dropTempTableSql); | ||
176 | } | ||
177 | |||
178 | return $numUpdated; | ||
179 | } | ||
180 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php b/vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php new file mode 100644 index 0000000..5445edb --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Exec/SingleSelectExecutor.php | |||
@@ -0,0 +1,31 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Exec; | ||
6 | |||
7 | use Doctrine\DBAL\Connection; | ||
8 | use Doctrine\DBAL\Result; | ||
9 | use Doctrine\ORM\Query\AST\SelectStatement; | ||
10 | use Doctrine\ORM\Query\SqlWalker; | ||
11 | |||
12 | /** | ||
13 | * Executor that executes the SQL statement for simple DQL SELECT statements. | ||
14 | * | ||
15 | * @link www.doctrine-project.org | ||
16 | */ | ||
17 | class SingleSelectExecutor extends AbstractSqlExecutor | ||
18 | { | ||
19 | public function __construct(SelectStatement $AST, SqlWalker $sqlWalker) | ||
20 | { | ||
21 | $this->sqlStatements = $sqlWalker->walkSelectStatement($AST); | ||
22 | } | ||
23 | |||
24 | /** | ||
25 | * {@inheritDoc} | ||
26 | */ | ||
27 | public function execute(Connection $conn, array $params, array $types): Result | ||
28 | { | ||
29 | return $conn->executeQuery($this->sqlStatements, $params, $types, $this->queryCacheProfile); | ||
30 | } | ||
31 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php b/vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php new file mode 100644 index 0000000..66696db --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Exec/SingleTableDeleteUpdateExecutor.php | |||
@@ -0,0 +1,42 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Exec; | ||
6 | |||
7 | use Doctrine\DBAL\Connection; | ||
8 | use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection; | ||
9 | use Doctrine\ORM\Query\AST; | ||
10 | use Doctrine\ORM\Query\SqlWalker; | ||
11 | |||
12 | /** | ||
13 | * Executor that executes the SQL statements for DQL DELETE/UPDATE statements on classes | ||
14 | * that are mapped to a single table. | ||
15 | * | ||
16 | * @link www.doctrine-project.org | ||
17 | * | ||
18 | * @todo This is exactly the same as SingleSelectExecutor. Unify in SingleStatementExecutor. | ||
19 | */ | ||
20 | class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor | ||
21 | { | ||
22 | public function __construct(AST\Node $AST, SqlWalker $sqlWalker) | ||
23 | { | ||
24 | if ($AST instanceof AST\UpdateStatement) { | ||
25 | $this->sqlStatements = $sqlWalker->walkUpdateStatement($AST); | ||
26 | } elseif ($AST instanceof AST\DeleteStatement) { | ||
27 | $this->sqlStatements = $sqlWalker->walkDeleteStatement($AST); | ||
28 | } | ||
29 | } | ||
30 | |||
31 | /** | ||
32 | * {@inheritDoc} | ||
33 | */ | ||
34 | public function execute(Connection $conn, array $params, array $types): int | ||
35 | { | ||
36 | if ($conn instanceof PrimaryReadReplicaConnection) { | ||
37 | $conn->ensureConnectedToPrimary(); | ||
38 | } | ||
39 | |||
40 | return $conn->executeStatement($this->sqlStatements, $params, $types); | ||
41 | } | ||
42 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr.php b/vendor/doctrine/orm/src/Query/Expr.php new file mode 100644 index 0000000..65f3082 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr.php | |||
@@ -0,0 +1,615 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\ORM\Internal\NoUnknownNamedArguments; | ||
8 | use Traversable; | ||
9 | |||
10 | use function implode; | ||
11 | use function is_bool; | ||
12 | use function is_float; | ||
13 | use function is_int; | ||
14 | use function is_iterable; | ||
15 | use function iterator_to_array; | ||
16 | use function str_replace; | ||
17 | |||
18 | /** | ||
19 | * This class is used to generate DQL expressions via a set of PHP static functions. | ||
20 | * | ||
21 | * @link www.doctrine-project.org | ||
22 | * | ||
23 | * @todo Rename: ExpressionBuilder | ||
24 | */ | ||
25 | class Expr | ||
26 | { | ||
27 | use NoUnknownNamedArguments; | ||
28 | |||
29 | /** | ||
30 | * Creates a conjunction of the given boolean expressions. | ||
31 | * | ||
32 | * Example: | ||
33 | * | ||
34 | * [php] | ||
35 | * // (u.type = ?1) AND (u.role = ?2) | ||
36 | * $expr->andX($expr->eq('u.type', ':1'), $expr->eq('u.role', ':2')); | ||
37 | * | ||
38 | * @param Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x Optional clause. Defaults to null, | ||
39 | * but requires at least one defined | ||
40 | * when converting to string. | ||
41 | */ | ||
42 | public function andX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Andx | ||
43 | { | ||
44 | self::validateVariadicParameter($x); | ||
45 | |||
46 | return new Expr\Andx($x); | ||
47 | } | ||
48 | |||
49 | /** | ||
50 | * Creates a disjunction of the given boolean expressions. | ||
51 | * | ||
52 | * Example: | ||
53 | * | ||
54 | * [php] | ||
55 | * // (u.type = ?1) OR (u.role = ?2) | ||
56 | * $q->where($q->expr()->orX('u.type = ?1', 'u.role = ?2')); | ||
57 | * | ||
58 | * @param Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x Optional clause. Defaults to null, | ||
59 | * but requires at least one defined | ||
60 | * when converting to string. | ||
61 | */ | ||
62 | public function orX(Expr\Comparison|Expr\Func|Expr\Andx|Expr\Orx|string ...$x): Expr\Orx | ||
63 | { | ||
64 | self::validateVariadicParameter($x); | ||
65 | |||
66 | return new Expr\Orx($x); | ||
67 | } | ||
68 | |||
69 | /** | ||
70 | * Creates an ASCending order expression. | ||
71 | */ | ||
72 | public function asc(mixed $expr): Expr\OrderBy | ||
73 | { | ||
74 | return new Expr\OrderBy($expr, 'ASC'); | ||
75 | } | ||
76 | |||
77 | /** | ||
78 | * Creates a DESCending order expression. | ||
79 | */ | ||
80 | public function desc(mixed $expr): Expr\OrderBy | ||
81 | { | ||
82 | return new Expr\OrderBy($expr, 'DESC'); | ||
83 | } | ||
84 | |||
85 | /** | ||
86 | * Creates an equality comparison expression with the given arguments. | ||
87 | * | ||
88 | * First argument is considered the left expression and the second is the right expression. | ||
89 | * When converted to string, it will generated a <left expr> = <right expr>. Example: | ||
90 | * | ||
91 | * [php] | ||
92 | * // u.id = ?1 | ||
93 | * $expr->eq('u.id', '?1'); | ||
94 | * | ||
95 | * @param mixed $x Left expression. | ||
96 | * @param mixed $y Right expression. | ||
97 | */ | ||
98 | public function eq(mixed $x, mixed $y): Expr\Comparison | ||
99 | { | ||
100 | return new Expr\Comparison($x, Expr\Comparison::EQ, $y); | ||
101 | } | ||
102 | |||
103 | /** | ||
104 | * Creates an instance of Expr\Comparison, with the given arguments. | ||
105 | * First argument is considered the left expression and the second is the right expression. | ||
106 | * When converted to string, it will generated a <left expr> <> <right expr>. Example: | ||
107 | * | ||
108 | * [php] | ||
109 | * // u.id <> ?1 | ||
110 | * $q->where($q->expr()->neq('u.id', '?1')); | ||
111 | * | ||
112 | * @param mixed $x Left expression. | ||
113 | * @param mixed $y Right expression. | ||
114 | */ | ||
115 | public function neq(mixed $x, mixed $y): Expr\Comparison | ||
116 | { | ||
117 | return new Expr\Comparison($x, Expr\Comparison::NEQ, $y); | ||
118 | } | ||
119 | |||
120 | /** | ||
121 | * Creates an instance of Expr\Comparison, with the given arguments. | ||
122 | * First argument is considered the left expression and the second is the right expression. | ||
123 | * When converted to string, it will generated a <left expr> < <right expr>. Example: | ||
124 | * | ||
125 | * [php] | ||
126 | * // u.id < ?1 | ||
127 | * $q->where($q->expr()->lt('u.id', '?1')); | ||
128 | * | ||
129 | * @param mixed $x Left expression. | ||
130 | * @param mixed $y Right expression. | ||
131 | */ | ||
132 | public function lt(mixed $x, mixed $y): Expr\Comparison | ||
133 | { | ||
134 | return new Expr\Comparison($x, Expr\Comparison::LT, $y); | ||
135 | } | ||
136 | |||
137 | /** | ||
138 | * Creates an instance of Expr\Comparison, with the given arguments. | ||
139 | * First argument is considered the left expression and the second is the right expression. | ||
140 | * When converted to string, it will generated a <left expr> <= <right expr>. Example: | ||
141 | * | ||
142 | * [php] | ||
143 | * // u.id <= ?1 | ||
144 | * $q->where($q->expr()->lte('u.id', '?1')); | ||
145 | * | ||
146 | * @param mixed $x Left expression. | ||
147 | * @param mixed $y Right expression. | ||
148 | */ | ||
149 | public function lte(mixed $x, mixed $y): Expr\Comparison | ||
150 | { | ||
151 | return new Expr\Comparison($x, Expr\Comparison::LTE, $y); | ||
152 | } | ||
153 | |||
154 | /** | ||
155 | * Creates an instance of Expr\Comparison, with the given arguments. | ||
156 | * First argument is considered the left expression and the second is the right expression. | ||
157 | * When converted to string, it will generated a <left expr> > <right expr>. Example: | ||
158 | * | ||
159 | * [php] | ||
160 | * // u.id > ?1 | ||
161 | * $q->where($q->expr()->gt('u.id', '?1')); | ||
162 | * | ||
163 | * @param mixed $x Left expression. | ||
164 | * @param mixed $y Right expression. | ||
165 | */ | ||
166 | public function gt(mixed $x, mixed $y): Expr\Comparison | ||
167 | { | ||
168 | return new Expr\Comparison($x, Expr\Comparison::GT, $y); | ||
169 | } | ||
170 | |||
171 | /** | ||
172 | * Creates an instance of Expr\Comparison, with the given arguments. | ||
173 | * First argument is considered the left expression and the second is the right expression. | ||
174 | * When converted to string, it will generated a <left expr> >= <right expr>. Example: | ||
175 | * | ||
176 | * [php] | ||
177 | * // u.id >= ?1 | ||
178 | * $q->where($q->expr()->gte('u.id', '?1')); | ||
179 | * | ||
180 | * @param mixed $x Left expression. | ||
181 | * @param mixed $y Right expression. | ||
182 | */ | ||
183 | public function gte(mixed $x, mixed $y): Expr\Comparison | ||
184 | { | ||
185 | return new Expr\Comparison($x, Expr\Comparison::GTE, $y); | ||
186 | } | ||
187 | |||
188 | /** | ||
189 | * Creates an instance of AVG() function, with the given argument. | ||
190 | * | ||
191 | * @param mixed $x Argument to be used in AVG() function. | ||
192 | */ | ||
193 | public function avg(mixed $x): Expr\Func | ||
194 | { | ||
195 | return new Expr\Func('AVG', [$x]); | ||
196 | } | ||
197 | |||
198 | /** | ||
199 | * Creates an instance of MAX() function, with the given argument. | ||
200 | * | ||
201 | * @param mixed $x Argument to be used in MAX() function. | ||
202 | */ | ||
203 | public function max(mixed $x): Expr\Func | ||
204 | { | ||
205 | return new Expr\Func('MAX', [$x]); | ||
206 | } | ||
207 | |||
208 | /** | ||
209 | * Creates an instance of MIN() function, with the given argument. | ||
210 | * | ||
211 | * @param mixed $x Argument to be used in MIN() function. | ||
212 | */ | ||
213 | public function min(mixed $x): Expr\Func | ||
214 | { | ||
215 | return new Expr\Func('MIN', [$x]); | ||
216 | } | ||
217 | |||
218 | /** | ||
219 | * Creates an instance of COUNT() function, with the given argument. | ||
220 | * | ||
221 | * @param mixed $x Argument to be used in COUNT() function. | ||
222 | */ | ||
223 | public function count(mixed $x): Expr\Func | ||
224 | { | ||
225 | return new Expr\Func('COUNT', [$x]); | ||
226 | } | ||
227 | |||
228 | /** | ||
229 | * Creates an instance of COUNT(DISTINCT) function, with the given argument. | ||
230 | * | ||
231 | * @param mixed ...$x Argument to be used in COUNT(DISTINCT) function. | ||
232 | */ | ||
233 | public function countDistinct(mixed ...$x): string | ||
234 | { | ||
235 | self::validateVariadicParameter($x); | ||
236 | |||
237 | return 'COUNT(DISTINCT ' . implode(', ', $x) . ')'; | ||
238 | } | ||
239 | |||
240 | /** | ||
241 | * Creates an instance of EXISTS() function, with the given DQL Subquery. | ||
242 | * | ||
243 | * @param mixed $subquery DQL Subquery to be used in EXISTS() function. | ||
244 | */ | ||
245 | public function exists(mixed $subquery): Expr\Func | ||
246 | { | ||
247 | return new Expr\Func('EXISTS', [$subquery]); | ||
248 | } | ||
249 | |||
250 | /** | ||
251 | * Creates an instance of ALL() function, with the given DQL Subquery. | ||
252 | * | ||
253 | * @param mixed $subquery DQL Subquery to be used in ALL() function. | ||
254 | */ | ||
255 | public function all(mixed $subquery): Expr\Func | ||
256 | { | ||
257 | return new Expr\Func('ALL', [$subquery]); | ||
258 | } | ||
259 | |||
260 | /** | ||
261 | * Creates a SOME() function expression with the given DQL subquery. | ||
262 | * | ||
263 | * @param mixed $subquery DQL Subquery to be used in SOME() function. | ||
264 | */ | ||
265 | public function some(mixed $subquery): Expr\Func | ||
266 | { | ||
267 | return new Expr\Func('SOME', [$subquery]); | ||
268 | } | ||
269 | |||
270 | /** | ||
271 | * Creates an ANY() function expression with the given DQL subquery. | ||
272 | * | ||
273 | * @param mixed $subquery DQL Subquery to be used in ANY() function. | ||
274 | */ | ||
275 | public function any(mixed $subquery): Expr\Func | ||
276 | { | ||
277 | return new Expr\Func('ANY', [$subquery]); | ||
278 | } | ||
279 | |||
280 | /** | ||
281 | * Creates a negation expression of the given restriction. | ||
282 | * | ||
283 | * @param mixed $restriction Restriction to be used in NOT() function. | ||
284 | */ | ||
285 | public function not(mixed $restriction): Expr\Func | ||
286 | { | ||
287 | return new Expr\Func('NOT', [$restriction]); | ||
288 | } | ||
289 | |||
290 | /** | ||
291 | * Creates an ABS() function expression with the given argument. | ||
292 | * | ||
293 | * @param mixed $x Argument to be used in ABS() function. | ||
294 | */ | ||
295 | public function abs(mixed $x): Expr\Func | ||
296 | { | ||
297 | return new Expr\Func('ABS', [$x]); | ||
298 | } | ||
299 | |||
300 | /** | ||
301 | * Creates a MOD($x, $y) function expression to return the remainder of $x divided by $y. | ||
302 | */ | ||
303 | public function mod(mixed $x, mixed $y): Expr\Func | ||
304 | { | ||
305 | return new Expr\Func('MOD', [$x, $y]); | ||
306 | } | ||
307 | |||
308 | /** | ||
309 | * Creates a product mathematical expression with the given arguments. | ||
310 | * | ||
311 | * First argument is considered the left expression and the second is the right expression. | ||
312 | * When converted to string, it will generated a <left expr> * <right expr>. Example: | ||
313 | * | ||
314 | * [php] | ||
315 | * // u.salary * u.percentAnnualSalaryIncrease | ||
316 | * $q->expr()->prod('u.salary', 'u.percentAnnualSalaryIncrease') | ||
317 | * | ||
318 | * @param mixed $x Left expression. | ||
319 | * @param mixed $y Right expression. | ||
320 | */ | ||
321 | public function prod(mixed $x, mixed $y): Expr\Math | ||
322 | { | ||
323 | return new Expr\Math($x, '*', $y); | ||
324 | } | ||
325 | |||
326 | /** | ||
327 | * Creates a difference mathematical expression with the given arguments. | ||
328 | * First argument is considered the left expression and the second is the right expression. | ||
329 | * When converted to string, it will generated a <left expr> - <right expr>. Example: | ||
330 | * | ||
331 | * [php] | ||
332 | * // u.monthlySubscriptionCount - 1 | ||
333 | * $q->expr()->diff('u.monthlySubscriptionCount', '1') | ||
334 | * | ||
335 | * @param mixed $x Left expression. | ||
336 | * @param mixed $y Right expression. | ||
337 | */ | ||
338 | public function diff(mixed $x, mixed $y): Expr\Math | ||
339 | { | ||
340 | return new Expr\Math($x, '-', $y); | ||
341 | } | ||
342 | |||
343 | /** | ||
344 | * Creates a sum mathematical expression with the given arguments. | ||
345 | * First argument is considered the left expression and the second is the right expression. | ||
346 | * When converted to string, it will generated a <left expr> + <right expr>. Example: | ||
347 | * | ||
348 | * [php] | ||
349 | * // u.numChildren + 1 | ||
350 | * $q->expr()->sum('u.numChildren', '1') | ||
351 | * | ||
352 | * @param mixed $x Left expression. | ||
353 | * @param mixed $y Right expression. | ||
354 | */ | ||
355 | public function sum(mixed $x, mixed $y): Expr\Math | ||
356 | { | ||
357 | return new Expr\Math($x, '+', $y); | ||
358 | } | ||
359 | |||
360 | /** | ||
361 | * Creates a quotient mathematical expression with the given arguments. | ||
362 | * First argument is considered the left expression and the second is the right expression. | ||
363 | * When converted to string, it will generated a <left expr> / <right expr>. Example: | ||
364 | * | ||
365 | * [php] | ||
366 | * // u.total / u.period | ||
367 | * $expr->quot('u.total', 'u.period') | ||
368 | * | ||
369 | * @param mixed $x Left expression. | ||
370 | * @param mixed $y Right expression. | ||
371 | */ | ||
372 | public function quot(mixed $x, mixed $y): Expr\Math | ||
373 | { | ||
374 | return new Expr\Math($x, '/', $y); | ||
375 | } | ||
376 | |||
377 | /** | ||
378 | * Creates a SQRT() function expression with the given argument. | ||
379 | * | ||
380 | * @param mixed $x Argument to be used in SQRT() function. | ||
381 | */ | ||
382 | public function sqrt(mixed $x): Expr\Func | ||
383 | { | ||
384 | return new Expr\Func('SQRT', [$x]); | ||
385 | } | ||
386 | |||
387 | /** | ||
388 | * Creates an IN() expression with the given arguments. | ||
389 | * | ||
390 | * @param string $x Field in string format to be restricted by IN() function. | ||
391 | * @param mixed $y Argument to be used in IN() function. | ||
392 | */ | ||
393 | public function in(string $x, mixed $y): Expr\Func | ||
394 | { | ||
395 | if (is_iterable($y)) { | ||
396 | if ($y instanceof Traversable) { | ||
397 | $y = iterator_to_array($y); | ||
398 | } | ||
399 | |||
400 | foreach ($y as &$literal) { | ||
401 | if (! ($literal instanceof Expr\Literal)) { | ||
402 | $literal = $this->quoteLiteral($literal); | ||
403 | } | ||
404 | } | ||
405 | } | ||
406 | |||
407 | return new Expr\Func($x . ' IN', (array) $y); | ||
408 | } | ||
409 | |||
410 | /** | ||
411 | * Creates a NOT IN() expression with the given arguments. | ||
412 | * | ||
413 | * @param string $x Field in string format to be restricted by NOT IN() function. | ||
414 | * @param mixed $y Argument to be used in NOT IN() function. | ||
415 | */ | ||
416 | public function notIn(string $x, mixed $y): Expr\Func | ||
417 | { | ||
418 | if (is_iterable($y)) { | ||
419 | if ($y instanceof Traversable) { | ||
420 | $y = iterator_to_array($y); | ||
421 | } | ||
422 | |||
423 | foreach ($y as &$literal) { | ||
424 | if (! ($literal instanceof Expr\Literal)) { | ||
425 | $literal = $this->quoteLiteral($literal); | ||
426 | } | ||
427 | } | ||
428 | } | ||
429 | |||
430 | return new Expr\Func($x . ' NOT IN', (array) $y); | ||
431 | } | ||
432 | |||
433 | /** | ||
434 | * Creates an IS NULL expression with the given arguments. | ||
435 | * | ||
436 | * @param string $x Field in string format to be restricted by IS NULL. | ||
437 | */ | ||
438 | public function isNull(string $x): string | ||
439 | { | ||
440 | return $x . ' IS NULL'; | ||
441 | } | ||
442 | |||
443 | /** | ||
444 | * Creates an IS NOT NULL expression with the given arguments. | ||
445 | * | ||
446 | * @param string $x Field in string format to be restricted by IS NOT NULL. | ||
447 | */ | ||
448 | public function isNotNull(string $x): string | ||
449 | { | ||
450 | return $x . ' IS NOT NULL'; | ||
451 | } | ||
452 | |||
453 | /** | ||
454 | * Creates a LIKE() comparison expression with the given arguments. | ||
455 | * | ||
456 | * @param string $x Field in string format to be inspected by LIKE() comparison. | ||
457 | * @param mixed $y Argument to be used in LIKE() comparison. | ||
458 | */ | ||
459 | public function like(string $x, mixed $y): Expr\Comparison | ||
460 | { | ||
461 | return new Expr\Comparison($x, 'LIKE', $y); | ||
462 | } | ||
463 | |||
464 | /** | ||
465 | * Creates a NOT LIKE() comparison expression with the given arguments. | ||
466 | * | ||
467 | * @param string $x Field in string format to be inspected by LIKE() comparison. | ||
468 | * @param mixed $y Argument to be used in LIKE() comparison. | ||
469 | */ | ||
470 | public function notLike(string $x, mixed $y): Expr\Comparison | ||
471 | { | ||
472 | return new Expr\Comparison($x, 'NOT LIKE', $y); | ||
473 | } | ||
474 | |||
475 | /** | ||
476 | * Creates a CONCAT() function expression with the given arguments. | ||
477 | * | ||
478 | * @param mixed ...$x Arguments to be used in CONCAT() function. | ||
479 | */ | ||
480 | public function concat(mixed ...$x): Expr\Func | ||
481 | { | ||
482 | self::validateVariadicParameter($x); | ||
483 | |||
484 | return new Expr\Func('CONCAT', $x); | ||
485 | } | ||
486 | |||
487 | /** | ||
488 | * Creates a SUBSTRING() function expression with the given arguments. | ||
489 | * | ||
490 | * @param mixed $x Argument to be used as string to be cropped by SUBSTRING() function. | ||
491 | * @param int $from Initial offset to start cropping string. May accept negative values. | ||
492 | * @param int|null $len Length of crop. May accept negative values. | ||
493 | */ | ||
494 | public function substring(mixed $x, int $from, int|null $len = null): Expr\Func | ||
495 | { | ||
496 | $args = [$x, $from]; | ||
497 | if ($len !== null) { | ||
498 | $args[] = $len; | ||
499 | } | ||
500 | |||
501 | return new Expr\Func('SUBSTRING', $args); | ||
502 | } | ||
503 | |||
504 | /** | ||
505 | * Creates a LOWER() function expression with the given argument. | ||
506 | * | ||
507 | * @param mixed $x Argument to be used in LOWER() function. | ||
508 | * | ||
509 | * @return Expr\Func A LOWER function expression. | ||
510 | */ | ||
511 | public function lower(mixed $x): Expr\Func | ||
512 | { | ||
513 | return new Expr\Func('LOWER', [$x]); | ||
514 | } | ||
515 | |||
516 | /** | ||
517 | * Creates an UPPER() function expression with the given argument. | ||
518 | * | ||
519 | * @param mixed $x Argument to be used in UPPER() function. | ||
520 | * | ||
521 | * @return Expr\Func An UPPER function expression. | ||
522 | */ | ||
523 | public function upper(mixed $x): Expr\Func | ||
524 | { | ||
525 | return new Expr\Func('UPPER', [$x]); | ||
526 | } | ||
527 | |||
528 | /** | ||
529 | * Creates a LENGTH() function expression with the given argument. | ||
530 | * | ||
531 | * @param mixed $x Argument to be used as argument of LENGTH() function. | ||
532 | * | ||
533 | * @return Expr\Func A LENGTH function expression. | ||
534 | */ | ||
535 | public function length(mixed $x): Expr\Func | ||
536 | { | ||
537 | return new Expr\Func('LENGTH', [$x]); | ||
538 | } | ||
539 | |||
540 | /** | ||
541 | * Creates a literal expression of the given argument. | ||
542 | * | ||
543 | * @param scalar $literal Argument to be converted to literal. | ||
544 | */ | ||
545 | public function literal(bool|string|int|float $literal): Expr\Literal | ||
546 | { | ||
547 | return new Expr\Literal($this->quoteLiteral($literal)); | ||
548 | } | ||
549 | |||
550 | /** | ||
551 | * Quotes a literal value, if necessary, according to the DQL syntax. | ||
552 | * | ||
553 | * @param scalar $literal The literal value. | ||
554 | */ | ||
555 | private function quoteLiteral(bool|string|int|float $literal): string | ||
556 | { | ||
557 | if (is_int($literal) || is_float($literal)) { | ||
558 | return (string) $literal; | ||
559 | } | ||
560 | |||
561 | if (is_bool($literal)) { | ||
562 | return $literal ? 'true' : 'false'; | ||
563 | } | ||
564 | |||
565 | return "'" . str_replace("'", "''", $literal) . "'"; | ||
566 | } | ||
567 | |||
568 | /** | ||
569 | * Creates an instance of BETWEEN() function, with the given argument. | ||
570 | * | ||
571 | * @param mixed $val Valued to be inspected by range values. | ||
572 | * @param int|string $x Starting range value to be used in BETWEEN() function. | ||
573 | * @param int|string $y End point value to be used in BETWEEN() function. | ||
574 | * | ||
575 | * @return string A BETWEEN expression. | ||
576 | */ | ||
577 | public function between(mixed $val, int|string $x, int|string $y): string | ||
578 | { | ||
579 | return $val . ' BETWEEN ' . $x . ' AND ' . $y; | ||
580 | } | ||
581 | |||
582 | /** | ||
583 | * Creates an instance of TRIM() function, with the given argument. | ||
584 | * | ||
585 | * @param mixed $x Argument to be used as argument of TRIM() function. | ||
586 | * | ||
587 | * @return Expr\Func a TRIM expression. | ||
588 | */ | ||
589 | public function trim(mixed $x): Expr\Func | ||
590 | { | ||
591 | return new Expr\Func('TRIM', $x); | ||
592 | } | ||
593 | |||
594 | /** | ||
595 | * Creates an instance of MEMBER OF function, with the given arguments. | ||
596 | * | ||
597 | * @param string $x Value to be checked | ||
598 | * @param string $y Value to be checked against | ||
599 | */ | ||
600 | public function isMemberOf(string $x, string $y): Expr\Comparison | ||
601 | { | ||
602 | return new Expr\Comparison($x, 'MEMBER OF', $y); | ||
603 | } | ||
604 | |||
605 | /** | ||
606 | * Creates an instance of INSTANCE OF function, with the given arguments. | ||
607 | * | ||
608 | * @param string $x Value to be checked | ||
609 | * @param string $y Value to be checked against | ||
610 | */ | ||
611 | public function isInstanceOf(string $x, string $y): Expr\Comparison | ||
612 | { | ||
613 | return new Expr\Comparison($x, 'INSTANCE OF', $y); | ||
614 | } | ||
615 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Andx.php b/vendor/doctrine/orm/src/Query/Expr/Andx.php new file mode 100644 index 0000000..a20bcef --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Andx.php | |||
@@ -0,0 +1,32 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | /** | ||
8 | * Expression class for building DQL and parts. | ||
9 | * | ||
10 | * @link www.doctrine-project.org | ||
11 | */ | ||
12 | class Andx extends Composite | ||
13 | { | ||
14 | protected string $separator = ' AND '; | ||
15 | |||
16 | /** @var string[] */ | ||
17 | protected array $allowedClasses = [ | ||
18 | Comparison::class, | ||
19 | Func::class, | ||
20 | Orx::class, | ||
21 | self::class, | ||
22 | ]; | ||
23 | |||
24 | /** @psalm-var list<string|Comparison|Func|Orx|self> */ | ||
25 | protected array $parts = []; | ||
26 | |||
27 | /** @psalm-return list<string|Comparison|Func|Orx|self> */ | ||
28 | public function getParts(): array | ||
29 | { | ||
30 | return $this->parts; | ||
31 | } | ||
32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Base.php b/vendor/doctrine/orm/src/Query/Expr/Base.php new file mode 100644 index 0000000..e0f2572 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Base.php | |||
@@ -0,0 +1,96 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | use InvalidArgumentException; | ||
8 | use Stringable; | ||
9 | |||
10 | use function array_key_exists; | ||
11 | use function count; | ||
12 | use function get_debug_type; | ||
13 | use function implode; | ||
14 | use function in_array; | ||
15 | use function is_array; | ||
16 | use function is_string; | ||
17 | use function sprintf; | ||
18 | |||
19 | /** | ||
20 | * Abstract base Expr class for building DQL parts. | ||
21 | * | ||
22 | * @link www.doctrine-project.org | ||
23 | */ | ||
24 | abstract class Base implements Stringable | ||
25 | { | ||
26 | protected string $preSeparator = '('; | ||
27 | protected string $separator = ', '; | ||
28 | protected string $postSeparator = ')'; | ||
29 | |||
30 | /** @var list<class-string> */ | ||
31 | protected array $allowedClasses = []; | ||
32 | |||
33 | /** @var list<string|Stringable> */ | ||
34 | protected array $parts = []; | ||
35 | |||
36 | public function __construct(mixed $args = []) | ||
37 | { | ||
38 | if (is_array($args) && array_key_exists(0, $args) && is_array($args[0])) { | ||
39 | $args = $args[0]; | ||
40 | } | ||
41 | |||
42 | $this->addMultiple($args); | ||
43 | } | ||
44 | |||
45 | /** | ||
46 | * @param string[]|object[]|string|object $args | ||
47 | * @psalm-param list<string|object>|string|object $args | ||
48 | * | ||
49 | * @return $this | ||
50 | */ | ||
51 | public function addMultiple(array|string|object $args = []): static | ||
52 | { | ||
53 | foreach ((array) $args as $arg) { | ||
54 | $this->add($arg); | ||
55 | } | ||
56 | |||
57 | return $this; | ||
58 | } | ||
59 | |||
60 | /** | ||
61 | * @return $this | ||
62 | * | ||
63 | * @throws InvalidArgumentException | ||
64 | */ | ||
65 | public function add(mixed $arg): static | ||
66 | { | ||
67 | if ($arg !== null && (! $arg instanceof self || $arg->count() > 0)) { | ||
68 | // If we decide to keep Expr\Base instances, we can use this check | ||
69 | if (! is_string($arg) && ! in_array($arg::class, $this->allowedClasses, true)) { | ||
70 | throw new InvalidArgumentException(sprintf( | ||
71 | "Expression of type '%s' not allowed in this context.", | ||
72 | get_debug_type($arg), | ||
73 | )); | ||
74 | } | ||
75 | |||
76 | $this->parts[] = $arg; | ||
77 | } | ||
78 | |||
79 | return $this; | ||
80 | } | ||
81 | |||
82 | /** @psalm-return 0|positive-int */ | ||
83 | public function count(): int | ||
84 | { | ||
85 | return count($this->parts); | ||
86 | } | ||
87 | |||
88 | public function __toString(): string | ||
89 | { | ||
90 | if ($this->count() === 1) { | ||
91 | return (string) $this->parts[0]; | ||
92 | } | ||
93 | |||
94 | return $this->preSeparator . implode($this->separator, $this->parts) . $this->postSeparator; | ||
95 | } | ||
96 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Comparison.php b/vendor/doctrine/orm/src/Query/Expr/Comparison.php new file mode 100644 index 0000000..ec8ef21 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Comparison.php | |||
@@ -0,0 +1,47 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | use Stringable; | ||
8 | |||
9 | /** | ||
10 | * Expression class for DQL comparison expressions. | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class Comparison implements Stringable | ||
15 | { | ||
16 | final public const EQ = '='; | ||
17 | final public const NEQ = '<>'; | ||
18 | final public const LT = '<'; | ||
19 | final public const LTE = '<='; | ||
20 | final public const GT = '>'; | ||
21 | final public const GTE = '>='; | ||
22 | |||
23 | /** Creates a comparison expression with the given arguments. */ | ||
24 | public function __construct(protected mixed $leftExpr, protected string $operator, protected mixed $rightExpr) | ||
25 | { | ||
26 | } | ||
27 | |||
28 | public function getLeftExpr(): mixed | ||
29 | { | ||
30 | return $this->leftExpr; | ||
31 | } | ||
32 | |||
33 | public function getOperator(): string | ||
34 | { | ||
35 | return $this->operator; | ||
36 | } | ||
37 | |||
38 | public function getRightExpr(): mixed | ||
39 | { | ||
40 | return $this->rightExpr; | ||
41 | } | ||
42 | |||
43 | public function __toString(): string | ||
44 | { | ||
45 | return $this->leftExpr . ' ' . $this->operator . ' ' . $this->rightExpr; | ||
46 | } | ||
47 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Composite.php b/vendor/doctrine/orm/src/Query/Expr/Composite.php new file mode 100644 index 0000000..f3007a7 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Composite.php | |||
@@ -0,0 +1,50 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | use Stringable; | ||
8 | |||
9 | use function implode; | ||
10 | use function is_object; | ||
11 | use function preg_match; | ||
12 | |||
13 | /** | ||
14 | * Expression class for building DQL and parts. | ||
15 | * | ||
16 | * @link www.doctrine-project.org | ||
17 | */ | ||
18 | class Composite extends Base | ||
19 | { | ||
20 | public function __toString(): string | ||
21 | { | ||
22 | if ($this->count() === 1) { | ||
23 | return (string) $this->parts[0]; | ||
24 | } | ||
25 | |||
26 | $components = []; | ||
27 | |||
28 | foreach ($this->parts as $part) { | ||
29 | $components[] = $this->processQueryPart($part); | ||
30 | } | ||
31 | |||
32 | return implode($this->separator, $components); | ||
33 | } | ||
34 | |||
35 | private function processQueryPart(string|Stringable $part): string | ||
36 | { | ||
37 | $queryPart = (string) $part; | ||
38 | |||
39 | if (is_object($part) && $part instanceof self && $part->count() > 1) { | ||
40 | return $this->preSeparator . $queryPart . $this->postSeparator; | ||
41 | } | ||
42 | |||
43 | // Fixes DDC-1237: User may have added a where item containing nested expression (with "OR" or "AND") | ||
44 | if (preg_match('/\s(OR|AND)\s/i', $queryPart)) { | ||
45 | return $this->preSeparator . $queryPart . $this->postSeparator; | ||
46 | } | ||
47 | |||
48 | return $queryPart; | ||
49 | } | ||
50 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/From.php b/vendor/doctrine/orm/src/Query/Expr/From.php new file mode 100644 index 0000000..21af078 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/From.php | |||
@@ -0,0 +1,48 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | use Stringable; | ||
8 | |||
9 | /** | ||
10 | * Expression class for DQL from. | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class From implements Stringable | ||
15 | { | ||
16 | /** | ||
17 | * @param class-string $from The class name. | ||
18 | * @param string $alias The alias of the class. | ||
19 | */ | ||
20 | public function __construct( | ||
21 | protected string $from, | ||
22 | protected string $alias, | ||
23 | protected string|null $indexBy = null, | ||
24 | ) { | ||
25 | } | ||
26 | |||
27 | /** @return class-string */ | ||
28 | public function getFrom(): string | ||
29 | { | ||
30 | return $this->from; | ||
31 | } | ||
32 | |||
33 | public function getAlias(): string | ||
34 | { | ||
35 | return $this->alias; | ||
36 | } | ||
37 | |||
38 | public function getIndexBy(): string|null | ||
39 | { | ||
40 | return $this->indexBy; | ||
41 | } | ||
42 | |||
43 | public function __toString(): string | ||
44 | { | ||
45 | return $this->from . ' ' . $this->alias . | ||
46 | ($this->indexBy ? ' INDEX BY ' . $this->indexBy : ''); | ||
47 | } | ||
48 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Func.php b/vendor/doctrine/orm/src/Query/Expr/Func.php new file mode 100644 index 0000000..cd9e8e0 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Func.php | |||
@@ -0,0 +1,48 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | use Stringable; | ||
8 | |||
9 | use function implode; | ||
10 | |||
11 | /** | ||
12 | * Expression class for generating DQL functions. | ||
13 | * | ||
14 | * @link www.doctrine-project.org | ||
15 | */ | ||
16 | class Func implements Stringable | ||
17 | { | ||
18 | /** @var mixed[] */ | ||
19 | protected array $arguments; | ||
20 | |||
21 | /** | ||
22 | * Creates a function, with the given argument. | ||
23 | * | ||
24 | * @psalm-param list<mixed>|mixed $arguments | ||
25 | */ | ||
26 | public function __construct( | ||
27 | protected string $name, | ||
28 | mixed $arguments, | ||
29 | ) { | ||
30 | $this->arguments = (array) $arguments; | ||
31 | } | ||
32 | |||
33 | public function getName(): string | ||
34 | { | ||
35 | return $this->name; | ||
36 | } | ||
37 | |||
38 | /** @psalm-return list<mixed> */ | ||
39 | public function getArguments(): array | ||
40 | { | ||
41 | return $this->arguments; | ||
42 | } | ||
43 | |||
44 | public function __toString(): string | ||
45 | { | ||
46 | return $this->name . '(' . implode(', ', $this->arguments) . ')'; | ||
47 | } | ||
48 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/GroupBy.php b/vendor/doctrine/orm/src/Query/Expr/GroupBy.php new file mode 100644 index 0000000..fa4625a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/GroupBy.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | /** | ||
8 | * Expression class for building DQL Group By parts. | ||
9 | * | ||
10 | * @link www.doctrine-project.org | ||
11 | */ | ||
12 | class GroupBy extends Base | ||
13 | { | ||
14 | protected string $preSeparator = ''; | ||
15 | protected string $postSeparator = ''; | ||
16 | |||
17 | /** @psalm-var list<string> */ | ||
18 | protected array $parts = []; | ||
19 | |||
20 | /** @psalm-return list<string> */ | ||
21 | public function getParts(): array | ||
22 | { | ||
23 | return $this->parts; | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Join.php b/vendor/doctrine/orm/src/Query/Expr/Join.php new file mode 100644 index 0000000..c3b6dc9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Join.php | |||
@@ -0,0 +1,77 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | use Stringable; | ||
8 | |||
9 | use function strtoupper; | ||
10 | |||
11 | /** | ||
12 | * Expression class for DQL join. | ||
13 | * | ||
14 | * @link www.doctrine-project.org | ||
15 | */ | ||
16 | class Join implements Stringable | ||
17 | { | ||
18 | final public const INNER_JOIN = 'INNER'; | ||
19 | final public const LEFT_JOIN = 'LEFT'; | ||
20 | |||
21 | final public const ON = 'ON'; | ||
22 | final public const WITH = 'WITH'; | ||
23 | |||
24 | /** | ||
25 | * @psalm-param self::INNER_JOIN|self::LEFT_JOIN $joinType | ||
26 | * @psalm-param self::ON|self::WITH|null $conditionType | ||
27 | */ | ||
28 | public function __construct( | ||
29 | protected string $joinType, | ||
30 | protected string $join, | ||
31 | protected string|null $alias = null, | ||
32 | protected string|null $conditionType = null, | ||
33 | protected string|Comparison|Composite|Func|null $condition = null, | ||
34 | protected string|null $indexBy = null, | ||
35 | ) { | ||
36 | } | ||
37 | |||
38 | /** @psalm-return self::INNER_JOIN|self::LEFT_JOIN */ | ||
39 | public function getJoinType(): string | ||
40 | { | ||
41 | return $this->joinType; | ||
42 | } | ||
43 | |||
44 | public function getJoin(): string | ||
45 | { | ||
46 | return $this->join; | ||
47 | } | ||
48 | |||
49 | public function getAlias(): string|null | ||
50 | { | ||
51 | return $this->alias; | ||
52 | } | ||
53 | |||
54 | /** @psalm-return self::ON|self::WITH|null */ | ||
55 | public function getConditionType(): string|null | ||
56 | { | ||
57 | return $this->conditionType; | ||
58 | } | ||
59 | |||
60 | public function getCondition(): string|Comparison|Composite|Func|null | ||
61 | { | ||
62 | return $this->condition; | ||
63 | } | ||
64 | |||
65 | public function getIndexBy(): string|null | ||
66 | { | ||
67 | return $this->indexBy; | ||
68 | } | ||
69 | |||
70 | public function __toString(): string | ||
71 | { | ||
72 | return strtoupper($this->joinType) . ' JOIN ' . $this->join | ||
73 | . ($this->alias ? ' ' . $this->alias : '') | ||
74 | . ($this->indexBy ? ' INDEX BY ' . $this->indexBy : '') | ||
75 | . ($this->condition ? ' ' . strtoupper($this->conditionType) . ' ' . $this->condition : ''); | ||
76 | } | ||
77 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Literal.php b/vendor/doctrine/orm/src/Query/Expr/Literal.php new file mode 100644 index 0000000..0c13030 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Literal.php | |||
@@ -0,0 +1,25 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | /** | ||
8 | * Expression class for generating DQL functions. | ||
9 | * | ||
10 | * @link www.doctrine-project.org | ||
11 | */ | ||
12 | class Literal extends Base | ||
13 | { | ||
14 | protected string $preSeparator = ''; | ||
15 | protected string $postSeparator = ''; | ||
16 | |||
17 | /** @psalm-var list<string> */ | ||
18 | protected array $parts = []; | ||
19 | |||
20 | /** @psalm-return list<string> */ | ||
21 | public function getParts(): array | ||
22 | { | ||
23 | return $this->parts; | ||
24 | } | ||
25 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Math.php b/vendor/doctrine/orm/src/Query/Expr/Math.php new file mode 100644 index 0000000..05e0b39 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Math.php | |||
@@ -0,0 +1,59 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | use Stringable; | ||
8 | |||
9 | /** | ||
10 | * Expression class for DQL math statements. | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class Math implements Stringable | ||
15 | { | ||
16 | /** | ||
17 | * Creates a mathematical expression with the given arguments. | ||
18 | */ | ||
19 | public function __construct( | ||
20 | protected mixed $leftExpr, | ||
21 | protected string $operator, | ||
22 | protected mixed $rightExpr, | ||
23 | ) { | ||
24 | } | ||
25 | |||
26 | public function getLeftExpr(): mixed | ||
27 | { | ||
28 | return $this->leftExpr; | ||
29 | } | ||
30 | |||
31 | public function getOperator(): string | ||
32 | { | ||
33 | return $this->operator; | ||
34 | } | ||
35 | |||
36 | public function getRightExpr(): mixed | ||
37 | { | ||
38 | return $this->rightExpr; | ||
39 | } | ||
40 | |||
41 | public function __toString(): string | ||
42 | { | ||
43 | // Adjusting Left Expression | ||
44 | $leftExpr = (string) $this->leftExpr; | ||
45 | |||
46 | if ($this->leftExpr instanceof Math) { | ||
47 | $leftExpr = '(' . $leftExpr . ')'; | ||
48 | } | ||
49 | |||
50 | // Adjusting Right Expression | ||
51 | $rightExpr = (string) $this->rightExpr; | ||
52 | |||
53 | if ($this->rightExpr instanceof Math) { | ||
54 | $rightExpr = '(' . $rightExpr . ')'; | ||
55 | } | ||
56 | |||
57 | return $leftExpr . ' ' . $this->operator . ' ' . $rightExpr; | ||
58 | } | ||
59 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/OrderBy.php b/vendor/doctrine/orm/src/Query/Expr/OrderBy.php new file mode 100644 index 0000000..ac9e160 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/OrderBy.php | |||
@@ -0,0 +1,60 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | use Stringable; | ||
8 | |||
9 | use function count; | ||
10 | use function implode; | ||
11 | |||
12 | /** | ||
13 | * Expression class for building DQL Order By parts. | ||
14 | * | ||
15 | * @link www.doctrine-project.org | ||
16 | */ | ||
17 | class OrderBy implements Stringable | ||
18 | { | ||
19 | protected string $preSeparator = ''; | ||
20 | protected string $separator = ', '; | ||
21 | protected string $postSeparator = ''; | ||
22 | |||
23 | /** @var string[] */ | ||
24 | protected array $allowedClasses = []; | ||
25 | |||
26 | /** @psalm-var list<string> */ | ||
27 | protected array $parts = []; | ||
28 | |||
29 | public function __construct( | ||
30 | string|null $sort = null, | ||
31 | string|null $order = null, | ||
32 | ) { | ||
33 | if ($sort) { | ||
34 | $this->add($sort, $order); | ||
35 | } | ||
36 | } | ||
37 | |||
38 | public function add(string $sort, string|null $order = null): void | ||
39 | { | ||
40 | $order = ! $order ? 'ASC' : $order; | ||
41 | $this->parts[] = $sort . ' ' . $order; | ||
42 | } | ||
43 | |||
44 | /** @psalm-return 0|positive-int */ | ||
45 | public function count(): int | ||
46 | { | ||
47 | return count($this->parts); | ||
48 | } | ||
49 | |||
50 | /** @psalm-return list<string> */ | ||
51 | public function getParts(): array | ||
52 | { | ||
53 | return $this->parts; | ||
54 | } | ||
55 | |||
56 | public function __toString(): string | ||
57 | { | ||
58 | return $this->preSeparator . implode($this->separator, $this->parts) . $this->postSeparator; | ||
59 | } | ||
60 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Orx.php b/vendor/doctrine/orm/src/Query/Expr/Orx.php new file mode 100644 index 0000000..2ae2332 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Orx.php | |||
@@ -0,0 +1,32 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | /** | ||
8 | * Expression class for building DQL OR clauses. | ||
9 | * | ||
10 | * @link www.doctrine-project.org | ||
11 | */ | ||
12 | class Orx extends Composite | ||
13 | { | ||
14 | protected string $separator = ' OR '; | ||
15 | |||
16 | /** @var string[] */ | ||
17 | protected array $allowedClasses = [ | ||
18 | Comparison::class, | ||
19 | Func::class, | ||
20 | Andx::class, | ||
21 | self::class, | ||
22 | ]; | ||
23 | |||
24 | /** @psalm-var list<string|Comparison|Func|Andx|self> */ | ||
25 | protected array $parts = []; | ||
26 | |||
27 | /** @psalm-return list<string|Comparison|Func|Andx|self> */ | ||
28 | public function getParts(): array | ||
29 | { | ||
30 | return $this->parts; | ||
31 | } | ||
32 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Expr/Select.php b/vendor/doctrine/orm/src/Query/Expr/Select.php new file mode 100644 index 0000000..91b0b60 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Expr/Select.php | |||
@@ -0,0 +1,28 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Expr; | ||
6 | |||
7 | /** | ||
8 | * Expression class for building DQL select statements. | ||
9 | * | ||
10 | * @link www.doctrine-project.org | ||
11 | */ | ||
12 | class Select extends Base | ||
13 | { | ||
14 | protected string $preSeparator = ''; | ||
15 | protected string $postSeparator = ''; | ||
16 | |||
17 | /** @var string[] */ | ||
18 | protected array $allowedClasses = [Func::class]; | ||
19 | |||
20 | /** @psalm-var list<string|Func> */ | ||
21 | protected array $parts = []; | ||
22 | |||
23 | /** @psalm-return list<string|Func> */ | ||
24 | public function getParts(): array | ||
25 | { | ||
26 | return $this->parts; | ||
27 | } | ||
28 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Filter/FilterException.php b/vendor/doctrine/orm/src/Query/Filter/FilterException.php new file mode 100644 index 0000000..37f12bc --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Filter/FilterException.php | |||
@@ -0,0 +1,23 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Filter; | ||
6 | |||
7 | use Doctrine\ORM\Exception\ORMException; | ||
8 | use LogicException; | ||
9 | |||
10 | use function sprintf; | ||
11 | |||
12 | class FilterException extends LogicException implements ORMException | ||
13 | { | ||
14 | public static function cannotConvertListParameterIntoSingleValue(string $name): self | ||
15 | { | ||
16 | return new self(sprintf('Cannot convert list-based SQL filter parameter "%s" into a single value.', $name)); | ||
17 | } | ||
18 | |||
19 | public static function cannotConvertSingleParameterIntoListValue(string $name): self | ||
20 | { | ||
21 | return new self(sprintf('Cannot convert single SQL filter parameter "%s" into a list value.', $name)); | ||
22 | } | ||
23 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Filter/SQLFilter.php b/vendor/doctrine/orm/src/Query/Filter/SQLFilter.php new file mode 100644 index 0000000..29f3775 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Filter/SQLFilter.php | |||
@@ -0,0 +1,174 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query\Filter; | ||
6 | |||
7 | use Doctrine\DBAL\Connection; | ||
8 | use Doctrine\DBAL\Types\Types; | ||
9 | use Doctrine\ORM\EntityManagerInterface; | ||
10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
11 | use Doctrine\ORM\Query\ParameterTypeInferer; | ||
12 | use InvalidArgumentException; | ||
13 | use Stringable; | ||
14 | |||
15 | use function array_map; | ||
16 | use function implode; | ||
17 | use function ksort; | ||
18 | use function serialize; | ||
19 | |||
20 | /** | ||
21 | * The base class that user defined filters should extend. | ||
22 | * | ||
23 | * Handles the setting and escaping of parameters. | ||
24 | * | ||
25 | * @abstract | ||
26 | */ | ||
27 | abstract class SQLFilter implements Stringable | ||
28 | { | ||
29 | /** | ||
30 | * Parameters for the filter. | ||
31 | * | ||
32 | * @psalm-var array<string,array{type: string, value: mixed, is_list: bool}> | ||
33 | */ | ||
34 | private array $parameters = []; | ||
35 | |||
36 | final public function __construct( | ||
37 | private readonly EntityManagerInterface $em, | ||
38 | ) { | ||
39 | } | ||
40 | |||
41 | /** | ||
42 | * Sets a parameter list that can be used by the filter. | ||
43 | * | ||
44 | * @param array<mixed> $values List of parameter values. | ||
45 | * @param string $type The parameter type. If specified, the given value will be run through | ||
46 | * the type conversion of this type. | ||
47 | * | ||
48 | * @return $this | ||
49 | */ | ||
50 | final public function setParameterList(string $name, array $values, string $type = Types::STRING): static | ||
51 | { | ||
52 | $this->parameters[$name] = ['value' => $values, 'type' => $type, 'is_list' => true]; | ||
53 | |||
54 | // Keep the parameters sorted for the hash | ||
55 | ksort($this->parameters); | ||
56 | |||
57 | // The filter collection of the EM is now dirty | ||
58 | $this->em->getFilters()->setFiltersStateDirty(); | ||
59 | |||
60 | return $this; | ||
61 | } | ||
62 | |||
63 | /** | ||
64 | * Sets a parameter that can be used by the filter. | ||
65 | * | ||
66 | * @param string|null $type The parameter type. If specified, the given value will be run through | ||
67 | * the type conversion of this type. This is usually not needed for | ||
68 | * strings and numeric types. | ||
69 | * | ||
70 | * @return $this | ||
71 | */ | ||
72 | final public function setParameter(string $name, mixed $value, string|null $type = null): static | ||
73 | { | ||
74 | if ($type === null) { | ||
75 | $type = ParameterTypeInferer::inferType($value); | ||
76 | } | ||
77 | |||
78 | $this->parameters[$name] = ['value' => $value, 'type' => $type, 'is_list' => false]; | ||
79 | |||
80 | // Keep the parameters sorted for the hash | ||
81 | ksort($this->parameters); | ||
82 | |||
83 | // The filter collection of the EM is now dirty | ||
84 | $this->em->getFilters()->setFiltersStateDirty(); | ||
85 | |||
86 | return $this; | ||
87 | } | ||
88 | |||
89 | /** | ||
90 | * Gets a parameter to use in a query. | ||
91 | * | ||
92 | * The function is responsible for the right output escaping to use the | ||
93 | * value in a query. | ||
94 | * | ||
95 | * @return string The SQL escaped parameter to use in a query. | ||
96 | * | ||
97 | * @throws InvalidArgumentException | ||
98 | */ | ||
99 | final public function getParameter(string $name): string | ||
100 | { | ||
101 | if (! isset($this->parameters[$name])) { | ||
102 | throw new InvalidArgumentException("Parameter '" . $name . "' does not exist."); | ||
103 | } | ||
104 | |||
105 | if ($this->parameters[$name]['is_list']) { | ||
106 | throw FilterException::cannotConvertListParameterIntoSingleValue($name); | ||
107 | } | ||
108 | |||
109 | return $this->em->getConnection()->quote((string) $this->parameters[$name]['value']); | ||
110 | } | ||
111 | |||
112 | /** | ||
113 | * Gets a parameter to use in a query assuming it's a list of entries. | ||
114 | * | ||
115 | * The function is responsible for the right output escaping to use the | ||
116 | * value in a query, separating each entry by comma to inline it into | ||
117 | * an IN() query part. | ||
118 | * | ||
119 | * @throws InvalidArgumentException | ||
120 | */ | ||
121 | final public function getParameterList(string $name): string | ||
122 | { | ||
123 | if (! isset($this->parameters[$name])) { | ||
124 | throw new InvalidArgumentException("Parameter '" . $name . "' does not exist."); | ||
125 | } | ||
126 | |||
127 | if ($this->parameters[$name]['is_list'] === false) { | ||
128 | throw FilterException::cannotConvertSingleParameterIntoListValue($name); | ||
129 | } | ||
130 | |||
131 | $param = $this->parameters[$name]; | ||
132 | $connection = $this->em->getConnection(); | ||
133 | |||
134 | $quoted = array_map( | ||
135 | static fn (mixed $value): string => $connection->quote((string) $value), | ||
136 | $param['value'], | ||
137 | ); | ||
138 | |||
139 | return implode(',', $quoted); | ||
140 | } | ||
141 | |||
142 | /** | ||
143 | * Checks if a parameter was set for the filter. | ||
144 | */ | ||
145 | final public function hasParameter(string $name): bool | ||
146 | { | ||
147 | return isset($this->parameters[$name]); | ||
148 | } | ||
149 | |||
150 | /** | ||
151 | * Returns as string representation of the SQLFilter parameters (the state). | ||
152 | */ | ||
153 | final public function __toString(): string | ||
154 | { | ||
155 | return serialize($this->parameters); | ||
156 | } | ||
157 | |||
158 | /** | ||
159 | * Returns the database connection used by the entity manager | ||
160 | */ | ||
161 | final protected function getConnection(): Connection | ||
162 | { | ||
163 | return $this->em->getConnection(); | ||
164 | } | ||
165 | |||
166 | /** | ||
167 | * Gets the SQL query part to add to a query. | ||
168 | * | ||
169 | * @psalm-param ClassMetadata<object> $targetEntity | ||
170 | * | ||
171 | * @return string The constraint SQL if there is available, empty string otherwise. | ||
172 | */ | ||
173 | abstract public function addFilterConstraint(ClassMetadata $targetEntity, string $targetTableAlias): string; | ||
174 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/FilterCollection.php b/vendor/doctrine/orm/src/Query/FilterCollection.php new file mode 100644 index 0000000..3d3c576 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/FilterCollection.php | |||
@@ -0,0 +1,260 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\ORM\Configuration; | ||
8 | use Doctrine\ORM\EntityManagerInterface; | ||
9 | use Doctrine\ORM\Query\Filter\SQLFilter; | ||
10 | use InvalidArgumentException; | ||
11 | |||
12 | use function assert; | ||
13 | use function ksort; | ||
14 | |||
15 | /** | ||
16 | * Collection class for all the query filters. | ||
17 | */ | ||
18 | class FilterCollection | ||
19 | { | ||
20 | /* Filter STATES */ | ||
21 | |||
22 | /** | ||
23 | * A filter object is in CLEAN state when it has no changed parameters. | ||
24 | */ | ||
25 | public const FILTERS_STATE_CLEAN = 1; | ||
26 | |||
27 | /** | ||
28 | * A filter object is in DIRTY state when it has changed parameters. | ||
29 | */ | ||
30 | public const FILTERS_STATE_DIRTY = 2; | ||
31 | |||
32 | private readonly Configuration $config; | ||
33 | |||
34 | /** | ||
35 | * Instances of enabled filters. | ||
36 | * | ||
37 | * @var array<string, SQLFilter> | ||
38 | */ | ||
39 | private array $enabledFilters = []; | ||
40 | |||
41 | /** The filter hash from the last time the query was parsed. */ | ||
42 | private string $filterHash = ''; | ||
43 | |||
44 | /** | ||
45 | * Instances of suspended filters. | ||
46 | * | ||
47 | * @var SQLFilter[] | ||
48 | * @psalm-var array<string, SQLFilter> | ||
49 | */ | ||
50 | private array $suspendedFilters = []; | ||
51 | |||
52 | /** | ||
53 | * The current state of this filter. | ||
54 | * | ||
55 | * @psalm-var self::FILTERS_STATE_* | ||
56 | */ | ||
57 | private int $filtersState = self::FILTERS_STATE_CLEAN; | ||
58 | |||
59 | public function __construct( | ||
60 | private readonly EntityManagerInterface $em, | ||
61 | ) { | ||
62 | $this->config = $em->getConfiguration(); | ||
63 | } | ||
64 | |||
65 | /** | ||
66 | * Gets all the enabled filters. | ||
67 | * | ||
68 | * @return array<string, SQLFilter> The enabled filters. | ||
69 | */ | ||
70 | public function getEnabledFilters(): array | ||
71 | { | ||
72 | return $this->enabledFilters; | ||
73 | } | ||
74 | |||
75 | /** | ||
76 | * Gets all the suspended filters. | ||
77 | * | ||
78 | * @return SQLFilter[] The suspended filters. | ||
79 | * @psalm-return array<string, SQLFilter> | ||
80 | */ | ||
81 | public function getSuspendedFilters(): array | ||
82 | { | ||
83 | return $this->suspendedFilters; | ||
84 | } | ||
85 | |||
86 | /** | ||
87 | * Enables a filter from the collection. | ||
88 | * | ||
89 | * @throws InvalidArgumentException If the filter does not exist. | ||
90 | */ | ||
91 | public function enable(string $name): SQLFilter | ||
92 | { | ||
93 | if (! $this->has($name)) { | ||
94 | throw new InvalidArgumentException("Filter '" . $name . "' does not exist."); | ||
95 | } | ||
96 | |||
97 | if (! $this->isEnabled($name)) { | ||
98 | $filterClass = $this->config->getFilterClassName($name); | ||
99 | |||
100 | assert($filterClass !== null); | ||
101 | |||
102 | $this->enabledFilters[$name] = new $filterClass($this->em); | ||
103 | |||
104 | // In case a suspended filter with the same name was forgotten | ||
105 | unset($this->suspendedFilters[$name]); | ||
106 | |||
107 | // Keep the enabled filters sorted for the hash | ||
108 | ksort($this->enabledFilters); | ||
109 | |||
110 | $this->setFiltersStateDirty(); | ||
111 | } | ||
112 | |||
113 | return $this->enabledFilters[$name]; | ||
114 | } | ||
115 | |||
116 | /** | ||
117 | * Disables a filter. | ||
118 | * | ||
119 | * @throws InvalidArgumentException If the filter does not exist. | ||
120 | */ | ||
121 | public function disable(string $name): SQLFilter | ||
122 | { | ||
123 | // Get the filter to return it | ||
124 | $filter = $this->getFilter($name); | ||
125 | |||
126 | unset($this->enabledFilters[$name]); | ||
127 | |||
128 | $this->setFiltersStateDirty(); | ||
129 | |||
130 | return $filter; | ||
131 | } | ||
132 | |||
133 | /** | ||
134 | * Suspend a filter. | ||
135 | * | ||
136 | * @param string $name Name of the filter. | ||
137 | * | ||
138 | * @return SQLFilter The suspended filter. | ||
139 | * | ||
140 | * @throws InvalidArgumentException If the filter does not exist. | ||
141 | */ | ||
142 | public function suspend(string $name): SQLFilter | ||
143 | { | ||
144 | // Get the filter to return it | ||
145 | $filter = $this->getFilter($name); | ||
146 | |||
147 | $this->suspendedFilters[$name] = $filter; | ||
148 | unset($this->enabledFilters[$name]); | ||
149 | |||
150 | $this->setFiltersStateDirty(); | ||
151 | |||
152 | return $filter; | ||
153 | } | ||
154 | |||
155 | /** | ||
156 | * Restore a disabled filter from the collection. | ||
157 | * | ||
158 | * @param string $name Name of the filter. | ||
159 | * | ||
160 | * @return SQLFilter The restored filter. | ||
161 | * | ||
162 | * @throws InvalidArgumentException If the filter does not exist. | ||
163 | */ | ||
164 | public function restore(string $name): SQLFilter | ||
165 | { | ||
166 | if (! $this->isSuspended($name)) { | ||
167 | throw new InvalidArgumentException("Filter '" . $name . "' is not suspended."); | ||
168 | } | ||
169 | |||
170 | $this->enabledFilters[$name] = $this->suspendedFilters[$name]; | ||
171 | unset($this->suspendedFilters[$name]); | ||
172 | |||
173 | // Keep the enabled filters sorted for the hash | ||
174 | ksort($this->enabledFilters); | ||
175 | |||
176 | $this->setFiltersStateDirty(); | ||
177 | |||
178 | return $this->enabledFilters[$name]; | ||
179 | } | ||
180 | |||
181 | /** | ||
182 | * Gets an enabled filter from the collection. | ||
183 | * | ||
184 | * @throws InvalidArgumentException If the filter is not enabled. | ||
185 | */ | ||
186 | public function getFilter(string $name): SQLFilter | ||
187 | { | ||
188 | if (! $this->isEnabled($name)) { | ||
189 | throw new InvalidArgumentException("Filter '" . $name . "' is not enabled."); | ||
190 | } | ||
191 | |||
192 | return $this->enabledFilters[$name]; | ||
193 | } | ||
194 | |||
195 | /** | ||
196 | * Checks whether filter with given name is defined. | ||
197 | */ | ||
198 | public function has(string $name): bool | ||
199 | { | ||
200 | return $this->config->getFilterClassName($name) !== null; | ||
201 | } | ||
202 | |||
203 | /** | ||
204 | * Checks if a filter is enabled. | ||
205 | */ | ||
206 | public function isEnabled(string $name): bool | ||
207 | { | ||
208 | return isset($this->enabledFilters[$name]); | ||
209 | } | ||
210 | |||
211 | /** | ||
212 | * Checks if a filter is suspended. | ||
213 | * | ||
214 | * @param string $name Name of the filter. | ||
215 | * | ||
216 | * @return bool True if the filter is suspended, false otherwise. | ||
217 | */ | ||
218 | public function isSuspended(string $name): bool | ||
219 | { | ||
220 | return isset($this->suspendedFilters[$name]); | ||
221 | } | ||
222 | |||
223 | /** | ||
224 | * Checks if the filter collection is clean. | ||
225 | */ | ||
226 | public function isClean(): bool | ||
227 | { | ||
228 | return $this->filtersState === self::FILTERS_STATE_CLEAN; | ||
229 | } | ||
230 | |||
231 | /** | ||
232 | * Generates a string of currently enabled filters to use for the cache id. | ||
233 | */ | ||
234 | public function getHash(): string | ||
235 | { | ||
236 | // If there are only clean filters, the previous hash can be returned | ||
237 | if ($this->filtersState === self::FILTERS_STATE_CLEAN) { | ||
238 | return $this->filterHash; | ||
239 | } | ||
240 | |||
241 | $filterHash = ''; | ||
242 | |||
243 | foreach ($this->enabledFilters as $name => $filter) { | ||
244 | $filterHash .= $name . $filter; | ||
245 | } | ||
246 | |||
247 | $this->filterHash = $filterHash; | ||
248 | $this->filtersState = self::FILTERS_STATE_CLEAN; | ||
249 | |||
250 | return $filterHash; | ||
251 | } | ||
252 | |||
253 | /** | ||
254 | * Sets the filter state to dirty. | ||
255 | */ | ||
256 | public function setFiltersStateDirty(): void | ||
257 | { | ||
258 | $this->filtersState = self::FILTERS_STATE_DIRTY; | ||
259 | } | ||
260 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Lexer.php b/vendor/doctrine/orm/src/Query/Lexer.php new file mode 100644 index 0000000..c446675 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Lexer.php | |||
@@ -0,0 +1,150 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\Common\Lexer\AbstractLexer; | ||
8 | |||
9 | use function constant; | ||
10 | use function ctype_alpha; | ||
11 | use function defined; | ||
12 | use function is_numeric; | ||
13 | use function str_contains; | ||
14 | use function str_replace; | ||
15 | use function stripos; | ||
16 | use function strlen; | ||
17 | use function strtoupper; | ||
18 | use function substr; | ||
19 | |||
20 | /** | ||
21 | * Scans a DQL query for tokens. | ||
22 | * | ||
23 | * @extends AbstractLexer<TokenType, string> | ||
24 | */ | ||
25 | class Lexer extends AbstractLexer | ||
26 | { | ||
27 | /** | ||
28 | * Creates a new query scanner object. | ||
29 | * | ||
30 | * @param string $input A query string. | ||
31 | */ | ||
32 | public function __construct(string $input) | ||
33 | { | ||
34 | $this->setInput($input); | ||
35 | } | ||
36 | |||
37 | /** | ||
38 | * {@inheritDoc} | ||
39 | */ | ||
40 | protected function getCatchablePatterns(): array | ||
41 | { | ||
42 | return [ | ||
43 | '[a-z_][a-z0-9_]*\:[a-z_][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // aliased name | ||
44 | '[a-z_\\\][a-z0-9_]*(?:\\\[a-z_][a-z0-9_]*)*', // identifier or qualified name | ||
45 | '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', // numbers | ||
46 | "'(?:[^']|'')*'", // quoted strings | ||
47 | '\?[0-9]*|:[a-z_][a-z0-9_]*', // parameters | ||
48 | ]; | ||
49 | } | ||
50 | |||
51 | /** | ||
52 | * {@inheritDoc} | ||
53 | */ | ||
54 | protected function getNonCatchablePatterns(): array | ||
55 | { | ||
56 | return ['\s+', '--.*', '(.)']; | ||
57 | } | ||
58 | |||
59 | protected function getType(string &$value): TokenType | ||
60 | { | ||
61 | $type = TokenType::T_NONE; | ||
62 | |||
63 | switch (true) { | ||
64 | // Recognize numeric values | ||
65 | case is_numeric($value): | ||
66 | if (str_contains($value, '.') || stripos($value, 'e') !== false) { | ||
67 | return TokenType::T_FLOAT; | ||
68 | } | ||
69 | |||
70 | return TokenType::T_INTEGER; | ||
71 | |||
72 | // Recognize quoted strings | ||
73 | case $value[0] === "'": | ||
74 | $value = str_replace("''", "'", substr($value, 1, strlen($value) - 2)); | ||
75 | |||
76 | return TokenType::T_STRING; | ||
77 | |||
78 | // Recognize identifiers, aliased or qualified names | ||
79 | case ctype_alpha($value[0]) || $value[0] === '_' || $value[0] === '\\': | ||
80 | $name = 'Doctrine\ORM\Query\TokenType::T_' . strtoupper($value); | ||
81 | |||
82 | if (defined($name)) { | ||
83 | $type = constant($name); | ||
84 | |||
85 | if ($type->value > 100) { | ||
86 | return $type; | ||
87 | } | ||
88 | } | ||
89 | |||
90 | if (str_contains($value, '\\')) { | ||
91 | return TokenType::T_FULLY_QUALIFIED_NAME; | ||
92 | } | ||
93 | |||
94 | return TokenType::T_IDENTIFIER; | ||
95 | |||
96 | // Recognize input parameters | ||
97 | case $value[0] === '?' || $value[0] === ':': | ||
98 | return TokenType::T_INPUT_PARAMETER; | ||
99 | |||
100 | // Recognize symbols | ||
101 | case $value === '.': | ||
102 | return TokenType::T_DOT; | ||
103 | |||
104 | case $value === ',': | ||
105 | return TokenType::T_COMMA; | ||
106 | |||
107 | case $value === '(': | ||
108 | return TokenType::T_OPEN_PARENTHESIS; | ||
109 | |||
110 | case $value === ')': | ||
111 | return TokenType::T_CLOSE_PARENTHESIS; | ||
112 | |||
113 | case $value === '=': | ||
114 | return TokenType::T_EQUALS; | ||
115 | |||
116 | case $value === '>': | ||
117 | return TokenType::T_GREATER_THAN; | ||
118 | |||
119 | case $value === '<': | ||
120 | return TokenType::T_LOWER_THAN; | ||
121 | |||
122 | case $value === '+': | ||
123 | return TokenType::T_PLUS; | ||
124 | |||
125 | case $value === '-': | ||
126 | return TokenType::T_MINUS; | ||
127 | |||
128 | case $value === '*': | ||
129 | return TokenType::T_MULTIPLY; | ||
130 | |||
131 | case $value === '/': | ||
132 | return TokenType::T_DIVIDE; | ||
133 | |||
134 | case $value === '!': | ||
135 | return TokenType::T_NEGATE; | ||
136 | |||
137 | case $value === '{': | ||
138 | return TokenType::T_OPEN_CURLY_BRACE; | ||
139 | |||
140 | case $value === '}': | ||
141 | return TokenType::T_CLOSE_CURLY_BRACE; | ||
142 | |||
143 | // Default | ||
144 | default: | ||
145 | // Do nothing | ||
146 | } | ||
147 | |||
148 | return $type; | ||
149 | } | ||
150 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Parameter.php b/vendor/doctrine/orm/src/Query/Parameter.php new file mode 100644 index 0000000..43eb7a4 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Parameter.php | |||
@@ -0,0 +1,89 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use function trim; | ||
8 | |||
9 | /** | ||
10 | * Defines a Query Parameter. | ||
11 | * | ||
12 | * @link www.doctrine-project.org | ||
13 | */ | ||
14 | class Parameter | ||
15 | { | ||
16 | /** | ||
17 | * Returns the internal representation of a parameter name. | ||
18 | */ | ||
19 | public static function normalizeName(int|string $name): string | ||
20 | { | ||
21 | return trim((string) $name, ':'); | ||
22 | } | ||
23 | |||
24 | /** | ||
25 | * The parameter name. | ||
26 | */ | ||
27 | private readonly string $name; | ||
28 | |||
29 | /** | ||
30 | * The parameter value. | ||
31 | */ | ||
32 | private mixed $value; | ||
33 | |||
34 | /** | ||
35 | * The parameter type. | ||
36 | */ | ||
37 | private mixed $type; | ||
38 | |||
39 | /** | ||
40 | * Whether the parameter type was explicitly specified or not | ||
41 | */ | ||
42 | private readonly bool $typeSpecified; | ||
43 | |||
44 | public function __construct(int|string $name, mixed $value, mixed $type = null) | ||
45 | { | ||
46 | $this->name = self::normalizeName($name); | ||
47 | $this->typeSpecified = $type !== null; | ||
48 | |||
49 | $this->setValue($value, $type); | ||
50 | } | ||
51 | |||
52 | /** | ||
53 | * Retrieves the Parameter name. | ||
54 | */ | ||
55 | public function getName(): string | ||
56 | { | ||
57 | return $this->name; | ||
58 | } | ||
59 | |||
60 | /** | ||
61 | * Retrieves the Parameter value. | ||
62 | */ | ||
63 | public function getValue(): mixed | ||
64 | { | ||
65 | return $this->value; | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * Retrieves the Parameter type. | ||
70 | */ | ||
71 | public function getType(): mixed | ||
72 | { | ||
73 | return $this->type; | ||
74 | } | ||
75 | |||
76 | /** | ||
77 | * Defines the Parameter value. | ||
78 | */ | ||
79 | public function setValue(mixed $value, mixed $type = null): void | ||
80 | { | ||
81 | $this->value = $value; | ||
82 | $this->type = $type ?: ParameterTypeInferer::inferType($value); | ||
83 | } | ||
84 | |||
85 | public function typeWasSpecified(): bool | ||
86 | { | ||
87 | return $this->typeSpecified; | ||
88 | } | ||
89 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/ParameterTypeInferer.php b/vendor/doctrine/orm/src/Query/ParameterTypeInferer.php new file mode 100644 index 0000000..dae28fa --- /dev/null +++ b/vendor/doctrine/orm/src/Query/ParameterTypeInferer.php | |||
@@ -0,0 +1,77 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use BackedEnum; | ||
8 | use DateInterval; | ||
9 | use DateTimeImmutable; | ||
10 | use DateTimeInterface; | ||
11 | use Doctrine\DBAL\ArrayParameterType; | ||
12 | use Doctrine\DBAL\ParameterType; | ||
13 | use Doctrine\DBAL\Types\Types; | ||
14 | |||
15 | use function current; | ||
16 | use function is_array; | ||
17 | use function is_bool; | ||
18 | use function is_int; | ||
19 | |||
20 | /** | ||
21 | * Provides an enclosed support for parameter inferring. | ||
22 | * | ||
23 | * @link www.doctrine-project.org | ||
24 | */ | ||
25 | final class ParameterTypeInferer | ||
26 | { | ||
27 | /** | ||
28 | * Infers type of a given value, returning a compatible constant: | ||
29 | * - Type (\Doctrine\DBAL\Types\Type::*) | ||
30 | * - Connection (\Doctrine\DBAL\Connection::PARAM_*) | ||
31 | */ | ||
32 | public static function inferType(mixed $value): ParameterType|ArrayParameterType|int|string | ||
33 | { | ||
34 | if (is_int($value)) { | ||
35 | return Types::INTEGER; | ||
36 | } | ||
37 | |||
38 | if (is_bool($value)) { | ||
39 | return Types::BOOLEAN; | ||
40 | } | ||
41 | |||
42 | if ($value instanceof DateTimeImmutable) { | ||
43 | return Types::DATETIME_IMMUTABLE; | ||
44 | } | ||
45 | |||
46 | if ($value instanceof DateTimeInterface) { | ||
47 | return Types::DATETIME_MUTABLE; | ||
48 | } | ||
49 | |||
50 | if ($value instanceof DateInterval) { | ||
51 | return Types::DATEINTERVAL; | ||
52 | } | ||
53 | |||
54 | if ($value instanceof BackedEnum) { | ||
55 | return is_int($value->value) | ||
56 | ? Types::INTEGER | ||
57 | : Types::STRING; | ||
58 | } | ||
59 | |||
60 | if (is_array($value)) { | ||
61 | $firstValue = current($value); | ||
62 | if ($firstValue instanceof BackedEnum) { | ||
63 | $firstValue = $firstValue->value; | ||
64 | } | ||
65 | |||
66 | return is_int($firstValue) | ||
67 | ? ArrayParameterType::INTEGER | ||
68 | : ArrayParameterType::STRING; | ||
69 | } | ||
70 | |||
71 | return ParameterType::STRING; | ||
72 | } | ||
73 | |||
74 | private function __construct() | ||
75 | { | ||
76 | } | ||
77 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Parser.php b/vendor/doctrine/orm/src/Query/Parser.php new file mode 100644 index 0000000..e948f2c --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Parser.php | |||
@@ -0,0 +1,3269 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\Common\Lexer\Token; | ||
8 | use Doctrine\ORM\EntityManagerInterface; | ||
9 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
11 | use Doctrine\ORM\Query; | ||
12 | use Doctrine\ORM\Query\AST\Functions; | ||
13 | use LogicException; | ||
14 | use ReflectionClass; | ||
15 | |||
16 | use function array_search; | ||
17 | use function assert; | ||
18 | use function class_exists; | ||
19 | use function count; | ||
20 | use function implode; | ||
21 | use function in_array; | ||
22 | use function interface_exists; | ||
23 | use function is_string; | ||
24 | use function sprintf; | ||
25 | use function str_contains; | ||
26 | use function strlen; | ||
27 | use function strpos; | ||
28 | use function strrpos; | ||
29 | use function strtolower; | ||
30 | use function substr; | ||
31 | |||
32 | /** | ||
33 | * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language. | ||
34 | * Parses a DQL query, reports any errors in it, and generates an AST. | ||
35 | * | ||
36 | * @psalm-type DqlToken = Token<TokenType, string> | ||
37 | * @psalm-type QueryComponent = array{ | ||
38 | * metadata?: ClassMetadata<object>, | ||
39 | * parent?: string|null, | ||
40 | * relation?: AssociationMapping|null, | ||
41 | * map?: string|null, | ||
42 | * resultVariable?: AST\Node|string, | ||
43 | * nestingLevel: int, | ||
44 | * token: DqlToken, | ||
45 | * } | ||
46 | */ | ||
47 | final class Parser | ||
48 | { | ||
49 | /** | ||
50 | * @readonly Maps BUILT-IN string function names to AST class names. | ||
51 | * @psalm-var array<string, class-string<Functions\FunctionNode>> | ||
52 | */ | ||
53 | private static array $stringFunctions = [ | ||
54 | 'concat' => Functions\ConcatFunction::class, | ||
55 | 'substring' => Functions\SubstringFunction::class, | ||
56 | 'trim' => Functions\TrimFunction::class, | ||
57 | 'lower' => Functions\LowerFunction::class, | ||
58 | 'upper' => Functions\UpperFunction::class, | ||
59 | 'identity' => Functions\IdentityFunction::class, | ||
60 | ]; | ||
61 | |||
62 | /** | ||
63 | * @readonly Maps BUILT-IN numeric function names to AST class names. | ||
64 | * @psalm-var array<string, class-string<Functions\FunctionNode>> | ||
65 | */ | ||
66 | private static array $numericFunctions = [ | ||
67 | 'length' => Functions\LengthFunction::class, | ||
68 | 'locate' => Functions\LocateFunction::class, | ||
69 | 'abs' => Functions\AbsFunction::class, | ||
70 | 'sqrt' => Functions\SqrtFunction::class, | ||
71 | 'mod' => Functions\ModFunction::class, | ||
72 | 'size' => Functions\SizeFunction::class, | ||
73 | 'date_diff' => Functions\DateDiffFunction::class, | ||
74 | 'bit_and' => Functions\BitAndFunction::class, | ||
75 | 'bit_or' => Functions\BitOrFunction::class, | ||
76 | |||
77 | // Aggregate functions | ||
78 | 'min' => Functions\MinFunction::class, | ||
79 | 'max' => Functions\MaxFunction::class, | ||
80 | 'avg' => Functions\AvgFunction::class, | ||
81 | 'sum' => Functions\SumFunction::class, | ||
82 | 'count' => Functions\CountFunction::class, | ||
83 | ]; | ||
84 | |||
85 | /** | ||
86 | * @readonly Maps BUILT-IN datetime function names to AST class names. | ||
87 | * @psalm-var array<string, class-string<Functions\FunctionNode>> | ||
88 | */ | ||
89 | private static array $datetimeFunctions = [ | ||
90 | 'current_date' => Functions\CurrentDateFunction::class, | ||
91 | 'current_time' => Functions\CurrentTimeFunction::class, | ||
92 | 'current_timestamp' => Functions\CurrentTimestampFunction::class, | ||
93 | 'date_add' => Functions\DateAddFunction::class, | ||
94 | 'date_sub' => Functions\DateSubFunction::class, | ||
95 | ]; | ||
96 | |||
97 | /* | ||
98 | * Expressions that were encountered during parsing of identifiers and expressions | ||
99 | * and still need to be validated. | ||
100 | */ | ||
101 | |||
102 | /** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */ | ||
103 | private array $deferredIdentificationVariables = []; | ||
104 | |||
105 | /** @psalm-var list<array{token: DqlToken|null, expression: AST\PathExpression, nestingLevel: int}> */ | ||
106 | private array $deferredPathExpressions = []; | ||
107 | |||
108 | /** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */ | ||
109 | private array $deferredResultVariables = []; | ||
110 | |||
111 | /** @psalm-var list<array{token: DqlToken|null, expression: AST\NewObjectExpression, nestingLevel: int}> */ | ||
112 | private array $deferredNewObjectExpressions = []; | ||
113 | |||
114 | /** | ||
115 | * The lexer. | ||
116 | */ | ||
117 | private readonly Lexer $lexer; | ||
118 | |||
119 | /** | ||
120 | * The parser result. | ||
121 | */ | ||
122 | private readonly ParserResult $parserResult; | ||
123 | |||
124 | /** | ||
125 | * The EntityManager. | ||
126 | */ | ||
127 | private readonly EntityManagerInterface $em; | ||
128 | |||
129 | /** | ||
130 | * Map of declared query components in the parsed query. | ||
131 | * | ||
132 | * @psalm-var array<string, QueryComponent> | ||
133 | */ | ||
134 | private array $queryComponents = []; | ||
135 | |||
136 | /** | ||
137 | * Keeps the nesting level of defined ResultVariables. | ||
138 | */ | ||
139 | private int $nestingLevel = 0; | ||
140 | |||
141 | /** | ||
142 | * Any additional custom tree walkers that modify the AST. | ||
143 | * | ||
144 | * @psalm-var list<class-string<TreeWalker>> | ||
145 | */ | ||
146 | private array $customTreeWalkers = []; | ||
147 | |||
148 | /** | ||
149 | * The custom last tree walker, if any, that is responsible for producing the output. | ||
150 | * | ||
151 | * @var class-string<SqlWalker>|null | ||
152 | */ | ||
153 | private $customOutputWalker; | ||
154 | |||
155 | /** @psalm-var array<string, AST\SelectExpression> */ | ||
156 | private array $identVariableExpressions = []; | ||
157 | |||
158 | /** | ||
159 | * Creates a new query parser object. | ||
160 | * | ||
161 | * @param Query $query The Query to parse. | ||
162 | */ | ||
163 | public function __construct(private readonly Query $query) | ||
164 | { | ||
165 | $this->em = $query->getEntityManager(); | ||
166 | $this->lexer = new Lexer((string) $query->getDQL()); | ||
167 | $this->parserResult = new ParserResult(); | ||
168 | } | ||
169 | |||
170 | /** | ||
171 | * Sets a custom tree walker that produces output. | ||
172 | * This tree walker will be run last over the AST, after any other walkers. | ||
173 | * | ||
174 | * @psalm-param class-string<SqlWalker> $className | ||
175 | */ | ||
176 | public function setCustomOutputTreeWalker(string $className): void | ||
177 | { | ||
178 | $this->customOutputWalker = $className; | ||
179 | } | ||
180 | |||
181 | /** | ||
182 | * Adds a custom tree walker for modifying the AST. | ||
183 | * | ||
184 | * @psalm-param class-string<TreeWalker> $className | ||
185 | */ | ||
186 | public function addCustomTreeWalker(string $className): void | ||
187 | { | ||
188 | $this->customTreeWalkers[] = $className; | ||
189 | } | ||
190 | |||
191 | /** | ||
192 | * Gets the lexer used by the parser. | ||
193 | */ | ||
194 | public function getLexer(): Lexer | ||
195 | { | ||
196 | return $this->lexer; | ||
197 | } | ||
198 | |||
199 | /** | ||
200 | * Gets the ParserResult that is being filled with information during parsing. | ||
201 | */ | ||
202 | public function getParserResult(): ParserResult | ||
203 | { | ||
204 | return $this->parserResult; | ||
205 | } | ||
206 | |||
207 | /** | ||
208 | * Gets the EntityManager used by the parser. | ||
209 | */ | ||
210 | public function getEntityManager(): EntityManagerInterface | ||
211 | { | ||
212 | return $this->em; | ||
213 | } | ||
214 | |||
215 | /** | ||
216 | * Parses and builds AST for the given Query. | ||
217 | */ | ||
218 | public function getAST(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement | ||
219 | { | ||
220 | // Parse & build AST | ||
221 | $AST = $this->QueryLanguage(); | ||
222 | |||
223 | // Process any deferred validations of some nodes in the AST. | ||
224 | // This also allows post-processing of the AST for modification purposes. | ||
225 | $this->processDeferredIdentificationVariables(); | ||
226 | |||
227 | if ($this->deferredPathExpressions) { | ||
228 | $this->processDeferredPathExpressions(); | ||
229 | } | ||
230 | |||
231 | if ($this->deferredResultVariables) { | ||
232 | $this->processDeferredResultVariables(); | ||
233 | } | ||
234 | |||
235 | if ($this->deferredNewObjectExpressions) { | ||
236 | $this->processDeferredNewObjectExpressions($AST); | ||
237 | } | ||
238 | |||
239 | $this->processRootEntityAliasSelected(); | ||
240 | |||
241 | // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot! | ||
242 | $this->fixIdentificationVariableOrder($AST); | ||
243 | |||
244 | return $AST; | ||
245 | } | ||
246 | |||
247 | /** | ||
248 | * Attempts to match the given token with the current lookahead token. | ||
249 | * | ||
250 | * If they match, updates the lookahead token; otherwise raises a syntax | ||
251 | * error. | ||
252 | * | ||
253 | * @throws QueryException If the tokens don't match. | ||
254 | */ | ||
255 | public function match(TokenType $token): void | ||
256 | { | ||
257 | $lookaheadType = $this->lexer->lookahead->type ?? null; | ||
258 | |||
259 | // Short-circuit on first condition, usually types match | ||
260 | if ($lookaheadType === $token) { | ||
261 | $this->lexer->moveNext(); | ||
262 | |||
263 | return; | ||
264 | } | ||
265 | |||
266 | // If parameter is not identifier (1-99) must be exact match | ||
267 | if ($token->value < TokenType::T_IDENTIFIER->value) { | ||
268 | $this->syntaxError($this->lexer->getLiteral($token)); | ||
269 | } | ||
270 | |||
271 | // If parameter is keyword (200+) must be exact match | ||
272 | if ($token->value > TokenType::T_IDENTIFIER->value) { | ||
273 | $this->syntaxError($this->lexer->getLiteral($token)); | ||
274 | } | ||
275 | |||
276 | // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+) | ||
277 | if ($token->value === TokenType::T_IDENTIFIER->value && $lookaheadType->value < TokenType::T_IDENTIFIER->value) { | ||
278 | $this->syntaxError($this->lexer->getLiteral($token)); | ||
279 | } | ||
280 | |||
281 | $this->lexer->moveNext(); | ||
282 | } | ||
283 | |||
284 | /** | ||
285 | * Frees this parser, enabling it to be reused. | ||
286 | * | ||
287 | * @param bool $deep Whether to clean peek and reset errors. | ||
288 | * @param int $position Position to reset. | ||
289 | */ | ||
290 | public function free(bool $deep = false, int $position = 0): void | ||
291 | { | ||
292 | // WARNING! Use this method with care. It resets the scanner! | ||
293 | $this->lexer->resetPosition($position); | ||
294 | |||
295 | // Deep = true cleans peek and also any previously defined errors | ||
296 | if ($deep) { | ||
297 | $this->lexer->resetPeek(); | ||
298 | } | ||
299 | |||
300 | $this->lexer->token = null; | ||
301 | $this->lexer->lookahead = null; | ||
302 | } | ||
303 | |||
304 | /** | ||
305 | * Parses a query string. | ||
306 | */ | ||
307 | public function parse(): ParserResult | ||
308 | { | ||
309 | $AST = $this->getAST(); | ||
310 | |||
311 | $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS); | ||
312 | if ($customWalkers !== false) { | ||
313 | $this->customTreeWalkers = $customWalkers; | ||
314 | } | ||
315 | |||
316 | $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER); | ||
317 | if ($customOutputWalker !== false) { | ||
318 | $this->customOutputWalker = $customOutputWalker; | ||
319 | } | ||
320 | |||
321 | // Run any custom tree walkers over the AST | ||
322 | if ($this->customTreeWalkers) { | ||
323 | $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents); | ||
324 | |||
325 | foreach ($this->customTreeWalkers as $walker) { | ||
326 | $treeWalkerChain->addTreeWalker($walker); | ||
327 | } | ||
328 | |||
329 | match (true) { | ||
330 | $AST instanceof AST\UpdateStatement => $treeWalkerChain->walkUpdateStatement($AST), | ||
331 | $AST instanceof AST\DeleteStatement => $treeWalkerChain->walkDeleteStatement($AST), | ||
332 | $AST instanceof AST\SelectStatement => $treeWalkerChain->walkSelectStatement($AST), | ||
333 | }; | ||
334 | |||
335 | $this->queryComponents = $treeWalkerChain->getQueryComponents(); | ||
336 | } | ||
337 | |||
338 | $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class; | ||
339 | $outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents); | ||
340 | |||
341 | // Assign an SQL executor to the parser result | ||
342 | $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST)); | ||
343 | |||
344 | return $this->parserResult; | ||
345 | } | ||
346 | |||
347 | /** | ||
348 | * Fixes order of identification variables. | ||
349 | * | ||
350 | * They have to appear in the select clause in the same order as the | ||
351 | * declarations (from ... x join ... y join ... z ...) appear in the query | ||
352 | * as the hydration process relies on that order for proper operation. | ||
353 | */ | ||
354 | private function fixIdentificationVariableOrder(AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST): void | ||
355 | { | ||
356 | if (count($this->identVariableExpressions) <= 1) { | ||
357 | return; | ||
358 | } | ||
359 | |||
360 | assert($AST instanceof AST\SelectStatement); | ||
361 | |||
362 | foreach ($this->queryComponents as $dqlAlias => $qComp) { | ||
363 | if (! isset($this->identVariableExpressions[$dqlAlias])) { | ||
364 | continue; | ||
365 | } | ||
366 | |||
367 | $expr = $this->identVariableExpressions[$dqlAlias]; | ||
368 | $key = array_search($expr, $AST->selectClause->selectExpressions, true); | ||
369 | |||
370 | unset($AST->selectClause->selectExpressions[$key]); | ||
371 | |||
372 | $AST->selectClause->selectExpressions[] = $expr; | ||
373 | } | ||
374 | } | ||
375 | |||
376 | /** | ||
377 | * Generates a new syntax error. | ||
378 | * | ||
379 | * @param string $expected Expected string. | ||
380 | * @param DqlToken|null $token Got token. | ||
381 | * | ||
382 | * @throws QueryException | ||
383 | */ | ||
384 | public function syntaxError(string $expected = '', Token|null $token = null): never | ||
385 | { | ||
386 | if ($token === null) { | ||
387 | $token = $this->lexer->lookahead; | ||
388 | } | ||
389 | |||
390 | $tokenPos = $token->position ?? '-1'; | ||
391 | |||
392 | $message = sprintf('line 0, col %d: Error: ', $tokenPos); | ||
393 | $message .= $expected !== '' ? sprintf('Expected %s, got ', $expected) : 'Unexpected '; | ||
394 | $message .= $this->lexer->lookahead === null ? 'end of string.' : sprintf("'%s'", $token->value); | ||
395 | |||
396 | throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL() ?? '')); | ||
397 | } | ||
398 | |||
399 | /** | ||
400 | * Generates a new semantical error. | ||
401 | * | ||
402 | * @param string $message Optional message. | ||
403 | * @psalm-param DqlToken|null $token | ||
404 | * | ||
405 | * @throws QueryException | ||
406 | */ | ||
407 | public function semanticalError(string $message = '', Token|null $token = null): never | ||
408 | { | ||
409 | if ($token === null) { | ||
410 | $token = $this->lexer->lookahead ?? new Token('fake token', 42, 0); | ||
411 | } | ||
412 | |||
413 | // Minimum exposed chars ahead of token | ||
414 | $distance = 12; | ||
415 | |||
416 | // Find a position of a final word to display in error string | ||
417 | $dql = $this->query->getDQL(); | ||
418 | $length = strlen($dql); | ||
419 | $pos = $token->position + $distance; | ||
420 | $pos = strpos($dql, ' ', $length > $pos ? $pos : $length); | ||
421 | $length = $pos !== false ? $pos - $token->position : $distance; | ||
422 | |||
423 | $tokenPos = $token->position > 0 ? $token->position : '-1'; | ||
424 | $tokenStr = substr($dql, $token->position, $length); | ||
425 | |||
426 | // Building informative message | ||
427 | $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message; | ||
428 | |||
429 | throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL())); | ||
430 | } | ||
431 | |||
432 | /** | ||
433 | * Peeks beyond the matched closing parenthesis and returns the first token after that one. | ||
434 | * | ||
435 | * @param bool $resetPeek Reset peek after finding the closing parenthesis. | ||
436 | * | ||
437 | * @psalm-return DqlToken|null | ||
438 | */ | ||
439 | private function peekBeyondClosingParenthesis(bool $resetPeek = true): Token|null | ||
440 | { | ||
441 | $token = $this->lexer->peek(); | ||
442 | $numUnmatched = 1; | ||
443 | |||
444 | while ($numUnmatched > 0 && $token !== null) { | ||
445 | switch ($token->type) { | ||
446 | case TokenType::T_OPEN_PARENTHESIS: | ||
447 | ++$numUnmatched; | ||
448 | break; | ||
449 | |||
450 | case TokenType::T_CLOSE_PARENTHESIS: | ||
451 | --$numUnmatched; | ||
452 | break; | ||
453 | |||
454 | default: | ||
455 | // Do nothing | ||
456 | } | ||
457 | |||
458 | $token = $this->lexer->peek(); | ||
459 | } | ||
460 | |||
461 | if ($resetPeek) { | ||
462 | $this->lexer->resetPeek(); | ||
463 | } | ||
464 | |||
465 | return $token; | ||
466 | } | ||
467 | |||
468 | /** | ||
469 | * Checks if the given token indicates a mathematical operator. | ||
470 | * | ||
471 | * @psalm-param DqlToken|null $token | ||
472 | */ | ||
473 | private function isMathOperator(Token|null $token): bool | ||
474 | { | ||
475 | return $token !== null && in_array($token->type, [TokenType::T_PLUS, TokenType::T_MINUS, TokenType::T_DIVIDE, TokenType::T_MULTIPLY], true); | ||
476 | } | ||
477 | |||
478 | /** | ||
479 | * Checks if the next-next (after lookahead) token starts a function. | ||
480 | * | ||
481 | * @return bool TRUE if the next-next tokens start a function, FALSE otherwise. | ||
482 | */ | ||
483 | private function isFunction(): bool | ||
484 | { | ||
485 | assert($this->lexer->lookahead !== null); | ||
486 | $lookaheadType = $this->lexer->lookahead->type; | ||
487 | $peek = $this->lexer->peek(); | ||
488 | |||
489 | $this->lexer->resetPeek(); | ||
490 | |||
491 | return $lookaheadType->value >= TokenType::T_IDENTIFIER->value && $peek !== null && $peek->type === TokenType::T_OPEN_PARENTHESIS; | ||
492 | } | ||
493 | |||
494 | /** | ||
495 | * Checks whether the given token type indicates an aggregate function. | ||
496 | * | ||
497 | * @return bool TRUE if the token type is an aggregate function, FALSE otherwise. | ||
498 | */ | ||
499 | private function isAggregateFunction(TokenType $tokenType): bool | ||
500 | { | ||
501 | return in_array( | ||
502 | $tokenType, | ||
503 | [TokenType::T_AVG, TokenType::T_MIN, TokenType::T_MAX, TokenType::T_SUM, TokenType::T_COUNT], | ||
504 | true, | ||
505 | ); | ||
506 | } | ||
507 | |||
508 | /** | ||
509 | * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME. | ||
510 | */ | ||
511 | private function isNextAllAnySome(): bool | ||
512 | { | ||
513 | assert($this->lexer->lookahead !== null); | ||
514 | |||
515 | return in_array( | ||
516 | $this->lexer->lookahead->type, | ||
517 | [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], | ||
518 | true, | ||
519 | ); | ||
520 | } | ||
521 | |||
522 | /** | ||
523 | * Validates that the given <tt>IdentificationVariable</tt> is semantically correct. | ||
524 | * It must exist in query components list. | ||
525 | */ | ||
526 | private function processDeferredIdentificationVariables(): void | ||
527 | { | ||
528 | foreach ($this->deferredIdentificationVariables as $deferredItem) { | ||
529 | $identVariable = $deferredItem['expression']; | ||
530 | |||
531 | // Check if IdentificationVariable exists in queryComponents | ||
532 | if (! isset($this->queryComponents[$identVariable])) { | ||
533 | $this->semanticalError( | ||
534 | sprintf("'%s' is not defined.", $identVariable), | ||
535 | $deferredItem['token'], | ||
536 | ); | ||
537 | } | ||
538 | |||
539 | $qComp = $this->queryComponents[$identVariable]; | ||
540 | |||
541 | // Check if queryComponent points to an AbstractSchemaName or a ResultVariable | ||
542 | if (! isset($qComp['metadata'])) { | ||
543 | $this->semanticalError( | ||
544 | sprintf("'%s' does not point to a Class.", $identVariable), | ||
545 | $deferredItem['token'], | ||
546 | ); | ||
547 | } | ||
548 | |||
549 | // Validate if identification variable nesting level is lower or equal than the current one | ||
550 | if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { | ||
551 | $this->semanticalError( | ||
552 | sprintf("'%s' is used outside the scope of its declaration.", $identVariable), | ||
553 | $deferredItem['token'], | ||
554 | ); | ||
555 | } | ||
556 | } | ||
557 | } | ||
558 | |||
559 | /** | ||
560 | * Validates that the given <tt>NewObjectExpression</tt>. | ||
561 | */ | ||
562 | private function processDeferredNewObjectExpressions(AST\SelectStatement $AST): void | ||
563 | { | ||
564 | foreach ($this->deferredNewObjectExpressions as $deferredItem) { | ||
565 | $expression = $deferredItem['expression']; | ||
566 | $token = $deferredItem['token']; | ||
567 | $className = $expression->className; | ||
568 | $args = $expression->args; | ||
569 | $fromClassName = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null; | ||
570 | |||
571 | // If the namespace is not given then assumes the first FROM entity namespace | ||
572 | if (! str_contains($className, '\\') && ! class_exists($className) && is_string($fromClassName) && str_contains($fromClassName, '\\')) { | ||
573 | $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\')); | ||
574 | $fqcn = $namespace . '\\' . $className; | ||
575 | |||
576 | if (class_exists($fqcn)) { | ||
577 | $expression->className = $fqcn; | ||
578 | $className = $fqcn; | ||
579 | } | ||
580 | } | ||
581 | |||
582 | if (! class_exists($className)) { | ||
583 | $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token); | ||
584 | } | ||
585 | |||
586 | $class = new ReflectionClass($className); | ||
587 | |||
588 | if (! $class->isInstantiable()) { | ||
589 | $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token); | ||
590 | } | ||
591 | |||
592 | if ($class->getConstructor() === null) { | ||
593 | $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token); | ||
594 | } | ||
595 | |||
596 | if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) { | ||
597 | $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token); | ||
598 | } | ||
599 | } | ||
600 | } | ||
601 | |||
602 | /** | ||
603 | * Validates that the given <tt>ResultVariable</tt> is semantically correct. | ||
604 | * It must exist in query components list. | ||
605 | */ | ||
606 | private function processDeferredResultVariables(): void | ||
607 | { | ||
608 | foreach ($this->deferredResultVariables as $deferredItem) { | ||
609 | $resultVariable = $deferredItem['expression']; | ||
610 | |||
611 | // Check if ResultVariable exists in queryComponents | ||
612 | if (! isset($this->queryComponents[$resultVariable])) { | ||
613 | $this->semanticalError( | ||
614 | sprintf("'%s' is not defined.", $resultVariable), | ||
615 | $deferredItem['token'], | ||
616 | ); | ||
617 | } | ||
618 | |||
619 | $qComp = $this->queryComponents[$resultVariable]; | ||
620 | |||
621 | // Check if queryComponent points to an AbstractSchemaName or a ResultVariable | ||
622 | if (! isset($qComp['resultVariable'])) { | ||
623 | $this->semanticalError( | ||
624 | sprintf("'%s' does not point to a ResultVariable.", $resultVariable), | ||
625 | $deferredItem['token'], | ||
626 | ); | ||
627 | } | ||
628 | |||
629 | // Validate if identification variable nesting level is lower or equal than the current one | ||
630 | if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { | ||
631 | $this->semanticalError( | ||
632 | sprintf("'%s' is used outside the scope of its declaration.", $resultVariable), | ||
633 | $deferredItem['token'], | ||
634 | ); | ||
635 | } | ||
636 | } | ||
637 | } | ||
638 | |||
639 | /** | ||
640 | * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules: | ||
641 | * | ||
642 | * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression | ||
643 | * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression | ||
644 | * StateFieldPathExpression ::= IdentificationVariable "." StateField | ||
645 | * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField | ||
646 | * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField | ||
647 | */ | ||
648 | private function processDeferredPathExpressions(): void | ||
649 | { | ||
650 | foreach ($this->deferredPathExpressions as $deferredItem) { | ||
651 | $pathExpression = $deferredItem['expression']; | ||
652 | |||
653 | $class = $this->getMetadataForDqlAlias($pathExpression->identificationVariable); | ||
654 | |||
655 | $field = $pathExpression->field; | ||
656 | if ($field === null) { | ||
657 | $field = $pathExpression->field = $class->identifier[0]; | ||
658 | } | ||
659 | |||
660 | // Check if field or association exists | ||
661 | if (! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) { | ||
662 | $this->semanticalError( | ||
663 | 'Class ' . $class->name . ' has no field or association named ' . $field, | ||
664 | $deferredItem['token'], | ||
665 | ); | ||
666 | } | ||
667 | |||
668 | $fieldType = AST\PathExpression::TYPE_STATE_FIELD; | ||
669 | |||
670 | if (isset($class->associationMappings[$field])) { | ||
671 | $assoc = $class->associationMappings[$field]; | ||
672 | |||
673 | $fieldType = $assoc->isToOne() | ||
674 | ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION | ||
675 | : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION; | ||
676 | } | ||
677 | |||
678 | // Validate if PathExpression is one of the expected types | ||
679 | $expectedType = $pathExpression->expectedType; | ||
680 | |||
681 | if (! ($expectedType & $fieldType)) { | ||
682 | // We need to recognize which was expected type(s) | ||
683 | $expectedStringTypes = []; | ||
684 | |||
685 | // Validate state field type | ||
686 | if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) { | ||
687 | $expectedStringTypes[] = 'StateFieldPathExpression'; | ||
688 | } | ||
689 | |||
690 | // Validate single valued association (*-to-one) | ||
691 | if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { | ||
692 | $expectedStringTypes[] = 'SingleValuedAssociationField'; | ||
693 | } | ||
694 | |||
695 | // Validate single valued association (*-to-many) | ||
696 | if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) { | ||
697 | $expectedStringTypes[] = 'CollectionValuedAssociationField'; | ||
698 | } | ||
699 | |||
700 | // Build the error message | ||
701 | $semanticalError = 'Invalid PathExpression. '; | ||
702 | $semanticalError .= count($expectedStringTypes) === 1 | ||
703 | ? 'Must be a ' . $expectedStringTypes[0] . '.' | ||
704 | : implode(' or ', $expectedStringTypes) . ' expected.'; | ||
705 | |||
706 | $this->semanticalError($semanticalError, $deferredItem['token']); | ||
707 | } | ||
708 | |||
709 | // We need to force the type in PathExpression | ||
710 | $pathExpression->type = $fieldType; | ||
711 | } | ||
712 | } | ||
713 | |||
714 | private function processRootEntityAliasSelected(): void | ||
715 | { | ||
716 | if (! count($this->identVariableExpressions)) { | ||
717 | return; | ||
718 | } | ||
719 | |||
720 | foreach ($this->identVariableExpressions as $dqlAlias => $expr) { | ||
721 | if (isset($this->queryComponents[$dqlAlias]) && ! isset($this->queryComponents[$dqlAlias]['parent'])) { | ||
722 | return; | ||
723 | } | ||
724 | } | ||
725 | |||
726 | $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.'); | ||
727 | } | ||
728 | |||
729 | /** | ||
730 | * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement | ||
731 | */ | ||
732 | public function QueryLanguage(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement | ||
733 | { | ||
734 | $statement = null; | ||
735 | |||
736 | $this->lexer->moveNext(); | ||
737 | |||
738 | switch ($this->lexer->lookahead->type ?? null) { | ||
739 | case TokenType::T_SELECT: | ||
740 | $statement = $this->SelectStatement(); | ||
741 | break; | ||
742 | |||
743 | case TokenType::T_UPDATE: | ||
744 | $statement = $this->UpdateStatement(); | ||
745 | break; | ||
746 | |||
747 | case TokenType::T_DELETE: | ||
748 | $statement = $this->DeleteStatement(); | ||
749 | break; | ||
750 | |||
751 | default: | ||
752 | $this->syntaxError('SELECT, UPDATE or DELETE'); | ||
753 | break; | ||
754 | } | ||
755 | |||
756 | // Check for end of string | ||
757 | if ($this->lexer->lookahead !== null) { | ||
758 | $this->syntaxError('end of string'); | ||
759 | } | ||
760 | |||
761 | return $statement; | ||
762 | } | ||
763 | |||
764 | /** | ||
765 | * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] | ||
766 | */ | ||
767 | public function SelectStatement(): AST\SelectStatement | ||
768 | { | ||
769 | $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause()); | ||
770 | |||
771 | $selectStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
772 | $selectStatement->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null; | ||
773 | $selectStatement->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null; | ||
774 | $selectStatement->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null; | ||
775 | |||
776 | return $selectStatement; | ||
777 | } | ||
778 | |||
779 | /** | ||
780 | * UpdateStatement ::= UpdateClause [WhereClause] | ||
781 | */ | ||
782 | public function UpdateStatement(): AST\UpdateStatement | ||
783 | { | ||
784 | $updateStatement = new AST\UpdateStatement($this->UpdateClause()); | ||
785 | |||
786 | $updateStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
787 | |||
788 | return $updateStatement; | ||
789 | } | ||
790 | |||
791 | /** | ||
792 | * DeleteStatement ::= DeleteClause [WhereClause] | ||
793 | */ | ||
794 | public function DeleteStatement(): AST\DeleteStatement | ||
795 | { | ||
796 | $deleteStatement = new AST\DeleteStatement($this->DeleteClause()); | ||
797 | |||
798 | $deleteStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
799 | |||
800 | return $deleteStatement; | ||
801 | } | ||
802 | |||
803 | /** | ||
804 | * IdentificationVariable ::= identifier | ||
805 | */ | ||
806 | public function IdentificationVariable(): string | ||
807 | { | ||
808 | $this->match(TokenType::T_IDENTIFIER); | ||
809 | |||
810 | assert($this->lexer->token !== null); | ||
811 | $identVariable = $this->lexer->token->value; | ||
812 | |||
813 | $this->deferredIdentificationVariables[] = [ | ||
814 | 'expression' => $identVariable, | ||
815 | 'nestingLevel' => $this->nestingLevel, | ||
816 | 'token' => $this->lexer->token, | ||
817 | ]; | ||
818 | |||
819 | return $identVariable; | ||
820 | } | ||
821 | |||
822 | /** | ||
823 | * AliasIdentificationVariable = identifier | ||
824 | */ | ||
825 | public function AliasIdentificationVariable(): string | ||
826 | { | ||
827 | $this->match(TokenType::T_IDENTIFIER); | ||
828 | |||
829 | assert($this->lexer->token !== null); | ||
830 | $aliasIdentVariable = $this->lexer->token->value; | ||
831 | $exists = isset($this->queryComponents[$aliasIdentVariable]); | ||
832 | |||
833 | if ($exists) { | ||
834 | $this->semanticalError( | ||
835 | sprintf("'%s' is already defined.", $aliasIdentVariable), | ||
836 | $this->lexer->token, | ||
837 | ); | ||
838 | } | ||
839 | |||
840 | return $aliasIdentVariable; | ||
841 | } | ||
842 | |||
843 | /** | ||
844 | * AbstractSchemaName ::= fully_qualified_name | identifier | ||
845 | */ | ||
846 | public function AbstractSchemaName(): string | ||
847 | { | ||
848 | if ($this->lexer->isNextToken(TokenType::T_FULLY_QUALIFIED_NAME)) { | ||
849 | $this->match(TokenType::T_FULLY_QUALIFIED_NAME); | ||
850 | assert($this->lexer->token !== null); | ||
851 | |||
852 | return $this->lexer->token->value; | ||
853 | } | ||
854 | |||
855 | $this->match(TokenType::T_IDENTIFIER); | ||
856 | assert($this->lexer->token !== null); | ||
857 | |||
858 | return $this->lexer->token->value; | ||
859 | } | ||
860 | |||
861 | /** | ||
862 | * Validates an AbstractSchemaName, making sure the class exists. | ||
863 | * | ||
864 | * @param string $schemaName The name to validate. | ||
865 | * | ||
866 | * @throws QueryException if the name does not exist. | ||
867 | */ | ||
868 | private function validateAbstractSchemaName(string $schemaName): void | ||
869 | { | ||
870 | assert($this->lexer->token !== null); | ||
871 | if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) { | ||
872 | $this->semanticalError( | ||
873 | sprintf("Class '%s' is not defined.", $schemaName), | ||
874 | $this->lexer->token, | ||
875 | ); | ||
876 | } | ||
877 | } | ||
878 | |||
879 | /** | ||
880 | * AliasResultVariable ::= identifier | ||
881 | */ | ||
882 | public function AliasResultVariable(): string | ||
883 | { | ||
884 | $this->match(TokenType::T_IDENTIFIER); | ||
885 | |||
886 | assert($this->lexer->token !== null); | ||
887 | $resultVariable = $this->lexer->token->value; | ||
888 | $exists = isset($this->queryComponents[$resultVariable]); | ||
889 | |||
890 | if ($exists) { | ||
891 | $this->semanticalError( | ||
892 | sprintf("'%s' is already defined.", $resultVariable), | ||
893 | $this->lexer->token, | ||
894 | ); | ||
895 | } | ||
896 | |||
897 | return $resultVariable; | ||
898 | } | ||
899 | |||
900 | /** | ||
901 | * ResultVariable ::= identifier | ||
902 | */ | ||
903 | public function ResultVariable(): string | ||
904 | { | ||
905 | $this->match(TokenType::T_IDENTIFIER); | ||
906 | |||
907 | assert($this->lexer->token !== null); | ||
908 | $resultVariable = $this->lexer->token->value; | ||
909 | |||
910 | // Defer ResultVariable validation | ||
911 | $this->deferredResultVariables[] = [ | ||
912 | 'expression' => $resultVariable, | ||
913 | 'nestingLevel' => $this->nestingLevel, | ||
914 | 'token' => $this->lexer->token, | ||
915 | ]; | ||
916 | |||
917 | return $resultVariable; | ||
918 | } | ||
919 | |||
920 | /** | ||
921 | * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) | ||
922 | */ | ||
923 | public function JoinAssociationPathExpression(): AST\JoinAssociationPathExpression | ||
924 | { | ||
925 | $identVariable = $this->IdentificationVariable(); | ||
926 | |||
927 | if (! isset($this->queryComponents[$identVariable])) { | ||
928 | $this->semanticalError( | ||
929 | 'Identification Variable ' . $identVariable . ' used in join path expression but was not defined before.', | ||
930 | ); | ||
931 | } | ||
932 | |||
933 | $this->match(TokenType::T_DOT); | ||
934 | $this->match(TokenType::T_IDENTIFIER); | ||
935 | |||
936 | assert($this->lexer->token !== null); | ||
937 | $field = $this->lexer->token->value; | ||
938 | |||
939 | // Validate association field | ||
940 | $class = $this->getMetadataForDqlAlias($identVariable); | ||
941 | |||
942 | if (! $class->hasAssociation($field)) { | ||
943 | $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); | ||
944 | } | ||
945 | |||
946 | return new AST\JoinAssociationPathExpression($identVariable, $field); | ||
947 | } | ||
948 | |||
949 | /** | ||
950 | * Parses an arbitrary path expression and defers semantical validation | ||
951 | * based on expected types. | ||
952 | * | ||
953 | * PathExpression ::= IdentificationVariable {"." identifier}* | ||
954 | * | ||
955 | * @psalm-param int-mask-of<AST\PathExpression::TYPE_*> $expectedTypes | ||
956 | */ | ||
957 | public function PathExpression(int $expectedTypes): AST\PathExpression | ||
958 | { | ||
959 | $identVariable = $this->IdentificationVariable(); | ||
960 | $field = null; | ||
961 | |||
962 | assert($this->lexer->token !== null); | ||
963 | if ($this->lexer->isNextToken(TokenType::T_DOT)) { | ||
964 | $this->match(TokenType::T_DOT); | ||
965 | $this->match(TokenType::T_IDENTIFIER); | ||
966 | |||
967 | $field = $this->lexer->token->value; | ||
968 | |||
969 | while ($this->lexer->isNextToken(TokenType::T_DOT)) { | ||
970 | $this->match(TokenType::T_DOT); | ||
971 | $this->match(TokenType::T_IDENTIFIER); | ||
972 | $field .= '.' . $this->lexer->token->value; | ||
973 | } | ||
974 | } | ||
975 | |||
976 | // Creating AST node | ||
977 | $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field); | ||
978 | |||
979 | // Defer PathExpression validation if requested to be deferred | ||
980 | $this->deferredPathExpressions[] = [ | ||
981 | 'expression' => $pathExpr, | ||
982 | 'nestingLevel' => $this->nestingLevel, | ||
983 | 'token' => $this->lexer->token, | ||
984 | ]; | ||
985 | |||
986 | return $pathExpr; | ||
987 | } | ||
988 | |||
989 | /** | ||
990 | * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression | ||
991 | */ | ||
992 | public function AssociationPathExpression(): AST\PathExpression | ||
993 | { | ||
994 | return $this->PathExpression( | ||
995 | AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION | | ||
996 | AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION, | ||
997 | ); | ||
998 | } | ||
999 | |||
1000 | /** | ||
1001 | * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression | ||
1002 | */ | ||
1003 | public function SingleValuedPathExpression(): AST\PathExpression | ||
1004 | { | ||
1005 | return $this->PathExpression( | ||
1006 | AST\PathExpression::TYPE_STATE_FIELD | | ||
1007 | AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, | ||
1008 | ); | ||
1009 | } | ||
1010 | |||
1011 | /** | ||
1012 | * StateFieldPathExpression ::= IdentificationVariable "." StateField | ||
1013 | */ | ||
1014 | public function StateFieldPathExpression(): AST\PathExpression | ||
1015 | { | ||
1016 | return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); | ||
1017 | } | ||
1018 | |||
1019 | /** | ||
1020 | * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField | ||
1021 | */ | ||
1022 | public function SingleValuedAssociationPathExpression(): AST\PathExpression | ||
1023 | { | ||
1024 | return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); | ||
1025 | } | ||
1026 | |||
1027 | /** | ||
1028 | * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField | ||
1029 | */ | ||
1030 | public function CollectionValuedPathExpression(): AST\PathExpression | ||
1031 | { | ||
1032 | return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); | ||
1033 | } | ||
1034 | |||
1035 | /** | ||
1036 | * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} | ||
1037 | */ | ||
1038 | public function SelectClause(): AST\SelectClause | ||
1039 | { | ||
1040 | $isDistinct = false; | ||
1041 | $this->match(TokenType::T_SELECT); | ||
1042 | |||
1043 | // Check for DISTINCT | ||
1044 | if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { | ||
1045 | $this->match(TokenType::T_DISTINCT); | ||
1046 | |||
1047 | $isDistinct = true; | ||
1048 | } | ||
1049 | |||
1050 | // Process SelectExpressions (1..N) | ||
1051 | $selectExpressions = []; | ||
1052 | $selectExpressions[] = $this->SelectExpression(); | ||
1053 | |||
1054 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1055 | $this->match(TokenType::T_COMMA); | ||
1056 | |||
1057 | $selectExpressions[] = $this->SelectExpression(); | ||
1058 | } | ||
1059 | |||
1060 | return new AST\SelectClause($selectExpressions, $isDistinct); | ||
1061 | } | ||
1062 | |||
1063 | /** | ||
1064 | * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression | ||
1065 | */ | ||
1066 | public function SimpleSelectClause(): AST\SimpleSelectClause | ||
1067 | { | ||
1068 | $isDistinct = false; | ||
1069 | $this->match(TokenType::T_SELECT); | ||
1070 | |||
1071 | if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { | ||
1072 | $this->match(TokenType::T_DISTINCT); | ||
1073 | |||
1074 | $isDistinct = true; | ||
1075 | } | ||
1076 | |||
1077 | return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct); | ||
1078 | } | ||
1079 | |||
1080 | /** | ||
1081 | * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}* | ||
1082 | */ | ||
1083 | public function UpdateClause(): AST\UpdateClause | ||
1084 | { | ||
1085 | $this->match(TokenType::T_UPDATE); | ||
1086 | assert($this->lexer->lookahead !== null); | ||
1087 | |||
1088 | $token = $this->lexer->lookahead; | ||
1089 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
1090 | |||
1091 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
1092 | |||
1093 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1094 | $this->match(TokenType::T_AS); | ||
1095 | } | ||
1096 | |||
1097 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
1098 | |||
1099 | $class = $this->em->getClassMetadata($abstractSchemaName); | ||
1100 | |||
1101 | // Building queryComponent | ||
1102 | $queryComponent = [ | ||
1103 | 'metadata' => $class, | ||
1104 | 'parent' => null, | ||
1105 | 'relation' => null, | ||
1106 | 'map' => null, | ||
1107 | 'nestingLevel' => $this->nestingLevel, | ||
1108 | 'token' => $token, | ||
1109 | ]; | ||
1110 | |||
1111 | $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; | ||
1112 | |||
1113 | $this->match(TokenType::T_SET); | ||
1114 | |||
1115 | $updateItems = []; | ||
1116 | $updateItems[] = $this->UpdateItem(); | ||
1117 | |||
1118 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1119 | $this->match(TokenType::T_COMMA); | ||
1120 | |||
1121 | $updateItems[] = $this->UpdateItem(); | ||
1122 | } | ||
1123 | |||
1124 | $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems); | ||
1125 | $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable; | ||
1126 | |||
1127 | return $updateClause; | ||
1128 | } | ||
1129 | |||
1130 | /** | ||
1131 | * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable | ||
1132 | */ | ||
1133 | public function DeleteClause(): AST\DeleteClause | ||
1134 | { | ||
1135 | $this->match(TokenType::T_DELETE); | ||
1136 | |||
1137 | if ($this->lexer->isNextToken(TokenType::T_FROM)) { | ||
1138 | $this->match(TokenType::T_FROM); | ||
1139 | } | ||
1140 | |||
1141 | assert($this->lexer->lookahead !== null); | ||
1142 | $token = $this->lexer->lookahead; | ||
1143 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
1144 | |||
1145 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
1146 | |||
1147 | $deleteClause = new AST\DeleteClause($abstractSchemaName); | ||
1148 | |||
1149 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1150 | $this->match(TokenType::T_AS); | ||
1151 | } | ||
1152 | |||
1153 | $aliasIdentificationVariable = $this->lexer->isNextToken(TokenType::T_IDENTIFIER) | ||
1154 | ? $this->AliasIdentificationVariable() | ||
1155 | : 'alias_should_have_been_set'; | ||
1156 | |||
1157 | $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable; | ||
1158 | $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName); | ||
1159 | |||
1160 | // Building queryComponent | ||
1161 | $queryComponent = [ | ||
1162 | 'metadata' => $class, | ||
1163 | 'parent' => null, | ||
1164 | 'relation' => null, | ||
1165 | 'map' => null, | ||
1166 | 'nestingLevel' => $this->nestingLevel, | ||
1167 | 'token' => $token, | ||
1168 | ]; | ||
1169 | |||
1170 | $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; | ||
1171 | |||
1172 | return $deleteClause; | ||
1173 | } | ||
1174 | |||
1175 | /** | ||
1176 | * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}* | ||
1177 | */ | ||
1178 | public function FromClause(): AST\FromClause | ||
1179 | { | ||
1180 | $this->match(TokenType::T_FROM); | ||
1181 | |||
1182 | $identificationVariableDeclarations = []; | ||
1183 | $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); | ||
1184 | |||
1185 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1186 | $this->match(TokenType::T_COMMA); | ||
1187 | |||
1188 | $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); | ||
1189 | } | ||
1190 | |||
1191 | return new AST\FromClause($identificationVariableDeclarations); | ||
1192 | } | ||
1193 | |||
1194 | /** | ||
1195 | * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}* | ||
1196 | */ | ||
1197 | public function SubselectFromClause(): AST\SubselectFromClause | ||
1198 | { | ||
1199 | $this->match(TokenType::T_FROM); | ||
1200 | |||
1201 | $identificationVariables = []; | ||
1202 | $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); | ||
1203 | |||
1204 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1205 | $this->match(TokenType::T_COMMA); | ||
1206 | |||
1207 | $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); | ||
1208 | } | ||
1209 | |||
1210 | return new AST\SubselectFromClause($identificationVariables); | ||
1211 | } | ||
1212 | |||
1213 | /** | ||
1214 | * WhereClause ::= "WHERE" ConditionalExpression | ||
1215 | */ | ||
1216 | public function WhereClause(): AST\WhereClause | ||
1217 | { | ||
1218 | $this->match(TokenType::T_WHERE); | ||
1219 | |||
1220 | return new AST\WhereClause($this->ConditionalExpression()); | ||
1221 | } | ||
1222 | |||
1223 | /** | ||
1224 | * HavingClause ::= "HAVING" ConditionalExpression | ||
1225 | */ | ||
1226 | public function HavingClause(): AST\HavingClause | ||
1227 | { | ||
1228 | $this->match(TokenType::T_HAVING); | ||
1229 | |||
1230 | return new AST\HavingClause($this->ConditionalExpression()); | ||
1231 | } | ||
1232 | |||
1233 | /** | ||
1234 | * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}* | ||
1235 | */ | ||
1236 | public function GroupByClause(): AST\GroupByClause | ||
1237 | { | ||
1238 | $this->match(TokenType::T_GROUP); | ||
1239 | $this->match(TokenType::T_BY); | ||
1240 | |||
1241 | $groupByItems = [$this->GroupByItem()]; | ||
1242 | |||
1243 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1244 | $this->match(TokenType::T_COMMA); | ||
1245 | |||
1246 | $groupByItems[] = $this->GroupByItem(); | ||
1247 | } | ||
1248 | |||
1249 | return new AST\GroupByClause($groupByItems); | ||
1250 | } | ||
1251 | |||
1252 | /** | ||
1253 | * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* | ||
1254 | */ | ||
1255 | public function OrderByClause(): AST\OrderByClause | ||
1256 | { | ||
1257 | $this->match(TokenType::T_ORDER); | ||
1258 | $this->match(TokenType::T_BY); | ||
1259 | |||
1260 | $orderByItems = []; | ||
1261 | $orderByItems[] = $this->OrderByItem(); | ||
1262 | |||
1263 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1264 | $this->match(TokenType::T_COMMA); | ||
1265 | |||
1266 | $orderByItems[] = $this->OrderByItem(); | ||
1267 | } | ||
1268 | |||
1269 | return new AST\OrderByClause($orderByItems); | ||
1270 | } | ||
1271 | |||
1272 | /** | ||
1273 | * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] | ||
1274 | */ | ||
1275 | public function Subselect(): AST\Subselect | ||
1276 | { | ||
1277 | // Increase query nesting level | ||
1278 | $this->nestingLevel++; | ||
1279 | |||
1280 | $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause()); | ||
1281 | |||
1282 | $subselect->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
1283 | $subselect->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null; | ||
1284 | $subselect->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null; | ||
1285 | $subselect->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null; | ||
1286 | |||
1287 | // Decrease query nesting level | ||
1288 | $this->nestingLevel--; | ||
1289 | |||
1290 | return $subselect; | ||
1291 | } | ||
1292 | |||
1293 | /** | ||
1294 | * UpdateItem ::= SingleValuedPathExpression "=" NewValue | ||
1295 | */ | ||
1296 | public function UpdateItem(): AST\UpdateItem | ||
1297 | { | ||
1298 | $pathExpr = $this->SingleValuedPathExpression(); | ||
1299 | |||
1300 | $this->match(TokenType::T_EQUALS); | ||
1301 | |||
1302 | return new AST\UpdateItem($pathExpr, $this->NewValue()); | ||
1303 | } | ||
1304 | |||
1305 | /** | ||
1306 | * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression | ||
1307 | */ | ||
1308 | public function GroupByItem(): string|AST\PathExpression | ||
1309 | { | ||
1310 | // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression | ||
1311 | $glimpse = $this->lexer->glimpse(); | ||
1312 | |||
1313 | if ($glimpse !== null && $glimpse->type === TokenType::T_DOT) { | ||
1314 | return $this->SingleValuedPathExpression(); | ||
1315 | } | ||
1316 | |||
1317 | assert($this->lexer->lookahead !== null); | ||
1318 | // Still need to decide between IdentificationVariable or ResultVariable | ||
1319 | $lookaheadValue = $this->lexer->lookahead->value; | ||
1320 | |||
1321 | if (! isset($this->queryComponents[$lookaheadValue])) { | ||
1322 | $this->semanticalError('Cannot group by undefined identification or result variable.'); | ||
1323 | } | ||
1324 | |||
1325 | return isset($this->queryComponents[$lookaheadValue]['metadata']) | ||
1326 | ? $this->IdentificationVariable() | ||
1327 | : $this->ResultVariable(); | ||
1328 | } | ||
1329 | |||
1330 | /** | ||
1331 | * OrderByItem ::= ( | ||
1332 | * SimpleArithmeticExpression | SingleValuedPathExpression | CaseExpression | | ||
1333 | * ScalarExpression | ResultVariable | FunctionDeclaration | ||
1334 | * ) ["ASC" | "DESC"] | ||
1335 | */ | ||
1336 | public function OrderByItem(): AST\OrderByItem | ||
1337 | { | ||
1338 | $this->lexer->peek(); // lookahead => '.' | ||
1339 | $this->lexer->peek(); // lookahead => token after '.' | ||
1340 | |||
1341 | $peek = $this->lexer->peek(); // lookahead => token after the token after the '.' | ||
1342 | |||
1343 | $this->lexer->resetPeek(); | ||
1344 | |||
1345 | $glimpse = $this->lexer->glimpse(); | ||
1346 | |||
1347 | assert($this->lexer->lookahead !== null); | ||
1348 | $expr = match (true) { | ||
1349 | $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(), | ||
1350 | $glimpse !== null && $glimpse->type === TokenType::T_DOT => $this->SingleValuedPathExpression(), | ||
1351 | $this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(), | ||
1352 | $this->lexer->lookahead->type === TokenType::T_CASE => $this->CaseExpression(), | ||
1353 | $this->isFunction() => $this->FunctionDeclaration(), | ||
1354 | default => $this->ResultVariable(), | ||
1355 | }; | ||
1356 | |||
1357 | $type = 'ASC'; | ||
1358 | $item = new AST\OrderByItem($expr); | ||
1359 | |||
1360 | switch (true) { | ||
1361 | case $this->lexer->isNextToken(TokenType::T_DESC): | ||
1362 | $this->match(TokenType::T_DESC); | ||
1363 | $type = 'DESC'; | ||
1364 | break; | ||
1365 | |||
1366 | case $this->lexer->isNextToken(TokenType::T_ASC): | ||
1367 | $this->match(TokenType::T_ASC); | ||
1368 | break; | ||
1369 | |||
1370 | default: | ||
1371 | // Do nothing | ||
1372 | } | ||
1373 | |||
1374 | $item->type = $type; | ||
1375 | |||
1376 | return $item; | ||
1377 | } | ||
1378 | |||
1379 | /** | ||
1380 | * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary | | ||
1381 | * EnumPrimary | SimpleEntityExpression | "NULL" | ||
1382 | * | ||
1383 | * NOTE: Since it is not possible to correctly recognize individual types, here is the full | ||
1384 | * grammar that needs to be supported: | ||
1385 | * | ||
1386 | * NewValue ::= SimpleArithmeticExpression | "NULL" | ||
1387 | * | ||
1388 | * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression | ||
1389 | */ | ||
1390 | public function NewValue(): AST\ArithmeticExpression|AST\InputParameter|null | ||
1391 | { | ||
1392 | if ($this->lexer->isNextToken(TokenType::T_NULL)) { | ||
1393 | $this->match(TokenType::T_NULL); | ||
1394 | |||
1395 | return null; | ||
1396 | } | ||
1397 | |||
1398 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
1399 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
1400 | assert($this->lexer->token !== null); | ||
1401 | |||
1402 | return new AST\InputParameter($this->lexer->token->value); | ||
1403 | } | ||
1404 | |||
1405 | return $this->ArithmeticExpression(); | ||
1406 | } | ||
1407 | |||
1408 | /** | ||
1409 | * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}* | ||
1410 | */ | ||
1411 | public function IdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration | ||
1412 | { | ||
1413 | $joins = []; | ||
1414 | $rangeVariableDeclaration = $this->RangeVariableDeclaration(); | ||
1415 | $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) | ||
1416 | ? $this->IndexBy() | ||
1417 | : null; | ||
1418 | |||
1419 | $rangeVariableDeclaration->isRoot = true; | ||
1420 | |||
1421 | while ( | ||
1422 | $this->lexer->isNextToken(TokenType::T_LEFT) || | ||
1423 | $this->lexer->isNextToken(TokenType::T_INNER) || | ||
1424 | $this->lexer->isNextToken(TokenType::T_JOIN) | ||
1425 | ) { | ||
1426 | $joins[] = $this->Join(); | ||
1427 | } | ||
1428 | |||
1429 | return new AST\IdentificationVariableDeclaration( | ||
1430 | $rangeVariableDeclaration, | ||
1431 | $indexBy, | ||
1432 | $joins, | ||
1433 | ); | ||
1434 | } | ||
1435 | |||
1436 | /** | ||
1437 | * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | ||
1438 | * | ||
1439 | * {Internal note: WARNING: Solution is harder than a bare implementation. | ||
1440 | * Desired EBNF support: | ||
1441 | * | ||
1442 | * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable) | ||
1443 | * | ||
1444 | * It demands that entire SQL generation to become programmatical. This is | ||
1445 | * needed because association based subselect requires "WHERE" conditional | ||
1446 | * expressions to be injected, but there is no scope to do that. Only scope | ||
1447 | * accessible is "FROM", prohibiting an easy implementation without larger | ||
1448 | * changes.} | ||
1449 | */ | ||
1450 | public function SubselectIdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration | ||
1451 | { | ||
1452 | /* | ||
1453 | NOT YET IMPLEMENTED! | ||
1454 | |||
1455 | $glimpse = $this->lexer->glimpse(); | ||
1456 | |||
1457 | if ($glimpse->type == TokenType::T_DOT) { | ||
1458 | $associationPathExpression = $this->AssociationPathExpression(); | ||
1459 | |||
1460 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1461 | $this->match(TokenType::T_AS); | ||
1462 | } | ||
1463 | |||
1464 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
1465 | $identificationVariable = $associationPathExpression->identificationVariable; | ||
1466 | $field = $associationPathExpression->associationField; | ||
1467 | |||
1468 | $class = $this->queryComponents[$identificationVariable]['metadata']; | ||
1469 | $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']); | ||
1470 | |||
1471 | // Building queryComponent | ||
1472 | $joinQueryComponent = array( | ||
1473 | 'metadata' => $targetClass, | ||
1474 | 'parent' => $identificationVariable, | ||
1475 | 'relation' => $class->getAssociationMapping($field), | ||
1476 | 'map' => null, | ||
1477 | 'nestingLevel' => $this->nestingLevel, | ||
1478 | 'token' => $this->lexer->lookahead | ||
1479 | ); | ||
1480 | |||
1481 | $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; | ||
1482 | |||
1483 | return new AST\SubselectIdentificationVariableDeclaration( | ||
1484 | $associationPathExpression, $aliasIdentificationVariable | ||
1485 | ); | ||
1486 | } | ||
1487 | */ | ||
1488 | |||
1489 | return $this->IdentificationVariableDeclaration(); | ||
1490 | } | ||
1491 | |||
1492 | /** | ||
1493 | * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" | ||
1494 | * (JoinAssociationDeclaration | RangeVariableDeclaration) | ||
1495 | * ["WITH" ConditionalExpression] | ||
1496 | */ | ||
1497 | public function Join(): AST\Join | ||
1498 | { | ||
1499 | // Check Join type | ||
1500 | $joinType = AST\Join::JOIN_TYPE_INNER; | ||
1501 | |||
1502 | switch (true) { | ||
1503 | case $this->lexer->isNextToken(TokenType::T_LEFT): | ||
1504 | $this->match(TokenType::T_LEFT); | ||
1505 | |||
1506 | $joinType = AST\Join::JOIN_TYPE_LEFT; | ||
1507 | |||
1508 | // Possible LEFT OUTER join | ||
1509 | if ($this->lexer->isNextToken(TokenType::T_OUTER)) { | ||
1510 | $this->match(TokenType::T_OUTER); | ||
1511 | |||
1512 | $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; | ||
1513 | } | ||
1514 | |||
1515 | break; | ||
1516 | |||
1517 | case $this->lexer->isNextToken(TokenType::T_INNER): | ||
1518 | $this->match(TokenType::T_INNER); | ||
1519 | break; | ||
1520 | |||
1521 | default: | ||
1522 | // Do nothing | ||
1523 | } | ||
1524 | |||
1525 | $this->match(TokenType::T_JOIN); | ||
1526 | |||
1527 | $next = $this->lexer->glimpse(); | ||
1528 | assert($next !== null); | ||
1529 | $joinDeclaration = $next->type === TokenType::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration(); | ||
1530 | $adhocConditions = $this->lexer->isNextToken(TokenType::T_WITH); | ||
1531 | $join = new AST\Join($joinType, $joinDeclaration); | ||
1532 | |||
1533 | // Describe non-root join declaration | ||
1534 | if ($joinDeclaration instanceof AST\RangeVariableDeclaration) { | ||
1535 | $joinDeclaration->isRoot = false; | ||
1536 | } | ||
1537 | |||
1538 | // Check for ad-hoc Join conditions | ||
1539 | if ($adhocConditions) { | ||
1540 | $this->match(TokenType::T_WITH); | ||
1541 | |||
1542 | $join->conditionalExpression = $this->ConditionalExpression(); | ||
1543 | } | ||
1544 | |||
1545 | return $join; | ||
1546 | } | ||
1547 | |||
1548 | /** | ||
1549 | * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable | ||
1550 | * | ||
1551 | * @throws QueryException | ||
1552 | */ | ||
1553 | public function RangeVariableDeclaration(): AST\RangeVariableDeclaration | ||
1554 | { | ||
1555 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()->type === TokenType::T_SELECT) { | ||
1556 | $this->semanticalError('Subquery is not supported here', $this->lexer->token); | ||
1557 | } | ||
1558 | |||
1559 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
1560 | |||
1561 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
1562 | |||
1563 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1564 | $this->match(TokenType::T_AS); | ||
1565 | } | ||
1566 | |||
1567 | assert($this->lexer->lookahead !== null); | ||
1568 | $token = $this->lexer->lookahead; | ||
1569 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
1570 | $classMetadata = $this->em->getClassMetadata($abstractSchemaName); | ||
1571 | |||
1572 | // Building queryComponent | ||
1573 | $queryComponent = [ | ||
1574 | 'metadata' => $classMetadata, | ||
1575 | 'parent' => null, | ||
1576 | 'relation' => null, | ||
1577 | 'map' => null, | ||
1578 | 'nestingLevel' => $this->nestingLevel, | ||
1579 | 'token' => $token, | ||
1580 | ]; | ||
1581 | |||
1582 | $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; | ||
1583 | |||
1584 | return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable); | ||
1585 | } | ||
1586 | |||
1587 | /** | ||
1588 | * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy] | ||
1589 | */ | ||
1590 | public function JoinAssociationDeclaration(): AST\JoinAssociationDeclaration | ||
1591 | { | ||
1592 | $joinAssociationPathExpression = $this->JoinAssociationPathExpression(); | ||
1593 | |||
1594 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1595 | $this->match(TokenType::T_AS); | ||
1596 | } | ||
1597 | |||
1598 | assert($this->lexer->lookahead !== null); | ||
1599 | |||
1600 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
1601 | $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) ? $this->IndexBy() : null; | ||
1602 | |||
1603 | $identificationVariable = $joinAssociationPathExpression->identificationVariable; | ||
1604 | $field = $joinAssociationPathExpression->associationField; | ||
1605 | |||
1606 | $class = $this->getMetadataForDqlAlias($identificationVariable); | ||
1607 | $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]->targetEntity); | ||
1608 | |||
1609 | // Building queryComponent | ||
1610 | $joinQueryComponent = [ | ||
1611 | 'metadata' => $targetClass, | ||
1612 | 'parent' => $joinAssociationPathExpression->identificationVariable, | ||
1613 | 'relation' => $class->getAssociationMapping($field), | ||
1614 | 'map' => null, | ||
1615 | 'nestingLevel' => $this->nestingLevel, | ||
1616 | 'token' => $this->lexer->lookahead, | ||
1617 | ]; | ||
1618 | |||
1619 | $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; | ||
1620 | |||
1621 | return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy); | ||
1622 | } | ||
1623 | |||
1624 | /** | ||
1625 | * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")" | ||
1626 | */ | ||
1627 | public function NewObjectExpression(): AST\NewObjectExpression | ||
1628 | { | ||
1629 | $args = []; | ||
1630 | $this->match(TokenType::T_NEW); | ||
1631 | |||
1632 | $className = $this->AbstractSchemaName(); // note that this is not yet validated | ||
1633 | $token = $this->lexer->token; | ||
1634 | |||
1635 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
1636 | |||
1637 | $args[] = $this->NewObjectArg(); | ||
1638 | |||
1639 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1640 | $this->match(TokenType::T_COMMA); | ||
1641 | |||
1642 | $args[] = $this->NewObjectArg(); | ||
1643 | } | ||
1644 | |||
1645 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
1646 | |||
1647 | $expression = new AST\NewObjectExpression($className, $args); | ||
1648 | |||
1649 | // Defer NewObjectExpression validation | ||
1650 | $this->deferredNewObjectExpressions[] = [ | ||
1651 | 'token' => $token, | ||
1652 | 'expression' => $expression, | ||
1653 | 'nestingLevel' => $this->nestingLevel, | ||
1654 | ]; | ||
1655 | |||
1656 | return $expression; | ||
1657 | } | ||
1658 | |||
1659 | /** | ||
1660 | * NewObjectArg ::= ScalarExpression | "(" Subselect ")" | ||
1661 | */ | ||
1662 | public function NewObjectArg(): mixed | ||
1663 | { | ||
1664 | assert($this->lexer->lookahead !== null); | ||
1665 | $token = $this->lexer->lookahead; | ||
1666 | $peek = $this->lexer->glimpse(); | ||
1667 | |||
1668 | assert($peek !== null); | ||
1669 | if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) { | ||
1670 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
1671 | $expression = $this->Subselect(); | ||
1672 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
1673 | |||
1674 | return $expression; | ||
1675 | } | ||
1676 | |||
1677 | return $this->ScalarExpression(); | ||
1678 | } | ||
1679 | |||
1680 | /** | ||
1681 | * IndexBy ::= "INDEX" "BY" SingleValuedPathExpression | ||
1682 | */ | ||
1683 | public function IndexBy(): AST\IndexBy | ||
1684 | { | ||
1685 | $this->match(TokenType::T_INDEX); | ||
1686 | $this->match(TokenType::T_BY); | ||
1687 | $pathExpr = $this->SingleValuedPathExpression(); | ||
1688 | |||
1689 | // Add the INDEX BY info to the query component | ||
1690 | $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field; | ||
1691 | |||
1692 | return new AST\IndexBy($pathExpr); | ||
1693 | } | ||
1694 | |||
1695 | /** | ||
1696 | * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary | | ||
1697 | * StateFieldPathExpression | BooleanPrimary | CaseExpression | | ||
1698 | * InstanceOfExpression | ||
1699 | * | ||
1700 | * @return mixed One of the possible expressions or subexpressions. | ||
1701 | */ | ||
1702 | public function ScalarExpression(): mixed | ||
1703 | { | ||
1704 | assert($this->lexer->token !== null); | ||
1705 | assert($this->lexer->lookahead !== null); | ||
1706 | $lookahead = $this->lexer->lookahead->type; | ||
1707 | $peek = $this->lexer->glimpse(); | ||
1708 | |||
1709 | switch (true) { | ||
1710 | case $lookahead === TokenType::T_INTEGER: | ||
1711 | case $lookahead === TokenType::T_FLOAT: | ||
1712 | // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) or ( - 1 ) or ( + 1 ) | ||
1713 | case $lookahead === TokenType::T_MINUS: | ||
1714 | case $lookahead === TokenType::T_PLUS: | ||
1715 | return $this->SimpleArithmeticExpression(); | ||
1716 | |||
1717 | case $lookahead === TokenType::T_STRING: | ||
1718 | return $this->StringPrimary(); | ||
1719 | |||
1720 | case $lookahead === TokenType::T_TRUE: | ||
1721 | case $lookahead === TokenType::T_FALSE: | ||
1722 | $this->match($lookahead); | ||
1723 | |||
1724 | return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value); | ||
1725 | |||
1726 | case $lookahead === TokenType::T_INPUT_PARAMETER: | ||
1727 | return match (true) { | ||
1728 | $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(), | ||
1729 | default => $this->InputParameter(), | ||
1730 | }; | ||
1731 | |||
1732 | case $lookahead === TokenType::T_CASE: | ||
1733 | case $lookahead === TokenType::T_COALESCE: | ||
1734 | case $lookahead === TokenType::T_NULLIF: | ||
1735 | // Since NULLIF and COALESCE can be identified as a function, | ||
1736 | // we need to check these before checking for FunctionDeclaration | ||
1737 | return $this->CaseExpression(); | ||
1738 | |||
1739 | case $lookahead === TokenType::T_OPEN_PARENTHESIS: | ||
1740 | return $this->SimpleArithmeticExpression(); | ||
1741 | |||
1742 | // this check must be done before checking for a filed path expression | ||
1743 | case $this->isFunction(): | ||
1744 | $this->lexer->peek(); | ||
1745 | |||
1746 | return match (true) { | ||
1747 | $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->SimpleArithmeticExpression(), | ||
1748 | default => $this->FunctionDeclaration(), | ||
1749 | }; | ||
1750 | |||
1751 | // it is no function, so it must be a field path | ||
1752 | case $lookahead === TokenType::T_IDENTIFIER: | ||
1753 | $this->lexer->peek(); // lookahead => '.' | ||
1754 | $this->lexer->peek(); // lookahead => token after '.' | ||
1755 | $peek = $this->lexer->peek(); // lookahead => token after the token after the '.' | ||
1756 | $this->lexer->resetPeek(); | ||
1757 | |||
1758 | if ($this->isMathOperator($peek)) { | ||
1759 | return $this->SimpleArithmeticExpression(); | ||
1760 | } | ||
1761 | |||
1762 | return $this->StateFieldPathExpression(); | ||
1763 | |||
1764 | default: | ||
1765 | $this->syntaxError(); | ||
1766 | } | ||
1767 | } | ||
1768 | |||
1769 | /** | ||
1770 | * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression | ||
1771 | * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" | ||
1772 | * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression | ||
1773 | * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" | ||
1774 | * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator | ||
1775 | * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression | ||
1776 | * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" | ||
1777 | * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" | ||
1778 | * | ||
1779 | * @return mixed One of the possible expressions or subexpressions. | ||
1780 | */ | ||
1781 | public function CaseExpression(): mixed | ||
1782 | { | ||
1783 | assert($this->lexer->lookahead !== null); | ||
1784 | $lookahead = $this->lexer->lookahead->type; | ||
1785 | |||
1786 | switch ($lookahead) { | ||
1787 | case TokenType::T_NULLIF: | ||
1788 | return $this->NullIfExpression(); | ||
1789 | |||
1790 | case TokenType::T_COALESCE: | ||
1791 | return $this->CoalesceExpression(); | ||
1792 | |||
1793 | case TokenType::T_CASE: | ||
1794 | $this->lexer->resetPeek(); | ||
1795 | $peek = $this->lexer->peek(); | ||
1796 | |||
1797 | assert($peek !== null); | ||
1798 | if ($peek->type === TokenType::T_WHEN) { | ||
1799 | return $this->GeneralCaseExpression(); | ||
1800 | } | ||
1801 | |||
1802 | return $this->SimpleCaseExpression(); | ||
1803 | |||
1804 | default: | ||
1805 | // Do nothing | ||
1806 | break; | ||
1807 | } | ||
1808 | |||
1809 | $this->syntaxError(); | ||
1810 | } | ||
1811 | |||
1812 | /** | ||
1813 | * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" | ||
1814 | */ | ||
1815 | public function CoalesceExpression(): AST\CoalesceExpression | ||
1816 | { | ||
1817 | $this->match(TokenType::T_COALESCE); | ||
1818 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
1819 | |||
1820 | // Process ScalarExpressions (1..N) | ||
1821 | $scalarExpressions = []; | ||
1822 | $scalarExpressions[] = $this->ScalarExpression(); | ||
1823 | |||
1824 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1825 | $this->match(TokenType::T_COMMA); | ||
1826 | |||
1827 | $scalarExpressions[] = $this->ScalarExpression(); | ||
1828 | } | ||
1829 | |||
1830 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
1831 | |||
1832 | return new AST\CoalesceExpression($scalarExpressions); | ||
1833 | } | ||
1834 | |||
1835 | /** | ||
1836 | * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" | ||
1837 | */ | ||
1838 | public function NullIfExpression(): AST\NullIfExpression | ||
1839 | { | ||
1840 | $this->match(TokenType::T_NULLIF); | ||
1841 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
1842 | |||
1843 | $firstExpression = $this->ScalarExpression(); | ||
1844 | $this->match(TokenType::T_COMMA); | ||
1845 | $secondExpression = $this->ScalarExpression(); | ||
1846 | |||
1847 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
1848 | |||
1849 | return new AST\NullIfExpression($firstExpression, $secondExpression); | ||
1850 | } | ||
1851 | |||
1852 | /** | ||
1853 | * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" | ||
1854 | */ | ||
1855 | public function GeneralCaseExpression(): AST\GeneralCaseExpression | ||
1856 | { | ||
1857 | $this->match(TokenType::T_CASE); | ||
1858 | |||
1859 | // Process WhenClause (1..N) | ||
1860 | $whenClauses = []; | ||
1861 | |||
1862 | do { | ||
1863 | $whenClauses[] = $this->WhenClause(); | ||
1864 | } while ($this->lexer->isNextToken(TokenType::T_WHEN)); | ||
1865 | |||
1866 | $this->match(TokenType::T_ELSE); | ||
1867 | $scalarExpression = $this->ScalarExpression(); | ||
1868 | $this->match(TokenType::T_END); | ||
1869 | |||
1870 | return new AST\GeneralCaseExpression($whenClauses, $scalarExpression); | ||
1871 | } | ||
1872 | |||
1873 | /** | ||
1874 | * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" | ||
1875 | * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator | ||
1876 | */ | ||
1877 | public function SimpleCaseExpression(): AST\SimpleCaseExpression | ||
1878 | { | ||
1879 | $this->match(TokenType::T_CASE); | ||
1880 | $caseOperand = $this->StateFieldPathExpression(); | ||
1881 | |||
1882 | // Process SimpleWhenClause (1..N) | ||
1883 | $simpleWhenClauses = []; | ||
1884 | |||
1885 | do { | ||
1886 | $simpleWhenClauses[] = $this->SimpleWhenClause(); | ||
1887 | } while ($this->lexer->isNextToken(TokenType::T_WHEN)); | ||
1888 | |||
1889 | $this->match(TokenType::T_ELSE); | ||
1890 | $scalarExpression = $this->ScalarExpression(); | ||
1891 | $this->match(TokenType::T_END); | ||
1892 | |||
1893 | return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression); | ||
1894 | } | ||
1895 | |||
1896 | /** | ||
1897 | * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression | ||
1898 | */ | ||
1899 | public function WhenClause(): AST\WhenClause | ||
1900 | { | ||
1901 | $this->match(TokenType::T_WHEN); | ||
1902 | $conditionalExpression = $this->ConditionalExpression(); | ||
1903 | $this->match(TokenType::T_THEN); | ||
1904 | |||
1905 | return new AST\WhenClause($conditionalExpression, $this->ScalarExpression()); | ||
1906 | } | ||
1907 | |||
1908 | /** | ||
1909 | * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression | ||
1910 | */ | ||
1911 | public function SimpleWhenClause(): AST\SimpleWhenClause | ||
1912 | { | ||
1913 | $this->match(TokenType::T_WHEN); | ||
1914 | $conditionalExpression = $this->ScalarExpression(); | ||
1915 | $this->match(TokenType::T_THEN); | ||
1916 | |||
1917 | return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression()); | ||
1918 | } | ||
1919 | |||
1920 | /** | ||
1921 | * SelectExpression ::= ( | ||
1922 | * IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | | ||
1923 | * "(" Subselect ")" | CaseExpression | NewObjectExpression | ||
1924 | * ) [["AS"] ["HIDDEN"] AliasResultVariable] | ||
1925 | */ | ||
1926 | public function SelectExpression(): AST\SelectExpression | ||
1927 | { | ||
1928 | assert($this->lexer->lookahead !== null); | ||
1929 | $expression = null; | ||
1930 | $identVariable = null; | ||
1931 | $peek = $this->lexer->glimpse(); | ||
1932 | $lookaheadType = $this->lexer->lookahead->type; | ||
1933 | assert($peek !== null); | ||
1934 | |||
1935 | switch (true) { | ||
1936 | // ScalarExpression (u.name) | ||
1937 | case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type === TokenType::T_DOT: | ||
1938 | $expression = $this->ScalarExpression(); | ||
1939 | break; | ||
1940 | |||
1941 | // IdentificationVariable (u) | ||
1942 | case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_OPEN_PARENTHESIS: | ||
1943 | $expression = $identVariable = $this->IdentificationVariable(); | ||
1944 | break; | ||
1945 | |||
1946 | // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...)) | ||
1947 | case $lookaheadType === TokenType::T_CASE: | ||
1948 | case $lookaheadType === TokenType::T_COALESCE: | ||
1949 | case $lookaheadType === TokenType::T_NULLIF: | ||
1950 | $expression = $this->CaseExpression(); | ||
1951 | break; | ||
1952 | |||
1953 | // DQL Function (SUM(u.value) or SUM(u.value) + 1) | ||
1954 | case $this->isFunction(): | ||
1955 | $this->lexer->peek(); // "(" | ||
1956 | |||
1957 | $expression = match (true) { | ||
1958 | $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(), | ||
1959 | default => $this->FunctionDeclaration(), | ||
1960 | }; | ||
1961 | |||
1962 | break; | ||
1963 | |||
1964 | // Subselect | ||
1965 | case $lookaheadType === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT: | ||
1966 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
1967 | $expression = $this->Subselect(); | ||
1968 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
1969 | break; | ||
1970 | |||
1971 | // Shortcut: ScalarExpression => SimpleArithmeticExpression | ||
1972 | case $lookaheadType === TokenType::T_OPEN_PARENTHESIS: | ||
1973 | case $lookaheadType === TokenType::T_INTEGER: | ||
1974 | case $lookaheadType === TokenType::T_STRING: | ||
1975 | case $lookaheadType === TokenType::T_FLOAT: | ||
1976 | // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) | ||
1977 | case $lookaheadType === TokenType::T_MINUS: | ||
1978 | case $lookaheadType === TokenType::T_PLUS: | ||
1979 | $expression = $this->SimpleArithmeticExpression(); | ||
1980 | break; | ||
1981 | |||
1982 | // NewObjectExpression (New ClassName(id, name)) | ||
1983 | case $lookaheadType === TokenType::T_NEW: | ||
1984 | $expression = $this->NewObjectExpression(); | ||
1985 | break; | ||
1986 | |||
1987 | default: | ||
1988 | $this->syntaxError( | ||
1989 | 'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression', | ||
1990 | $this->lexer->lookahead, | ||
1991 | ); | ||
1992 | } | ||
1993 | |||
1994 | // [["AS"] ["HIDDEN"] AliasResultVariable] | ||
1995 | $mustHaveAliasResultVariable = false; | ||
1996 | |||
1997 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1998 | $this->match(TokenType::T_AS); | ||
1999 | |||
2000 | $mustHaveAliasResultVariable = true; | ||
2001 | } | ||
2002 | |||
2003 | $hiddenAliasResultVariable = false; | ||
2004 | |||
2005 | if ($this->lexer->isNextToken(TokenType::T_HIDDEN)) { | ||
2006 | $this->match(TokenType::T_HIDDEN); | ||
2007 | |||
2008 | $hiddenAliasResultVariable = true; | ||
2009 | } | ||
2010 | |||
2011 | $aliasResultVariable = null; | ||
2012 | |||
2013 | if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) { | ||
2014 | assert($expression instanceof AST\Node || is_string($expression)); | ||
2015 | $token = $this->lexer->lookahead; | ||
2016 | $aliasResultVariable = $this->AliasResultVariable(); | ||
2017 | |||
2018 | // Include AliasResultVariable in query components. | ||
2019 | $this->queryComponents[$aliasResultVariable] = [ | ||
2020 | 'resultVariable' => $expression, | ||
2021 | 'nestingLevel' => $this->nestingLevel, | ||
2022 | 'token' => $token, | ||
2023 | ]; | ||
2024 | } | ||
2025 | |||
2026 | // AST | ||
2027 | |||
2028 | $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable); | ||
2029 | |||
2030 | if ($identVariable) { | ||
2031 | $this->identVariableExpressions[$identVariable] = $expr; | ||
2032 | } | ||
2033 | |||
2034 | return $expr; | ||
2035 | } | ||
2036 | |||
2037 | /** | ||
2038 | * SimpleSelectExpression ::= ( | ||
2039 | * StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | | ||
2040 | * AggregateExpression | "(" Subselect ")" | ScalarExpression | ||
2041 | * ) [["AS"] AliasResultVariable] | ||
2042 | */ | ||
2043 | public function SimpleSelectExpression(): AST\SimpleSelectExpression | ||
2044 | { | ||
2045 | assert($this->lexer->lookahead !== null); | ||
2046 | $peek = $this->lexer->glimpse(); | ||
2047 | assert($peek !== null); | ||
2048 | |||
2049 | switch ($this->lexer->lookahead->type) { | ||
2050 | case TokenType::T_IDENTIFIER: | ||
2051 | switch (true) { | ||
2052 | case $peek->type === TokenType::T_DOT: | ||
2053 | $expression = $this->StateFieldPathExpression(); | ||
2054 | |||
2055 | return new AST\SimpleSelectExpression($expression); | ||
2056 | |||
2057 | case $peek->type !== TokenType::T_OPEN_PARENTHESIS: | ||
2058 | $expression = $this->IdentificationVariable(); | ||
2059 | |||
2060 | return new AST\SimpleSelectExpression($expression); | ||
2061 | |||
2062 | case $this->isFunction(): | ||
2063 | // SUM(u.id) + COUNT(u.id) | ||
2064 | if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) { | ||
2065 | return new AST\SimpleSelectExpression($this->ScalarExpression()); | ||
2066 | } | ||
2067 | |||
2068 | // COUNT(u.id) | ||
2069 | if ($this->isAggregateFunction($this->lexer->lookahead->type)) { | ||
2070 | return new AST\SimpleSelectExpression($this->AggregateExpression()); | ||
2071 | } | ||
2072 | |||
2073 | // IDENTITY(u) | ||
2074 | return new AST\SimpleSelectExpression($this->FunctionDeclaration()); | ||
2075 | |||
2076 | default: | ||
2077 | // Do nothing | ||
2078 | } | ||
2079 | |||
2080 | break; | ||
2081 | |||
2082 | case TokenType::T_OPEN_PARENTHESIS: | ||
2083 | if ($peek->type !== TokenType::T_SELECT) { | ||
2084 | // Shortcut: ScalarExpression => SimpleArithmeticExpression | ||
2085 | $expression = $this->SimpleArithmeticExpression(); | ||
2086 | |||
2087 | return new AST\SimpleSelectExpression($expression); | ||
2088 | } | ||
2089 | |||
2090 | // Subselect | ||
2091 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2092 | $expression = $this->Subselect(); | ||
2093 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2094 | |||
2095 | return new AST\SimpleSelectExpression($expression); | ||
2096 | |||
2097 | default: | ||
2098 | // Do nothing | ||
2099 | } | ||
2100 | |||
2101 | $this->lexer->peek(); | ||
2102 | |||
2103 | $expression = $this->ScalarExpression(); | ||
2104 | $expr = new AST\SimpleSelectExpression($expression); | ||
2105 | |||
2106 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
2107 | $this->match(TokenType::T_AS); | ||
2108 | } | ||
2109 | |||
2110 | if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER)) { | ||
2111 | $token = $this->lexer->lookahead; | ||
2112 | $resultVariable = $this->AliasResultVariable(); | ||
2113 | $expr->fieldIdentificationVariable = $resultVariable; | ||
2114 | |||
2115 | // Include AliasResultVariable in query components. | ||
2116 | $this->queryComponents[$resultVariable] = [ | ||
2117 | 'resultvariable' => $expr, | ||
2118 | 'nestingLevel' => $this->nestingLevel, | ||
2119 | 'token' => $token, | ||
2120 | ]; | ||
2121 | } | ||
2122 | |||
2123 | return $expr; | ||
2124 | } | ||
2125 | |||
2126 | /** | ||
2127 | * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* | ||
2128 | */ | ||
2129 | public function ConditionalExpression(): AST\ConditionalExpression|AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm | ||
2130 | { | ||
2131 | $conditionalTerms = []; | ||
2132 | $conditionalTerms[] = $this->ConditionalTerm(); | ||
2133 | |||
2134 | while ($this->lexer->isNextToken(TokenType::T_OR)) { | ||
2135 | $this->match(TokenType::T_OR); | ||
2136 | |||
2137 | $conditionalTerms[] = $this->ConditionalTerm(); | ||
2138 | } | ||
2139 | |||
2140 | // Phase 1 AST optimization: Prevent AST\ConditionalExpression | ||
2141 | // if only one AST\ConditionalTerm is defined | ||
2142 | if (count($conditionalTerms) === 1) { | ||
2143 | return $conditionalTerms[0]; | ||
2144 | } | ||
2145 | |||
2146 | return new AST\ConditionalExpression($conditionalTerms); | ||
2147 | } | ||
2148 | |||
2149 | /** | ||
2150 | * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* | ||
2151 | */ | ||
2152 | public function ConditionalTerm(): AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm | ||
2153 | { | ||
2154 | $conditionalFactors = []; | ||
2155 | $conditionalFactors[] = $this->ConditionalFactor(); | ||
2156 | |||
2157 | while ($this->lexer->isNextToken(TokenType::T_AND)) { | ||
2158 | $this->match(TokenType::T_AND); | ||
2159 | |||
2160 | $conditionalFactors[] = $this->ConditionalFactor(); | ||
2161 | } | ||
2162 | |||
2163 | // Phase 1 AST optimization: Prevent AST\ConditionalTerm | ||
2164 | // if only one AST\ConditionalFactor is defined | ||
2165 | if (count($conditionalFactors) === 1) { | ||
2166 | return $conditionalFactors[0]; | ||
2167 | } | ||
2168 | |||
2169 | return new AST\ConditionalTerm($conditionalFactors); | ||
2170 | } | ||
2171 | |||
2172 | /** | ||
2173 | * ConditionalFactor ::= ["NOT"] ConditionalPrimary | ||
2174 | */ | ||
2175 | public function ConditionalFactor(): AST\ConditionalFactor|AST\ConditionalPrimary | ||
2176 | { | ||
2177 | $not = false; | ||
2178 | |||
2179 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2180 | $this->match(TokenType::T_NOT); | ||
2181 | |||
2182 | $not = true; | ||
2183 | } | ||
2184 | |||
2185 | $conditionalPrimary = $this->ConditionalPrimary(); | ||
2186 | |||
2187 | // Phase 1 AST optimization: Prevent AST\ConditionalFactor | ||
2188 | // if only one AST\ConditionalPrimary is defined | ||
2189 | if (! $not) { | ||
2190 | return $conditionalPrimary; | ||
2191 | } | ||
2192 | |||
2193 | return new AST\ConditionalFactor($conditionalPrimary, $not); | ||
2194 | } | ||
2195 | |||
2196 | /** | ||
2197 | * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" | ||
2198 | */ | ||
2199 | public function ConditionalPrimary(): AST\ConditionalPrimary | ||
2200 | { | ||
2201 | $condPrimary = new AST\ConditionalPrimary(); | ||
2202 | |||
2203 | if (! $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { | ||
2204 | $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); | ||
2205 | |||
2206 | return $condPrimary; | ||
2207 | } | ||
2208 | |||
2209 | // Peek beyond the matching closing parenthesis ')' | ||
2210 | $peek = $this->peekBeyondClosingParenthesis(); | ||
2211 | |||
2212 | if ( | ||
2213 | $peek !== null && ( | ||
2214 | in_array($peek->value, ['=', '<', '<=', '<>', '>', '>=', '!='], true) || | ||
2215 | in_array($peek->type, [TokenType::T_NOT, TokenType::T_BETWEEN, TokenType::T_LIKE, TokenType::T_IN, TokenType::T_IS, TokenType::T_EXISTS], true) || | ||
2216 | $this->isMathOperator($peek) | ||
2217 | ) | ||
2218 | ) { | ||
2219 | $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); | ||
2220 | |||
2221 | return $condPrimary; | ||
2222 | } | ||
2223 | |||
2224 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2225 | $condPrimary->conditionalExpression = $this->ConditionalExpression(); | ||
2226 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2227 | |||
2228 | return $condPrimary; | ||
2229 | } | ||
2230 | |||
2231 | /** | ||
2232 | * SimpleConditionalExpression ::= | ||
2233 | * ComparisonExpression | BetweenExpression | LikeExpression | | ||
2234 | * InExpression | NullComparisonExpression | ExistsExpression | | ||
2235 | * EmptyCollectionComparisonExpression | CollectionMemberExpression | | ||
2236 | * InstanceOfExpression | ||
2237 | */ | ||
2238 | public function SimpleConditionalExpression(): AST\ExistsExpression|AST\BetweenExpression|AST\LikeExpression|AST\InListExpression|AST\InSubselectExpression|AST\InstanceOfExpression|AST\CollectionMemberExpression|AST\NullComparisonExpression|AST\EmptyCollectionComparisonExpression|AST\ComparisonExpression | ||
2239 | { | ||
2240 | assert($this->lexer->lookahead !== null); | ||
2241 | if ($this->lexer->isNextToken(TokenType::T_EXISTS)) { | ||
2242 | return $this->ExistsExpression(); | ||
2243 | } | ||
2244 | |||
2245 | $token = $this->lexer->lookahead; | ||
2246 | $peek = $this->lexer->glimpse(); | ||
2247 | $lookahead = $token; | ||
2248 | |||
2249 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2250 | $token = $this->lexer->glimpse(); | ||
2251 | } | ||
2252 | |||
2253 | assert($token !== null); | ||
2254 | assert($peek !== null); | ||
2255 | if ($token->type === TokenType::T_IDENTIFIER || $token->type === TokenType::T_INPUT_PARAMETER || $this->isFunction()) { | ||
2256 | // Peek beyond the matching closing parenthesis. | ||
2257 | $beyond = $this->lexer->peek(); | ||
2258 | |||
2259 | switch ($peek->value) { | ||
2260 | case '(': | ||
2261 | // Peeks beyond the matched closing parenthesis. | ||
2262 | $token = $this->peekBeyondClosingParenthesis(false); | ||
2263 | assert($token !== null); | ||
2264 | |||
2265 | if ($token->type === TokenType::T_NOT) { | ||
2266 | $token = $this->lexer->peek(); | ||
2267 | assert($token !== null); | ||
2268 | } | ||
2269 | |||
2270 | if ($token->type === TokenType::T_IS) { | ||
2271 | $lookahead = $this->lexer->peek(); | ||
2272 | } | ||
2273 | |||
2274 | break; | ||
2275 | |||
2276 | default: | ||
2277 | // Peek beyond the PathExpression or InputParameter. | ||
2278 | $token = $beyond; | ||
2279 | |||
2280 | while ($token->value === '.') { | ||
2281 | $this->lexer->peek(); | ||
2282 | |||
2283 | $token = $this->lexer->peek(); | ||
2284 | assert($token !== null); | ||
2285 | } | ||
2286 | |||
2287 | // Also peek beyond a NOT if there is one. | ||
2288 | assert($token !== null); | ||
2289 | if ($token->type === TokenType::T_NOT) { | ||
2290 | $token = $this->lexer->peek(); | ||
2291 | assert($token !== null); | ||
2292 | } | ||
2293 | |||
2294 | // We need to go even further in case of IS (differentiate between NULL and EMPTY) | ||
2295 | $lookahead = $this->lexer->peek(); | ||
2296 | } | ||
2297 | |||
2298 | assert($lookahead !== null); | ||
2299 | // Also peek beyond a NOT if there is one. | ||
2300 | if ($lookahead->type === TokenType::T_NOT) { | ||
2301 | $lookahead = $this->lexer->peek(); | ||
2302 | } | ||
2303 | |||
2304 | $this->lexer->resetPeek(); | ||
2305 | } | ||
2306 | |||
2307 | if ($token->type === TokenType::T_BETWEEN) { | ||
2308 | return $this->BetweenExpression(); | ||
2309 | } | ||
2310 | |||
2311 | if ($token->type === TokenType::T_LIKE) { | ||
2312 | return $this->LikeExpression(); | ||
2313 | } | ||
2314 | |||
2315 | if ($token->type === TokenType::T_IN) { | ||
2316 | return $this->InExpression(); | ||
2317 | } | ||
2318 | |||
2319 | if ($token->type === TokenType::T_INSTANCE) { | ||
2320 | return $this->InstanceOfExpression(); | ||
2321 | } | ||
2322 | |||
2323 | if ($token->type === TokenType::T_MEMBER) { | ||
2324 | return $this->CollectionMemberExpression(); | ||
2325 | } | ||
2326 | |||
2327 | assert($lookahead !== null); | ||
2328 | if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_NULL) { | ||
2329 | return $this->NullComparisonExpression(); | ||
2330 | } | ||
2331 | |||
2332 | if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_EMPTY) { | ||
2333 | return $this->EmptyCollectionComparisonExpression(); | ||
2334 | } | ||
2335 | |||
2336 | return $this->ComparisonExpression(); | ||
2337 | } | ||
2338 | |||
2339 | /** | ||
2340 | * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY" | ||
2341 | */ | ||
2342 | public function EmptyCollectionComparisonExpression(): AST\EmptyCollectionComparisonExpression | ||
2343 | { | ||
2344 | $pathExpression = $this->CollectionValuedPathExpression(); | ||
2345 | $this->match(TokenType::T_IS); | ||
2346 | |||
2347 | $not = false; | ||
2348 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2349 | $this->match(TokenType::T_NOT); | ||
2350 | $not = true; | ||
2351 | } | ||
2352 | |||
2353 | $this->match(TokenType::T_EMPTY); | ||
2354 | |||
2355 | return new AST\EmptyCollectionComparisonExpression( | ||
2356 | $pathExpression, | ||
2357 | $not, | ||
2358 | ); | ||
2359 | } | ||
2360 | |||
2361 | /** | ||
2362 | * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression | ||
2363 | * | ||
2364 | * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression | ||
2365 | * SimpleEntityExpression ::= IdentificationVariable | InputParameter | ||
2366 | */ | ||
2367 | public function CollectionMemberExpression(): AST\CollectionMemberExpression | ||
2368 | { | ||
2369 | $not = false; | ||
2370 | $entityExpr = $this->EntityExpression(); | ||
2371 | |||
2372 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2373 | $this->match(TokenType::T_NOT); | ||
2374 | |||
2375 | $not = true; | ||
2376 | } | ||
2377 | |||
2378 | $this->match(TokenType::T_MEMBER); | ||
2379 | |||
2380 | if ($this->lexer->isNextToken(TokenType::T_OF)) { | ||
2381 | $this->match(TokenType::T_OF); | ||
2382 | } | ||
2383 | |||
2384 | return new AST\CollectionMemberExpression( | ||
2385 | $entityExpr, | ||
2386 | $this->CollectionValuedPathExpression(), | ||
2387 | $not, | ||
2388 | ); | ||
2389 | } | ||
2390 | |||
2391 | /** | ||
2392 | * Literal ::= string | char | integer | float | boolean | ||
2393 | */ | ||
2394 | public function Literal(): AST\Literal | ||
2395 | { | ||
2396 | assert($this->lexer->lookahead !== null); | ||
2397 | assert($this->lexer->token !== null); | ||
2398 | switch ($this->lexer->lookahead->type) { | ||
2399 | case TokenType::T_STRING: | ||
2400 | $this->match(TokenType::T_STRING); | ||
2401 | |||
2402 | return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); | ||
2403 | |||
2404 | case TokenType::T_INTEGER: | ||
2405 | case TokenType::T_FLOAT: | ||
2406 | $this->match( | ||
2407 | $this->lexer->isNextToken(TokenType::T_INTEGER) ? TokenType::T_INTEGER : TokenType::T_FLOAT, | ||
2408 | ); | ||
2409 | |||
2410 | return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token->value); | ||
2411 | |||
2412 | case TokenType::T_TRUE: | ||
2413 | case TokenType::T_FALSE: | ||
2414 | $this->match( | ||
2415 | $this->lexer->isNextToken(TokenType::T_TRUE) ? TokenType::T_TRUE : TokenType::T_FALSE, | ||
2416 | ); | ||
2417 | |||
2418 | return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value); | ||
2419 | |||
2420 | default: | ||
2421 | $this->syntaxError('Literal'); | ||
2422 | } | ||
2423 | } | ||
2424 | |||
2425 | /** | ||
2426 | * InParameter ::= ArithmeticExpression | InputParameter | ||
2427 | */ | ||
2428 | public function InParameter(): AST\InputParameter|AST\ArithmeticExpression | ||
2429 | { | ||
2430 | assert($this->lexer->lookahead !== null); | ||
2431 | if ($this->lexer->lookahead->type === TokenType::T_INPUT_PARAMETER) { | ||
2432 | return $this->InputParameter(); | ||
2433 | } | ||
2434 | |||
2435 | return $this->ArithmeticExpression(); | ||
2436 | } | ||
2437 | |||
2438 | /** | ||
2439 | * InputParameter ::= PositionalParameter | NamedParameter | ||
2440 | */ | ||
2441 | public function InputParameter(): AST\InputParameter | ||
2442 | { | ||
2443 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
2444 | assert($this->lexer->token !== null); | ||
2445 | |||
2446 | return new AST\InputParameter($this->lexer->token->value); | ||
2447 | } | ||
2448 | |||
2449 | /** | ||
2450 | * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" | ||
2451 | */ | ||
2452 | public function ArithmeticExpression(): AST\ArithmeticExpression | ||
2453 | { | ||
2454 | $expr = new AST\ArithmeticExpression(); | ||
2455 | |||
2456 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { | ||
2457 | $peek = $this->lexer->glimpse(); | ||
2458 | assert($peek !== null); | ||
2459 | |||
2460 | if ($peek->type === TokenType::T_SELECT) { | ||
2461 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2462 | $expr->subselect = $this->Subselect(); | ||
2463 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2464 | |||
2465 | return $expr; | ||
2466 | } | ||
2467 | } | ||
2468 | |||
2469 | $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression(); | ||
2470 | |||
2471 | return $expr; | ||
2472 | } | ||
2473 | |||
2474 | /** | ||
2475 | * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}* | ||
2476 | */ | ||
2477 | public function SimpleArithmeticExpression(): AST\Node|string | ||
2478 | { | ||
2479 | $terms = []; | ||
2480 | $terms[] = $this->ArithmeticTerm(); | ||
2481 | |||
2482 | while (($isPlus = $this->lexer->isNextToken(TokenType::T_PLUS)) || $this->lexer->isNextToken(TokenType::T_MINUS)) { | ||
2483 | $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS); | ||
2484 | |||
2485 | assert($this->lexer->token !== null); | ||
2486 | $terms[] = $this->lexer->token->value; | ||
2487 | $terms[] = $this->ArithmeticTerm(); | ||
2488 | } | ||
2489 | |||
2490 | // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression | ||
2491 | // if only one AST\ArithmeticTerm is defined | ||
2492 | if (count($terms) === 1) { | ||
2493 | return $terms[0]; | ||
2494 | } | ||
2495 | |||
2496 | return new AST\SimpleArithmeticExpression($terms); | ||
2497 | } | ||
2498 | |||
2499 | /** | ||
2500 | * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* | ||
2501 | */ | ||
2502 | public function ArithmeticTerm(): AST\Node|string | ||
2503 | { | ||
2504 | $factors = []; | ||
2505 | $factors[] = $this->ArithmeticFactor(); | ||
2506 | |||
2507 | while (($isMult = $this->lexer->isNextToken(TokenType::T_MULTIPLY)) || $this->lexer->isNextToken(TokenType::T_DIVIDE)) { | ||
2508 | $this->match($isMult ? TokenType::T_MULTIPLY : TokenType::T_DIVIDE); | ||
2509 | |||
2510 | assert($this->lexer->token !== null); | ||
2511 | $factors[] = $this->lexer->token->value; | ||
2512 | $factors[] = $this->ArithmeticFactor(); | ||
2513 | } | ||
2514 | |||
2515 | // Phase 1 AST optimization: Prevent AST\ArithmeticTerm | ||
2516 | // if only one AST\ArithmeticFactor is defined | ||
2517 | if (count($factors) === 1) { | ||
2518 | return $factors[0]; | ||
2519 | } | ||
2520 | |||
2521 | return new AST\ArithmeticTerm($factors); | ||
2522 | } | ||
2523 | |||
2524 | /** | ||
2525 | * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary | ||
2526 | */ | ||
2527 | public function ArithmeticFactor(): AST\Node|string|AST\ArithmeticFactor | ||
2528 | { | ||
2529 | $sign = null; | ||
2530 | |||
2531 | $isPlus = $this->lexer->isNextToken(TokenType::T_PLUS); | ||
2532 | if ($isPlus || $this->lexer->isNextToken(TokenType::T_MINUS)) { | ||
2533 | $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS); | ||
2534 | $sign = $isPlus; | ||
2535 | } | ||
2536 | |||
2537 | $primary = $this->ArithmeticPrimary(); | ||
2538 | |||
2539 | // Phase 1 AST optimization: Prevent AST\ArithmeticFactor | ||
2540 | // if only one AST\ArithmeticPrimary is defined | ||
2541 | if ($sign === null) { | ||
2542 | return $primary; | ||
2543 | } | ||
2544 | |||
2545 | return new AST\ArithmeticFactor($primary, $sign); | ||
2546 | } | ||
2547 | |||
2548 | /** | ||
2549 | * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression | ||
2550 | * | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings | ||
2551 | * | FunctionsReturningDatetime | IdentificationVariable | ResultVariable | ||
2552 | * | InputParameter | CaseExpression | ||
2553 | */ | ||
2554 | public function ArithmeticPrimary(): AST\Node|string | ||
2555 | { | ||
2556 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { | ||
2557 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2558 | |||
2559 | $expr = $this->SimpleArithmeticExpression(); | ||
2560 | |||
2561 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2562 | |||
2563 | return new AST\ParenthesisExpression($expr); | ||
2564 | } | ||
2565 | |||
2566 | if ($this->lexer->lookahead === null) { | ||
2567 | $this->syntaxError('ArithmeticPrimary'); | ||
2568 | } | ||
2569 | |||
2570 | switch ($this->lexer->lookahead->type) { | ||
2571 | case TokenType::T_COALESCE: | ||
2572 | case TokenType::T_NULLIF: | ||
2573 | case TokenType::T_CASE: | ||
2574 | return $this->CaseExpression(); | ||
2575 | |||
2576 | case TokenType::T_IDENTIFIER: | ||
2577 | $peek = $this->lexer->glimpse(); | ||
2578 | |||
2579 | if ($peek !== null && $peek->value === '(') { | ||
2580 | return $this->FunctionDeclaration(); | ||
2581 | } | ||
2582 | |||
2583 | if ($peek !== null && $peek->value === '.') { | ||
2584 | return $this->SingleValuedPathExpression(); | ||
2585 | } | ||
2586 | |||
2587 | if (isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable'])) { | ||
2588 | return $this->ResultVariable(); | ||
2589 | } | ||
2590 | |||
2591 | return $this->StateFieldPathExpression(); | ||
2592 | |||
2593 | case TokenType::T_INPUT_PARAMETER: | ||
2594 | return $this->InputParameter(); | ||
2595 | |||
2596 | default: | ||
2597 | $peek = $this->lexer->glimpse(); | ||
2598 | |||
2599 | if ($peek !== null && $peek->value === '(') { | ||
2600 | return $this->FunctionDeclaration(); | ||
2601 | } | ||
2602 | |||
2603 | return $this->Literal(); | ||
2604 | } | ||
2605 | } | ||
2606 | |||
2607 | /** | ||
2608 | * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")" | ||
2609 | */ | ||
2610 | public function StringExpression(): AST\Subselect|AST\Node|string | ||
2611 | { | ||
2612 | $peek = $this->lexer->glimpse(); | ||
2613 | assert($peek !== null); | ||
2614 | |||
2615 | // Subselect | ||
2616 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $peek->type === TokenType::T_SELECT) { | ||
2617 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2618 | $expr = $this->Subselect(); | ||
2619 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2620 | |||
2621 | return $expr; | ||
2622 | } | ||
2623 | |||
2624 | assert($this->lexer->lookahead !== null); | ||
2625 | // ResultVariable (string) | ||
2626 | if ( | ||
2627 | $this->lexer->isNextToken(TokenType::T_IDENTIFIER) && | ||
2628 | isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable']) | ||
2629 | ) { | ||
2630 | return $this->ResultVariable(); | ||
2631 | } | ||
2632 | |||
2633 | return $this->StringPrimary(); | ||
2634 | } | ||
2635 | |||
2636 | /** | ||
2637 | * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression | ||
2638 | */ | ||
2639 | public function StringPrimary(): AST\Node | ||
2640 | { | ||
2641 | assert($this->lexer->lookahead !== null); | ||
2642 | $lookaheadType = $this->lexer->lookahead->type; | ||
2643 | |||
2644 | switch ($lookaheadType) { | ||
2645 | case TokenType::T_IDENTIFIER: | ||
2646 | $peek = $this->lexer->glimpse(); | ||
2647 | assert($peek !== null); | ||
2648 | |||
2649 | if ($peek->value === '.') { | ||
2650 | return $this->StateFieldPathExpression(); | ||
2651 | } | ||
2652 | |||
2653 | if ($peek->value === '(') { | ||
2654 | // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions. | ||
2655 | return $this->FunctionDeclaration(); | ||
2656 | } | ||
2657 | |||
2658 | $this->syntaxError("'.' or '('"); | ||
2659 | break; | ||
2660 | |||
2661 | case TokenType::T_STRING: | ||
2662 | $this->match(TokenType::T_STRING); | ||
2663 | assert($this->lexer->token !== null); | ||
2664 | |||
2665 | return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); | ||
2666 | |||
2667 | case TokenType::T_INPUT_PARAMETER: | ||
2668 | return $this->InputParameter(); | ||
2669 | |||
2670 | case TokenType::T_CASE: | ||
2671 | case TokenType::T_COALESCE: | ||
2672 | case TokenType::T_NULLIF: | ||
2673 | return $this->CaseExpression(); | ||
2674 | |||
2675 | default: | ||
2676 | assert($lookaheadType !== null); | ||
2677 | if ($this->isAggregateFunction($lookaheadType)) { | ||
2678 | return $this->AggregateExpression(); | ||
2679 | } | ||
2680 | } | ||
2681 | |||
2682 | $this->syntaxError( | ||
2683 | 'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression', | ||
2684 | ); | ||
2685 | } | ||
2686 | |||
2687 | /** | ||
2688 | * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression | ||
2689 | */ | ||
2690 | public function EntityExpression(): AST\InputParameter|AST\PathExpression | ||
2691 | { | ||
2692 | $glimpse = $this->lexer->glimpse(); | ||
2693 | assert($glimpse !== null); | ||
2694 | |||
2695 | if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER) && $glimpse->value === '.') { | ||
2696 | return $this->SingleValuedAssociationPathExpression(); | ||
2697 | } | ||
2698 | |||
2699 | return $this->SimpleEntityExpression(); | ||
2700 | } | ||
2701 | |||
2702 | /** | ||
2703 | * SimpleEntityExpression ::= IdentificationVariable | InputParameter | ||
2704 | */ | ||
2705 | public function SimpleEntityExpression(): AST\InputParameter|AST\PathExpression | ||
2706 | { | ||
2707 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
2708 | return $this->InputParameter(); | ||
2709 | } | ||
2710 | |||
2711 | return $this->StateFieldPathExpression(); | ||
2712 | } | ||
2713 | |||
2714 | /** | ||
2715 | * AggregateExpression ::= | ||
2716 | * ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")" | ||
2717 | */ | ||
2718 | public function AggregateExpression(): AST\AggregateExpression | ||
2719 | { | ||
2720 | assert($this->lexer->lookahead !== null); | ||
2721 | $lookaheadType = $this->lexer->lookahead->type; | ||
2722 | $isDistinct = false; | ||
2723 | |||
2724 | if (! in_array($lookaheadType, [TokenType::T_COUNT, TokenType::T_AVG, TokenType::T_MAX, TokenType::T_MIN, TokenType::T_SUM], true)) { | ||
2725 | $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT'); | ||
2726 | } | ||
2727 | |||
2728 | $this->match($lookaheadType); | ||
2729 | assert($this->lexer->token !== null); | ||
2730 | $functionName = $this->lexer->token->value; | ||
2731 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2732 | |||
2733 | if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { | ||
2734 | $this->match(TokenType::T_DISTINCT); | ||
2735 | $isDistinct = true; | ||
2736 | } | ||
2737 | |||
2738 | $pathExp = $this->SimpleArithmeticExpression(); | ||
2739 | |||
2740 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2741 | |||
2742 | return new AST\AggregateExpression($functionName, $pathExp, $isDistinct); | ||
2743 | } | ||
2744 | |||
2745 | /** | ||
2746 | * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")" | ||
2747 | */ | ||
2748 | public function QuantifiedExpression(): AST\QuantifiedExpression | ||
2749 | { | ||
2750 | assert($this->lexer->lookahead !== null); | ||
2751 | $lookaheadType = $this->lexer->lookahead->type; | ||
2752 | $value = $this->lexer->lookahead->value; | ||
2753 | |||
2754 | if (! in_array($lookaheadType, [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], true)) { | ||
2755 | $this->syntaxError('ALL, ANY or SOME'); | ||
2756 | } | ||
2757 | |||
2758 | $this->match($lookaheadType); | ||
2759 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2760 | |||
2761 | $qExpr = new AST\QuantifiedExpression($this->Subselect()); | ||
2762 | $qExpr->type = $value; | ||
2763 | |||
2764 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2765 | |||
2766 | return $qExpr; | ||
2767 | } | ||
2768 | |||
2769 | /** | ||
2770 | * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression | ||
2771 | */ | ||
2772 | public function BetweenExpression(): AST\BetweenExpression | ||
2773 | { | ||
2774 | $not = false; | ||
2775 | $arithExpr1 = $this->ArithmeticExpression(); | ||
2776 | |||
2777 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2778 | $this->match(TokenType::T_NOT); | ||
2779 | $not = true; | ||
2780 | } | ||
2781 | |||
2782 | $this->match(TokenType::T_BETWEEN); | ||
2783 | $arithExpr2 = $this->ArithmeticExpression(); | ||
2784 | $this->match(TokenType::T_AND); | ||
2785 | $arithExpr3 = $this->ArithmeticExpression(); | ||
2786 | |||
2787 | return new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3, $not); | ||
2788 | } | ||
2789 | |||
2790 | /** | ||
2791 | * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) | ||
2792 | */ | ||
2793 | public function ComparisonExpression(): AST\ComparisonExpression | ||
2794 | { | ||
2795 | $this->lexer->glimpse(); | ||
2796 | |||
2797 | $leftExpr = $this->ArithmeticExpression(); | ||
2798 | $operator = $this->ComparisonOperator(); | ||
2799 | $rightExpr = $this->isNextAllAnySome() | ||
2800 | ? $this->QuantifiedExpression() | ||
2801 | : $this->ArithmeticExpression(); | ||
2802 | |||
2803 | return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr); | ||
2804 | } | ||
2805 | |||
2806 | /** | ||
2807 | * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")" | ||
2808 | */ | ||
2809 | public function InExpression(): AST\InListExpression|AST\InSubselectExpression | ||
2810 | { | ||
2811 | $expression = $this->ArithmeticExpression(); | ||
2812 | |||
2813 | $not = false; | ||
2814 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2815 | $this->match(TokenType::T_NOT); | ||
2816 | $not = true; | ||
2817 | } | ||
2818 | |||
2819 | $this->match(TokenType::T_IN); | ||
2820 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2821 | |||
2822 | if ($this->lexer->isNextToken(TokenType::T_SELECT)) { | ||
2823 | $inExpression = new AST\InSubselectExpression( | ||
2824 | $expression, | ||
2825 | $this->Subselect(), | ||
2826 | $not, | ||
2827 | ); | ||
2828 | } else { | ||
2829 | $literals = [$this->InParameter()]; | ||
2830 | |||
2831 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
2832 | $this->match(TokenType::T_COMMA); | ||
2833 | $literals[] = $this->InParameter(); | ||
2834 | } | ||
2835 | |||
2836 | $inExpression = new AST\InListExpression( | ||
2837 | $expression, | ||
2838 | $literals, | ||
2839 | $not, | ||
2840 | ); | ||
2841 | } | ||
2842 | |||
2843 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2844 | |||
2845 | return $inExpression; | ||
2846 | } | ||
2847 | |||
2848 | /** | ||
2849 | * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")") | ||
2850 | */ | ||
2851 | public function InstanceOfExpression(): AST\InstanceOfExpression | ||
2852 | { | ||
2853 | $identificationVariable = $this->IdentificationVariable(); | ||
2854 | |||
2855 | $not = false; | ||
2856 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2857 | $this->match(TokenType::T_NOT); | ||
2858 | $not = true; | ||
2859 | } | ||
2860 | |||
2861 | $this->match(TokenType::T_INSTANCE); | ||
2862 | $this->match(TokenType::T_OF); | ||
2863 | |||
2864 | $exprValues = $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) | ||
2865 | ? $this->InstanceOfParameterList() | ||
2866 | : [$this->InstanceOfParameter()]; | ||
2867 | |||
2868 | return new AST\InstanceOfExpression( | ||
2869 | $identificationVariable, | ||
2870 | $exprValues, | ||
2871 | $not, | ||
2872 | ); | ||
2873 | } | ||
2874 | |||
2875 | /** @return non-empty-list<AST\InputParameter|string> */ | ||
2876 | public function InstanceOfParameterList(): array | ||
2877 | { | ||
2878 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2879 | |||
2880 | $exprValues = [$this->InstanceOfParameter()]; | ||
2881 | |||
2882 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
2883 | $this->match(TokenType::T_COMMA); | ||
2884 | |||
2885 | $exprValues[] = $this->InstanceOfParameter(); | ||
2886 | } | ||
2887 | |||
2888 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2889 | |||
2890 | return $exprValues; | ||
2891 | } | ||
2892 | |||
2893 | /** | ||
2894 | * InstanceOfParameter ::= AbstractSchemaName | InputParameter | ||
2895 | */ | ||
2896 | public function InstanceOfParameter(): AST\InputParameter|string | ||
2897 | { | ||
2898 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
2899 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
2900 | assert($this->lexer->token !== null); | ||
2901 | |||
2902 | return new AST\InputParameter($this->lexer->token->value); | ||
2903 | } | ||
2904 | |||
2905 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
2906 | |||
2907 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
2908 | |||
2909 | return $abstractSchemaName; | ||
2910 | } | ||
2911 | |||
2912 | /** | ||
2913 | * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char] | ||
2914 | */ | ||
2915 | public function LikeExpression(): AST\LikeExpression | ||
2916 | { | ||
2917 | $stringExpr = $this->StringExpression(); | ||
2918 | $not = false; | ||
2919 | |||
2920 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2921 | $this->match(TokenType::T_NOT); | ||
2922 | $not = true; | ||
2923 | } | ||
2924 | |||
2925 | $this->match(TokenType::T_LIKE); | ||
2926 | |||
2927 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
2928 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
2929 | assert($this->lexer->token !== null); | ||
2930 | $stringPattern = new AST\InputParameter($this->lexer->token->value); | ||
2931 | } else { | ||
2932 | $stringPattern = $this->StringPrimary(); | ||
2933 | } | ||
2934 | |||
2935 | $escapeChar = null; | ||
2936 | |||
2937 | if ($this->lexer->lookahead !== null && $this->lexer->lookahead->type === TokenType::T_ESCAPE) { | ||
2938 | $this->match(TokenType::T_ESCAPE); | ||
2939 | $this->match(TokenType::T_STRING); | ||
2940 | assert($this->lexer->token !== null); | ||
2941 | |||
2942 | $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); | ||
2943 | } | ||
2944 | |||
2945 | return new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar, $not); | ||
2946 | } | ||
2947 | |||
2948 | /** | ||
2949 | * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL" | ||
2950 | */ | ||
2951 | public function NullComparisonExpression(): AST\NullComparisonExpression | ||
2952 | { | ||
2953 | switch (true) { | ||
2954 | case $this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER): | ||
2955 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
2956 | assert($this->lexer->token !== null); | ||
2957 | |||
2958 | $expr = new AST\InputParameter($this->lexer->token->value); | ||
2959 | break; | ||
2960 | |||
2961 | case $this->lexer->isNextToken(TokenType::T_NULLIF): | ||
2962 | $expr = $this->NullIfExpression(); | ||
2963 | break; | ||
2964 | |||
2965 | case $this->lexer->isNextToken(TokenType::T_COALESCE): | ||
2966 | $expr = $this->CoalesceExpression(); | ||
2967 | break; | ||
2968 | |||
2969 | case $this->isFunction(): | ||
2970 | $expr = $this->FunctionDeclaration(); | ||
2971 | break; | ||
2972 | |||
2973 | default: | ||
2974 | // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression | ||
2975 | $glimpse = $this->lexer->glimpse(); | ||
2976 | assert($glimpse !== null); | ||
2977 | |||
2978 | if ($glimpse->type === TokenType::T_DOT) { | ||
2979 | $expr = $this->SingleValuedPathExpression(); | ||
2980 | |||
2981 | // Leave switch statement | ||
2982 | break; | ||
2983 | } | ||
2984 | |||
2985 | assert($this->lexer->lookahead !== null); | ||
2986 | $lookaheadValue = $this->lexer->lookahead->value; | ||
2987 | |||
2988 | // Validate existing component | ||
2989 | if (! isset($this->queryComponents[$lookaheadValue])) { | ||
2990 | $this->semanticalError('Cannot add having condition on undefined result variable.'); | ||
2991 | } | ||
2992 | |||
2993 | // Validate SingleValuedPathExpression (ie.: "product") | ||
2994 | if (isset($this->queryComponents[$lookaheadValue]['metadata'])) { | ||
2995 | $expr = $this->SingleValuedPathExpression(); | ||
2996 | break; | ||
2997 | } | ||
2998 | |||
2999 | // Validating ResultVariable | ||
3000 | if (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) { | ||
3001 | $this->semanticalError('Cannot add having condition on a non result variable.'); | ||
3002 | } | ||
3003 | |||
3004 | $expr = $this->ResultVariable(); | ||
3005 | break; | ||
3006 | } | ||
3007 | |||
3008 | $this->match(TokenType::T_IS); | ||
3009 | |||
3010 | $not = false; | ||
3011 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
3012 | $this->match(TokenType::T_NOT); | ||
3013 | |||
3014 | $not = true; | ||
3015 | } | ||
3016 | |||
3017 | $this->match(TokenType::T_NULL); | ||
3018 | |||
3019 | return new AST\NullComparisonExpression($expr, $not); | ||
3020 | } | ||
3021 | |||
3022 | /** | ||
3023 | * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")" | ||
3024 | */ | ||
3025 | public function ExistsExpression(): AST\ExistsExpression | ||
3026 | { | ||
3027 | $not = false; | ||
3028 | |||
3029 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
3030 | $this->match(TokenType::T_NOT); | ||
3031 | $not = true; | ||
3032 | } | ||
3033 | |||
3034 | $this->match(TokenType::T_EXISTS); | ||
3035 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
3036 | |||
3037 | $subselect = $this->Subselect(); | ||
3038 | |||
3039 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
3040 | |||
3041 | return new AST\ExistsExpression($subselect, $not); | ||
3042 | } | ||
3043 | |||
3044 | /** | ||
3045 | * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!=" | ||
3046 | */ | ||
3047 | public function ComparisonOperator(): string | ||
3048 | { | ||
3049 | assert($this->lexer->lookahead !== null); | ||
3050 | switch ($this->lexer->lookahead->value) { | ||
3051 | case '=': | ||
3052 | $this->match(TokenType::T_EQUALS); | ||
3053 | |||
3054 | return '='; | ||
3055 | |||
3056 | case '<': | ||
3057 | $this->match(TokenType::T_LOWER_THAN); | ||
3058 | $operator = '<'; | ||
3059 | |||
3060 | if ($this->lexer->isNextToken(TokenType::T_EQUALS)) { | ||
3061 | $this->match(TokenType::T_EQUALS); | ||
3062 | $operator .= '='; | ||
3063 | } elseif ($this->lexer->isNextToken(TokenType::T_GREATER_THAN)) { | ||
3064 | $this->match(TokenType::T_GREATER_THAN); | ||
3065 | $operator .= '>'; | ||
3066 | } | ||
3067 | |||
3068 | return $operator; | ||
3069 | |||
3070 | case '>': | ||
3071 | $this->match(TokenType::T_GREATER_THAN); | ||
3072 | $operator = '>'; | ||
3073 | |||
3074 | if ($this->lexer->isNextToken(TokenType::T_EQUALS)) { | ||
3075 | $this->match(TokenType::T_EQUALS); | ||
3076 | $operator .= '='; | ||
3077 | } | ||
3078 | |||
3079 | return $operator; | ||
3080 | |||
3081 | case '!': | ||
3082 | $this->match(TokenType::T_NEGATE); | ||
3083 | $this->match(TokenType::T_EQUALS); | ||
3084 | |||
3085 | return '<>'; | ||
3086 | |||
3087 | default: | ||
3088 | $this->syntaxError('=, <, <=, <>, >, >=, !='); | ||
3089 | } | ||
3090 | } | ||
3091 | |||
3092 | /** | ||
3093 | * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime | ||
3094 | */ | ||
3095 | public function FunctionDeclaration(): Functions\FunctionNode | ||
3096 | { | ||
3097 | assert($this->lexer->lookahead !== null); | ||
3098 | $token = $this->lexer->lookahead; | ||
3099 | $funcName = strtolower($token->value); | ||
3100 | |||
3101 | $customFunctionDeclaration = $this->CustomFunctionDeclaration(); | ||
3102 | |||
3103 | // Check for custom functions functions first! | ||
3104 | switch (true) { | ||
3105 | case $customFunctionDeclaration !== null: | ||
3106 | return $customFunctionDeclaration; | ||
3107 | |||
3108 | case isset(self::$stringFunctions[$funcName]): | ||
3109 | return $this->FunctionsReturningStrings(); | ||
3110 | |||
3111 | case isset(self::$numericFunctions[$funcName]): | ||
3112 | return $this->FunctionsReturningNumerics(); | ||
3113 | |||
3114 | case isset(self::$datetimeFunctions[$funcName]): | ||
3115 | return $this->FunctionsReturningDatetime(); | ||
3116 | |||
3117 | default: | ||
3118 | $this->syntaxError('known function', $token); | ||
3119 | } | ||
3120 | } | ||
3121 | |||
3122 | /** | ||
3123 | * Helper function for FunctionDeclaration grammar rule. | ||
3124 | */ | ||
3125 | private function CustomFunctionDeclaration(): Functions\FunctionNode|null | ||
3126 | { | ||
3127 | assert($this->lexer->lookahead !== null); | ||
3128 | $token = $this->lexer->lookahead; | ||
3129 | $funcName = strtolower($token->value); | ||
3130 | |||
3131 | // Check for custom functions afterwards | ||
3132 | $config = $this->em->getConfiguration(); | ||
3133 | |||
3134 | return match (true) { | ||
3135 | $config->getCustomStringFunction($funcName) !== null => $this->CustomFunctionsReturningStrings(), | ||
3136 | $config->getCustomNumericFunction($funcName) !== null => $this->CustomFunctionsReturningNumerics(), | ||
3137 | $config->getCustomDatetimeFunction($funcName) !== null => $this->CustomFunctionsReturningDatetime(), | ||
3138 | default => null, | ||
3139 | }; | ||
3140 | } | ||
3141 | |||
3142 | /** | ||
3143 | * FunctionsReturningNumerics ::= | ||
3144 | * "LENGTH" "(" StringPrimary ")" | | ||
3145 | * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" | | ||
3146 | * "ABS" "(" SimpleArithmeticExpression ")" | | ||
3147 | * "SQRT" "(" SimpleArithmeticExpression ")" | | ||
3148 | * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | | ||
3149 | * "SIZE" "(" CollectionValuedPathExpression ")" | | ||
3150 | * "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | | ||
3151 | * "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | | ||
3152 | * "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | ||
3153 | */ | ||
3154 | public function FunctionsReturningNumerics(): AST\Functions\FunctionNode | ||
3155 | { | ||
3156 | assert($this->lexer->lookahead !== null); | ||
3157 | $funcNameLower = strtolower($this->lexer->lookahead->value); | ||
3158 | $funcClass = self::$numericFunctions[$funcNameLower]; | ||
3159 | |||
3160 | $function = new $funcClass($funcNameLower); | ||
3161 | $function->parse($this); | ||
3162 | |||
3163 | return $function; | ||
3164 | } | ||
3165 | |||
3166 | public function CustomFunctionsReturningNumerics(): AST\Functions\FunctionNode | ||
3167 | { | ||
3168 | assert($this->lexer->lookahead !== null); | ||
3169 | // getCustomNumericFunction is case-insensitive | ||
3170 | $functionName = strtolower($this->lexer->lookahead->value); | ||
3171 | $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName); | ||
3172 | |||
3173 | assert($functionClass !== null); | ||
3174 | |||
3175 | $function = is_string($functionClass) | ||
3176 | ? new $functionClass($functionName) | ||
3177 | : $functionClass($functionName); | ||
3178 | |||
3179 | $function->parse($this); | ||
3180 | |||
3181 | return $function; | ||
3182 | } | ||
3183 | |||
3184 | /** | ||
3185 | * FunctionsReturningDateTime ::= | ||
3186 | * "CURRENT_DATE" | | ||
3187 | * "CURRENT_TIME" | | ||
3188 | * "CURRENT_TIMESTAMP" | | ||
3189 | * "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" | | ||
3190 | * "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" | ||
3191 | */ | ||
3192 | public function FunctionsReturningDatetime(): AST\Functions\FunctionNode | ||
3193 | { | ||
3194 | assert($this->lexer->lookahead !== null); | ||
3195 | $funcNameLower = strtolower($this->lexer->lookahead->value); | ||
3196 | $funcClass = self::$datetimeFunctions[$funcNameLower]; | ||
3197 | |||
3198 | $function = new $funcClass($funcNameLower); | ||
3199 | $function->parse($this); | ||
3200 | |||
3201 | return $function; | ||
3202 | } | ||
3203 | |||
3204 | public function CustomFunctionsReturningDatetime(): AST\Functions\FunctionNode | ||
3205 | { | ||
3206 | assert($this->lexer->lookahead !== null); | ||
3207 | // getCustomDatetimeFunction is case-insensitive | ||
3208 | $functionName = $this->lexer->lookahead->value; | ||
3209 | $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName); | ||
3210 | |||
3211 | assert($functionClass !== null); | ||
3212 | |||
3213 | $function = is_string($functionClass) | ||
3214 | ? new $functionClass($functionName) | ||
3215 | : $functionClass($functionName); | ||
3216 | |||
3217 | $function->parse($this); | ||
3218 | |||
3219 | return $function; | ||
3220 | } | ||
3221 | |||
3222 | /** | ||
3223 | * FunctionsReturningStrings ::= | ||
3224 | * "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" | | ||
3225 | * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | | ||
3226 | * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" | | ||
3227 | * "LOWER" "(" StringPrimary ")" | | ||
3228 | * "UPPER" "(" StringPrimary ")" | | ||
3229 | * "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")" | ||
3230 | */ | ||
3231 | public function FunctionsReturningStrings(): AST\Functions\FunctionNode | ||
3232 | { | ||
3233 | assert($this->lexer->lookahead !== null); | ||
3234 | $funcNameLower = strtolower($this->lexer->lookahead->value); | ||
3235 | $funcClass = self::$stringFunctions[$funcNameLower]; | ||
3236 | |||
3237 | $function = new $funcClass($funcNameLower); | ||
3238 | $function->parse($this); | ||
3239 | |||
3240 | return $function; | ||
3241 | } | ||
3242 | |||
3243 | public function CustomFunctionsReturningStrings(): Functions\FunctionNode | ||
3244 | { | ||
3245 | assert($this->lexer->lookahead !== null); | ||
3246 | // getCustomStringFunction is case-insensitive | ||
3247 | $functionName = $this->lexer->lookahead->value; | ||
3248 | $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName); | ||
3249 | |||
3250 | assert($functionClass !== null); | ||
3251 | |||
3252 | $function = is_string($functionClass) | ||
3253 | ? new $functionClass($functionName) | ||
3254 | : $functionClass($functionName); | ||
3255 | |||
3256 | $function->parse($this); | ||
3257 | |||
3258 | return $function; | ||
3259 | } | ||
3260 | |||
3261 | private function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata | ||
3262 | { | ||
3263 | if (! isset($this->queryComponents[$dqlAlias]['metadata'])) { | ||
3264 | throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); | ||
3265 | } | ||
3266 | |||
3267 | return $this->queryComponents[$dqlAlias]['metadata']; | ||
3268 | } | ||
3269 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/ParserResult.php b/vendor/doctrine/orm/src/Query/ParserResult.php new file mode 100644 index 0000000..8b5ee1f --- /dev/null +++ b/vendor/doctrine/orm/src/Query/ParserResult.php | |||
@@ -0,0 +1,118 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\ORM\Query\Exec\AbstractSqlExecutor; | ||
8 | use LogicException; | ||
9 | |||
10 | use function sprintf; | ||
11 | |||
12 | /** | ||
13 | * Encapsulates the resulting components from a DQL query parsing process that | ||
14 | * can be serialized. | ||
15 | * | ||
16 | * @link http://www.doctrine-project.org | ||
17 | */ | ||
18 | class ParserResult | ||
19 | { | ||
20 | /** | ||
21 | * The SQL executor used for executing the SQL. | ||
22 | */ | ||
23 | private AbstractSqlExecutor|null $sqlExecutor = null; | ||
24 | |||
25 | /** | ||
26 | * The ResultSetMapping that describes how to map the SQL result set. | ||
27 | */ | ||
28 | private ResultSetMapping $resultSetMapping; | ||
29 | |||
30 | /** | ||
31 | * The mappings of DQL parameter names/positions to SQL parameter positions. | ||
32 | * | ||
33 | * @psalm-var array<string|int, list<int>> | ||
34 | */ | ||
35 | private array $parameterMappings = []; | ||
36 | |||
37 | /** | ||
38 | * Initializes a new instance of the <tt>ParserResult</tt> class. | ||
39 | * The new instance is initialized with an empty <tt>ResultSetMapping</tt>. | ||
40 | */ | ||
41 | public function __construct() | ||
42 | { | ||
43 | $this->resultSetMapping = new ResultSetMapping(); | ||
44 | } | ||
45 | |||
46 | /** | ||
47 | * Gets the ResultSetMapping for the parsed query. | ||
48 | * | ||
49 | * @return ResultSetMapping The result set mapping of the parsed query | ||
50 | */ | ||
51 | public function getResultSetMapping(): ResultSetMapping | ||
52 | { | ||
53 | return $this->resultSetMapping; | ||
54 | } | ||
55 | |||
56 | /** | ||
57 | * Sets the ResultSetMapping of the parsed query. | ||
58 | */ | ||
59 | public function setResultSetMapping(ResultSetMapping $rsm): void | ||
60 | { | ||
61 | $this->resultSetMapping = $rsm; | ||
62 | } | ||
63 | |||
64 | /** | ||
65 | * Sets the SQL executor that should be used for this ParserResult. | ||
66 | */ | ||
67 | public function setSqlExecutor(AbstractSqlExecutor $executor): void | ||
68 | { | ||
69 | $this->sqlExecutor = $executor; | ||
70 | } | ||
71 | |||
72 | /** | ||
73 | * Gets the SQL executor used by this ParserResult. | ||
74 | */ | ||
75 | public function getSqlExecutor(): AbstractSqlExecutor | ||
76 | { | ||
77 | if ($this->sqlExecutor === null) { | ||
78 | throw new LogicException(sprintf( | ||
79 | 'Executor not set yet. Call %s::setSqlExecutor() first.', | ||
80 | self::class, | ||
81 | )); | ||
82 | } | ||
83 | |||
84 | return $this->sqlExecutor; | ||
85 | } | ||
86 | |||
87 | /** | ||
88 | * Adds a DQL to SQL parameter mapping. One DQL parameter name/position can map to | ||
89 | * several SQL parameter positions. | ||
90 | */ | ||
91 | public function addParameterMapping(string|int $dqlPosition, int $sqlPosition): void | ||
92 | { | ||
93 | $this->parameterMappings[$dqlPosition][] = $sqlPosition; | ||
94 | } | ||
95 | |||
96 | /** | ||
97 | * Gets all DQL to SQL parameter mappings. | ||
98 | * | ||
99 | * @psalm-return array<int|string, list<int>> The parameter mappings. | ||
100 | */ | ||
101 | public function getParameterMappings(): array | ||
102 | { | ||
103 | return $this->parameterMappings; | ||
104 | } | ||
105 | |||
106 | /** | ||
107 | * Gets the SQL parameter positions for a DQL parameter name/position. | ||
108 | * | ||
109 | * @param string|int $dqlPosition The name or position of the DQL parameter. | ||
110 | * | ||
111 | * @return int[] The positions of the corresponding SQL parameters. | ||
112 | * @psalm-return list<int> | ||
113 | */ | ||
114 | public function getSqlParameterPositions(string|int $dqlPosition): array | ||
115 | { | ||
116 | return $this->parameterMappings[$dqlPosition]; | ||
117 | } | ||
118 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/Printer.php b/vendor/doctrine/orm/src/Query/Printer.php new file mode 100644 index 0000000..db1f159 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Printer.php | |||
@@ -0,0 +1,64 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use function str_repeat; | ||
8 | |||
9 | /** | ||
10 | * A parse tree printer for Doctrine Query Language parser. | ||
11 | * | ||
12 | * @link http://www.phpdoctrine.org | ||
13 | */ | ||
14 | class Printer | ||
15 | { | ||
16 | /** Current indentation level */ | ||
17 | protected int $indent = 0; | ||
18 | |||
19 | /** | ||
20 | * Constructs a new parse tree printer. | ||
21 | * | ||
22 | * @param bool $silent Parse tree will not be printed if true. | ||
23 | */ | ||
24 | public function __construct(protected bool $silent = false) | ||
25 | { | ||
26 | } | ||
27 | |||
28 | /** | ||
29 | * Prints an opening parenthesis followed by production name and increases | ||
30 | * indentation level by one. | ||
31 | * | ||
32 | * This method is called before executing a production. | ||
33 | * | ||
34 | * @param string $name Production name. | ||
35 | */ | ||
36 | public function startProduction(string $name): void | ||
37 | { | ||
38 | $this->println('(' . $name); | ||
39 | $this->indent++; | ||
40 | } | ||
41 | |||
42 | /** | ||
43 | * Decreases indentation level by one and prints a closing parenthesis. | ||
44 | * | ||
45 | * This method is called after executing a production. | ||
46 | */ | ||
47 | public function endProduction(): void | ||
48 | { | ||
49 | $this->indent--; | ||
50 | $this->println(')'); | ||
51 | } | ||
52 | |||
53 | /** | ||
54 | * Prints text indented with spaces depending on current indentation level. | ||
55 | * | ||
56 | * @param string $str The text. | ||
57 | */ | ||
58 | public function println(string $str): void | ||
59 | { | ||
60 | if (! $this->silent) { | ||
61 | echo str_repeat(' ', $this->indent), $str, "\n"; | ||
62 | } | ||
63 | } | ||
64 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/QueryException.php b/vendor/doctrine/orm/src/Query/QueryException.php new file mode 100644 index 0000000..ae945b1 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/QueryException.php | |||
@@ -0,0 +1,155 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\ORM\Exception\ORMException; | ||
8 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
9 | use Doctrine\ORM\Query\AST\PathExpression; | ||
10 | use Exception; | ||
11 | use Stringable; | ||
12 | use Throwable; | ||
13 | |||
14 | class QueryException extends Exception implements ORMException | ||
15 | { | ||
16 | public static function dqlError(string $dql): self | ||
17 | { | ||
18 | return new self($dql); | ||
19 | } | ||
20 | |||
21 | public static function syntaxError(string $message, Throwable|null $previous = null): self | ||
22 | { | ||
23 | return new self('[Syntax Error] ' . $message, 0, $previous); | ||
24 | } | ||
25 | |||
26 | public static function semanticalError(string $message, Throwable|null $previous = null): self | ||
27 | { | ||
28 | return new self('[Semantical Error] ' . $message, 0, $previous); | ||
29 | } | ||
30 | |||
31 | public static function invalidLockMode(): self | ||
32 | { | ||
33 | return new self('Invalid lock mode hint provided.'); | ||
34 | } | ||
35 | |||
36 | public static function invalidParameterType(string $expected, string $received): self | ||
37 | { | ||
38 | return new self('Invalid parameter type, ' . $received . ' given, but ' . $expected . ' expected.'); | ||
39 | } | ||
40 | |||
41 | public static function invalidParameterPosition(string $pos): self | ||
42 | { | ||
43 | return new self('Invalid parameter position: ' . $pos); | ||
44 | } | ||
45 | |||
46 | public static function tooManyParameters(int $expected, int $received): self | ||
47 | { | ||
48 | return new self('Too many parameters: the query defines ' . $expected . ' parameters and you bound ' . $received); | ||
49 | } | ||
50 | |||
51 | public static function tooFewParameters(int $expected, int $received): self | ||
52 | { | ||
53 | return new self('Too few parameters: the query defines ' . $expected . ' parameters but you only bound ' . $received); | ||
54 | } | ||
55 | |||
56 | public static function invalidParameterFormat(string $value): self | ||
57 | { | ||
58 | return new self('Invalid parameter format, ' . $value . ' given, but :<name> or ?<num> expected.'); | ||
59 | } | ||
60 | |||
61 | public static function unknownParameter(string $key): self | ||
62 | { | ||
63 | return new self('Invalid parameter: token ' . $key . ' is not defined in the query.'); | ||
64 | } | ||
65 | |||
66 | public static function parameterTypeMismatch(): self | ||
67 | { | ||
68 | return new self('DQL Query parameter and type numbers mismatch, but have to be exactly equal.'); | ||
69 | } | ||
70 | |||
71 | public static function invalidPathExpression(PathExpression $pathExpr): self | ||
72 | { | ||
73 | return new self( | ||
74 | "Invalid PathExpression '" . $pathExpr->identificationVariable . '.' . $pathExpr->field . "'.", | ||
75 | ); | ||
76 | } | ||
77 | |||
78 | public static function invalidLiteral(string|Stringable $literal): self | ||
79 | { | ||
80 | return new self("Invalid literal '" . $literal . "'"); | ||
81 | } | ||
82 | |||
83 | public static function iterateWithFetchJoinCollectionNotAllowed(AssociationMapping $assoc): self | ||
84 | { | ||
85 | return new self( | ||
86 | 'Invalid query operation: Not allowed to iterate over fetch join collections ' . | ||
87 | 'in class ' . $assoc->sourceEntity . ' association ' . $assoc->fieldName, | ||
88 | ); | ||
89 | } | ||
90 | |||
91 | /** | ||
92 | * @param string[] $assoc | ||
93 | * @psalm-param array<string, string> $assoc | ||
94 | */ | ||
95 | public static function overwritingJoinConditionsNotYetSupported(array $assoc): self | ||
96 | { | ||
97 | return new self( | ||
98 | 'Unsupported query operation: It is not yet possible to overwrite the join ' . | ||
99 | 'conditions in class ' . $assoc['sourceEntityName'] . ' association ' . $assoc['fieldName'] . '. ' . | ||
100 | 'Use WITH to append additional join conditions to the association.', | ||
101 | ); | ||
102 | } | ||
103 | |||
104 | public static function associationPathInverseSideNotSupported(PathExpression $pathExpr): self | ||
105 | { | ||
106 | return new self( | ||
107 | 'A single-valued association path expression to an inverse side is not supported in DQL queries. ' . | ||
108 | 'Instead of "' . $pathExpr->identificationVariable . '.' . $pathExpr->field . '" use an explicit join.', | ||
109 | ); | ||
110 | } | ||
111 | |||
112 | public static function iterateWithFetchJoinNotAllowed(AssociationMapping $assoc): self | ||
113 | { | ||
114 | return new self( | ||
115 | 'Iterate with fetch join in class ' . $assoc->sourceEntity . | ||
116 | ' using association ' . $assoc->fieldName . ' not allowed.', | ||
117 | ); | ||
118 | } | ||
119 | |||
120 | public static function eagerFetchJoinWithNotAllowed(string $sourceEntity, string $fieldName): self | ||
121 | { | ||
122 | return new self( | ||
123 | 'Associations with fetch-mode=EAGER may not be using WITH conditions in | ||
124 | "' . $sourceEntity . '#' . $fieldName . '".', | ||
125 | ); | ||
126 | } | ||
127 | |||
128 | public static function iterateWithMixedResultNotAllowed(): self | ||
129 | { | ||
130 | return new self('Iterating a query with mixed results (using scalars) is not supported.'); | ||
131 | } | ||
132 | |||
133 | public static function associationPathCompositeKeyNotSupported(): self | ||
134 | { | ||
135 | return new self( | ||
136 | 'A single-valued association path expression to an entity with a composite primary ' . | ||
137 | 'key is not supported. Explicitly name the components of the composite primary key ' . | ||
138 | 'in the query.', | ||
139 | ); | ||
140 | } | ||
141 | |||
142 | public static function instanceOfUnrelatedClass(string $className, string $rootClass): self | ||
143 | { | ||
144 | return new self("Cannot check if a child of '" . $rootClass . "' is instanceof '" . $className . "', " . | ||
145 | 'inheritance hierarchy does not exists between these two classes.'); | ||
146 | } | ||
147 | |||
148 | public static function invalidQueryComponent(string $dqlAlias): self | ||
149 | { | ||
150 | return new self( | ||
151 | "Invalid query component given for DQL alias '" . $dqlAlias . "', " . | ||
152 | "requires 'metadata', 'parent', 'relation', 'map', 'nestingLevel' and 'token' keys.", | ||
153 | ); | ||
154 | } | ||
155 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php b/vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php new file mode 100644 index 0000000..3e0ec65 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/QueryExpressionVisitor.php | |||
@@ -0,0 +1,180 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\Common\Collections\ArrayCollection; | ||
8 | use Doctrine\Common\Collections\Expr\Comparison; | ||
9 | use Doctrine\Common\Collections\Expr\CompositeExpression; | ||
10 | use Doctrine\Common\Collections\Expr\ExpressionVisitor; | ||
11 | use Doctrine\Common\Collections\Expr\Value; | ||
12 | use RuntimeException; | ||
13 | |||
14 | use function count; | ||
15 | use function str_replace; | ||
16 | use function str_starts_with; | ||
17 | |||
18 | /** | ||
19 | * Converts Collection expressions to Query expressions. | ||
20 | */ | ||
21 | class QueryExpressionVisitor extends ExpressionVisitor | ||
22 | { | ||
23 | private const OPERATOR_MAP = [ | ||
24 | Comparison::GT => Expr\Comparison::GT, | ||
25 | Comparison::GTE => Expr\Comparison::GTE, | ||
26 | Comparison::LT => Expr\Comparison::LT, | ||
27 | Comparison::LTE => Expr\Comparison::LTE, | ||
28 | ]; | ||
29 | |||
30 | private readonly Expr $expr; | ||
31 | |||
32 | /** @var list<mixed> */ | ||
33 | private array $parameters = []; | ||
34 | |||
35 | /** @param mixed[] $queryAliases */ | ||
36 | public function __construct( | ||
37 | private readonly array $queryAliases, | ||
38 | ) { | ||
39 | $this->expr = new Expr(); | ||
40 | } | ||
41 | |||
42 | /** | ||
43 | * Gets bound parameters. | ||
44 | * Filled after {@link dispach()}. | ||
45 | * | ||
46 | * @return ArrayCollection<int, mixed> | ||
47 | */ | ||
48 | public function getParameters(): ArrayCollection | ||
49 | { | ||
50 | return new ArrayCollection($this->parameters); | ||
51 | } | ||
52 | |||
53 | public function clearParameters(): void | ||
54 | { | ||
55 | $this->parameters = []; | ||
56 | } | ||
57 | |||
58 | /** | ||
59 | * Converts Criteria expression to Query one based on static map. | ||
60 | */ | ||
61 | private static function convertComparisonOperator(string $criteriaOperator): string|null | ||
62 | { | ||
63 | return self::OPERATOR_MAP[$criteriaOperator] ?? null; | ||
64 | } | ||
65 | |||
66 | public function walkCompositeExpression(CompositeExpression $expr): mixed | ||
67 | { | ||
68 | $expressionList = []; | ||
69 | |||
70 | foreach ($expr->getExpressionList() as $child) { | ||
71 | $expressionList[] = $this->dispatch($child); | ||
72 | } | ||
73 | |||
74 | return match ($expr->getType()) { | ||
75 | CompositeExpression::TYPE_AND => new Expr\Andx($expressionList), | ||
76 | CompositeExpression::TYPE_OR => new Expr\Orx($expressionList), | ||
77 | CompositeExpression::TYPE_NOT => $this->expr->not($expressionList[0]), | ||
78 | default => throw new RuntimeException('Unknown composite ' . $expr->getType()), | ||
79 | }; | ||
80 | } | ||
81 | |||
82 | public function walkComparison(Comparison $comparison): mixed | ||
83 | { | ||
84 | if (! isset($this->queryAliases[0])) { | ||
85 | throw new QueryException('No aliases are set before invoking walkComparison().'); | ||
86 | } | ||
87 | |||
88 | $field = $this->queryAliases[0] . '.' . $comparison->getField(); | ||
89 | |||
90 | foreach ($this->queryAliases as $alias) { | ||
91 | if (str_starts_with($comparison->getField() . '.', $alias . '.')) { | ||
92 | $field = $comparison->getField(); | ||
93 | break; | ||
94 | } | ||
95 | } | ||
96 | |||
97 | $parameterName = str_replace('.', '_', $comparison->getField()); | ||
98 | |||
99 | foreach ($this->parameters as $parameter) { | ||
100 | if ($parameter->getName() === $parameterName) { | ||
101 | $parameterName .= '_' . count($this->parameters); | ||
102 | break; | ||
103 | } | ||
104 | } | ||
105 | |||
106 | $parameter = new Parameter($parameterName, $this->walkValue($comparison->getValue())); | ||
107 | $placeholder = ':' . $parameterName; | ||
108 | |||
109 | switch ($comparison->getOperator()) { | ||
110 | case Comparison::IN: | ||
111 | $this->parameters[] = $parameter; | ||
112 | |||
113 | return $this->expr->in($field, $placeholder); | ||
114 | |||
115 | case Comparison::NIN: | ||
116 | $this->parameters[] = $parameter; | ||
117 | |||
118 | return $this->expr->notIn($field, $placeholder); | ||
119 | |||
120 | case Comparison::EQ: | ||
121 | case Comparison::IS: | ||
122 | if ($this->walkValue($comparison->getValue()) === null) { | ||
123 | return $this->expr->isNull($field); | ||
124 | } | ||
125 | |||
126 | $this->parameters[] = $parameter; | ||
127 | |||
128 | return $this->expr->eq($field, $placeholder); | ||
129 | |||
130 | case Comparison::NEQ: | ||
131 | if ($this->walkValue($comparison->getValue()) === null) { | ||
132 | return $this->expr->isNotNull($field); | ||
133 | } | ||
134 | |||
135 | $this->parameters[] = $parameter; | ||
136 | |||
137 | return $this->expr->neq($field, $placeholder); | ||
138 | |||
139 | case Comparison::CONTAINS: | ||
140 | $parameter->setValue('%' . $parameter->getValue() . '%', $parameter->getType()); | ||
141 | $this->parameters[] = $parameter; | ||
142 | |||
143 | return $this->expr->like($field, $placeholder); | ||
144 | |||
145 | case Comparison::MEMBER_OF: | ||
146 | return $this->expr->isMemberOf($comparison->getField(), $comparison->getValue()->getValue()); | ||
147 | |||
148 | case Comparison::STARTS_WITH: | ||
149 | $parameter->setValue($parameter->getValue() . '%', $parameter->getType()); | ||
150 | $this->parameters[] = $parameter; | ||
151 | |||
152 | return $this->expr->like($field, $placeholder); | ||
153 | |||
154 | case Comparison::ENDS_WITH: | ||
155 | $parameter->setValue('%' . $parameter->getValue(), $parameter->getType()); | ||
156 | $this->parameters[] = $parameter; | ||
157 | |||
158 | return $this->expr->like($field, $placeholder); | ||
159 | |||
160 | default: | ||
161 | $operator = self::convertComparisonOperator($comparison->getOperator()); | ||
162 | if ($operator) { | ||
163 | $this->parameters[] = $parameter; | ||
164 | |||
165 | return new Expr\Comparison( | ||
166 | $field, | ||
167 | $operator, | ||
168 | $placeholder, | ||
169 | ); | ||
170 | } | ||
171 | |||
172 | throw new RuntimeException('Unknown comparison operator: ' . $comparison->getOperator()); | ||
173 | } | ||
174 | } | ||
175 | |||
176 | public function walkValue(Value $value): mixed | ||
177 | { | ||
178 | return $value->getValue(); | ||
179 | } | ||
180 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/ResultSetMapping.php b/vendor/doctrine/orm/src/Query/ResultSetMapping.php new file mode 100644 index 0000000..612474d --- /dev/null +++ b/vendor/doctrine/orm/src/Query/ResultSetMapping.php | |||
@@ -0,0 +1,547 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use function count; | ||
8 | |||
9 | /** | ||
10 | * A ResultSetMapping describes how a result set of an SQL query maps to a Doctrine result. | ||
11 | * | ||
12 | * IMPORTANT NOTE: | ||
13 | * The properties of this class are only public for fast internal READ access and to (drastically) | ||
14 | * reduce the size of serialized instances for more effective caching due to better (un-)serialization | ||
15 | * performance. | ||
16 | * | ||
17 | * <b>Users should use the public methods.</b> | ||
18 | * | ||
19 | * @todo Think about whether the number of lookup maps can be reduced. | ||
20 | */ | ||
21 | class ResultSetMapping | ||
22 | { | ||
23 | /** | ||
24 | * Whether the result is mixed (contains scalar values together with field values). | ||
25 | * | ||
26 | * @ignore | ||
27 | */ | ||
28 | public bool $isMixed = false; | ||
29 | |||
30 | /** | ||
31 | * Whether the result is a select statement. | ||
32 | * | ||
33 | * @ignore | ||
34 | */ | ||
35 | public bool $isSelect = true; | ||
36 | |||
37 | /** | ||
38 | * Maps alias names to class names. | ||
39 | * | ||
40 | * @ignore | ||
41 | * @psalm-var array<string, class-string> | ||
42 | */ | ||
43 | public array $aliasMap = []; | ||
44 | |||
45 | /** | ||
46 | * Maps alias names to related association field names. | ||
47 | * | ||
48 | * @ignore | ||
49 | * @psalm-var array<string, string> | ||
50 | */ | ||
51 | public array $relationMap = []; | ||
52 | |||
53 | /** | ||
54 | * Maps alias names to parent alias names. | ||
55 | * | ||
56 | * @ignore | ||
57 | * @psalm-var array<string, string> | ||
58 | */ | ||
59 | public array $parentAliasMap = []; | ||
60 | |||
61 | /** | ||
62 | * Maps column names in the result set to field names for each class. | ||
63 | * | ||
64 | * @ignore | ||
65 | * @psalm-var array<string, string> | ||
66 | */ | ||
67 | public array $fieldMappings = []; | ||
68 | |||
69 | /** | ||
70 | * Maps column names in the result set to the alias/field name to use in the mapped result. | ||
71 | * | ||
72 | * @ignore | ||
73 | * @psalm-var array<string, string|int> | ||
74 | */ | ||
75 | public array $scalarMappings = []; | ||
76 | |||
77 | /** | ||
78 | * Maps scalar columns to enums | ||
79 | * | ||
80 | * @ignore | ||
81 | * @psalm-var array<string, string> | ||
82 | */ | ||
83 | public $enumMappings = []; | ||
84 | |||
85 | /** | ||
86 | * Maps column names in the result set to the alias/field type to use in the mapped result. | ||
87 | * | ||
88 | * @ignore | ||
89 | * @psalm-var array<string, string> | ||
90 | */ | ||
91 | public array $typeMappings = []; | ||
92 | |||
93 | /** | ||
94 | * Maps entities in the result set to the alias name to use in the mapped result. | ||
95 | * | ||
96 | * @ignore | ||
97 | * @psalm-var array<string, string|null> | ||
98 | */ | ||
99 | public array $entityMappings = []; | ||
100 | |||
101 | /** | ||
102 | * Maps column names of meta columns (foreign keys, discriminator columns, ...) to field names. | ||
103 | * | ||
104 | * @ignore | ||
105 | * @psalm-var array<string, string> | ||
106 | */ | ||
107 | public array $metaMappings = []; | ||
108 | |||
109 | /** | ||
110 | * Maps column names in the result set to the alias they belong to. | ||
111 | * | ||
112 | * @ignore | ||
113 | * @psalm-var array<string, string> | ||
114 | */ | ||
115 | public array $columnOwnerMap = []; | ||
116 | |||
117 | /** | ||
118 | * List of columns in the result set that are used as discriminator columns. | ||
119 | * | ||
120 | * @ignore | ||
121 | * @psalm-var array<string, string> | ||
122 | */ | ||
123 | public array $discriminatorColumns = []; | ||
124 | |||
125 | /** | ||
126 | * Maps alias names to field names that should be used for indexing. | ||
127 | * | ||
128 | * @ignore | ||
129 | * @psalm-var array<string, string> | ||
130 | */ | ||
131 | public array $indexByMap = []; | ||
132 | |||
133 | /** | ||
134 | * Map from column names to class names that declare the field the column is mapped to. | ||
135 | * | ||
136 | * @ignore | ||
137 | * @psalm-var array<string, class-string> | ||
138 | */ | ||
139 | public array $declaringClasses = []; | ||
140 | |||
141 | /** | ||
142 | * This is necessary to hydrate derivate foreign keys correctly. | ||
143 | * | ||
144 | * @psalm-var array<string, array<string, bool>> | ||
145 | */ | ||
146 | public array $isIdentifierColumn = []; | ||
147 | |||
148 | /** | ||
149 | * Maps column names in the result set to field names for each new object expression. | ||
150 | * | ||
151 | * @psalm-var array<string, array<string, mixed>> | ||
152 | */ | ||
153 | public array $newObjectMappings = []; | ||
154 | |||
155 | /** | ||
156 | * Maps metadata parameter names to the metadata attribute. | ||
157 | * | ||
158 | * @psalm-var array<int|string, string> | ||
159 | */ | ||
160 | public array $metadataParameterMapping = []; | ||
161 | |||
162 | /** | ||
163 | * Contains query parameter names to be resolved as discriminator values | ||
164 | * | ||
165 | * @psalm-var array<string, string> | ||
166 | */ | ||
167 | public array $discriminatorParameters = []; | ||
168 | |||
169 | /** | ||
170 | * Adds an entity result to this ResultSetMapping. | ||
171 | * | ||
172 | * @param string $class The class name of the entity. | ||
173 | * @param string $alias The alias for the class. The alias must be unique among all entity | ||
174 | * results or joined entity results within this ResultSetMapping. | ||
175 | * @param string|null $resultAlias The result alias with which the entity result should be | ||
176 | * placed in the result structure. | ||
177 | * @psalm-param class-string $class | ||
178 | * | ||
179 | * @return $this | ||
180 | * | ||
181 | * @todo Rename: addRootEntity | ||
182 | */ | ||
183 | public function addEntityResult(string $class, string $alias, string|null $resultAlias = null): static | ||
184 | { | ||
185 | $this->aliasMap[$alias] = $class; | ||
186 | $this->entityMappings[$alias] = $resultAlias; | ||
187 | |||
188 | if ($resultAlias !== null) { | ||
189 | $this->isMixed = true; | ||
190 | } | ||
191 | |||
192 | return $this; | ||
193 | } | ||
194 | |||
195 | /** | ||
196 | * Sets a discriminator column for an entity result or joined entity result. | ||
197 | * The discriminator column will be used to determine the concrete class name to | ||
198 | * instantiate. | ||
199 | * | ||
200 | * @param string $alias The alias of the entity result or joined entity result the discriminator | ||
201 | * column should be used for. | ||
202 | * @param string $discrColumn The name of the discriminator column in the SQL result set. | ||
203 | * | ||
204 | * @return $this | ||
205 | * | ||
206 | * @todo Rename: addDiscriminatorColumn | ||
207 | */ | ||
208 | public function setDiscriminatorColumn(string $alias, string $discrColumn): static | ||
209 | { | ||
210 | $this->discriminatorColumns[$alias] = $discrColumn; | ||
211 | $this->columnOwnerMap[$discrColumn] = $alias; | ||
212 | |||
213 | return $this; | ||
214 | } | ||
215 | |||
216 | /** | ||
217 | * Sets a field to use for indexing an entity result or joined entity result. | ||
218 | * | ||
219 | * @param string $alias The alias of an entity result or joined entity result. | ||
220 | * @param string $fieldName The name of the field to use for indexing. | ||
221 | * | ||
222 | * @return $this | ||
223 | */ | ||
224 | public function addIndexBy(string $alias, string $fieldName): static | ||
225 | { | ||
226 | $found = false; | ||
227 | |||
228 | foreach ([...$this->metaMappings, ...$this->fieldMappings] as $columnName => $columnFieldName) { | ||
229 | if (! ($columnFieldName === $fieldName && $this->columnOwnerMap[$columnName] === $alias)) { | ||
230 | continue; | ||
231 | } | ||
232 | |||
233 | $this->addIndexByColumn($alias, $columnName); | ||
234 | $found = true; | ||
235 | |||
236 | break; | ||
237 | } | ||
238 | |||
239 | /* TODO: check if this exception can be put back, for now it's gone because of assumptions made by some ORM internals | ||
240 | if ( ! $found) { | ||
241 | $message = sprintf( | ||
242 | 'Cannot add index by for DQL alias %s and field %s without calling addFieldResult() for them before.', | ||
243 | $alias, | ||
244 | $fieldName | ||
245 | ); | ||
246 | |||
247 | throw new \LogicException($message); | ||
248 | } | ||
249 | */ | ||
250 | |||
251 | return $this; | ||
252 | } | ||
253 | |||
254 | /** | ||
255 | * Sets to index by a scalar result column name. | ||
256 | * | ||
257 | * @return $this | ||
258 | */ | ||
259 | public function addIndexByScalar(string $resultColumnName): static | ||
260 | { | ||
261 | $this->indexByMap['scalars'] = $resultColumnName; | ||
262 | |||
263 | return $this; | ||
264 | } | ||
265 | |||
266 | /** | ||
267 | * Sets a column to use for indexing an entity or joined entity result by the given alias name. | ||
268 | * | ||
269 | * @return $this | ||
270 | */ | ||
271 | public function addIndexByColumn(string $alias, string $resultColumnName): static | ||
272 | { | ||
273 | $this->indexByMap[$alias] = $resultColumnName; | ||
274 | |||
275 | return $this; | ||
276 | } | ||
277 | |||
278 | /** | ||
279 | * Checks whether an entity result or joined entity result with a given alias has | ||
280 | * a field set for indexing. | ||
281 | * | ||
282 | * @todo Rename: isIndexed($alias) | ||
283 | */ | ||
284 | public function hasIndexBy(string $alias): bool | ||
285 | { | ||
286 | return isset($this->indexByMap[$alias]); | ||
287 | } | ||
288 | |||
289 | /** | ||
290 | * Checks whether the column with the given name is mapped as a field result | ||
291 | * as part of an entity result or joined entity result. | ||
292 | * | ||
293 | * @param string $columnName The name of the column in the SQL result set. | ||
294 | * | ||
295 | * @todo Rename: isField | ||
296 | */ | ||
297 | public function isFieldResult(string $columnName): bool | ||
298 | { | ||
299 | return isset($this->fieldMappings[$columnName]); | ||
300 | } | ||
301 | |||
302 | /** | ||
303 | * Adds a field to the result that belongs to an entity or joined entity. | ||
304 | * | ||
305 | * @param string $alias The alias of the root entity or joined entity to which the field belongs. | ||
306 | * @param string $columnName The name of the column in the SQL result set. | ||
307 | * @param string $fieldName The name of the field on the declaring class. | ||
308 | * @param string|null $declaringClass The name of the class that declares/owns the specified field. | ||
309 | * When $alias refers to a superclass in a mapped hierarchy but | ||
310 | * the field $fieldName is defined on a subclass, specify that here. | ||
311 | * If not specified, the field is assumed to belong to the class | ||
312 | * designated by $alias. | ||
313 | * @psalm-param class-string|null $declaringClass | ||
314 | * | ||
315 | * @return $this | ||
316 | * | ||
317 | * @todo Rename: addField | ||
318 | */ | ||
319 | public function addFieldResult(string $alias, string $columnName, string $fieldName, string|null $declaringClass = null): static | ||
320 | { | ||
321 | // column name (in result set) => field name | ||
322 | $this->fieldMappings[$columnName] = $fieldName; | ||
323 | // column name => alias of owner | ||
324 | $this->columnOwnerMap[$columnName] = $alias; | ||
325 | // field name => class name of declaring class | ||
326 | $this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias]; | ||
327 | |||
328 | if (! $this->isMixed && $this->scalarMappings) { | ||
329 | $this->isMixed = true; | ||
330 | } | ||
331 | |||
332 | return $this; | ||
333 | } | ||
334 | |||
335 | /** | ||
336 | * Adds a joined entity result. | ||
337 | * | ||
338 | * @param string $class The class name of the joined entity. | ||
339 | * @param string $alias The unique alias to use for the joined entity. | ||
340 | * @param string $parentAlias The alias of the entity result that is the parent of this joined result. | ||
341 | * @param string $relation The association field that connects the parent entity result | ||
342 | * with the joined entity result. | ||
343 | * @psalm-param class-string $class | ||
344 | * | ||
345 | * @return $this | ||
346 | * | ||
347 | * @todo Rename: addJoinedEntity | ||
348 | */ | ||
349 | public function addJoinedEntityResult(string $class, string $alias, string $parentAlias, string $relation): static | ||
350 | { | ||
351 | $this->aliasMap[$alias] = $class; | ||
352 | $this->parentAliasMap[$alias] = $parentAlias; | ||
353 | $this->relationMap[$alias] = $relation; | ||
354 | |||
355 | return $this; | ||
356 | } | ||
357 | |||
358 | /** | ||
359 | * Adds a scalar result mapping. | ||
360 | * | ||
361 | * @param string $columnName The name of the column in the SQL result set. | ||
362 | * @param string|int $alias The result alias with which the scalar result should be placed in the result structure. | ||
363 | * @param string $type The column type | ||
364 | * | ||
365 | * @return $this | ||
366 | * | ||
367 | * @todo Rename: addScalar | ||
368 | */ | ||
369 | public function addScalarResult(string $columnName, string|int $alias, string $type = 'string'): static | ||
370 | { | ||
371 | $this->scalarMappings[$columnName] = $alias; | ||
372 | $this->typeMappings[$columnName] = $type; | ||
373 | |||
374 | if (! $this->isMixed && $this->fieldMappings) { | ||
375 | $this->isMixed = true; | ||
376 | } | ||
377 | |||
378 | return $this; | ||
379 | } | ||
380 | |||
381 | /** | ||
382 | * Adds a scalar result mapping. | ||
383 | * | ||
384 | * @param string $columnName The name of the column in the SQL result set. | ||
385 | * @param string $enumType The enum type | ||
386 | * | ||
387 | * @return $this | ||
388 | */ | ||
389 | public function addEnumResult(string $columnName, string $enumType): static | ||
390 | { | ||
391 | $this->enumMappings[$columnName] = $enumType; | ||
392 | |||
393 | return $this; | ||
394 | } | ||
395 | |||
396 | /** | ||
397 | * Adds a metadata parameter mappings. | ||
398 | */ | ||
399 | public function addMetadataParameterMapping(string|int $parameter, string $attribute): void | ||
400 | { | ||
401 | $this->metadataParameterMapping[$parameter] = $attribute; | ||
402 | } | ||
403 | |||
404 | /** | ||
405 | * Checks whether a column with a given name is mapped as a scalar result. | ||
406 | * | ||
407 | * @todo Rename: isScalar | ||
408 | */ | ||
409 | public function isScalarResult(string $columnName): bool | ||
410 | { | ||
411 | return isset($this->scalarMappings[$columnName]); | ||
412 | } | ||
413 | |||
414 | /** | ||
415 | * Gets the name of the class of an entity result or joined entity result, | ||
416 | * identified by the given unique alias. | ||
417 | * | ||
418 | * @psalm-return class-string | ||
419 | */ | ||
420 | public function getClassName(string $alias): string | ||
421 | { | ||
422 | return $this->aliasMap[$alias]; | ||
423 | } | ||
424 | |||
425 | /** | ||
426 | * Gets the field alias for a column that is mapped as a scalar value. | ||
427 | * | ||
428 | * @param string $columnName The name of the column in the SQL result set. | ||
429 | */ | ||
430 | public function getScalarAlias(string $columnName): string|int | ||
431 | { | ||
432 | return $this->scalarMappings[$columnName]; | ||
433 | } | ||
434 | |||
435 | /** | ||
436 | * Gets the name of the class that owns a field mapping for the specified column. | ||
437 | * | ||
438 | * @psalm-return class-string | ||
439 | */ | ||
440 | public function getDeclaringClass(string $columnName): string | ||
441 | { | ||
442 | return $this->declaringClasses[$columnName]; | ||
443 | } | ||
444 | |||
445 | public function getRelation(string $alias): string | ||
446 | { | ||
447 | return $this->relationMap[$alias]; | ||
448 | } | ||
449 | |||
450 | public function isRelation(string $alias): bool | ||
451 | { | ||
452 | return isset($this->relationMap[$alias]); | ||
453 | } | ||
454 | |||
455 | /** | ||
456 | * Gets the alias of the class that owns a field mapping for the specified column. | ||
457 | */ | ||
458 | public function getEntityAlias(string $columnName): string | ||
459 | { | ||
460 | return $this->columnOwnerMap[$columnName]; | ||
461 | } | ||
462 | |||
463 | /** | ||
464 | * Gets the parent alias of the given alias. | ||
465 | */ | ||
466 | public function getParentAlias(string $alias): string | ||
467 | { | ||
468 | return $this->parentAliasMap[$alias]; | ||
469 | } | ||
470 | |||
471 | /** | ||
472 | * Checks whether the given alias has a parent alias. | ||
473 | */ | ||
474 | public function hasParentAlias(string $alias): bool | ||
475 | { | ||
476 | return isset($this->parentAliasMap[$alias]); | ||
477 | } | ||
478 | |||
479 | /** | ||
480 | * Gets the field name for a column name. | ||
481 | */ | ||
482 | public function getFieldName(string $columnName): string | ||
483 | { | ||
484 | return $this->fieldMappings[$columnName]; | ||
485 | } | ||
486 | |||
487 | /** @psalm-return array<string, class-string> */ | ||
488 | public function getAliasMap(): array | ||
489 | { | ||
490 | return $this->aliasMap; | ||
491 | } | ||
492 | |||
493 | /** | ||
494 | * Gets the number of different entities that appear in the mapped result. | ||
495 | * | ||
496 | * @psalm-return 0|positive-int | ||
497 | */ | ||
498 | public function getEntityResultCount(): int | ||
499 | { | ||
500 | return count($this->aliasMap); | ||
501 | } | ||
502 | |||
503 | /** | ||
504 | * Checks whether this ResultSetMapping defines a mixed result. | ||
505 | * | ||
506 | * Mixed results can only occur in object and array (graph) hydration. In such a | ||
507 | * case a mixed result means that scalar values are mixed with objects/array in | ||
508 | * the result. | ||
509 | */ | ||
510 | public function isMixedResult(): bool | ||
511 | { | ||
512 | return $this->isMixed; | ||
513 | } | ||
514 | |||
515 | /** | ||
516 | * Adds a meta column (foreign key or discriminator column) to the result set. | ||
517 | * | ||
518 | * @param string $alias The result alias with which the meta result should be placed in the result structure. | ||
519 | * @param string $columnName The name of the column in the SQL result set. | ||
520 | * @param string $fieldName The name of the field on the declaring class. | ||
521 | * @param string|null $type The column type | ||
522 | * | ||
523 | * @return $this | ||
524 | * | ||
525 | * @todo Make all methods of this class require all parameters and not infer anything | ||
526 | */ | ||
527 | public function addMetaResult( | ||
528 | string $alias, | ||
529 | string $columnName, | ||
530 | string $fieldName, | ||
531 | bool $isIdentifierColumn = false, | ||
532 | string|null $type = null, | ||
533 | ): static { | ||
534 | $this->metaMappings[$columnName] = $fieldName; | ||
535 | $this->columnOwnerMap[$columnName] = $alias; | ||
536 | |||
537 | if ($isIdentifierColumn) { | ||
538 | $this->isIdentifierColumn[$alias][$columnName] = true; | ||
539 | } | ||
540 | |||
541 | if ($type) { | ||
542 | $this->typeMappings[$columnName] = $type; | ||
543 | } | ||
544 | |||
545 | return $this; | ||
546 | } | ||
547 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php b/vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php new file mode 100644 index 0000000..f28f3a9 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/ResultSetMappingBuilder.php | |||
@@ -0,0 +1,281 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\DBAL\Types\Type; | ||
8 | use Doctrine\ORM\EntityManagerInterface; | ||
9 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
11 | use Doctrine\ORM\Utility\PersisterHelper; | ||
12 | use InvalidArgumentException; | ||
13 | use Stringable; | ||
14 | |||
15 | use function in_array; | ||
16 | use function sprintf; | ||
17 | |||
18 | /** | ||
19 | * A ResultSetMappingBuilder uses the EntityManager to automatically populate entity fields. | ||
20 | */ | ||
21 | class ResultSetMappingBuilder extends ResultSetMapping implements Stringable | ||
22 | { | ||
23 | use SQLResultCasing; | ||
24 | |||
25 | /** | ||
26 | * Picking this rename mode will register entity columns as is, | ||
27 | * as they are in the database. This can cause clashes when multiple | ||
28 | * entities are fetched that have columns with the same name. | ||
29 | */ | ||
30 | public const COLUMN_RENAMING_NONE = 1; | ||
31 | |||
32 | /** | ||
33 | * Picking custom renaming allows the user to define the renaming | ||
34 | * of specific columns with a rename array that contains column names as | ||
35 | * keys and result alias as values. | ||
36 | */ | ||
37 | public const COLUMN_RENAMING_CUSTOM = 2; | ||
38 | |||
39 | /** | ||
40 | * Incremental renaming uses a result set mapping internal counter to add a | ||
41 | * number to each column result, leading to uniqueness. This only works if | ||
42 | * you use {@see generateSelectClause()} to generate the SELECT clause for | ||
43 | * you. | ||
44 | */ | ||
45 | public const COLUMN_RENAMING_INCREMENT = 3; | ||
46 | |||
47 | private int $sqlCounter = 0; | ||
48 | |||
49 | /** @psalm-param self::COLUMN_RENAMING_* $defaultRenameMode */ | ||
50 | public function __construct( | ||
51 | private readonly EntityManagerInterface $em, | ||
52 | private readonly int $defaultRenameMode = self::COLUMN_RENAMING_NONE, | ||
53 | ) { | ||
54 | } | ||
55 | |||
56 | /** | ||
57 | * Adds a root entity and all of its fields to the result set. | ||
58 | * | ||
59 | * @param string $class The class name of the root entity. | ||
60 | * @param string $alias The unique alias to use for the root entity. | ||
61 | * @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName). | ||
62 | * @psalm-param class-string $class | ||
63 | * @psalm-param array<string, string> $renamedColumns | ||
64 | * @psalm-param self::COLUMN_RENAMING_*|null $renameMode | ||
65 | */ | ||
66 | public function addRootEntityFromClassMetadata( | ||
67 | string $class, | ||
68 | string $alias, | ||
69 | array $renamedColumns = [], | ||
70 | int|null $renameMode = null, | ||
71 | ): void { | ||
72 | $renameMode = $renameMode ?: $this->defaultRenameMode; | ||
73 | $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns); | ||
74 | |||
75 | $this->addEntityResult($class, $alias); | ||
76 | $this->addAllClassFields($class, $alias, $columnAliasMap); | ||
77 | } | ||
78 | |||
79 | /** | ||
80 | * Adds a joined entity and all of its fields to the result set. | ||
81 | * | ||
82 | * @param string $class The class name of the joined entity. | ||
83 | * @param string $alias The unique alias to use for the joined entity. | ||
84 | * @param string $parentAlias The alias of the entity result that is the parent of this joined result. | ||
85 | * @param string $relation The association field that connects the parent entity result | ||
86 | * with the joined entity result. | ||
87 | * @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName). | ||
88 | * @psalm-param class-string $class | ||
89 | * @psalm-param array<string, string> $renamedColumns | ||
90 | * @psalm-param self::COLUMN_RENAMING_*|null $renameMode | ||
91 | */ | ||
92 | public function addJoinedEntityFromClassMetadata( | ||
93 | string $class, | ||
94 | string $alias, | ||
95 | string $parentAlias, | ||
96 | string $relation, | ||
97 | array $renamedColumns = [], | ||
98 | int|null $renameMode = null, | ||
99 | ): void { | ||
100 | $renameMode = $renameMode ?: $this->defaultRenameMode; | ||
101 | $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns); | ||
102 | |||
103 | $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation); | ||
104 | $this->addAllClassFields($class, $alias, $columnAliasMap); | ||
105 | } | ||
106 | |||
107 | /** | ||
108 | * Adds all fields of the given class to the result set mapping (columns and meta fields). | ||
109 | * | ||
110 | * @param string[] $columnAliasMap | ||
111 | * @psalm-param array<string, string> $columnAliasMap | ||
112 | * | ||
113 | * @throws InvalidArgumentException | ||
114 | */ | ||
115 | protected function addAllClassFields(string $class, string $alias, array $columnAliasMap = []): void | ||
116 | { | ||
117 | $classMetadata = $this->em->getClassMetadata($class); | ||
118 | $platform = $this->em->getConnection()->getDatabasePlatform(); | ||
119 | |||
120 | if (! $this->isInheritanceSupported($classMetadata)) { | ||
121 | throw new InvalidArgumentException('ResultSetMapping builder does not currently support your inheritance scheme.'); | ||
122 | } | ||
123 | |||
124 | foreach ($classMetadata->getColumnNames() as $columnName) { | ||
125 | $propertyName = $classMetadata->getFieldName($columnName); | ||
126 | $columnAlias = $this->getSQLResultCasing($platform, $columnAliasMap[$columnName]); | ||
127 | |||
128 | if (isset($this->fieldMappings[$columnAlias])) { | ||
129 | throw new InvalidArgumentException(sprintf( | ||
130 | "The column '%s' conflicts with another column in the mapper.", | ||
131 | $columnName, | ||
132 | )); | ||
133 | } | ||
134 | |||
135 | $this->addFieldResult($alias, $columnAlias, $propertyName); | ||
136 | |||
137 | $enumType = $classMetadata->getFieldMapping($propertyName)->enumType ?? null; | ||
138 | if (! empty($enumType)) { | ||
139 | $this->addEnumResult($columnAlias, $enumType); | ||
140 | } | ||
141 | } | ||
142 | |||
143 | foreach ($classMetadata->associationMappings as $associationMapping) { | ||
144 | if ($associationMapping->isToOneOwningSide()) { | ||
145 | $targetClass = $this->em->getClassMetadata($associationMapping->targetEntity); | ||
146 | $isIdentifier = isset($associationMapping->id) && $associationMapping->id === true; | ||
147 | |||
148 | foreach ($associationMapping->joinColumns as $joinColumn) { | ||
149 | $columnName = $joinColumn->name; | ||
150 | $columnAlias = $this->getSQLResultCasing($platform, $columnAliasMap[$columnName]); | ||
151 | $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
152 | |||
153 | if (isset($this->metaMappings[$columnAlias])) { | ||
154 | throw new InvalidArgumentException(sprintf( | ||
155 | "The column '%s' conflicts with another column in the mapper.", | ||
156 | $columnAlias, | ||
157 | )); | ||
158 | } | ||
159 | |||
160 | $this->addMetaResult($alias, $columnAlias, $columnName, $isIdentifier, $columnType); | ||
161 | } | ||
162 | } | ||
163 | } | ||
164 | } | ||
165 | |||
166 | private function isInheritanceSupported(ClassMetadata $classMetadata): bool | ||
167 | { | ||
168 | if ( | ||
169 | $classMetadata->isInheritanceTypeSingleTable() | ||
170 | && in_array($classMetadata->name, $classMetadata->discriminatorMap, true) | ||
171 | ) { | ||
172 | return true; | ||
173 | } | ||
174 | |||
175 | return ! ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined()); | ||
176 | } | ||
177 | |||
178 | /** | ||
179 | * Gets column alias for a given column. | ||
180 | * | ||
181 | * @psalm-param array<string, string> $customRenameColumns | ||
182 | * | ||
183 | * @psalm-assert self::COLUMN_RENAMING_* $mode | ||
184 | */ | ||
185 | private function getColumnAlias(string $columnName, int $mode, array $customRenameColumns): string | ||
186 | { | ||
187 | return match ($mode) { | ||
188 | self::COLUMN_RENAMING_INCREMENT => $columnName . $this->sqlCounter++, | ||
189 | self::COLUMN_RENAMING_CUSTOM => $customRenameColumns[$columnName] ?? $columnName, | ||
190 | self::COLUMN_RENAMING_NONE => $columnName, | ||
191 | default => throw new InvalidArgumentException(sprintf('%d is not a valid value for $mode', $mode)), | ||
192 | }; | ||
193 | } | ||
194 | |||
195 | /** | ||
196 | * Retrieves a class columns and join columns aliases that are used in the SELECT clause. | ||
197 | * | ||
198 | * This depends on the renaming mode selected by the user. | ||
199 | * | ||
200 | * @psalm-param class-string $className | ||
201 | * @psalm-param self::COLUMN_RENAMING_* $mode | ||
202 | * @psalm-param array<string, string> $customRenameColumns | ||
203 | * | ||
204 | * @return string[] | ||
205 | * @psalm-return array<array-key, string> | ||
206 | */ | ||
207 | private function getColumnAliasMap( | ||
208 | string $className, | ||
209 | int $mode, | ||
210 | array $customRenameColumns, | ||
211 | ): array { | ||
212 | if ($customRenameColumns) { // for BC with 2.2-2.3 API | ||
213 | $mode = self::COLUMN_RENAMING_CUSTOM; | ||
214 | } | ||
215 | |||
216 | $columnAlias = []; | ||
217 | $class = $this->em->getClassMetadata($className); | ||
218 | |||
219 | foreach ($class->getColumnNames() as $columnName) { | ||
220 | $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns); | ||
221 | } | ||
222 | |||
223 | foreach ($class->associationMappings as $associationMapping) { | ||
224 | if ($associationMapping->isToOneOwningSide()) { | ||
225 | foreach ($associationMapping->joinColumns as $joinColumn) { | ||
226 | $columnName = $joinColumn->name; | ||
227 | $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns); | ||
228 | } | ||
229 | } | ||
230 | } | ||
231 | |||
232 | return $columnAlias; | ||
233 | } | ||
234 | |||
235 | /** | ||
236 | * Generates the Select clause from this ResultSetMappingBuilder. | ||
237 | * | ||
238 | * Works only for all the entity results. The select parts for scalar | ||
239 | * expressions have to be written manually. | ||
240 | * | ||
241 | * @param string[] $tableAliases | ||
242 | * @psalm-param array<string, string> $tableAliases | ||
243 | */ | ||
244 | public function generateSelectClause(array $tableAliases = []): string | ||
245 | { | ||
246 | $sql = ''; | ||
247 | |||
248 | foreach ($this->columnOwnerMap as $columnName => $dqlAlias) { | ||
249 | $tableAlias = $tableAliases[$dqlAlias] ?? $dqlAlias; | ||
250 | |||
251 | if ($sql !== '') { | ||
252 | $sql .= ', '; | ||
253 | } | ||
254 | |||
255 | if (isset($this->fieldMappings[$columnName])) { | ||
256 | $class = $this->em->getClassMetadata($this->declaringClasses[$columnName]); | ||
257 | $fieldName = $this->fieldMappings[$columnName]; | ||
258 | $classFieldMapping = $class->fieldMappings[$fieldName]; | ||
259 | $columnSql = $tableAlias . '.' . $classFieldMapping->columnName; | ||
260 | |||
261 | $type = Type::getType($classFieldMapping->type); | ||
262 | $columnSql = $type->convertToPHPValueSQL($columnSql, $this->em->getConnection()->getDatabasePlatform()); | ||
263 | |||
264 | $sql .= $columnSql; | ||
265 | } elseif (isset($this->metaMappings[$columnName])) { | ||
266 | $sql .= $tableAlias . '.' . $this->metaMappings[$columnName]; | ||
267 | } elseif (isset($this->discriminatorColumns[$dqlAlias])) { | ||
268 | $sql .= $tableAlias . '.' . $this->discriminatorColumns[$dqlAlias]; | ||
269 | } | ||
270 | |||
271 | $sql .= ' AS ' . $columnName; | ||
272 | } | ||
273 | |||
274 | return $sql; | ||
275 | } | ||
276 | |||
277 | public function __toString(): string | ||
278 | { | ||
279 | return $this->generateSelectClause([]); | ||
280 | } | ||
281 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/SqlWalker.php b/vendor/doctrine/orm/src/Query/SqlWalker.php new file mode 100644 index 0000000..c6f98c1 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/SqlWalker.php | |||
@@ -0,0 +1,2264 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use BadMethodCallException; | ||
8 | use Doctrine\DBAL\Connection; | ||
9 | use Doctrine\DBAL\LockMode; | ||
10 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
11 | use Doctrine\DBAL\Types\Type; | ||
12 | use Doctrine\ORM\EntityManagerInterface; | ||
13 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
14 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
15 | use Doctrine\ORM\OptimisticLockException; | ||
16 | use Doctrine\ORM\Query; | ||
17 | use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver; | ||
18 | use Doctrine\ORM\Utility\LockSqlHelper; | ||
19 | use Doctrine\ORM\Utility\PersisterHelper; | ||
20 | use InvalidArgumentException; | ||
21 | use LogicException; | ||
22 | |||
23 | use function array_diff; | ||
24 | use function array_filter; | ||
25 | use function array_keys; | ||
26 | use function array_map; | ||
27 | use function array_merge; | ||
28 | use function assert; | ||
29 | use function count; | ||
30 | use function implode; | ||
31 | use function is_array; | ||
32 | use function is_float; | ||
33 | use function is_int; | ||
34 | use function is_numeric; | ||
35 | use function is_string; | ||
36 | use function preg_match; | ||
37 | use function reset; | ||
38 | use function sprintf; | ||
39 | use function strtolower; | ||
40 | use function strtoupper; | ||
41 | use function trim; | ||
42 | |||
43 | /** | ||
44 | * The SqlWalker walks over a DQL AST and constructs the corresponding SQL. | ||
45 | * | ||
46 | * @psalm-import-type QueryComponent from Parser | ||
47 | * @psalm-consistent-constructor | ||
48 | */ | ||
49 | class SqlWalker | ||
50 | { | ||
51 | use LockSqlHelper; | ||
52 | |||
53 | public const HINT_DISTINCT = 'doctrine.distinct'; | ||
54 | |||
55 | private readonly ResultSetMapping $rsm; | ||
56 | |||
57 | /** | ||
58 | * Counter for generating unique column aliases. | ||
59 | */ | ||
60 | private int $aliasCounter = 0; | ||
61 | |||
62 | /** | ||
63 | * Counter for generating unique table aliases. | ||
64 | */ | ||
65 | private int $tableAliasCounter = 0; | ||
66 | |||
67 | /** | ||
68 | * Counter for generating unique scalar result. | ||
69 | */ | ||
70 | private int $scalarResultCounter = 1; | ||
71 | |||
72 | /** | ||
73 | * Counter for generating unique parameter indexes. | ||
74 | */ | ||
75 | private int $sqlParamIndex = 0; | ||
76 | |||
77 | /** | ||
78 | * Counter for generating indexes. | ||
79 | */ | ||
80 | private int $newObjectCounter = 0; | ||
81 | |||
82 | private readonly EntityManagerInterface $em; | ||
83 | private readonly Connection $conn; | ||
84 | |||
85 | /** @var mixed[] */ | ||
86 | private array $tableAliasMap = []; | ||
87 | |||
88 | /** | ||
89 | * Map from result variable names to their SQL column alias names. | ||
90 | * | ||
91 | * @psalm-var array<string|int, string|list<string>> | ||
92 | */ | ||
93 | private array $scalarResultAliasMap = []; | ||
94 | |||
95 | /** | ||
96 | * Map from Table-Alias + Column-Name to OrderBy-Direction. | ||
97 | * | ||
98 | * @var array<string, string> | ||
99 | */ | ||
100 | private array $orderedColumnsMap = []; | ||
101 | |||
102 | /** | ||
103 | * Map from DQL-Alias + Field-Name to SQL Column Alias. | ||
104 | * | ||
105 | * @var array<string, array<string, string>> | ||
106 | */ | ||
107 | private array $scalarFields = []; | ||
108 | |||
109 | /** | ||
110 | * A list of classes that appear in non-scalar SelectExpressions. | ||
111 | * | ||
112 | * @psalm-var array<string, array{class: ClassMetadata, dqlAlias: string, resultAlias: string|null}> | ||
113 | */ | ||
114 | private array $selectedClasses = []; | ||
115 | |||
116 | /** | ||
117 | * The DQL alias of the root class of the currently traversed query. | ||
118 | * | ||
119 | * @psalm-var list<string> | ||
120 | */ | ||
121 | private array $rootAliases = []; | ||
122 | |||
123 | /** | ||
124 | * Flag that indicates whether to generate SQL table aliases in the SQL. | ||
125 | * These should only be generated for SELECT queries, not for UPDATE/DELETE. | ||
126 | */ | ||
127 | private bool $useSqlTableAliases = true; | ||
128 | |||
129 | /** | ||
130 | * The database platform abstraction. | ||
131 | */ | ||
132 | private readonly AbstractPlatform $platform; | ||
133 | |||
134 | /** | ||
135 | * The quote strategy. | ||
136 | */ | ||
137 | private readonly QuoteStrategy $quoteStrategy; | ||
138 | |||
139 | /** @psalm-param array<string, QueryComponent> $queryComponents The query components (symbol table). */ | ||
140 | public function __construct( | ||
141 | private readonly Query $query, | ||
142 | private readonly ParserResult $parserResult, | ||
143 | private array $queryComponents, | ||
144 | ) { | ||
145 | $this->rsm = $parserResult->getResultSetMapping(); | ||
146 | $this->em = $query->getEntityManager(); | ||
147 | $this->conn = $this->em->getConnection(); | ||
148 | $this->platform = $this->conn->getDatabasePlatform(); | ||
149 | $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); | ||
150 | } | ||
151 | |||
152 | /** | ||
153 | * Gets the Query instance used by the walker. | ||
154 | */ | ||
155 | public function getQuery(): Query | ||
156 | { | ||
157 | return $this->query; | ||
158 | } | ||
159 | |||
160 | /** | ||
161 | * Gets the Connection used by the walker. | ||
162 | */ | ||
163 | public function getConnection(): Connection | ||
164 | { | ||
165 | return $this->conn; | ||
166 | } | ||
167 | |||
168 | /** | ||
169 | * Gets the EntityManager used by the walker. | ||
170 | */ | ||
171 | public function getEntityManager(): EntityManagerInterface | ||
172 | { | ||
173 | return $this->em; | ||
174 | } | ||
175 | |||
176 | /** | ||
177 | * Gets the information about a single query component. | ||
178 | * | ||
179 | * @param string $dqlAlias The DQL alias. | ||
180 | * | ||
181 | * @return mixed[] | ||
182 | * @psalm-return QueryComponent | ||
183 | */ | ||
184 | public function getQueryComponent(string $dqlAlias): array | ||
185 | { | ||
186 | return $this->queryComponents[$dqlAlias]; | ||
187 | } | ||
188 | |||
189 | public function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata | ||
190 | { | ||
191 | return $this->queryComponents[$dqlAlias]['metadata'] | ||
192 | ?? throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); | ||
193 | } | ||
194 | |||
195 | /** | ||
196 | * Returns internal queryComponents array. | ||
197 | * | ||
198 | * @return array<string, QueryComponent> | ||
199 | */ | ||
200 | public function getQueryComponents(): array | ||
201 | { | ||
202 | return $this->queryComponents; | ||
203 | } | ||
204 | |||
205 | /** | ||
206 | * Sets or overrides a query component for a given dql alias. | ||
207 | * | ||
208 | * @psalm-param QueryComponent $queryComponent | ||
209 | */ | ||
210 | public function setQueryComponent(string $dqlAlias, array $queryComponent): void | ||
211 | { | ||
212 | $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token']; | ||
213 | |||
214 | if (array_diff($requiredKeys, array_keys($queryComponent))) { | ||
215 | throw QueryException::invalidQueryComponent($dqlAlias); | ||
216 | } | ||
217 | |||
218 | $this->queryComponents[$dqlAlias] = $queryComponent; | ||
219 | } | ||
220 | |||
221 | /** | ||
222 | * Gets an executor that can be used to execute the result of this walker. | ||
223 | */ | ||
224 | public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): Exec\AbstractSqlExecutor | ||
225 | { | ||
226 | return match (true) { | ||
227 | $statement instanceof AST\SelectStatement | ||
228 | => new Exec\SingleSelectExecutor($statement, $this), | ||
229 | $statement instanceof AST\UpdateStatement | ||
230 | => $this->em->getClassMetadata($statement->updateClause->abstractSchemaName)->isInheritanceTypeJoined() | ||
231 | ? new Exec\MultiTableUpdateExecutor($statement, $this) | ||
232 | : new Exec\SingleTableDeleteUpdateExecutor($statement, $this), | ||
233 | $statement instanceof AST\DeleteStatement | ||
234 | => $this->em->getClassMetadata($statement->deleteClause->abstractSchemaName)->isInheritanceTypeJoined() | ||
235 | ? new Exec\MultiTableDeleteExecutor($statement, $this) | ||
236 | : new Exec\SingleTableDeleteUpdateExecutor($statement, $this), | ||
237 | }; | ||
238 | } | ||
239 | |||
240 | /** | ||
241 | * Generates a unique, short SQL table alias. | ||
242 | */ | ||
243 | public function getSQLTableAlias(string $tableName, string $dqlAlias = ''): string | ||
244 | { | ||
245 | $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : ''; | ||
246 | |||
247 | if (! isset($this->tableAliasMap[$tableName])) { | ||
248 | $this->tableAliasMap[$tableName] = (preg_match('/[a-z]/i', $tableName[0]) ? strtolower($tableName[0]) : 't') | ||
249 | . $this->tableAliasCounter++ . '_'; | ||
250 | } | ||
251 | |||
252 | return $this->tableAliasMap[$tableName]; | ||
253 | } | ||
254 | |||
255 | /** | ||
256 | * Forces the SqlWalker to use a specific alias for a table name, rather than | ||
257 | * generating an alias on its own. | ||
258 | */ | ||
259 | public function setSQLTableAlias(string $tableName, string $alias, string $dqlAlias = ''): string | ||
260 | { | ||
261 | $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : ''; | ||
262 | |||
263 | $this->tableAliasMap[$tableName] = $alias; | ||
264 | |||
265 | return $alias; | ||
266 | } | ||
267 | |||
268 | /** | ||
269 | * Gets an SQL column alias for a column name. | ||
270 | */ | ||
271 | public function getSQLColumnAlias(string $columnName): string | ||
272 | { | ||
273 | return $this->quoteStrategy->getColumnAlias($columnName, $this->aliasCounter++, $this->platform); | ||
274 | } | ||
275 | |||
276 | /** | ||
277 | * Generates the SQL JOINs that are necessary for Class Table Inheritance | ||
278 | * for the given class. | ||
279 | */ | ||
280 | private function generateClassTableInheritanceJoins( | ||
281 | ClassMetadata $class, | ||
282 | string $dqlAlias, | ||
283 | ): string { | ||
284 | $sql = ''; | ||
285 | |||
286 | $baseTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
287 | |||
288 | // INNER JOIN parent class tables | ||
289 | foreach ($class->parentClasses as $parentClassName) { | ||
290 | $parentClass = $this->em->getClassMetadata($parentClassName); | ||
291 | $tableAlias = $this->getSQLTableAlias($parentClass->getTableName(), $dqlAlias); | ||
292 | |||
293 | // If this is a joined association we must use left joins to preserve the correct result. | ||
294 | $sql .= isset($this->queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER '; | ||
295 | $sql .= 'JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
296 | |||
297 | $sqlParts = []; | ||
298 | |||
299 | foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) { | ||
300 | $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; | ||
301 | } | ||
302 | |||
303 | // Add filters on the root class | ||
304 | $sqlParts[] = $this->generateFilterConditionSQL($parentClass, $tableAlias); | ||
305 | |||
306 | $sql .= implode(' AND ', array_filter($sqlParts)); | ||
307 | } | ||
308 | |||
309 | // LEFT JOIN child class tables | ||
310 | foreach ($class->subClasses as $subClassName) { | ||
311 | $subClass = $this->em->getClassMetadata($subClassName); | ||
312 | $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); | ||
313 | |||
314 | $sql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
315 | |||
316 | $sqlParts = []; | ||
317 | |||
318 | foreach ($this->quoteStrategy->getIdentifierColumnNames($subClass, $this->platform) as $columnName) { | ||
319 | $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; | ||
320 | } | ||
321 | |||
322 | $sql .= implode(' AND ', $sqlParts); | ||
323 | } | ||
324 | |||
325 | return $sql; | ||
326 | } | ||
327 | |||
328 | private function generateOrderedCollectionOrderByItems(): string | ||
329 | { | ||
330 | $orderedColumns = []; | ||
331 | |||
332 | foreach ($this->selectedClasses as $selectedClass) { | ||
333 | $dqlAlias = $selectedClass['dqlAlias']; | ||
334 | $qComp = $this->queryComponents[$dqlAlias]; | ||
335 | |||
336 | if (! isset($qComp['relation']->orderBy)) { | ||
337 | continue; | ||
338 | } | ||
339 | |||
340 | assert(isset($qComp['metadata'])); | ||
341 | $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); | ||
342 | |||
343 | foreach ($qComp['relation']->orderBy as $fieldName => $orientation) { | ||
344 | $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform); | ||
345 | $tableName = $qComp['metadata']->isInheritanceTypeJoined() | ||
346 | ? $persister->getOwningTable($fieldName) | ||
347 | : $qComp['metadata']->getTableName(); | ||
348 | |||
349 | $orderedColumn = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName; | ||
350 | |||
351 | // OrderByClause should replace an ordered relation. see - DDC-2475 | ||
352 | if (isset($this->orderedColumnsMap[$orderedColumn])) { | ||
353 | continue; | ||
354 | } | ||
355 | |||
356 | $this->orderedColumnsMap[$orderedColumn] = $orientation; | ||
357 | $orderedColumns[] = $orderedColumn . ' ' . $orientation; | ||
358 | } | ||
359 | } | ||
360 | |||
361 | return implode(', ', $orderedColumns); | ||
362 | } | ||
363 | |||
364 | /** | ||
365 | * Generates a discriminator column SQL condition for the class with the given DQL alias. | ||
366 | * | ||
367 | * @psalm-param list<string> $dqlAliases List of root DQL aliases to inspect for discriminator restrictions. | ||
368 | */ | ||
369 | private function generateDiscriminatorColumnConditionSQL(array $dqlAliases): string | ||
370 | { | ||
371 | $sqlParts = []; | ||
372 | |||
373 | foreach ($dqlAliases as $dqlAlias) { | ||
374 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
375 | |||
376 | if (! $class->isInheritanceTypeSingleTable()) { | ||
377 | continue; | ||
378 | } | ||
379 | |||
380 | $sqlTableAlias = $this->useSqlTableAliases | ||
381 | ? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.' | ||
382 | : ''; | ||
383 | |||
384 | $conn = $this->em->getConnection(); | ||
385 | $values = []; | ||
386 | |||
387 | if ($class->discriminatorValue !== null) { // discriminators can be 0 | ||
388 | $values[] = $class->getDiscriminatorColumn()->type === 'integer' && is_int($class->discriminatorValue) | ||
389 | ? $class->discriminatorValue | ||
390 | : $conn->quote((string) $class->discriminatorValue); | ||
391 | } | ||
392 | |||
393 | foreach ($class->subClasses as $subclassName) { | ||
394 | $subclassMetadata = $this->em->getClassMetadata($subclassName); | ||
395 | |||
396 | // Abstract entity classes show up in the list of subClasses, but may be omitted | ||
397 | // from the discriminator map. In that case, they have a null discriminator value. | ||
398 | if ($subclassMetadata->discriminatorValue === null) { | ||
399 | continue; | ||
400 | } | ||
401 | |||
402 | $values[] = $subclassMetadata->getDiscriminatorColumn()->type === 'integer' && is_int($subclassMetadata->discriminatorValue) | ||
403 | ? $subclassMetadata->discriminatorValue | ||
404 | : $conn->quote((string) $subclassMetadata->discriminatorValue); | ||
405 | } | ||
406 | |||
407 | if ($values !== []) { | ||
408 | $sqlParts[] = $sqlTableAlias . $class->getDiscriminatorColumn()->name . ' IN (' . implode(', ', $values) . ')'; | ||
409 | } else { | ||
410 | $sqlParts[] = '1=0'; // impossible condition | ||
411 | } | ||
412 | } | ||
413 | |||
414 | $sql = implode(' AND ', $sqlParts); | ||
415 | |||
416 | return count($sqlParts) > 1 ? '(' . $sql . ')' : $sql; | ||
417 | } | ||
418 | |||
419 | /** | ||
420 | * Generates the filter SQL for a given entity and table alias. | ||
421 | */ | ||
422 | private function generateFilterConditionSQL( | ||
423 | ClassMetadata $targetEntity, | ||
424 | string $targetTableAlias, | ||
425 | ): string { | ||
426 | if (! $this->em->hasFilters()) { | ||
427 | return ''; | ||
428 | } | ||
429 | |||
430 | switch ($targetEntity->inheritanceType) { | ||
431 | case ClassMetadata::INHERITANCE_TYPE_NONE: | ||
432 | break; | ||
433 | case ClassMetadata::INHERITANCE_TYPE_JOINED: | ||
434 | // The classes in the inheritance will be added to the query one by one, | ||
435 | // but only the root node is getting filtered | ||
436 | if ($targetEntity->name !== $targetEntity->rootEntityName) { | ||
437 | return ''; | ||
438 | } | ||
439 | |||
440 | break; | ||
441 | case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE: | ||
442 | // With STI the table will only be queried once, make sure that the filters | ||
443 | // are added to the root entity | ||
444 | $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName); | ||
445 | break; | ||
446 | default: | ||
447 | //@todo: throw exception? | ||
448 | return ''; | ||
449 | } | ||
450 | |||
451 | $filterClauses = []; | ||
452 | foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { | ||
453 | $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); | ||
454 | if ($filterExpr !== '') { | ||
455 | $filterClauses[] = '(' . $filterExpr . ')'; | ||
456 | } | ||
457 | } | ||
458 | |||
459 | return implode(' AND ', $filterClauses); | ||
460 | } | ||
461 | |||
462 | /** | ||
463 | * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. | ||
464 | */ | ||
465 | public function walkSelectStatement(AST\SelectStatement $selectStatement): string | ||
466 | { | ||
467 | $limit = $this->query->getMaxResults(); | ||
468 | $offset = $this->query->getFirstResult(); | ||
469 | $lockMode = $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE; | ||
470 | $sql = $this->walkSelectClause($selectStatement->selectClause) | ||
471 | . $this->walkFromClause($selectStatement->fromClause) | ||
472 | . $this->walkWhereClause($selectStatement->whereClause); | ||
473 | |||
474 | if ($selectStatement->groupByClause) { | ||
475 | $sql .= $this->walkGroupByClause($selectStatement->groupByClause); | ||
476 | } | ||
477 | |||
478 | if ($selectStatement->havingClause) { | ||
479 | $sql .= $this->walkHavingClause($selectStatement->havingClause); | ||
480 | } | ||
481 | |||
482 | if ($selectStatement->orderByClause) { | ||
483 | $sql .= $this->walkOrderByClause($selectStatement->orderByClause); | ||
484 | } | ||
485 | |||
486 | $orderBySql = $this->generateOrderedCollectionOrderByItems(); | ||
487 | if (! $selectStatement->orderByClause && $orderBySql) { | ||
488 | $sql .= ' ORDER BY ' . $orderBySql; | ||
489 | } | ||
490 | |||
491 | $sql = $this->platform->modifyLimitQuery($sql, $limit, $offset); | ||
492 | |||
493 | if ($lockMode === LockMode::NONE) { | ||
494 | return $sql; | ||
495 | } | ||
496 | |||
497 | if ($lockMode === LockMode::PESSIMISTIC_READ) { | ||
498 | return $sql . ' ' . $this->getReadLockSQL($this->platform); | ||
499 | } | ||
500 | |||
501 | if ($lockMode === LockMode::PESSIMISTIC_WRITE) { | ||
502 | return $sql . ' ' . $this->getWriteLockSQL($this->platform); | ||
503 | } | ||
504 | |||
505 | if ($lockMode !== LockMode::OPTIMISTIC) { | ||
506 | throw QueryException::invalidLockMode(); | ||
507 | } | ||
508 | |||
509 | foreach ($this->selectedClasses as $selectedClass) { | ||
510 | if (! $selectedClass['class']->isVersioned) { | ||
511 | throw OptimisticLockException::lockFailed($selectedClass['class']->name); | ||
512 | } | ||
513 | } | ||
514 | |||
515 | return $sql; | ||
516 | } | ||
517 | |||
518 | /** | ||
519 | * Walks down a UpdateStatement AST node, thereby generating the appropriate SQL. | ||
520 | */ | ||
521 | public function walkUpdateStatement(AST\UpdateStatement $updateStatement): string | ||
522 | { | ||
523 | $this->useSqlTableAliases = false; | ||
524 | $this->rsm->isSelect = false; | ||
525 | |||
526 | return $this->walkUpdateClause($updateStatement->updateClause) | ||
527 | . $this->walkWhereClause($updateStatement->whereClause); | ||
528 | } | ||
529 | |||
530 | /** | ||
531 | * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. | ||
532 | */ | ||
533 | public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): string | ||
534 | { | ||
535 | $this->useSqlTableAliases = false; | ||
536 | $this->rsm->isSelect = false; | ||
537 | |||
538 | return $this->walkDeleteClause($deleteStatement->deleteClause) | ||
539 | . $this->walkWhereClause($deleteStatement->whereClause); | ||
540 | } | ||
541 | |||
542 | /** | ||
543 | * Walks down an IdentificationVariable AST node, thereby generating the appropriate SQL. | ||
544 | * This one differs of ->walkIdentificationVariable() because it generates the entity identifiers. | ||
545 | */ | ||
546 | public function walkEntityIdentificationVariable(string $identVariable): string | ||
547 | { | ||
548 | $class = $this->getMetadataForDqlAlias($identVariable); | ||
549 | $tableAlias = $this->getSQLTableAlias($class->getTableName(), $identVariable); | ||
550 | $sqlParts = []; | ||
551 | |||
552 | foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) { | ||
553 | $sqlParts[] = $tableAlias . '.' . $columnName; | ||
554 | } | ||
555 | |||
556 | return implode(', ', $sqlParts); | ||
557 | } | ||
558 | |||
559 | /** | ||
560 | * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL. | ||
561 | */ | ||
562 | public function walkIdentificationVariable(string $identificationVariable, string|null $fieldName = null): string | ||
563 | { | ||
564 | $class = $this->getMetadataForDqlAlias($identificationVariable); | ||
565 | |||
566 | if ( | ||
567 | $fieldName !== null && $class->isInheritanceTypeJoined() && | ||
568 | isset($class->fieldMappings[$fieldName]->inherited) | ||
569 | ) { | ||
570 | $class = $this->em->getClassMetadata($class->fieldMappings[$fieldName]->inherited); | ||
571 | } | ||
572 | |||
573 | return $this->getSQLTableAlias($class->getTableName(), $identificationVariable); | ||
574 | } | ||
575 | |||
576 | /** | ||
577 | * Walks down a PathExpression AST node, thereby generating the appropriate SQL. | ||
578 | */ | ||
579 | public function walkPathExpression(AST\PathExpression $pathExpr): string | ||
580 | { | ||
581 | $sql = ''; | ||
582 | assert($pathExpr->field !== null); | ||
583 | |||
584 | switch ($pathExpr->type) { | ||
585 | case AST\PathExpression::TYPE_STATE_FIELD: | ||
586 | $fieldName = $pathExpr->field; | ||
587 | $dqlAlias = $pathExpr->identificationVariable; | ||
588 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
589 | |||
590 | if ($this->useSqlTableAliases) { | ||
591 | $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.'; | ||
592 | } | ||
593 | |||
594 | $sql .= $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
595 | break; | ||
596 | |||
597 | case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: | ||
598 | // 1- the owning side: | ||
599 | // Just use the foreign key, i.e. u.group_id | ||
600 | $fieldName = $pathExpr->field; | ||
601 | $dqlAlias = $pathExpr->identificationVariable; | ||
602 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
603 | |||
604 | if (isset($class->associationMappings[$fieldName]->inherited)) { | ||
605 | $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited); | ||
606 | } | ||
607 | |||
608 | $assoc = $class->associationMappings[$fieldName]; | ||
609 | |||
610 | if (! $assoc->isOwningSide()) { | ||
611 | throw QueryException::associationPathInverseSideNotSupported($pathExpr); | ||
612 | } | ||
613 | |||
614 | assert($assoc->isToOneOwningSide()); | ||
615 | |||
616 | // COMPOSITE KEYS NOT (YET?) SUPPORTED | ||
617 | if (count($assoc->sourceToTargetKeyColumns) > 1) { | ||
618 | throw QueryException::associationPathCompositeKeyNotSupported(); | ||
619 | } | ||
620 | |||
621 | if ($this->useSqlTableAliases) { | ||
622 | $sql .= $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.'; | ||
623 | } | ||
624 | |||
625 | $sql .= reset($assoc->targetToSourceKeyColumns); | ||
626 | break; | ||
627 | |||
628 | default: | ||
629 | throw QueryException::invalidPathExpression($pathExpr); | ||
630 | } | ||
631 | |||
632 | return $sql; | ||
633 | } | ||
634 | |||
635 | /** | ||
636 | * Walks down a SelectClause AST node, thereby generating the appropriate SQL. | ||
637 | */ | ||
638 | public function walkSelectClause(AST\SelectClause $selectClause): string | ||
639 | { | ||
640 | $sql = 'SELECT ' . ($selectClause->isDistinct ? 'DISTINCT ' : ''); | ||
641 | $sqlSelectExpressions = array_filter(array_map($this->walkSelectExpression(...), $selectClause->selectExpressions)); | ||
642 | |||
643 | if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && $selectClause->isDistinct) { | ||
644 | $this->query->setHint(self::HINT_DISTINCT, true); | ||
645 | } | ||
646 | |||
647 | $addMetaColumns = $this->query->getHydrationMode() === Query::HYDRATE_OBJECT | ||
648 | || $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS); | ||
649 | |||
650 | foreach ($this->selectedClasses as $selectedClass) { | ||
651 | $class = $selectedClass['class']; | ||
652 | $dqlAlias = $selectedClass['dqlAlias']; | ||
653 | $resultAlias = $selectedClass['resultAlias']; | ||
654 | |||
655 | // Register as entity or joined entity result | ||
656 | if (! isset($this->queryComponents[$dqlAlias]['relation'])) { | ||
657 | $this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias); | ||
658 | } else { | ||
659 | assert(isset($this->queryComponents[$dqlAlias]['parent'])); | ||
660 | |||
661 | $this->rsm->addJoinedEntityResult( | ||
662 | $class->name, | ||
663 | $dqlAlias, | ||
664 | $this->queryComponents[$dqlAlias]['parent'], | ||
665 | $this->queryComponents[$dqlAlias]['relation']->fieldName, | ||
666 | ); | ||
667 | } | ||
668 | |||
669 | if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) { | ||
670 | // Add discriminator columns to SQL | ||
671 | $rootClass = $this->em->getClassMetadata($class->rootEntityName); | ||
672 | $tblAlias = $this->getSQLTableAlias($rootClass->getTableName(), $dqlAlias); | ||
673 | $discrColumn = $rootClass->getDiscriminatorColumn(); | ||
674 | $columnAlias = $this->getSQLColumnAlias($discrColumn->name); | ||
675 | |||
676 | $sqlSelectExpressions[] = $tblAlias . '.' . $discrColumn->name . ' AS ' . $columnAlias; | ||
677 | |||
678 | $this->rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); | ||
679 | $this->rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn->fieldName, false, $discrColumn->type); | ||
680 | if (! empty($discrColumn->enumType)) { | ||
681 | $this->rsm->addEnumResult($columnAlias, $discrColumn->enumType); | ||
682 | } | ||
683 | } | ||
684 | |||
685 | // Add foreign key columns to SQL, if necessary | ||
686 | if (! $addMetaColumns && ! $class->containsForeignIdentifier) { | ||
687 | continue; | ||
688 | } | ||
689 | |||
690 | // Add foreign key columns of class and also parent classes | ||
691 | foreach ($class->associationMappings as $assoc) { | ||
692 | if ( | ||
693 | ! $assoc->isToOneOwningSide() | ||
694 | || ( ! $addMetaColumns && ! isset($assoc->id)) | ||
695 | ) { | ||
696 | continue; | ||
697 | } | ||
698 | |||
699 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
700 | $isIdentifier = (isset($assoc->id) && $assoc->id === true); | ||
701 | $owningClass = isset($assoc->inherited) ? $this->em->getClassMetadata($assoc->inherited) : $class; | ||
702 | $sqlTableAlias = $this->getSQLTableAlias($owningClass->getTableName(), $dqlAlias); | ||
703 | |||
704 | foreach ($assoc->joinColumns as $joinColumn) { | ||
705 | $columnName = $joinColumn->name; | ||
706 | $columnAlias = $this->getSQLColumnAlias($columnName); | ||
707 | $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
708 | |||
709 | $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
710 | $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; | ||
711 | |||
712 | $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $isIdentifier, $columnType); | ||
713 | } | ||
714 | } | ||
715 | |||
716 | // Add foreign key columns to SQL, if necessary | ||
717 | if (! $addMetaColumns) { | ||
718 | continue; | ||
719 | } | ||
720 | |||
721 | // Add foreign key columns of subclasses | ||
722 | foreach ($class->subClasses as $subClassName) { | ||
723 | $subClass = $this->em->getClassMetadata($subClassName); | ||
724 | $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); | ||
725 | |||
726 | foreach ($subClass->associationMappings as $assoc) { | ||
727 | // Skip if association is inherited | ||
728 | if (isset($assoc->inherited)) { | ||
729 | continue; | ||
730 | } | ||
731 | |||
732 | if ($assoc->isToOneOwningSide()) { | ||
733 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
734 | |||
735 | foreach ($assoc->joinColumns as $joinColumn) { | ||
736 | $columnName = $joinColumn->name; | ||
737 | $columnAlias = $this->getSQLColumnAlias($columnName); | ||
738 | $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
739 | |||
740 | $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform); | ||
741 | $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; | ||
742 | |||
743 | $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $subClass->isIdentifier($columnName), $columnType); | ||
744 | } | ||
745 | } | ||
746 | } | ||
747 | } | ||
748 | } | ||
749 | |||
750 | return $sql . implode(', ', $sqlSelectExpressions); | ||
751 | } | ||
752 | |||
753 | /** | ||
754 | * Walks down a FromClause AST node, thereby generating the appropriate SQL. | ||
755 | */ | ||
756 | public function walkFromClause(AST\FromClause $fromClause): string | ||
757 | { | ||
758 | $identificationVarDecls = $fromClause->identificationVariableDeclarations; | ||
759 | $sqlParts = []; | ||
760 | |||
761 | foreach ($identificationVarDecls as $identificationVariableDecl) { | ||
762 | $sqlParts[] = $this->walkIdentificationVariableDeclaration($identificationVariableDecl); | ||
763 | } | ||
764 | |||
765 | return ' FROM ' . implode(', ', $sqlParts); | ||
766 | } | ||
767 | |||
768 | /** | ||
769 | * Walks down a IdentificationVariableDeclaration AST node, thereby generating the appropriate SQL. | ||
770 | */ | ||
771 | public function walkIdentificationVariableDeclaration(AST\IdentificationVariableDeclaration $identificationVariableDecl): string | ||
772 | { | ||
773 | $sql = $this->walkRangeVariableDeclaration($identificationVariableDecl->rangeVariableDeclaration); | ||
774 | |||
775 | if ($identificationVariableDecl->indexBy) { | ||
776 | $this->walkIndexBy($identificationVariableDecl->indexBy); | ||
777 | } | ||
778 | |||
779 | foreach ($identificationVariableDecl->joins as $join) { | ||
780 | $sql .= $this->walkJoin($join); | ||
781 | } | ||
782 | |||
783 | return $sql; | ||
784 | } | ||
785 | |||
786 | /** | ||
787 | * Walks down a IndexBy AST node. | ||
788 | */ | ||
789 | public function walkIndexBy(AST\IndexBy $indexBy): void | ||
790 | { | ||
791 | $pathExpression = $indexBy->singleValuedPathExpression; | ||
792 | $alias = $pathExpression->identificationVariable; | ||
793 | assert($pathExpression->field !== null); | ||
794 | |||
795 | switch ($pathExpression->type) { | ||
796 | case AST\PathExpression::TYPE_STATE_FIELD: | ||
797 | $field = $pathExpression->field; | ||
798 | break; | ||
799 | |||
800 | case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: | ||
801 | // Just use the foreign key, i.e. u.group_id | ||
802 | $fieldName = $pathExpression->field; | ||
803 | $class = $this->getMetadataForDqlAlias($alias); | ||
804 | |||
805 | if (isset($class->associationMappings[$fieldName]->inherited)) { | ||
806 | $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited); | ||
807 | } | ||
808 | |||
809 | $association = $class->associationMappings[$fieldName]; | ||
810 | |||
811 | if (! $association->isOwningSide()) { | ||
812 | throw QueryException::associationPathInverseSideNotSupported($pathExpression); | ||
813 | } | ||
814 | |||
815 | assert($association->isToOneOwningSide()); | ||
816 | |||
817 | if (count($association->sourceToTargetKeyColumns) > 1) { | ||
818 | throw QueryException::associationPathCompositeKeyNotSupported(); | ||
819 | } | ||
820 | |||
821 | $field = reset($association->targetToSourceKeyColumns); | ||
822 | break; | ||
823 | |||
824 | default: | ||
825 | throw QueryException::invalidPathExpression($pathExpression); | ||
826 | } | ||
827 | |||
828 | if (isset($this->scalarFields[$alias][$field])) { | ||
829 | $this->rsm->addIndexByScalar($this->scalarFields[$alias][$field]); | ||
830 | |||
831 | return; | ||
832 | } | ||
833 | |||
834 | $this->rsm->addIndexBy($alias, $field); | ||
835 | } | ||
836 | |||
837 | /** | ||
838 | * Walks down a RangeVariableDeclaration AST node, thereby generating the appropriate SQL. | ||
839 | */ | ||
840 | public function walkRangeVariableDeclaration(AST\RangeVariableDeclaration $rangeVariableDeclaration): string | ||
841 | { | ||
842 | return $this->generateRangeVariableDeclarationSQL($rangeVariableDeclaration, false); | ||
843 | } | ||
844 | |||
845 | /** | ||
846 | * Generate appropriate SQL for RangeVariableDeclaration AST node | ||
847 | */ | ||
848 | private function generateRangeVariableDeclarationSQL( | ||
849 | AST\RangeVariableDeclaration $rangeVariableDeclaration, | ||
850 | bool $buildNestedJoins, | ||
851 | ): string { | ||
852 | $class = $this->em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName); | ||
853 | $dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable; | ||
854 | |||
855 | if ($rangeVariableDeclaration->isRoot) { | ||
856 | $this->rootAliases[] = $dqlAlias; | ||
857 | } | ||
858 | |||
859 | $sql = $this->platform->appendLockHint( | ||
860 | $this->quoteStrategy->getTableName($class, $this->platform) . ' ' . | ||
861 | $this->getSQLTableAlias($class->getTableName(), $dqlAlias), | ||
862 | $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE, | ||
863 | ); | ||
864 | |||
865 | if (! $class->isInheritanceTypeJoined()) { | ||
866 | return $sql; | ||
867 | } | ||
868 | |||
869 | $classTableInheritanceJoins = $this->generateClassTableInheritanceJoins($class, $dqlAlias); | ||
870 | |||
871 | if (! $buildNestedJoins) { | ||
872 | return $sql . $classTableInheritanceJoins; | ||
873 | } | ||
874 | |||
875 | return $classTableInheritanceJoins === '' ? $sql : '(' . $sql . $classTableInheritanceJoins . ')'; | ||
876 | } | ||
877 | |||
878 | /** | ||
879 | * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL. | ||
880 | * | ||
881 | * @psalm-param AST\Join::JOIN_TYPE_* $joinType | ||
882 | * | ||
883 | * @throws QueryException | ||
884 | */ | ||
885 | public function walkJoinAssociationDeclaration( | ||
886 | AST\JoinAssociationDeclaration $joinAssociationDeclaration, | ||
887 | int $joinType = AST\Join::JOIN_TYPE_INNER, | ||
888 | AST\ConditionalExpression|AST\Phase2OptimizableConditional|null $condExpr = null, | ||
889 | ): string { | ||
890 | $sql = ''; | ||
891 | |||
892 | $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression; | ||
893 | $joinedDqlAlias = $joinAssociationDeclaration->aliasIdentificationVariable; | ||
894 | $indexBy = $joinAssociationDeclaration->indexBy; | ||
895 | |||
896 | $relation = $this->queryComponents[$joinedDqlAlias]['relation'] ?? null; | ||
897 | assert($relation !== null); | ||
898 | $targetClass = $this->em->getClassMetadata($relation->targetEntity); | ||
899 | $sourceClass = $this->em->getClassMetadata($relation->sourceEntity); | ||
900 | $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); | ||
901 | |||
902 | $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); | ||
903 | $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable); | ||
904 | |||
905 | // Ensure we got the owning side, since it has all mapping info | ||
906 | $assoc = $this->em->getMetadataFactory()->getOwningSide($relation); | ||
907 | |||
908 | if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && (! $this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) { | ||
909 | if ($relation->isToMany()) { | ||
910 | throw QueryException::iterateWithFetchJoinNotAllowed($assoc); | ||
911 | } | ||
912 | } | ||
913 | |||
914 | $fetchMode = $this->query->getHint('fetchMode')[$assoc->sourceEntity][$assoc->fieldName] ?? $relation->fetch; | ||
915 | |||
916 | if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) { | ||
917 | throw QueryException::eagerFetchJoinWithNotAllowed($assoc->sourceEntity, $assoc->fieldName); | ||
918 | } | ||
919 | |||
920 | // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot | ||
921 | // be the owning side and previously we ensured that $assoc is always the owning side of the associations. | ||
922 | // The owning side is necessary at this point because only it contains the JoinColumn information. | ||
923 | switch (true) { | ||
924 | case $assoc->isToOne(): | ||
925 | assert($assoc->isToOneOwningSide()); | ||
926 | $conditions = []; | ||
927 | |||
928 | foreach ($assoc->joinColumns as $joinColumn) { | ||
929 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
930 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
931 | |||
932 | if ($relation->isOwningSide()) { | ||
933 | $conditions[] = $sourceTableAlias . '.' . $quotedSourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; | ||
934 | |||
935 | continue; | ||
936 | } | ||
937 | |||
938 | $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $quotedSourceColumn; | ||
939 | } | ||
940 | |||
941 | // Apply remaining inheritance restrictions | ||
942 | $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]); | ||
943 | |||
944 | if ($discrSql) { | ||
945 | $conditions[] = $discrSql; | ||
946 | } | ||
947 | |||
948 | // Apply the filters | ||
949 | $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); | ||
950 | |||
951 | if ($filterExpr) { | ||
952 | $conditions[] = $filterExpr; | ||
953 | } | ||
954 | |||
955 | $targetTableJoin = [ | ||
956 | 'table' => $targetTableName . ' ' . $targetTableAlias, | ||
957 | 'condition' => implode(' AND ', $conditions), | ||
958 | ]; | ||
959 | break; | ||
960 | |||
961 | case $assoc->isManyToMany(): | ||
962 | // Join relation table | ||
963 | $joinTable = $assoc->joinTable; | ||
964 | $joinTableAlias = $this->getSQLTableAlias($joinTable->name, $joinedDqlAlias); | ||
965 | $joinTableName = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->platform); | ||
966 | |||
967 | $conditions = []; | ||
968 | $relationColumns = $relation->isOwningSide() | ||
969 | ? $assoc->joinTable->joinColumns | ||
970 | : $assoc->joinTable->inverseJoinColumns; | ||
971 | |||
972 | foreach ($relationColumns as $joinColumn) { | ||
973 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
974 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
975 | |||
976 | $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; | ||
977 | } | ||
978 | |||
979 | $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions); | ||
980 | |||
981 | // Join target table | ||
982 | $sql .= $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER ? ' LEFT JOIN ' : ' INNER JOIN '; | ||
983 | |||
984 | $conditions = []; | ||
985 | $relationColumns = $relation->isOwningSide() | ||
986 | ? $assoc->joinTable->inverseJoinColumns | ||
987 | : $assoc->joinTable->joinColumns; | ||
988 | |||
989 | foreach ($relationColumns as $joinColumn) { | ||
990 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
991 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
992 | |||
993 | $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; | ||
994 | } | ||
995 | |||
996 | // Apply remaining inheritance restrictions | ||
997 | $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]); | ||
998 | |||
999 | if ($discrSql) { | ||
1000 | $conditions[] = $discrSql; | ||
1001 | } | ||
1002 | |||
1003 | // Apply the filters | ||
1004 | $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); | ||
1005 | |||
1006 | if ($filterExpr) { | ||
1007 | $conditions[] = $filterExpr; | ||
1008 | } | ||
1009 | |||
1010 | $targetTableJoin = [ | ||
1011 | 'table' => $targetTableName . ' ' . $targetTableAlias, | ||
1012 | 'condition' => implode(' AND ', $conditions), | ||
1013 | ]; | ||
1014 | break; | ||
1015 | |||
1016 | default: | ||
1017 | throw new BadMethodCallException('Type of association must be one of *_TO_ONE or MANY_TO_MANY'); | ||
1018 | } | ||
1019 | |||
1020 | // Handle WITH clause | ||
1021 | $withCondition = $condExpr === null ? '' : ('(' . $this->walkConditionalExpression($condExpr) . ')'); | ||
1022 | |||
1023 | if ($targetClass->isInheritanceTypeJoined()) { | ||
1024 | $ctiJoins = $this->generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); | ||
1025 | // If we have WITH condition, we need to build nested joins for target class table and cti joins | ||
1026 | if ($withCondition && $ctiJoins) { | ||
1027 | $sql .= '(' . $targetTableJoin['table'] . $ctiJoins . ') ON ' . $targetTableJoin['condition']; | ||
1028 | } else { | ||
1029 | $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'] . $ctiJoins; | ||
1030 | } | ||
1031 | } else { | ||
1032 | $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition']; | ||
1033 | } | ||
1034 | |||
1035 | if ($withCondition) { | ||
1036 | $sql .= ' AND ' . $withCondition; | ||
1037 | } | ||
1038 | |||
1039 | // Apply the indexes | ||
1040 | if ($indexBy) { | ||
1041 | // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. | ||
1042 | $this->walkIndexBy($indexBy); | ||
1043 | } elseif ($relation->isIndexed()) { | ||
1044 | $this->rsm->addIndexBy($joinedDqlAlias, $relation->indexBy()); | ||
1045 | } | ||
1046 | |||
1047 | return $sql; | ||
1048 | } | ||
1049 | |||
1050 | /** | ||
1051 | * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. | ||
1052 | */ | ||
1053 | public function walkFunction(AST\Functions\FunctionNode $function): string | ||
1054 | { | ||
1055 | return $function->getSql($this); | ||
1056 | } | ||
1057 | |||
1058 | /** | ||
1059 | * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. | ||
1060 | */ | ||
1061 | public function walkOrderByClause(AST\OrderByClause $orderByClause): string | ||
1062 | { | ||
1063 | $orderByItems = array_map($this->walkOrderByItem(...), $orderByClause->orderByItems); | ||
1064 | |||
1065 | $collectionOrderByItems = $this->generateOrderedCollectionOrderByItems(); | ||
1066 | if ($collectionOrderByItems !== '') { | ||
1067 | $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems); | ||
1068 | } | ||
1069 | |||
1070 | return ' ORDER BY ' . implode(', ', $orderByItems); | ||
1071 | } | ||
1072 | |||
1073 | /** | ||
1074 | * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. | ||
1075 | */ | ||
1076 | public function walkOrderByItem(AST\OrderByItem $orderByItem): string | ||
1077 | { | ||
1078 | $type = strtoupper($orderByItem->type); | ||
1079 | $expr = $orderByItem->expression; | ||
1080 | $sql = $expr instanceof AST\Node | ||
1081 | ? $expr->dispatch($this) | ||
1082 | : $this->walkResultVariable($this->queryComponents[$expr]['token']->value); | ||
1083 | |||
1084 | $this->orderedColumnsMap[$sql] = $type; | ||
1085 | |||
1086 | if ($expr instanceof AST\Subselect) { | ||
1087 | return '(' . $sql . ') ' . $type; | ||
1088 | } | ||
1089 | |||
1090 | return $sql . ' ' . $type; | ||
1091 | } | ||
1092 | |||
1093 | /** | ||
1094 | * Walks down a HavingClause AST node, thereby generating the appropriate SQL. | ||
1095 | */ | ||
1096 | public function walkHavingClause(AST\HavingClause $havingClause): string | ||
1097 | { | ||
1098 | return ' HAVING ' . $this->walkConditionalExpression($havingClause->conditionalExpression); | ||
1099 | } | ||
1100 | |||
1101 | /** | ||
1102 | * Walks down a Join AST node and creates the corresponding SQL. | ||
1103 | */ | ||
1104 | public function walkJoin(AST\Join $join): string | ||
1105 | { | ||
1106 | $joinType = $join->joinType; | ||
1107 | $joinDeclaration = $join->joinAssociationDeclaration; | ||
1108 | |||
1109 | $sql = $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER | ||
1110 | ? ' LEFT JOIN ' | ||
1111 | : ' INNER JOIN '; | ||
1112 | |||
1113 | switch (true) { | ||
1114 | case $joinDeclaration instanceof AST\RangeVariableDeclaration: | ||
1115 | $class = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName); | ||
1116 | $dqlAlias = $joinDeclaration->aliasIdentificationVariable; | ||
1117 | $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); | ||
1118 | $conditions = []; | ||
1119 | |||
1120 | if ($join->conditionalExpression) { | ||
1121 | $conditions[] = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; | ||
1122 | } | ||
1123 | |||
1124 | $isUnconditionalJoin = $conditions === []; | ||
1125 | $condExprConjunction = $class->isInheritanceTypeJoined() && $joinType !== AST\Join::JOIN_TYPE_LEFT && $joinType !== AST\Join::JOIN_TYPE_LEFTOUTER && $isUnconditionalJoin | ||
1126 | ? ' AND ' | ||
1127 | : ' ON '; | ||
1128 | |||
1129 | $sql .= $this->generateRangeVariableDeclarationSQL($joinDeclaration, ! $isUnconditionalJoin); | ||
1130 | |||
1131 | // Apply remaining inheritance restrictions | ||
1132 | $discrSql = $this->generateDiscriminatorColumnConditionSQL([$dqlAlias]); | ||
1133 | |||
1134 | if ($discrSql) { | ||
1135 | $conditions[] = $discrSql; | ||
1136 | } | ||
1137 | |||
1138 | // Apply the filters | ||
1139 | $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); | ||
1140 | |||
1141 | if ($filterExpr) { | ||
1142 | $conditions[] = $filterExpr; | ||
1143 | } | ||
1144 | |||
1145 | if ($conditions) { | ||
1146 | $sql .= $condExprConjunction . implode(' AND ', $conditions); | ||
1147 | } | ||
1148 | |||
1149 | break; | ||
1150 | |||
1151 | case $joinDeclaration instanceof AST\JoinAssociationDeclaration: | ||
1152 | $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType, $join->conditionalExpression); | ||
1153 | break; | ||
1154 | } | ||
1155 | |||
1156 | return $sql; | ||
1157 | } | ||
1158 | |||
1159 | /** | ||
1160 | * Walks down a CoalesceExpression AST node and generates the corresponding SQL. | ||
1161 | */ | ||
1162 | public function walkCoalesceExpression(AST\CoalesceExpression $coalesceExpression): string | ||
1163 | { | ||
1164 | $sql = 'COALESCE('; | ||
1165 | |||
1166 | $scalarExpressions = []; | ||
1167 | |||
1168 | foreach ($coalesceExpression->scalarExpressions as $scalarExpression) { | ||
1169 | $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression); | ||
1170 | } | ||
1171 | |||
1172 | return $sql . implode(', ', $scalarExpressions) . ')'; | ||
1173 | } | ||
1174 | |||
1175 | /** | ||
1176 | * Walks down a NullIfExpression AST node and generates the corresponding SQL. | ||
1177 | */ | ||
1178 | public function walkNullIfExpression(AST\NullIfExpression $nullIfExpression): string | ||
1179 | { | ||
1180 | $firstExpression = is_string($nullIfExpression->firstExpression) | ||
1181 | ? $this->conn->quote($nullIfExpression->firstExpression) | ||
1182 | : $this->walkSimpleArithmeticExpression($nullIfExpression->firstExpression); | ||
1183 | |||
1184 | $secondExpression = is_string($nullIfExpression->secondExpression) | ||
1185 | ? $this->conn->quote($nullIfExpression->secondExpression) | ||
1186 | : $this->walkSimpleArithmeticExpression($nullIfExpression->secondExpression); | ||
1187 | |||
1188 | return 'NULLIF(' . $firstExpression . ', ' . $secondExpression . ')'; | ||
1189 | } | ||
1190 | |||
1191 | /** | ||
1192 | * Walks down a GeneralCaseExpression AST node and generates the corresponding SQL. | ||
1193 | */ | ||
1194 | public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression): string | ||
1195 | { | ||
1196 | $sql = 'CASE'; | ||
1197 | |||
1198 | foreach ($generalCaseExpression->whenClauses as $whenClause) { | ||
1199 | $sql .= ' WHEN ' . $this->walkConditionalExpression($whenClause->caseConditionExpression); | ||
1200 | $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($whenClause->thenScalarExpression); | ||
1201 | } | ||
1202 | |||
1203 | $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression) . ' END'; | ||
1204 | |||
1205 | return $sql; | ||
1206 | } | ||
1207 | |||
1208 | /** | ||
1209 | * Walks down a SimpleCaseExpression AST node and generates the corresponding SQL. | ||
1210 | */ | ||
1211 | public function walkSimpleCaseExpression(AST\SimpleCaseExpression $simpleCaseExpression): string | ||
1212 | { | ||
1213 | $sql = 'CASE ' . $this->walkStateFieldPathExpression($simpleCaseExpression->caseOperand); | ||
1214 | |||
1215 | foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) { | ||
1216 | $sql .= ' WHEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression); | ||
1217 | $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression); | ||
1218 | } | ||
1219 | |||
1220 | $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression) . ' END'; | ||
1221 | |||
1222 | return $sql; | ||
1223 | } | ||
1224 | |||
1225 | /** | ||
1226 | * Walks down a SelectExpression AST node and generates the corresponding SQL. | ||
1227 | */ | ||
1228 | public function walkSelectExpression(AST\SelectExpression $selectExpression): string | ||
1229 | { | ||
1230 | $sql = ''; | ||
1231 | $expr = $selectExpression->expression; | ||
1232 | $hidden = $selectExpression->hiddenAliasResultVariable; | ||
1233 | |||
1234 | switch (true) { | ||
1235 | case $expr instanceof AST\PathExpression: | ||
1236 | if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) { | ||
1237 | throw QueryException::invalidPathExpression($expr); | ||
1238 | } | ||
1239 | |||
1240 | assert($expr->field !== null); | ||
1241 | $fieldName = $expr->field; | ||
1242 | $dqlAlias = $expr->identificationVariable; | ||
1243 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1244 | |||
1245 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: $fieldName; | ||
1246 | $tableName = $class->isInheritanceTypeJoined() | ||
1247 | ? $this->em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName) | ||
1248 | : $class->getTableName(); | ||
1249 | |||
1250 | $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); | ||
1251 | $fieldMapping = $class->fieldMappings[$fieldName]; | ||
1252 | $columnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
1253 | $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); | ||
1254 | $col = $sqlTableAlias . '.' . $columnName; | ||
1255 | |||
1256 | $type = Type::getType($fieldMapping->type); | ||
1257 | $col = $type->convertToPHPValueSQL($col, $this->conn->getDatabasePlatform()); | ||
1258 | |||
1259 | $sql .= $col . ' AS ' . $columnAlias; | ||
1260 | |||
1261 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
1262 | |||
1263 | if (! $hidden) { | ||
1264 | $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldMapping->type); | ||
1265 | $this->scalarFields[$dqlAlias][$fieldName] = $columnAlias; | ||
1266 | |||
1267 | if (! empty($fieldMapping->enumType)) { | ||
1268 | $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); | ||
1269 | } | ||
1270 | } | ||
1271 | |||
1272 | break; | ||
1273 | |||
1274 | case $expr instanceof AST\AggregateExpression: | ||
1275 | case $expr instanceof AST\Functions\FunctionNode: | ||
1276 | case $expr instanceof AST\SimpleArithmeticExpression: | ||
1277 | case $expr instanceof AST\ArithmeticTerm: | ||
1278 | case $expr instanceof AST\ArithmeticFactor: | ||
1279 | case $expr instanceof AST\ParenthesisExpression: | ||
1280 | case $expr instanceof AST\Literal: | ||
1281 | case $expr instanceof AST\NullIfExpression: | ||
1282 | case $expr instanceof AST\CoalesceExpression: | ||
1283 | case $expr instanceof AST\GeneralCaseExpression: | ||
1284 | case $expr instanceof AST\SimpleCaseExpression: | ||
1285 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
1286 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
1287 | |||
1288 | $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; | ||
1289 | |||
1290 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
1291 | |||
1292 | if ($hidden) { | ||
1293 | break; | ||
1294 | } | ||
1295 | |||
1296 | if (! $expr instanceof Query\AST\TypedExpression) { | ||
1297 | // Conceptually we could resolve field type here by traverse through AST to retrieve field type, | ||
1298 | // but this is not a feasible solution; assume 'string'. | ||
1299 | $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string'); | ||
1300 | |||
1301 | break; | ||
1302 | } | ||
1303 | |||
1304 | $this->rsm->addScalarResult($columnAlias, $resultAlias, Type::getTypeRegistry()->lookupName($expr->getReturnType())); | ||
1305 | |||
1306 | break; | ||
1307 | |||
1308 | case $expr instanceof AST\Subselect: | ||
1309 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
1310 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
1311 | |||
1312 | $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; | ||
1313 | |||
1314 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
1315 | |||
1316 | if (! $hidden) { | ||
1317 | // We cannot resolve field type here; assume 'string'. | ||
1318 | $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string'); | ||
1319 | } | ||
1320 | |||
1321 | break; | ||
1322 | |||
1323 | case $expr instanceof AST\NewObjectExpression: | ||
1324 | $sql .= $this->walkNewObject($expr, $selectExpression->fieldIdentificationVariable); | ||
1325 | break; | ||
1326 | |||
1327 | default: | ||
1328 | $dqlAlias = $expr; | ||
1329 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1330 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: null; | ||
1331 | |||
1332 | if (! isset($this->selectedClasses[$dqlAlias])) { | ||
1333 | $this->selectedClasses[$dqlAlias] = [ | ||
1334 | 'class' => $class, | ||
1335 | 'dqlAlias' => $dqlAlias, | ||
1336 | 'resultAlias' => $resultAlias, | ||
1337 | ]; | ||
1338 | } | ||
1339 | |||
1340 | $sqlParts = []; | ||
1341 | |||
1342 | // Select all fields from the queried class | ||
1343 | foreach ($class->fieldMappings as $fieldName => $mapping) { | ||
1344 | $tableName = isset($mapping->inherited) | ||
1345 | ? $this->em->getClassMetadata($mapping->inherited)->getTableName() | ||
1346 | : $class->getTableName(); | ||
1347 | |||
1348 | $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); | ||
1349 | $columnAlias = $this->getSQLColumnAlias($mapping->columnName); | ||
1350 | $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
1351 | |||
1352 | $col = $sqlTableAlias . '.' . $quotedColumnName; | ||
1353 | |||
1354 | $type = Type::getType($mapping->type); | ||
1355 | $col = $type->convertToPHPValueSQL($col, $this->platform); | ||
1356 | |||
1357 | $sqlParts[] = $col . ' AS ' . $columnAlias; | ||
1358 | |||
1359 | $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; | ||
1360 | |||
1361 | $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); | ||
1362 | |||
1363 | if (! empty($mapping->enumType)) { | ||
1364 | $this->rsm->addEnumResult($columnAlias, $mapping->enumType); | ||
1365 | } | ||
1366 | } | ||
1367 | |||
1368 | // Add any additional fields of subclasses (excluding inherited fields) | ||
1369 | // 1) on Single Table Inheritance: always, since its marginal overhead | ||
1370 | // 2) on Class Table Inheritance | ||
1371 | foreach ($class->subClasses as $subClassName) { | ||
1372 | $subClass = $this->em->getClassMetadata($subClassName); | ||
1373 | $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); | ||
1374 | |||
1375 | foreach ($subClass->fieldMappings as $fieldName => $mapping) { | ||
1376 | if (isset($mapping->inherited)) { | ||
1377 | continue; | ||
1378 | } | ||
1379 | |||
1380 | $columnAlias = $this->getSQLColumnAlias($mapping->columnName); | ||
1381 | $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform); | ||
1382 | |||
1383 | $col = $sqlTableAlias . '.' . $quotedColumnName; | ||
1384 | |||
1385 | $type = Type::getType($mapping->type); | ||
1386 | $col = $type->convertToPHPValueSQL($col, $this->platform); | ||
1387 | |||
1388 | $sqlParts[] = $col . ' AS ' . $columnAlias; | ||
1389 | |||
1390 | $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; | ||
1391 | |||
1392 | $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); | ||
1393 | } | ||
1394 | } | ||
1395 | |||
1396 | $sql .= implode(', ', $sqlParts); | ||
1397 | } | ||
1398 | |||
1399 | return $sql; | ||
1400 | } | ||
1401 | |||
1402 | public function walkQuantifiedExpression(AST\QuantifiedExpression $qExpr): string | ||
1403 | { | ||
1404 | return ' ' . strtoupper($qExpr->type) . '(' . $this->walkSubselect($qExpr->subselect) . ')'; | ||
1405 | } | ||
1406 | |||
1407 | /** | ||
1408 | * Walks down a Subselect AST node, thereby generating the appropriate SQL. | ||
1409 | */ | ||
1410 | public function walkSubselect(AST\Subselect $subselect): string | ||
1411 | { | ||
1412 | $useAliasesBefore = $this->useSqlTableAliases; | ||
1413 | $rootAliasesBefore = $this->rootAliases; | ||
1414 | |||
1415 | $this->rootAliases = []; // reset the rootAliases for the subselect | ||
1416 | $this->useSqlTableAliases = true; | ||
1417 | |||
1418 | $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause); | ||
1419 | $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause); | ||
1420 | $sql .= $this->walkWhereClause($subselect->whereClause); | ||
1421 | |||
1422 | $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : ''; | ||
1423 | $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : ''; | ||
1424 | $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : ''; | ||
1425 | |||
1426 | $this->rootAliases = $rootAliasesBefore; // put the main aliases back | ||
1427 | $this->useSqlTableAliases = $useAliasesBefore; | ||
1428 | |||
1429 | return $sql; | ||
1430 | } | ||
1431 | |||
1432 | /** | ||
1433 | * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. | ||
1434 | */ | ||
1435 | public function walkSubselectFromClause(AST\SubselectFromClause $subselectFromClause): string | ||
1436 | { | ||
1437 | $identificationVarDecls = $subselectFromClause->identificationVariableDeclarations; | ||
1438 | $sqlParts = []; | ||
1439 | |||
1440 | foreach ($identificationVarDecls as $subselectIdVarDecl) { | ||
1441 | $sqlParts[] = $this->walkIdentificationVariableDeclaration($subselectIdVarDecl); | ||
1442 | } | ||
1443 | |||
1444 | return ' FROM ' . implode(', ', $sqlParts); | ||
1445 | } | ||
1446 | |||
1447 | /** | ||
1448 | * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. | ||
1449 | */ | ||
1450 | public function walkSimpleSelectClause(AST\SimpleSelectClause $simpleSelectClause): string | ||
1451 | { | ||
1452 | return 'SELECT' . ($simpleSelectClause->isDistinct ? ' DISTINCT' : '') | ||
1453 | . $this->walkSimpleSelectExpression($simpleSelectClause->simpleSelectExpression); | ||
1454 | } | ||
1455 | |||
1456 | public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesisExpression): string | ||
1457 | { | ||
1458 | return sprintf('(%s)', $parenthesisExpression->expression->dispatch($this)); | ||
1459 | } | ||
1460 | |||
1461 | public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string | ||
1462 | { | ||
1463 | $sqlSelectExpressions = []; | ||
1464 | $objIndex = $newObjectResultAlias ?: $this->newObjectCounter++; | ||
1465 | |||
1466 | foreach ($newObjectExpression->args as $argIndex => $e) { | ||
1467 | $resultAlias = $this->scalarResultCounter++; | ||
1468 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
1469 | $fieldType = 'string'; | ||
1470 | |||
1471 | switch (true) { | ||
1472 | case $e instanceof AST\NewObjectExpression: | ||
1473 | $sqlSelectExpressions[] = $e->dispatch($this); | ||
1474 | break; | ||
1475 | |||
1476 | case $e instanceof AST\Subselect: | ||
1477 | $sqlSelectExpressions[] = '(' . $e->dispatch($this) . ') AS ' . $columnAlias; | ||
1478 | break; | ||
1479 | |||
1480 | case $e instanceof AST\PathExpression: | ||
1481 | assert($e->field !== null); | ||
1482 | $dqlAlias = $e->identificationVariable; | ||
1483 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1484 | $fieldName = $e->field; | ||
1485 | $fieldMapping = $class->fieldMappings[$fieldName]; | ||
1486 | $fieldType = $fieldMapping->type; | ||
1487 | $col = trim($e->dispatch($this)); | ||
1488 | |||
1489 | $type = Type::getType($fieldType); | ||
1490 | $col = $type->convertToPHPValueSQL($col, $this->platform); | ||
1491 | |||
1492 | $sqlSelectExpressions[] = $col . ' AS ' . $columnAlias; | ||
1493 | |||
1494 | if (! empty($fieldMapping->enumType)) { | ||
1495 | $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); | ||
1496 | } | ||
1497 | |||
1498 | break; | ||
1499 | |||
1500 | case $e instanceof AST\Literal: | ||
1501 | switch ($e->type) { | ||
1502 | case AST\Literal::BOOLEAN: | ||
1503 | $fieldType = 'boolean'; | ||
1504 | break; | ||
1505 | |||
1506 | case AST\Literal::NUMERIC: | ||
1507 | $fieldType = is_float($e->value) ? 'float' : 'integer'; | ||
1508 | break; | ||
1509 | } | ||
1510 | |||
1511 | $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; | ||
1512 | break; | ||
1513 | |||
1514 | default: | ||
1515 | $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; | ||
1516 | break; | ||
1517 | } | ||
1518 | |||
1519 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
1520 | $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType); | ||
1521 | |||
1522 | $this->rsm->newObjectMappings[$columnAlias] = [ | ||
1523 | 'className' => $newObjectExpression->className, | ||
1524 | 'objIndex' => $objIndex, | ||
1525 | 'argIndex' => $argIndex, | ||
1526 | ]; | ||
1527 | } | ||
1528 | |||
1529 | return implode(', ', $sqlSelectExpressions); | ||
1530 | } | ||
1531 | |||
1532 | /** | ||
1533 | * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. | ||
1534 | */ | ||
1535 | public function walkSimpleSelectExpression(AST\SimpleSelectExpression $simpleSelectExpression): string | ||
1536 | { | ||
1537 | $expr = $simpleSelectExpression->expression; | ||
1538 | $sql = ' '; | ||
1539 | |||
1540 | switch (true) { | ||
1541 | case $expr instanceof AST\PathExpression: | ||
1542 | $sql .= $this->walkPathExpression($expr); | ||
1543 | break; | ||
1544 | |||
1545 | case $expr instanceof AST\Subselect: | ||
1546 | $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
1547 | |||
1548 | $columnAlias = 'sclr' . $this->aliasCounter++; | ||
1549 | $this->scalarResultAliasMap[$alias] = $columnAlias; | ||
1550 | |||
1551 | $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; | ||
1552 | break; | ||
1553 | |||
1554 | case $expr instanceof AST\Functions\FunctionNode: | ||
1555 | case $expr instanceof AST\SimpleArithmeticExpression: | ||
1556 | case $expr instanceof AST\ArithmeticTerm: | ||
1557 | case $expr instanceof AST\ArithmeticFactor: | ||
1558 | case $expr instanceof AST\Literal: | ||
1559 | case $expr instanceof AST\NullIfExpression: | ||
1560 | case $expr instanceof AST\CoalesceExpression: | ||
1561 | case $expr instanceof AST\GeneralCaseExpression: | ||
1562 | case $expr instanceof AST\SimpleCaseExpression: | ||
1563 | $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
1564 | |||
1565 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
1566 | $this->scalarResultAliasMap[$alias] = $columnAlias; | ||
1567 | |||
1568 | $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; | ||
1569 | break; | ||
1570 | |||
1571 | case $expr instanceof AST\ParenthesisExpression: | ||
1572 | $sql .= $this->walkParenthesisExpression($expr); | ||
1573 | break; | ||
1574 | |||
1575 | default: // IdentificationVariable | ||
1576 | $sql .= $this->walkEntityIdentificationVariable($expr); | ||
1577 | break; | ||
1578 | } | ||
1579 | |||
1580 | return $sql; | ||
1581 | } | ||
1582 | |||
1583 | /** | ||
1584 | * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. | ||
1585 | */ | ||
1586 | public function walkAggregateExpression(AST\AggregateExpression $aggExpression): string | ||
1587 | { | ||
1588 | return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '') | ||
1589 | . $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) . ')'; | ||
1590 | } | ||
1591 | |||
1592 | /** | ||
1593 | * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. | ||
1594 | */ | ||
1595 | public function walkGroupByClause(AST\GroupByClause $groupByClause): string | ||
1596 | { | ||
1597 | $sqlParts = []; | ||
1598 | |||
1599 | foreach ($groupByClause->groupByItems as $groupByItem) { | ||
1600 | $sqlParts[] = $this->walkGroupByItem($groupByItem); | ||
1601 | } | ||
1602 | |||
1603 | return ' GROUP BY ' . implode(', ', $sqlParts); | ||
1604 | } | ||
1605 | |||
1606 | /** | ||
1607 | * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. | ||
1608 | */ | ||
1609 | public function walkGroupByItem(AST\PathExpression|string $groupByItem): string | ||
1610 | { | ||
1611 | // StateFieldPathExpression | ||
1612 | if (! is_string($groupByItem)) { | ||
1613 | return $this->walkPathExpression($groupByItem); | ||
1614 | } | ||
1615 | |||
1616 | // ResultVariable | ||
1617 | if (isset($this->queryComponents[$groupByItem]['resultVariable'])) { | ||
1618 | $resultVariable = $this->queryComponents[$groupByItem]['resultVariable']; | ||
1619 | |||
1620 | if ($resultVariable instanceof AST\PathExpression) { | ||
1621 | return $this->walkPathExpression($resultVariable); | ||
1622 | } | ||
1623 | |||
1624 | if ($resultVariable instanceof AST\Node && isset($resultVariable->pathExpression)) { | ||
1625 | return $this->walkPathExpression($resultVariable->pathExpression); | ||
1626 | } | ||
1627 | |||
1628 | return $this->walkResultVariable($groupByItem); | ||
1629 | } | ||
1630 | |||
1631 | // IdentificationVariable | ||
1632 | $sqlParts = []; | ||
1633 | |||
1634 | foreach ($this->getMetadataForDqlAlias($groupByItem)->fieldNames as $field) { | ||
1635 | $item = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $field); | ||
1636 | $item->type = AST\PathExpression::TYPE_STATE_FIELD; | ||
1637 | |||
1638 | $sqlParts[] = $this->walkPathExpression($item); | ||
1639 | } | ||
1640 | |||
1641 | foreach ($this->getMetadataForDqlAlias($groupByItem)->associationMappings as $mapping) { | ||
1642 | if ($mapping->isToOneOwningSide()) { | ||
1643 | $item = new AST\PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $groupByItem, $mapping->fieldName); | ||
1644 | $item->type = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; | ||
1645 | |||
1646 | $sqlParts[] = $this->walkPathExpression($item); | ||
1647 | } | ||
1648 | } | ||
1649 | |||
1650 | return implode(', ', $sqlParts); | ||
1651 | } | ||
1652 | |||
1653 | /** | ||
1654 | * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. | ||
1655 | */ | ||
1656 | public function walkDeleteClause(AST\DeleteClause $deleteClause): string | ||
1657 | { | ||
1658 | $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName); | ||
1659 | $tableName = $class->getTableName(); | ||
1660 | $sql = 'DELETE FROM ' . $this->quoteStrategy->getTableName($class, $this->platform); | ||
1661 | |||
1662 | $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable); | ||
1663 | $this->rootAliases[] = $deleteClause->aliasIdentificationVariable; | ||
1664 | |||
1665 | return $sql; | ||
1666 | } | ||
1667 | |||
1668 | /** | ||
1669 | * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. | ||
1670 | */ | ||
1671 | public function walkUpdateClause(AST\UpdateClause $updateClause): string | ||
1672 | { | ||
1673 | $class = $this->em->getClassMetadata($updateClause->abstractSchemaName); | ||
1674 | $tableName = $class->getTableName(); | ||
1675 | $sql = 'UPDATE ' . $this->quoteStrategy->getTableName($class, $this->platform); | ||
1676 | |||
1677 | $this->setSQLTableAlias($tableName, $tableName, $updateClause->aliasIdentificationVariable); | ||
1678 | $this->rootAliases[] = $updateClause->aliasIdentificationVariable; | ||
1679 | |||
1680 | return $sql . ' SET ' . implode(', ', array_map($this->walkUpdateItem(...), $updateClause->updateItems)); | ||
1681 | } | ||
1682 | |||
1683 | /** | ||
1684 | * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. | ||
1685 | */ | ||
1686 | public function walkUpdateItem(AST\UpdateItem $updateItem): string | ||
1687 | { | ||
1688 | $useTableAliasesBefore = $this->useSqlTableAliases; | ||
1689 | $this->useSqlTableAliases = false; | ||
1690 | |||
1691 | $sql = $this->walkPathExpression($updateItem->pathExpression) . ' = '; | ||
1692 | $newValue = $updateItem->newValue; | ||
1693 | |||
1694 | $sql .= match (true) { | ||
1695 | $newValue instanceof AST\Node => $newValue->dispatch($this), | ||
1696 | $newValue === null => 'NULL', | ||
1697 | }; | ||
1698 | |||
1699 | $this->useSqlTableAliases = $useTableAliasesBefore; | ||
1700 | |||
1701 | return $sql; | ||
1702 | } | ||
1703 | |||
1704 | /** | ||
1705 | * Walks down a WhereClause AST node, thereby generating the appropriate SQL. | ||
1706 | * | ||
1707 | * WhereClause or not, the appropriate discriminator sql is added. | ||
1708 | */ | ||
1709 | public function walkWhereClause(AST\WhereClause|null $whereClause): string | ||
1710 | { | ||
1711 | $condSql = $whereClause !== null ? $this->walkConditionalExpression($whereClause->conditionalExpression) : ''; | ||
1712 | $discrSql = $this->generateDiscriminatorColumnConditionSQL($this->rootAliases); | ||
1713 | |||
1714 | if ($this->em->hasFilters()) { | ||
1715 | $filterClauses = []; | ||
1716 | foreach ($this->rootAliases as $dqlAlias) { | ||
1717 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1718 | $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); | ||
1719 | |||
1720 | $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); | ||
1721 | if ($filterExpr) { | ||
1722 | $filterClauses[] = $filterExpr; | ||
1723 | } | ||
1724 | } | ||
1725 | |||
1726 | if (count($filterClauses)) { | ||
1727 | if ($condSql) { | ||
1728 | $condSql = '(' . $condSql . ') AND '; | ||
1729 | } | ||
1730 | |||
1731 | $condSql .= implode(' AND ', $filterClauses); | ||
1732 | } | ||
1733 | } | ||
1734 | |||
1735 | if ($condSql) { | ||
1736 | return ' WHERE ' . (! $discrSql ? $condSql : '(' . $condSql . ') AND ' . $discrSql); | ||
1737 | } | ||
1738 | |||
1739 | if ($discrSql) { | ||
1740 | return ' WHERE ' . $discrSql; | ||
1741 | } | ||
1742 | |||
1743 | return ''; | ||
1744 | } | ||
1745 | |||
1746 | /** | ||
1747 | * Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL. | ||
1748 | */ | ||
1749 | public function walkConditionalExpression( | ||
1750 | AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr, | ||
1751 | ): string { | ||
1752 | // Phase 2 AST optimization: Skip processing of ConditionalExpression | ||
1753 | // if only one ConditionalTerm is defined | ||
1754 | if (! ($condExpr instanceof AST\ConditionalExpression)) { | ||
1755 | return $this->walkConditionalTerm($condExpr); | ||
1756 | } | ||
1757 | |||
1758 | return implode(' OR ', array_map($this->walkConditionalTerm(...), $condExpr->conditionalTerms)); | ||
1759 | } | ||
1760 | |||
1761 | /** | ||
1762 | * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. | ||
1763 | */ | ||
1764 | public function walkConditionalTerm( | ||
1765 | AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm, | ||
1766 | ): string { | ||
1767 | // Phase 2 AST optimization: Skip processing of ConditionalTerm | ||
1768 | // if only one ConditionalFactor is defined | ||
1769 | if (! ($condTerm instanceof AST\ConditionalTerm)) { | ||
1770 | return $this->walkConditionalFactor($condTerm); | ||
1771 | } | ||
1772 | |||
1773 | return implode(' AND ', array_map($this->walkConditionalFactor(...), $condTerm->conditionalFactors)); | ||
1774 | } | ||
1775 | |||
1776 | /** | ||
1777 | * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. | ||
1778 | */ | ||
1779 | public function walkConditionalFactor( | ||
1780 | AST\ConditionalFactor|AST\ConditionalPrimary $factor, | ||
1781 | ): string { | ||
1782 | // Phase 2 AST optimization: Skip processing of ConditionalFactor | ||
1783 | // if only one ConditionalPrimary is defined | ||
1784 | return ! ($factor instanceof AST\ConditionalFactor) | ||
1785 | ? $this->walkConditionalPrimary($factor) | ||
1786 | : ($factor->not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary); | ||
1787 | } | ||
1788 | |||
1789 | /** | ||
1790 | * Walks down a ConditionalPrimary AST node, thereby generating the appropriate SQL. | ||
1791 | */ | ||
1792 | public function walkConditionalPrimary(AST\ConditionalPrimary $primary): string | ||
1793 | { | ||
1794 | if ($primary->isSimpleConditionalExpression()) { | ||
1795 | return $primary->simpleConditionalExpression->dispatch($this); | ||
1796 | } | ||
1797 | |||
1798 | if ($primary->isConditionalExpression()) { | ||
1799 | $condExpr = $primary->conditionalExpression; | ||
1800 | |||
1801 | return '(' . $this->walkConditionalExpression($condExpr) . ')'; | ||
1802 | } | ||
1803 | |||
1804 | throw new LogicException('Unexpected state of ConditionalPrimary node.'); | ||
1805 | } | ||
1806 | |||
1807 | /** | ||
1808 | * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. | ||
1809 | */ | ||
1810 | public function walkExistsExpression(AST\ExistsExpression $existsExpr): string | ||
1811 | { | ||
1812 | $sql = $existsExpr->not ? 'NOT ' : ''; | ||
1813 | |||
1814 | $sql .= 'EXISTS (' . $this->walkSubselect($existsExpr->subselect) . ')'; | ||
1815 | |||
1816 | return $sql; | ||
1817 | } | ||
1818 | |||
1819 | /** | ||
1820 | * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. | ||
1821 | */ | ||
1822 | public function walkCollectionMemberExpression(AST\CollectionMemberExpression $collMemberExpr): string | ||
1823 | { | ||
1824 | $sql = $collMemberExpr->not ? 'NOT ' : ''; | ||
1825 | $sql .= 'EXISTS (SELECT 1 FROM '; | ||
1826 | |||
1827 | $entityExpr = $collMemberExpr->entityExpression; | ||
1828 | $collPathExpr = $collMemberExpr->collectionValuedPathExpression; | ||
1829 | assert($collPathExpr->field !== null); | ||
1830 | |||
1831 | $fieldName = $collPathExpr->field; | ||
1832 | $dqlAlias = $collPathExpr->identificationVariable; | ||
1833 | |||
1834 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1835 | |||
1836 | switch (true) { | ||
1837 | // InputParameter | ||
1838 | case $entityExpr instanceof AST\InputParameter: | ||
1839 | $dqlParamKey = $entityExpr->name; | ||
1840 | $entitySql = '?'; | ||
1841 | break; | ||
1842 | |||
1843 | // SingleValuedAssociationPathExpression | IdentificationVariable | ||
1844 | case $entityExpr instanceof AST\PathExpression: | ||
1845 | $entitySql = $this->walkPathExpression($entityExpr); | ||
1846 | break; | ||
1847 | |||
1848 | default: | ||
1849 | throw new BadMethodCallException('Not implemented'); | ||
1850 | } | ||
1851 | |||
1852 | $assoc = $class->associationMappings[$fieldName]; | ||
1853 | |||
1854 | if ($assoc->isOneToMany()) { | ||
1855 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
1856 | $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName()); | ||
1857 | $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
1858 | |||
1859 | $sql .= $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' ' . $targetTableAlias . ' WHERE '; | ||
1860 | |||
1861 | $owningAssoc = $targetClass->associationMappings[$assoc->mappedBy]; | ||
1862 | assert($owningAssoc->isManyToOne()); | ||
1863 | $sqlParts = []; | ||
1864 | |||
1865 | foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) { | ||
1866 | $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $this->platform); | ||
1867 | |||
1868 | $sqlParts[] = $sourceTableAlias . '.' . $targetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; | ||
1869 | } | ||
1870 | |||
1871 | foreach ($this->quoteStrategy->getIdentifierColumnNames($targetClass, $this->platform) as $targetColumnName) { | ||
1872 | if (isset($dqlParamKey)) { | ||
1873 | $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); | ||
1874 | } | ||
1875 | |||
1876 | $sqlParts[] = $targetTableAlias . '.' . $targetColumnName . ' = ' . $entitySql; | ||
1877 | } | ||
1878 | |||
1879 | $sql .= implode(' AND ', $sqlParts); | ||
1880 | } else { // many-to-many | ||
1881 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
1882 | |||
1883 | $owningAssoc = $this->em->getMetadataFactory()->getOwningSide($assoc); | ||
1884 | assert($owningAssoc->isManyToManyOwningSide()); | ||
1885 | $joinTable = $owningAssoc->joinTable; | ||
1886 | |||
1887 | // SQL table aliases | ||
1888 | $joinTableAlias = $this->getSQLTableAlias($joinTable->name); | ||
1889 | $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
1890 | |||
1891 | $sql .= $this->quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $this->platform) . ' ' . $joinTableAlias . ' WHERE '; | ||
1892 | |||
1893 | $joinColumns = $assoc->isOwningSide() ? $joinTable->joinColumns : $joinTable->inverseJoinColumns; | ||
1894 | $sqlParts = []; | ||
1895 | |||
1896 | foreach ($joinColumns as $joinColumn) { | ||
1897 | $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$joinColumn->referencedColumnName], $class, $this->platform); | ||
1898 | |||
1899 | $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' = ' . $sourceTableAlias . '.' . $targetColumn; | ||
1900 | } | ||
1901 | |||
1902 | $joinColumns = $assoc->isOwningSide() ? $joinTable->inverseJoinColumns : $joinTable->joinColumns; | ||
1903 | |||
1904 | foreach ($joinColumns as $joinColumn) { | ||
1905 | if (isset($dqlParamKey)) { | ||
1906 | $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); | ||
1907 | } | ||
1908 | |||
1909 | $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' IN (' . $entitySql . ')'; | ||
1910 | } | ||
1911 | |||
1912 | $sql .= implode(' AND ', $sqlParts); | ||
1913 | } | ||
1914 | |||
1915 | return $sql . ')'; | ||
1916 | } | ||
1917 | |||
1918 | /** | ||
1919 | * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL. | ||
1920 | */ | ||
1921 | public function walkEmptyCollectionComparisonExpression(AST\EmptyCollectionComparisonExpression $emptyCollCompExpr): string | ||
1922 | { | ||
1923 | $sizeFunc = new AST\Functions\SizeFunction('size'); | ||
1924 | $sizeFunc->collectionPathExpression = $emptyCollCompExpr->expression; | ||
1925 | |||
1926 | return $sizeFunc->getSql($this) . ($emptyCollCompExpr->not ? ' > 0' : ' = 0'); | ||
1927 | } | ||
1928 | |||
1929 | /** | ||
1930 | * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. | ||
1931 | */ | ||
1932 | public function walkNullComparisonExpression(AST\NullComparisonExpression $nullCompExpr): string | ||
1933 | { | ||
1934 | $expression = $nullCompExpr->expression; | ||
1935 | $comparison = ' IS' . ($nullCompExpr->not ? ' NOT' : '') . ' NULL'; | ||
1936 | |||
1937 | // Handle ResultVariable | ||
1938 | if (is_string($expression) && isset($this->queryComponents[$expression]['resultVariable'])) { | ||
1939 | return $this->walkResultVariable($expression) . $comparison; | ||
1940 | } | ||
1941 | |||
1942 | // Handle InputParameter mapping inclusion to ParserResult | ||
1943 | if ($expression instanceof AST\InputParameter) { | ||
1944 | return $this->walkInputParameter($expression) . $comparison; | ||
1945 | } | ||
1946 | |||
1947 | assert(! is_string($expression)); | ||
1948 | |||
1949 | return $expression->dispatch($this) . $comparison; | ||
1950 | } | ||
1951 | |||
1952 | /** | ||
1953 | * Walks down an InExpression AST node, thereby generating the appropriate SQL. | ||
1954 | */ | ||
1955 | public function walkInListExpression(AST\InListExpression $inExpr): string | ||
1956 | { | ||
1957 | return $this->walkArithmeticExpression($inExpr->expression) | ||
1958 | . ($inExpr->not ? ' NOT' : '') . ' IN (' | ||
1959 | . implode(', ', array_map($this->walkInParameter(...), $inExpr->literals)) | ||
1960 | . ')'; | ||
1961 | } | ||
1962 | |||
1963 | /** | ||
1964 | * Walks down an InExpression AST node, thereby generating the appropriate SQL. | ||
1965 | */ | ||
1966 | public function walkInSubselectExpression(AST\InSubselectExpression $inExpr): string | ||
1967 | { | ||
1968 | return $this->walkArithmeticExpression($inExpr->expression) | ||
1969 | . ($inExpr->not ? ' NOT' : '') . ' IN (' | ||
1970 | . $this->walkSubselect($inExpr->subselect) | ||
1971 | . ')'; | ||
1972 | } | ||
1973 | |||
1974 | /** | ||
1975 | * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. | ||
1976 | * | ||
1977 | * @throws QueryException | ||
1978 | */ | ||
1979 | public function walkInstanceOfExpression(AST\InstanceOfExpression $instanceOfExpr): string | ||
1980 | { | ||
1981 | $sql = ''; | ||
1982 | |||
1983 | $dqlAlias = $instanceOfExpr->identificationVariable; | ||
1984 | $discrClass = $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1985 | |||
1986 | if ($class->discriminatorColumn) { | ||
1987 | $discrClass = $this->em->getClassMetadata($class->rootEntityName); | ||
1988 | } | ||
1989 | |||
1990 | if ($this->useSqlTableAliases) { | ||
1991 | $sql .= $this->getSQLTableAlias($discrClass->getTableName(), $dqlAlias) . '.'; | ||
1992 | } | ||
1993 | |||
1994 | $sql .= $class->getDiscriminatorColumn()->name . ($instanceOfExpr->not ? ' NOT IN ' : ' IN '); | ||
1995 | $sql .= $this->getChildDiscriminatorsFromClassMetadata($discrClass, $instanceOfExpr); | ||
1996 | |||
1997 | return $sql; | ||
1998 | } | ||
1999 | |||
2000 | public function walkInParameter(mixed $inParam): string | ||
2001 | { | ||
2002 | return $inParam instanceof AST\InputParameter | ||
2003 | ? $this->walkInputParameter($inParam) | ||
2004 | : $this->walkArithmeticExpression($inParam); | ||
2005 | } | ||
2006 | |||
2007 | /** | ||
2008 | * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. | ||
2009 | */ | ||
2010 | public function walkLiteral(AST\Literal $literal): string | ||
2011 | { | ||
2012 | return match ($literal->type) { | ||
2013 | AST\Literal::STRING => $this->conn->quote($literal->value), | ||
2014 | AST\Literal::BOOLEAN => (string) $this->conn->getDatabasePlatform()->convertBooleans(strtolower($literal->value) === 'true'), | ||
2015 | AST\Literal::NUMERIC => (string) $literal->value, | ||
2016 | default => throw QueryException::invalidLiteral($literal), | ||
2017 | }; | ||
2018 | } | ||
2019 | |||
2020 | /** | ||
2021 | * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. | ||
2022 | */ | ||
2023 | public function walkBetweenExpression(AST\BetweenExpression $betweenExpr): string | ||
2024 | { | ||
2025 | $sql = $this->walkArithmeticExpression($betweenExpr->expression); | ||
2026 | |||
2027 | if ($betweenExpr->not) { | ||
2028 | $sql .= ' NOT'; | ||
2029 | } | ||
2030 | |||
2031 | $sql .= ' BETWEEN ' . $this->walkArithmeticExpression($betweenExpr->leftBetweenExpression) | ||
2032 | . ' AND ' . $this->walkArithmeticExpression($betweenExpr->rightBetweenExpression); | ||
2033 | |||
2034 | return $sql; | ||
2035 | } | ||
2036 | |||
2037 | /** | ||
2038 | * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. | ||
2039 | */ | ||
2040 | public function walkLikeExpression(AST\LikeExpression $likeExpr): string | ||
2041 | { | ||
2042 | $stringExpr = $likeExpr->stringExpression; | ||
2043 | if (is_string($stringExpr)) { | ||
2044 | if (! isset($this->queryComponents[$stringExpr]['resultVariable'])) { | ||
2045 | throw new LogicException(sprintf('No result variable found for string expression "%s".', $stringExpr)); | ||
2046 | } | ||
2047 | |||
2048 | $leftExpr = $this->walkResultVariable($stringExpr); | ||
2049 | } else { | ||
2050 | $leftExpr = $stringExpr->dispatch($this); | ||
2051 | } | ||
2052 | |||
2053 | $sql = $leftExpr . ($likeExpr->not ? ' NOT' : '') . ' LIKE '; | ||
2054 | |||
2055 | if ($likeExpr->stringPattern instanceof AST\InputParameter) { | ||
2056 | $sql .= $this->walkInputParameter($likeExpr->stringPattern); | ||
2057 | } elseif ($likeExpr->stringPattern instanceof AST\Functions\FunctionNode) { | ||
2058 | $sql .= $this->walkFunction($likeExpr->stringPattern); | ||
2059 | } elseif ($likeExpr->stringPattern instanceof AST\PathExpression) { | ||
2060 | $sql .= $this->walkPathExpression($likeExpr->stringPattern); | ||
2061 | } else { | ||
2062 | $sql .= $this->walkLiteral($likeExpr->stringPattern); | ||
2063 | } | ||
2064 | |||
2065 | if ($likeExpr->escapeChar) { | ||
2066 | $sql .= ' ESCAPE ' . $this->walkLiteral($likeExpr->escapeChar); | ||
2067 | } | ||
2068 | |||
2069 | return $sql; | ||
2070 | } | ||
2071 | |||
2072 | /** | ||
2073 | * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. | ||
2074 | */ | ||
2075 | public function walkStateFieldPathExpression(AST\PathExpression $stateFieldPathExpression): string | ||
2076 | { | ||
2077 | return $this->walkPathExpression($stateFieldPathExpression); | ||
2078 | } | ||
2079 | |||
2080 | /** | ||
2081 | * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. | ||
2082 | */ | ||
2083 | public function walkComparisonExpression(AST\ComparisonExpression $compExpr): string | ||
2084 | { | ||
2085 | $leftExpr = $compExpr->leftExpression; | ||
2086 | $rightExpr = $compExpr->rightExpression; | ||
2087 | $sql = ''; | ||
2088 | |||
2089 | $sql .= $leftExpr instanceof AST\Node | ||
2090 | ? $leftExpr->dispatch($this) | ||
2091 | : (is_numeric($leftExpr) ? $leftExpr : $this->conn->quote($leftExpr)); | ||
2092 | |||
2093 | $sql .= ' ' . $compExpr->operator . ' '; | ||
2094 | |||
2095 | $sql .= $rightExpr instanceof AST\Node | ||
2096 | ? $rightExpr->dispatch($this) | ||
2097 | : (is_numeric($rightExpr) ? $rightExpr : $this->conn->quote($rightExpr)); | ||
2098 | |||
2099 | return $sql; | ||
2100 | } | ||
2101 | |||
2102 | /** | ||
2103 | * Walks down an InputParameter AST node, thereby generating the appropriate SQL. | ||
2104 | */ | ||
2105 | public function walkInputParameter(AST\InputParameter $inputParam): string | ||
2106 | { | ||
2107 | $this->parserResult->addParameterMapping($inputParam->name, $this->sqlParamIndex++); | ||
2108 | |||
2109 | $parameter = $this->query->getParameter($inputParam->name); | ||
2110 | |||
2111 | if ($parameter) { | ||
2112 | $type = $parameter->getType(); | ||
2113 | if (is_string($type) && Type::hasType($type)) { | ||
2114 | return Type::getType($type)->convertToDatabaseValueSQL('?', $this->platform); | ||
2115 | } | ||
2116 | } | ||
2117 | |||
2118 | return '?'; | ||
2119 | } | ||
2120 | |||
2121 | /** | ||
2122 | * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. | ||
2123 | */ | ||
2124 | public function walkArithmeticExpression(AST\ArithmeticExpression $arithmeticExpr): string | ||
2125 | { | ||
2126 | return $arithmeticExpr->isSimpleArithmeticExpression() | ||
2127 | ? $this->walkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression) | ||
2128 | : '(' . $this->walkSubselect($arithmeticExpr->subselect) . ')'; | ||
2129 | } | ||
2130 | |||
2131 | /** | ||
2132 | * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. | ||
2133 | */ | ||
2134 | public function walkSimpleArithmeticExpression(AST\Node|string $simpleArithmeticExpr): string | ||
2135 | { | ||
2136 | if (! ($simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression)) { | ||
2137 | return $this->walkArithmeticTerm($simpleArithmeticExpr); | ||
2138 | } | ||
2139 | |||
2140 | return implode(' ', array_map($this->walkArithmeticTerm(...), $simpleArithmeticExpr->arithmeticTerms)); | ||
2141 | } | ||
2142 | |||
2143 | /** | ||
2144 | * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. | ||
2145 | */ | ||
2146 | public function walkArithmeticTerm(AST\Node|string $term): string | ||
2147 | { | ||
2148 | if (is_string($term)) { | ||
2149 | return isset($this->queryComponents[$term]) | ||
2150 | ? $this->walkResultVariable($this->queryComponents[$term]['token']->value) | ||
2151 | : $term; | ||
2152 | } | ||
2153 | |||
2154 | // Phase 2 AST optimization: Skip processing of ArithmeticTerm | ||
2155 | // if only one ArithmeticFactor is defined | ||
2156 | if (! ($term instanceof AST\ArithmeticTerm)) { | ||
2157 | return $this->walkArithmeticFactor($term); | ||
2158 | } | ||
2159 | |||
2160 | return implode(' ', array_map($this->walkArithmeticFactor(...), $term->arithmeticFactors)); | ||
2161 | } | ||
2162 | |||
2163 | /** | ||
2164 | * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. | ||
2165 | */ | ||
2166 | public function walkArithmeticFactor(AST\Node|string $factor): string | ||
2167 | { | ||
2168 | if (is_string($factor)) { | ||
2169 | return isset($this->queryComponents[$factor]) | ||
2170 | ? $this->walkResultVariable($this->queryComponents[$factor]['token']->value) | ||
2171 | : $factor; | ||
2172 | } | ||
2173 | |||
2174 | // Phase 2 AST optimization: Skip processing of ArithmeticFactor | ||
2175 | // if only one ArithmeticPrimary is defined | ||
2176 | if (! ($factor instanceof AST\ArithmeticFactor)) { | ||
2177 | return $this->walkArithmeticPrimary($factor); | ||
2178 | } | ||
2179 | |||
2180 | $sign = $factor->isNegativeSigned() ? '-' : ($factor->isPositiveSigned() ? '+' : ''); | ||
2181 | |||
2182 | return $sign . $this->walkArithmeticPrimary($factor->arithmeticPrimary); | ||
2183 | } | ||
2184 | |||
2185 | /** | ||
2186 | * Walks down an ArithmeticPrimary that represents an AST node, thereby generating the appropriate SQL. | ||
2187 | */ | ||
2188 | public function walkArithmeticPrimary(AST\Node|string $primary): string | ||
2189 | { | ||
2190 | if ($primary instanceof AST\SimpleArithmeticExpression) { | ||
2191 | return '(' . $this->walkSimpleArithmeticExpression($primary) . ')'; | ||
2192 | } | ||
2193 | |||
2194 | if ($primary instanceof AST\Node) { | ||
2195 | return $primary->dispatch($this); | ||
2196 | } | ||
2197 | |||
2198 | return $this->walkEntityIdentificationVariable($primary); | ||
2199 | } | ||
2200 | |||
2201 | /** | ||
2202 | * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. | ||
2203 | */ | ||
2204 | public function walkStringPrimary(AST\Node|string $stringPrimary): string | ||
2205 | { | ||
2206 | return is_string($stringPrimary) | ||
2207 | ? $this->conn->quote($stringPrimary) | ||
2208 | : $stringPrimary->dispatch($this); | ||
2209 | } | ||
2210 | |||
2211 | /** | ||
2212 | * Walks down a ResultVariable that represents an AST node, thereby generating the appropriate SQL. | ||
2213 | */ | ||
2214 | public function walkResultVariable(string $resultVariable): string | ||
2215 | { | ||
2216 | if (! isset($this->scalarResultAliasMap[$resultVariable])) { | ||
2217 | throw new InvalidArgumentException(sprintf('Unknown result variable: %s', $resultVariable)); | ||
2218 | } | ||
2219 | |||
2220 | $resultAlias = $this->scalarResultAliasMap[$resultVariable]; | ||
2221 | |||
2222 | if (is_array($resultAlias)) { | ||
2223 | return implode(', ', $resultAlias); | ||
2224 | } | ||
2225 | |||
2226 | return $resultAlias; | ||
2227 | } | ||
2228 | |||
2229 | /** | ||
2230 | * @return string The list in parentheses of valid child discriminators from the given class | ||
2231 | * | ||
2232 | * @throws QueryException | ||
2233 | */ | ||
2234 | private function getChildDiscriminatorsFromClassMetadata( | ||
2235 | ClassMetadata $rootClass, | ||
2236 | AST\InstanceOfExpression $instanceOfExpr, | ||
2237 | ): string { | ||
2238 | $sqlParameterList = []; | ||
2239 | $discriminators = []; | ||
2240 | foreach ($instanceOfExpr->value as $parameter) { | ||
2241 | if ($parameter instanceof AST\InputParameter) { | ||
2242 | $this->rsm->discriminatorParameters[$parameter->name] = $parameter->name; | ||
2243 | $sqlParameterList[] = $this->walkInParameter($parameter); | ||
2244 | continue; | ||
2245 | } | ||
2246 | |||
2247 | $metadata = $this->em->getClassMetadata($parameter); | ||
2248 | |||
2249 | if ($metadata->getName() !== $rootClass->name && ! $metadata->getReflectionClass()->isSubclassOf($rootClass->name)) { | ||
2250 | throw QueryException::instanceOfUnrelatedClass($parameter, $rootClass->name); | ||
2251 | } | ||
2252 | |||
2253 | $discriminators += HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($metadata, $this->em); | ||
2254 | } | ||
2255 | |||
2256 | foreach (array_keys($discriminators) as $discriminatorValue) { | ||
2257 | $sqlParameterList[] = $rootClass->getDiscriminatorColumn()->type === 'integer' && is_int($discriminatorValue) | ||
2258 | ? $discriminatorValue | ||
2259 | : $this->conn->quote((string) $discriminatorValue); | ||
2260 | } | ||
2261 | |||
2262 | return '(' . implode(', ', $sqlParameterList) . ')'; | ||
2263 | } | ||
2264 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/TokenType.php b/vendor/doctrine/orm/src/Query/TokenType.php new file mode 100644 index 0000000..e745e4a --- /dev/null +++ b/vendor/doctrine/orm/src/Query/TokenType.php | |||
@@ -0,0 +1,91 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | enum TokenType: int | ||
8 | { | ||
9 | // All tokens that are not valid identifiers must be < 100 | ||
10 | case T_NONE = 1; | ||
11 | case T_INTEGER = 2; | ||
12 | case T_STRING = 3; | ||
13 | case T_INPUT_PARAMETER = 4; | ||
14 | case T_FLOAT = 5; | ||
15 | case T_CLOSE_PARENTHESIS = 6; | ||
16 | case T_OPEN_PARENTHESIS = 7; | ||
17 | case T_COMMA = 8; | ||
18 | case T_DIVIDE = 9; | ||
19 | case T_DOT = 10; | ||
20 | case T_EQUALS = 11; | ||
21 | case T_GREATER_THAN = 12; | ||
22 | case T_LOWER_THAN = 13; | ||
23 | case T_MINUS = 14; | ||
24 | case T_MULTIPLY = 15; | ||
25 | case T_NEGATE = 16; | ||
26 | case T_PLUS = 17; | ||
27 | case T_OPEN_CURLY_BRACE = 18; | ||
28 | case T_CLOSE_CURLY_BRACE = 19; | ||
29 | |||
30 | // All tokens that are identifiers or keywords that could be considered as identifiers should be >= 100 | ||
31 | case T_FULLY_QUALIFIED_NAME = 101; | ||
32 | case T_IDENTIFIER = 102; | ||
33 | |||
34 | // All keyword tokens should be >= 200 | ||
35 | case T_ALL = 200; | ||
36 | case T_AND = 201; | ||
37 | case T_ANY = 202; | ||
38 | case T_AS = 203; | ||
39 | case T_ASC = 204; | ||
40 | case T_AVG = 205; | ||
41 | case T_BETWEEN = 206; | ||
42 | case T_BOTH = 207; | ||
43 | case T_BY = 208; | ||
44 | case T_CASE = 209; | ||
45 | case T_COALESCE = 210; | ||
46 | case T_COUNT = 211; | ||
47 | case T_DELETE = 212; | ||
48 | case T_DESC = 213; | ||
49 | case T_DISTINCT = 214; | ||
50 | case T_ELSE = 215; | ||
51 | case T_EMPTY = 216; | ||
52 | case T_END = 217; | ||
53 | case T_ESCAPE = 218; | ||
54 | case T_EXISTS = 219; | ||
55 | case T_FALSE = 220; | ||
56 | case T_FROM = 221; | ||
57 | case T_GROUP = 222; | ||
58 | case T_HAVING = 223; | ||
59 | case T_HIDDEN = 224; | ||
60 | case T_IN = 225; | ||
61 | case T_INDEX = 226; | ||
62 | case T_INNER = 227; | ||
63 | case T_INSTANCE = 228; | ||
64 | case T_IS = 229; | ||
65 | case T_JOIN = 230; | ||
66 | case T_LEADING = 231; | ||
67 | case T_LEFT = 232; | ||
68 | case T_LIKE = 233; | ||
69 | case T_MAX = 234; | ||
70 | case T_MEMBER = 235; | ||
71 | case T_MIN = 236; | ||
72 | case T_NEW = 237; | ||
73 | case T_NOT = 238; | ||
74 | case T_NULL = 239; | ||
75 | case T_NULLIF = 240; | ||
76 | case T_OF = 241; | ||
77 | case T_OR = 242; | ||
78 | case T_ORDER = 243; | ||
79 | case T_OUTER = 244; | ||
80 | case T_SELECT = 246; | ||
81 | case T_SET = 247; | ||
82 | case T_SOME = 248; | ||
83 | case T_SUM = 249; | ||
84 | case T_THEN = 250; | ||
85 | case T_TRAILING = 251; | ||
86 | case T_TRUE = 252; | ||
87 | case T_UPDATE = 253; | ||
88 | case T_WHEN = 254; | ||
89 | case T_WHERE = 255; | ||
90 | case T_WITH = 256; | ||
91 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/TreeWalker.php b/vendor/doctrine/orm/src/Query/TreeWalker.php new file mode 100644 index 0000000..6c21577 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/TreeWalker.php | |||
@@ -0,0 +1,44 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\ORM\AbstractQuery; | ||
8 | |||
9 | /** | ||
10 | * Interface for walkers of DQL ASTs (abstract syntax trees). | ||
11 | * | ||
12 | * @psalm-import-type QueryComponent from Parser | ||
13 | */ | ||
14 | interface TreeWalker | ||
15 | { | ||
16 | /** | ||
17 | * Initializes TreeWalker with important information about the ASTs to be walked. | ||
18 | * | ||
19 | * @psalm-param array<string, QueryComponent> $queryComponents The query components (symbol table). | ||
20 | */ | ||
21 | public function __construct(AbstractQuery $query, ParserResult $parserResult, array $queryComponents); | ||
22 | |||
23 | /** | ||
24 | * Returns internal queryComponents array. | ||
25 | * | ||
26 | * @psalm-return array<string, QueryComponent> | ||
27 | */ | ||
28 | public function getQueryComponents(): array; | ||
29 | |||
30 | /** | ||
31 | * Walks down a SelectStatement AST node. | ||
32 | */ | ||
33 | public function walkSelectStatement(AST\SelectStatement $selectStatement): void; | ||
34 | |||
35 | /** | ||
36 | * Walks down an UpdateStatement AST node. | ||
37 | */ | ||
38 | public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void; | ||
39 | |||
40 | /** | ||
41 | * Walks down a DeleteStatement AST node. | ||
42 | */ | ||
43 | public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void; | ||
44 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php b/vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php new file mode 100644 index 0000000..a7948db --- /dev/null +++ b/vendor/doctrine/orm/src/Query/TreeWalkerAdapter.php | |||
@@ -0,0 +1,90 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\ORM\AbstractQuery; | ||
8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
9 | use LogicException; | ||
10 | |||
11 | use function array_diff; | ||
12 | use function array_keys; | ||
13 | use function sprintf; | ||
14 | |||
15 | /** | ||
16 | * An adapter implementation of the TreeWalker interface. The methods in this class | ||
17 | * are empty. This class exists as convenience for creating tree walkers. | ||
18 | * | ||
19 | * @psalm-import-type QueryComponent from Parser | ||
20 | */ | ||
21 | abstract class TreeWalkerAdapter implements TreeWalker | ||
22 | { | ||
23 | /** | ||
24 | * {@inheritDoc} | ||
25 | */ | ||
26 | public function __construct( | ||
27 | private readonly AbstractQuery $query, | ||
28 | private readonly ParserResult $parserResult, | ||
29 | private array $queryComponents, | ||
30 | ) { | ||
31 | } | ||
32 | |||
33 | /** | ||
34 | * {@inheritDoc} | ||
35 | */ | ||
36 | public function getQueryComponents(): array | ||
37 | { | ||
38 | return $this->queryComponents; | ||
39 | } | ||
40 | |||
41 | public function walkSelectStatement(AST\SelectStatement $selectStatement): void | ||
42 | { | ||
43 | } | ||
44 | |||
45 | public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void | ||
46 | { | ||
47 | } | ||
48 | |||
49 | public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void | ||
50 | { | ||
51 | } | ||
52 | |||
53 | /** | ||
54 | * Sets or overrides a query component for a given dql alias. | ||
55 | * | ||
56 | * @psalm-param QueryComponent $queryComponent | ||
57 | */ | ||
58 | protected function setQueryComponent(string $dqlAlias, array $queryComponent): void | ||
59 | { | ||
60 | $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token']; | ||
61 | |||
62 | if (array_diff($requiredKeys, array_keys($queryComponent))) { | ||
63 | throw QueryException::invalidQueryComponent($dqlAlias); | ||
64 | } | ||
65 | |||
66 | $this->queryComponents[$dqlAlias] = $queryComponent; | ||
67 | } | ||
68 | |||
69 | /** | ||
70 | * Retrieves the Query Instance responsible for the current walkers execution. | ||
71 | */ | ||
72 | protected function _getQuery(): AbstractQuery | ||
73 | { | ||
74 | return $this->query; | ||
75 | } | ||
76 | |||
77 | /** | ||
78 | * Retrieves the ParserResult. | ||
79 | */ | ||
80 | protected function _getParserResult(): ParserResult | ||
81 | { | ||
82 | return $this->parserResult; | ||
83 | } | ||
84 | |||
85 | protected function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata | ||
86 | { | ||
87 | return $this->queryComponents[$dqlAlias]['metadata'] | ||
88 | ?? throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); | ||
89 | } | ||
90 | } | ||
diff --git a/vendor/doctrine/orm/src/Query/TreeWalkerChain.php b/vendor/doctrine/orm/src/Query/TreeWalkerChain.php new file mode 100644 index 0000000..7bb3051 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/TreeWalkerChain.php | |||
@@ -0,0 +1,88 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\ORM\AbstractQuery; | ||
8 | use Generator; | ||
9 | |||
10 | /** | ||
11 | * Represents a chain of tree walkers that modify an AST and finally emit output. | ||
12 | * Only the last walker in the chain can emit output. Any previous walkers can modify | ||
13 | * the AST to influence the final output produced by the last walker. | ||
14 | * | ||
15 | * @psalm-import-type QueryComponent from Parser | ||
16 | */ | ||
17 | class TreeWalkerChain implements TreeWalker | ||
18 | { | ||
19 | /** | ||
20 | * The tree walkers. | ||
21 | * | ||
22 | * @var string[] | ||
23 | * @psalm-var list<class-string<TreeWalker>> | ||
24 | */ | ||
25 | private array $walkers = []; | ||
26 | |||
27 | /** | ||
28 | * {@inheritDoc} | ||
29 | */ | ||
30 | public function __construct( | ||
31 | private readonly AbstractQuery $query, | ||
32 | private readonly ParserResult $parserResult, | ||
33 | private array $queryComponents, | ||
34 | ) { | ||
35 | } | ||
36 | |||
37 | /** | ||
38 | * Returns the internal queryComponents array. | ||
39 | * | ||
40 | * {@inheritDoc} | ||
41 | */ | ||
42 | public function getQueryComponents(): array | ||
43 | { | ||
44 | return $this->queryComponents; | ||
45 | } | ||
46 | |||
47 | /** | ||
48 | * Adds a tree walker to the chain. | ||
49 | * | ||
50 | * @param string $walkerClass The class of the walker to instantiate. | ||
51 | * @psalm-param class-string<TreeWalker> $walkerClass | ||
52 | */ | ||
53 | public function addTreeWalker(string $walkerClass): void | ||
54 | { | ||
55 | $this->walkers[] = $walkerClass; | ||
56 | } | ||
57 | |||
58 | public function walkSelectStatement(AST\SelectStatement $selectStatement): void | ||
59 | { | ||
60 | foreach ($this->getWalkers() as $walker) { | ||
61 | $walker->walkSelectStatement($selectStatement); | ||
62 | |||
63 | $this->queryComponents = $walker->getQueryComponents(); | ||
64 | } | ||
65 | } | ||
66 | |||
67 | public function walkUpdateStatement(AST\UpdateStatement $updateStatement): void | ||
68 | { | ||
69 | foreach ($this->getWalkers() as $walker) { | ||
70 | $walker->walkUpdateStatement($updateStatement); | ||
71 | } | ||
72 | } | ||
73 | |||
74 | public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): void | ||
75 | { | ||
76 | foreach ($this->getWalkers() as $walker) { | ||
77 | $walker->walkDeleteStatement($deleteStatement); | ||
78 | } | ||
79 | } | ||
80 | |||
81 | /** @psalm-return Generator<int, TreeWalker> */ | ||
82 | private function getWalkers(): Generator | ||
83 | { | ||
84 | foreach ($this->walkers as $walkerClass) { | ||
85 | yield new $walkerClass($this->query, $this->parserResult, $this->queryComponents); | ||
86 | } | ||
87 | } | ||
88 | } | ||