2

I have an abstract entity "Node" superclass, with the following code that is the base for four other entities subclasses. I want to nest them in any order to create a loose hierarchy where any kind of node can be parent or child of any other type, and any node can have multiple parents.

src/Entity/Node.php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use ApiPlatform\Core\Annotation\ApiSubresource;

/**
 * @ORM\Entity(repositoryClass="App\Repository\NodeRepository")
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="type", type="string")
 * @ORM\DiscriminatorMap({
 *     "ART" = "Article",
 *     "CAT" = "Category",
 *     "LOC" = "Location",
 *     "PJT" = "Project"
 * })
 */
abstract class Node
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Node", inversedBy="childNodes")
     * @ApiSubresource
     */
    private $parentNodes;

    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Node", mappedBy="parentNodes")
     * @ApiSubresource
     */
    private $childNodes;

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

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

    /**
     * @return Collection|self[]
     */
    public function getParentNodes(): Collection
    {
        return $this->parentNodes;
    }

    public function addParentNode(self $parentNode): self
    {
        if (!$this->parentNodes->contains($parentNode)) {
            $this->parentNodes[] = $parentNode;
        }

        return $this;
    }

    public function removeParentNode(self $parentNode): self
    {
        if ($this->parentNodes->contains($parentNode)) {
            $this->parentNodes->removeElement($parentNode);
        }

        return $this;
    }

    /**
     * @return Collection|self[]
     */
    public function getChildNodes(): Collection
    {
        return $this->childNodes;
    }

    public function addChildNode(self $childNode): self
    {
        if (!$this->childNodes->contains($childNode)) {
            $this->childNodes[] = $childNode;
            $childNode->addParentNode($this);
        }

        return $this;
    }

    public function removeChildNode(self $childNode): self
    {
        if ($this->childNodes->contains($childNode)) {
            $this->childNodes->removeElement($childNode);
            $childNode->removeParentNode($this);
        }

        return $this;
    }
}

This class is extended by the four other entities, that are exposed as an ApiResource using Api Platform in a Symfony 5 project. All of them have almost the same code:

src/Entity/Project.php

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource()
 * @ORM\Entity(repositoryClass="App\Repository\ProjectRepository")
 */
