Skip to content

Commit 4907344

Browse files
committed
Allow nested properties for AssociationField again to fix BC break
1 parent 7644c5f commit 4907344

File tree

6 files changed

+136
-31
lines changed

6 files changed

+136
-31
lines changed

src/Field/Configurator/AssociationConfigurator.php

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace EasyCorp\Bundle\EasyAdminBundle\Field\Configurator;
44

55
use Doctrine\ORM\EntityRepository;
6+
use Doctrine\ORM\Mapping\ClassMetadata;
67
use Doctrine\ORM\PersistentCollection;
78
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
89
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
@@ -56,14 +57,14 @@ public function supports(FieldDto $field, EntityDto $entityDto): bool
5657
public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $context): void
5758
{
5859
$propertyName = $field->getProperty();
59-
if (!$entityDto->getClassMetadata()->hasAssociation($propertyName)) {
60+
61+
if (!$this->isAssociation($entityDto->getClassMetadata(), $propertyName)) {
6062
throw new \RuntimeException(sprintf('The "%s" field is not a Doctrine association, so it cannot be used as an association field.', $propertyName));
6163
}
6264

63-
$targetEntityFqcn = $entityDto->getClassMetadata()->getAssociationTargetClass($propertyName);
6465
// the target CRUD controller can be NULL; in that case, field value doesn't link to the related entity
6566
$targetCrudControllerFqcn = $field->getCustomOption(AssociationField::OPTION_EMBEDDED_CRUD_FORM_CONTROLLER)
66-
?? $context->getCrudControllers()->findCrudFqcnByEntityFqcn($targetEntityFqcn);
67+
?? $context->getCrudControllers()->findCrudFqcnByEntityFqcn($entityDto->getClassMetadata()->getAssociationTargetClass($propertyName));
6768

6869
if (true === $field->getCustomOption(AssociationField::OPTION_RENDER_AS_EMBEDDED_FORM)) {
6970
if (false === $entityDto->getClassMetadata()->isSingleValuedAssociation($propertyName)) {
@@ -82,12 +83,18 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
8283
'The "%s" association field of "%s" wants to render its contents using an EasyAdmin CRUD form. However, no CRUD form was found related to this field. You can either create a CRUD controller for the entity "%s" or pass the CRUD controller to use as the first argument of the "renderAsEmbeddedForm()" method.',
8384
$field->getProperty(),
8485
$context->getCrud()?->getControllerFqcn(),
85-
$targetEntityFqcn
86+
$entityDto->getClassMetadata()->getAssociationTargetClass($propertyName)
8687
)
8788
);
8889
}
8990

90-
$this->configureCrudForm($field, $entityDto, $propertyName, $targetEntityFqcn, $targetCrudControllerFqcn);
91+
$this->configureCrudForm(
92+
$field,
93+
$entityDto,
94+
$propertyName,
95+
$entityDto->getClassMetadata()->getAssociationTargetClass($propertyName),
96+
$targetCrudControllerFqcn,
97+
);
9198

9299
return;
93100
}
@@ -190,6 +197,28 @@ public function configure(FieldDto $field, EntityDto $entityDto, AdminContext $c
190197
}
191198
}
192199

