Symfony 3 Doctrine 2: Circular reference on relations - php

I'm trying to get working 4 entities in Symfony 3 with Doctrine 2 but I'm stuck on a circular reference exception when I want to serialize an Account entity for example:
A circular reference has been detected (configured limit: 1).
I chose bi-directional relations in my entities and schema is like this:
- Account [1] ---- [0..*] AccountSheet
- AccountSheet [1] ---- [0..*] Operation
- Operation [0..*] ---- [1] Category
Here are entities (with some cleanings for clarity):
src\AppBundle\Entity\Account.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\AbstractGenericEntity;
/**
* #ORM\Entity()
* #ORM\Table(name="accounts",
* uniqueConstraints={#ORM\UniqueConstraint(name="accounts_name_unique",columns={"name"})})
*/
class Account extends AbstractGenericEntity{
/**
* #ORM\OneToMany(targetEntity="AccountSheet", mappedBy="account")
* #var AccountSheet[]
*/
protected $accountSheets;
public function __construct($name = null, $description = null){
$this->accountSheets = new ArrayCollection();
$this->name = $name;
$this->description = $description;
}
}
src\AppBundle\Entity\AccountSheet.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\AbstractGenericEntity;
/**
* #ORM\Entity()
* #ORM\Table(name="accounts_sheets",
* uniqueConstraints={#ORM\UniqueConstraint(name="accountsheet_account_unique", columns={"name", "account_id"})})
* #ORM\HasLifecycleCallbacks
*/
class AccountSheet extends AbstractGenericEntity{
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Account", inversedBy="accountSheets")
* #var Account
*/
protected $account;
/**
* #ORM\OneToMany(targetEntity="Operation", mappedBy="accountSheet")
* #var Operation[]
*/
protected $operations;
public function __construct($name = null){
$this->operations = new ArrayCollection();
$this->name = $name;
}
}
src\AppBundle\Entity\Operation.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\AbstractGenericEntity;
/**
* #ORM\Entity()
* #ORM\Table(name="operations")
*/
class Operation extends AbstractGenericEntity{
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\AccountSheet", inversedBy="operations")
* #ORM\JoinColumn(nullable=false)
* #var AccountSheet
*/
protected $accountSheet;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Category", inversedBy="operations")
* #var Category
*/
protected $category;
public function __construct($type = null, $label = null, $montant = null, $comment = null){
$this->label = $label;
$this->type = $type;
$this->comment = $comment;
$this->montant = $montant;
}
}
src\AppBundle\Entity\Category.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\AbstractGenericEntity;
/**
* #ORM\Entity()
* #ORM\Table(name="categories")
*/
class Category extends AbstractGenericEntity{
/**
* #ORM\Column(type="string")
*/
protected $label;
/**
* #ORM\Column(type="string")
*/
protected $description;
/**
* #ORM\OneToMany(targetEntity="Operation", mappedBy="category")
* #var Operation[]
*/
protected $operations;
public function __construct($name = null){
$this->operations = new ArrayCollection();
$this->name = $name;
}
}
I guess it's on the Operation entity, where AccountSheet is referenced again. The bi-directional on operation is not really needed.
How could I rearrange this?
Thanks!

