Pregunta

I am building a JSON-based REST API using Symfony2.4 with Doctrine2.

EDIT : with JsonNormalizer, I can disabled some attributes, but what if I would like to set them, without recursivity ?

Basically, what I have (working) now is :

{
    "tasks": [
        {
            "id": 1,
            "name": "first task"
        },
        {
            "id": 2,
            "name": "second task"
        }
    ]
}

What I would like is :

{
    "tasks": [
        {
            "id": 1,
            "name": "first task",
            "category": {
                "id": 1,
                "name": "first category"
            }
        },
        {
            "id": 2,
            "name": "second task",
            "category": {
                "id": 1,
                "name": "first category"
            }
        }
    ]
}

What was my initial problem is :

{
    "tasks": [
        {
            "id": 1,
            "name": "first task",
            "category": {
                "id": 1,
                "name": "first category",
                "tasks": [
                    {
                        "id": 1,
                        "name": "first task",
                        "category": {
                            "id": 1,
                            "name": "first category",
                            "tasks": [...] // infinite...
                        }
                    },
                    {
                        "id": 2,
                        "name": "second task",
                        "category": {
                            "id": 1,
                            "name": "first category",
                            "tasks": [...] // infinite...
                        }
                    }
                ]
            }
        },
        {
            "id": 2,
            "name": "second task",
            "category": {
                "id": 1,
                "name": "first category",
                "tasks": [
                    {
                        "id": 1,
                        "name": "first task",
                        "category": {
                            "id": 1,
                            "name": "first category",
                            "tasks": [...]
                        }
                    },
                    {
                        "id": 2,
                        "name": "second task",
                        "category": {
                            "id": 1,
                            "name": "first category",
                            "tasks": [...]
                        }
                    }
                ]
            }
        }
    ]
}

I have a entity A with a manyToOne relation to another entity B.

I have implemented the reverse-side, to be able to retrieve the related A entities on the B one.

class Task
{
    /**
     * @ORM\ManyToOne(targetEntity="List", inversedBy="task")
     * @ORM\JoinColumn(name="list_id", referencedColumnName="id")
     */
    private $list;

    public function toArray($recursive = false)
    {
        $entityAsArray = get_object_vars($this);

        if ($recursive) {
            foreach ($entityAsArray as &$var) {
                if ((is_object($var)) && (method_exists($var, 'toArray'))) {
                    $var = $var->toArray($recursive);
                }
            }
        }

        return $entityAsArray;
    }
}

use Doctrine\Common\Collections\ArrayCollection;

class List
{
    /**
     * @ORM\OneToMany(targetEntity="Task", mappedBy="list")
     */
    private $tasks;

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

Then I am building the different API routes and controllers, Rendering the output as JsonResponses, And I would like to render, for a given list, the different tasks using the route :

/api/v1/lists/1/tasks

The task action of my controller :

public function tasksAction($id)
{
    $em = $this->getDoctrine()->getManager();

    $list = $em->getRepository('MyRestBundle:List')->findOneActive($id);

    if (!$list) {
        throw $this->createNotFoundException('List undefined');
    }

    $tasks = $list->getTasks()->toArray();

    foreach ($tasks as &$task) {
        // recursively format the tasks as array
        $task = $task->toArray(true);
    }

    $serializer = $this->get('serializer');

    return $this->generateJsonResponse($serializer->normalize($tasks), 200);
}

But unfortunately, I always get a memory leak, because the call of toArray() is recursive, so each task has a list property which has a tasks collection etc.

PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 130968 bytes) in src/Symfony/Component/Serializer/Serializer.php on line 146

I am wondering what would be the cleanest way to render entities with relations as JSON objects with Symfony2 ?

Do I really have to loop on my tasks to execute the "toArray()" method ?

I have also tried without it, without more success, except that the leak in in the file : src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php...

I have also tried without the JMSSeralizer, and the memory leak is thrown in my own php file.

Of course, I could increase the memory limit, but as it is an infinite loop problem of toArray() calls, it will not solve my problem.

How to format it properly ?

¿Fue útil?

Solución

I have a feeling that we might be overthinking this. Would that work for you?

// In your controller

$repo = $this->getDoctrine()->getRepository('MyRestBundle:List');
$list = $repo->findActiveOne($id);

$tasks = $list->getTasks()->toArray();

$serializer = $this->get('serializer');
$json = $serializer->serialize($tasks, 'json');

Why is Task Entity recursive? A task can not include another task. Only a List can include an array of Tasks. So basically all we should do is get this array from the List entity and serialize it. Unless I am missing something.

EDIT:

You can ask the serializer to ignore certain attributes as mentioned by the documentation:

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

$normalizer = new GetSetMethodNormalizer();
$normalizer->setIgnoredAttributes(array('age'));
$encoder = new JsonEncoder();

$serializer = new Serializer(array($normalizer), array($encoder));
$serializer->serialize($person, 'json'); // Output: {"name":"foo"}

Try to follow that example and just ignore the $list attribute in the Task entity.

EDIT2:

You don't need 'list' inside each task since it's the same. Your json should have 'list' and 'tasks' at the same level. then 'tasks' would be an array of tasks which will not contain 'list'. to achieve that you can have something like array('list' => $list, 'tasks' => $tasks) and serialize that.

Otros consejos

EDIT

If I understand what your code is doing, I think the toArray($recursive) function goes into infinite recursion both directly (whenever $var = $this) and indirectly (i.e. by a sibling iterating through its own list and calling toArray of the original task again). Try keeping track of what's been processed to prevent infinite recursion:

/**
 * @ORM\ManyToOne(targetEntity="List", inversedBy="task")
 * @ORM\JoinColumn(name="list_id", referencedColumnName="id")
 */
private $list;
private $toArrayProcessed = array();

public function toArray($recursive = false)
{
    $this->toArrayProcessed[$this->getId()] = 1;

    $entityAsArray = get_object_vars($this);

    if ($recursive) {
        foreach ($entityAsArray as &$var) {
            if ((is_object($var)) && (method_exists($var, 'toArray')) && !isset($this->toArrayProcessed[$var->getId()]) {
                $var = $var->toArray($recursive);
            }
        }
    }

    return $entityAsArray;
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top