如何在 API-Platform 中使用继承的 类
How to use inherited classes with API-Platform
我希望使用 API-Platform 对对象层次结构 classes 执行 CRUD 操作。当使用带有 API-Platform 的继承 classes 时,我发现写得很少,当与 Symfony 的序列化程序一起使用时,我发现写的很少,但我正在寻找更好的方向,以了解需要以不同方式实现的内容,特别是对于继承 classes.
假设我从 Animal 继承了 Dog、Cat 和 Mouse,其中 Animal 是抽象的(见下文)。这些实体是使用 bin/console make:entity
创建的,仅进行了修改以扩展父 class(以及它们各自的存储库)并添加了 Api-Platform 注释。
组应该如何与继承的 classes 一起使用?每个子 classes(即狗、猫、老鼠)是否应该有自己的组,还是应该只使用父 animal
组?当全部使用 animal
组时,一些路由会响应 The total number of joined relations has exceeded the specified maximum. ...
,而当混合使用时,有时会得到 Association name expected, 'miceEaten' is not an association.
。这些组是否也允许 Api 父实体的属性应用于子实体(即 Animal::weight 的默认 openapi_context 示例值为 1000)?
API-平台不讨论 CTI 或 STI,我在文档中找到的唯一相关参考是关于 MappedSuperclass。除了 CLI 或 STI 之外,还需要使用 MappedSuperclass 吗?请注意,我尝试将 MappedSuperclass
应用于 Animal
,但收到了预期的错误。
基于this post as well as others, it appears that the preferred RESTful implementation is to use a single endpoint /animals
instead of individual /dogs
, /cats
, and /mice
. Agree? How could this be implemented with API-Platform? If the @ApiResource()
annotation is applied only to Animal, I get this single desired URL but don't get the child properties for Dog, Cat, and Mouse in the OpenAPI Swagger documentation nor the actual request. If the @ApiResource()
annotation is applied only to Dog, Cat, and Mouse, then there is no way to get a combined collection of all animals and I have multiple endpoints. Need it be applied to all three? It appears that OpenApi's key words oneOf
, allOf
, and anyOf
might provide a solution as described by this Whosebug answer as well as this Open-Api specification。 Api-平台是否支持此功能?如果支持,如何支持?
动物
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use App\Repository\AnimalRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"animal:read", "dog:read", "cat:read", "mouse:read"}},
* denormalizationContext={"groups"={"animal:write", "dog:write", "cat:write", "mouse:write"}}
* )
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="type", type="string", length=32)
* @ORM\DiscriminatorMap({"dog" = "Dog", "cat" = "Cat", "mouse" = "Mouse"})
* @ORM\Entity(repositoryClass=AnimalRepository::class)
*/
abstract class Animal
{
/**
* @Groups({"animal:read"})
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @Groups({"animal:read", "animal:write"})
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @Groups({"animal:read", "animal:write"})
* @ORM\Column(type="string", length=255)
*/
private $sex;
/**
* @Groups({"animal:read", "animal:write"})
* @ORM\Column(type="integer")
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"=1000
* }
* }
* )
*/
private $weight;
/**
* @Groups({"animal:read", "animal:write"})
* @ORM\Column(type="date")
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"="2020/1/1"
* }
* }
* )
*/
private $birthday;
/**
* @Groups({"animal:read", "animal:write"})
* @ORM\Column(type="string", length=255)
*/
private $color;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getSex(): ?string
{
return $this->sex;
}
public function setSex(string $sex): self
{
$this->sex = $sex;
return $this;
}
public function getWeight(): ?int
{
return $this->weight;
}
public function setWeight(int $weight): self
{
$this->weight = $weight;
return $this;
}
public function getBirthday(): ?\DateTimeInterface
{
return $this->birthday;
}
public function setBirthday(\DateTimeInterface $birthday): self
{
$this->birthday = $birthday;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): self
{
$this->color = $color;
return $this;
}
}
狗
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use App\Repository\DogRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"dog:read"}},
* denormalizationContext={"groups"={"dog:write"}}
* )
* @ORM\Entity(repositoryClass=DogRepository::class)
*/
class Dog extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"dog:read", "dog:write"})
*/
private $playsFetch;
/**
* @ORM\Column(type="string", length=255)
* @Groups({"dog:read", "dog:write"})
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"="red"
* }
* }
* )
*/
private $doghouseColor;
/**
* #@ApiSubresource()
* @ORM\ManyToMany(targetEntity=Cat::class, mappedBy="dogsChasedBy")
* @MaxDepth(2)
* @Groups({"dog:read", "dog:write"})
*/
private $catsChased;
public function __construct()
{
$this->catsChased = new ArrayCollection();
}
public function getPlaysFetch(): ?bool
{
return $this->playsFetch;
}
public function setPlaysFetch(bool $playsFetch): self
{
$this->playsFetch = $playsFetch;
return $this;
}
public function getDoghouseColor(): ?string
{
return $this->doghouseColor;
}
public function setDoghouseColor(string $doghouseColor): self
{
$this->doghouseColor = $doghouseColor;
return $this;
}
/**
* @return Collection|Cat[]
*/
public function getCatsChased(): Collection
{
return $this->catsChased;
}
public function addCatsChased(Cat $catsChased): self
{
if (!$this->catsChased->contains($catsChased)) {
$this->catsChased[] = $catsChased;
$catsChased->addDogsChasedBy($this);
}
return $this;
}
public function removeCatsChased(Cat $catsChased): self
{
if ($this->catsChased->removeElement($catsChased)) {
$catsChased->removeDogsChasedBy($this);
}
return $this;
}
}
猫
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use App\Repository\CatRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"cat:read"}},
* denormalizationContext={"groups"={"cat:write"}}
* )
* @ORM\Entity(repositoryClass=CatRepository::class)
*/
class Cat extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"cat:read", "cat:write"})
*/
private $likesToPurr;
/**
* #@ApiSubresource()
* @ORM\OneToMany(targetEntity=Mouse::class, mappedBy="ateByCat")
* @MaxDepth(2)
* @Groups({"cat:read", "cat:write"})
*/
private $miceEaten;
/**
* #@ApiSubresource()
* @ORM\ManyToMany(targetEntity=Dog::class, inversedBy="catsChased")
* @MaxDepth(2)
* @Groups({"cat:read", "cat:write"})
*/
private $dogsChasedBy;
public function __construct()
{
$this->miceEaten = new ArrayCollection();
$this->dogsChasedBy = new ArrayCollection();
}
public function getLikesToPurr(): ?bool
{
return $this->likesToPurr;
}
public function setLikesToPurr(bool $likesToPurr): self
{
$this->likesToPurr = $likesToPurr;
return $this;
}
/**
* @return Collection|Mouse[]
*/
public function getMiceEaten(): Collection
{
return $this->miceEaten;
}
public function addMiceEaten(Mouse $miceEaten): self
{
if (!$this->miceEaten->contains($miceEaten)) {
$this->miceEaten[] = $miceEaten;
$miceEaten->setAteByCat($this);
}
return $this;
}
public function removeMiceEaten(Mouse $miceEaten): self
{
if ($this->miceEaten->removeElement($miceEaten)) {
// set the owning side to null (unless already changed)
if ($miceEaten->getAteByCat() === $this) {
$miceEaten->setAteByCat(null);
}
}
return $this;
}
/**
* @return Collection|Dog[]
*/
public function getDogsChasedBy(): Collection
{
return $this->dogsChasedBy;
}
public function addDogsChasedBy(Dog $dogsChasedBy): self
{
if (!$this->dogsChasedBy->contains($dogsChasedBy)) {
$this->dogsChasedBy[] = $dogsChasedBy;
}
return $this;
}
public function removeDogsChasedBy(Dog $dogsChasedBy): self
{
$this->dogsChasedBy->removeElement($dogsChasedBy);
return $this;
}
}
鼠标
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use App\Repository\MouseRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"mouse:read"}},
* denormalizationContext={"groups"={"mouse:write"}}
* )
* @ORM\Entity(repositoryClass=MouseRepository::class)
*/
class Mouse extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"mouse:read", "mouse:write"})
*/
private $likesCheese;
/**
* #@ApiSubresource()
* @ORM\ManyToOne(targetEntity=Cat::class, inversedBy="miceEaten")
* @MaxDepth(2)
* @Groups({"mouse:read", "mouse:write"})
*/
private $ateByCat;
public function getLikesCheese(): ?bool
{
return $this->likesCheese;
}
public function setLikesCheese(bool $likesCheese): self
{
$this->likesCheese = $likesCheese;
return $this;
}
public function getAteByCat(): ?Cat
{
return $this->ateByCat;
}
public function setAteByCat(?Cat $ateByCat): self
{
$this->ateByCat = $ateByCat;
return $this;
}
}
MetaClass 回答的补充信息
以下是我的存储库方法,关键要点是最具体的 class 在构造函数中设置实体。
class AnimalRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry, ?string $class=null)
{
parent::__construct($registry, $class??Animal::class);
}
}
class DogRepository extends AnimalRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Dog::class);
}
}
// Cat and Mouse Repository similar
我希望遵循“REST 的普遍偏好,通常使用单个端点 /animals”,但理解您“为 /dogs、/cats 和 /mice 选择单个端点”的理由。为了克服你的理由,我还考虑过使 Animal 具体化并使用组合来实现多态性,这样 Animal 就会有某种动物类型的对象。我想最终仍然需要 Doctrine 继承来允许 Animal 与这个对象建立一对一的关系,但唯一的属性是 PK ID 和鉴别器。我可能会放弃这个追求。
不确定我是否同意你不使用 denormalizationContext 的方法,但我会采用你的方法,除非情况发生变化并且我需要更大的灵活性。
我不明白你对标签的使用。起初我认为这是一些唯一的标识符,或者可能是某种暴露鉴别器的方法,但我不这么认为。请详细说明。
关于“为了避免在每个具体子 class 中重复这些属性的定义,我使用 yaml 添加了一些组”,我的方法是为抽象动物 class 设置属性,而不是private 以便 PHP 可以使用反射,并在抽象 Animal 中使用组“animal:read”,在单个具体 classes 中使用组“mouse:read”等,并得到了我想要的结果。
是的,请参阅您关于限制列表与详细信息的结果的观点。
我原以为 @MaxDepth
可以解决递归问题,但无法正常工作。然而,起作用的是使用 @ApiProperty(readableLink=false)
.
我发现有些情况下 API-平台生成的 swagger 规范在 SwaggerUI 中显示 anyOf
,但同意 API-平台似乎并不真正支持 oneOf、allOf 或任何。然而,不知何故,是否需要实施这一点?例如,动物 ID 在其他 table 中,文档需要是猫、狗或老鼠中的一个,不是吗?或者这个长长的类型列表是由序列化组的每种组合产生的吗?
我认为在这个主题上没有可靠的来源,但我确实有 long experience with frameworks, abstract user interfaces and php and created MetaClass Tutorial Api Platform 所以我会尝试自己回答你的问题。
本教程旨在涵盖 api 平台 api 和使用 api 平台客户端生成器生成的 React 客户端的大多数 CRUD 和搜索应用程序的共同点。本教程不涉及继承和多态性,因为我认为它不会出现在许多 CRUD 和搜索应用程序中,但它解决了很多方面的问题,有关概述,请参阅本教程附带的 readme of the master branch. Api Platform offers a lot of generic functionality for the api of such apps out of the box that only needs to be configured for specific resources and operations. In the react branches this led to recurring patterns and refactoring into common components and eventually to an extended react client generator 中的章节列表。这个答案中连载组的方案有点通用,因为我对这个主题的理解随着时间的推移而提高。
您的 classes 在 Api Platform 2.6 上开箱即用,但存储库 classes 不包括在内。我从注释中删除了它们,因为现在 none 似乎调用了它们的特定方法。您可以随时在需要时再次添加它们。
反对 REST 通常使用单个端点 /animals 的普遍偏好,我为 /dogs、/cats 和 /mice 选择了单个端点,因为:
- Api 平台通过 iri 识别资源实例 classes 引用这些特定端点,并在序列化这些实例时将它们作为 @id 的值包含在内。客户端生成器,我想管理客户端也依赖于这些端点来进行 crud 操作,
- 使用 Api 特定于平台的 post 操作可以开箱即用地使用 doctrine orm。一个端点 /animals 需要一个自定义的 Denormalizer,它可以决定要实例化哪个具体 class。
- 通过序列化组,特定端点可以更好地控制序列化。否则很难使序列化与教程第 4 章中的方式兼容,
- 在 Api 平台的许多 extension points 中,很容易让特定资源工作,文档中的所有示例都使用了它。使它们特定于手头对象的实际具体子 class 没有记录,而且可能并非总是可行。
我只包含 /animals get collection 操作,因为它允许客户端在单个请求中检索、搜索和排序动物的多态集合。
根据教程的第 4 章,我删除了写入注释组。 Api 平台反序列化已经允许客户端仅包含那些带有 post、put 和 patch 的属性,这些属性保存数据并且应该被设置,因此反序列化组的唯一目的可能是禁止某些属性被通过 api 的(某些操作)设置或允许通过嵌套文档创建相关对象。当我尝试通过 post 将其作为鼠标的 $ateByCat 值添加一只新猫时,出现错误“不允许属性“ateByCat”的嵌套文档。请改用 IRI。”通过 Dog::$catsChased 添加一个时也会发生同样的情况,因此在没有编写注释组的情况下,通过授予某些角色的操作的安全性似乎不会受到损害。对我来说似乎是默认的声音。
我向 Animal 添加了一个 ::getLabel 方法以用单个字符串表示每个(注释为 http://schema.org/name)。基本 CRUD 和搜索客户端主要向用户显示单一类型的实体并以这种方式表示相关实体。有一个特定的模式。org/name 属性 对客户端来说更方便,使它成为派生的 属性 比根据实体类型添加不同的属性更灵活。标签 属性 是唯一添加到“相关”组的 属性。该组被添加到每种类型的规范化上下文中,因此对于 Cat、Doc 和 Mouse 的“get”操作,它是唯一的 属性 相关对象的序列化对象:
{
"@context": "/contexts/Cat",
"@id": "/cats/1",
"@type": "Cat",
"likesToPurr": true,
"miceEaten": [
{
"@id": "/mice/3",
"@type": "Mouse",
"label": "2021-01-13"
}
],
"dogsChasedBy": [
{
"@id": "/dogs/2",
"@type": "Dog",
"label": "Bella"
}
],
"name": "Felix",
"sex": "m",
"weight": 12,
"birthday": "2020-03-13T00:00:00+00:00",
"color": "grey",
"label": "Felix"
}
为了得到这个结果,我必须使继承属性的序列化组特定于具体的子classes。为了避免在每个具体的 subclass 中重复这些属性的定义,我使用 yaml 添加了一些组(添加在这个答案的底部)。为了使它们工作,将以下内容添加到 api/config/packages/framework.yaml:
serializer:
mapping:
paths: ['%kernel.project_dir%/config/serialization']
yaml 配置与注释很好地融合在一起,并且只覆盖 Animal class.
根据本教程的第 4 章,我还为一组更有限的属性添加了列表组,这些属性将包含在获取集合操作的结果中。当实体集合呈现给用户时,信息量很快就会变得多余 and/or 布满屏幕,即使有分页也是如此。如果 api 开发人员清楚客户端的目的,在 api 中进行选择将加快数据传输速度,尤其是在省略了对多关系的情况下。这导致像这样的一组老鼠的序列化:
{
"@context": "/contexts/Mouse",
"@id": "/mice",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/mice/3",
"@type": "Mouse",
"ateByCat": {
"@id": "/cats/1",
"@type": "Cat",
"label": "Felix"
},
"label": "2021-01-13",
"name": "mimi",
"birthday": "2021-01-13T00:00:00+00:00",
"color": "grey"
}
],
"hydra:totalItems": 1
}
get /animals 的序列化配置是一种妥协。如果我包括所有 subclasses 的列表组:
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"cat:list", "dog:list", "mouse:list", "related"}}
* },
* },
我得到了一个很好的多态响应,但相关对象还包含其类型列表组的所有属性,而不仅仅是标签:
{
"@context": "/contexts/Animal",
"@id": "/animals",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/cats/1",
"@type": "Cat",
"likesToPurr": true,
"name": "Felix",
"birthday": "2020-03-13T00:00:00+00:00",
"color": "grey",
"label": "Felix"
},
{
"@id": "/dogs/2",
"@type": "Dog",
"playsFetch": true,
"name": "Bella",
"birthday": "2019-03-13T00:00:00+00:00",
"color": "brown",
"label": "Bella"
},
{
"@id": "/mice/3",
"@type": "Mouse",
"ateByCat": {
"@id": "/cats/1",
"@type": "Cat",
"likesToPurr": true,
"name": "Felix",
"birthday": "2020-03-13T00:00:00+00:00",
"color": "grey",
"label": "Felix"
},
"label": "2021-01-13",
"name": "mimi",
"birthday": "2021-01-13T00:00:00+00:00",
"color": "grey"
}
],
"hydra:totalItems": 3
}
这对于手头的例子来说很好,但是随着更多的关系,它可能会变得有点大,所以为了一般的妥协,我只包括“动物:列表”和“推荐”,从而导致更小的响应:
{
"@context": "/contexts/Animal",
"@id": "/animals",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/cats/1",
"@type": "Cat",
"name": "Felix",
"color": "grey",
"label": "Felix"
},
{
"@id": "/dogs/2",
"@type": "Dog",
"name": "Bella",
"color": "brown",
"label": "Bella"
},
{
"@id": "/mice/3",
"@type": "Mouse",
"ateByCat": {
"@id": "/cats/1",
"@type": "Cat",
"name": "Felix",
"color": "grey",
"label": "Felix"
},
"label": "2021-01-13",
"name": "mimi",
"color": "grey"
}
],
"hydra:totalItems": 3
}
如您所见,多态性仍然是可能的 (ateByCat),问题确实变小了,但并没有消失。这个问题不能用序列化组来解决,因为从序列化上下文来看,猫吃老鼠的关系是递归的。更好的解决方案可能是为一对一递归关系的属性装饰 api_platform.serializer.context_builder to add a custom callback ,但是序列化递归关系的问题并不特定于继承,因此超出了这个问题的范围,所以现在我不详细说明这个解决方案。
Api 平台 2.6 不支持 oneOf、allOf 或 anyOf。相反,它会生成相当长的类型列表,这些类型是由所使用的序列化组的每种组合产生的,每个类型都在一个平面列表中包含所有包含的属性。生成的 json 恕我直言太大,无法包含在此处,因此我只包含类型名称列表:
Animal-animal.list_related
Animal.jsonld-animal.list_related
Cat
Cat-cat.list_related
Cat-cat.read_cat.list_related
Cat-dog.read_dog.list_related
Cat-mouse.list_related
Cat-mouse.read_mouse.list_related
Cat.jsonld
Cat.jsonld-cat.list_related
Cat.jsonld-cat.read_cat.list_related
Cat.jsonld-dog.read_dog.list_related
Cat.jsonld-mouse.list_related
Cat.jsonld-mouse.read_mouse.list_related
Dog
Dog-cat.read_cat.list_related
Dog-dog.list_related
Dog-dog.read_dog.list_related
Dog.jsonld
Dog.jsonld-cat.read_cat.list_related
Dog.jsonld-dog.list_related
Dog.jsonld-dog.read_dog.list_related
Greeting
Greeting.jsonld
Mouse
Mouse-cat.read_cat.list_related
Mouse-mouse.list_related
Mouse-mouse.read_mouse.list_related
Mouse.jsonld
Mouse.jsonld-cat.read_cat.list_related
Mouse.jsonld-mouse.list_related
Mouse.jsonld-mouse.read_mouse.list_related
如果您将下面的代码粘贴到 api 平台标准版的相应文件中并进行描述的配置,您应该能够从 https://[= 检索整个 openapi 方案79=].json
代码
<?php
// api/src/Entity/Animal.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"animal:list", "related"}}
* },
* },
* itemOperations={},
* )
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="type", type="string", length=32)
* @ORM\DiscriminatorMap({"dog" = "Dog", "cat" = "Cat", "mouse" = "Mouse"})
* @ORM\Entity()
*/
abstract class Animal
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
* @Groups({"animal:list"})
*/
private $name;
/**
* @ORM\Column(type="string", length=255)
*/
private $sex;
/**
* @ORM\Column(type="integer")
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"=1000
* }
* }
* )
*/
private $weight;
/**
* @ORM\Column(type="date")
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"="2020/1/1"
* }
* }
* )
*/
private $birthday;
/**
* @ORM\Column(type="string", length=255)
* @Groups({"animal:list"})
*/
private $color;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getSex(): ?string
{
return $this->sex;
}
public function setSex(string $sex): self
{
$this->sex = $sex;
return $this;
}
public function getWeight(): ?int
{
return $this->weight;
}
public function setWeight(int $weight): self
{
$this->weight = $weight;
return $this;
}
public function getBirthday(): ?\DateTimeInterface
{
return $this->birthday;
}
public function setBirthday(\DateTimeInterface $birthday): self
{
$this->birthday = $birthday;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): self
{
$this->color = $color;
return $this;
}
/**
* Represent the entity to the user in a single string
* @return string
* @ApiProperty(iri="http://schema.org/name")
* @Groups({"related"})
*/
function getLabel() {
return $this->getName();
}
}
<?php
// api/src/Entity/Cat.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"cat:list", "related"}}
* },
* "post"
* },
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"cat:read", "cat:list", "related"}}
* )
* @ORM\Entity()
*/
class Cat extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"cat:list"})
*/
private $likesToPurr;
/**
* #@ApiSubresource()
* @ORM\OneToMany(targetEntity=Mouse::class, mappedBy="ateByCat")
* @MaxDepth(2)
* @Groups({"cat:read"})
*/
private $miceEaten;
/**
* #@ApiSubresource()
* @ORM\ManyToMany(targetEntity=Dog::class, inversedBy="catsChased")
* @MaxDepth(2)
* @Groups({"cat:read"})
*/
private $dogsChasedBy;
public function __construct()
{
$this->miceEaten = new ArrayCollection();
$this->dogsChasedBy = new ArrayCollection();
}
public function getLikesToPurr(): ?bool
{
return $this->likesToPurr;
}
public function setLikesToPurr(bool $likesToPurr): self
{
$this->likesToPurr = $likesToPurr;
return $this;
}
/**
* @return Collection|Mouse[]
*/
public function getMiceEaten(): Collection
{
return $this->miceEaten;
}
public function addMiceEaten(Mouse $miceEaten): self
{
if (!$this->miceEaten->contains($miceEaten)) {
$this->miceEaten[] = $miceEaten;
$miceEaten->setAteByCat($this);
}
return $this;
}
public function removeMiceEaten(Mouse $miceEaten): self
{
if ($this->miceEaten->removeElement($miceEaten)) {
// set the owning side to null (unless already changed)
if ($miceEaten->getAteByCat() === $this) {
$miceEaten->setAteByCat(null);
}
}
return $this;
}
/**
* @return Collection|Dog[]
*/
public function getDogsChasedBy(): Collection
{
return $this->dogsChasedBy;
}
public function addDogsChasedBy(Dog $dogsChasedBy): self
{
if (!$this->dogsChasedBy->contains($dogsChasedBy)) {
$this->dogsChasedBy[] = $dogsChasedBy;
}
return $this;
}
public function removeDogsChasedBy(Dog $dogsChasedBy): self
{
$this->dogsChasedBy->removeElement($dogsChasedBy);
return $this;
}
}
<?php
// api/src/Entity/Dog.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"dog:list", "related"}}
* },
* "post"
* },
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"dog:read", "dog:list", "related"}},
* )
* @ORM\Entity()
*/
class Dog extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"dog:list"})
*/
private $playsFetch;
/**
* @ORM\Column(type="string", length=255)
* @Groups({"dog:read"})
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"="red"
* }
* }
* )
*/
private $doghouseColor;
/**
* #@ApiSubresource()
* @ORM\ManyToMany(targetEntity=Cat::class, mappedBy="dogsChasedBy")
* @MaxDepth(2)
* @Groups({"dog:read"})
*/
private $catsChased;
public function __construct()
{
$this->catsChased = new ArrayCollection();
}
public function getPlaysFetch(): ?bool
{
return $this->playsFetch;
}
public function setPlaysFetch(bool $playsFetch): self
{
$this->playsFetch = $playsFetch;
return $this;
}
public function getDoghouseColor(): ?string
{
return $this->doghouseColor;
}
public function setDoghouseColor(string $doghouseColor): self
{
$this->doghouseColor = $doghouseColor;
return $this;
}
/**
* @return Collection|Cat[]
*/
public function getCatsChased(): Collection
{
return $this->catsChased;
}
public function addCatsChased(Cat $catsChased): self
{
if (!$this->catsChased->contains($catsChased)) {
$this->catsChased[] = $catsChased;
$catsChased->addDogsChasedBy($this);
}
return $this;
}
public function removeCatsChased(Cat $catsChased): self
{
if ($this->catsChased->removeElement($catsChased)) {
$catsChased->removeDogsChasedBy($this);
}
return $this;
}
}
<?php
// api/src/Entity/Mouse.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"mouse:list", "related"}}
* },
* "post"
* },
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"mouse:read", "mouse:list", "related"}},
* )
* @ORM\Entity()
*/
class Mouse extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"mouse:read"})
*/
private $likesCheese;
/**
* #@ApiSubresource()
* @ORM\ManyToOne(targetEntity=Cat::class, inversedBy="miceEaten")
* @MaxDepth(2)
* @Groups({"mouse:list", "animal:list"})
*/
private $ateByCat;
public function getLikesCheese(): ?bool
{
return $this->likesCheese;
}
public function setLikesCheese(bool $likesCheese): self
{
$this->likesCheese = $likesCheese;
return $this;
}
public function getAteByCat(): ?Cat
{
return $this->ateByCat;
}
public function setAteByCat(?Cat $ateByCat): self
{
$this->ateByCat = $ateByCat;
return $this;
}
/**
* Represent the entity to the user in a single string
* @return string
* @ApiProperty(iri="http://schema.org/name")
* @Groups({"related"})
*/
function getLabel() {
return $this->getBirthday()->format('Y-m-d');
}
}
# api/config/serialization/Cat.yaml
App\Entity\Cat:
attributes:
name:
groups: ['cat:list']
sex:
groups: ['cat:read']
weight:
groups: ['cat:read']
birthday:
groups: ['cat:list']
color:
groups: ['cat:list']
# api/config/serialization/Dog.yaml
App\Entity\Dog:
attributes:
name:
groups: ['dog:list']
sex:
groups: ['dog:read']
weight:
groups: ['dog:read']
birthday:
groups: ['dog:list']
color:
groups: ['dog:list']
# api/config/serialization/Mouse.yaml
App\Entity\Mouse:
attributes:
name:
groups: ['mouse:list']
sex:
groups: ['mouse:read']
weight:
groups: ['mouse:read']
birthday:
groups: ['mouse:list']
color:
groups: ['mouse:list']
对补充信息的反应
关于标签的使用,请参阅 the tutorial 的第 4 章(两个分支的自述文件)。 ::getLabel 方法也带来了封装:可以在不改变 api.
的情况下修改表示
关于 oneOf、allOf 或 anyOf:Apip 生成的一长串类型很难看,但我想它会是
为想要自动验证 属性 值和抽象用户界面(如管理客户端)的客户端工作。对于 designing/scaffolding 客户端和自定义抽象用户界面,它们可能会很麻烦,所以如果 Api 平台能够自动适当地使用它们会很好,但对于大多数开发团队来说,我不认为投资投入改进 OpenApi 文档工厂将获得回报。换句话说,手动调整客户端通常工作量较小。所以现在我不会花任何时间在这上面。
更有问题的是,在 JsonLD 文档中,使用“output”指定的操作类型的属性=被合并到资源本身的类型中。但这与继承无关。
我希望使用 API-Platform 对对象层次结构 classes 执行 CRUD 操作。当使用带有 API-Platform 的继承 classes 时,我发现写得很少,当与 Symfony 的序列化程序一起使用时,我发现写的很少,但我正在寻找更好的方向,以了解需要以不同方式实现的内容,特别是对于继承 classes.
假设我从 Animal 继承了 Dog、Cat 和 Mouse,其中 Animal 是抽象的(见下文)。这些实体是使用 bin/console make:entity
创建的,仅进行了修改以扩展父 class(以及它们各自的存储库)并添加了 Api-Platform 注释。
组应该如何与继承的 classes 一起使用?每个子 classes(即狗、猫、老鼠)是否应该有自己的组,还是应该只使用父 animal
组?当全部使用 animal
组时,一些路由会响应 The total number of joined relations has exceeded the specified maximum. ...
,而当混合使用时,有时会得到 Association name expected, 'miceEaten' is not an association.
。这些组是否也允许 Api 父实体的属性应用于子实体(即 Animal::weight 的默认 openapi_context 示例值为 1000)?
API-平台不讨论 CTI 或 STI,我在文档中找到的唯一相关参考是关于 MappedSuperclass。除了 CLI 或 STI 之外,还需要使用 MappedSuperclass 吗?请注意,我尝试将 MappedSuperclass
应用于 Animal
,但收到了预期的错误。
基于this post as well as others, it appears that the preferred RESTful implementation is to use a single endpoint /animals
instead of individual /dogs
, /cats
, and /mice
. Agree? How could this be implemented with API-Platform? If the @ApiResource()
annotation is applied only to Animal, I get this single desired URL but don't get the child properties for Dog, Cat, and Mouse in the OpenAPI Swagger documentation nor the actual request. If the @ApiResource()
annotation is applied only to Dog, Cat, and Mouse, then there is no way to get a combined collection of all animals and I have multiple endpoints. Need it be applied to all three? It appears that OpenApi's key words oneOf
, allOf
, and anyOf
might provide a solution as described by this Whosebug answer as well as this Open-Api specification。 Api-平台是否支持此功能?如果支持,如何支持?
动物
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use App\Repository\AnimalRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"animal:read", "dog:read", "cat:read", "mouse:read"}},
* denormalizationContext={"groups"={"animal:write", "dog:write", "cat:write", "mouse:write"}}
* )
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="type", type="string", length=32)
* @ORM\DiscriminatorMap({"dog" = "Dog", "cat" = "Cat", "mouse" = "Mouse"})
* @ORM\Entity(repositoryClass=AnimalRepository::class)
*/
abstract class Animal
{
/**
* @Groups({"animal:read"})
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @Groups({"animal:read", "animal:write"})
* @ORM\Column(type="string", length=255)
*/
private $name;
/**
* @Groups({"animal:read", "animal:write"})
* @ORM\Column(type="string", length=255)
*/
private $sex;
/**
* @Groups({"animal:read", "animal:write"})
* @ORM\Column(type="integer")
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"=1000
* }
* }
* )
*/
private $weight;
/**
* @Groups({"animal:read", "animal:write"})
* @ORM\Column(type="date")
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"="2020/1/1"
* }
* }
* )
*/
private $birthday;
/**
* @Groups({"animal:read", "animal:write"})
* @ORM\Column(type="string", length=255)
*/
private $color;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getSex(): ?string
{
return $this->sex;
}
public function setSex(string $sex): self
{
$this->sex = $sex;
return $this;
}
public function getWeight(): ?int
{
return $this->weight;
}
public function setWeight(int $weight): self
{
$this->weight = $weight;
return $this;
}
public function getBirthday(): ?\DateTimeInterface
{
return $this->birthday;
}
public function setBirthday(\DateTimeInterface $birthday): self
{
$this->birthday = $birthday;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): self
{
$this->color = $color;
return $this;
}
}
狗
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use App\Repository\DogRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"dog:read"}},
* denormalizationContext={"groups"={"dog:write"}}
* )
* @ORM\Entity(repositoryClass=DogRepository::class)
*/
class Dog extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"dog:read", "dog:write"})
*/
private $playsFetch;
/**
* @ORM\Column(type="string", length=255)
* @Groups({"dog:read", "dog:write"})
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"="red"
* }
* }
* )
*/
private $doghouseColor;
/**
* #@ApiSubresource()
* @ORM\ManyToMany(targetEntity=Cat::class, mappedBy="dogsChasedBy")
* @MaxDepth(2)
* @Groups({"dog:read", "dog:write"})
*/
private $catsChased;
public function __construct()
{
$this->catsChased = new ArrayCollection();
}
public function getPlaysFetch(): ?bool
{
return $this->playsFetch;
}
public function setPlaysFetch(bool $playsFetch): self
{
$this->playsFetch = $playsFetch;
return $this;
}
public function getDoghouseColor(): ?string
{
return $this->doghouseColor;
}
public function setDoghouseColor(string $doghouseColor): self
{
$this->doghouseColor = $doghouseColor;
return $this;
}
/**
* @return Collection|Cat[]
*/
public function getCatsChased(): Collection
{
return $this->catsChased;
}
public function addCatsChased(Cat $catsChased): self
{
if (!$this->catsChased->contains($catsChased)) {
$this->catsChased[] = $catsChased;
$catsChased->addDogsChasedBy($this);
}
return $this;
}
public function removeCatsChased(Cat $catsChased): self
{
if ($this->catsChased->removeElement($catsChased)) {
$catsChased->removeDogsChasedBy($this);
}
return $this;
}
}
猫
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use App\Repository\CatRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"cat:read"}},
* denormalizationContext={"groups"={"cat:write"}}
* )
* @ORM\Entity(repositoryClass=CatRepository::class)
*/
class Cat extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"cat:read", "cat:write"})
*/
private $likesToPurr;
/**
* #@ApiSubresource()
* @ORM\OneToMany(targetEntity=Mouse::class, mappedBy="ateByCat")
* @MaxDepth(2)
* @Groups({"cat:read", "cat:write"})
*/
private $miceEaten;
/**
* #@ApiSubresource()
* @ORM\ManyToMany(targetEntity=Dog::class, inversedBy="catsChased")
* @MaxDepth(2)
* @Groups({"cat:read", "cat:write"})
*/
private $dogsChasedBy;
public function __construct()
{
$this->miceEaten = new ArrayCollection();
$this->dogsChasedBy = new ArrayCollection();
}
public function getLikesToPurr(): ?bool
{
return $this->likesToPurr;
}
public function setLikesToPurr(bool $likesToPurr): self
{
$this->likesToPurr = $likesToPurr;
return $this;
}
/**
* @return Collection|Mouse[]
*/
public function getMiceEaten(): Collection
{
return $this->miceEaten;
}
public function addMiceEaten(Mouse $miceEaten): self
{
if (!$this->miceEaten->contains($miceEaten)) {
$this->miceEaten[] = $miceEaten;
$miceEaten->setAteByCat($this);
}
return $this;
}
public function removeMiceEaten(Mouse $miceEaten): self
{
if ($this->miceEaten->removeElement($miceEaten)) {
// set the owning side to null (unless already changed)
if ($miceEaten->getAteByCat() === $this) {
$miceEaten->setAteByCat(null);
}
}
return $this;
}
/**
* @return Collection|Dog[]
*/
public function getDogsChasedBy(): Collection
{
return $this->dogsChasedBy;
}
public function addDogsChasedBy(Dog $dogsChasedBy): self
{
if (!$this->dogsChasedBy->contains($dogsChasedBy)) {
$this->dogsChasedBy[] = $dogsChasedBy;
}
return $this;
}
public function removeDogsChasedBy(Dog $dogsChasedBy): self
{
$this->dogsChasedBy->removeElement($dogsChasedBy);
return $this;
}
}
鼠标
<?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use App\Repository\MouseRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={"get", "post"},
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"mouse:read"}},
* denormalizationContext={"groups"={"mouse:write"}}
* )
* @ORM\Entity(repositoryClass=MouseRepository::class)
*/
class Mouse extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"mouse:read", "mouse:write"})
*/
private $likesCheese;
/**
* #@ApiSubresource()
* @ORM\ManyToOne(targetEntity=Cat::class, inversedBy="miceEaten")
* @MaxDepth(2)
* @Groups({"mouse:read", "mouse:write"})
*/
private $ateByCat;
public function getLikesCheese(): ?bool
{
return $this->likesCheese;
}
public function setLikesCheese(bool $likesCheese): self
{
$this->likesCheese = $likesCheese;
return $this;
}
public function getAteByCat(): ?Cat
{
return $this->ateByCat;
}
public function setAteByCat(?Cat $ateByCat): self
{
$this->ateByCat = $ateByCat;
return $this;
}
}
MetaClass 回答的补充信息
以下是我的存储库方法,关键要点是最具体的 class 在构造函数中设置实体。
class AnimalRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry, ?string $class=null)
{
parent::__construct($registry, $class??Animal::class);
}
}
class DogRepository extends AnimalRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Dog::class);
}
}
// Cat and Mouse Repository similar
我希望遵循“REST 的普遍偏好,通常使用单个端点 /animals”,但理解您“为 /dogs、/cats 和 /mice 选择单个端点”的理由。为了克服你的理由,我还考虑过使 Animal 具体化并使用组合来实现多态性,这样 Animal 就会有某种动物类型的对象。我想最终仍然需要 Doctrine 继承来允许 Animal 与这个对象建立一对一的关系,但唯一的属性是 PK ID 和鉴别器。我可能会放弃这个追求。
不确定我是否同意你不使用 denormalizationContext 的方法,但我会采用你的方法,除非情况发生变化并且我需要更大的灵活性。
我不明白你对标签的使用。起初我认为这是一些唯一的标识符,或者可能是某种暴露鉴别器的方法,但我不这么认为。请详细说明。
关于“为了避免在每个具体子 class 中重复这些属性的定义,我使用 yaml 添加了一些组”,我的方法是为抽象动物 class 设置属性,而不是private 以便 PHP 可以使用反射,并在抽象 Animal 中使用组“animal:read”,在单个具体 classes 中使用组“mouse:read”等,并得到了我想要的结果。
是的,请参阅您关于限制列表与详细信息的结果的观点。
我原以为 @MaxDepth
可以解决递归问题,但无法正常工作。然而,起作用的是使用 @ApiProperty(readableLink=false)
.
我发现有些情况下 API-平台生成的 swagger 规范在 SwaggerUI 中显示 anyOf
,但同意 API-平台似乎并不真正支持 oneOf、allOf 或任何。然而,不知何故,是否需要实施这一点?例如,动物 ID 在其他 table 中,文档需要是猫、狗或老鼠中的一个,不是吗?或者这个长长的类型列表是由序列化组的每种组合产生的吗?
我认为在这个主题上没有可靠的来源,但我确实有 long experience with frameworks, abstract user interfaces and php and created MetaClass Tutorial Api Platform 所以我会尝试自己回答你的问题。
本教程旨在涵盖 api 平台 api 和使用 api 平台客户端生成器生成的 React 客户端的大多数 CRUD 和搜索应用程序的共同点。本教程不涉及继承和多态性,因为我认为它不会出现在许多 CRUD 和搜索应用程序中,但它解决了很多方面的问题,有关概述,请参阅本教程附带的 readme of the master branch. Api Platform offers a lot of generic functionality for the api of such apps out of the box that only needs to be configured for specific resources and operations. In the react branches this led to recurring patterns and refactoring into common components and eventually to an extended react client generator 中的章节列表。这个答案中连载组的方案有点通用,因为我对这个主题的理解随着时间的推移而提高。
您的 classes 在 Api Platform 2.6 上开箱即用,但存储库 classes 不包括在内。我从注释中删除了它们,因为现在 none 似乎调用了它们的特定方法。您可以随时在需要时再次添加它们。
反对 REST 通常使用单个端点 /animals 的普遍偏好,我为 /dogs、/cats 和 /mice 选择了单个端点,因为:
- Api 平台通过 iri 识别资源实例 classes 引用这些特定端点,并在序列化这些实例时将它们作为 @id 的值包含在内。客户端生成器,我想管理客户端也依赖于这些端点来进行 crud 操作,
- 使用 Api 特定于平台的 post 操作可以开箱即用地使用 doctrine orm。一个端点 /animals 需要一个自定义的 Denormalizer,它可以决定要实例化哪个具体 class。
- 通过序列化组,特定端点可以更好地控制序列化。否则很难使序列化与教程第 4 章中的方式兼容,
- 在 Api 平台的许多 extension points 中,很容易让特定资源工作,文档中的所有示例都使用了它。使它们特定于手头对象的实际具体子 class 没有记录,而且可能并非总是可行。
我只包含 /animals get collection 操作,因为它允许客户端在单个请求中检索、搜索和排序动物的多态集合。
根据教程的第 4 章,我删除了写入注释组。 Api 平台反序列化已经允许客户端仅包含那些带有 post、put 和 patch 的属性,这些属性保存数据并且应该被设置,因此反序列化组的唯一目的可能是禁止某些属性被通过 api 的(某些操作)设置或允许通过嵌套文档创建相关对象。当我尝试通过 post 将其作为鼠标的 $ateByCat 值添加一只新猫时,出现错误“不允许属性“ateByCat”的嵌套文档。请改用 IRI。”通过 Dog::$catsChased 添加一个时也会发生同样的情况,因此在没有编写注释组的情况下,通过授予某些角色的操作的安全性似乎不会受到损害。对我来说似乎是默认的声音。
我向 Animal 添加了一个 ::getLabel 方法以用单个字符串表示每个(注释为 http://schema.org/name)。基本 CRUD 和搜索客户端主要向用户显示单一类型的实体并以这种方式表示相关实体。有一个特定的模式。org/name 属性 对客户端来说更方便,使它成为派生的 属性 比根据实体类型添加不同的属性更灵活。标签 属性 是唯一添加到“相关”组的 属性。该组被添加到每种类型的规范化上下文中,因此对于 Cat、Doc 和 Mouse 的“get”操作,它是唯一的 属性 相关对象的序列化对象:
{
"@context": "/contexts/Cat",
"@id": "/cats/1",
"@type": "Cat",
"likesToPurr": true,
"miceEaten": [
{
"@id": "/mice/3",
"@type": "Mouse",
"label": "2021-01-13"
}
],
"dogsChasedBy": [
{
"@id": "/dogs/2",
"@type": "Dog",
"label": "Bella"
}
],
"name": "Felix",
"sex": "m",
"weight": 12,
"birthday": "2020-03-13T00:00:00+00:00",
"color": "grey",
"label": "Felix"
}
为了得到这个结果,我必须使继承属性的序列化组特定于具体的子classes。为了避免在每个具体的 subclass 中重复这些属性的定义,我使用 yaml 添加了一些组(添加在这个答案的底部)。为了使它们工作,将以下内容添加到 api/config/packages/framework.yaml:
serializer:
mapping:
paths: ['%kernel.project_dir%/config/serialization']
yaml 配置与注释很好地融合在一起,并且只覆盖 Animal class.
根据本教程的第 4 章,我还为一组更有限的属性添加了列表组,这些属性将包含在获取集合操作的结果中。当实体集合呈现给用户时,信息量很快就会变得多余 and/or 布满屏幕,即使有分页也是如此。如果 api 开发人员清楚客户端的目的,在 api 中进行选择将加快数据传输速度,尤其是在省略了对多关系的情况下。这导致像这样的一组老鼠的序列化:
{
"@context": "/contexts/Mouse",
"@id": "/mice",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/mice/3",
"@type": "Mouse",
"ateByCat": {
"@id": "/cats/1",
"@type": "Cat",
"label": "Felix"
},
"label": "2021-01-13",
"name": "mimi",
"birthday": "2021-01-13T00:00:00+00:00",
"color": "grey"
}
],
"hydra:totalItems": 1
}
get /animals 的序列化配置是一种妥协。如果我包括所有 subclasses 的列表组:
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"cat:list", "dog:list", "mouse:list", "related"}}
* },
* },
我得到了一个很好的多态响应,但相关对象还包含其类型列表组的所有属性,而不仅仅是标签:
{
"@context": "/contexts/Animal",
"@id": "/animals",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/cats/1",
"@type": "Cat",
"likesToPurr": true,
"name": "Felix",
"birthday": "2020-03-13T00:00:00+00:00",
"color": "grey",
"label": "Felix"
},
{
"@id": "/dogs/2",
"@type": "Dog",
"playsFetch": true,
"name": "Bella",
"birthday": "2019-03-13T00:00:00+00:00",
"color": "brown",
"label": "Bella"
},
{
"@id": "/mice/3",
"@type": "Mouse",
"ateByCat": {
"@id": "/cats/1",
"@type": "Cat",
"likesToPurr": true,
"name": "Felix",
"birthday": "2020-03-13T00:00:00+00:00",
"color": "grey",
"label": "Felix"
},
"label": "2021-01-13",
"name": "mimi",
"birthday": "2021-01-13T00:00:00+00:00",
"color": "grey"
}
],
"hydra:totalItems": 3
}
这对于手头的例子来说很好,但是随着更多的关系,它可能会变得有点大,所以为了一般的妥协,我只包括“动物:列表”和“推荐”,从而导致更小的响应:
{
"@context": "/contexts/Animal",
"@id": "/animals",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/cats/1",
"@type": "Cat",
"name": "Felix",
"color": "grey",
"label": "Felix"
},
{
"@id": "/dogs/2",
"@type": "Dog",
"name": "Bella",
"color": "brown",
"label": "Bella"
},
{
"@id": "/mice/3",
"@type": "Mouse",
"ateByCat": {
"@id": "/cats/1",
"@type": "Cat",
"name": "Felix",
"color": "grey",
"label": "Felix"
},
"label": "2021-01-13",
"name": "mimi",
"color": "grey"
}
],
"hydra:totalItems": 3
}
如您所见,多态性仍然是可能的 (ateByCat),问题确实变小了,但并没有消失。这个问题不能用序列化组来解决,因为从序列化上下文来看,猫吃老鼠的关系是递归的。更好的解决方案可能是为一对一递归关系的属性装饰 api_platform.serializer.context_builder to add a custom callback ,但是序列化递归关系的问题并不特定于继承,因此超出了这个问题的范围,所以现在我不详细说明这个解决方案。
Api 平台 2.6 不支持 oneOf、allOf 或 anyOf。相反,它会生成相当长的类型列表,这些类型是由所使用的序列化组的每种组合产生的,每个类型都在一个平面列表中包含所有包含的属性。生成的 json 恕我直言太大,无法包含在此处,因此我只包含类型名称列表:
Animal-animal.list_related
Animal.jsonld-animal.list_related
Cat
Cat-cat.list_related
Cat-cat.read_cat.list_related
Cat-dog.read_dog.list_related
Cat-mouse.list_related
Cat-mouse.read_mouse.list_related
Cat.jsonld
Cat.jsonld-cat.list_related
Cat.jsonld-cat.read_cat.list_related
Cat.jsonld-dog.read_dog.list_related
Cat.jsonld-mouse.list_related
Cat.jsonld-mouse.read_mouse.list_related
Dog
Dog-cat.read_cat.list_related
Dog-dog.list_related
Dog-dog.read_dog.list_related
Dog.jsonld
Dog.jsonld-cat.read_cat.list_related
Dog.jsonld-dog.list_related
Dog.jsonld-dog.read_dog.list_related
Greeting
Greeting.jsonld
Mouse
Mouse-cat.read_cat.list_related
Mouse-mouse.list_related
Mouse-mouse.read_mouse.list_related
Mouse.jsonld
Mouse.jsonld-cat.read_cat.list_related
Mouse.jsonld-mouse.list_related
Mouse.jsonld-mouse.read_mouse.list_related
如果您将下面的代码粘贴到 api 平台标准版的相应文件中并进行描述的配置,您应该能够从 https://[= 检索整个 openapi 方案79=].json
代码
<?php
// api/src/Entity/Animal.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"animal:list", "related"}}
* },
* },
* itemOperations={},
* )
* @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="type", type="string", length=32)
* @ORM\DiscriminatorMap({"dog" = "Dog", "cat" = "Cat", "mouse" = "Mouse"})
* @ORM\Entity()
*/
abstract class Animal
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
* @Groups({"animal:list"})
*/
private $name;
/**
* @ORM\Column(type="string", length=255)
*/
private $sex;
/**
* @ORM\Column(type="integer")
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"=1000
* }
* }
* )
*/
private $weight;
/**
* @ORM\Column(type="date")
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"="2020/1/1"
* }
* }
* )
*/
private $birthday;
/**
* @ORM\Column(type="string", length=255)
* @Groups({"animal:list"})
*/
private $color;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getSex(): ?string
{
return $this->sex;
}
public function setSex(string $sex): self
{
$this->sex = $sex;
return $this;
}
public function getWeight(): ?int
{
return $this->weight;
}
public function setWeight(int $weight): self
{
$this->weight = $weight;
return $this;
}
public function getBirthday(): ?\DateTimeInterface
{
return $this->birthday;
}
public function setBirthday(\DateTimeInterface $birthday): self
{
$this->birthday = $birthday;
return $this;
}
public function getColor(): ?string
{
return $this->color;
}
public function setColor(string $color): self
{
$this->color = $color;
return $this;
}
/**
* Represent the entity to the user in a single string
* @return string
* @ApiProperty(iri="http://schema.org/name")
* @Groups({"related"})
*/
function getLabel() {
return $this->getName();
}
}
<?php
// api/src/Entity/Cat.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"cat:list", "related"}}
* },
* "post"
* },
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"cat:read", "cat:list", "related"}}
* )
* @ORM\Entity()
*/
class Cat extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"cat:list"})
*/
private $likesToPurr;
/**
* #@ApiSubresource()
* @ORM\OneToMany(targetEntity=Mouse::class, mappedBy="ateByCat")
* @MaxDepth(2)
* @Groups({"cat:read"})
*/
private $miceEaten;
/**
* #@ApiSubresource()
* @ORM\ManyToMany(targetEntity=Dog::class, inversedBy="catsChased")
* @MaxDepth(2)
* @Groups({"cat:read"})
*/
private $dogsChasedBy;
public function __construct()
{
$this->miceEaten = new ArrayCollection();
$this->dogsChasedBy = new ArrayCollection();
}
public function getLikesToPurr(): ?bool
{
return $this->likesToPurr;
}
public function setLikesToPurr(bool $likesToPurr): self
{
$this->likesToPurr = $likesToPurr;
return $this;
}
/**
* @return Collection|Mouse[]
*/
public function getMiceEaten(): Collection
{
return $this->miceEaten;
}
public function addMiceEaten(Mouse $miceEaten): self
{
if (!$this->miceEaten->contains($miceEaten)) {
$this->miceEaten[] = $miceEaten;
$miceEaten->setAteByCat($this);
}
return $this;
}
public function removeMiceEaten(Mouse $miceEaten): self
{
if ($this->miceEaten->removeElement($miceEaten)) {
// set the owning side to null (unless already changed)
if ($miceEaten->getAteByCat() === $this) {
$miceEaten->setAteByCat(null);
}
}
return $this;
}
/**
* @return Collection|Dog[]
*/
public function getDogsChasedBy(): Collection
{
return $this->dogsChasedBy;
}
public function addDogsChasedBy(Dog $dogsChasedBy): self
{
if (!$this->dogsChasedBy->contains($dogsChasedBy)) {
$this->dogsChasedBy[] = $dogsChasedBy;
}
return $this;
}
public function removeDogsChasedBy(Dog $dogsChasedBy): self
{
$this->dogsChasedBy->removeElement($dogsChasedBy);
return $this;
}
}
<?php
// api/src/Entity/Dog.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"dog:list", "related"}}
* },
* "post"
* },
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"dog:read", "dog:list", "related"}},
* )
* @ORM\Entity()
*/
class Dog extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"dog:list"})
*/
private $playsFetch;
/**
* @ORM\Column(type="string", length=255)
* @Groups({"dog:read"})
* @ApiProperty(
* attributes={
* "openapi_context"={
* "example"="red"
* }
* }
* )
*/
private $doghouseColor;
/**
* #@ApiSubresource()
* @ORM\ManyToMany(targetEntity=Cat::class, mappedBy="dogsChasedBy")
* @MaxDepth(2)
* @Groups({"dog:read"})
*/
private $catsChased;
public function __construct()
{
$this->catsChased = new ArrayCollection();
}
public function getPlaysFetch(): ?bool
{
return $this->playsFetch;
}
public function setPlaysFetch(bool $playsFetch): self
{
$this->playsFetch = $playsFetch;
return $this;
}
public function getDoghouseColor(): ?string
{
return $this->doghouseColor;
}
public function setDoghouseColor(string $doghouseColor): self
{
$this->doghouseColor = $doghouseColor;
return $this;
}
/**
* @return Collection|Cat[]
*/
public function getCatsChased(): Collection
{
return $this->catsChased;
}
public function addCatsChased(Cat $catsChased): self
{
if (!$this->catsChased->contains($catsChased)) {
$this->catsChased[] = $catsChased;
$catsChased->addDogsChasedBy($this);
}
return $this;
}
public function removeCatsChased(Cat $catsChased): self
{
if ($this->catsChased->removeElement($catsChased)) {
$catsChased->removeDogsChasedBy($this);
}
return $this;
}
}
<?php
// api/src/Entity/Mouse.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Doctrine\ORM\Mapping as ORM;
/**
* @ApiResource(
* collectionOperations={
* "get"={
* "normalization_context"={"groups"={"mouse:list", "related"}}
* },
* "post"
* },
* itemOperations={"get", "put", "patch", "delete"},
* normalizationContext={"groups"={"mouse:read", "mouse:list", "related"}},
* )
* @ORM\Entity()
*/
class Mouse extends Animal
{
/**
* @ORM\Column(type="boolean")
* @Groups({"mouse:read"})
*/
private $likesCheese;
/**
* #@ApiSubresource()
* @ORM\ManyToOne(targetEntity=Cat::class, inversedBy="miceEaten")
* @MaxDepth(2)
* @Groups({"mouse:list", "animal:list"})
*/
private $ateByCat;
public function getLikesCheese(): ?bool
{
return $this->likesCheese;
}
public function setLikesCheese(bool $likesCheese): self
{
$this->likesCheese = $likesCheese;
return $this;
}
public function getAteByCat(): ?Cat
{
return $this->ateByCat;
}
public function setAteByCat(?Cat $ateByCat): self
{
$this->ateByCat = $ateByCat;
return $this;
}
/**
* Represent the entity to the user in a single string
* @return string
* @ApiProperty(iri="http://schema.org/name")
* @Groups({"related"})
*/
function getLabel() {
return $this->getBirthday()->format('Y-m-d');
}
}
# api/config/serialization/Cat.yaml
App\Entity\Cat:
attributes:
name:
groups: ['cat:list']
sex:
groups: ['cat:read']
weight:
groups: ['cat:read']
birthday:
groups: ['cat:list']
color:
groups: ['cat:list']
# api/config/serialization/Dog.yaml
App\Entity\Dog:
attributes:
name:
groups: ['dog:list']
sex:
groups: ['dog:read']
weight:
groups: ['dog:read']
birthday:
groups: ['dog:list']
color:
groups: ['dog:list']
# api/config/serialization/Mouse.yaml
App\Entity\Mouse:
attributes:
name:
groups: ['mouse:list']
sex:
groups: ['mouse:read']
weight:
groups: ['mouse:read']
birthday:
groups: ['mouse:list']
color:
groups: ['mouse:list']
对补充信息的反应
关于标签的使用,请参阅 the tutorial 的第 4 章(两个分支的自述文件)。 ::getLabel 方法也带来了封装:可以在不改变 api.
的情况下修改表示关于 oneOf、allOf 或 anyOf:Apip 生成的一长串类型很难看,但我想它会是 为想要自动验证 属性 值和抽象用户界面(如管理客户端)的客户端工作。对于 designing/scaffolding 客户端和自定义抽象用户界面,它们可能会很麻烦,所以如果 Api 平台能够自动适当地使用它们会很好,但对于大多数开发团队来说,我不认为投资投入改进 OpenApi 文档工厂将获得回报。换句话说,手动调整客户端通常工作量较小。所以现在我不会花任何时间在这上面。
更有问题的是,在 JsonLD 文档中,使用“output”指定的操作类型的属性=被合并到资源本身的类型中。但这与继承无关。