200+
/**
201+
* Recursive check if a string is a Doctrine association (e.g. "foo") or a nested Doctrine
202+
* association (e.g. "foo.bar").
203+
*/
204+
private function isAssociation(ClassMetadata $entityClassMetadata, string $property): bool
205+
{
206+
$nestedProperties = explode('.', $property);
207+
208+
$nextProperty = array_shift($nestedProperties);
209+
210+
if (!$entityClassMetadata->hasAssociation($nextProperty)) {
211+
return false;
212+
} elseif (!$nestedProperties) {
213+
return true;
214+
}
215+
216+
return $this->isAssociation(
217+
$this->entityFactory->getEntityMetadata($entityClassMetadata->getAssociationTargetClass($nextProperty)),
218+
implode('.', $nestedProperties),
219+
);
220+
}
221+
193222
private function configureToOneAssociation(FieldDto $field, EntityDto $entityDto): void
194223
{
195224
$field->setCustomOption(AssociationField::OPTION_DOCTRINE_ASSOCIATION_TYPE, 'toOne');

tests/Field/Configurator/AssociationConfiguratorTest.php

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace EasyCorp\Bundle\EasyAdminBundle\Tests\Field\Configurator;
44

55
use Doctrine\ORM\EntityManagerInterface;
6+
use Doctrine\ORM\Mapping\ClassMetadata;
7+
use EasyCorp\Bundle\EasyAdminBundle\Collection\FieldCollection;
68
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
79
use EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto;
810
use EasyCorp\Bundle\EasyAdminBundle\Factory\ControllerFactory;
@@ -15,8 +17,10 @@
1517
use EasyCorp\Bundle\EasyAdminBundle\Tests\Field\AbstractFieldTest;
1618
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Controller\ProjectDomain\DeveloperCrudController;
1719
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Controller\ProjectDomain\ProjectCrudController;
20+
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Controller\ProjectDomain\ProjectReleaseCategoryCrudController;
1821
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\ProjectDomain\Developer;
1922
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\ProjectDomain\Project;
23+
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\ProjectDomain\ProjectReleaseCategory;
2024
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\ProjectDomain\ProjectTag;
2125
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
2226
use Symfony\Component\HttpFoundation\RequestStack;
@@ -47,42 +51,43 @@ protected function getEntityDto(): EntityDto
4751
return $this->projectDto;
4852
}
4953

50-
/**
51-
* @dataProvider toOneAssociation
52-
*/
53-
public function testToOneAssociation(FieldInterface $field): void
54+
public function testToOneAssociation(): void
5455
{
56+
$field = AssociationField::new('leadDeveloper');
57+
$entityDto = new EntityDto(Project::class, $this->createStub(ClassMetadata::class));
58+
$entityDto->setFields(FieldCollection::new([$field]));
59+
5560
$field->getAsDto()->setDoctrineMetadata((array) $this->projectDto->getClassMetadata()->getAssociationMapping($field->getAsDto()->getProperty()));
5661
$field->setCustomOption(AssociationField::OPTION_EMBEDDED_CRUD_FORM_CONTROLLER, DeveloperCrudController::class);
5762

58-
$field = $this->configure($field, controllerFqcn: ProjectCrudController::class);
59-
$this->assertSame('toOne', $field->getCustomOption(AssociationField::OPTION_DOCTRINE_ASSOCIATION_TYPE));
60-
$this->assertSame(EntityType::class, $field->getFormType());
61-
$this->assertSame(Developer::class, $field->getFormTypeOption('class'));
62-
}
63-
64-
public static function toOneAssociation(): \Generator
65-
{
66-
yield [AssociationField::new('leadDeveloper')];
63+
$fieldDto = $this->configure($field, controllerFqcn: ProjectCrudController::class);
64+
$this->assertSame('toOne', $fieldDto->getCustomOption(AssociationField::OPTION_DOCTRINE_ASSOCIATION_TYPE));
65+
$this->assertSame(EntityType::class, $fieldDto->getFormType());
66+
$this->assertSame(Developer::class, $fieldDto->getFormTypeOption('class'));
6767
}
6868

69-
/**
70-
* @dataProvider toManyAssociation
71-
*/
72-
public function testToManyAssociation(FieldInterface $field): void
69+
public function testToManyAssociation(): void
7370
{
71+
$field = AssociationField::new('projectTags');
7472
$field->getAsDto()->setDoctrineMetadata((array) $this->projectDto->getClassMetadata()->getAssociationMapping($field->getAsDto()->getProperty()));
7573
$field->setCustomOption(AssociationField::OPTION_EMBEDDED_CRUD_FORM_CONTROLLER, DeveloperCrudController::class);
7674

77-
$field = $this->configure($field, controllerFqcn: ProjectCrudController::class);
78-
$this->assertSame('toMany', $field->getCustomOption(AssociationField::OPTION_DOCTRINE_ASSOCIATION_TYPE));
79-
$this->assertSame(EntityType::class, $field->getFormType());
80-
$this->assertSame(ProjectTag::class, $field->getFormTypeOption('class'));
75+
$fieldDto = $this->configure($field, controllerFqcn: ProjectCrudController::class);
76+
$this->assertSame('toMany', $fieldDto->getCustomOption(AssociationField::OPTION_DOCTRINE_ASSOCIATION_TYPE));
77+
$this->assertSame(EntityType::class, $fieldDto->getFormType());
78+
$this->assertSame(ProjectTag::class, $fieldDto->getFormTypeOption('class'));
8179
}
8280

