Search code examples
apisymfonyserializationentityfosrestbundle

Symfony API with FOSRestBundle : circular reference has been detected


I'm working on a symfony project to build a rest API, I have 4 entities related to each other like that :

class diagram

I've installed FOSRestBundle to build just a GET web service, when i want to get a resource for example :

http://evaluation.dev/app_dev.php/api/families

I got this error :

message: "A circular reference has been detected (configured limit:1).",

this is my controller :

<?php

namespace API\APIBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use FOS\RestBundle\Controller\Annotations as Rest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Serializer;

class CartographyController extends Controller
{
    /**
     * @Rest\View()
     * @Rest\Get("/posts")
     * @param Request $request
     * @return Response
     */
    public function getPostsAction(Request $request)
    {
        $encoder = new JsonEncoder();
        $normalizer = new GetSetMethodNormalizer();

        $serializer = new Serializer(array($normalizer), array($encoder));
        $posts = $this->get('doctrine.orm.entity_manager')
            ->getRepository('EvalBundle:Post')
            ->findAll();

        return new Response($serializer->serialize($posts, 'json'));

    }

    /**
     * @Rest\View()
     * @Rest\Get("/employments")
     * @param Request $request
     * @return Response
     */
    public function geEmploymentAction(Request $request)
    {
        $encoder = new JsonEncoder();
        $normalizer = new GetSetMethodNormalizer();

        $serializer = new Serializer(array($normalizer), array($encoder));
        $employments = $this->get('doctrine.orm.entity_manager')
            ->getRepository('EvalBundle:Employment')
            ->findAll();
        return new Response($serializer->serialize($employments, 'json'));

    }

    /**
     * @Rest\View()
     * @Rest\Get("/professions")
     * @param Request $request
     * @return Response
     */
    public function geProfessionsAction(Request $request)
    {
        $encoder = new JsonEncoder();
        $normalizer = new GetSetMethodNormalizer();

        $serializer = new Serializer(array($normalizer), array($encoder));
        $professions = $this->get('doctrine.orm.entity_manager')
            ->getRepository('EvalBundle:Profession')
            ->findAll();

        return new Response($serializer->serialize($professions, 'json'));    }

    /**
     * @Rest\View()
     * @Rest\Get("/families")
     * @param Request $request
     * @return Response
     */
    public function geFamiliesAction(Request $request)
    {
        $encoder = new JsonEncoder();
        $normalizer = new GetSetMethodNormalizer();

        $serializer = new Serializer(array($normalizer), array($encoder));
        $families = $this->get('doctrine.orm.entity_manager')
            ->getRepository('EvalBundle:Family')
            ->findAll();

        return new Response($serializer->serialize($families, 'json'));
    }
}

Family entity :

<?php

namespace EvalBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;

/**
 * Family
 *
 * @ORM\Table(name="family")
 * @ORM\Entity(repositoryClass="EvalBundle\Repository\FamilyRepository")
   */
class Family
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255, unique=true)
     */
    private $name;

    /**
     * One Family has Many Professions.
     * @ORM\OneToMany(targetEntity="Profession", mappedBy="family",orphanRemoval=true,cascade={"persist", "remove"},fetch="EAGER")
     */
    protected  $professions;



    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     *
     * @return Family
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return mixed
     */
    public function getProfessions()
    {
        return $this->professions;
    }

    /**
     * @param mixed $professions
     */
    public function setProfessions($professions)
    {
        $this->professions = $professions;
    }
    public function __toString() {
        return $this->name;
    }
}

Profession entity :

<?php

namespace EvalBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Profession
 *
 * @ORM\Table(name="profession")
 * @ORM\Entity(repositoryClass="EvalBundle\Repository\ProfessionRepository")
 */
class Profession
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255, unique=true)
     */
    private $name;

    /**
     * Many professions have One Family.
     * @ORM\ManyToOne(targetEntity="Family", inversedBy="professions",cascade={"persist", "remove"})
     */
    public $family;

    /**
     * One Profession has Many Employment.
     * @ORM\OneToMany(targetEntity="Employment", mappedBy="profession",cascade={"persist", "remove"}, orphanRemoval=true,fetch="EAGER")
     */
    private $employments;

    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     *
     * @return Profession
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return mixed
     */
    public function getEmployments()
    {
        return $this->employments;
    }

    /**
     * @param mixed $employments
     */
    public function setEmployments($employments)
    {
        $this->employments = $employments;
    }

    /**
     * @return mixed
     */
    public function getFamily()
    {
        return $this->family;
    }

    /**
     * @param mixed $family
     */
    public function setFamily($family)
    {
        $this->family = $family;
    }

    public function __toString() {
        return $this->name;
    }
}

Post entity

<?php

namespace EvalBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Post
 *
 * @ORM\Table(name="post")
 * @ORM\Entity(repositoryClass="EvalBundle\Repository\PostRepository")
 */
