diff --git a/src/LiveComponent/src/Hydration/DoctrineEntityHydrationExtension.php b/src/LiveComponent/src/Hydration/DoctrineEntityHydrationExtension.php index 43b6400d97c..53f6b0be7a9 100644 --- a/src/LiveComponent/src/Hydration/DoctrineEntityHydrationExtension.php +++ b/src/LiveComponent/src/Hydration/DoctrineEntityHydrationExtension.php @@ -48,8 +48,8 @@ public function hydrate(mixed $value, string $className): ?object return null; } - // $data is the single identifier or array of identifiers - if (\is_scalar($value) || (\is_array($value) && isset($value[0]))) { + // $data is a single identifier or array of identifiers + if (\is_scalar($value) || \is_array($value)) { return $this->objectManagerFor($className)->find($className, $value); } @@ -64,6 +64,9 @@ public function dehydrate(object $object): mixed ->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); + switch (\count($id)) { case 0: // a non-persisted entity diff --git a/src/LiveComponent/tests/Fixtures/Entity/CompositeIdEntity.php b/src/LiveComponent/tests/Fixtures/Entity/CompositeIdEntity.php new file mode 100644 index 00000000000..5ac7a9d460a --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Entity/CompositeIdEntity.php @@ -0,0 +1,29 @@ + + * + * 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; + +#[ORM\Entity] +class CompositeIdEntity +{ + public function __construct( + #[ORM\Id] + #[ORM\Column(type: 'integer')] + private int $firstIdPart, + + #[ORM\Id] + #[ORM\Column(type: 'integer')] + private int $secondIdPart, + ) { + } +} diff --git a/src/LiveComponent/tests/Fixtures/Entity/ForeignKeyIdEntity.php b/src/LiveComponent/tests/Fixtures/Entity/ForeignKeyIdEntity.php new file mode 100644 index 00000000000..85aeb0a02b4 --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Entity/ForeignKeyIdEntity.php @@ -0,0 +1,25 @@ + + * + * 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; + +#[ORM\Entity] +class ForeignKeyIdEntity +{ + public function __construct( + #[ORM\Id] + #[ORM\ManyToOne(cascade: ['persist'])] + public Entity1 $id, + ) { + } +} diff --git a/src/LiveComponent/tests/Fixtures/Factory/CompositeIdEntityFactory.php b/src/LiveComponent/tests/Fixtures/Factory/CompositeIdEntityFactory.php new file mode 100644 index 00000000000..7cdf882da60 --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Factory/CompositeIdEntityFactory.php @@ -0,0 +1,52 @@ + + * + * 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\Factory; + +use Doctrine\ORM\EntityRepository; +use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\CompositeIdEntity; +use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\RepositoryProxy; + +/** + * @extends ModelFactory + * + * @method static CompositeIdEntity|Proxy createOne(array $attributes = []) + * @method static CompositeIdEntity[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static CompositeIdEntity|Proxy find(object|array|mixed $criteria) + * @method static CompositeIdEntity|Proxy findOrCreate(array $attributes) + * @method static CompositeIdEntity|Proxy first(string $sortedField = 'id') + * @method static CompositeIdEntity|Proxy last(string $sortedField = 'id') + * @method static CompositeIdEntity|Proxy random(array $attributes = []) + * @method static CompositeIdEntity|Proxy randomOrCreate(array $attributes = [])) + * @method static CompositeIdEntity[]|Proxy[] all() + * @method static CompositeIdEntity[]|Proxy[] findBy(array $attributes) + * @method static CompositeIdEntity[]|Proxy[] randomSet(int $number, array $attributes = [])) + * @method static CompositeIdEntity[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])) + * @method static EntityRepository|RepositoryProxy repository() + * @method CompositeIdEntity|Proxy create(array|callable $attributes = []) + */ +class CompositeIdEntityFactory extends ModelFactory +{ + protected static function getClass(): string + { + return CompositeIdEntity::class; + } + + protected function getDefaults(): array + { + return [ + 'firstIdPart' => rand(1, \PHP_INT_MAX), + 'secondIdPart' => rand(1, \PHP_INT_MAX), + ]; + } +} diff --git a/src/LiveComponent/tests/Fixtures/Factory/ForeignKeyIdEntityFactory.php b/src/LiveComponent/tests/Fixtures/Factory/ForeignKeyIdEntityFactory.php new file mode 100644 index 00000000000..ba5f9ac537d --- /dev/null +++ b/src/LiveComponent/tests/Fixtures/Factory/ForeignKeyIdEntityFactory.php @@ -0,0 +1,52 @@ + + * + * 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\Factory; + +use Doctrine\ORM\EntityRepository; +use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\Entity1; +use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\ForeignKeyIdEntity; +use Zenstruck\Foundry\ModelFactory; +use Zenstruck\Foundry\Proxy; +use Zenstruck\Foundry\RepositoryProxy; + +use function Zenstruck\Foundry\lazy; + +/** + * @extends ModelFactory + * + * @method static ForeignKeyIdEntity|Proxy createOne(array $attributes = []) + * @method static ForeignKeyIdEntity[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static ForeignKeyIdEntity|Proxy find(object|array|mixed $criteria) + * @method static ForeignKeyIdEntity|Proxy findOrCreate(array $attributes) + * @method static ForeignKeyIdEntity|Proxy first(string $sortedField = 'id') + * @method static ForeignKeyIdEntity|Proxy last(string $sortedField = 'id') + * @method static ForeignKeyIdEntity|Proxy random(array $attributes = []) + * @method static ForeignKeyIdEntity|Proxy randomOrCreate(array $attributes = [])) + * @method static ForeignKeyIdEntity[]|Proxy[] all() + * @method static ForeignKeyIdEntity[]|Proxy[] findBy(array $attributes) + * @method static ForeignKeyIdEntity[]|Proxy[] randomSet(int $number, array $attributes = [])) + * @method static ForeignKeyIdEntity[]|Proxy[] randomRange(int $min, int $max, array $attributes = [])) + * @method static EntityRepository|RepositoryProxy repository() + * @method ForeignKeyIdEntity|Proxy create(array|callable $attributes = []) + */ +class ForeignKeyIdEntityFactory extends ModelFactory +{ + protected static function getClass(): string + { + return ForeignKeyIdEntity::class; + } + + protected function getDefaults(): array + { + return ['id' => lazy(static fn () => new Entity1())]; + } +} diff --git a/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php b/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php new file mode 100644 index 00000000000..ff7b60ca04e --- /dev/null +++ b/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\LiveComponent\Tests\Unit\Hydration; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\UX\LiveComponent\Hydration\DoctrineEntityHydrationExtension; +use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\CompositeIdEntity; +use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\ForeignKeyIdEntity; +use Symfony\UX\LiveComponent\Tests\Fixtures\Factory\CompositeIdEntityFactory; +use Symfony\UX\LiveComponent\Tests\Fixtures\Factory\ForeignKeyIdEntityFactory; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +class DoctrineEntityHydrationExtensionTest extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + public function testCompositeId(): void + { + $compositeIdEntity = CompositeIdEntityFactory::createOne()->save()->object(); + + /** @var DoctrineEntityHydrationExtension $extension */ + $extension = self::getContainer()->get('ux.live_component.doctrine_entity_hydration_extension'); + + self::assertSame( + $compositeIdEntity, + $extension->hydrate($extension->dehydrate($compositeIdEntity), CompositeIdEntity::class) + ); + } + + public function testForeignKeyId(): void + { + $foreignKeyIdEntity = ForeignKeyIdEntityFactory::createOne()->save()->object(); + + /** @var DoctrineEntityHydrationExtension $extension */ + $extension = self::getContainer()->get('ux.live_component.doctrine_entity_hydration_extension'); + + $dehydrated = $extension->dehydrate($foreignKeyIdEntity); + + self::assertSame($foreignKeyIdEntity->id->id, $dehydrated); + self::assertSame($foreignKeyIdEntity, $extension->hydrate($dehydrated, ForeignKeyIdEntity::class)); + } +}