Search code examples
symfonydoctrine-ormsymfony4

circular reference when trying to find entities


It's my first Symfony app. I try to do an intervention manager, so I have created three entities: Intervention, Comment and User.
Users are managed by FOSUserBundle.

Classes:
Intervention

<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\InterventionRepository")
 */
class Intervention
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="smallint")
     */
    private $num_rue;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $nom_rue;

    /**
     * @ORM\Column(type="string", length=5)
     */
    private $cp;

    /**
     * @ORM\Column(type="decimal", precision=12, scale=9, nullable=true)
     */
    private $lat;

    /**
     * @ORM\Column(type="decimal", precision=12, scale=9, nullable=true)
     */
    private $longi;

    /**
     * @ORM\Column(type="text", nullable=true)
     */
    private $description;

    /**
     * @ORM\Column(type="boolean", options={"default":true})
     */
    private $statut;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="intervention", orphanRemoval=true)
     */
    private $comments;

    public function __construct()
    {
        $this->comments = new ArrayCollection();
    }



    public function getId(): ?int
    {
        return $this->id;
    }

    public function getNumRue(): ?int
    {
        return $this->num_rue;
    }

    public function setNumRue(int $num_rue): self
    {
        $this->num_rue = $num_rue;

        return $this;
    }

    public function getNomRue(): ?string
    {
        return $this->nom_rue;
    }

    public function setNomRue(string $nom_rue): self
    {
        $this->nom_rue = $nom_rue;

        return $this;
    }

    public function getCp(): ?string
    {
        return $this->cp;
    }

    public function setCp(string $cp): self
    {
        $this->cp = $cp;

        return $this;
    }

    public function getLat()
    {
        return $this->lat;
    }

    public function setLat($lat): self
    {
        $this->lat = $lat;

        return $this;
    }

    public function getLongi()
    {
        return $this->longi;
    }

    public function setLongi($longi): self
    {
        $this->longi = $longi;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function setDescription(?string $description): self
    {
        $this->description = $description;

        return $this;
    }

    public function getStatut(): ?bool
    {
        return $this->statut;
    }

    public function setStatut(bool $statut): self
    {
        $this->statut = $statut;

        return $this;
    }

    /**
     * @return Collection|Comment[]
     */
    public function getComments(): Collection
    {
        return $this->comments;
    }

    public function addComment(Comment $comment): self
    {
        if (!$this->comments->contains($comment)) {
            $this->comments[] = $comment;
            $comment->setIntervention($this);
        }

        return $this;
    }

    public function removeComment(Comment $comment): self
    {
        if ($this->comments->contains($comment)) {
            $this->comments->removeElement($comment);
            // set the owning side to null (unless already changed)
            if ($comment->getIntervention() === $this) {
                $comment->setIntervention(null);
            }
        }

        return $this;
    }


}

Comment

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\CommentRepository")
 */
class Comment
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="text")
     */
    private $text;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\Intervention", inversedBy="comments")
     * @ORM\JoinColumn(name="intervention_id", referencedColumnName="id")
     */
    private $intervention;

    /**
     * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="comments")
     * @ORM\JoinColumn(name="author_id", referencedColumnName="id")
     */ 
    private $author;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getText(): ?string
    {
        return $this->text;
    }

    public function setText(string $text): self
    {
        $this->text = $text;

        return $this;
    }

    public function getIntervention(): ?Intervention
    {
        return $this->intervention;
    }

    public function setIntervention(?Intervention $intervention): self
    {
        $this->intervention = $intervention;

        return $this;
    }

    public function getAuthor(): ?User
    {
        return $this->author;
    }

    public function setAuthor(?User $author): self
    {
        $this->author = $author;

        return $this;
    }
}

User

<?php
// src/Entity/User.php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;


/**
 * @ORM\Entity
 * @ORM\Table(name="fos_user")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\OneToMany(targetEntity="App\Entity\Comment", mappedBy="author")
     */
    private $comments;

    public function __construct()
    {
        parent::__construct();
        $this->comments = new ArrayCollection();

    }

    /**
     * @return Collection|Comment[]
     */
    public function getComments(): Collection
    {
        return $this->comments;
    }

    public function addComment(Comment $comment): self
    {
        if (!$this->comments->contains($comment)) {
            $this->comments[] = $comment;
            $comment->setAuthor($this);
        }

        return $this;
    }

    public function removeComment(Comment $comment): self
    {
        if ($this->comments->contains($comment)) {
            $this->comments->removeElement($comment);
            // set the owning side to null (unless already changed)
            if ($comment->getAuthor() === $this) {
                $comment->setAuthor(null);
            }
        }

        return $this;
    }
}

