Symfony 3.2 A circular reference has been detected (configured limit: 1) - symfony

I have this two entities in my project
class PoliceGroupe
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="code", type="string", length=50)
*/
private $code;
/**
* #ORM\ManyToMany(targetEntity="PointVente", inversedBy="policegroupe")
* #ORM\JoinTable(name="police_groupe_point_vente",
* joinColumns={#ORM\JoinColumn(name="police_groupe_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="point_vente_id", referencedColumnName="id")}
* )
*/
private $pointVente;
/**
* Constructor
*/
public function __construct($produit)
{
$this->pointVente = new \Doctrine\Common\Collections\ArrayCollection();
}
}
And here is my other entity
class PointVente
{
/**
* #var string
*
* #ORM\Column(name="abb", type="string", length=50)
*/
private $abb;
/**
* #var string
*
* #ORM\Column(name="libelle", type="string", length=255)
*/
private $libelle;
/**
*
* #ORM\ManyToMany(targetEntity="PoliceGroupe", mappedBy="pointVente")
*/
private $policegroupe;
}
and i'm trying to run this code in my controller
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$serializer = new Serializer($normalizers, $encoders);
$em = $this->getDoctrine()->getManager();
$data = $request->get('data');
$policegroupe=$em->getRepository('StatBundle:PoliceGroupe')->findOneBy(array('id' => $data));
$pointventes = $policegroupe->getPointVente();
$jsonContent = $serializer->serialize($pointventes, 'json');
return new JsonResponse( array('pointventes'=>$jsonContent) );
But I get this exception
Symfony\Component\Serializer\Exception\CircularReferenceException: A circular reference has been detected (configured limit: 1).
at n/a
in C:\wamp\www\Sys\vendor\symfony\symfony\src\Symfony\Component\Serializer\Normalizer\AbstractNormalizer.php line 194
I mapped my entities according to the doctrine annotations. Am I missing something?

Symfony 3.2
Use the setCircularReferenceLimit method. For example:
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceLimit(2);
// Add Circular reference handler
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getId();
});
$normalizers = array($normalizer);
$serializer = new Serializer($normalizers, $encoders);
The reason is that the circular referencing in your entities causes some problems when you try to serialize them. The effect of the method is to define the maximum depth of the serialization hierarchy.
Edit: Added circular reference handler (A circular reference has been detected (configured limit: 1) Serializer SYMFONY)
EDIT : Update (Symfony 4.2)
To be tried with Symfony 3.2, but the circular_reference_limit is not the problem here (and the defaults to 1 is OK, else your entity will be retrieved 2 times), the problem is the way the entity is handled by circular_reference_handler. Telling that id is the entity identifier solves the problem. See Symfony Docs at the bottom of this paragraph.
Since setCircularReferenceHandler is deprecated in favour of the following keys of the context circular_reference_handler, we can write:
// Tip : Inject SerializerInterface $serializer in the controller method
// and avoid these 3 lines of instanciation/configuration
$encoders = [new JsonEncoder()]; // If no need for XmlEncoder
$normalizers = [new ObjectNormalizer()];
$serializer = new Serializer($normalizers, $encoders);
// Serialize your object in Json
$jsonObject = $serializer->serialize($objectToSerialize, 'json', [
'circular_reference_handler' => function ($object) {
return $object->getId();
}
]);
// For instance, return a Response with encoded Json
return new Response($jsonObject, 200, ['Content-Type' => 'application/json']);

The best way is to use the useCircularReferenceLimit method. As it has already been clearly explained in this post.
But we have another option. As an option, there's a way to ignore attributes from the origin object. We can ignore it if we definitely don't need it in a serialized object.
The advantage of this solution is that the serialized object is smaller and easier to read, and the disadvantage is that we will no longer refer to the ignored attribute.
Symfony 2.3 - 4.1
To remove those attributes use the setIgnoredAttributes() method on the normalizer definition:
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
$normalizer = new ObjectNormalizer();
$normalizer->setIgnoredAttributes(array('age'));
$encoder = new JsonEncoder();
$serializer = new Serializer(array($normalizer), array($encoder));
$serializer->serialize($person, 'json'); // Output: {"name":"foo","sportsperson":false}
The setIgnoredAttributes() method was introduced in Symfony 2.3.
Prior to Symfony 2.7, attributes were only ignored while serializing. Since Symfony 2.7, they are ignored when deserializing too.
Symfony 4.2 - 5.0
The setIgnoredAttributes() method that was used as an alternative to the ignored_attributes option was deprecated in Symfony 4.2.
To remove those attributes provide an array via the ignored_attributes key in the context parameter of the desired serializer method:
use Acme\Person;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
$person = new Person();
$person->setName('foo');
$person->setAge(99);
$normalizer = new ObjectNormalizer();
$encoder = new JsonEncoder();
$serializer = new Serializer([$normalizer], [$encoder]);
$serializer->serialize($person, 'json', ['ignored_attributes' => ['age']]); // Output: {"name":"foo"}
In my Symfony 3.4 projects I use a mix of these two methods setIgnoredAttributes() and setCircularReferenceLimit() and it works fine.
Source: https://symfony.com/doc/3.4/components/serializer.html

