Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bfa695d

Browse files
committed
Add an Entity Argument Resolver
1 parent fd2cab9 commit bfa695d

3 files changed

Lines changed: 883 additions & 0 deletions

File tree

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Bridge\Doctrine\ArgumentResolver;
13+
14+
use Doctrine\DBAL\Types\ConversionException;
15+
use Doctrine\ORM\EntityManagerInterface;
16+
use Doctrine\ORM\NoResultException;
17+
use Doctrine\Persistence\ManagerRegistry;
18+
use Doctrine\Persistence\ObjectManager;
19+
use Symfony\Bridge\Doctrine\Attribute\Entity;
20+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
21+
use Symfony\Component\HttpFoundation\Request;
22+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
23+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
24+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
25+
26+
/**
27+
* Yields the entity matching the criteria provided in the route.
28+
*
29+
* @author Fabien Potencier <[email protected]>
30+
* @author Jérémy Derussé <[email protected]>
31+
*/
32+
final class EntityValueResolver implements ArgumentValueResolverInterface
33+
{
34+
private array $defaultOptions;
35+
36+
public function __construct(
37+
private ManagerRegistry $registry,
38+
private ?ExpressionLanguage $language = null,
39+
array $defaultOptions = []
40+
) {
41+
$this->defaultOptions = array_merge([
42+
'entity_manager' => null,
43+
'expr' => null,
44+
'auto_mapping' => true,
45+
'mapping' => [],
46+
'exclude' => [],
47+
'strip_null' => false,
48+
'id' => null,
49+
'evict_cache' => false,
50+
], $defaultOptions);
51+
}
52+
53+
/**
54+
* {@inheritdoc}
55+
*/
56+
public function supports(Request $request, ArgumentMetadata $argument): bool
57+
{
58+
if (0 === \count($this->registry->getManagerNames())) {
59+
return false;
60+
}
61+
62+
$options = $this->getOptions($argument);
63+
if (null === $options['class']) {
64+
return false;
65+
}
66+
67+
// Doctrine Entity?
68+
$em = $this->getManager($options['entity_manager'], $options['class']);
69+
if (null === $em) {
70+
return false;
71+
}
72+
73+
return !$em->getMetadataFactory()->isTransient($options['class']);
74+
}
75+
76+
/**
77+
* {@inheritdoc}
78+
*/
79+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
80+
{
81+
$options = $this->getOptions($argument);
82+
83+
$name = $argument->getName();
84+
$class = $options['class'];
85+
86+
$errorMessage = null;
87+
if (null !== $options['expr']) {
88+
$object = $this->findViaExpression($class, $request, $options['expr'], $options);
89+
90+
if (null === $object) {
91+
$errorMessage = sprintf('The expression "%s" returned null', $options['expr']);
92+
}
93+
// find by identifier?
94+
} else {
95+
$object = $this->find($class, $request, $options, $name);
96+
if (false === $object) {
97+
// find by criteria
98+
$object = $this->findOneBy($class, $request, $options);
99+
if (false === $object) {
100+
if (!$argument->isNullable()) {
101+
throw new \LogicException(sprintf('Unable to guess how to get a Doctrine instance from the request information for parameter "%s".', $name));
102+
}
103+
104+
$object = null;
105+
}
106+
}
107+
}
108+
109+
if (null === $object && !$argument->isNullable()) {
110+
$message = sprintf('"%s" object not found by the "%s" Argument Resolver.', $class, self::class);
111+
if ($errorMessage) {
112+
$message .= ' '.$errorMessage;
113+
}
114+
115+
throw new NotFoundHttpException($message);
116+
}
117+
118+
return [$object];
119+
}
120+
121+
private function getManager(?string $name, string $class): ?ObjectManager
122+
{
123+
if (null === $name) {
124+
return $this->registry->getManagerForClass($class);
125+
}
126+
127+
return $this->registry->getManager($name);
128+
}
129+
130+
private function find(string $class, Request $request, array $options, string $name): false|object|null
131+
{
132+
if ($options['mapping'] || $options['exclude']) {
133+
return false;
134+
}
135+
136+
$id = $this->getIdentifier($request, $options, $name);
137+
if (false === $id || null === $id) {
138+
return false;
139+
}
140+
141+
$em = $this->getManager($options['entity_manager'], $class);
142+
if ($options['evict_cache'] && $em instanceof EntityManagerInterface) {
143+
$cacheProvider = $em->getCache();
144+
if ($cacheProvider && $cacheProvider->containsEntity($class, $id)) {
145+
$cacheProvider->evictEntity($class, $id);
146+
}
147+
}
148+
149+
try {
150+
return $em->getRepository($class)->find($id);
151+
} catch (NoResultException|ConversionException $e) {
152+
return null;
153+
}
154+
}
155+
156+
private function getIdentifier(Request $request, array $options, string $name): mixed
157+
{
158+
if (null !== $options['id']) {
159+
if (\is_array($options['id'])) {
160+
$id = [];
161+
foreach ($options['id'] as $field) {
162+
// Convert "%s_uuid" to "foobar_uuid"
163+
if (str_contains($field, '%s')) {
164+
$field = sprintf($field, $name);
165+
}
166+
167+
$id[$field] = $request->attributes->get($field);
168+
}
169+
170+
return $id;
171+
}
172+
173+
$name = $options['id'];
174+
}
175+
176+
if ($request->attributes->has($name)) {
177+
return $request->attributes->get($name);
178+
}
179+
180+
if ($request->attributes->has('id') && !$options['id']) {
181+
return $request->attributes->get('id');
182+
}
183+
184+
return false;
185+
}
186+
187+
private function findOneBy(string $class, Request $request, array $options): false|object|null
188+
{
189+
if (!$options['mapping']) {
190+
if (!$options['auto_mapping']) {
191+
return false;
192+
}
193+
194+
$keys = $request->attributes->keys();
195+
$options['mapping'] = $keys ? array_combine($keys, $keys) : [];
196+
}
197+
198+
foreach ($options['exclude'] as $exclude) {
199+
unset($options['mapping'][$exclude]);
200+
}
201+
202+
if (!$options['mapping']) {
203+
return false;
204+
}
205+
206+
// if a specific id has been defined in the options and there is no corresponding attribute
207+
// return false in order to avoid a fallback to the id which might be of another object
208+
if ($options['id'] && null === $request->attributes->get($options['id'])) {
209+
return false;
210+
}
211+
212+
$criteria = [];
213+
$em = $this->getManager($options['entity_manager'], $class);
214+
$metadata = $em->getClassMetadata($class);
215+
216+
foreach ($options['mapping'] as $attribute => $field) {
217+
if (!$metadata->hasField($field) && (!$metadata->hasAssociation(
218+
$field
219+
) || !$metadata->isSingleValuedAssociation($field))) {
220+
continue;
221+
}
222+
223+
$criteria[$field] = $request->attributes->get($attribute);
224+
}
225+
226+
if ($options['strip_null']) {
227+
$criteria = array_filter($criteria, static function ($value) {
228+
return null !== $value;
229+
});
230+
}
231+
232+
if (!$criteria) {
233+
return false;
234+
}
235+
236+
try {
237+
return $em->getRepository($class)->findOneBy($criteria);
238+
} catch (NoResultException|ConversionException $e) {
239+
return null;
240+
}
241+
}
242+
243+
private function findViaExpression(string $class, Request $request, string $expression, array $options): ?object
244+
{
245+
if (null === $this->language) {
246+
throw new \LogicException(sprintf('You cannot use the "%s" if the ExpressionLanguage component is not available. Try running "composer require symfony/expression-language".', __CLASS__));
247+
}
248+
249+
$repository = $this->getManager($options['entity_manager'], $class)->getRepository($class);
250+
$variables = array_merge($request->attributes->all(), ['repository' => $repository]);
251+
252+
try {
253+
return $this->language->evaluate($expression, $variables);
254+
} catch (NoResultException|ConversionException $e) {
255+
return null;
256+
}
257+
}
258+
259+
private function getOptions(ArgumentMetadata $argument): array
260+
{
261+
/** @var ?Entity $configuration */
262+
$configuration = method_exists($argument, 'getAttributes') ? $argument->getAttributes(Entity::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null : null;
263+
264+
$argumentClass = $argument->getType();
265+
if (!class_exists($argumentClass)) {
266+
$argumentClass = null;
267+
}
268+
269+
if (null === $configuration) {
270+
return array_merge($this->defaultOptions, [
271+
'class' => $argumentClass,
272+
]);
273+
}
274+
275+
return array_merge($this->defaultOptions, [
276+
'class' => $configuration->class ?? $argumentClass,
277+
'entity_manager' => $configuration->entityManager,
278+
'expr' => $configuration->expr,
279+
'mapping' => $configuration->mapping,
280+
'exclude' => $configuration->exclude,
281+
'strip_null' => $configuration->stripNull,
282+
'id' => $configuration->id,
283+
'evict_cache' => $configuration->evictCache,
284+
]);
285+
}
286+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[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+
namespace Symfony\Bridge\Doctrine\Attribute;
13+
14+
/**
15+
* Indicates that a controller argument should receive an Entity.
16+
*/
17+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
18+
class Entity
19+
{
20+
public function __construct(
21+
public ?string $class = null,
22+
public ?string $entityManager = null,
23+
public ?string $expr = null,
24+
public array $mapping = [],
25+
public array $exclude = [],
26+
public bool $stripNull = false,
27+
public array|string|null $id = null,
28+
public bool $evictCache = false,
29+
) {
30+
}
31+
}

0 commit comments

Comments
 (0)