ObjectNormalizer overrides RelationshipNormalizer causing code to crash, why? - php

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]);

Related

Symfony custom normalizer "the injected serializer is not a normalizer"

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/

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;
}
}

Denormalize nested structure in objects with Symfony 2 serializer

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"
}
]
}

Override a complicated class with multiple parameters

I am trying to override the class UsernamePasswordFormAuthenticationListener.
parameters:
security.authentication.listener.form.class: AppBundle\Listener\LoginFormListener
class LoginFormListener extends UsernamePasswordFormAuthenticationListener
{
/**
* {#inheritdoc}
*/
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfProviderInterface $csrfProvider = null)
{
parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, $options, $logger, $dispatcher, $csrfProvider);
}
protected function attemptAuthentication(Request $request)
{
if (null !== $this->csrfTokenManager) {
$csrfToken = $request->get($this->options['csrf_parameter'], null, true);
if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['intention'], $csrfToken))) {
throw new InvalidCsrfTokenException('Invalid CSRF token.');
}
}
if ($this->options['post_only']) {
$username = trim($request->request->get($this->options['username_parameter'], null, true));
$password = $request->request->get($this->options['password_parameter'], null, true);
} else {
$username = trim($request->get($this->options['username_parameter'], null, true));
$password = $request->get($this->options['password_parameter'], null, true);
}
$request->getSession()->set(Security::LAST_USERNAME, $username);
$apiRequest = new ApiRequest();
$apiRequest->addMethod('login', array('email' => $username, 'password' => $password));
$response = $apiRequest->sendRequest();
dump($response);
exit;
}
}
But when I execute it, I have this error :
Catchable Fatal Error: Argument 1 passed to AppBundle\Listener\LoginFormListener::__construct() must implement interface Symfony\Component\Security\Core\SecurityContextInterface, instance of Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage given, called in /Users/dimitri/Sites/rennes/app/cache/dev/appDevDebugProjectContainer.php on line 4039 and defined
Any idea how I can make this work ?
You can simply change SecurityContextInterface type hint to TokenStorageInterface in your class and all should work fine - this class service has been changed recently (deprecated in 2.7) , so your example code might be outdated.
Check blog post entry to get more info about
SecurityContextInterface vs TokenStorageInterface

Categories