From the official documentation :
Circular references are common when dealing with entity relations
To avoid infinite loops, GetSetMethodNormalizer throws a CircularReferenceException when such a case is encountered:
$member = new Member();
$member->setName('Kévin');
$org = new Organization();
$org->setName('Les-Tilleuls.coop');
$org->setMembers(array($member));
$member->setOrganization($org);
echo $serializer->serialize($org, 'json'); // Throws a CircularReferenceException
So, from this point, you have 3 solutions to get rid of this issue :
Set a circular reference handler :
Instead of throwing an exception, circular references can also be handled by custom callables. This is especially useful when serializing entities having unique identifiers:
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getName();
});
$serializer = new Serializer(array($normalizer), array($encoder));
var_dump($serializer->serialize($org, 'json'));
// {"name":"Les-Tilleuls.coop","members":[{"name":"K\u00e9vin", organization: "Les-Tilleuls.coop"}]}
Set ignored attributes (not my preferred solution) :
in your case :
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
normalizer->setIgnoredAttributes(array("account", "accountSheet", "category", "operation"));
$serializer = new Serializer(array($normalizer), array($encoder));
var_dump($serializer->serialize($org, 'json'));
Use group attributes (my preferred solution) :
This method is similar to setting ignored attributes because you will chose which attribute you want to serialize by adding the group annotation on it and the rest will be ignored for recursivity during normalization process.
Using Serialization Groups Annotations
Attributes Groups
In your case with the Account entity for example do this on the account side :
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\AbstractGenericEntity;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ORM\Entity()
* #ORM\Table(name="accounts",
* uniqueConstraints={#ORM\UniqueConstraint(name="accounts_name_unique",columns={"name"})})
*/
class Account extends AbstractGenericEntity{
/**
* #ORM\OneToMany(targetEntity="AccountSheet", mappedBy="account")
* #var AccountSheet[]
* #Groups({"account"})
*/
protected $accountSheets;
public function __construct($name = null, $description = null){
$this->accountSheets = new ArrayCollection();
$this->name = $name;
$this->description = $description;
}
}
Then do not put this group annotation on the $account field in the AccountSheet entity to get rid of the circular reference issue.
Finally you serialize your Account :
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
$serializer = new Serializer(array($normalizer), array($encoder));
var_dump($serializer->serialize($account, 'json', array('groups' => array('account')) ));

$jsonContent = $serializer->serialize($yourObject, 'json', [
'circular_reference_handler' => function ($object) {
return $object->getId();
}
]);
Above code works for me to fix circular reference exception. (Symfony >=4.2)

Related

Why is Id AutoGenerated anew on UPDATE with Relation using Symfony Doctrine?

Symfony 5.3
Doctrine bundle ^2.4 ORM ^2.9
MariaDB 10.6.4
This has been rather difficult to diagnose especially as I was dealing with some complicated layered code. If I was sure, I would have filed a bug with Doctrine, but I want to first make sure I'm not making some glaring mistake or such, in implementation.
I have painstakingly tried to reduce the code to a simplified working example. On my test database tables, there are additional columns that are not referred to in the demo code.
// src/Entity/Record.php
declare(strict_types = 1);
namespace App\Entity;
use App\Repository\RecordRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=RecordRepository::class)
* #ORM\Table(name="Records")
*/
class Record {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
public function getId(): ?int {
return $this->id;
}
/**
* #ORM\OneToOne(targetEntity="App\Entity\RecordStatus", mappedBy="record", cascade={"persist"})
* #ORM\JoinColumn(name="Id")
*/
private ?RecordStatus $recordStatus = NULL;
public function getRecordStatus(): ?RecordStatus {
return $this->recordStatus;
}
public function setRecordStatus(RecordStatus $value): void {
$value->setRecord($this);
$this->recordStatus = $value;
}
}
// src/Entity/RecordStatus.php
declare(strict_types = 1);
namespace App\Entity;
use App\Repository\RecordStatusRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=RecordStatusRepository::class)
* #ORM\Table(name="Record_Statuses")
*/
class RecordStatus {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
public function getId(): ?int {
return $this->id;
}
/**
* #ORM\OneToOne(targetEntity="App\Entity\Record", inversedBy="record")
* #ORM\JoinColumn(name="RecordId")
*/
private Record $record;
public function getRecord(): Record {
return $this->record;
}
public function setRecord(Record $value): void {
$this->record = $value;
}
}
// src/Controller/DefaultController.php
declare(strict_types = 1);
namespace App\Controller;
use App\Entity\Record;
use App\Entity\RecordStatus;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DefaultController extends AbstractController {
/**
* #Route("/")
*/
public function home(): Response {
$doctrine = $this->getDoctrine();
$entityManager = $doctrine->getManager();
$recordRepository = $doctrine->getRepository(Record::class);
$recordStatus = new RecordStatus;
$item = $recordRepository->find(1);
$item->setRecordStatus($recordStatus);
$entityManager->flush();
return new JsonResponse(['id' => $item->getId()]);
}
}
When this route ("/") is triggered, the record is updated and the old Id is displayed. But after the update, checking the database shows that the record now has a newly Auto-Generated Id, that comes after the last previous record in the table. Note that this is on UPDATE and not INSERT.
My current partial workaround is to load the currently mapped relation if one exists and update it instead of using new RecordStatus, but it should also be possible to use a new Related instance if required (especially if there was no Relation assigned on first insert).

