summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php')
-rw-r--r--vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php155
1 files changed, 155 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php
new file mode 100644
index 0000000..3fb0eee
--- /dev/null
+++ b/vendor/doctrine/orm/src/Tools/Pagination/LimitSubqueryWalker.php
@@ -0,0 +1,155 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Tools\Pagination;
6
7use Doctrine\DBAL\Types\Type;
8use Doctrine\ORM\Query;
9use Doctrine\ORM\Query\AST\Functions\IdentityFunction;
10use Doctrine\ORM\Query\AST\Node;
11use Doctrine\ORM\Query\AST\PathExpression;
12use Doctrine\ORM\Query\AST\SelectExpression;
13use Doctrine\ORM\Query\AST\SelectStatement;
14use Doctrine\ORM\Query\TreeWalkerAdapter;
15use RuntimeException;
16
17use function count;
18use function is_string;
19use function reset;
20
21/**
22 * Replaces the selectClause of the AST with a SELECT DISTINCT root.id equivalent.
23 */
24class LimitSubqueryWalker extends TreeWalkerAdapter
25{
26 public const IDENTIFIER_TYPE = 'doctrine_paginator.id.type';
27
28 public const FORCE_DBAL_TYPE_CONVERSION = 'doctrine_paginator.scalar_result.force_dbal_type_conversion';
29
30 /**
31 * Counter for generating unique order column aliases.
32 */
33 private int $aliasCounter = 0;
34
35 public function walkSelectStatement(SelectStatement $selectStatement): void
36 {
37 // Get the root entity and alias from the AST fromClause
38 $from = $selectStatement->fromClause->identificationVariableDeclarations;
39 $fromRoot = reset($from);
40 $rootAlias = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
41 $rootClass = $this->getMetadataForDqlAlias($rootAlias);
42
43 $this->validate($selectStatement);
44 $identifier = $rootClass->getSingleIdentifierFieldName();
45
46 if (isset($rootClass->associationMappings[$identifier])) {
47 throw new RuntimeException('Paginating an entity with foreign key as identifier only works when using the Output Walkers. Call Paginator#setUseOutputWalkers(true) before iterating the paginator.');
48 }
49
50 $query = $this->_getQuery();
51
52 $query->setHint(
53 self::IDENTIFIER_TYPE,
54 Type::getType($rootClass->fieldMappings[$identifier]->type),
55 );
56
57 $query->setHint(self::FORCE_DBAL_TYPE_CONVERSION, true);
58
59 $pathExpression = new PathExpression(
60 PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION,
61 $rootAlias,
62 $identifier,
63 );
64
65 $pathExpression->type = PathExpression::TYPE_STATE_FIELD;
66
67 $selectStatement->selectClause->selectExpressions = [new SelectExpression($pathExpression, '_dctrn_id')];
68 $selectStatement->selectClause->isDistinct = ($query->getHints()[Paginator::HINT_ENABLE_DISTINCT] ?? true) === true;
69
70 if (! isset($selectStatement->orderByClause)) {
71 return;
72 }
73
74 $queryComponents = $this->getQueryComponents();
75 foreach ($selectStatement->orderByClause->orderByItems as $item) {
76 if ($item->expression instanceof PathExpression) {
77 $selectStatement->selectClause->selectExpressions[] = new SelectExpression(
78 $this->createSelectExpressionItem($item->expression),
79 '_dctrn_ord' . $this->aliasCounter++,
80 );
81
82 continue;
83 }
84
85 if (is_string($item->expression) && isset($queryComponents[$item->expression])) {
86 $qComp = $queryComponents[$item->expression];
87
88 if (isset($qComp['resultVariable'])) {
89 $selectStatement->selectClause->selectExpressions[] = new SelectExpression(
90 $qComp['resultVariable'],
91 $item->expression,
92 );
93 }
94 }
95 }
96 }
97
98 /**
99 * Validate the AST to ensure that this walker is able to properly manipulate it.
100 */
101 private function validate(SelectStatement $AST): void
102 {
103 // Prevent LimitSubqueryWalker from being used with queries that include
104 // a limit, a fetched to-many join, and an order by condition that
105 // references a column from the fetch joined table.
106 $queryComponents = $this->getQueryComponents();
107 $query = $this->_getQuery();
108 $from = $AST->fromClause->identificationVariableDeclarations;
109 $fromRoot = reset($from);
110
111 if (
112 $query instanceof Query
113 && $query->getMaxResults() !== null
114 && $AST->orderByClause
115 && count($fromRoot->joins)
116 ) {
117 // Check each orderby item.
118 // TODO: check complex orderby items too...
119 foreach ($AST->orderByClause->orderByItems as $orderByItem) {
120 $expression = $orderByItem->expression;
121 if (
122 $orderByItem->expression instanceof PathExpression
123 && isset($queryComponents[$expression->identificationVariable])
124 ) {
125 $queryComponent = $queryComponents[$expression->identificationVariable];
126 if (
127 isset($queryComponent['parent'])
128 && isset($queryComponent['relation'])
129 && $queryComponent['relation']->isToMany()
130 ) {
131 throw new RuntimeException('Cannot select distinct identifiers from query with LIMIT and ORDER BY on a column from a fetch joined to-many association. Use output walkers.');
132 }
133 }
134 }
135 }
136 }
137
138 /**
139 * Retrieve either an IdentityFunction (IDENTITY(u.assoc)) or a state field (u.name).
140 *
141 * @return IdentityFunction|PathExpression
142 */
143 private function createSelectExpressionItem(PathExpression $pathExpression): Node
144 {
145 if ($pathExpression->type === PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {
146 $identity = new IdentityFunction('identity');
147
148 $identity->pathExpression = clone $pathExpression;
149
150 return $identity;
151 }
152
153 return clone $pathExpression;
154 }
155}