Denormalize nested structure in objects with Symfony 2 serializer - php

I'm working on a Symfony 2 project with version 2.8 and I'm using the build-in component Serializer -> http://symfony.com/doc/current/components/serializer.html
I have a JSON structure provided by a web service.
After deserialization, I want to denormalize my content in objects. Here is my structure (model/make in a car application context).
[{
"0": {
"id": 0,
"code": 1,
"model": "modelA",
"make": {
"id": 0,
"code": 1,
"name": "makeA"
}
}
} , {
"1": {
"id": 1,
"code": 2,
"model": "modelB",
"make": {
"id": 0,
"code": 1,
"name": "makeA"
}
}
}]
My idea is to populate a VehicleModel object which contains a reference to a VehicleMake object.
class VehicleModel {
public $id;
public $code;
public $model;
public $make; // VehicleMake
}
Here is what I do:
// Retrieve data in JSON
$data = ...
$serializer = new Serializer([new ObjectNormalizer(), new ArrayDenormalizer()], [new JsonEncoder()]);
$models = $serializer->deserialize($data, '\Namespace\VehicleModel[]', 'json');
In result, my object VehicleModel is correctly populated but $make is logically a key/value array. Here I want a VehicleMake instead.
Is there a way to do that?

The ObjectNormalizer needs more configuration. You will at least need to supply the fourth parameter of type PropertyTypeExtractorInterface.
Here's a (rather hacky) example:
<?php
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
$a = new VehicleModel();
$a->id = 0;
$a->code = 1;
$a->model = 'modalA';
$a->make = new VehicleMake();
$a->make->id = 0;
$a->make->code = 1;
$a->make->name = 'makeA';
$b = new VehicleModel();
$b->id = 1;
$b->code = 2;
$b->model = 'modelB';
$b->make = new VehicleMake();
$b->make->id = 0;
$b->make->code = 1;
$b->make->name = 'makeA';
$data = [$a, $b];
$serializer = new Serializer(
[new ObjectNormalizer(null, null, null, new class implements PropertyTypeExtractorInterface {
/**
* {#inheritdoc}
*/
public function getTypes($class, $property, array $context = array())
{
if (!is_a($class, VehicleModel::class, true)) {
return null;
}
if ('make' !== $property) {
return null;
}
return [
new Type(Type::BUILTIN_TYPE_OBJECT, true, VehicleMake::class)
];
}
}), new ArrayDenormalizer()],
[new JsonEncoder()]
);
$json = $serializer->serialize($data, 'json');
print_r($json);
$models = $serializer->deserialize($json, VehicleModel::class . '[]', 'json');
print_r($models);
Note that in your example json, the first entry has an array as value for make. I took this to be a typo, if it's deliberate, please leave a comment.
To make this more automatic you might want to experiment with the PhpDocExtractor.

In cases when you need more flexibility in denormalization it's good to create your own denormalizers.
$serializer = new Serializer(
[
new ArrayNormalizer(),
new VehicleDenormalizer(),
new VehicleMakeDenormalizer()
], [
new JsonEncoder()
]
);
$models = $serializer->deserialize(
$data,
'\Namespace\VehicleModel[]',
'json'
);
Here the rough code of such denormalizer
class VehicleDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
public function denormalize($data, $class, $format, $context)
{
$vehicle = new VehicleModel();
...
$vehicleMake = $this->denormalizer->denormalize(
$data->make,
VehicleMake::class,
$format,
$context
);
$vehicle->setMake($vehicleMake);
...
}
}
I only have doubts on should we rely on $this->denormalizer->denormalize (which works properly just because we use Symfony\Component\Serializer\Serializer) or we must explicitly inject VehicleMakeDenormalizer into VehicleDenormalizer
$vehicleDenormalizer = new VehicleDenormalizer();
$vehicleDenormalizer->setVehicleMakeDenormalizer(new VehicleMakeDenormalizer());

The easiest way would be to use the ReflectionExtractor if your Vehicle class has some type hints.
class VehicleModel {
public $id;
public $code;
public $model;
/** #var VehicleMake */
public $make;
}
You can pass the Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor as argument to the ObjectNormalizer when you initialize the Serializer
$serializer = new Serializer([new ObjectNormalizer(null, null, null, new ReflectionExtractor()), new ArrayDenormalizer()], [new JsonEncoder()]);
$models = $serializer->deserialize($data, '\Namespace\VehicleModel[]', 'json');

