Symfony 5 Object Serialization with ManyToMany Relation Times Out - symfony

In my Symfony 5 application, I have an entity class Product which has two properties $categories and $bundles. The product class has a ManyToMany relation with both the properties. When I comment out either one of the properties the Product serialization works perfectly. But incase both properties are present the serialization times out.
The code excerpt from Product class.
class Product
{
/**
* #ORM\ManyToMany(targetEntity=ProductBundle::class, mappedBy="products")
*/
private $productBundles;
/**
* #ORM\ManyToMany(targetEntity=Category::class, mappedBy="products")
* #MaxDepth(1)
*/
private $categories;
}
The code for the serialization is below.
$products = $productRepository->findBySearchQuery($name);
$productsJson = $serializerInterface->serialize($products, 'json', [
ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
return $object->getId();
}
]);
I have tried using the #ORM/JoinTable annotation suggested on some other Stackoverflow answers and #MaxDepth as well but no luck. The code works if any of the properties are commented out. Would be grateful for any advice on this.

okay, 20 products is actually not much. so I guess you're outputting the same objects over and over again if you let the relations be serialized unhindered.
I actually don't know how to achieve this reliably with the serializer. But the standard ways would just be enough probably. I like serializing via the JsonSerializable interface on your entities like this (omitting the ORM stuff for brevity):
class Product implements \JsonSerializable {
public $name;
public $categories; // relation
// getters + setters omitted
// this implements \JsonSerializable
public function jsonSerialize() {
return [
'name' => $this->name,
'categories' => array_map(function($category) {
return $category->jsonSerializeChild();
}, $this->categories),
];
}
// this function effectively stops recursion by leaving out relations
public function jsonSerializeChild() {
return [
'name' => $this->name,
];
}
}
If you implement this on all your entities you can very effectively limit the depth of serialization to two (i.e. the "base" entities and their connected entities).
also, the symfony serializer will use the JsonSerializable interface if it's defined if your serializing to JSON. Obviously, this is not as elegant as some fancy annotation-based serialization or a "smart" serializer, that actually manages to stop ... but it'll probably work better...

Pointed out by #Jakumi the serializer was looping over and over the object properties $categories and $bundles. I avoided that by using the Serialization groups.
The product class
class Product
{
/**
* #ORM\ManyToMany(targetEntity=ProductBundle::class, mappedBy="products")
* #Groups("product_listing:read")
*/
private $productBundles;
/**
* #ORM\ManyToMany(targetEntity=Category::class, mappedBy="products")
* #Groups("product_listing:read")
*/
private $categories;
}
The category class
class Category
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups("product_listing:read")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Groups("product_listing:read")
*/
private $name;
}
The call to serializer
$products = $productRepository->findBySearchQuery($name);
$productsJson = $serializerInterface->serialize($products, 'json', ['groups' => 'product_listing:read']);
I hope this helps someone in future.

Related

Symfony/Doctrine Infinite recursion while fetching from database with inverted side of a ManyToOne relationship

