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\Extension; |
||
15 | |||
16 | use ApiPlatform\Core\Api\ResourceClassResolverInterface; |
||
17 | use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\EagerLoadingTrait; |
||
18 | use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryBuilderHelper; |
||
19 | use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; |
||
20 | use ApiPlatform\Core\Exception\InvalidArgumentException; |
||
21 | use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
||
22 | use Doctrine\ORM\Query\Expr\Join; |
||
23 | use Doctrine\ORM\QueryBuilder; |
||
24 | |||
25 | /** |
||
26 | * Fixes filters on OneToMany associations |
||
27 | * https://github.com/api-platform/core/issues/944. |
||
28 | */ |
||
29 | final class FilterEagerLoadingExtension implements ContextAwareQueryCollectionExtensionInterface |
||
30 | { |
||
31 | use EagerLoadingTrait; |
||
32 | |||
33 | private $resourceClassResolver; |
||
34 | |||
35 | public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, bool $forceEager = true, ResourceClassResolverInterface $resourceClassResolver = null) |
||
36 | { |
||
37 | $this->resourceMetadataFactory = $resourceMetadataFactory; |
||
38 | $this->forceEager = $forceEager; |
||
39 | $this->resourceClassResolver = $resourceClassResolver; |
||
40 | } |
||
41 | |||
42 | /** |
||
43 | * {@inheritdoc} |
||
44 | */ |
||
45 | public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass = null, string $operationName = null, array $context = []) |
||
46 | { |
||
47 | if (null === $resourceClass) { |
||
48 | throw new InvalidArgumentException('The "$resourceClass" parameter must not be null'); |
||
49 | } |
||
50 | |||
51 | $em = $queryBuilder->getEntityManager(); |
||
52 | $classMetadata = $em->getClassMetadata($resourceClass); |
||
53 | |||
54 | if (!$this->shouldOperationForceEager($resourceClass, ['collection_operation_name' => $operationName]) && !$this->hasFetchEagerAssociation($em, $classMetadata)) { |
||
55 | return; |
||
56 | } |
||
57 | |||
58 | //If no where part, nothing to do |
||
59 | $wherePart = $queryBuilder->getDQLPart('where'); |
||
60 | |||
61 | if (!$wherePart) { |
||
62 | return; |
||
63 | } |
||
64 | |||
65 | $joinParts = $queryBuilder->getDQLPart('join'); |
||
66 | $originAlias = $queryBuilder->getRootAliases()[0]; |
||
67 | |||
68 | if (!$joinParts || !isset($joinParts[$originAlias])) { |
||
69 | return; |
||
70 | } |
||
71 | |||
72 | $queryBuilderClone = clone $queryBuilder; |
||
73 | $queryBuilderClone->resetDQLPart('where'); |
||
74 | $changedWhereClause = false; |
||
75 | |||
76 | if (!$classMetadata->isIdentifierComposite) { |
||
77 | $replacementAlias = $queryNameGenerator->generateJoinAlias($originAlias); |
||
78 | $in = $this->getQueryBuilderWithNewAliases($queryBuilder, $queryNameGenerator, $originAlias, $replacementAlias); |
||
79 | $in->select($replacementAlias); |
||
80 | $queryBuilderClone->andWhere($queryBuilderClone->expr()->in($originAlias, $in->getDQL())); |
||
81 | $changedWhereClause = true; |
||
82 | } else { |
||
83 | // Because Doctrine doesn't support WHERE ( foo, bar ) IN () (https://github.com/doctrine/doctrine2/issues/5238), we are building as many subqueries as they are identifiers |
||
84 | foreach ($classMetadata->getIdentifier() as $identifier) { |
||
85 | if (!$classMetadata->hasAssociation($identifier)) { |
||
86 | continue; |
||
87 | } |
||
88 | |||
89 | $replacementAlias = $queryNameGenerator->generateJoinAlias($originAlias); |
||
90 | $in = $this->getQueryBuilderWithNewAliases($queryBuilder, $queryNameGenerator, $originAlias, $replacementAlias); |
||
91 | $in->select("IDENTITY($replacementAlias.$identifier)"); |
||
92 | $queryBuilderClone->andWhere($queryBuilderClone->expr()->in("$originAlias.$identifier", $in->getDQL())); |
||
93 | $changedWhereClause = true; |
||
94 | } |
||
95 | } |
||
96 | |||
97 | if (false === $changedWhereClause) { |
||
98 | return; |
||
99 | } |
||
100 | |||
101 | $queryBuilder->resetDQLPart('where'); |
||
102 | $queryBuilder->add('where', $queryBuilderClone->getDQLPart('where')); |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * Returns a clone of the given query builder where everything gets re-aliased. |
||
107 | * |
||
108 | * @param string $originAlias the base alias |
||
109 | * @param string $replacement the replacement for the base alias, will change the from alias |
||
110 | */ |
||
111 | private function getQueryBuilderWithNewAliases(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $originAlias = 'o', string $replacement = 'o_2'): QueryBuilder |
||
112 | { |
||
113 | $queryBuilderClone = clone $queryBuilder; |
||
114 | |||
115 | $joinParts = $queryBuilder->getDQLPart('join'); |
||
116 | $wherePart = $queryBuilder->getDQLPart('where'); |
||
117 | |||
118 | //reset parts |
||
119 | $queryBuilderClone->resetDQLPart('join'); |
||
120 | $queryBuilderClone->resetDQLPart('where'); |
||
121 | $queryBuilderClone->resetDQLPart('orderBy'); |
||
122 | $queryBuilderClone->resetDQLPart('groupBy'); |
||
123 | $queryBuilderClone->resetDQLPart('having'); |
||
124 | |||
125 | //Change from alias |
||
126 | $from = $queryBuilderClone->getDQLPart('from')[0]; |
||
127 | $queryBuilderClone->resetDQLPart('from'); |
||
128 | $queryBuilderClone->from($from->getFrom(), $replacement); |
||
129 | |||
130 | $aliases = ["$originAlias."]; |
||
131 | $replacements = ["$replacement."]; |
||
132 | |||
133 | //Change join aliases |
||
134 | foreach ($joinParts[$originAlias] as $joinPart) { |
||
135 | /** @var Join $joinPart */ |
||
136 | $joinString = str_replace($aliases, $replacements, $joinPart->getJoin()); |
||
137 | $pos = strpos($joinString, '.'); |
||
138 | if (false === $pos) { |
||
139 | if (null !== $joinPart->getCondition() && null !== $this->resourceClassResolver && $this->resourceClassResolver->isResourceClass($joinString)) { |
||
140 | $newAlias = $queryNameGenerator->generateJoinAlias($joinPart->getAlias()); |
||
141 | $aliases[] = "{$joinPart->getAlias()}."; |
||
142 | $replacements[] = "$newAlias."; |
||
143 | $condition = str_replace($aliases, $replacements, $joinPart->getCondition()); |
||
144 | $join = new Join($joinPart->getJoinType(), $joinPart->getJoin(), $newAlias, $joinPart->getConditionType(), $condition); |
||
145 | $queryBuilderClone->add('join', [$replacement => $join], true); |
||
146 | } |
||
147 | |||
148 | continue; |
||
149 | } |
||
150 | $alias = substr($joinString, 0, $pos); |
||
151 | $association = substr($joinString, $pos + 1); |
||
152 | $newAlias = $queryNameGenerator->generateJoinAlias($association); |
||
153 | $aliases[] = "{$joinPart->getAlias()}."; |
||
154 | $replacements[] = "$newAlias."; |
||
155 | $condition = str_replace($aliases, $replacements, $joinPart->getCondition()); |
||
156 | QueryBuilderHelper::addJoinOnce($queryBuilderClone, $queryNameGenerator, $alias, $association, $joinPart->getJoinType(), $joinPart->getConditionType(), $condition, $originAlias, $newAlias); |
||
157 | } |
||
158 | |||
159 | $queryBuilderClone->add('where', str_replace($aliases, $replacements, (string) $wherePart)); |
||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
160 | |||
161 | return $queryBuilderClone; |
||
162 | } |
||
163 | } |
||
164 |