I have jms serializer and in my class entity have annotation. I have case when I need apply jms serializer annotation only in some case. How to do this ? First I think need move annotation from entity class to uml maybe and create some handler where I can enable this annotation for this entity, in the rest of the time jms are not applicable to entity
my entity
/**
* #Annotation\ExclusionPolicy("all")
*/
class Application implements ContainsRecordedMessages
{
/**
* #var int
*
* #Annotation\Groups({
* "get_application"
* })
* #Annotation\Expose()
*/
private $id;
/**
* #var int
*
* #Annotation\Groups({
* "post_application"
* })
* #Annotation\SerializedName("company_id")
* #Annotation\Expose()
*/
private $companyId;
/**
* #var string
*
* #Annotation\Groups({
* "post_application"
* })
* #Annotation\SerializedName("channel_sale")
* #Annotation\Expose()
*/
private $channelSale;
and I have class manager for this entity where I want enable jms and the disable jms serializer annotation
class ApplicationManager
{
public function someFunction()
{
$this->enableJmsForEntity(Application::class);
//some logic
//
$this->disableJmsForEntity(Application::class);
}
}
my goal - that jms serializer don't work in serializer proccess for response. Only in my service, wehre I create entity with deserializer function, jms serializer annotation for entity enable. Because in this old project all respons look like this
return $this->json($application, Response::HTTP_CREATED);
vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php:114
protected function json($data, $status = 200, $headers = array(), $context = array())
{
if ($this->container->has('serializer')) {
$json = $this->container->get('serializer')->serialize($data, 'json', array_merge(array(
'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS,
), $context));
return new JsonResponse($json, $status, $headers, true);
}
return new JsonResponse($data, $status, $headers);
}
and after adding jms annotation we have problem, response in changing ..
Related
Question:
Why does my response return "blank" when I set the setCircularReferenceHandler callback?
EDIT:
Would appear that it returns nothing, but does set the header to 500 Internal Server Error. This is confusing as Symfony should send some kind of error response concerning the error?
I wrapped $json = $serializer->serialize($data, 'json'); in a try/catch but no explicit error is thrown so nothing is caught. Any ideas would be really helpful.
Context:
When querying for an Entity Media I get a blank response. Entity Media is mapped (with Doctrine) to Entity Author. As they are linked, indefinite loops can occur when trying to serialize.
I had hoped that using the Circular Reference Handler I could avoid just that, but it's not working.
Error:
This is the error I'm getting when I'm NOT setting the Circular Reference Handler:
A circular reference has been detected when serializing the object of class "Proxies__CG__\AppBundle\Entity\Author\Author" (configured limit: 1) (500 Internal Server Error)
Now this error is completely expected, as my Entity Author points back to the Entity Media when originally querying for a Media ( Media -> Author -> Media )
class Author implements JsonSerializable {
//Properties, Getters and setters here
/**
* Specify data which should be serialized to JSON
* #link http://php.net/manual/en/jsonserializable.jsonserialize.php
* #return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* #since 5.4.0
*/
function jsonSerialize()
{
return [
"title" => $this->getTitle(),
"id" => $this->getId(),
"firstname" => $this->getFirstname(),
"lastname" => $this->getLastname(),
//This is the problem right here. Circular reference.
"medias" => $this->getAuthorsMedia()->map(function($object){
return $object->getMedia();
})
];
}
}
What I've tried:
My Entities implement JsonSerializable interface so I define what attributes are returned (Which is what JsonSerializeNormalizer requires). This works completely when I remove the "medias" property in the Author's class, everything works.
Here is how I use my serliazer with my normalizer.
/**
* #Route("/media")
* Class MediaController
* #package BackBundle\Controller\Media
*/
class MediaController extends Controller
{
/**
* #Route("")
* #Method({"GET"})
*/
public function listAction(){
/** #var MediaService $mediaS */
$mediaS= $this->get("app.media");
/** #var array $data */
$data = $mediaS->getAll();
$normalizer = new JsonSerializableNormalizer();
$normalizer->setCircularReferenceLimit(1);
$normalizer->setCircularReferenceHandler(function($object){
return $object->getId();
});
$serializer = new Serializer([$normalizer], [new JsonEncoder()]);
$json = $serializer->serialize($data, 'json');
return new Response($json);
}
}
Github issue opened
I tried to reproduce your error, and for me everything worked as expected (see code samples below).
So, your setCircularReferenceHandler() works fine.
Maybe try to run my code, and update it with your real entities and data sources step by step, until you see what causes the error.
Test (instead of your controller):
class SerializerTest extends \PHPUnit\Framework\TestCase
{
public function testIndex()
{
$media = new Media();
$author = new Author();
$media->setAuthor($author);
$author->addMedia($media);
$data = [$media];
$normalizer = new JsonSerializableNormalizer();
$normalizer->setCircularReferenceLimit(1);
$normalizer->setCircularReferenceHandler(function($object){
/** #var Media $object */
return $object->getId();
});
$serializer = new Serializer([$normalizer], [new JsonEncoder()]);
$json = $serializer->serialize($data, 'json');
$this->assertJson($json);
$this->assertCount(1, json_decode($json));
}
}
Media entity
class Media implements \JsonSerializable
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var Author
*
* #ORM\ManyToOne(targetEntity="Author", inversedBy="medias")
* #ORM\JoinColumn(name="author_id", referencedColumnName="id")
*/
private $author;
/**
* {#inheritdoc}
*/
function jsonSerialize()
{
return [
"id" => $this->getId(),
"author" => $this->getAuthor(),
];
}
//todo: here getter and setters, generated by doctrine
}
Author entity
class Author implements \JsonSerializable
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var Media[]
*
* #ORM\OneToMany(targetEntity="Media", mappedBy="author")
*/
private $medias;
/**
* {#inheritdoc}
*/
function jsonSerialize()
{
return [
"id" => $this->getId(),
"medias" => $this->getMedias(),
];
}
//todo: here getter and setters, generated by doctrine
}
I am using RabbitMQBundle in a Symfony 2.8 project, and I would like to use a custom producer class which persists an entity (Message) in database before publishing the RabbitMQ message.
I defined the custom producer class in config.yml:
old_sound_rabbit_mq:
...
producers:
myproducer:
class: AppBundle\Services\GenericProducer
connection: default
exchange_options: {name: 'my_exchange', type: direct}
And the custom Producer class:
<?php
namespace AppBundle\Services;
use AppBundle\Entity\Message;
use OldSound\RabbitMqBundle\RabbitMq\Producer;
/**
* Customised Producer, that publishes AMQP Messages
* but also:
* - writes an entry in the Message table
*/
class GenericProducer extends Producer
{
/**
* Entity Manager
*/
protected $em;
public function setEntityManager($entityManager)
{
$this->em = $entityManager;
return $this;
}
/**
* Publishes the message and merges additional properties with basic properties
* And also:
* - writes an entry in the Message table
*
* #param string $action
* #param array $parameters
* #param string $routingKey
* #param array $additionalProperties
* #param array|null $headers
*/
public function publish($action, $parameters = array() , $routingKey = '', $additionalProperties = array(), array $headers = null)
{
$message = new Message();
$message->setAction($action)
->setParameters($parameters);
$this->em->persist($message);
$this->em->flush();
$msgBody = array(
'action' => $action,
'parameters' => $parameters
);
parent::publish($msgBody, $routingKey, $additionalProperties, $headers);
}
}
How can I make a call to GenericProducer->setEntityManager, as the producer is not defined in services.yml, like other services ?
Is there another way to achieve this ?
Thanks for your time.
the producer service definition is generated dynamically by the bundle in the Dependency Injection Extension of the bundle.
You can either try to decorate the existing service or create a compiler pass where you fetch the existing service and extend it by calling the setEntityManager function.
Following #lordrhodos suggestion, I decorated the producer service generated by RabbitMQBundle. Here is the complete code:
config.yml (nothing special to do):
old_sound_rabbit_mq:
...
producers:
myproducer:
connection: default
exchange_options: {name: 'my_exchange', type: direct}
services.yml (here you define a decorating service):
app.decorating_myproducer_producer:
class: AppBundle\Services\GenericProducer
decorates: old_sound_rabbit_mq.myproducer_producer
arguments: ['#
app.decorating_myproducer_producer.inner', '#doctrine.orm.entity_manager']
public: false
decorator class:
<?php
namespace AppBundle\Services;
use AppBundle\Entity\Message;
use OldSound\RabbitMqBundle\RabbitMq\Producer;
/**
* Customised Producer, that publishes AMQP Messages
* but also:
* - writes an entry in the Message table
*/
class GenericProducer extends Producer
{
/**
* #var Producer
*/
protected $producer;
/**
* #var EntityManager
*/
protected $em;
/**
* GenericProducer constructor.
* #param Producer $producer
* #param EntityManager $entityManager
*/
public function __construct(Producer $producer, EntityManager $entityManager)
{
$this->producer = $producer;
$this->em = $entityManager;
}
/**
* Publishes the message and merges additional properties with basic properties
* And also:
* - writes an entry in the Message table
*
* #param string $action
* #param array $parameters
* #param string $routingKey
* #param array $additionalProperties
* #param array|null $headers
*/
public function publish($action, $parameters = array() , $routingKey = '', $additionalProperties = array(), array $headers = null)
{
$message = new Message();
$message->setAction($action)
->setParameters($parameters);
$this->em->persist($message);
$this->em->flush();
$msgBody = array(
'action' => $action,
'parameters' => $parameters
);
$this->producer->publish(serialize($msgBody), $routingKey, $additionalProperties, $headers);
}
}
Finally, call the original producer from a controller:
$this->get('old_sound_rabbit_mq.myproducer_producer')->publish('wait', ['time' => 30]);
I tried map data from a given array to an object with the jms serializer (in a unit test) to test a doctrine entity:
Given is a simple entity class:
/**
* CashPosition
*/
class CashPosition
{
/**
* #var integer
*/
protected $cashPositionId;
/**
* #var \DateTime
*/
protected $date;
/**
* #var float
*/
protected $value;
/**
* Get cashPositionId
*
* #return integer
*/
public function getCashPositionId()
{
return $this->cashPositionId;
}
/**
* Set date
*
* #param \DateTime $date
*
* #return $this
*/
public function setDate($date)
{
$this->date = $date;
return $this;
}
/**
* Get date
*
* #return \DateTime
*/
public function getDate()
{
return $this->date;
}
/**
* Set value
*
* #param string $value
*
* #return $this
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
/**
* Get value
*
* #return float
*/
public function getValue()
{
return $this->value;
}
}
I defined the serialization under Resources\config\serializer\Entity.CashPosition.yml
MyBundle\Entity\CashPosition:
exclusion_policy: ALL
access_type: public_method
properties:
cashPositionId:
exclude: false
expose: true
type: integer
access_type: property
date:
exclude: false
expose: true
type: DateTime<'Y-m-d'>
value:
exclude: false
expose: true
type: float
And tried to cover this with a serialization test:
public function testSerialization()
{
$data = [
'cashPositionId' => 1,
'date' => date('Y-m-d'),
'value' => 1.0,
];
/* #var $serializer Serializer */
$serializer = $this->container->get('serializer');
$cashPosition = $serializer->fromArray($data, CashPosition::class);
$this->assertInstanceOf(CashPosition::class, $cashPosition);
$this->assertEquals($data, $serializer->toArray($cashPosition));
}
But the test fails since the fromArray method does not set the cashPositionId. I tried some different configurations with the access type but had no luck. I'm not sure what's the problem here.
I'm using the following version of jms serializer:
jms/metadata 1.6.0 Class/method/property metadata management in PHP
jms/parser-lib 1.0.0 A library for easily creating recursive-descent parsers.
jms/serializer 1.6.2 Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.
jms/serializer-bundle 1.4.0 Allows you to easily serialize, and deserialize data of any complexity
Hello i think you miss the serialized_name property for cashPositionId, by default jms will translate properties from camel case to snake case.
JMS Doc
When I deserialize my doctrine entity, the initial object is constructed/initiated correctly, however all child relations are trying to be called as arrays.
The root level object's addChild(ChildEntity $entity) method is being called, but Symfony is throwing an error that addChild is receiving an array and not an instance of ChildEntity.
Does Symfony's own serializer have a way to deserialize nested arrays (child entities) to the entity type?
JMS Serializer handles this by specifying a #Type("ArrayCollection<ChildEntity>") annotation on the property.
I believe the Symfony serializer attempts to be minimal compared to the JMS Serializer, so you might have to implement your own denormalizer for the class. You can see how the section on adding normalizers.
There may be an easier way, but so far with Symfony I am using Discriminator interface annotation and type property for array of Objects. It can also handle multiple types in one array (MongoDB):
namespace App\Model;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/**
* #DiscriminatorMap(typeProperty="type", mapping={
* "text"="App\Model\BlogContentTextModel",
* "code"="App\Model\BlogContentCodeModel"
* })
*/
interface BlogContentInterface
{
/**
* #return string
*/
public function getType(): string;
}
and parent object will need to define property as interface and get, add, remove methods:
/**
* #var BlogContentInterface[]
*/
protected $contents = [];
/**
* #return BlogContentInterface[]
*/
public function getContents(): array
{
return $this->contents;
}
/**
* #param BlogContentInterface[] $contents
*/
public function setContents($contents): void
{
$this->contents = $contents;
}
/**
* #param BlogContentInterface $content
*/
public function addContent(BlogContentInterface $content): void
{
$this->contents[] = $content;
}
/**
* #param BlogContentInterface $content
*/
public function removeContent(BlogContentInterface $content): void
{
$index = array_search($content, $this->contents);
if ($index !== false) {
unset($this->contents[$index]);
}
}
I'm using couchDb in symfony 2.7.2.
I have several doubts.
Now I installed this Bundle
And I create one entity for testing
<?php
namespace foo\GarageBundle\Document;
use Doctrine\ODM\CouchDB\Mapping\Annotations as CouchDB;
/**
* #CouchDB\Document
*/
class Utente
{
/** #CouchDB\Id */
private $id;
/** #CouchDB\Field(type="string") */
private $nome;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set nome
*
* #param string $nome
* #return Utente
*/
public function setNome($nome)
{
$this->nome = $nome;
return $this;
}
/**
* Get nome
*
* #return string
*/
public function getNome()
{
return $this->nome;
}
}
In my controller I added this Code
$dm = $this->container->get('doctrine_couchdb.client.default_connection');
$doc = $this->container->get('doctrine_couchdb.odm.default_document_manager');
try{
$dm->createDatabase($dm->getDatabase());
}catch(\Exception $e){
$msg = $e->getMessage();
}
$user = new Utente();
$user->setNome('foo');
$doc->persist($user);
$doc->flush();
my config.yml is
doctrine_couch_db:
client:
default_connection: default
connections:
default:
dbname: symfony2
odm:
default_document_manager: default
document_managers:
default:
auto_mapping: true
With controller I created Database but I can't insert the new Document, I got this error
The class 'foo\GarageBundle\Document\Utente' was not found in the chain configured namespaces
And I don't understand why it is useful to use a bundle as what I am using ( I know it could be a stupid question ), and why I have to use * #CouchDB\Document instead of #Document inside my entity ?
Seems a problem related the namespace of the entity class.
The automapping is registering the CouchDocument subnamespace of
your bundle, not Document (which is auto-mapped by
DoctrineMongoDBBundle)
So use a different namespace for the User class and the other Counch you use, as follow:
namespace foo\GarageBundle\CouchDocument;
In particular:
<?php
namespace foo\GarageBundle\CouchDocument;
use Doctrine\ODM\CouchDB\Mapping\Annotations as CouchDB;
/**
* #CouchDB\Document
*/
class Utente
{
Hope this help
See this discussion on github.
/**
* #CouchDB\Document
* #CouchDB\Index
*/
class Utente
{