Skip to content

Commit 3162f9f

Browse files
bug #746 [Live] Complex hydration fixes for 2.8 (weaverryan)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [Live] Complex hydration fixes for 2.8 | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Tickets | Fix #740 | License | MIT In 2.8, we have the ability to (de)hydrate non-persisted entity objects. That makes user's life a lot simpler, but when you do this, there are a lot of edge cases. This smashes a few more of those: Cheers! Commits ------- 6796a536 [Live] Complex hydration fixes for 2.8
2 parents 49b0ad4 + 5556d56 commit 3162f9f

File tree

13 files changed

+547
-91
lines changed

13 files changed

+547
-91
lines changed

src/LiveComponent/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"doctrine/annotations": "^1.0",
3636
"doctrine/doctrine-bundle": "^2.0",
3737
"doctrine/orm": "^2.7",
38+
"phpdocumentor/reflection-docblock": "5.x-dev",
3839
"symfony/dependency-injection": "^5.4|^6.0",
3940
"symfony/form": "^5.4|^6.0",
4041
"symfony/framework-bundle": "^5.4|^6.0",

src/LiveComponent/doc/index.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,11 @@ the ``Context`` attribute from Symfony's serializer::
541541
If your property has writable paths, those will be normalized/denormalized
542542
using the same `Context` set on the property itself.
543543

544-
Or, you can take full control over the (de)hydration process by setting the ``hydrateWith``
545-
and ``dehydrateWith`` options on ``LiveProp``. For example::
544+
Using the serializer isn't meant to work out-of-the-box in every possible situation
545+
and it's always simpler to use scalar `LiveProp` values instead of complex objects.
546+
If you're having (de)hydrating a complex object, you can take full control by
547+
setting the ``hydrateWith`` and ``dehydrateWith`` options on ``LiveProp``. For
548+
example::
546549

547550
class ComponentWithAddressDto
548551
{

src/LiveComponent/src/LiveComponentHydrator.php

Lines changed: 248 additions & 44 deletions
Large diffs are not rendered by default.

src/LiveComponent/src/Metadata/LiveComponentMetadataFactory.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ public static function createPropMetadatas(\ReflectionClass $class): array
6161
$property->getName(),
6262
$attribute->newInstance(),
6363
$type ? $type->getName() : null,
64-
$type ? $type->allowsNull() : false,
6564
$type ? $type->isBuiltin() : false,
6665
);
6766
}

src/LiveComponent/src/Metadata/LivePropMetadata.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ public function __construct(
2626
private string $name,
2727
private LiveProp $liveProp,
2828
private ?string $typeName,
29-
private bool $allowsNull,
3029
private bool $isBuiltIn,
3130
) {
3231
}
@@ -41,11 +40,6 @@ public function getType(): ?string
4140
return $this->typeName;
4241
}
4342