You need to use context:
$normalizer->normalize($article, 'It doesn`t matter', [
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function($object) {
return $object->getId();
}
]);
Big example:
namespace App\Command;
use App\Entity\Oltp\Article;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use function json_encode;
class SqliteFirstCommand extends Command
{
protected static $defaultName = 'app:first';
private EntityManagerInterface $entityManager;
private NormalizerInterface $normalizer;
public function __construct(
EntityManagerInterface $entityManager,
NormalizerInterface $normalizer
)
{
parent::__construct(self::$defaultName);
$this->entityManager = $entityManager;
$this->normalizer = $normalizer;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$articles = $this->entityManager->getRepository(Article::class)->findAll();
foreach ($articles as $article) {
$array = $this->normalizer->normalize($article, 'It doesn`t matter', [
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function($object) {
return $object->getId();
}
]);
$output->writeln(json_encode($array));
}
return 0;
}
}

Fixed same issue by
use JMS\Serializer\SerializerBuilder;
...
$products = $em->getRepository('AppBundle:Product')->findAll();
$serializer = SerializerBuilder::create()->build();
$jsonObject = $serializer->serialize($products, 'json');
Read here

Related

Symfony denormalization: Recursive Denormalization not working

I'am trying to desirialize the following Json structure into a Profile object
{
"label": "lorem label",
"info": {
"name": "lorem name",
"title": "lorem title"
}
}
I have a class Profile
namespace App\Document;
class profile
{
/**
* #var string
*/
protected $label;
/**
* #var Info
*/
protected $info;
// getters and setters
}
and a class Info
namespace App\Document;
class Info
{
/**
* #var string
*/
protected name;
/**
* #var string
*/
protected title;
// getters & setters
}
my controller's code is the following
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use App\Document;
public function addProfile(Request $request, DocumentManager $dm, $profile_id)
{
$phpDocNormalizer = new ObjectNormalizer(null, null, null, new PhpDocExtractor());
$reflectionNormalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
$serializer = new Serializer([$phpDocNormalizer, $reflectionNormalizer], [new JsonEncoder()]);
$profile = $serializer->deserialize($request->getContent(), Profile::class, 'json');
return new JsonResponse(Response::HTTP_CREATED);
}
As the symfony docs says, i have the phpDocExtractor set and working, and the doc annotations are set too, the PropertyInfo Component is installed too. But i keep getting this NotNormalizableValueException:
The type of the "info" attribute for class "App\Document\Profile" must be one of "App\Document\Info" ("array" given).
I've been stuck with this one for a while now, any help is much appreciated.
public function addProfile(Request $request, DocumentManager $dm, $profile_id)
{
$extractors = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);
$normalizer = new ObjectNormalizer(null, null, null, $extractors);
$serializer = new Serializer([$normalizer], [new JsonEncoder()]);
$profile = $serializer->deserialize($request->getContent(), Profile::class, 'json');
return new JsonResponse(Response::HTTP_CREATED);
}
You can find a more detailed example here : https://symfony.com/doc/current/components/property_info.html#usage

Symfony validation not returning any violations for NotBlank

