【发布时间】:2021-05-29 14:49:22
【问题描述】:
我希望使用 API-Platform 对对象层次结构类执行 CRUD 操作。我发现在使用带有 API-Platform 的继承类时写得很少,而在与 Symfony 的序列化程序一起使用时写得很少,并且我正在寻找更好的方向来了解需要专门针对继承类以不同方式实现的内容。
假设我从 Animal 继承了 Dog、Cat 和 Mouse,其中 Animal 是抽象的(见下文)。这些实体是使用bin/console make:entity 创建的,并且仅进行了修改以扩展父类(以及它们各自的存储库)并添加了 Api-Platform 注释。
组应该如何与继承的类一起使用?每个子类(即 Dog、Cat、Mouse)应该有自己的组还是应该只使用父组 animal 组?当全部使用animal 组时,某些路由会以The total number of joined relations has exceeded the specified maximum. ... 响应,而当混合使用时,有时会得到Association name expected, 'miceEaten' is not an association.。这些组是否也允许父实体上的 ApiPropertys 应用于子实体(即 Animal::weight 的默认 openapi_context 示例值为 1000)?
API-Platform 不讨论 CTI 或 STI,我在文档中找到的唯一相关参考是关于 MappedSuperclass。除了 CLI 或 STI 之外,还需要使用 MappedSuperclass 吗?请注意,我尝试将MappedSuperclass 应用到Animal,但收到了预期的错误。
基于this post 和其他人,似乎首选的RESTful 实现是使用单个端点/animals 而不是单独的/dogs、/cats 和/mice。同意?这如何通过 API 平台实现?如果@ApiResource() 注释仅应用于Animal,我会得到这个单一的所需URL,但在OpenAPI Swagger 文档和实际请求中都没有得到Dog、Cat 和Mouse 的子属性。如果 @ApiResource() 注释仅应用于 Dog、Cat 和 Mouse,则无法获得所有动物的组合集合,并且我有多个端点。需要将其应用于所有三个吗? OpenApi 的关键字oneOf、allOf 和anyOf 似乎可以提供如stackoverflow answer 和Open-Api specification 所描述的解决方案。 Api-Platform 是否支持这一点?如果支持,如何支持?
动物
<?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 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 的方法,但除非情况发生变化并且我需要更大的灵活性,否则会采用您的方法。
我不明白您对标签的使用。起初我认为这是一些唯一标识符,或者可能是某种暴露鉴别器的手段,但不认为是这种情况。请详细说明。
关于“为了避免在每个具体子类中重复这些属性的定义,我使用 yaml 添加了一些组”,我的方法是为抽象 Animal 类设置属性,而不是私有,以便 PHP 可以使用反射,并使用组抽象动物中的“animal:read”和各个具体类中的组“mouse:read”等,得到了我想要的结果。
是的,请参阅您关于限制列表与详细信息的结果的观点。
我原本以为@MaxDepth 会解决递归问题,但无法让它发挥作用。然而,真正起作用的是使用@ApiProperty(readableLink=false)。
我发现在一些情况下,API-Platform 生成的 swagger 规范在 SwaggerUI 中显示 anyOf,但同意 API-Platform 似乎并不真正支持 oneOf、allOf 或 anyOf。然而,不知何故,是否需要实现这一点?例如,动物 ID 在其他表中,文档需要一个 Cat、Dog 或 Mouse,不是吗?还是这个长长的类型列表是由使用的每个序列化组组合产生的?
【问题讨论】:
-
在top answer arguing for a single endpoint per resource 中,
Cat和Dog资源公开的数据被概括为具有类似的多态Animal结构。两者都被视为通过单个/animals端点公开的Animal资源。您的用例想要区分同一/animals端点中的Animal子类型结构。Cat不是Dog。类似于function(Dog | Cat $animal)与funtion(Animal $animal)不同的签名。 -
@JeroenvanderLaan 同意。此回复中有几个 cmets 询问我在问什么,但没有真正的解决方案,而且显然不是特定于 api 平台的。您的回复是否表明我应该有单独的
\cat和\dog端点?谢谢 -
我还没有尝试配置 api 平台以使用 OAS v3
oneOf功能记录端点。如果你能让它工作,这可能就是你正在寻找的东西。不过,我确实倾向于为不共享相同结构的资源设置单独的端点。
标签: rest symfony doctrine-orm api-platform.com jms-serializer