class Post
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255, unique=true)
     */
    private $name;

    /**
     * Many Posts have One Employment.
     * @ORM\ManyToOne(targetEntity="Employment", inversedBy="posts", fetch="EAGER")
     * @ORM\JoinColumn(name="employment_id", referencedColumnName="id",nullable=false)
     */
    public $employment;

    /**
     * One Post has Many LevelRequired.
     * @ORM\OneToMany(targetEntity="RequiredLevel", mappedBy="post")
     */
    private $requiredLevels;

    /**
     * One Post has Many PostEvaluation.
     * @ORM\OneToMany(targetEntity="PostEvaluation", mappedBy="post")
     */
    private $postEvaluations;


    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     *
     * @return Post
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return mixed
     */
    public function getEmployment()
    {
        return $this->employment;
    }


    public function getProfession(){
        return $this->employment->profession;
    }

    public function getFamily(){
        return $this->employment->profession->family;
    }


    /**
     * @param mixed $employment
     */
    public function setEmployment($employment)
    {
        $this->employment = $employment;
    }

    public function __toString() {
        return $this->name;
    }
}

employment entity

<?php

namespace EvalBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Employment
 *
 * @ORM\Table(name="employment")
 * @ORM\Entity(repositoryClass="EvalBundle\Repository\EmploymentRepository")
 */
class Employment
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255, unique=true)
     */
    private $name;

    /**
     * Many Employments have One Profession.
     * @ORM\ManyToOne(targetEntity="Profession", inversedBy="employments",fetch="EAGER")
     */
    public $profession;

    /**
     * One Employment has Many post.
     * @ORM\OneToMany(targetEntity="Post", mappedBy="employment",cascade={"persist", "remove"}, orphanRemoval=true,cascade={"persist", "remove"})
     */
    private $posts;

    /**
     * One Employment has One Grid.
     * @ORM\OneToOne(targetEntity="Grid", mappedBy="employment")
     */
    private $grid;


    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     *
     * @return Employment
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return mixed
     */
    public function getProfession()
    {
        return $this->profession;
    }

    /**
     * @param mixed $profession
     */
    public function setProfession($profession)
    {
        $this->profession = $profession;
    }

    /**
     * @return mixed
     */
    public function getPosts()
    {
        return $this->posts;
    }

    /**
     * @param mixed $posts
     */
    public function setPosts($posts)
    {
        $this->posts = $posts;
    }

    /**
     * @return mixed
     */
    public function getGrid()
    {
        return $this->grid;
    }

    /**
     * @param mixed $grid
     */
    public function setGrid($grid)
    {
        $this->grid = $grid;
    }
    public function __toString() {
        return $this->name;
    }
}

any solution please ?


Solution

  • For you example, you can avoid the CircularReference like this

    $normalizer = new ObjectNormalizer();
    $normalizer->setCircularReferenceLimit(1);
    
    $normalizer->setCircularReferenceHandler(function ($object) {
        return $object->getName();
    });
    
    $serializer = new Serializer(array($normalizer), array(new JsonEncoder()));
    var_dump($serializer->serialize($org, 'json'));
    

    But in your example, you don't use the FOSRestBundle for the view in your controller. The FOSRestController give you a handleView() and a view() method. Like this

    class CartographyController extends FOSRestController 
    {
        public function getPostsAction(Request $request)
        {
            $posts = $this->get('doctrine.orm.entity_manager')
                ->getRepository('EvalBundle:Post')
                ->findAll();
            $view = $this->view($posts, 200);
    
            return $this->handleView($view);
        }
    

    In this case, the serialiser is a service, this service is activated in config.yml :

    In your app/config/config.yml

    framework:
        serializer: { enabled: true }
    

    In order to avoid circularReference, you can do this. In your app/config/services.yml

    circular_reference_handler:
        public: false
        class: callback
        factory: [AppBundle\Serializer\CircularHandlerFactory, getId]
    serializer.normalizer.object:
        class: Symfony\Component\Serializer\Normalizer\ObjectNormalizer
        arguments: ["@serializer.mapping.class_metadata_factory", null, "@serializer.property_accessor"]
        public: false
        tags: [serializer.normalizer]
        calls:
            - method: setCircularReferenceLimit
              arguments: [1]
            - method: setCircularReferenceHandler
              arguments: ["@circular_reference_handler"]
    

    The factory can be like this:

    namespace AppBundle\Serializer;
    
    class CircularHandlerFactory
    {
        /**
         * @return \Closure
         */
        public static function getId()
        {
            return function ($object) {
                return $object->getId();
            };
        }
    }
    

    Another idea is to use the GROUPS for the serializer. There is an annotation given by FosRestBundle : @Rest\View(serializerGroups={"user"})

    More information here: https://symfony.com/doc/current/serializer.html#using-serialization-groups-annotations Symfony2, FOSRestBundle. How to use group with JMSSerializerBundle?