Context
In a simple Symfony project, I've created two entities, Product and Category, which are related by a #ManyToOne and a #OneToMany relationship with Doctrine Annotations. One category can have multiple products and one product relates to one category. I've manually inserted data in the Category table.
When I fetch data using Category entity repository and I display it with a var_dump(...), an infinite recursion happens. When I return a JSON response with these data, it is just empty. It should retrieve exactly the data I inserted manually.
Do you have any idea of how to avoid this error without removing the inverse side relationship in the Category entity?
What I've tried
Adding the Doctrine Annotation fetch="LAZY" in one side, the other side and both side of the relationship.
Inserting Category object in the database using Doctrine to see if the database connection is working. Yes it is.
Removing the inverse side of the relationship. It worked but it's not what I want.
Code snippet
Controller
dummy/src/Controller/DefaultController.php
...
$entityManager = $this->getDoctrine()->getManager();
$repository = $entityManager->getRepository(Category::class);
// ===== PROBLEM HERE =====
//var_dump($repository->findOneByName('house'));
//return $this->json($repository->findOneByName('house'));
...
Entities
dummy/src/Entity/Category.php
<?php
namespace App\Entity;
use App\Repository\CategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=CategoryRepository::class)
*/
class Category
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="id", type="integer")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity=Product::class, mappedBy="category", fetch="LAZY")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* #return Collection|Product[]
*/
public function getProducts(): Collection
{
return $this->products;
}
public function addProduct(Product $product): self
{
if (!$this->products->contains($product)) {
$this->products[] = $product;
$product->setCategory($this);
}
return $this;
}
public function removeProduct(Product $product): self
{
if ($this->products->contains($product)) {
$this->products->removeElement($product);
// set the owning side to null (unless already changed)
if ($product->getCategory() === $this) {
$product->setCategory(null);
}
}
return $this;
}
}
dummy/src/Entity/Product.php
<?php
namespace App\Entity;
use App\Repository\ProductRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=ProductRepository::class)
*/
class Product
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(name="id", type="integer")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity=Category::class, inversedBy="products", fetch="LAZY")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id")
*/
private $category;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
}
I assume you use var_dump for debugging purposes. For debugging purposes use dump or dd which is from symfony/debug and should already be enabled on dev by default. Both dump and dd should abort the infinite recursion in time. (Lots of symfony/doctrine objects/services have circular references or just a lot of referenced objects.) dump adds the given php var(s) to either the profiler (target mark symbol in the profiler bar) or to the output. dd adds the given var(s) like dump but also ends the process (so dump and die). - On production never use dump/dd/var_dump, but properly serialize your data.
Secondly, $this->json is essentially a shortcut for packing json_encode into a JsonResponse object (or use the symfony/serializer instead). json_encode on the other hand serializes public properties of the object(s) given unless the object(s) implement JsonSerializable (see below). Since almost all entities usually have all their properties private, the result is usually an empty object(s) serialization.
There are a multitude of options to choose from, but essentially you need to solve the problem of infinite recursion. The imho standard options are:
using the symfony serializer which can handle circular references (which cause the infinite recursion/loop) and thus turning the object into a safe array. However, the results may still not be to your liking...
implementing JsonSerializable on your entity and carefully avoid recursively adding the child-objects.
building a safe array yourself from the object, to pass to $this->json ("the manual approach").
A safe array in this context is one, that contains only strings, numbers and (nested) arrays of strings and numbers, which essentially means, losing all actual objects.
There are probably other options, but I find these the most convenient ones. I usually prefer the JsonSerializable option, but it's a matter of taste. One example for this would be:
class Category implements \JsonSerializable { // <-- new implements!
// ... your entity stuff
public function jsonSerialize() {
return [
'id' => $this->id,
'name' => $this->name,
'products' => $this->products->map(function(Product $product) {
return [
'id' => $product->getId(),
'name' => $product->getName(),
// purposefully excluding category here!
];
})->toArray(),
];
}
}
After adding this your code should just work. For dev, you always should use dump as mentioned and all $this->json will just work. That's why I usually prefer this option. However, the caveat: You only can have one json serialization scheme for categories this way. For any additional ways, you would have to use other options then ... which is almost always true anyway.

Construct object during deserialization on JMS Serializer

