symfony/serializer usage with object collection

…衆ロ難τιáo~ 提交于 2021-01-29 20:29:40

问题


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:

  1. Symfony Framework 5.2 wit common flex configuration
symfony new serializer-demo
composer.phar require serializer doctrine jms\serializer
  1. 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;
    }

}

  1. 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:

  1. @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
    }
  ]
}
  1. @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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!