class Project extends Node
{
    /**
     * @ORM\Id()
     * @ORM\OneToOne(targetEntity="App\Entity\Node")
     */
    private $id;

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

The exposed entities are shown in the Api Platform dashboard and can be queried successfully if the abstract Node entity does not have a self parent-child relation. In other words, if I remove all get/add/removeXXXXNodes methods and related variables from Node class the API works.

But if the relationship is present, then a 400 error occurs:

GET http://localhost/api/projects

{
  "@context": "\/api\/contexts\/Error",
  "@type": "hydra:Error",
  "hydra:title": "An error occurred",
  "hydra:description": "Unable to generate an IRI for \"App\\Entity\\Project\".",
  "trace": [
    {
      "namespace": "",
      "short_class": "",
      "class": "",
      "type": "",
      "function": "",
      "file": "\/var\/www\/html\/app\/vendor\/api-platform\/core\/src\/Bridge\/Symfony\/Routing\/IriConverter.php",
      "line": 155,
      "args": []
    },
    {
      "namespace": "ApiPlatform\\Core\\Bridge\\Symfony\\Routing",
      "short_class": "IriConverter",
      "class": "ApiPlatform\\Core\\Bridge\\Symfony\\Routing\\IriConverter",
      "type": "->",
      "function": "getItemIriFromResourceClass",
      "file": "\/var\/www\/html\/app\/vendor\/api-platform\/core\/src\/Bridge\/Symfony\/Routing\/IriConverter.php",
      "line": 128,
      "args": [
        [
          "string",
          "App\\Entity\\Project"
        ],
        [
          "array",
          [
            [
              "string",
              ""
            ]
          ]
        ],
        [
          "integer",
          1
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Bridge\\Symfony\\Routing",
      "short_class": "IriConverter",
      "class": "ApiPlatform\\Core\\Bridge\\Symfony\\Routing\\IriConverter",
      "type": "->",
      "function": "getIriFromItem",
      "file": "\/var\/www\/html\/app\/vendor\/api-platform\/core\/src\/JsonLd\/Serializer\/ItemNormalizer.php",
      "line": 74,
      "args": [
        [
          "object",
          "App\\Entity\\Project"
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\JsonLd\\Serializer",
      "short_class": "ItemNormalizer",
      "class": "ApiPlatform\\Core\\JsonLd\\Serializer\\ItemNormalizer",
      "type": "->",
      "function": "normalize",
      "file": "\/var\/www\/html\/app\/vendor\/symfony\/serializer\/Serializer.php",
      "line": 146,
      "args": [
        [
          "object",
          "App\\Entity\\Project"
        ],
        [
          "string",
          "jsonld"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "get"
            ],
            "resource_class": [
              "string",
              "App\\Entity\\Project"
            ],
            "input": [
              "null",
              null
            ],
            "output": [
              "null",
              null
            ],
            "request_uri": [
              "string",
              "\/api\/projects"
            ],
            "uri": [
              "string",
              "http:\/\/localhost\/api\/projects"
            ],
            "skip_null_values": [
              "boolean",
              true
            ],
            "resources": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ],
            "exclude_from_cache_key": [
              "array",
              [
                [
                  "string",
                  "resources"
                ],
                [
                  "string",
                  "resources_to_push"
                ]
              ]
            ],
            "resources_to_push": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ],
            "api_sub_level": [
              "boolean",
              true
            ],
            "jsonld_has_context": [
              "boolean",
              true
            ]
          }
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\Serializer",
      "short_class": "Serializer",
      "class": "Symfony\\Component\\Serializer\\Serializer",
      "type": "->",
      "function": "normalize",
      "file": "\/var\/www\/html\/app\/vendor\/api-platform\/core\/src\/Hydra\/Serializer\/CollectionNormalizer.php",
      "line": 87,
      "args": [
        [
          "object",
          "App\\Entity\\Project"
        ],
        [
          "string",
          "jsonld"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "get"
            ],
            "resource_class": [
              "string",
              "App\\Entity\\Project"
            ],
            "input": [
              "null",
              null
            ],
            "output": [
              "null",
              null
            ],
            "request_uri": [
              "string",
              "\/api\/projects"
            ],
            "uri": [
              "string",
              "http:\/\/localhost\/api\/projects"
            ],
            "skip_null_values": [
              "boolean",
              true
            ],
            "resources": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ],
            "exclude_from_cache_key": [
              "array",
              [
                [
                  "string",
                  "resources"
                ],
                [
                  "string",
                  "resources_to_push"
                ]
              ]
            ],
            "resources_to_push": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ],
            "api_sub_level": [
              "boolean",
              true
            ],
            "jsonld_has_context": [
              "boolean",
              true
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Hydra\\Serializer",
      "short_class": "CollectionNormalizer",
      "class": "ApiPlatform\\Core\\Hydra\\Serializer\\CollectionNormalizer",
      "type": "->",
      "function": "normalize",
      "file": "\/var\/www\/html\/app\/vendor\/api-platform\/core\/src\/Hydra\/Serializer\/PartialCollectionViewNormalizer.php",
      "line": 55,
      "args": [
        [
          "object",
          "ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Paginator"
        ],
        [
          "string",
          "jsonld"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "get"
            ],
            "resource_class": [
              "string",
              "App\\Entity\\Project"
            ],
            "input": [
              "null",
              null
            ],
            "output": [
              "null",
              null
            ],
            "request_uri": [
              "string",
              "\/api\/projects"
            ],
            "uri": [
              "string",
              "http:\/\/localhost\/api\/projects"
            ],
            "skip_null_values": [
              "boolean",
              true
            ],
            "resources": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ],
            "exclude_from_cache_key": [
              "array",
              [
                [
                  "string",
                  "resources"
                ],
                [
                  "string",
                  "resources_to_push"
                ]
              ]
            ],
            "resources_to_push": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ],
            "api_sub_level": [
              "boolean",
              true
            ],
            "jsonld_has_context": [
              "boolean",
              true
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Hydra\\Serializer",
      "short_class": "PartialCollectionViewNormalizer",
      "class": "ApiPlatform\\Core\\Hydra\\Serializer\\PartialCollectionViewNormalizer",
      "type": "->",
      "function": "normalize",
      "file": "\/var\/www\/html\/app\/vendor\/api-platform\/core\/src\/Hydra\/Serializer\/CollectionFiltersNormalizer.php",
      "line": 73,
      "args": [
        [
          "object",
          "ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Paginator"
        ],
        [
          "string",
          "jsonld"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "get"
            ],
            "resource_class": [
              "string",
              "App\\Entity\\Project"
            ],
            "input": [
              "null",
              null
            ],
            "output": [
              "null",
              null
            ],
            "request_uri": [
              "string",
              "\/api\/projects"
            ],
            "uri": [
              "string",
              "http:\/\/localhost\/api\/projects"
            ],
            "skip_null_values": [
              "boolean",
              true
            ],
            "resources": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ],
            "exclude_from_cache_key": [
              "array",
              [
                [
                  "string",
                  "resources"
                ],
                [
                  "string",
                  "resources_to_push"
                ]
              ]
            ],
            "resources_to_push": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\Hydra\\Serializer",
      "short_class": "CollectionFiltersNormalizer",
      "class": "ApiPlatform\\Core\\Hydra\\Serializer\\CollectionFiltersNormalizer",
      "type": "->",
      "function": "normalize",
      "file": "\/var\/www\/html\/app\/vendor\/symfony\/serializer\/Serializer.php",
      "line": 146,
      "args": [
        [
          "object",
          "ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Paginator"
        ],
        [
          "string",
          "jsonld"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "get"
            ],
            "resource_class": [
              "string",
              "App\\Entity\\Project"
            ],
            "input": [
              "null",
              null
            ],
            "output": [
              "null",
              null
            ],
            "request_uri": [
              "string",
              "\/api\/projects"
            ],
            "uri": [
              "string",
              "http:\/\/localhost\/api\/projects"
            ],
            "skip_null_values": [
              "boolean",
              true
            ],
            "resources": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ],
            "exclude_from_cache_key": [
              "array",
              [
                [
                  "string",
                  "resources"
                ],
                [
                  "string",
                  "resources_to_push"
                ]
              ]
            ],
            "resources_to_push": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\Serializer",
      "short_class": "Serializer",
      "class": "Symfony\\Component\\Serializer\\Serializer",
      "type": "->",
      "function": "normalize",
      "file": "\/var\/www\/html\/app\/vendor\/symfony\/serializer\/Serializer.php",
      "line": 119,
      "args": [
        [
          "object",
          "ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Paginator"
        ],
        [
          "string",
          "jsonld"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "get"
            ],
            "resource_class": [
              "string",
              "App\\Entity\\Project"
            ],
            "input": [
              "null",
              null
            ],
            "output": [
              "null",
              null
            ],
            "request_uri": [
              "string",
              "\/api\/projects"
            ],
            "uri": [
              "string",
              "http:\/\/localhost\/api\/projects"
            ],
            "skip_null_values": [
              "boolean",
              true
            ],
            "resources": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ],
            "exclude_from_cache_key": [
              "array",
              [
                [
                  "string",
                  "resources"
                ],
                [
                  "string",
                  "resources_to_push"
                ]
              ]
            ],
            "resources_to_push": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\Serializer",
      "short_class": "Serializer",
      "class": "Symfony\\Component\\Serializer\\Serializer",
      "type": "->",
      "function": "serialize",
      "file": "\/var\/www\/html\/app\/vendor\/api-platform\/core\/src\/EventListener\/SerializeListener.php",
      "line": 95,
      "args": [
        [
          "object",
          "ApiPlatform\\Core\\Bridge\\Doctrine\\Orm\\Paginator"
        ],
        [
          "string",
          "jsonld"
        ],
        [
          "array",
          {
            "operation_type": [
              "string",
              "collection"
            ],
            "collection_operation_name": [
              "string",
              "get"
            ],
            "resource_class": [
              "string",
              "App\\Entity\\Project"
            ],
            "input": [
              "null",
              null
            ],
            "output": [
              "null",
              null
            ],
            "request_uri": [
              "string",
              "\/api\/projects"
            ],
            "uri": [
              "string",
              "http:\/\/localhost\/api\/projects"
            ],
            "skip_null_values": [
              "boolean",
              true
            ],
            "resources": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ],
            "exclude_from_cache_key": [
              "array",
              [
                [
                  "string",
                  "resources"
                ],
                [
                  "string",
                  "resources_to_push"
                ]
              ]
            ],
            "resources_to_push": [
              "object",
              "ApiPlatform\\Core\\Serializer\\ResourceList"
            ]
          }
        ]
      ]
    },
    {
      "namespace": "ApiPlatform\\Core\\EventListener",
      "short_class": "SerializeListener",
      "class": "ApiPlatform\\Core\\EventListener\\SerializeListener",
      "type": "->",
      "function": "onKernelView",
      "file": "\/var\/www\/html\/app\/vendor\/symfony\/event-dispatcher\/Debug\/WrappedListener.php",
      "line": 117,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\ViewEvent"
        ],
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Debug\\TraceableEventDispatcher"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher\\Debug",
      "short_class": "WrappedListener",
      "class": "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener",
      "type": "->",
      "function": "__invoke",
      "file": "\/var\/www\/html\/app\/vendor\/symfony\/event-dispatcher\/EventDispatcher.php",
      "line": 230,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\ViewEvent"
        ],
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Debug\\TraceableEventDispatcher"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher",
      "short_class": "EventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher",
      "type": "->",
      "function": "callListeners",
      "file": "\/var\/www\/html\/app\/vendor\/symfony\/event-dispatcher\/EventDispatcher.php",
      "line": 59,
      "args": [
        [
          "array",
          [
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ],
            [
              "object",
              "Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener"
            ]
          ]
        ],
        [
          "string",
          "kernel.view"
        ],
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\ViewEvent"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher",
      "short_class": "EventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\EventDispatcher",
      "type": "->",
      "function": "dispatch",
      "file": "\/var\/www\/html\/app\/vendor\/symfony\/event-dispatcher\/Debug\/TraceableEventDispatcher.php",
      "line": 151,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\ViewEvent"
        ],
        [
          "string",
          "kernel.view"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\EventDispatcher\\Debug",
      "short_class": "TraceableEventDispatcher",
      "class": "Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher",
      "type": "->",
      "function": "dispatch",
      "file": "\/var\/www\/html\/app\/vendor\/symfony\/http-kernel\/HttpKernel.php",
      "line": 162,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpKernel\\Event\\ViewEvent"
        ],
        [
          "string",
          "kernel.view"
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "HttpKernel",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->",
      "function": "handleRaw",
      "file": "\/var\/www\/html\/app\/vendor\/symfony\/http-kernel\/HttpKernel.php",
      "line": 79,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "integer",
          1
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "HttpKernel",
      "class": "Symfony\\Component\\HttpKernel\\HttpKernel",
      "type": "->",
      "function": "handle",
      "file": "\/var\/www\/html\/app\/vendor\/symfony\/http-kernel\/Kernel.php",
      "line": 191,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ],
        [
          "integer",
          1
        ],
        [
          "boolean",
          true
        ]
      ]
    },
    {
      "namespace": "Symfony\\Component\\HttpKernel",
      "short_class": "Kernel",
      "class": "Symfony\\Component\\HttpKernel\\Kernel",
      "type": "->",
      "function": "handle",
      "file": "\/var\/www\/html\/app\/public\/index.php",
      "line": 25,
      "args": [
        [
          "object",
          "Symfony\\Component\\HttpFoundation\\Request"
        ]
      ]
    }
  ]
}

The problem seems to be that getItemIriFromResourceClass() method in vendor/api-platform/core/src/Bridge/Symfony/Routing/IriConverter.php recives an empty value when a Node does not have a parent-child relation with other node.

Does anybody know how to make this work?. Any help is appreciated. Thanks in advance.

References:

Reinventia
  • 21
  • 6

1 Answers1

1

Remove property $id and function getId() from child class. It worked for me.