Updating Entity OneToMany relationship by passing arraycollection

Im trying to change an old arraycolection of $linhas for a new one by using the method
setLinhas(Arraycollection $linhas)
but what happens when it does the changes is that internally he creates a new object with the new lines and dont update the old object with the new lines. It creates a new instance with the same values as the old object. It was suppose to update the same object and not create a new one!
Entity's Property :
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\LinhasPrecos", mappedBy="preco",orphanRemoval=true,cascade={"persist","merge"})
*/
protected $linhas;
/**
* #param $linhas
*/
public function setLinhas($linhas)
{
$this->linhas = new ArrayCollection($linhas);
}
In the service:
$oldObject->setLinhas($newObectWithNewLinhas->getLinhas());
$this->em->persist($oldObject);
but if I do the change manually it will work:
$oldLinhas = $oldObject->getLinhas()->getValues();
foreach($oldLinhas as $oldLinha)
{
$oldObject->removeLinha($oldLinha);
}
$linhaToCopy = $newObectWithNewLinhas->getLinhas()->getValues();
foreach($linhasCopyNew as $linhaCopyNew)
{
$oldObject->addLinha($linhaCopyNew);
}
thanks in advance!
You are doing it wrong!
use this constructor and setter instead:
Preco
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class Preco
{
//...
/**
* #var Collection
*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\LinhasPrecos", mappedBy="preco", orphanRemoval=true, cascade={"persist","merge"})
*/
protected $linhas;
//...
public function __construct()
{
$this->linhas = new ArrayCollection();
}
public function setLinhas($linhas)
{
$this->linhas = $linhas;
}
}
Notice
You should pass a doctrine collection into setLinhas.
This way you are totally replacing an old collection, with the new collection (and not adding an element to the old collection).

Reading annotations with Symfony4

