2
votes

Recently I've started a project based on API Platform. I've designed my entities as I do with every other Symfony project. Creating / Updating of normal entities with no relations works very good but I have a "special" entity with two Many-to-Many relations, for simplicity I've limited this post on one of the two relations. If I try to create a new entity, including a new Element for the Many-to-Many relation through the API Call it to produce a 500 error.

I'm a bit confused maybe you can help me.

Shortend Stacktrace:

[Tue Mar 13 12:19:35 2018] PHP Fatal error:  Maximum function nesting level of '256' reached, aborting! in <ProjectPath>/vendor/symfony/debug/ErrorHandler.php on line 605
[Tue Mar 13 12:19:35 2018] PHP Stack trace:
[Tue Mar 13 12:19:35 2018] PHP   1. {main}() <ProjectPath>/public/index.php:0
[Tue Mar 13 12:19:35 2018] PHP   2. Symfony\Component\HttpKernel\Kernel->handle() <ProjectPath>/public/index.php:37
[Tue Mar 13 12:19:35 2018] PHP   3. Symfony\Component\HttpKernel\HttpKernel->handle() <ProjectPath>/vendor/symfony/http-kernel/Kernel.php:202
[Tue Mar 13 12:19:35 2018] PHP   4. Symfony\Component\HttpKernel\HttpKernel->handleRaw() <ProjectPath>/vendor/symfony/http-kernel/HttpKernel.php:68
[Tue Mar 13 12:19:35 2018] PHP   5. Symfony\Component\EventDispatcher\EventDispatcher->dispatch() <ProjectPath>/vendor/symfony/http-kernel/HttpKernel.php:127
[Tue Mar 13 12:19:35 2018] PHP   6. Symfony\Component\EventDispatcher\EventDispatcher->doDispatch() <ProjectPath>/vendor/symfony/event-dispatcher/EventDispatcher.php:44
[Tue Mar 13 12:19:35 2018] PHP   7. ApiPlatform\Core\EventListener\DeserializeListener->onKernelRequest() <ProjectPath>/vendor/symfony/event-dispatcher/EventDispatcher.php:212
[Tue Mar 13 12:19:35 2018] PHP   8. Symfony\Component\Serializer\Serializer->deserialize() <ProjectPath>/vendor/api-platform/core/src/EventListener/DeserializeListener.php:71
[Tue Mar 13 12:19:35 2018] PHP   9. Symfony\Component\Serializer\Serializer->denormalize() <ProjectPath>/vendor/symfony/serializer/Serializer.php:133
[Tue Mar 13 12:19:35 2018] PHP  10. ApiPlatform\Core\JsonLd\Serializer\ItemNormalizer->denormalize() <ProjectPath>/vendor/symfony/serializer/Serializer.php:182
[Tue Mar 13 12:19:35 2018] PHP  11. ApiPlatform\Core\Serializer\AbstractItemNormalizer->denormalize() <ProjectPath>/vendor/api-platform/core/src/JsonLd/Serializer/ItemNormalizer.php:108
[Tue Mar 13 12:19:35 2018] PHP  12. Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->denormalize() <ProjectPath>/vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php:121
[Tue Mar 13 12:19:35 2018] PHP  13. ApiPlatform\Core\Serializer\AbstractItemNormalizer->setAttributeValue() <ProjectPath>/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php:205
[Tue Mar 13 12:19:35 2018] PHP  14. ApiPlatform\Core\Serializer\AbstractItemNormalizer->setValue() <ProjectPath>/vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php:191
[Tue Mar 13 12:19:35 2018] PHP  15. Symfony\Component\PropertyAccess\PropertyAccessor->setValue() <ProjectPath>/vendor/api-platform/core/src/Serializer/AbstractItemNormalizer.php:344
[Tue Mar 13 12:19:35 2018] PHP  16. Symfony\Component\PropertyAccess\PropertyAccessor->writeProperty() <ProjectPath>/vendor/symfony/property-access/PropertyAccessor.php:217
[Tue Mar 13 12:19:35 2018] PHP  17. Symfony\Component\PropertyAccess\PropertyAccessor->writeCollection() <ProjectPath>/vendor/symfony/property-access/PropertyAccessor.php:627
[Tue Mar 13 12:19:35 2018] PHP  18. App\Entity\Pool->addTag() <ProjectPath>/vendor/symfony/property-access/PropertyAccessor.php:679
[Tue Mar 13 12:19:35 2018] PHP  19. App\Entity\Tag->addPool() <ProjectPath>/src/Entity/Pool.php:284
[Tue Mar 13 12:19:35 2018] PHP  20. App\Entity\Pool->addTag() <ProjectPath>/src/Entity/Tag.php:85
[Tue Mar 13 12:19:35 2018] PHP  21. App\Entity\Tag->addPool() <ProjectPath>/src/Entity/Pool.php:284
[Tue Mar 13 12:19:35 2018] PHP  22. App\Entity\Pool->addTag() <ProjectPath>/src/Entity/Tag.php:85
[Tue Mar 13 12:19:35 2018] PHP  23. App\Entity\Tag->addPool() <ProjectPath>/src/Entity/Pool.php:284
[Tue Mar 13 12:19:35 2018] PHP  24. App\Entity\Pool->addTag() <ProjectPath>/src/Entity/Tag.php:85
[Tue Mar 13 12:19:35 2018] PHP  25. App\Entity\Tag->addPool() <ProjectPath>/src/Entity/Pool.php:284
[Tue Mar 13 12:19:35 2018] PHP  26. App\Entity\Pool->addTag() <ProjectPath>/src/Entity/Tag.php:85
[Tue Mar 13 12:19:35 2018] PHP  27. App\Entity\Tag->addPool() <ProjectPath>/src/Entity/Pool.php:284
[Tue Mar 13 12:19:35 2018] PHP  28. App\Entity\Pool->addTag() <ProjectPath>/src/Entity/Tag.php:85
[Tue Mar 13 12:19:35 2018] PHP  29. App\Entity\Tag->addPool() <ProjectPath>/src/Entity/Pool.php:284
[Tue Mar 13 12:19:35 2018] PHP  30. App\Entity\Pool->addTag() <ProjectPath>/src/Entity/Tag.php:85
[Tue Mar 13 12:19:35 2018] PHP  31. App\Entity\Tag->addPool() <ProjectPath>/src/Entity/Pool.php:284
[Tue Mar 13 12:19:35 2018] PHP  32. App\Entity\Pool->addTag() <ProjectPath>/src/Entity/Tag.php:85
[Tue Mar 13 12:19:35 2018] PHP  33. App\Entity\Tag->addPool() <ProjectPath>/src/Entity/Pool.php:284
[Tue Mar 13 12:19:35 2018] PHP  34. App\Entity\Pool->addTag() <ProjectPath>/src/Entity/Tag.php:85
[Tue Mar 13 12:19:35 2018] PHP  35. App\Entity\Tag->addPool() <ProjectPath>/src/Entity/Pool.php:284

