Skip to content

Commit d0bb978

Browse files
committed
bug #1957 [LiveComponent] Fix (de)hydration of composite and/or foreign ID entities (MatTheCat)
This PR was merged into the 2.x branch. Discussion ---------- [LiveComponent] Fix (de)hydration of composite and/or foreign ID entities | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Issues | N/A | License | MIT This PR fixes two issues (tests included) with the `DoctrineEntityHydrationExtension`: - Entities with composite IDs couldn’t be hydrated because in this case the object manager needs the properties as keys. Throwing if the `0` key does not exist prevents this to work, so this PR removes this check. - Dehydrated entities whose ID is a foreign key would bear the corresponding entity (or proxy), preventing their future correct hydration. This PR dehydrates ID values as well if needed. Commits ------- f1c4e0d [LiveComponent] Fix (de)hydration of composite and/or foreign ID entities
2 parents f9bbaf5 + f1c4e0d commit d0bb978

File tree

6 files changed

+216
-2
lines changed

6 files changed

+216
-2
lines changed

src/LiveComponent/src/Hydration/DoctrineEntityHydrationExtension.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ public function hydrate(mixed $value, string $className): ?object
4848
return null;
4949
}
5050

51-
// $data is the single identifier or array of identifiers
52-
if (\is_scalar($value) || (\is_array($value) && isset($value[0]))) {
51+
// $data is a single identifier or array of identifiers
52+
if (\is_scalar($value) || \is_array($value)) {
5353
return $this->objectManagerFor($className)->find($className, $value);
5454
}
5555

@@ -64,6 +64,9 @@ public function dehydrate(object $object): mixed
6464
->getIdentifierValues($object)
6565
;
6666

67+
// Dehydrate ID values in case they are other entities
68+
$id = array_map(fn ($id) => \is_object($id) && $this->supports($id::class) ? $this->dehydrate($id) : $id, $id);
69+
6770
switch (\count($id)) {
6871
case 0:
6972
// a non-persisted entity
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\UX\LiveComponent\Tests\Fixtures\Entity;
13+
14+
use Doctrine\ORM\Mapping as ORM;
15+
16+
#[ORM\Entity]
17+
class CompositeIdEntity
18+
{
19+
public function __construct(
20+
#[ORM\Id]
21+
#[ORM\Column(type: 'integer')]
22+
private int $firstIdPart,
23+
24+
#[ORM\Id]
25+
#[ORM\Column(type: 'integer')]
26+
private int $secondIdPart,
27+
) {
28+
}
29+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\UX\LiveComponent\Tests\Fixtures\Entity;
13+
14+
use Doctrine\ORM\Mapping as ORM;
15+
16+
#[ORM\Entity]
17+
class ForeignKeyIdEntity
18+
{
19+
public function __construct(
20+
#[ORM\Id]
21+
#[ORM\ManyToOne(cascade: ['persist'])]
22+
public Entity1 $id,
23+
) {
24+
}
25+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\UX\LiveComponent\Tests\Fixtures\Factory;
13+
14+
use Doctrine\ORM\EntityRepository;
15+
use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\CompositeIdEntity;
16+
use Zenstruck\Foundry\ModelFactory;
17+
use Zenstruck\Foundry\Proxy;
18+
use Zenstruck\Foundry\RepositoryProxy;
19+
20+
/**
21+
* @extends ModelFactory<CompositeIdEntity>
22+
*
23+
* @method static CompositeIdEntity|Proxy createOne(array $attributes = [])
24+
* @method static CompositeIdEntity[]|Proxy[] createMany(int $number, array|callable $attributes = [])
25+
* @method static CompositeIdEntity|Proxy find(object|array|mixed $criteria)
26+
* @method static CompositeIdEntity|Proxy findOrCreate(array $attributes)
27+
* @method static CompositeIdEntity|Proxy first(string $sortedField = 'id')
28+
* @method static CompositeIdEntity|Proxy last(string $sortedField = 'id')
29+
* @method static CompositeIdEntity|Proxy random(array $attributes = [])
30+
* @method static CompositeIdEntity|Proxy randomOrCreate(array $attributes = []))
31+
* @method static CompositeIdEntity[]|Proxy[] all()
32+
* @method static CompositeIdEntity[]|Proxy[] findBy(array $attributes)
33+
* @method static CompositeIdEntity[]|Proxy[] randomSet(int $number, array $attributes = []))
34+
* @method static CompositeIdEntity[]|Proxy[] randomRange(int $min, int $max, array $attributes = []))
35+
* @method static EntityRepository|RepositoryProxy repository()
36+
* @method CompositeIdEntity|Proxy create(array|callable $attributes = [])
37+
*/
38+
class CompositeIdEntityFactory extends ModelFactory
39+
{
40+
protected static function getClass(): string
41+
{
42+
return CompositeIdEntity::class;
43+
}
44+
45+
protected function getDefaults(): array
46+
{
47+
return [
48+
'firstIdPart' => rand(1, \PHP_INT_MAX),
49+
'secondIdPart' => rand(1, \PHP_INT_MAX),
50+
];
51+
}
52+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\UX\LiveComponent\Tests\Fixtures\Factory;
13+
14+
use Doctrine\ORM\EntityRepository;
15+
use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\Entity1;
16+
use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\ForeignKeyIdEntity;
17+
use Zenstruck\Foundry\ModelFactory;
18+
use Zenstruck\Foundry\Proxy;
19+
use Zenstruck\Foundry\RepositoryProxy;
20+
21+
use function Zenstruck\Foundry\lazy;
22+
23+
/**
24+
* @extends ModelFactory<ForeignKeyIdEntity>
25+
*
26+
* @method static ForeignKeyIdEntity|Proxy createOne(array $attributes = [])
27+
* @method static ForeignKeyIdEntity[]|Proxy[] createMany(int $number, array|callable $attributes = [])
28+
* @method static ForeignKeyIdEntity|Proxy find(object|array|mixed $criteria)
29+
* @method static ForeignKeyIdEntity|Proxy findOrCreate(array $attributes)
30+
* @method static ForeignKeyIdEntity|Proxy first(string $sortedField = 'id')
31+
* @method static ForeignKeyIdEntity|Proxy last(string $sortedField = 'id')
32+
* @method static ForeignKeyIdEntity|Proxy random(array $attributes = [])
33+
* @method static ForeignKeyIdEntity|Proxy randomOrCreate(array $attributes = []))
34+
* @method static ForeignKeyIdEntity[]|Proxy[] all()
35+
* @method static ForeignKeyIdEntity[]|Proxy[] findBy(array $attributes)
36+
* @method static ForeignKeyIdEntity[]|Proxy[] randomSet(int $number, array $attributes = []))
37+
* @method static ForeignKeyIdEntity[]|Proxy[] randomRange(int $min, int $max, array $attributes = []))
38+
* @method static EntityRepository|RepositoryProxy repository()
39+
* @method ForeignKeyIdEntity|Proxy create(array|callable $attributes = [])
40+
*/
41+
class ForeignKeyIdEntityFactory extends ModelFactory
42+
{
43+
protected static function getClass(): string
44+
{
45+
return ForeignKeyIdEntity::class;
46+
}
47+
48+
protected function getDefaults(): array
49+
{
50+
return ['id' => lazy(static fn () => new Entity1())];
51+
}
52+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\UX\LiveComponent\Tests\Unit\Hydration;
13+
14+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
15+
use Symfony\UX\LiveComponent\Hydration\DoctrineEntityHydrationExtension;
16+
use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\CompositeIdEntity;
17+
use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\ForeignKeyIdEntity;
18+
use Symfony\UX\LiveComponent\Tests\Fixtures\Factory\CompositeIdEntityFactory;
19+
use Symfony\UX\LiveComponent\Tests\Fixtures\Factory\ForeignKeyIdEntityFactory;
20+
use Zenstruck\Foundry\Test\Factories;
21+
use Zenstruck\Foundry\Test\ResetDatabase;
22+
23+
class DoctrineEntityHydrationExtensionTest extends KernelTestCase
24+
{
25+
use Factories;
26+
use ResetDatabase;
27+
28+
public function testCompositeId(): void
29+
{
30+
$compositeIdEntity = CompositeIdEntityFactory::createOne()->save()->object();
31+
32+
/** @var DoctrineEntityHydrationExtension $extension */
33+
$extension = self::getContainer()->get('ux.live_component.doctrine_entity_hydration_extension');
34+
35+
self::assertSame(
36+
$compositeIdEntity,
37+
$extension->hydrate($extension->dehydrate($compositeIdEntity), CompositeIdEntity::class)
38+
);
39+
}
40+
41+
public function testForeignKeyId(): void
42+
{
43+
$foreignKeyIdEntity = ForeignKeyIdEntityFactory::createOne()->save()->object();
44+
45+
/** @var DoctrineEntityHydrationExtension $extension */
46+
$extension = self::getContainer()->get('ux.live_component.doctrine_entity_hydration_extension');
47+
48+
$dehydrated = $extension->dehydrate($foreignKeyIdEntity);
49+
50+
self::assertSame($foreignKeyIdEntity->id->id, $dehydrated);
51+
self::assertSame($foreignKeyIdEntity, $extension->hydrate($dehydrated, ForeignKeyIdEntity::class));
52+
}
53+
}

0 commit comments

Comments
 (0)