How do I use the normalizer correctly?
I have following code:
$encoder = new JsonEncoder();
$normalizer = new TestNormalizer(new ObjectNormalizer());
$serializer = new Serializer([$normalizer], [$encoder]);
My TestNormalizer:
class TestNormalizer implements NormalizerInterface
{
public function __construct(private ObjectNormalizer $normalizer)
{
}
public function normalize($object, string $format = null, array $context = []): array
{
$data = $this->normalizer->normalize($object, $format, $context);
return $data;
}
public function supportsNormalization($data, string $format = null, array $context = []): bool
{
return true;
}
}
I try to convert an Entity to a json array and get following error:
Cannot normalize attribute "xy" because the injected
serializer is not a normalizer.
If this works, the object should be processed later on
When using symfony serializations, you can get this error
This error happens when you use an ObjectNormalizer Symfony\Component\Serializer\Normalizer\ObjectNormalizer
instead of an Symfony\Component\Serializer\Normalizer\NormalizerInterface
To avoit this error Use
Symfony\Component\Serializer\Normalizer\NormalizerInterface;
Source :
http://fikri.fr/cannot-normalize-attribute-because-the-injected-serializer-is-not-a-normalizer/
Related
I'm using Symfony 5. In my app, I have a react app that posts entity data to the server. Then on the server side, I'll json_decode the data into an object, then validate and persist it to the database. I use DTO object instead of the entity class to handle the constraint. The problem came when writing the constraint to handle array of objects.
my data object structure
{
id: 1,
name: "john",
images: [
{ id: 1, img_name: "hello" },
{ id: 2, img_name: "world" }
]
}
DTO file
class PersonDTO {
/**
* #Assert\NotBlank
* #Assert\Type("integer")
*/
public $id
/**
* #Assert\All({
* #Assert\Collection(
* fields = {
* "id" = #Assert\Type("integer")
* }
*/
public $images
}
controller
public function index(Request $request, ValidatorInterface $validator)
{
$jsonData = $request->getContent();
$dataObject = json_decode($jsonData);
$personDTO = new PersonDTO();
$personDTO->id = $dataObject->id;
$personDTO->images = $dataObject->images
$errors = $validator->validate($personDTO);
}
I have followed the Symfony documentation on handling the array of object, but it doesn't work. The validation failed on the images array with the message Object(App\DTO\PersonDTO).images[0]: This value should be of type array|(Traversable&ArrayAccess).
What did I do wrong?
Alright I found the problem. The Symfony validation can only validate array, not object. So to validate array of object, I'll need to convert it to array of arrays.
public function index(Request $request, ValidatorInterface $validator)
{
// converting from json string
$dataArray = json_decode($json, true);
// converting from object
$dataArray = json_decode(json_encode(dataObject), true);
}
After that just pass the value to the DTO object for validation
$personDTO = new PersonDTO();
$personDTO->id= $dataArray['id']
$personDTO->images = $dataArray['images']
$errors = $validator->validate($personDTO);
Ultimately I make this into a static function in the DTO class to make the controller cleaner.
// PersonDTO class
public static function fromJson(string $json): self
{
$PersonDTO = new self();
$dataArray = json_decode($json, true);
$PersonDTO ->id = $dataArray['id'];
$PersonDTO ->images = $dataArray['images'];
return $PersonDTO ;
}
// controller
$personDTO = PersonDTO::fromJson($jsonData);
$errors = $validator->validate($PersonDTO );
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
}
}
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;
}
}
I am trying to add my own normalizer since I need to convert (denormalize) some raw values to it's related entities. This is what I have done:
namespace MMI\IntegrationBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class RelationshipNormalizer implements NormalizerInterface, DenormalizerInterface
{
public function normalize($object, $format = null, array $context = [])
{
// #TODO implement this method
}
public function supportsNormalization($data, $format = null): bool
{
return $data instanceof \AgreementType;
}
public function denormalize($data, $class, $format = null, array $context = [])
{
// #TODO implement this method
}
public function supportsDenormalization($data, $type, $format = null): bool
{
$supportedTypes = [
\AgreementType::class => true
];
return isset($supportedTypes[$type]);
}
}
And this is how I am using it from the controller:
$propertyNameConverter = new PropertyNameConverter();
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer(
null,
$propertyNameConverter,
null,
new ReflectionExtractor()
);
$serializer = new Serializer([
new DateTimeNormalizer(),
new RelationshipNormalizer(),
$normalizer,
new ArrayDenormalizer(),
], [$encoder]);
When the code reach this method:
private function getNormalizer($data, $format, array $context)
{
foreach ($this->normalizers as $normalizer) {
if ($normalizer instanceof NormalizerInterface && $normalizer->supportsNormalization($data, $format, $context)) {
return $normalizer;
}
}
}
Using Xdebug and the IDE I can see how the condition $data instanceof \AgreementType is accomplish but then the code try again to check the Normalizer and then this function is executed:
public function supportsDenormalization($data, $type, $format = null)
{
return class_exists($type);
}
And that's exactly where I get the wrong normalizer causing the following error:
Notice: Uninitialized string offset: 0 in
vendor/symfony/symfony/src/Symfony/Component/Inflector/Inflector.php
at line 179
UPDATE:
I have tried this other way and result is exactly the same as before meaning same error message:
$callback = function ($value) {
$value = $this->em->getRepository('QuoteBundle:' . $this->table_mapping[$this->entity])->find($value);
return $value;
};
$entityNormalizer = new GetSetMethodNormalizer();
$entityNormalizer->setCallbacks([
'agreementType' => $callback,
]);
$serializer = new Serializer([
new DateTimeNormalizer(),
$normalizer,
$entityNormalizer,
new ArrayDenormalizer(),
], [$encoder]);
What I am missing here?
After get some help on #symfony-devs channel on Slack I did found that order matters. Here is the solution to my issue (compare the piece of code below to the one on the OP and you'll see the difference):
$normalizer = new ObjectNormalizer(
null,
$propertyNameConverter,
null,
new ReflectionExtractor()
);
// Notice how ObjectNormalizer() is the last normalizer
$serializer = new Serializer([
new ArrayDenormalizer(),
new DateTimeNormalizer(),
new RelationshipNormalizer($em),
$normalizer,
], [$encoder]);
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"
}
]
}