I'm trying to read annotations with Symfony4 but looks like something is not working!
The class I'm trying to read from:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\OAuthClientRepository")
*/
class OAuthClient {
{
/**
* #Assert\NotBlank()
* #ORM\Column(type="string")
*/
protected $name;
}
The code I'm using to read the annotations:
<?php
namespace App\Test;
use Doctrine\Common\Annotations\SimpleAnnotationReader as DocReader;
/**
* Helper AnnotationReader
*/
class AnnotationReader
{
/**
* #param $class
* #return null|\StdClass
*/
public static function getClass($class)
{
$reader = new DocReader;
$reflector = new \ReflectionClass($class);
return $reader->getClassAnnotations($reflector);
}
/**
* #param $class
* #param $property
* #return array
*/
public static function getProperty($class, $property)
{
$reader = new DocReader;
$reflector = new \ReflectionProperty($class, $property);
return $reader->getPropertyAnnotations($reflector);
}
/**
* #param $class
* #param $method
* #return array
*/
public static function getMethod($class, $method)
{
$reader = new DocReader;
$reflector = new \ReflectionMethod($class, $method);
return $reader->getMethodAnnotations($reflector);
}
}
I get empty arrays when I call:
App\Test\AnnotationReader::getClass(App\Entity\OAuthClient::class);
App\Test\AnnotationReader::getProperty(App\Entity\OAuthClient::class, 'name');
What am I doing wrong?
What is the best way to read annotation?
I'm looking to read the validations used on a class property.
Thank you for your help!
change
use Doctrine\Common\Annotations\SimpleAnnotationReader as DocReader;
to
use Doctrine\Common\Annotations\AnnotationReader as DocReader;
and it works.
You may have to call the addNamespace() method on the SimpleAnnotationReader instance.
For instance, for ORM annotations:
$reader->addNamespace('Doctrine\ORM\Mapping');
And for validation annotations:
$reader->addNamespace('Symfony\Component\Validator\Constraints');
See:
SimpleAnnotationReader API: https://www.doctrine-project.org/api/annotations/latest/Doctrine/Annotations/SimpleAnnotationReader.html
SimpleAnnotationReader examples: https://github.com/doctrine/doctrine2/blob/462173ad71ae63cd9877e1e642f7968ed1f9971b/lib/Doctrine/ORM/Configuration.php#L140-L141

Doctrine 2 persist only save header object why?

I want save purchase order header with purchase order details.This my PurchaseOrder Entity Class=>
namespace AppBundle\Entity;
use AppBundle\Entity\PurchaseInvoiceDetail;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* PurchaseOrder
*
* #ORM\Table(name="purchase_order", indexes={#ORM\Index(name="fk_purchase_order_supplier1_idx", columns={"supplier_id"})})
* #ORM\Entity
*/
class PurchaseOrder
{
/**
* #var PurchaseOrderDetails
*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\PurchaseOrderDetails", mappedBy="purchaseOrder",cascade={"cascade"})
* #JMS\Type("ArrayCollection<FinanceBundle\Entity\AutoAllocation>")
*/
private $purchaseOrderDetails;
public function __construct()
{
$this->purchaseOrderDetails = new ArrayCollection();
}
public function addPurchaseOrderDetail(PurchaseOrderDetails $purchaseOrderDetails)
{
$this->purchaseOrderDetails->add($purchaseOrderDetails);
}
/**
* #return PurchaseOrderDetails
*/
public function getPurchaseOrderDetails()
{
return $this->purchaseOrderDetails;
}
/**
* #param PurchaseOrderDetails $purchaseOrderDetails
*/
public function setPurchaseOrderDetails($purchaseOrderDetails)
{
$this->purchaseOrderDetails = $purchaseOrderDetails;
}
}
and PurchaseOrderDetail class as this =>
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* PurchaseOrderDetails
*
* #ORM\Table(name="purchase_order_details", indexes={#ORM\Index(name="fk_purchase_order_details_purchase_order1_idx", columns={"purchase_order_id"}), #ORM\Index(name="fk_purchase_order_details_invt_item1_idx", columns={"id_item"})})
* #ORM\Entity
*/
class PurchaseOrderDetails
{
/**
* #var \AppBundle\Entity\PurchaseOrder
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\PurchaseOrder",inversedBy="purchaseOrderDetails")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="purchase_order_id", referencedColumnName="id")
* })
*/
private $purchaseOrder;
/**
* Set purchaseOrder
*
* #param \AppBundle\Entity\PurchaseOrder $purchaseOrder
*
* #return PurchaseOrderDetails
*/
public function setPurchaseOrder(\AppBundle\Entity\PurchaseOrder $purchaseOrder = null)
{
$this->purchaseOrder = $purchaseOrder;
return $this;
}
/**
* Get purchaseOrder
*
* #return \AppBundle\Entity\PurchaseOrder
*/
public function getPurchaseOrder()
{
return $this->purchaseOrder;
}
}
my php code in symfony 3.1 as follows=>
$em = $this->getDoctrine()->getManager();
$purchaseOrder = new PurchaseOrder();
$puchaseOrderDetail = new PurchaseOrderDetails();
$puchaseOrderDetail->setPrice(100);
$purchaseOrder->setPurchaseOrderDetails($puchaseOrderDetail);
$puchaseOrderDetail->setPurchaseOrder($purchaseOrder);
$em->persist($purchaseOrder);
$em->flush();
no errors occurred and just only purchase order have persisted and purchase order detail doesn't
You are not persisting the detail object. Either persist it manually with
$em->persist($purchaseOrderDetail);
or fix
cascade={"persist"}
in the #ORM\OneToMany annotation of PurchaseOrder::$purchaseOrderDetails (cascade={"cascade"} is probably a typo).
You need to persist PurchaseOrderDetails as well.
The below code should save both of your entities.
$em = $this->getDoctrine()->getManager();
$purchaseOrder = new PurchaseOrder();
$puchaseOrderDetail = new PurchaseOrderDetails();
$puchaseOrderDetail->setPrice(100);
$purchaseOrder->setPurchaseOrderDetails($puchaseOrderDetail);
$puchaseOrderDetail->setPurchaseOrder($purchaseOrder);
$em->persist($purchaseOrder);
$em->persist($puchaseOrderDetail);
$em->flush();
As #Finwe has mentioned, If your business logic requires, and you don't want to persist separately PurchaseOrderDetails entity while creating a new PurchaseOrder. You might consider configuring cascade_persist. which will persist automatically associated entity.
To do so, add cascade option to your association config :
#ORM\OneToMany(targetEntity="AppBundle\Entity\PurchaseOrderDetails", mappedBy="purchaseOrder",cascade={"persist"})

How to do a ManytoMany or ManyToOne Doctrine Neo4j

I'm learning how to work with Neo4j and Doctrine OGM, and I'm having problems with my source code. I don't know how to use manytomany because I'm just starting learn. When I save I see:
Catchable fatal error: Argument 1 passed to Entity\Empresas::setTelefone() must be an instance of Entity\Entity\Telefones, string given, called in /Applications/MAMP/htdocs/neo4j/n4j/save.php on line 17 and defined in /Applications/MAMP/htdocs/neo4j/n4j/Empresas.php on line 50
My Empresas.php entity
namespace Entity;
use HireVoice\Neo4j\Annotation as OGM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* All entity classes must be declared as such.
*
* #OGM\Entity(labels="Empresas")
*/
class Empresas
{
/**
* The internal node ID from Neo4j must be stored. Thus an Auto field is required
* #OGM\Auto
*/
protected $id;
/**
* #OGM\Property
* #OGM\Index
*/
protected $nome;
/**
* #OGM\Property
*/
protected $keywords;
/**
* #OGM\ManyToOne(relation="tem_telefone")
*/
protected $telefone;
function getID(){
return $this->id;
}
function setNome($nome){
$this->nome = $nome;
}
function setKeywords($keywords){
$this->keywords = $keywords;
}
public function getTelefone() {
return $this->telefone;
}
public function setTelefone(Entity\Telefones $telefone) {
$this->telefone = $telefone;
}
}`
My Telefones.php Entity
<?php
namespace Entity;
use HireVoice\Neo4j\Annotation as OGM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* All entity classes must be declared as such.
*
* #OGM\Entity(labels="Empresas")
*/
class Telefones
{
/**
* The internal node ID from Neo4j must be stored. Thus an Auto field is required
* #OGM\Auto
*/
protected $id;
/**
* #OGM\Property
* #OGM\Index
*/
protected $telefone;
function getID(){
return $this->id;
}
}
And my Save.php Entity
<?php
require 'bootstrap.php';
require 'Empresas.php';
require 'Telefones.php';
$repo = $em->getRepository('Entity\\Empresas');
$empresa_container = $em->find('Entity\\Empresas', "22");
$telefones = new Entity\Telefones();
$empresa = new Entity\Empresas;
$empresa->setNome("nome");
$empresa->setKeywords("keywords");
$empresa->setTelefone("telefone");
$em->persist($telefones);
$em->persist($empresa);
$em->flush();
echo $empresa->getId();
Error

Categories