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; |
||||
15 | |||||
16 | use ApiPlatform\Core\Bridge\Doctrine\Common\Util\IdentifierManagerTrait; |
||||
17 | use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\FilterEagerLoadingExtension; |
||||
18 | use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; |
||||
19 | use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; |
||||
20 | use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface; |
||||
21 | use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultItemExtensionInterface; |
||||
22 | use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; |
||||
23 | use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; |
||||
24 | use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; |
||||
25 | use ApiPlatform\Core\Exception\RuntimeException; |
||||
26 | use ApiPlatform\Core\Identifier\IdentifierConverterInterface; |
||||
27 | use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
||||
28 | use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
||||
29 | use Doctrine\Common\Persistence\ManagerRegistry; |
||||
30 | use Doctrine\ORM\EntityManagerInterface; |
||||
31 | use Doctrine\ORM\Mapping\ClassMetadataInfo; |
||||
32 | use Doctrine\ORM\QueryBuilder; |
||||
33 | |||||
34 | /** |
||||
35 | * Subresource data provider for the Doctrine ORM. |
||||
36 | * |
||||
37 | * @author Antoine Bluchet <[email protected]> |
||||
38 | */ |
||||
39 | final class SubresourceDataProvider implements SubresourceDataProviderInterface |
||||
40 | { |
||||
41 | use IdentifierManagerTrait; |
||||
42 | |||||
43 | private $managerRegistry; |
||||
44 | private $collectionExtensions; |
||||
45 | private $itemExtensions; |
||||
46 | |||||
47 | /** |
||||
48 | * @param QueryCollectionExtensionInterface[] $collectionExtensions |
||||
49 | * @param QueryItemExtensionInterface[] $itemExtensions |
||||
50 | */ |
||||
51 | public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = [], iterable $itemExtensions = []) |
||||
52 | { |
||||
53 | $this->managerRegistry = $managerRegistry; |
||||
54 | $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
||||
55 | $this->propertyMetadataFactory = $propertyMetadataFactory; |
||||
56 | $this->collectionExtensions = $collectionExtensions; |
||||
57 | $this->itemExtensions = $itemExtensions; |
||||
58 | } |
||||
59 | |||||
60 | /** |
||||
61 | * {@inheritdoc} |
||||
62 | * |
||||
63 | * @throws RuntimeException |
||||
64 | */ |
||||
65 | public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null) |
||||
66 | { |
||||
67 | $manager = $this->managerRegistry->getManagerForClass($resourceClass); |
||||
68 | if (null === $manager) { |
||||
69 | throw new ResourceClassNotSupportedException(sprintf('The object manager associated with the "%s" resource class cannot be retrieved.', $resourceClass)); |
||||
70 | } |
||||
71 | |||||
72 | $repository = $manager->getRepository($resourceClass); |
||||
73 | if (!method_exists($repository, 'createQueryBuilder')) { |
||||
74 | throw new RuntimeException('The repository class must have a "createQueryBuilder" method.'); |
||||
75 | } |
||||
76 | |||||
77 | if (!isset($context['identifiers'], $context['property'])) { |
||||
78 | throw new ResourceClassNotSupportedException('The given resource class is not a subresource.'); |
||||
79 | } |
||||
80 | |||||
81 | $queryNameGenerator = new QueryNameGenerator(); |
||||
82 | |||||
83 | /* |
||||
84 | * The following recursively translates to this pseudo-dql: |
||||
85 | * |
||||
86 | * SELECT thirdLevel WHERE thirdLevel IN ( |
||||
87 | * SELECT thirdLevel FROM relatedDummies WHERE relatedDummies = ? AND relatedDummies IN ( |
||||
88 | * SELECT relatedDummies FROM Dummy WHERE Dummy = ? |
||||
89 | * ) |
||||
90 | * ) |
||||
91 | * |
||||
92 | * By using subqueries, we're forcing the SQL execution plan to go through indexes on doctrine identifiers. |
||||
93 | */ |
||||
94 | $queryBuilder = $this->buildQuery($identifiers, $context, $queryNameGenerator, $repository->createQueryBuilder($alias = 'o'), $alias, \count($context['identifiers'])); |
||||
95 | |||||
96 | if (true === $context['collection']) { |
||||
97 | foreach ($this->collectionExtensions as $extension) { |
||||
98 | // We don't need this anymore because we already made sub queries to ensure correct results |
||||
99 | if ($extension instanceof FilterEagerLoadingExtension) { |
||||
100 | continue; |
||||
101 | } |
||||
102 | |||||
103 | $extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName, $context); |
||||
104 | if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) { |
||||
105 | return $extension->getResult($queryBuilder, $resourceClass, $operationName, $context); |
||||
106 | } |
||||
107 | } |
||||
108 | } else { |
||||
109 | foreach ($this->itemExtensions as $extension) { |
||||
110 | $extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName, $context); |
||||
111 | if ($extension instanceof QueryResultItemExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) { |
||||
0 ignored issues
–
show
|
|||||
112 | return $extension->getResult($queryBuilder, $resourceClass, $operationName, $context); |
||||
0 ignored issues
–
show
The call to
ApiPlatform\Core\Bridge\...nInterface::getResult() has too many arguments starting with $resourceClass .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.
Loading history...
|
|||||
113 | } |
||||
114 | } |
||||
115 | } |
||||
116 | |||||
117 | $query = $queryBuilder->getQuery(); |
||||
118 | |||||
119 | return $context['collection'] ? $query->getResult() : $query->getOneOrNullResult(); |
||||
120 | } |
||||
121 | |||||
122 | /** |
||||
123 | * @throws RuntimeException |
||||
124 | */ |
||||
125 | private function buildQuery(array $identifiers, array $context, QueryNameGenerator $queryNameGenerator, QueryBuilder $previousQueryBuilder, string $previousAlias, int $remainingIdentifiers, QueryBuilder $topQueryBuilder = null): QueryBuilder |
||||
126 | { |
||||
127 | if ($remainingIdentifiers <= 0) { |
||||
128 | return $previousQueryBuilder; |
||||
129 | } |
||||
130 | |||||
131 | $topQueryBuilder = $topQueryBuilder ?? $previousQueryBuilder; |
||||
132 | |||||
133 | [$identifier, $identifierResourceClass] = $context['identifiers'][$remainingIdentifiers - 1]; |
||||
134 | $previousAssociationProperty = $context['identifiers'][$remainingIdentifiers][0] ?? $context['property']; |
||||
135 | |||||
136 | $manager = $this->managerRegistry->getManagerForClass($identifierResourceClass); |
||||
137 | |||||
138 | if (!$manager instanceof EntityManagerInterface) { |
||||
139 | throw new RuntimeException("The manager for $identifierResourceClass must be an EntityManager."); |
||||
140 | } |
||||
141 | |||||
142 | $classMetadata = $manager->getClassMetadata($identifierResourceClass); |
||||
143 | |||||
144 | if (!$classMetadata instanceof ClassMetadataInfo) { |
||||
145 | throw new RuntimeException( |
||||
146 | "The class metadata for $identifierResourceClass must be an instance of ClassMetadataInfo." |
||||
147 | ); |
||||
148 | } |
||||
149 | |||||
150 | $qb = $manager->createQueryBuilder(); |
||||
151 | $alias = $queryNameGenerator->generateJoinAlias($identifier); |
||||
152 | $normalizedIdentifiers = []; |
||||
153 | |||||
154 | if (isset($identifiers[$identifier])) { |
||||
155 | // if it's an array it's already normalized, the IdentifierManagerTrait is deprecated |
||||
156 | if ($context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] ?? false) { |
||||
157 | $normalizedIdentifiers = $identifiers[$identifier]; |
||||
158 | } else { |
||||
159 | $normalizedIdentifiers = $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass); |
||||
160 | } |
||||
161 | } |
||||
162 | |||||
163 | if ($classMetadata->hasAssociation($previousAssociationProperty)) { |
||||
164 | $relationType = $classMetadata->getAssociationMapping($previousAssociationProperty)['type']; |
||||
165 | switch ($relationType) { |
||||
166 | // MANY_TO_MANY relations need an explicit join so that the identifier part can be retrieved |
||||
167 | case ClassMetadataInfo::MANY_TO_MANY: |
||||
168 | $joinAlias = $queryNameGenerator->generateJoinAlias($previousAssociationProperty); |
||||
169 | |||||
170 | $qb->select($joinAlias) |
||||
171 | ->from($identifierResourceClass, $alias) |
||||
172 | ->innerJoin("$alias.$previousAssociationProperty", $joinAlias); |
||||
173 | break; |
||||
174 | case ClassMetadataInfo::ONE_TO_MANY: |
||||
175 | $mappedBy = $classMetadata->getAssociationMapping($previousAssociationProperty)['mappedBy']; |
||||
176 | $previousAlias = "$previousAlias.$mappedBy"; |
||||
177 | |||||
178 | $qb->select($alias) |
||||
179 | ->from($identifierResourceClass, $alias); |
||||
180 | break; |
||||
181 | case ClassMetadataInfo::ONE_TO_ONE: |
||||
182 | $association = $classMetadata->getAssociationMapping($previousAssociationProperty); |
||||
183 | if (!isset($association['mappedBy'])) { |
||||
184 | $qb->select("IDENTITY($alias.$previousAssociationProperty)") |
||||
185 | ->from($identifierResourceClass, $alias); |
||||
186 | break; |
||||
187 | } |
||||
188 | $mappedBy = $association['mappedBy']; |
||||
189 | $previousAlias = "$previousAlias.$mappedBy"; |
||||
190 | |||||
191 | $qb->select($alias) |
||||
192 | ->from($identifierResourceClass, $alias); |
||||
193 | break; |
||||
194 | default: |
||||
195 | $qb->select("IDENTITY($alias.$previousAssociationProperty)") |
||||
196 | ->from($identifierResourceClass, $alias); |
||||
197 | } |
||||
198 | } elseif ($classMetadata->isIdentifier($previousAssociationProperty)) { |
||||
199 | $qb->select($alias) |
||||
200 | ->from($identifierResourceClass, $alias); |
||||
201 | } |
||||
202 | |||||
203 | // Add where clause for identifiers |
||||
204 | foreach ($normalizedIdentifiers as $key => $value) { |
||||
205 | $placeholder = $queryNameGenerator->generateParameterName($key); |
||||
206 | $qb->andWhere("$alias.$key = :$placeholder"); |
||||
207 | $topQueryBuilder->setParameter($placeholder, $value, (string) $classMetadata->getTypeOfField($key)); |
||||
208 | } |
||||
209 | |||||
210 | // Recurse queries |
||||
211 | $qb = $this->buildQuery($identifiers, $context, $queryNameGenerator, $qb, $alias, --$remainingIdentifiers, $topQueryBuilder); |
||||
212 | |||||
213 | return $previousQueryBuilder->andWhere($qb->expr()->in($previousAlias, $qb->getDQL())); |
||||
214 | } |
||||
215 | } |
||||
216 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.