Skip to content

[Live] Complex hydration fixes for 2.8 #746

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/LiveComponent/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"doctrine/annotations": "^1.0",
"doctrine/doctrine-bundle": "^2.0",
"doctrine/orm": "^2.7",
"phpdocumentor/reflection-docblock": "5.x-dev",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/form": "^5.4|^6.0",
"symfony/framework-bundle": "^5.4|^6.0",
Expand Down
7 changes: 5 additions & 2 deletions src/LiveComponent/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,11 @@ the ``Context`` attribute from Symfony's serializer::
If your property has writable paths, those will be normalized/denormalized
using the same `Context` set on the property itself.

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

class ComponentWithAddressDto
{
Expand Down
292 changes: 248 additions & 44 deletions src/LiveComponent/src/LiveComponentHydrator.php

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ public static function createPropMetadatas(\ReflectionClass $class): array
$property->getName(),
$attribute->newInstance(),
$type ? $type->getName() : null,
$type ? $type->allowsNull() : false,
$type ? $type->isBuiltin() : false,
);
}
Expand Down
6 changes: 0 additions & 6 deletions src/LiveComponent/src/Metadata/LivePropMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public function __construct(
private string $name,
private LiveProp $liveProp,
private ?string $typeName,
private bool $allowsNull,
private bool $isBuiltIn,
) {
}
Expand All @@ -41,11 +40,6 @@ public function getType(): ?string
return $this->typeName;
}

public function allowsNull(): bool
{
return $this->allowsNull;
}

public function isBuiltIn(): bool
{
return $this->isBuiltIn;
Expand Down
43 changes: 11 additions & 32 deletions src/LiveComponent/src/Normalizer/DoctrineObjectNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Psr\Container\ContainerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
Expand All @@ -30,9 +29,6 @@
*/
final class DoctrineObjectNormalizer implements NormalizerInterface, DenormalizerInterface, ServiceSubscriberInterface
{
/** Flag to avoid recursion in the normalizer */
private const DOCTRINE_OBJECT_ALREADY_NORMALIZED = 'doctrine_object_normalizer.normalized';

/**
* @param ManagerRegistry[] $managerRegistries
*/
Expand Down Expand Up @@ -89,45 +85,24 @@ public function supportsNormalization(mixed $data, string $format = null, array

public function denormalize(mixed $data, string $type, string $format = null, array $context = []): ?object
{
if (null === $data) {
return null;
}

// $data is the single identifier or array of identifiers
if (\is_scalar($data) || (\is_array($data) && isset($data[0]))) {
return $this->objectManagerFor($type)->find($type, $data);
}

// $data is an associative array to denormalize the entity
// allow the object to be denormalized using the default denormalizer
// except that the denormalizer has problems with "nullable: false" columns
// https://github.com/symfony/symfony/issues/49149
// so, we send the object through the denormalizer, but turn type-checks off
// NOTE: The hydration system will already have prevented a writable property
// from reaching this.
$context[AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT] = true;
$context[self::DOCTRINE_OBJECT_ALREADY_NORMALIZED] = true;

return $this->getDenormalizer()->denormalize($data, $type, $format, $context);
throw new \LogicException('Invalid denormalization case');
}

public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = [])
{
if (true !== ($context[LiveComponentHydrator::LIVE_CONTEXT] ?? null) || !class_exists($type)) {
return false;
}

// not an entity?
if (null === $this->objectManagerFor($type)) {
return false;
}

// avoid recursion
if ($context[self::DOCTRINE_OBJECT_ALREADY_NORMALIZED] ?? false) {
return false;
if (
(\is_scalar($data) || (\is_array($data) && isset($data[0])))
&& null !== $this->objectManagerFor($type)
) {
return true;
}

return true;
return false;
}

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

private function objectManagerFor(string $class): ?ObjectManager
{
if (!class_exists($class)) {
return null;
}

// todo cache/warmup an array of classes that are "doctrine objects"
foreach ($this->managerRegistries as $registry) {
if ($om = $registry->getManagerForClass($class)) {
Expand Down
69 changes: 69 additions & 0 deletions src/LiveComponent/tests/Fixtures/Entity/TodoItemFixtureEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
*/
class TodoItemFixtureEntity
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
public $id;

/**
* @ORM\Column(type="string")
*/
private ?string $name = null;

/**
* @ORM\ManyToOne(targetEntity=TodoListFixtureEntity::class, inversedBy="todoItems")
*/
private TodoListFixtureEntity $todoList;

/**
* @param string $name
*/
public function __construct(string $name = null)
{
$this->name = $name;
}

public function getTodoList(): TodoListFixtureEntity
{
return $this->todoList;
}

public function setTodoList(?TodoListFixtureEntity $todoList): self
{
$this->todoList = $todoList;

return $this;
}

public function getName(): ?string
{
return $this->name;
}

public function setName(?string $name)
{
$this->name = $name;
}
}
72 changes: 72 additions & 0 deletions src/LiveComponent/tests/Fixtures/Entity/TodoListFixtureEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
*/
class TodoListFixtureEntity
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
public $id;

/**
* @ORM\Column(type="string")
*/
public string $listTitle = '';

/**
* @ORM\OneToMany(targetEntity=TodoItemFixtureEntity::class, mappedBy="todoList")
*/
private Collection $todoItems;

public function __construct(string $listTitle = '')
{
$this->listTitle = $listTitle;
$this->todoItems = new ArrayCollection();
}

public function getTodoItems(): Collection
{
return $this->todoItems;
}

public function addTodoItem(TodoItemFixtureEntity $todoItem): self
{
if (!$this->todoItems->contains($todoItem)) {
$this->todoItems[] = $todoItem;
$todoItem->setTodoList($this);
}

return $this;
}

public function removeTodoItem(TodoItemFixtureEntity $todoItem): self
{
if ($this->todoItems->removeElement($todoItem)) {
// set the owning side to null (unless already changed)
if ($todoItem->getTodoList() === $this) {
$todoItem->setTodoList(null);
}
}

return $this;
}
}
Loading