I try to load object from database (Symfony, Doctrine) during deserialization using JMS Serializer. Lets say that I have a simple football api application, two entities Team and Game, teams with id 45 and 46 are already in db.
When creating a new game from json:
{
"teamHost": 45,
"teamGues": 46,
"scoreHost": 54,
"scoreGuest": 42,
}
Game entity:
class Game {
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Team")
* #ORM\JoinColumn(nullable=false)
*/
private $teamHost;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Team")
* #ORM\JoinColumn(nullable=false)
*/
private $teamGuest;
I would like to create a Game object that has already loaded teams from the database.
$game = $this->serializer->deserialize($requestBody, \App\Entity\Game::class, 'json');
Looking for a solution I found something like jms_serializer.doctrine_object_constructor but there are no specific examples in the documentation.
Are you able to help me with the creation of an object from the database during deserialization?
You need to create a custom handler:
https://jmsyst.com/libs/serializer/master/handlers
A simple example:
<?php
namespace App\Serializer\Handler;
use App\Entity\Team;
use Doctrine\ORM\EntityManagerInterface;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonDeserializationVisitor;
class TeamHandler implements SubscribingHandlerInterface
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public static function getSubscribingMethods()
{
return [
[
'direction' => GraphNavigator::DIRECTION_DESERIALIZATION,
'format' => 'json',
'type' => Team::class,
'method' => 'deserializeTeam',
],
];
}
public function deserializeTeam(JsonDeserializationVisitor $visitor, $id, array $type, Context $context)
{
return $this->em->getRepository(Team::class)->find($id);
}
}
Altough I would recommend universal approach to handle any entity you want by a single handler.
Example: https://gist.github.com/Glifery/f035e698b5e3a99f11b5
Also, this question has been asked before:
JMSSerializer deserialize entity by id

cascade persist option doesnt work on ajax submit(api)

I'm working with symfony 3.4.6 and fosrestbundle. I have three entities related as follow:
Embarque class
class Embarque{
//...
/**
* #var EmbarqueContenedor[]|ArrayCollection
*
* #Serializer\SerializedName("contenedores")
* #ORM\OneToMany(targetEntity="AppBundle\Entity\EmbarqueContenedor",mappedBy="embarque",cascade={"persist"})
*/
private $contenedores;
public function addEmbarqueContenedor($embarqueContenedor)
{
if (!$this->contenedores->contains($embarqueContenedor)) {
$this->contenedores->add($embarqueContenedor);
//$embarqueContenedor->setEmbarque($this);
}
}
public function removeEmbarqueContenedor($embarqueContenedor)
{
if ($this->contenedores->contains($embarqueContenedor)) {
$this->contenedores->removeElement($embarqueContenedor);
}
}
}
EmbarqueContenedor class
class EmbarqueContenedor{
/**
* #var Embarque
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Embarque",inversedBy="contenedores",)
*/
private $embarque;
/**
* #var Contenedor
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Contenedor",inversedBy="embarques")
*/
private $contenedor;
}
Contenedor class
class Contenedor{
/**
* #var EmbarqueContenedor[]|ArrayCollection
*
* #Serializer\SerializedName("contenedorEmbarques")
* #ORM\OneToMany(targetEntity="AppBundle\Entity\EmbarqueContenedor",mappedBy="contenedor")
*/
private $embarques;
public function addEmbarqueContenedor($embarqueContenedor)
{
if (!$this->embarques->contains($embarqueContenedor)) {
$this->embarques->add($embarqueContenedor);
$embarqueContenedor->setContenedor($this);
}
}
public function removeEmbarqueContenedor($embarqueContenedor)
{
if ($this->embarques->contains($embarqueContenedor)) {
$this->embarques->removeElement($embarqueContenedor);
}
}
}
in forms is as follow
class EmbarqueType{
$builder->add('contenedores', CollectionType::class, [
'entry_type' => EmbarqueContenedorType::class,
'allow_add' => true,
]);
}
class EmbarqueContenedorType{
$builder->add('contenedor', EntityType::class, [
'class' => Contenedor::class,
])
}
The entity contenedor is create apart and selected in EmbarqueContenedorType when adding or editing, the EmbarqueContenedorEntity is created from EmbarqueType.
The problem is that the records are persisted in the database but with out any reference. The EmbarqueContenedor table has no reference for the Embarque or Contenedor tables.
There is no error because the data is persisted but not referenced. How could this be??
Thanks in advance!
Edit
I noticed that I was not serializing the Id property of Contenedor Entity so is imposible to make the reference, now is fixed but the Embarque entity still not being referenced.
I think the problem is in the design of the tables relationships. The table/entity EmbarqueContenedor is unnecessary. When you have a many-to-many relation just say that to the Doctrine and the Doctrine takes care of the rest (Doctrine will create all necessary tables).
So the solution should be to define your relations with ManyToMany annotation.

