diff --git a/src/LiveComponent/src/Hydration/DoctrineEntityHydrationExtension.php b/src/LiveComponent/src/Hydration/DoctrineEntityHydrationExtension.php index 53f6b0be7a9..583717bfc0d 100644 --- a/src/LiveComponent/src/Hydration/DoctrineEntityHydrationExtension.php +++ b/src/LiveComponent/src/Hydration/DoctrineEntityHydrationExtension.php @@ -13,6 +13,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\Mapping\MappingException; use Doctrine\Persistence\ObjectManager; /** @@ -61,8 +62,7 @@ public function dehydrate(object $object): mixed $id = $this ->objectManagerFor($class = $object::class) ->getClassMetadata($class) - ->getIdentifierValues($object) - ; + ->getIdentifierValues($object); // Dehydrate ID values in case they are other entities $id = array_map(fn ($id) => \is_object($id) && $this->supports($id::class) ? $this->dehydrate($id) : $id, $id); @@ -81,14 +81,44 @@ public function dehydrate(object $object): mixed private function objectManagerFor(string $class): ?ObjectManager { - if (!class_exists($class)) { + if (class_exists($class)) { + // Keep the standard way for a class + // todo cache/warmup an array of classes that are "doctrine objects" + foreach ($this->managerRegistries as $registry) { + if ($om = $registry->getManagerForClass($class)) { + return self::ensureManagedObject($om, $class); + } + } + return null; } - // todo cache/warmup an array of classes that are "doctrine objects" - foreach ($this->managerRegistries as $registry) { - if ($om = $registry->getManagerForClass($class)) { - return self::ensureManagedObject($om, $class); + if (interface_exists($class)) { + // special handler for interfaces + // For use cases : @see https://symfony.com/doc/current/doctrine/resolve_target_entity.html + // As today, getManagerForClass don't resolve nicely aliased interfaces + // The workaround is to enum over each object manager and trying to get metadata + // The metadata are indeed, resolved nicely + // Fore more details : + // @see \Doctrine\ORM\Tools\ResolveTargetEntityListener + + // todo cache/warmup an array of interfaces that are resolved "doctrine objects" + foreach ($this->managerRegistries as $registry) { + foreach ($registry->getManagers() as $om) { + // the getClassMetaData can indeed throw an exception + // so, we + try { + if (null !== $om->getClassMetadata($class)) { + return self::ensureManagedObject($om, $class); + } + } catch (MappingException $e) { + // I did not find a nice way to check if it is because the class is really unknown + // It is good to check for a specific exception ? + // eg: \Doctrine\Persistence\Mapping\MappingException + // @see \Doctrine\Persistence\Mapping\AbstractClassMetadataFactory::getMetadataFor + // throw $e; + } + } } } diff --git a/src/LiveComponent/tests/Fixtures/Dto/Aliased.php b/src/LiveComponent/tests/Fixtures/Dto/Aliased.php new file mode 100644 index 00000000000..fa88ed42002 --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Dto/Aliased.php @@ -0,0 +1,9 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Tests\Fixtures\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Doctrine\ORM\Mapping\Column; + +#[ORM\Entity] +class AliasedEntity implements AliasedEntityInterface +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + public $id; + + #[Column(type: 'string')] + public ?string $name = null; +} diff --git a/src/LiveComponent/tests/Fixtures/Entity/AliasedEntityInterface.php b/src/LiveComponent/tests/Fixtures/Entity/AliasedEntityInterface.php new file mode 100644 index 00000000000..add24a9366e --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Entity/AliasedEntityInterface.php @@ -0,0 +1,7 @@ + 'XML', ], ], + 'resolve_target_entities' => [ + AliasedEntityInterface::class => AliasedEntity::class], ], ]; diff --git a/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php b/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php index 1b9de696f8b..1bdb55436f7 100644 --- a/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php +++ b/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php @@ -11,8 +11,11 @@ namespace Symfony\UX\LiveComponent\Tests\Integration\Hydration; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\UX\LiveComponent\Hydration\DoctrineEntityHydrationExtension; +use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\AliasedEntity; +use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\AliasedEntityInterface; use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\CompositeIdEntity; use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\ForeignKeyIdEntity; use Symfony\UX\LiveComponent\Tests\Fixtures\Factory\CompositeIdEntityFactory; @@ -50,4 +53,34 @@ public function testForeignKeyId(): void self::assertSame($foreignKeyIdEntity->id->id, $dehydrated); self::assertSame($foreignKeyIdEntity, $extension->hydrate($dehydrated, ForeignKeyIdEntity::class)); } + + public function testSupportInterface(): void + { + /** @var DoctrineEntityHydrationExtension $extension */ + $extension = self::getContainer()->get('ux.live_component.doctrine_entity_hydration_extension'); + + self::assertTrue($extension->supports(AliasedEntityInterface::class), 'AliasedEntityInterface should be supported'); + self::assertTrue($extension->supports(AliasedEntity::class), 'AliasedEntity should be supported'); + self::assertFalse($extension->supports('UnknownClass'), 'UnknownClass should not be supported'); + } + + public function testHydrationFromInterface(): void + { + /** @var DoctrineEntityHydrationExtension $extension */ + $extension = self::getContainer()->get('ux.live_component.doctrine_entity_hydration_extension'); + $em = self::getContainer()->get(EntityManagerInterface::class); + + $existingEntity = new AliasedEntity(); + $existingEntity->name = 'foo'; + + $em->persist($existingEntity); + $em->flush(); + + $dehydratedData = $extension->dehydrate($existingEntity); + + $entityFromDehydratation = $extension->hydrate($dehydratedData, AliasedEntityInterface::class); + + self::assertSame($existingEntity, $entityFromDehydratation, 'instance should be the same'); + self::assertNull($extension->hydrate(null, AliasedEntityInterface::class), 'should return null if null is passed'); + } }