The problem is when I try to access the Intervention with Doctrine, I have a circular reference.

I thought that comments under intervention return user collection which also returns comments...

$repository = $this->getDoctrine()->getRepository(Intervention::class);
$intervention = $repository->findBy(['statut' => 1]);
return $this->json($intervention);

Do you have any suggestion to make my code work please? Thank you.

Solution:

use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;

public function getLocations(){
        $encoder = new JsonEncoder();
        $normalizer = new ObjectNormalizer();
        $normalizer->setCircularReferenceHandler(function ($object, string $format = null, array $context = array()) {
            return $object->getId();
        });
        $serializer = new Serializer(array($normalizer), array($encoder));

        $repository = $this->getDoctrine()->getRepository(Intervention::class);
        $intervention = $repository->findBy(['statut' => 1]);



        $response = new Response($serializer->serialize($intervention, 'json'));
        $response->headers->set('Content-Type', 'application/json');
        return $response;

    }

Thanks for your help.


this is just the 1/100 of the dump:

array(3) { [0]=> object(App\Entity\Intervention)#483 (9) { ["id":"App\Entity\Intervention":private]=> int(1) ["num_rue":"App\Entity\Intervention":private]=> int(4) ["nom_rue":"App\Entity\Intervention":private]=> string(14) "rue d’alsace" ["cp":"App\Entity\Intervention":private]=> string(5) "49000" ["lat":"App\Entity\Intervention":private]=> string(12) "47.470484600" ["longi":"App\Entity\Intervention":private]=> string(12) "-0.551233000" ["description":"App\Entity\Intervention":private]=> string(69) "
étanchéité toiture

carrelage SB

" ["statut":"App\Entity\Intervention":private]=> bool(true) ["comments":"App\Entity\Intervention":private]=> object(Doctrine\ORM\PersistentCollection)#485 (9) { ["snapshot":"Doctrine\ORM\PersistentCollection":private]=> array(0) { } ["owner":"Doctrine\ORM\PersistentCollection":private]=> *RECURSION* ["association":"Doctrine\ORM\PersistentCollection":private]=> array(15) { ["fieldName"]=> string(8) "comments" ["mappedBy"]=> string(12) "intervention" ["targetEntity"]=> string(18) "App\Entity\Comment" ["cascade"]=> array(0) { } ["orphanRemoval"]=> bool(true) ["fetch"]=> int(2) ["type"]=> int(4) ["inversedBy"]=> NULL ["isOwningSide"]=> bool(false) ["sourceEntity"]=> string(23) "App\Entity\Intervention" ["isCascadeRemove"]=> bool(true) ["isCascadePersist"]=> bool(false) ["isCascadeRefresh"]=> bool(false) ["isCascadeMerge"]=> bool(false) ["isCascadeDetach"]=> bool(false) } ["em":"Doctrine\ORM\PersistentCollection":private]=> object(Doctrine\ORM\EntityManager)#234 (11) { ["config":"Doctrine\ORM\EntityManager":private]=> object(Doctrine\ORM\Configuration)#188 (1) { ["_attributes":protected]=> array(14) { ["entityNamespaces"]=> array(1) { ["App"]=> string(10) "App\Entity" } ["metadataCacheImpl"]=> object(Symfony\Component\Cache\DoctrineProvider)#195 (3) { ["pool":"Symfony\Component\Cache\DoctrineProvider":private]=> object(Symfony\Component\Cache\Adapter\PhpFilesAdapter)#197 (16) { ["createCacheItem":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> object(Closure)#199 (2) { ["static"]=> array(1) { ["defaultLifetime"]=> int(0) } ["parameter"]=> array(3) { ["$key"]=> string(10) "" ["$value"]=> string(10) "" ["$isHit"]=> string(10) "" } } ["mergeByLifetime":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> object(Closure)#201 (2) { ["static"]=> array(1) { ["getId"]=> object(Closure)#198 (2) { ["this"]=> *RECURSION* ["parameter"]=> array(1) { ["$key"]=> string(10) "" } } } ["parameter"]=> array(3) { ["$deferred"]=> string(10) "" ["$namespace"]=> string(10) "" ["&$expiredIds"]=> string(10) "" } } ["namespace":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> string(0) "" ["namespaceVersion":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> string(0) "" ["versioningIsEnabled":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> bool(false) ["deferred":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> array(0) { } ["ids":"Symfony\Component\Cache\Adapter\AbstractAdapter":private]=> array(4) { ["DoctrineNamespaceCacheKey%5B%5D"]=> string(31) "DoctrineNamespaceCacheKey%5B%5D" ["%5BApp%5CEntity%5CUser%24CLASSMETADATA%5D%5B1%5D"]=> string(48) "%5BApp%5CEntity%5CUser%24CLASSMETADATA%5D%5B1%5D" ["%5BApp%5CEntity%5CComment%24CLASSMETADATA%5D%5B1%5D"]=> string(51) "%5BApp%5CEntity%5CComment%24CLASSMETADATA%5D%5B1%5D" ["%5BApp%5CEntity%5CIntervention%24CLASSMETADATA%5D%5B1%5D"]=> string(56) "%5BApp%5CEntity%5CIntervention%24CLASSMETADATA%5D%5B1%5D" } ["maxIdLength":protected]=> NULL ["logger":protected]=> object(Symfony\Bridge\Monolog\Logger)#196 (5) { ["name":protected]=> string(5) "cache" ["handlers":protected]=> array(2) { [0]=> object(Monolog\Handler\FingersCrossedHandler)#94 (11) { ["handler":protected]=> object(Monolog\Handler\StreamHandler)#92 (10) { ["stream":protected]=> NULL ["url":protected]=> string(56) "xxxxxxxxxxxxxxxxxxxxxxxx/var/log/prod.log" ["errorMessage":"Monolog\Handler\StreamHandler":private]=> NULL ["filePermission":protected]=> NULL ["useLocking":protected]=> bool(false) ["dirCreated":"Monolog\Handler\StreamHandler":private]=> NULL ["level":protected]=> int(100) ["bubble":protected]=> bool(true) ["formatter":protected]=> NULL ["processors":protected]=> array(1) { [0]=> object(Monolog\Processor\PsrLogMessageProcessor)#93 (0) { } } } ["activationStrategy":protected]=> object(Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy)#95 (3) { ["blacklist":"Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy":private]=> string(7) "{(^/)}i" ["requestStack":"Symfony\Bridge\Monolog\Handler\FingersCrossed\NotFoundActivationStrategy":private]=> object(Symfony\Component\HttpFoundation\RequestStack)#96 (1) { ["requests":"Symfony\Component\HttpFoundation\RequestStack":private]=> array(1) { [0]=> object(Symfony\Component\HttpFoundation\Request)#4 (23) { ["attributes"]=> object(Symfony\Component\HttpFoundation\ParameterBag)#7 (1) { ["parameters":protected]=> array(5) { ["_route"]=> string(13) "location_list" ["_controller"]=> string(44) "App\Controller\OsimgController::getLocations" ["_route_params"]=> array(0) { } ["_firewall_context"]=> string(34) "security.firewall.map.context.main" ["_security"]=> array(1) { [0]=> object(Sensio\Bundle\FrameworkExtraBundle\Configuration\Security)#407 (3) { ["expression":"Sensio\Bundle\FrameworkExtraBundle\Configuration\Security":private]=> string(21) "has_role('ROLE_USER')" ["statusCode":protected]=> NULL ["message":protected]=> string(14) "Access denied." } } } } ["request"]=> object(Symfony\Component\HttpFoundation\ParameterBag)#5 (1) { ["parameters":protected]=> array(0) { } } ["query"]=> object(Symfony\Component\HttpFoundation\ParameterBag)#6 (1) { ["parameters":protected]=> array(0) { } } ["server"]=> object(Symfony\Component\HttpFoundation\ServerBag)#10 (1) { ["parameters":protected]=> array(66) { ["PATH"]=> string(28) "/usr/local/bin:/usr/bin:/bin" ["TEMP"]=> string(4) "/tmp" ["TMP"]=> string(4) "/tmp" ["TMPDIR"]=> string(4) "/tmp" ["PWD"]=> string(1) "/" ["HTTP_ACCEPT"]=> 

Solution

  • If you want to return as a json, serializer will serialize it,
    You have 3 options,
    You can Install jms serializer, which is more easy to serialize, less painless.
    - https://symfony.com/doc/current/components/serializer.html#handling-circular-references
    or
    - https://symfony.com/doc/current/reference/configuration/framework.html#reference-serializer-circular-reference-handler

    List item