我有一个抽象实体“节点”超类,下面的代码是其他四个实体子类的基础。我想以任何顺序嵌套它们以创建一个松散的层次结构,其中任何类型的节点都可以是任何其他类型的父节点或子节点,并且任何节点都可以有多个父节点。
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;
}
}
此类由其他四个实体扩展,它们在 Symfony 5 项目中使用 Api 平台作为 ApiResource 公开。它们都有几乎相同的代码:
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;
}
}
暴露的实体显示在 Api Platform Dashboard 中,如果抽象 Node 实体没有自父子关系,可以查询成功。换句话说,如果我从 Node 类中删除所有 get/add/removeXXXXNodes 方法和相关变量,API 就可以工作。
但如果存在关系,则会出现 400 错误:
获取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"
]
]
}
]
}
问题似乎是 vendor/api-platform/core/src/Bridge/Symfony/Routing/IriConverter.php 中的 getItemIriFromResourceClass() 方法在节点与其他节点没有父子关系时接收空值。
有谁知道如何使这项工作?任何帮助表示赞赏。提前致谢。
参考: