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; |
||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||
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
str_replace($aliases, $r...ts, (string)$wherePart) of type string is incompatible with the type array|object expected by parameter $dqlPart of Doctrine\ORM\QueryBuilder::add() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
160 | |||||
161 | return $queryBuilderClone; |
||||
162 | } |
||||
163 | } |
||||
164 |