Symfony - FOSRestBundle - show selected fields

I'm trying to show only selected fields in my REST action in controller.
I've found one solution - I can set groups in Entities/Models and select this group in annotation above action in my Controller.
But actually i don't want use groups, i want determine which fields i wanna expose.
I see one solution - I can create one group for every field in my Entities/Model. Like this:
class User
{
/**
* #var integer
*
* #Groups({"entity_user_id"})
*/
protected $id;
/**
* #var string
*
* #Groups({"entity_user_firstName"})
*/
protected $firstName;
/**
* #var string
*
* #Groups({"entity_user_lastName"})
*/
protected $lastName;
}
And then i can list fields above controller action.
My questions are:
Can I use better solution for this?
Can I list all groups? Like I can list all routes or all services.
This is mainly about serialization not about fosrestbundle itself.
The right way would be to create your own fieldserialization strategy.
This article got it down really nicely:
http://jolicode.com/blog/how-to-implement-your-own-fields-inclusion-rules-with-jms-serializer
It build a custom exclusion strategy as describeted here:
How do I create a custom exclusion strategy for JMS Serializer that allows me to make run-time decisions about whether to include a particular field?
Example code from first link for reference:
custom FieldExclusion strategy:
namespace Acme\Bundle\ApiBundle\Serializer\Exclusion;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;
class FieldsListExclusionStrategy implements ExclusionStrategyInterface
{
private $fields = array();
public function __construct(array $fields)
{
$this->fields = $fields;
}
/**
* {#inheritDoc}
*/
public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext)
{
return false;
}
/**
* {#inheritDoc}
*/
public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext)
{
if (empty($this->fields)) {
return false;
}
$name = $property->serializedName ?: $property->name;
return !in_array($name, $this->fields);
}
}
Interface
interface ExclusionStrategyInterface
{
public function shouldSkipClass(ClassMetadata $metadata, Context $context);
public function shouldSkipProperty(PropertyMetadata $property, Context $context);
}
usage
in controller or where you need it:
$context = new SerializationContext();
$fieldList = ['id', 'title']; // fields to return
$context->addExclusionStrategy(
new FieldsListExclusionStrategy($fieldList)
);
// serialization
$serializer->serialize(new Pony(), 'json', $context);
You should be also able to mix and match with groups eg. you can also set $content->setGroups(['myGroup']) together with the fieldExclusio

Get entities from a unidirectional many to many relation with Doctrine2 and Symfony2