44-
public function allowsNull(): bool
45-
{
46-
return $this->allowsNull;
47-
}
48-
4943
public function isBuiltIn(): bool
5044
{
5145
return $this->isBuiltIn;

src/LiveComponent/src/Normalizer/DoctrineObjectNormalizer.php

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Doctrine\Persistence\ManagerRegistry;
1616
use Doctrine\Persistence\ObjectManager;
1717
use Psr\Container\ContainerInterface;
18-
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
1918
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
2019
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2120
use Symfony\Contracts\Service\ServiceSubscriberInterface;
@@ -30,9 +29,6 @@
3029
*/
3130
final class DoctrineObjectNormalizer implements NormalizerInterface, DenormalizerInterface, ServiceSubscriberInterface
3231
{
33-
/** Flag to avoid recursion in the normalizer */
34-
private const DOCTRINE_OBJECT_ALREADY_NORMALIZED = 'doctrine_object_normalizer.normalized';
35-
3632
/**
3733
* @param ManagerRegistry[] $managerRegistries
3834
*/
@@ -89,45 +85,24 @@ public function supportsNormalization(mixed $data, string $format = null, array
8985

9086
public function denormalize(mixed $data, string $type, string $format = null, array $context = []): ?object
9187
{
92-
if (null === $data) {
93-
return null;
94-
}
95-
9688
// $data is the single identifier or array of identifiers
9789
if (\is_scalar($data) || (\is_array($data) && isset($data[0]))) {
9890
return $this->objectManagerFor($type)->find($type, $data);
9991
}
10092

101-
// $data is an associative array to denormalize the entity
102-
// allow the object to be denormalized using the default denormalizer
103-
// except that the denormalizer has problems with "nullable: false" columns
104-
// https://github.com/symfony/symfony/issues/49149
105-
// so, we send the object through the denormalizer, but turn type-checks off
106-
// NOTE: The hydration system will already have prevented a writable property
107-
// from reaching this.
108-
$context[AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT] = true;
109-
$context[self::DOCTRINE_OBJECT_ALREADY_NORMALIZED] = true;
110-
111-
return $this->getDenormalizer()->denormalize($data, $type, $format, $context);
93+
throw new \LogicException('Invalid denormalization case');
11294
}
11395

11496
public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = [])
11597
{
116-
if (true !== ($context[LiveComponentHydrator::LIVE_CONTEXT] ?? null) || !class_exists($type)) {
117-
return false;
118-
}
119-
120-
// not an entity?
121-
if (null === $this->objectManagerFor($type)) {
122-
return false;
123-
}
124-
125-
// avoid recursion
126-
if ($context[self::DOCTRINE_OBJECT_ALREADY_NORMALIZED] ?? false) {
127-
return false;
98+
if (
99+
(\is_scalar($data) || (\is_array($data) && isset($data[0])))
100+
&& null !== $this->objectManagerFor($type)
101+
) {
102+
return true;
128103
}
129104

130-
return true;
105+
return false;
131106
}
132107

133108
public static function getSubscribedServices(): array
@@ -139,6 +114,10 @@ public static function getSubscribedServices(): array
139114

140115
private function objectManagerFor(string $class): ?ObjectManager
141116
{
117+
if (!class_exists($class)) {
118+
return null;
119+
}
120+
142121
// todo cache/warmup an array of classes that are "doctrine objects"
143122
foreach ($this->managerRegistries as $registry) {
144123
if ($om = $registry->getManagerForClass($class)) {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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\Common\Collections\ArrayCollection;
15+
use Doctrine\Common\Collections\Collection;
16+
use Doctrine\ORM\Mapping as ORM;
17+
18+
/**
19+
* @ORM\Entity
20+
*/
21+
class TodoItemFixtureEntity
22+
{
23+
/**
24+
* @ORM\Id
25+
* @ORM\GeneratedValue
26+
* @ORM\Column(type="integer")
27+
*/
28+
public $id;
29+
30+
/**
31+
* @ORM\Column(type="string")
32+
*/
33+
private ?string $name = null;
34+
35+
/**
36+
* @ORM\ManyToOne(targetEntity=TodoListFixtureEntity::class, inversedBy="todoItems")
37+
*/
38+
private TodoListFixtureEntity $todoList;
39+
40+
/**
41+
* @param string $name
42+
*/
43+
public function __construct(string $name = null)
44+
{
45+
$this->name = $name;
46+
}
47+
48+
public function getTodoList(): TodoListFixtureEntity
49+
{
50+
return $this->todoList;
51+
}
52+
53+
public function setTodoList(?TodoListFixtureEntity $todoList): self
54+
{
55+
$this->todoList = $todoList;
56+
57+
return $this;
58+
}
59+
60+
public function getName(): ?string
61+
{
62+
return $this->name;
63+
}
64+
65+
public function setName(?string $name)
66+
{
67+
$this->name = $name;
68+
}
69+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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\Common\Collections\ArrayCollection;
15+
use Doctrine\Common\Collections\Collection;
16+
use Doctrine\ORM\Mapping as ORM;
17+
18+
/**
19+
* @ORM\Entity
20+
*/
21+
class TodoListFixtureEntity
22+
{
23+
/**
24+
* @ORM\Id
25+
* @ORM\GeneratedValue
26+
* @ORM\Column(type="integer")
27+
*/
28+
public $id;
29+
30+
/**
31+
* @ORM\Column(type="string")
32+
*/
33+
public string $listTitle = '';
34+
35+
/**
36+
* @ORM\OneToMany(targetEntity=TodoItemFixtureEntity::class, mappedBy="todoList")
37+
*/
38+
private Collection $todoItems;
39+
40+
public function __construct(string $listTitle = '')
41+
{
42+
$this->listTitle = $listTitle;
43+
$this->todoItems = new ArrayCollection();
44+
}
45+
46+
public function getTodoItems(): Collection
47+
{
48+
return $this->todoItems;
49+
}
50+
51+
public function addTodoItem(TodoItemFixtureEntity $todoItem): self
52+
{
53+
if (!$this->todoItems->contains($todoItem)) {
54+
$this->todoItems[] = $todoItem;
55+
$todoItem->setTodoList($this);
56+
}
57+
58+
return $this;
59+
}
60+
61+
public function removeTodoItem(TodoItemFixtureEntity $todoItem): self
62+
{
63+
if ($this->todoItems->removeElement($todoItem)) {
64+
// set the owning side to null (unless already changed)
65+
if ($todoItem->getTodoList() === $this) {
66+
$todoItem->setTodoList(null);
67+
}
68+
}
69+
70+
return $this;
71+
}
72+
}

0 commit comments

Comments
 (0)