Truncated Entity Pool

namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Class Pool - This Entity describes a pool of tasks.
 * @package App\Entity
 * @ApiResource(attributes={
 *     "normalization_context"={"groups"={"read"}},
 *     "denormalization_context"={"groups"={"write"}}
 * })
 * @ORM\Entity
 */
class Pool
{
    /**
     * @var  ArrayCollection|Tag[] $tags
     * @param ArrayCollection|Tag[] $tags all tags that are that are related with this pool
     * @ApiProperty(
     *     attributes={
     *         "swagger_context"={
     *             "$ref"="#/definitions/Tag",
     *         }
     *     }
     * )
     * @ORM\ManyToMany(targetEntity="Tag", inversedBy="pools", cascade={"persist"})
     * @Groups({"read", "write"})
     */
    private $tags;

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

    /**
     * @return ArrayCollection
     */
    public function getTags()
    {
        return $this->tags;
    }

    /**
     * @param ArrayCollection $tags
     */
    public function setTags(ArrayCollection $tags)
    {
        $this->tags = $tags;
    }

    /**
     * @param Tag $tag
     */
    public function addTag(Tag $tag):void
    {
        $tag->addPool($this);
        $this->tags->add($tag);
    }

    /**
     * @param Tag $tag
     */
    public function removeTag(Tag $tag):void
    {
        $tag->removePool($this);
        $this->tags->removeElement($tag);
    }
}