I'm currently working on a language assessment project which enables you to take an exam in the language you want and evaluate your level. I use Symfony2 framework and work with Doctrine2 as well. My issue is the following one:
I have two entities Exam and Question linked by a Many-To-Many relation (Exam being the owner). Each exam can be related to several questions, and each question can be related to several exams.
Here is my code:
Exam entity
/**
* Exam
*
* #ORM\Table(name="cids_exam")
* #ORM\Entity(repositoryClass="LA\AdminBundle\Entity\ExamRepository")
*/
class Exam
{
...
/**
* #ORM\ManyToMany(targetEntity="LA\AdminBundle\Entity\Question", cascade={"persist"})
* #ORM\JoinTable(name="cids_exam_question")
*/
private $questions;
...
/**
* Constructor
*/
public function __construct()
{
$this->questions = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add questions
*
* #param \LA\AdminBundle\Entity\Question $questions
* #return Exam
*/
public function addQuestion(\LA\AdminBundle\Entity\Question $questions)
{
$this->questions[] = $questions;
return $this;
}
/**
* Remove questions
*
* #param \LA\AdminBundle\Entity\Question $questions
*/
public function removeQuestion(\LA\AdminBundle\Entity\Question $questions)
{
$this->questions->removeElement($questions);
}
/**
* Get questions
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getQuestions()
{
return $this->questions;
}
}
As long as it is a unidirectional relation, there is no 'exams' attribute in my Question class.
Now, what I want to do is getting all the questions related to a specific exam, calling the getQuestions() method, like this:
$questions = $exam->getQuestions();
But this method returns an empty array, even if I have data in my database. If I var_dump the $exam variable, I can see the questions array is empty:
object(LA\AdminBundle\Entity\Exam)[47]
private 'id' => int 5
...
private 'questions' =>
object(Doctrine\ORM\PersistentCollection)[248]
private 'snapshot' =>
array (size=0)
empty
private 'owner' => null
private 'association' => null
private 'em' => null
private 'backRefFieldName' => null
private 'typeClass' => null
private 'isDirty' => boolean false
private 'initialized' => boolean false
private 'coll' =>
object(Doctrine\Common\Collections\ArrayCollection)[249]
private '_elements' =>
array (size=0)
...
I think I could maybe write a findByExam() function in my QuestionRepository, but I don't really know how to implement the joins in this case.
Any help would be great!
To have a findByExam() method in your QuestionRepository do the following:
public function findByExam($exam)
{
$q = $this->createQueryBuilder('q')
->where('q.exam = :exam')
->setParameter('exam', $exam)
->getQuery();
return $q->getResult();
}
You could also create a bi-directional relationship not uni-directional !
Each exam can be related to several questions, and each question can
be related to several exams.
Create a bi-directional relationship by adding this to your Question entity:
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Vendor\YourExamBundle\Entity\ExamInterface;
class Question
{
protected $exams;
public function __construct()
{
$this->exams = new ArrayCollection();
}
public function getExams()
{
return $this->exams;
}
public function addExam(ExamInterface $exam)
{
if !($this->exams->contains($exam)) {
$this->exams->add($exam);
}
return $this;
}
public function setExams(Collection $exams)
{
$this->exams = $exams;
return $this;
}
// ...
Afterwards you can use...
$question->getExams()
... in your controller.
To automatically join your related entities doctrine's fetch option can be used with:
LAZY ( loads the relations when accessed )
EAGER ( auto-joins the relations )
EXTRA_LAZY ( manual fetching )
example:
/**
* #ManyToMany(targetEntity="Question",inversedBy="exams", cascade={"all"}, fetch="EAGER")
*/
Though eager loading has a downside in terms of performance it might be an option for you.
Doctrine Fetch with EAGER
Whenever you query for an entity that has persistent associations and
these associations are mapped as EAGER, they will automatically be
loaded together with the entity being queried and is thus immediately
available to your application.
Read more about it in the Doctrine Documentation.
Another option you should check when working with relations is the cascade option.
See the Doctrine - Working with Associations chapter of the documentation.
Tip:
You should create interfaces for exams and questions and use them instead of the original entity in your set and add methods to allow easier extending.
Bi-Directional Relations using Doctrine2 ORM with association table exam_questions
exam_id question_id
<?php
class Exams
....OTHER PROPERTIES...
/**
* Owning Side
*
* #ManyToMany(targetEntity="Questions", inversedBy="exams")
* #JoinTable(name="exam_questions",
* joinColumns={#JoinColumn(name="exam_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="question_id", referencedColumnName="id")}
* )
*/
private $questions;
..OTHER CODES..
}
class Questions{
..OTHER CODES..
/**
* Inverse Side
*
* #ManyToMany(targetEntity="Exams", mappedBy="questions")
*/
private $exams;
..OTHER CODES..
}
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#annref-manytomany

Resources