83-
public static function toManyAssociation(): \Generator
81+
public function testNestedAssociationWithCrudControllerSet(): void
8482
{
85-
yield [AssociationField::new('projectTags')];
83+
$field = AssociationField::new('latestRelease.category')
84+
->setCrudController(ProjectReleaseCategoryCrudController::class)
85+
;
86+
87+
$fieldDto = $this->configure($field);
88+
89+
$this->assertSame(EntityType::class, $fieldDto->getFormType());
90+
$this->assertSame(ProjectReleaseCategory::class, $fieldDto->getFormTypeOption('class'));
8691
}
8792

8893
/**
@@ -103,7 +108,7 @@ public static function failsIfPropertyIsNotAssociation(): \Generator
103108
{
104109
yield [TextField::new('name')];
105110
yield [TextField::new('price')];
106-
yield [TextField::new('price.currency')];
111+
yield [TextField::new('price.currency')]; // Doctrine embeddable
107112
}
108113

109114
/**
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Controller\ProjectDomain;
4+
5+
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
6+
use EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\ProjectDomain\ProjectReleaseCategory;
7+
8+
/**
9+
* @extends AbstractCrudController<ProjectReleaseCategory>
10+
*/
11+
class ProjectReleaseCategoryCrudController extends AbstractCrudController
12+
{
13+
public static function getEntityFqcn(): string
14+
{
15+
return ProjectReleaseCategory::class;
16+
}
17+
}

tests/TestApplication/src/Entity/ProjectDomain/Project.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ class Project implements \Stringable
3030
/**
3131
* @var Collection<int, ProjectIssue>
3232
*/
33-
#[ORM\OneToMany(targetEntity: ProjectIssue::class, mappedBy: 'project', cascade: ['persist', 'remove'], orphanRemoval: true)]
33+
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectIssue::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
3434
private Collection $projectIssues;
3535

3636
/**
3737
* @var Collection<int, Developer>
3838
*/
39-
#[ORM\OneToMany(targetEntity: Developer::class, mappedBy: 'favouriteProject')]
39+
#[ORM\OneToMany(mappedBy: 'favouriteProject', targetEntity: Developer::class)]
4040
private Collection $favouriteProjectOf;
4141

4242
/**

tests/TestApplication/src/Entity/ProjectDomain/ProjectRelease.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ class ProjectRelease implements \Stringable
1515
#[ORM\Column(length: 255)]
1616
private ?string $name = null;
1717

18+
#[ORM\OneToOne]
19+
private ?ProjectReleaseCategory $category = null;
20+
1821
public function __toString(): string
1922
{
2023
return $this->name;
@@ -36,4 +39,16 @@ public function setName(string $name): static
3639

3740
return $this;
3841
}
42+
43+
public function getCategory(): ?ProjectReleaseCategory
44+
{
45+
return $this->category;
46+
}
47+
48+
public function setCategory(?ProjectReleaseCategory $category): static
49+
{
50+
$this->category = $category;
51+
52+
return $this;
53+
}
3954
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace EasyCorp\Bundle\EasyAdminBundle\Tests\TestApplication\Entity\ProjectDomain;
4+
5+
use Doctrine\ORM\Mapping as ORM;
6+
7+
#[ORM\Entity]
8+
class ProjectReleaseCategory implements \Stringable
9+
{
10+
#[ORM\Id]
11+
#[ORM\GeneratedValue]
12+
#[ORM\Column]
13+
private ?int $id = null;
14+
15+
#[ORM\Column(length: 255)]
16+
private ?string $name = null;
17+
18+
public function __toString(): string
19+
{
20+
return $this->name;
21+
}
22+
23+
public function getId(): ?int
24+
{
25+
return $this->id;
26+
}
27+
28+
public function getName(): ?string
29+
{
30+
return $this->name;
31+
}
32+
33+
public function setName(string $name): static
34+
{
35+
$this->name = $name;
36+
37+
return $this;
38+
}
39+
}

0 commit comments

Comments
 (0)