Skip to content

Commit 0966e43

Browse files
Improve EntityValueResolver (#3)
Co-authored-by: Jérémy Derussé <jeremy@derusse.com>
1 parent a9f09ae commit 0966e43

File tree

4 files changed

+69
-95
lines changed

4 files changed

+69
-95
lines changed

src/Symfony/Bridge/Doctrine/ArgumentResolver/EntityValueResolver.php

+38-85
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,11 @@
3131
*/
3232
final class EntityValueResolver implements ArgumentValueResolverInterface
3333
{
34-
private array $defaultOptions = [
35-
'object_manager' => null,
36-
'expr' => null,
37-
'mapping' => [],
38-
'exclude' => [],
39-
'strip_null' => false,
40-
'id' => null,
41-
'evict_cache' => false,
42-
'auto_mapping' => true,
43-
'attribute_only' => false,
44-
];
45-
4634
public function __construct(
4735
private ManagerRegistry $registry,
48-
private ?ExpressionLanguage $language = null,
49-
array $defaultOptions = [],
36+
private ?ExpressionLanguage $expressionLanguage = null,
37+
private MapEntity $defaults = new MapEntity(),
5038
) {
51-
$this->defaultOptions = array_merge($this->defaultOptions, $defaultOptions);
5239
}
5340

5441
/**
@@ -61,20 +48,16 @@ public function supports(Request $request, ArgumentMetadata $argument): bool
6148
}
6249

6350
$options = $this->getOptions($argument);
64-
if (null === $options['class']) {
65-
return false;
66-
}
67-
68-
if ($options['attribute_only'] && !$options['has_attribute']) {
51+
if (!$options->class || $options->disabled) {
6952
return false;
7053
}
7154

7255
// Doctrine Entity?
73-
if (null === $objectManager = $this->getManager($options['object_manager'], $options['class'])) {
56+
if (!$objectManager = $this->getManager($options->objectManager, $options->class)) {
7457
return false;
7558
}
7659

77-
return !$objectManager->getMetadataFactory()->isTransient($options['class']);
60+
return !$objectManager->getMetadataFactory()->isTransient($options->class);
7861
}
7962

8063
/**
@@ -83,20 +66,18 @@ public function supports(Request $request, ArgumentMetadata $argument): bool
8366
public function resolve(Request $request, ArgumentMetadata $argument): iterable
8467
{
8568
$options = $this->getOptions($argument);
86-
8769
$name = $argument->getName();
88-
$class = $options['class'];
70+
$class = $options->class;
8971

9072
$errorMessage = null;
91-
if (null !== $options['expr']) {
92-
if (null === $object = $this->findViaExpression($class, $request, $options['expr'], $options)) {
93-
$errorMessage = sprintf('The expression "%s" returned null', $options['expr']);
73+
if (null !== $options->expr) {
74+
if (null === $object = $this->findViaExpression($class, $request, $options->expr, $options)) {
75+
$errorMessage = sprintf('The expression "%s" returned null', $options->expr);
9476
}
9577
// find by identifier?
9678
} elseif (false === $object = $this->find($class, $request, $options, $name)) {
9779
// find by criteria
98-
$object = $this->findOneBy($class, $request, $options);
99-
if (false === $object) {
80+
if (false === $object = $this->findOneBy($class, $request, $options)) {
10081
if (!$argument->isNullable()) {
10182
throw new \LogicException(sprintf('Unable to guess how to get a Doctrine instance from the request information for parameter "%s".', $name));
10283
}
@@ -134,9 +115,9 @@ private function getManager(?string $name, string $class): ?ObjectManager
134115
}
135116
}
136117

137-
private function find(string $class, Request $request, array $options, string $name): false|object|null
118+
private function find(string $class, Request $request, MapEntity $options, string $name): false|object|null
138119
{
139-
if ($options['mapping'] || $options['exclude']) {
120+
if ($options->mapping || $options->exclude) {
140121
return false;
141122
}
142123

@@ -145,8 +126,8 @@ private function find(string $class, Request $request, array $options, string $n
145126
return false;
146127
}
147128

148-
$objectManager = $this->getManager($options['object_manager'], $class);
149-
if ($options['evict_cache'] && $objectManager instanceof EntityManagerInterface) {
129+
$objectManager = $this->getManager($options->objectManager, $class);
130+
if ($options->evictCache && $objectManager instanceof EntityManagerInterface) {
150131
$cacheProvider = $objectManager->getCache();
151132
if ($cacheProvider && $cacheProvider->containsEntity($class, $id)) {
152133
$cacheProvider->evictEntity($class, $id);
@@ -160,11 +141,11 @@ private function find(string $class, Request $request, array $options, string $n
160141
}
161142
}
162143

163-
private function getIdentifier(Request $request, array $options, string $name): mixed
144+
private function getIdentifier(Request $request, MapEntity $options, string $name): mixed
164145
{
165-
if (\is_array($options['id'])) {
146+
if (\is_array($options->id)) {
166147
$id = [];
167-
foreach ($options['id'] as $field) {
148+
foreach ($options->id as $field) {
168149
// Convert "%s_uuid" to "foobar_uuid"
169150
if (str_contains($field, '%s')) {
170151
$field = sprintf($field, $name);
@@ -176,59 +157,55 @@ private function getIdentifier(Request $request, array $options, string $name):
176157
return $id;
177158
}
178159

179-
if (null !== $options['id']) {
180-
$name = $options['id'];
160+
if (null !== $options->id) {
161+
$name = $options->id;
181162
}
182163

183164
if ($request->attributes->has($name)) {
184165
return $request->attributes->get($name);
185166
}
186167

187-
if (!$options['id'] && $request->attributes->has('id')) {
168+
if (!$options->id && $request->attributes->has('id')) {
188169
return $request->attributes->get('id');
189170
}
190171

191172
return false;
192173
}
193174

194-
private function findOneBy(string $class, Request $request, array $options): false|object|null
175+
private function findOneBy(string $class, Request $request, MapEntity $options): false|object|null
195176
{
196-
if (!$options['mapping']) {
197-
if (!$options['auto_mapping']) {
198-
return false;
199-
}
200-
177+
if (null === $mapping = $options->mapping) {
201178
$keys = $request->attributes->keys();
202-
$options['mapping'] = $keys ? array_combine($keys, $keys) : [];
179+
$mapping = $keys ? array_combine($keys, $keys) : [];
203180
}
204181

205-
foreach ($options['exclude'] as $exclude) {
206-
unset($options['mapping'][$exclude]);
182+
foreach ($options->exclude as $exclude) {
183+
unset($mapping[$exclude]);
207184
}
208185

209-
if (!$options['mapping']) {
186+
if (!$mapping) {
210187
return false;
211188
}
212189

213190
// if a specific id has been defined in the options and there is no corresponding attribute
214191
// return false in order to avoid a fallback to the id which might be of another object
215-
if ($options['id'] && null === $request->attributes->get($options['id'])) {
192+
if (\is_string($options->id) && null === $request->attributes->get($options->id)) {
216193
return false;
217194
}
218195

219196
$criteria = [];
220-
$objectManager = $this->getManager($options['object_manager'], $class);
197+
$objectManager = $this->getManager($options->objectManager, $class);
221198
$metadata = $objectManager->getClassMetadata($class);
222199

223-
foreach ($options['mapping'] as $attribute => $field) {
200+
foreach ($mapping as $attribute => $field) {
224201
if (!$metadata->hasField($field) && (!$metadata->hasAssociation($field) || !$metadata->isSingleValuedAssociation($field))) {
225202
continue;
226203
}
227204

228205
$criteria[$field] = $request->attributes->get($attribute);
229206
}
230207

231-
if ($options['strip_null']) {
208+
if ($options->stripNull) {
232209
$criteria = array_filter($criteria, static fn ($value) => null !== $value);
233210
}
234211

@@ -243,51 +220,27 @@ private function findOneBy(string $class, Request $request, array $options): fal
243220
}
244221
}
245222

246-
private function findViaExpression(string $class, Request $request, string $expression, array $options): ?object
223+
private function findViaExpression(string $class, Request $request, string $expression, MapEntity $options): ?object
247224
{
248-
if (null === $this->language) {
225+
if (!$this->expressionLanguage) {
249226
throw new \LogicException(sprintf('You cannot use the "%s" if the ExpressionLanguage component is not available. Try running "composer require symfony/expression-language".', __CLASS__));
250227
}
251228

252-
$repository = $this->getManager($options['object_manager'], $class)->getRepository($class);
229+
$repository = $this->getManager($options->objectManager, $class)->getRepository($class);
253230
$variables = array_merge($request->attributes->all(), ['repository' => $repository]);
254231

255232
try {
256-
return $this->language->evaluate($expression, $variables);
233+
return $this->expressionLanguage->evaluate($expression, $variables);
257234
} catch (NoResultException|ConversionException) {
258235
return null;
259236
}
260237
}
261238

262-
private function getOptions(ArgumentMetadata $argument): array
239+
private function getOptions(ArgumentMetadata $argument): MapEntity
263240
{
264-
/** @var ?MapEntity $configuration */
265-
$configuration = $argument->getAttributes(MapEntity::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null;
266-
267-
$argumentClass = $argument->getType();
268-
if ($argumentClass && !class_exists($argumentClass)) {
269-
$argumentClass = null;
270-
}
271-
272-
if (null === $configuration) {
273-
return array_merge($this->defaultOptions, [
274-
'class' => $argumentClass,
275-
'has_attribute' => false,
276-
]);
277-
}
241+
/** @var MapEntity $options */
242+
$options = $argument->getAttributes(MapEntity::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? $this->defaults;
278243

279-
return [
280-
'class' => $configuration->class ?? $argumentClass,
281-
'object_manager' => $configuration->objectManager ?? $this->defaultOptions['object_manager'],
282-
'expr' => $configuration->expr ?? $this->defaultOptions['expr'],
283-
'mapping' => $configuration->mapping ?? $this->defaultOptions['mapping'],
284-
'exclude' => $configuration->exclude ?? $this->defaultOptions['exclude'],
285-
'strip_null' => $configuration->stripNull ?? $this->defaultOptions['strip_null'],
286-
'id' => $configuration->id ?? $this->defaultOptions['id'],
287-
'evict_cache' => $configuration->evictCache ?? $this->defaultOptions['evict_cache'],
288-
'has_attribute' => true,
289-
'auto_mapping' => $this->defaultOptions['auto_mapping'],
290-
'attribute_only' => $this->defaultOptions['attribute_only'],
291-
];
244+
return $options->withDefaults($this->defaults, $argument->getType());
292245
}
293246
}

src/Symfony/Bridge/Doctrine/Attribute/MapEntity.php

+24-8
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,30 @@
1818
class MapEntity
1919
{
2020
public function __construct(
21-
public readonly ?string $class = null,
22-
public readonly ?string $objectManager = null,
23-
public readonly ?string $expr = null,
24-
public readonly array $mapping = [],
25-
public readonly array $exclude = [],
26-
public readonly bool $stripNull = false,
27-
public readonly array|string|null $id = null,
28-
public readonly bool $evictCache = false,
21+
public ?string $class = null,
22+
public ?string $objectManager = null,
23+
public ?string $expr = null,
24+
public ?array $mapping = null,
25+
public ?array $exclude = null,
26+
public ?bool $stripNull = null,
27+
public array|string|null $id = null,
28+
public ?bool $evictCache = null,
29+
public bool $disabled = false,
2930
) {
3031
}
32+
33+
public function withDefaults(self $defaults, ?string $class): static
34+
{
35+
$clone = clone $this;
36+
$clone->class ??= class_exists($class ?? '') ? $class : null;
37+
$clone->objectManager ??= $defaults->objectManager;
38+
$clone->expr ??= $defaults->expr;
39+
$clone->mapping ??= $defaults->mapping;
40+
$clone->exclude ??= $defaults->exclude ?? [];
41+
$clone->stripNull ??= $defaults->stripNull ?? false;
42+
$clone->id ??= $defaults->id;
43+
$clone->evictCache ??= $defaults->evictCache ?? false;
44+
45+
return $clone;
46+
}
3147
}

src/Symfony/Bridge/Doctrine/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.2
5+
---
6+
7+
* Add `#[MapEntity]` with its corresponding `EntityArgumentResolver`
8+
49
6.0
510
---
611

src/Symfony/Bridge/Doctrine/Tests/ArgumentResolver/EntityValueResolverTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public function testSupportWithoutClass()
8585
public function testSupportWithoutAttribute()
8686
{
8787
$registry = $this->getMockBuilder(ManagerRegistry::class)->getMock();
88-
$converter = new EntityValueResolver($registry, null, ['attribute_only' => true]);
88+
$converter = new EntityValueResolver($registry, null, new MapEntity(disabled: true));
8989

9090
$registry->expects($this->once())
9191
->method('getManagerNames')
@@ -353,7 +353,7 @@ public function testApplyWithMappingAndExclude()
353353
public function testIgnoreMappingWhenAutoMappingDisabled()
354354
{
355355
$registry = $this->getMockBuilder(ManagerRegistry::class)->getMock();
356-
$converter = new EntityValueResolver($registry, null, ['auto_mapping' => false]);
356+
$converter = new EntityValueResolver($registry, null, new MapEntity(mapping: []));
357357

358358
$request = new Request();
359359
$request->attributes->set('foo', 1);

0 commit comments

Comments
 (0)