In Symfony4+, you can inject the serializer and it will do the job for you based on either your phpdoc (eg #var) or type hinting. Phpdoc seems safer as it manages collections of objects.
Example:
App\Model\Skill.php
<?php
namespace App\Model;
class Skill
{
public $name = 'Taxi Driver';
/** #var Category */
public $category;
/** #var Person[] */
public $people = [];
}
App\Model\Category.php
<?php
namespace App\Model;
class Category
{
public $label = 'Transports';
}
App\Model\Person.php
<?php
namespace App\Model;
class Person
{
public $firstname;
}
App\Command\TestCommand.php
<?php
namespace App\Command;
use App\Model\Category;
use App\Model\Person;
use App\Model\Skill;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Serializer\SerializerInterface;
class TestCommand extends Command
{
/**
* #var SerializerInterface
*/
private $serializer;
public function __construct(SerializerInterface $serializer)
{
parent::__construct();
$this->serializer = $serializer;
}
protected function configure()
{
parent::configure();
$this
->setName('test')
->setDescription('Does stuff');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$personA = new Person();
$personA->firstname = 'bruno';
$personB = new Person();
$personB->firstname = 'alice';
$badge = new Skill();
$badge->name = 'foo';
$badge->category = new Category();
$badge->people = [$personA, $personB];
$output->writeln(
$serialized = $this->serializer->serialize($badge, 'json')
);
$test = $this->serializer->deserialize($serialized, Skill::class, 'json');
dump($test);
return 0;
}
}
Will give the following expected result:
{"name":"foo","category":{"label":"Transports"},"people":[{"firstname":"bruno"},{"firstname":"alice"}]}
^ App\Model\BadgeFacade^ {#2531
+name: "foo"
+category: App\Model\CategoryFacade^ {#2540
+label: "Transports"
}
+people: array:2 [
0 => App\Model\PersonFacade^ {#2644
+firstname: "bruno"
}
1 => App\Model\PersonFacade^ {#2623
+firstname: "alice"
}
]
}

Related

Symfony normalizing array of strings into an Entity

I am receiving a payload that looks like this:
{
"date": "2019-03-14 14:48:26 +0000",
"events": [
"E09FDE82-4CAA-4641-87AF-6C092D6E71C1",
"AE12A6BC-DA37-4C37-BF49-DD0CE096AE00"
],
"location": null
}
The wrapper object is an Animal entity and the events is an array of UUIDs that belong to Event entities. These may or may not exist in the events table.
I want to be able to serialize this into an Animal entity using the symfony serializer like so:
$serializer = $this->get("serializer");
if($request->getMethod() == Request::METHOD_POST) {
$data = $request->getContent();
$entity = $serializer->deserialize($data, $this->type, 'json');
...
...
What I would like to do is during deserialization, I need to look for that particular key and iterate over it, creating new Events (or getting existing ones) and call the setter on the animal with these.
I have had a look at symfony normalizers but I don't think these are the right things? I made this but not sure where to go from here:
<?php
namespace App\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use App\Entity\Event;
class EventNormalizer implements NormalizerInterface {
private $normalizer;
public function __construct(ObjectNormalizer $normalizer){
$this->normalizer = $normalizer;
}
public function normalize($event, $format = null, array $context = [])
{
$data = $this->normalizer->normalize($event, $format, $context);
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
return $data instanceof Event;
}
}
According to the documentation, this is how you would edit existing values or add new ones but I have no idea how I would tell the normalizer that "hey, when you see this key, you're up, do your thing".
Any help appreciated.
Here you need a Denormalizer, try to implement DenormalizerInterface
class EventNormalizer implements NormalizerInterface, DenormalizerInterface {
...
public function denormalize($data, string $type, string $format = null, array $context = [])
{
// retrieve your events from $data and return the object (Animal) with related events
// $this->entityManager->find($data['events'][0]) ...
}
public function supportsDenormalization($data, string $type, string $format = null)
{
// your code here
}
}

How can I seralize to json with circular reference handler in Symfony?

I want to serialize my database table into a json file:
$table = $this->em->getRepository($EntityName)->findAll();
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$encoders = [new JsonEncoder()];
$normalizers = [new DateTimeNormalizer(array('datetime_format' => 'd.m.Y')), new ObjectNormalizer($classMetadataFactory)];
$serializer = new Serializer($normalizers, $encoders);
$context = [
'circular_reference_handler' => function ($object) {
return $object->getId();
},
'circular_reference_limit' => 0,
];
$data = $serializer->serialize($table, 'json', $context);
But I have some problems with the performance. My page loads really slow, for several seconds, and then I get a blank page.
I use this to serialize my objects and send a JsonResponse
<?php
namespace App\Services\Api\Serializer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Doctrine\Common\Annotations\AnnotationReader;
class ObjectSerializer
{
private $object;
private $specifics_attributes;
public function __construct($object, Array $groups = ['default'], Array $specifics_attributes = null)
{
$this->object = $object;
$this->groups = $groups;
$this->specifics_attributes = $specifics_attributes;
}
public function serializeObject(): ?Array
{
$object = $this->object;
$defaultContext = [
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
return $object;
},
];
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = [new DateTimeNormalizer('Y-m-d'), new ObjectNormalizer($classMetadataFactory, null, null, null, null, null, $defaultContext)];
$serializer = new Serializer($normalizer);
return $serializer->normalize($object, null, $this->normalizerFilters());
}
private function normalizerFilters():Array
{
$groups = $this->groups;
$specifics_attributes = $this->specifics_attributes;
$normalizer_filter = [
'groups' => $groups,
];
if ($specifics_attributes) {
$normalizer_filter['attributes'] = $specifics_attributes;
}
return $normalizer_filter;
}
}
Then in the controller or another service we can use that like this
use App\Services\Api\Serializer\ObjectSerializer;
/* Some codes */
$objectSerializer = new ObjectSerializer($objects, ['my_custom_attributes_groupe']);
return new JsonResponse([
'status' => 'success',
'MyEntityName' => $objectSerializer->serializeObject(),
], JsonResponse::HTTP_OK);

How to use (chain?) multiple normalizers with Symfony Serializer?

can somebody try to explain me how to use multiple normalizers when serializing data from multiple classes with the Symfony serializer?
Lets say that I have the following classes:
class User
{
private $name;
private $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
// getters and setters
}
class Book
{
private $title;
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
}
And I want to serialize an user who has multiple books.
$first = new Book();
$first->setTitle('First book');
$second = new Book();
$second->setTitle('Second book');
$user = new User();
$user->setName('Person name');
$user->addBook($first);
$user->addBook($second);
dump($this->get('serializer')->serialize($user, 'json'));
die();
Let's say that I also want to include a hash when serializing a book, so I have the following normalizer:
class BookNormalizer implements NormalizerInterface
{
public function normalize($object, $format = null, array $context = array())
{
return [
'title' => $object->getTitle(),
'hash' => md5($object->getTitle())
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Book;
}
}
And I am getting the expected result:
{"name":"Person name","books":[{"title":"First book","hash":"a9c04245e768bc5bedd57ebd62a6309e"},{"title":"Second book","hash":"c431a001cb16a82a937579a50ea12e51"}]}
The problem comes when I also add a normalizer for the User class:
class UserNormalizer implements NormalizerInterface
{
public function normalize($object, $format = null, array $context = array())
{
return [
'name' => $object->getName(),
'books' => $object->getBooks()
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof User;
}
}
Now, the books aren't normalized using the previously given normalizer, and i get the following:
{"name":"Person name","books":[{},{}]}
I tried to find a way (documentation and other articles) to always call the normalizers for the given types (eg. always call the book normalizer when the type is Book, even if the data is nested and used in another normalizer) but could not succeed.
I think i have misunderstood something about normalizers but don't know what. Can somebody explain to is what i want possible and how to do it?
You have to use the NormalizerAwareTrait so you can access the normalizer for books
add interface
use trait
call normalize() method for books
code:
class UserNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function normalize($object, $format = null, array $context = array())
{
return [
'name' => $object->getName(),
'books' => $this->normalizer->normalize($object->getBooks(), $format, $context)
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof User;
}
}

Symfony 2 denormalization of nested objects with custom denormalizers

Here I'm building Symfony SDK for REST API. Most of data are JSON objects with nested other JSON objects. Like here
{
"id": "eng_pl",
"name": "Premier League",
"_links": {
"self": {
"href": "/tournaments/eng_pl"
},
"seasons": {
"href": "/tournaments/eng_pl/seasons/"
}
},
"coverage": {
"id": "eng",
"name": "England",
"_links": {
"self": {
"href": "/territories/eng"
}
}
}
}
Deserialization must produce an object equal to object produced by the code listed below:
$tournament = new Tournament();
$tournament->setId('eng_pl');
$tournament->setName('Premier League');
$coverage = new Territory();
$coverage->setId('eng');
$coverage->setName('England');
$tournament->setCoverage($coverage);
I'm using my own custom Denormalizers, below the fragment of code of denormalizer for Tournament objects:
class TournamentDenormalizer implements DenormalizerInterface
{
/**
* #inheritdoc
*/
public function supportsDenormalization($object, $type, $format = null)
{
if ($type != Tournament::class) {
return false;
}
return true;
}
/**
* #inheritdoc
* #return Tournament
*/
public function denormalize($object, $class, $format = null, array $context = array())
{
$tournament = new Tournament();
$tournament->setId($object->id);
$tournament->setName($object->name);
if (isset($object->coverage)) {
/** #var Territory $coverage */
$coverage = ???; //HOWTO how to implement Territory denormalization here???
$tournament->setCoverage(
$coverage
);
}
return $tournament;
}
}
The question is how should I access TerritoryDenormalizer inside TournamentDenormalizer? I see two options:
First one (I'm using now) is to add implements DenormalizerAwareInterface to signature of denormalizer class and rely on Symfony\Component\Serializer\Serializer class:
$serializer = new Symfony\Component\Serializer\Serializer(
[
new TournamentDenormalizer(),
new TerritoryDenormalizer()
], [
new Symfony\Component\Serializer\Encoder\JsonDecode()
]
);
$serializer->deserialize($json, Tournament::class, 'json');
So in TournamentDeserializer it will be like here:
if (isset($object->coverage)) {
/** #var Territory $coverage */
$coverage = $this->denormalizer->denormalize(
$object->coverage,
Territory::class,
$format,
$context
);
$tournament->setCoverage(
$coverage
);
}
}
The second approach
Is to inject necessary denormalizers explicitly
$tournamentDenormalizer = new TournamentDenormalizer();
$tournamentDenormalizer->setTerritoryDenormalizer(new TerritoryDenormalizer());
So in TournamentDeserializer it will be like here:
if (isset($object->coverage)) {
/** #var Territory $coverage */
$coverage = $this->territoryDenormalizer->denormalize(
$object->coverage,
Territory::class,
$format,
$context
);
$tournament->setCoverage(
$coverage
);
}
}
Which of approaches is the best? What alternative approaches are possible?
Making your normalizer implement NormalizerAwareInterface (and eventually use NormalizerAwareTrait) is the way to go, this interface has been introduced for this specific use case.

How deserialize array in Symfony

I'd like to deserialize array to class in Symfony but I can't find a way to do it without using e.g json or XML.
This is class:
class Product
{
protected $id;
protected $name;
...
public function getName(){
return $this->name;
}
...
}
Array that I'd like to deserialize to Product class.
$product['id'] = 1;
$product['name'] = "Test";
...
You need to use denormalizer directly.
Version:
class Version
{
/**
* Version string.
*
* #var string
*/
protected $version = '0.1.0';
public function setVersion($version)
{
$this->version = $version;
return $this;
}
}
usage:
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Version;
$serializer = new Serializer(array(new ObjectNormalizer()));
$obj2 = $serializer->denormalize(
array('version' => '3.0'),
'Version',
null
);
dump($obj2);die;
result:
Version {#795 ▼
#version: "3.0"
}
You could do it through reflection like this..
function unserialzeArray($className, array $data)
{
$reflectionClass = new \ReflectionClass($className);
$object = $reflectionClass->newInstanceWithoutConstructor();
foreach ($data as $property => $value) {
if (!$reflectionClass->hasProperty($property)) {
throw new \Exception(sprintf(
'Class "%s" does not have property "%s"',
$className,
$property
));
}
$reflectionProperty = $reflectionClass->getProperty($property);
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($object, $value);
}
return $object;
}
Which you would then call like..
$product = unserializeArray(Product::class, array('id' => 1, 'name' => 'Test'));

Categories