问题
Symfony serializer lost collection object while denormalizing.
Example:
We have some entity with collection in the property (I'm using Doctrine ArrayCollection
but it is reproduced for any Collection which implement PHP Iterator
interface)
Let's serialize and deserialize it with Symfony and JMS serializer and compare results:
Origin:
^ App\Entity\Entity^ {#195
-rewardItems: Doctrine\Common\Collections\ArrayCollection^ {#179
-elements: array:2 [
0 => App\Entity\Item^ {#224
-type: 1
}
1 => App\Entity\Item^ {#180
-type: 2
}
]
}
}
Symfony deserialized:
^ App\Entity\Entity^ {#192
-rewardItems: array:2 [
0 => App\Entity\Item^ {#343
-type: 1
}
1 => App\Entity\Item^ {#325
-type: 2
}
]
}
Jms Deserialized
^ App\Entity\Entity^ {#369
-rewardItems: Doctrine\Common\Collections\ArrayCollection^ {#357
-elements: array:2 [
0 => App\Entity\Item^ {#374
-type: 1
}
1 => App\Entity\Item^ {#289
-type: 2
}
]
}
}
Pay attention, that both original entity and JMS deserialized entity has internal ArrayCollection
, while Symfony deserialized entity lost ArrayCollection
and replace it with php array
.
I found only one solution: add my own Symfony normalizer. But I see very poor developer experience here. For each Entity in my project I should provide boilerplate normalizer :(
Does symfony have simpler solution for Collections (f.e. like JMS) ?
Reproduce stand:
- Symfony Framework 5.2 wit common flex configuration
symfony new serializer-demo
composer.phar require serializer doctrine jms\serializer
- Very simple entities
class Entity
{
/**
* @var ArrayCollection<\App\Entity\Item>
*
* @Type("ArrayCollection<App\Entity\Item>")
*
*/
private $rewardItems;
public function __construct()
{
$this->rewardItems = new ArrayCollection();
}
public function getRewardItems(): ArrayCollection
{
return $this->rewardItems;
}
public function setRewardItems($rewardItems): void
{
$this->rewardItems = $rewardItems;
}
}
class Item
{
/**
* @var int
* @Type("int")
*/
private $type;
public function __construct(int $type)
{
$this->type = $type;
}
/**
* @return int
*/
public function getType(): int
{
return $this->type;
}
/**
* @param int $type
*/
public function setType(int $type): void
{
$this->type = $type;
}
}
- Run in console command
$a = new Entity();
$a->getRewardItems()->add(new Item(1));
$a->getRewardItems()->add(new Item(2));
$output->writeln("Origin:");
VarDumper::dump($a);
$this->symfony($a, $output);
$this->jms($a, $output);
// Symfony
$serializer = $this->serializer; // injected by DI
$serialized = $serializer->serialize($a, 'json');
$d = $serializer->deserialize($serialized, Entity::class, 'json');
$output->writeln("<warning>Symfony deserialized:</warning>");
VarDumper::dump($d);
// JMS
$serializer = SerializerBuilder::create()->build();
$jsonContent = $serializer->serialize($a, 'json');
$des = $serializer->deserialize($jsonContent, Entity::class, 'json');
$output->writeln("Jms Deserialized");
VarDumper::dump($des);```
UPD0: Providing PHPDoc type-hints for $rewardItems property changes result, but don't really helped:
@var ArrayCollection<\App\Entity\Item>
,ArrayCollection<Item>
,@var Item[]
- result the same as in original topic:
^ App\Entity\Entity^ {#192
-rewardItems: array:2 [
0 => App\Entity\Item^ {#343
-type: 1
}
1 => App\Entity\Item^ {#325
-type: 2
}
]
}
@var ArrayCollection<>
Result a bit different:
App\Entity\Entity {#192
-rewardItems: Doctrine\Common\Collections\ArrayCollection {#307
-elements: []
}
}
回答1:
I think there are multiple simple solutions to tackle this. I'm not quite certain if there is a better method. That works better with the Serializer.
most preferable - if you want to actively avoid adders and removers
I somehow doubt this will work.
since your original post shows no sign of it but it might be relevant, since the type of a property might be very different from the type of the parameter expected and the most sensible assumption is used (which would be array, since you shouldn't expose your internal implementation):
/**
* @param ArrayCollection<\App\Entity\Item> $rewardItems
*/
public function setRewardItems($rewardItems): void
{
$this->rewardItems = $rewardItems;
}
using real type-hints
and if you don't want adders/removers (which I would prefer in general, but that's just preference)
use Doctrine\Common\Collections\Collection; // <-- or use more specific ones ...
// adding real type-hint:
public function setRewardItems(Collection $rewardItems): void
{
$this->rewardItems = $rewardItems;
}
This could lead to the symfony serializer just failing, when it doesn't provide a Collection.
adders over setters
The PropertyAccessor will prioritize adders over setters - you must add a remover too
public function addRewardItem(Item $item): void
{
if (!$this->rewardItems->contains($item)) {
$this->rewardItems[] = $item;
}
}
complete replacement
this can fail, if something different than a collection is provided
use Doctrine\Common\Collections\Collection; // <--!
public function setRewardItems($rewardItems): void
{
$this->rewardItems = ($rewardItems instanceof Collection) ? $rewardItems : new ArrayCollection($rewardItems);
}
one of those approaches should definitely work, and I'm not quite sure, if there is a more elegant solution to communicate to the serializer that you expect an ArrayCollection
comment
I prefer adders and removers, because it leaves the original Collection object in place, thus making it easier for doctrine to notice changes. You could achieve the same thing with a setter, but it's more complicated. I don't like exposing the internal handling of rewardItems to the "outside" world, because essentially when someone calls "getRewardItems" and you hand your collections to the caller, it might change the collection in any way it likes - calling a getter should not lead to the internal state of an object being changeable/changed.I prefer returning $this->rewardItems->toArray()
because it doesn't expose anything about how the rewardItems are handled internally. And there is no danger in some code changing the collection for whatever reason. Yes, it's defensive.
来源:https://stackoverflow.com/questions/65441877/symfony-serializer-usage-with-object-collection