I'm using Symfony validation within my own app (not Symfony). I'm expecting it to return violations as I haven't populated the Id property:
$user = new User();
$user->setId('');
//...
$validator = \Symfony\Component\Validator\Validation::createValidator();
$errors = $validator->validate($user);
var_dump(count($errors)); exit; // outputs: 0
However, validate returns no violations.
Here is my User class with constraint annotations:
use Symfony\Component\Validator\Constraints as Assert;
class User {
/**
* #Assert\NotBlank
*/
private $id;
//...
public function getId(): string {
return $this->id;
}
public function setId(string $id): void {
$this->id = $id;
}
//...
Where am I going wrong? According to the docs, a blank string should trigger a violation for this constraint - https://symfony.com/doc/current/reference/constraints/NotBlank.html
You don't need Annotations if you use Validation.
Try this :
$validator = Validation::createValidator();
$violations = $validator->validate($user->getId, [
new NotBlank(),
]);

JMSSerializerBundle deserialization skip groups exclusion on id property using DoctrineObjectConstructor

I'm using jms/serializer-bundle 2.4.3 on a symfony 4.2 and a I noticed an annoying problem in my application :
when I post an entity, the DoctrineObjectConstructor uses id in content to retrieve another entity and thus patch it while it is excluded by my security groups
see rather entity
class Entity
{
/**
* #var int
*
* #ORM\Column(name="id", type="int")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #Serializer\Groups({"GetEntity"})
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string")
* #Serializer\Groups({"GetEntity", "PostEntity"})
*/
private $name;
}
controller
/**
* #Route("/entity", name="post_entity", methods={"POST"})
*/
public function postEntity(Request $request, EntityManagerInterface $entityManager, SerializerInterface $serializer): JsonResponse
{
$deserializationContext = DeserializationContext::create();
$deserializationContext->setGroups(['PostEntity']);
$entity = $serializer->deserialize($request->getContent(), Entity::class, 'json', $deserializationContext);
$entityManager->persist($entity);
$entityManager->flush();
return $this->json($entity, Response::HTTP_OK, [], ['groups' => ['GetEntity']]);
}
I have some JMS configurations changes in services
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: true
jms_serializer.unserialize_object_constructor:
class: App\Serializer\ObjectConstructor
If anyone can explain to me how to ignore the id in this case I'm open to any suggestions.
Regards and thanks for any help
To resolve, just add override in your services.yaml
jms_serializer.doctrine_object_constructor:
class: App\Serializer\DoctrineObjectConstructor
arguments:
- '#doctrine'
- '#jms_serializer.unserialize_object_constructor'
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
and add a local DoctrineObjectConstructor updated to ignore entities without current deserialization group on id property
class DoctrineObjectConstructor implements ObjectConstructorInterface
{
const ON_MISSING_NULL = 'null';
const ON_MISSING_EXCEPTION = 'exception';
const ON_MISSING_FALLBACK = 'fallback';
private $fallbackStrategy;
private $managerRegistry;
private $fallbackConstructor;
/**
* Constructor.
*
* #param ManagerRegistry $managerRegistry Manager registry
* #param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
* #param string $fallbackStrategy
*/
public function __construct(ManagerRegistry $managerRegistry, ObjectConstructorInterface $fallbackConstructor, $fallbackStrategy = self::ON_MISSING_NULL)
{
$this->managerRegistry = $managerRegistry;
$this->fallbackConstructor = $fallbackConstructor;
$this->fallbackStrategy = $fallbackStrategy;
}
/**
* {#inheritdoc}
*/
public function construct(VisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context)
{
// Locate possible ObjectManager
$objectManager = $this->managerRegistry->getManagerForClass($metadata->name);
if (!$objectManager) {
// No ObjectManager found, proceed with normal deserialization
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
// Locate possible ClassMetadata
$classMetadataFactory = $objectManager->getMetadataFactory();
if ($classMetadataFactory->isTransient($metadata->name)) {
// No ClassMetadata found, proceed with normal deserialization
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
// Managed entity, check for proxy load
if (!\is_array($data)) {
// Single identifier, load proxy
return $objectManager->getReference($metadata->name, $data);
}
// Fallback to default constructor if missing identifier(s)
$classMetadata = $objectManager->getClassMetadata($metadata->name);
$identifierList = [];
foreach ($classMetadata->getIdentifierFieldNames() as $name) {
$propertyGroups = [];
if ($visitor instanceof AbstractVisitor) {
/** #var PropertyNamingStrategyInterface $namingStrategy */
$namingStrategy = $visitor->getNamingStrategy();
$dataName = $namingStrategy->translateName($metadata->propertyMetadata[$name]);
$propertyGroups = $metadata->propertyMetadata[$name]->groups;
} else {
$dataName = $name;
}
if (!array_key_exists($dataName, $data) || true === empty(array_intersect($context->getAttribute('groups'), $propertyGroups))) {
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
$identifierList[$name] = $data[$dataName];
}
// Entity update, load it from database
$object = $objectManager->find($metadata->name, $identifierList);
if (null === $object) {
switch ($this->fallbackStrategy) {
case self::ON_MISSING_NULL:
return null;
case self::ON_MISSING_EXCEPTION:
throw new ObjectConstructionException(sprintf('Entity %s can not be found', $metadata->name));
case self::ON_MISSING_FALLBACK:
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
default:
throw new InvalidArgumentException('The provided fallback strategy for the object constructor is not valid');
}
}
$objectManager->initializeObject($object);
return $object;
}
}

symfony no supporting normalizer found while normalizing an entity

I'm working on symfony 4.1.
I defined two normalizer in my service.yml.
api.tone_normalizer:
class: App\Serializer\Normalizer\JnToneNormalizer
tags: [serializer.normalizer]
and
api.wskeytone_normalizer:
class: App\Serializer\Normalizer\ApiWsKeyToneToneNormalizer
tags: [serializer.normalizer]
Here the first normalizer. Is aware about JnTone entities.
<?php
namespace App\Serializer\Normalizer;
use App\Entity\JnTone;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* JnTone normalizer
*/
class JnToneNormalizer implements NormalizerInterface
{
/**
* {#inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return [
'id' => $object->getId(),
'name' => $object->getName(),
];
}
/**
* {#inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof JnTone;
}
}
And the normalizer where I want to call the first one. rootTone is an instance of JnTone entity so I want to call my JnTone normalizer.
<?php
namespace App\Serializer\Normalizer;
use App\Entity\JnWsKey;
use App\Entity\JnTone;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\SerializerAwareNormalizer;
use Symfony\Component\Serializer\SerializerAwareTrait;
use Symfony\Component\Serializer\SerializerAwareInterface;
/**
* JnWsKey normalizer
*/
class ApiWsKeyNormalizer implements NormalizerInterface, SerializerAwareInterface
{
use NormalizerAwareTrait;
use SerializerAwareTrait;
private $tones;
/**
* {#inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
return [
'id'=>$object->getId(),
'name'=>$object->getName(),
'rootTone'=>$this->serializer->normalize($object->getRootTone(),$format,$context)
];
}
/**
* {#inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof JnWsKey ;
}
}
I can't get this working. The first normalizer isn't find
Could not normalize object of type App\Entity\JnTone, no supporting normalizer found.
What I am doing wrong?
I just did not realize that I have to declare all needed normailizer in the serializer definition. I solved it doing:
$encoder = new JsonEncoder();
$serializer = new Serializer(array(
new JnToneNormalizer(),
new JnWsKeyToneNormalizer()
), array($encoder));
Symfony seems to have ObjectNormalizer. I think you may take advantage of it. Check the installation and usage. I think there is also a way to perform serialization of complex nested objects using annotations and groups.

deserializing json to embedded object in Symfony 3.2 - Expected argument of type "Acme\StoreBundle\Document\Gps", "array" given

Acme\StoreBundle\Document\Person
/**
* #MongoDB\Document
*/
class Person
{
/**
* #MongoDB\bool
*/
private $hasemail;
/**
* #MongoDB\EmbedOne(targetDocument="Gps")
*/
private $gps;
/**
* #MongoDB\Field(name="email", type="collection")
*/
private $email;
}
...
Acme\StoreBundle\Document\Gps
/**
* #MongoDB\EmbeddedDocument
*/
class Gps
{
/**
* #MongoDB\Field(type="float")
*/
private $latitude;
/**
* #MongoDB\Field(type="float")
*/
private $longitude;
}
...
mongo json document
{
"hasemail": true,
"gps": {
"latitude": 42.941579990394,
"longitude": -85.244641161525
},
"email": [
"sdfgsdfg#sfgsdfg.org",
"sdfgsdfg#fgsdfg.com",
"sdfgsdfg#sdfgsdfg.com"
]
}
serializing mongo document to json works perfect. but deserializing json to document throws an error: "Expected argument of type 'Acme\StoreBundle\Document\Gps', 'array' given"
deserialize code:
$post = $request->getContent();
$serializer = $this->get('serializer');
$person = $serializer->deserialize($post, Person::class, 'json');
so finally I used ReflectionExtractor and it worked like charm. No custom denormalizers necessary.
http://symfony.com/doc/current/components/serializer.html#recursive-denormalization-and-type-safety
$post = $request->getContent();
$normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); //
$serializer = new Serializer(array($normalizer, new DateTimeNormalizer(\DateTime::ISO8601), new ObjectNormalizer()), [new JsonEncoder()]);
$person = $serializer->deserialize($post, Person::class, 'json');
$dm = $this->get('doctrine_mongodb')->getManager();
$dm->persist($person);
$dm->flush();
return new Response('Created id ' . $person->getId());
Your $post is likely an array and not a raw JSON object as you're expecting.
[{
key: value
}]
is different from..
{
key: value
}
Separately, there looks to be a separate issue based on your error message and the example code you have here.
Acme\StoreBundle\Document\Gps is the expected class, while deserializing Voter::class so you may want to check your namespaces and class names as well.

Resources