Truncated Tag Entity

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Class Tag
 * @package App\Entity
 * @ApiResource(attributes={
 *     "normalization_context"={"groups"={"tag_read"}},
 *     "denormalization_context"={"groups"={"write"}}
 * })
 * @ORM\Entity
 */
class Tag
{
    /**
     * @var ArrayCollection[Pool]
     * @param ArrayCollection[Pool] $tasks all pool that are related with this tag
     * @Groups({"tag_read", "write"})
     * @ApiProperty(
     *     attributes={
     *         "swagger_context"={
     *             "$ref"="#/definitions/Pool",
     *         }
     *     }
     * )
     * @ORM\ManyToMany(targetEntity="Pool", mappedBy="tags",cascade={"persist"})
     */
    private $pools;

    /**
     * @var string
     * @param string $identifier the hashtag
     * @ORM\Column(type="string")
     * @Assert\NotBlank
     * @Groups({"read", "write"})
     */
    private $tag;

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

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

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

    public function addPool(Pool $pool):void
    {
        $pool->addTag($this);
        $this->pools->add($pool);
    }

    public function removePool(Pool $pool):void
    {
        $pool->removeTag($this);
        $this->pools->removeElement($pool);
    }
}

POST Request to API Route

{
  "name": "This is a Test",
  "description": "A pool build for tests",
  "public": true,
  "tags": [{"tag":"testTag"},{"tag":"testTag2"}]
}
4
PHP Fatal error: Maximum function nesting level of '256' reached, aborting! Aren't you calling something recursively with those many-to-many relations? - kry
I do not actively call this function, api platform builds CRUD Operations based on the entities. So i have no clue why it is called recursively. @kry - ljbergmann
@MathieuDormeval i've change my Entities according to the issue. This solves the described Problem but now i get a new error: "hydra:description": "Nested documents for attribute \"tags\" are not allowed. Use IRIs instead.", - ljbergmann

4 Answers

4
votes

You should have 3 tables in order to achieve Many To Many: Pool, Tag and PoolTag

In your Pool Entity :

class Pool
{
    /**
    * @ORM\ManyToMany(targetEntity="Tag", inversedBy="pools")
    * @ORM\JoinTable(
    *  name="pool_tag",
    *  joinColumns={
    *      @ORM\JoinColumn(name="pool_id", referencedColumnName="id")
    *  },
    *  inverseJoinColumns={
    *      @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
    *  }
    * )
    */
    private $tags;

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

In your Tag Entity :

class Tag
{
    /**
    * @ORM\ManyToMany(targetEntity="Pool", mappedBy="tags")
    */
    private $pools
}

POST Request to API Route

 {
      "name": "This is a Test",
      "description": "A pool build for tests",
      "public": true,
      "tags": ["/api/tags/1","/api/tags/2"]
  }
2
votes

Your error: Maximum function nesting level of '256' reached is actually an error that occurs when you're using XDebug. It's a hint that you've created an endless recursion, which is the case here.

You're calling addTag() in Pool, which calls addPool() in Tag, calling addTag() in Pool, etc etc.

0
votes

The problem you have is not related to the API Platform itself, it is a semantic error that is causing an infinite recursion (just as someone said in a previous comment).
A solution can be to put a condition in the add and remove methods to avoid this error:

public function addTag(Tag $tag):void
{
    if (!$this->tags->contains($tag)) {
        $tag->addPool($this);
        $this->tags->add($tag);
    }
}


public function removeTag(Tag $tag):void
{
    if ($this->tags->contains($tag)) {
        $tag->removePool($this);
        $this->tags->removeElement($tag);
    }
}




public function addPool(Pool $pool):void
{
    if (!$this->pools->contains($pool)) {
        $pool->addTag($this);
        $this->pools->add($pool);
    }
}

public function removePool(Pool $pool):void
{
    if ($this->pools->contains($pool)) {
        $pool->removeTag($this);
        $this->pools->removeElement($pool);
    }
}
-6
votes

To solve such an issue, you might want to use Symfony forms (and therefore embedded) forms, as explained here.

Symfony doc - How to Embed a Collection of Forms