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

Skip to content

Commit 4706ebf

Browse files
committed
[DoctrineBridge] Allow using \Closure in #[MapEntity]
1 parent ee9180b commit 4706ebf

7 files changed

Lines changed: 183 additions & 14 deletions

File tree

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ public function resolve(string $argumentName, InputInterface $input, ReflectionM
7878
}
7979

8080
$message = '';
81-
if (null !== $options->expr) {
81+
if ($options->expr instanceof \Closure) {
82+
$object = $this->findViaClosure($manager, $options, $input);
83+
} elseif (null !== $options->expr) {
8284
$variables = array_merge($input->getArguments(), ['input' => $input]);
8385
if (null === $object = $this->findViaExpression($this->expressionLanguage, $manager, $options, $variables)) {
8486
$message = \sprintf(' The expression "%s" returned null.', $options->expr);

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ public function resolve(Request $request, ArgumentMetadata $argument): array
6060
}
6161

6262
$message = '';
63-
if (null !== $options->expr) {
63+
if ($options->expr instanceof \Closure) {
64+
$object = $this->findViaClosure($manager, $options, $request);
65+
} elseif (null !== $options->expr) {
6466
$variables = array_merge($request->attributes->all(), ['request' => $request]);
6567
if (null === $object = $this->findViaExpression($this->expressionLanguage, $manager, $options, $variables)) {
6668
$message = \sprintf(' The expression "%s" returned null.', $options->expr);

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ private function findViaExpression(?ExpressionLanguage $expressionLanguage, Obje
9999
}
100100
}
101101

102+
/**
103+
* Finds an entity via closure.
104+
*/
105+
private function findViaClosure(ObjectManager $manager, MapEntity $options, mixed $context): object|iterable|null
106+
{
107+
try {
108+
return ($options->expr)($context, $manager->getRepository($options->class));
109+
} catch (NoResultException|ConversionException) {
110+
return null;
111+
}
112+
}
113+
102114
/**
103115
* Finds an entity by criteria.
104116
*/

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

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111

1212
namespace Symfony\Bridge\Doctrine\Attribute;
1313

14+
use Doctrine\Persistence\ObjectRepository;
1415
use Symfony\Bridge\Doctrine\ArgumentResolver\EntityValueResolver;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\HttpFoundation\Request;
1518
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
1619

1720
/**
@@ -21,22 +24,24 @@
2124
class MapEntity extends ValueResolver
2225
{
2326
/**
24-
* @param class-string|null $class The entity class
25-
* @param string|null $objectManager Specify the object manager used to retrieve the entity
26-
* @param string|null $expr An expression to fetch the entity using the {@see https://symfony.com/doc/current/components/expression_language.html ExpressionLanguage} syntax.
27-
* Any request attribute are available as a variable, and your entity repository in the 'repository' variable.
28-
* @param array<string, string>|null $mapping Configures the properties and values to use with the findOneBy() method
29-
* The key is the route placeholder name and the value is the Doctrine property name
30-
* @param string[]|null $exclude Configures the properties that should be used in the findOneBy() method by excluding
31-
* one or more properties so that not all are used
32-
* @param bool|null $stripNull Whether to prevent null values from being used as parameters in the query (defaults to false)
33-
* @param string[]|string|null $id If an id option is configured and matches a route parameter, then the resolver will find by the primary key
34-
* @param bool|null $evictCache If true, forces Doctrine to always fetch the entity from the database instead of cache (defaults to false)
27+
* @param class-string|null $class The entity class
28+
* @param string|null $objectManager Specify the object manager used to retrieve the entity
29+
* @param string|\Closure(Request|InputInterface, ObjectRepository):object|iterable|null $expr An expression or closure to fetch the entity.
30+
* As a string, uses the {@see https://symfony.com/doc/current/components/expression_language.html ExpressionLanguage} syntax
31+
* where any request attribute is available as a variable, and the entity repository in the 'repository' variable.
32+
* As a closure, it receives the request/input and the repository as arguments.
33+
* @param array<string, string>|null $mapping Configures the properties and values to use with the findOneBy() method
34+
* The key is the route placeholder name and the value is the Doctrine property name
35+
* @param string[]|null $exclude Configures the properties that should be used in the findOneBy() method by excluding
36+
* one or more properties so that not all are used
37+
* @param bool|null $stripNull Whether to prevent null values from being used as parameters in the query (defaults to false)
38+
* @param string[]|string|null $id If an id option is configured and matches a route parameter, then the resolver will find by the primary key
39+
* @param bool|null $evictCache If true, forces Doctrine to always fetch the entity from the database instead of cache (defaults to false)
3540
*/
3641
public function __construct(
3742
public ?string $class = null,
3843
public ?string $objectManager = null,
39-
public ?string $expr = null,
44+
public string|\Closure|null $expr = null,
4045
public ?array $mapping = null,
4146
public ?array $exclude = null,
4247
public ?bool $stripNull = null,

src/Symfony/Bridge/Doctrine/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add option `uid_format` to `EntityType`
88
* Deprecate setting an `$aliasMap` in `RegisterMappingsPass`. Namespace aliases are no longer supported in Doctrine.
9+
* Allow using closures with the `#[MapEntity]` attribute.
910

1011
8.0
1112
---

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,92 @@ public function testExpressionSyntaxErrorThrowsException()
490490
$resolver->resolve($request, $argument);
491491
}
492492

493+
public function testClosureMapsToArgument()
494+
{
495+
$manager = $this->createMock(ObjectManager::class);
496+
$registry = $this->createRegistry($manager);
497+
$resolver = new EntityValueResolver($registry);
498+
499+
$request = new Request();
500+
$request->attributes->set('id', 5);
501+
$argument = $this->createArgument(
502+
'stdClass',
503+
new MapEntity(expr: static fn (Request $request, ObjectRepository $repository) => $repository->findOneBy(['id' => $request->attributes->get('id')])),
504+
'arg1'
505+
);
506+
507+
$repository = $this->createMock(ObjectRepository::class);
508+
$repository->expects($this->never())
509+
->method('find');
510+
$repository->expects($this->once())
511+
->method('findOneBy')
512+
->with(['id' => 5])
513+
->willReturn($object = new \stdClass());
514+
515+
$manager->expects($this->once())
516+
->method('getRepository')
517+
->with(\stdClass::class)
518+
->willReturn($repository);
519+
520+
$this->assertSame([$object], $resolver->resolve($request, $argument));
521+
}
522+
523+
public function testClosureReturnsNullThrows404()
524+
{
525+
$manager = $this->createMock(ObjectManager::class);
526+
$registry = $this->createRegistry($manager);
527+
$resolver = new EntityValueResolver($registry);
528+
529+
$request = new Request();
530+
$argument = $this->createArgument(
531+
'stdClass',
532+
new MapEntity(expr: static fn () => null),
533+
'arg1'
534+
);
535+
536+
$repository = $this->createMock(ObjectRepository::class);
537+
$manager->expects($this->once())
538+
->method('getRepository')
539+
->with(\stdClass::class)
540+
->willReturn($repository);
541+
542+
$this->expectException(NotFoundHttpException::class);
543+
544+
$resolver->resolve($request, $argument);
545+
}
546+
547+
public function testClosureFailureReturns404()
548+
{
549+
$manager = $this->createMock(ObjectManager::class);
550+
$registry = $this->createRegistry($manager);
551+
$resolver = new EntityValueResolver($registry);
552+
553+
$request = new Request();
554+
$argument = $this->createArgument(
555+
'stdClass',
556+
new MapEntity(expr: static function (Request $request, ObjectRepository $repository) {
557+
$repository->findOneBy(['id' => $request->attributes->get('id')]);
558+
559+
throw new ConversionException();
560+
}),
561+
'arg1'
562+
);
563+
564+
$repository = $this->createMock(ObjectRepository::class);
565+
$repository->expects($this->once())
566+
->method('findOneBy')
567+
->with(['id' => null]);
568+
569+
$manager->expects($this->once())
570+
->method('getRepository')
571+
->with(\stdClass::class)
572+
->willReturn($repository);
573+
574+
$this->expectException(NotFoundHttpException::class);
575+
576+
$resolver->resolve($request, $argument);
577+
}
578+
493579
public function testAlreadyResolved()
494580
{
495581
$manager = $this->createStub(ObjectManager::class);

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Bridge\Doctrine\Tests\Console\ArgumentResolver;
1313

14+
use Doctrine\DBAL\Types\ConversionException;
1415
use Doctrine\Persistence\ManagerRegistry;
1516
use Doctrine\Persistence\ObjectManager;
1617
use Doctrine\Persistence\ObjectRepository;
@@ -24,6 +25,7 @@
2425
use Symfony\Component\Console\Input\ArrayInput;
2526
use Symfony\Component\Console\Input\InputArgument;
2627
use Symfony\Component\Console\Input\InputDefinition;
28+
use Symfony\Component\Console\Input\InputInterface;
2729
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
2830

2931
class EntityValueResolverTest extends TestCase
@@ -250,6 +252,65 @@ public function testResolveWithExpression()
250252
$this->assertSame([$object], iterator_to_array($resolver->resolve('entity', $input, $member)));
251253
}
252254

255+
public function testResolveWithClosure()
256+
{
257+
$manager = $this->createMock(ObjectManager::class);
258+
$registry = $this->createRegistry($manager);
259+
$resolver = new EntityValueResolver($registry, null, new MapEntity(expr: static fn (InputInterface $input, ObjectRepository $repository) => $repository->findOneBy(['id' => $input->getArgument('entity')])));
260+
261+
$input = new ArrayInput(['entity' => 1], new InputDefinition([
262+
new InputArgument('entity'),
263+
]));
264+
265+
$repository = $this->createMock(ObjectRepository::class);
266+
$repository->expects($this->never())
267+
->method('find');
268+
$repository->expects($this->once())
269+
->method('findOneBy')
270+
->with(['id' => 1])
271+
->willReturn($object = new \stdClass());
272+
273+
$manager->expects($this->once())
274+
->method('getRepository')
275+
->with(\stdClass::class)
276+
->willReturn($repository);
277+
278+
$member = $this->createMember('entity', \stdClass::class);
279+
280+
$this->assertSame([$object], iterator_to_array($resolver->resolve('entity', $input, $member)));
281+
}
282+
283+
public function testResolveWithClosureFailureThrowsException()
284+
{
285+
$manager = $this->createMock(ObjectManager::class);
286+
$registry = $this->createRegistry($manager);
287+
$resolver = new EntityValueResolver($registry, null, new MapEntity(expr: static function (InputInterface $input, ObjectRepository $repository) {
288+
$repository->findOneBy(['id' => $input->getArgument('entity')]);
289+
290+
throw new ConversionException();
291+
}));
292+
293+
$input = new ArrayInput(['entity' => 1], new InputDefinition([
294+
new InputArgument('entity'),
295+
]));
296+
297+
$repository = $this->createMock(ObjectRepository::class);
298+
$repository->expects($this->once())
299+
->method('findOneBy')
300+
->with(['id' => 1]);
301+
302+
$manager->expects($this->once())
303+
->method('getRepository')
304+
->with(\stdClass::class)
305+
->willReturn($repository);
306+
307+
$member = $this->createMember('entity', \stdClass::class);
308+
309+
$this->expectException(RuntimeException::class);
310+
311+
iterator_to_array($resolver->resolve('entity', $input, $member));
312+
}
313+
253314
public function testResolveWithStripNull()
254315
{
255316
$manager = $this->createMock(ObjectManager::class);

0 commit comments

Comments
 (0)