1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
/* |
5
|
|
|
* This file is part of the API Platform project. |
6
|
|
|
* |
7
|
|
|
* (c) Kévin Dunglas <[email protected]> |
8
|
|
|
* |
9
|
|
|
* For the full copyright and license information, please view the LICENSE |
10
|
|
|
* file that was distributed with this source code. |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace ApiPlatform\Core\Bridge\Doctrine\Orm; |
14
|
|
|
|
15
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface; |
16
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface; |
17
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface; |
18
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultItemExtensionInterface; |
19
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\IdentifierManagerTrait; |
20
|
|
|
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGenerator; |
21
|
|
|
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; |
22
|
|
|
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException; |
23
|
|
|
use ApiPlatform\Core\Exception\RuntimeException; |
24
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
25
|
|
|
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
26
|
|
|
use Doctrine\Common\Persistence\ManagerRegistry; |
27
|
|
|
use Doctrine\ORM\EntityManagerInterface; |
28
|
|
|
use Doctrine\ORM\Mapping\ClassMetadataInfo; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Subresource data provider for the Doctrine ORM. |
32
|
|
|
* |
33
|
|
|
* @author Antoine Bluchet <[email protected]> |
34
|
|
|
*/ |
35
|
|
|
final class SubresourceDataProvider implements SubresourceDataProviderInterface |
36
|
|
|
{ |
37
|
|
|
use IdentifierManagerTrait; |
38
|
|
|
|
39
|
|
|
private $managerRegistry; |
40
|
|
|
private $collectionExtensions; |
41
|
|
|
private $itemExtensions; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @param ManagerRegistry $managerRegistry |
45
|
|
|
* @param PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory |
46
|
|
|
* @param PropertyMetadataFactoryInterface $propertyMetadataFactory |
47
|
|
|
* @param QueryCollectionExtensionInterface[] $collectionExtensions |
48
|
|
|
* @param QueryItemExtensionInterface[] $itemExtensions |
49
|
|
|
*/ |
50
|
|
View Code Duplication |
public function __construct(ManagerRegistry $managerRegistry, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, array $collectionExtensions = [], array $itemExtensions = []) |
|
|
|
|
51
|
|
|
{ |
52
|
|
|
$this->managerRegistry = $managerRegistry; |
53
|
|
|
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
54
|
|
|
$this->propertyMetadataFactory = $propertyMetadataFactory; |
55
|
|
|
$this->collectionExtensions = $collectionExtensions; |
56
|
|
|
$this->itemExtensions = $itemExtensions; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* {@inheritdoc} |
61
|
|
|
* |
62
|
|
|
* @throws RuntimeException |
63
|
|
|
*/ |
64
|
|
|
public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null) |
65
|
|
|
{ |
66
|
|
|
$manager = $this->managerRegistry->getManagerForClass($resourceClass); |
67
|
|
|
if (null === $manager) { |
68
|
|
|
throw new ResourceClassNotSupportedException(); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
$repository = $manager->getRepository($resourceClass); |
72
|
|
|
if (!method_exists($repository, 'createQueryBuilder')) { |
73
|
|
|
throw new RuntimeException('The repository class must have a "createQueryBuilder" method.'); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
if (!isset($context['identifiers']) || !isset($context['property'])) { |
77
|
|
|
throw new ResourceClassNotSupportedException('The given resource class is not a subresource.'); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
$originAlias = 'o'; |
81
|
|
|
$queryBuilder = $repository->createQueryBuilder($originAlias); |
82
|
|
|
$queryNameGenerator = new QueryNameGenerator(); |
83
|
|
|
$previousQueryBuilder = null; |
84
|
|
|
$previousAlias = null; |
85
|
|
|
|
86
|
|
|
$num = count($context['identifiers']); |
87
|
|
|
|
88
|
|
|
while ($num--) { |
89
|
|
|
list($identifier, $identifierResourceClass) = $context['identifiers'][$num]; |
90
|
|
|
$previousAssociationProperty = $context['identifiers'][$num + 1][0] ?? $context['property']; |
91
|
|
|
|
92
|
|
|
$manager = $this->managerRegistry->getManagerForClass($identifierResourceClass); |
93
|
|
|
|
94
|
|
|
if (!$manager instanceof EntityManagerInterface) { |
95
|
|
|
throw new RuntimeException("The manager for $identifierResourceClass must be an EntityManager."); |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
$classMetadata = $manager->getClassMetadata($identifierResourceClass); |
99
|
|
|
|
100
|
|
|
if (!$classMetadata instanceof ClassMetadataInfo) { |
101
|
|
|
throw new RuntimeException("The class metadata for $identifierResourceClass must be an instance of ClassMetadataInfo."); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
$qb = $manager->createQueryBuilder(); |
105
|
|
|
$alias = $queryNameGenerator->generateJoinAlias($identifier); |
106
|
|
|
$relationType = $classMetadata->getAssociationMapping($previousAssociationProperty)['type']; |
107
|
|
|
$normalizedIdentifiers = $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass); |
108
|
|
|
|
109
|
|
|
switch ($relationType) { |
110
|
|
|
//MANY_TO_MANY relations need an explicit join so that the identifier part can be retrieved |
111
|
|
|
case ClassMetadataInfo::MANY_TO_MANY: |
|
|
|
|
112
|
|
|
$joinAlias = $queryNameGenerator->generateJoinAlias($previousAssociationProperty); |
113
|
|
|
|
114
|
|
|
$qb->select($joinAlias) |
115
|
|
|
->from($identifierResourceClass, $alias) |
116
|
|
|
->innerJoin("$alias.$previousAssociationProperty", $joinAlias); |
117
|
|
|
|
118
|
|
|
break; |
119
|
|
|
case ClassMetadataInfo::ONE_TO_MANY: |
|
|
|
|
120
|
|
|
$mappedBy = $classMetadata->getAssociationMapping($previousAssociationProperty)['mappedBy']; |
121
|
|
|
|
122
|
|
|
// first pass, o.property instead of alias.property |
123
|
|
|
if (null === $previousQueryBuilder) { |
124
|
|
|
$originAlias = "$originAlias.$mappedBy"; |
125
|
|
|
} else { |
126
|
|
|
$previousAlias = "$previousAlias.$mappedBy"; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
$qb->select($alias) |
130
|
|
|
->from($identifierResourceClass, $alias); |
131
|
|
|
break; |
132
|
|
|
default: |
133
|
|
|
$qb->select("IDENTITY($alias.$previousAssociationProperty)") |
134
|
|
|
->from($identifierResourceClass, $alias); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
// Add where clause for identifiers |
138
|
|
|
foreach ($normalizedIdentifiers as $key => $value) { |
139
|
|
|
$placeholder = $queryNameGenerator->generateParameterName($key); |
140
|
|
|
$qb->andWhere("$alias.$key = :$placeholder"); |
141
|
|
|
$queryBuilder->setParameter($placeholder, $value); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
// recurse queries |
145
|
|
|
if (null === $previousQueryBuilder) { |
146
|
|
|
$previousQueryBuilder = $qb; |
147
|
|
|
} else { |
148
|
|
|
$previousQueryBuilder->andWhere($qb->expr()->in($previousAlias, $qb->getDQL())); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
$previousAlias = $alias; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/* |
155
|
|
|
* The following translate to this pseudo-dql: |
156
|
|
|
* |
157
|
|
|
* SELECT thirdLevel WHERE thirdLevel IN ( |
158
|
|
|
* SELECT thirdLevel FROM relatedDummies WHERE relatedDummies = ? AND relatedDummies IN ( |
159
|
|
|
* SELECT relatedDummies FROM Dummy WHERE Dummy = ? |
160
|
|
|
* ) |
161
|
|
|
* ) |
162
|
|
|
* |
163
|
|
|
* By using subqueries, we're forcing the SQL execution plan to go through indexes on doctrine identifiers. |
164
|
|
|
*/ |
165
|
|
|
$queryBuilder->where( |
166
|
|
|
$queryBuilder->expr()->in($originAlias, $previousQueryBuilder->getDQL()) |
167
|
|
|
); |
168
|
|
|
|
169
|
|
|
if (true === $context['collection']) { |
170
|
|
View Code Duplication |
foreach ($this->collectionExtensions as $extension) { |
|
|
|
|
171
|
|
|
$extension->applyToCollection($queryBuilder, $queryNameGenerator, $resourceClass, $operationName); |
172
|
|
|
|
173
|
|
|
if ($extension instanceof QueryResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName)) { |
174
|
|
|
return $extension->getResult($queryBuilder); |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
} else { |
178
|
|
View Code Duplication |
foreach ($this->itemExtensions as $extension) { |
|
|
|
|
179
|
|
|
$extension->applyToItem($queryBuilder, $queryNameGenerator, $resourceClass, $identifiers, $operationName, $context); |
180
|
|
|
|
181
|
|
|
if ($extension instanceof QueryResultItemExtensionInterface && $extension->supportsResult($resourceClass, $operationName)) { |
182
|
|
|
return $extension->getResult($queryBuilder); |
183
|
|
|
} |
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$query = $queryBuilder->getQuery(); |
188
|
|
|
|
189
|
|
|
return $context['collection'] ? $query->getResult() : $query->getOneOrNullResult(); |
190
|
|
|
} |
191
|
|
|
} |
192
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.