1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the API Platform project. |
5
|
|
|
* |
6
|
|
|
* (c) Kévin Dunglas <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
declare(strict_types=1); |
13
|
|
|
|
14
|
|
|
namespace ApiPlatform\Core\Bridge\Doctrine\Orm\Util; |
15
|
|
|
|
16
|
|
|
use Doctrine\Common\Persistence\ManagerRegistry; |
17
|
|
|
use Doctrine\ORM\Query\Expr\Join; |
18
|
|
|
use Doctrine\ORM\QueryBuilder; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @author Vincent Chalamon <[email protected]> |
22
|
|
|
* |
23
|
|
|
* @internal |
24
|
|
|
*/ |
25
|
|
|
final class QueryBuilderHelper |
26
|
|
|
{ |
27
|
|
|
private function __construct() |
28
|
|
|
{ |
29
|
|
|
} |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Adds a join to the QueryBuilder if none exists. |
33
|
|
|
*/ |
34
|
|
|
public static function addJoinOnce(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $alias, string $association, string $joinType = null, string $conditionType = null, string $condition = null, string $originAlias = null): string |
35
|
|
|
{ |
36
|
|
|
$join = self::getExistingJoin($queryBuilder, $alias, $association, $originAlias); |
37
|
|
|
|
38
|
|
|
if (null !== $join) { |
39
|
|
|
return $join->getAlias(); |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
$associationAlias = $queryNameGenerator->generateJoinAlias($association); |
43
|
|
|
$query = "$alias.$association"; |
44
|
|
|
|
45
|
|
|
if (Join::LEFT_JOIN === $joinType || QueryChecker::hasLeftJoin($queryBuilder)) { |
46
|
|
|
$queryBuilder->leftJoin($query, $associationAlias, $conditionType, $condition); |
47
|
|
|
} else { |
48
|
|
|
$queryBuilder->innerJoin($query, $associationAlias, $conditionType, $condition); |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
return $associationAlias; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Gets the entity class name by an alias used in the QueryBuilder. |
56
|
|
|
*/ |
57
|
|
|
public static function getEntityClassByAlias(string $alias, QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): string |
58
|
|
|
{ |
59
|
|
|
if (!\in_array($alias, $queryBuilder->getAllAliases(), true)) { |
60
|
|
|
throw new \LogicException(sprintf('The alias "%s" does not exist in the QueryBuilder.', $alias)); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
$rootAliasMap = self::mapRootAliases($queryBuilder->getRootAliases(), $queryBuilder->getRootEntities()); |
64
|
|
|
|
65
|
|
|
if (isset($rootAliasMap[$alias])) { |
66
|
|
|
return $rootAliasMap[$alias]; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
$metadata = null; |
70
|
|
|
|
71
|
|
|
foreach (self::traverseJoins($alias, $queryBuilder, $managerRegistry) as [$metadata]) { |
|
|
|
|
72
|
|
|
// do nothing, but we will use the $metadata variable |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
if (null === $metadata) { |
76
|
|
|
throw new \LogicException(sprintf('The alias "%s" does not exist in the QueryBuilder.', $alias)); |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
return $metadata->getName(); |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* Finds the root alias for an alias used in the QueryBuilder. |
84
|
|
|
*/ |
85
|
|
|
public static function findRootAlias(string $alias, QueryBuilder $queryBuilder): string |
86
|
|
|
{ |
87
|
|
|
if (!\in_array($alias, $queryBuilder->getAllAliases(), true)) { |
88
|
|
|
throw new \LogicException(sprintf('The alias "%s" does not exist in the QueryBuilder.', $alias)); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
if (\in_array($alias, $queryBuilder->getRootAliases(), true)) { |
92
|
|
|
return $alias; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
foreach ($queryBuilder->getDQLPart('join') as $rootAlias => $joins) { |
96
|
|
|
foreach ($joins as $join) { |
97
|
|
|
if ($alias === $join->getAlias()) { |
98
|
|
|
return $rootAlias; |
99
|
|
|
} |
100
|
|
|
} |
101
|
|
|
} |
|
|
|
|
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Traverses through the joins for an alias used in the QueryBuilder. |
106
|
|
|
* |
107
|
|
|
* @return \Generator<string, array> |
108
|
|
|
*/ |
109
|
|
|
public static function traverseJoins(string $alias, QueryBuilder $queryBuilder, ManagerRegistry $managerRegistry): \Generator |
110
|
|
|
{ |
111
|
|
|
$rootAliasMap = self::mapRootAliases($queryBuilder->getRootAliases(), $queryBuilder->getRootEntities()); |
112
|
|
|
|
113
|
|
|
$joinParts = $queryBuilder->getDQLPart('join'); |
114
|
|
|
$rootAlias = self::findRootAlias($alias, $queryBuilder); |
115
|
|
|
|
116
|
|
|
$joinAliasMap = self::mapJoinAliases($joinParts[$rootAlias]); |
117
|
|
|
|
118
|
|
|
$aliasMap = array_merge($rootAliasMap, $joinAliasMap); |
119
|
|
|
|
120
|
|
|
$apexEntityClass = null; |
121
|
|
|
$associationStack = []; |
122
|
|
|
$aliasStack = []; |
123
|
|
|
$currentAlias = $alias; |
124
|
|
|
|
125
|
|
|
while (null === $apexEntityClass) { |
126
|
|
|
if (!isset($aliasMap[$currentAlias])) { |
127
|
|
|
throw new \LogicException(sprintf('Unknown alias "%s".', $currentAlias)); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
if (\is_string($aliasMap[$currentAlias])) { |
131
|
|
|
$aliasStack[] = $currentAlias; |
132
|
|
|
$apexEntityClass = $aliasMap[$currentAlias]; |
133
|
|
|
} else { |
134
|
|
|
[$parentAlias, $association] = $aliasMap[$currentAlias]; |
135
|
|
|
|
136
|
|
|
$associationStack[] = $association; |
137
|
|
|
$aliasStack[] = $currentAlias; |
138
|
|
|
$currentAlias = $parentAlias; |
139
|
|
|
} |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
$entityClass = $apexEntityClass; |
143
|
|
|
$association = null; |
|
|
|
|
144
|
|
|
|
145
|
|
|
while (null !== ($alias = array_pop($aliasStack))) { |
146
|
|
|
$metadata = $managerRegistry |
147
|
|
|
->getManagerForClass($entityClass) |
148
|
|
|
->getClassMetadata($entityClass); |
149
|
|
|
|
150
|
|
|
$association = array_pop($associationStack); |
151
|
|
|
|
152
|
|
|
yield $alias => [ |
153
|
|
|
$metadata, |
154
|
|
|
$association, |
155
|
|
|
]; |
156
|
|
|
|
157
|
|
|
if (null !== $association) { |
158
|
|
|
$entityClass = $metadata->getAssociationTargetClass($association); |
159
|
|
|
} |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Gets the existing join from QueryBuilder DQL parts. |
165
|
|
|
*/ |
166
|
|
|
private static function getExistingJoin(QueryBuilder $queryBuilder, string $alias, string $association, string $originAlias = null): ?Join |
167
|
|
|
{ |
168
|
|
|
$parts = $queryBuilder->getDQLPart('join'); |
169
|
|
|
$rootAlias = $originAlias ?? $queryBuilder->getRootAliases()[0]; |
170
|
|
|
|
171
|
|
|
if (!isset($parts[$rootAlias])) { |
172
|
|
|
return null; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
foreach ($parts[$rootAlias] as $join) { |
176
|
|
|
/** @var Join $join */ |
177
|
|
|
if (sprintf('%s.%s', $alias, $association) === $join->getJoin()) { |
178
|
|
|
return $join; |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
return null; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Maps the root aliases to root entity classes. |
187
|
|
|
* |
188
|
|
|
* @return array<string, string> |
189
|
|
|
*/ |
190
|
|
|
private static function mapRootAliases(array $rootAliases, array $rootEntities): array |
191
|
|
|
{ |
192
|
|
|
$aliasMap = array_combine($rootAliases, $rootEntities); |
193
|
|
|
if (false === $aliasMap) { |
194
|
|
|
throw new \LogicException('Number of root aliases and root entities do not match.'); |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
return $aliasMap; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Maps the join aliases to the parent alias and association, or the entity class. |
202
|
|
|
* |
203
|
|
|
* @return array<string, string[]|string> |
204
|
|
|
*/ |
205
|
|
|
private static function mapJoinAliases(iterable $joins): array |
206
|
|
|
{ |
207
|
|
|
$aliasMap = []; |
208
|
|
|
|
209
|
|
|
foreach ($joins as $join) { |
210
|
|
|
$alias = $join->getAlias(); |
211
|
|
|
$relationship = $join->getJoin(); |
212
|
|
|
|
213
|
|
|
if (false !== strpos($relationship, '.')) { |
214
|
|
|
$aliasMap[$alias] = explode('.', $relationship); |
215
|
|
|
} else { |
216
|
|
|
$aliasMap[$alias] = $relationship; |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
return $aliasMap; |
221
|
|
|
} |
222
|
|
|
} |
223
|
|
|
|
This check looks for
foreach
loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.Consider removing the loop.