如何在 API-Platform 中使用继承的类

问题描述

我希望使用 API-Platform 对对象层次结构类执行 CRUD 操作。我发现在 API-Platform 中使用继承的类时写的很少,而当与 Symfony 的序列化器一起使用时,我发现写得很少,我正在寻找更好的方向,专门针对继承的类需要以不同的方式实现。

假设我有从 Animal 继承的 Dog、Cat 和 Mouse,其中 Animal 是抽象的(见下文)。这些实体是使用 bin/console make:entity 创建的,并且仅进行了修改以扩展父类(以及它们各自的存储库)并添加了 Api-Platform 注释。

组应该如何与继承的类一起使用?每个子类(即狗、猫、老鼠)都应该有自己的组还是应该只使用父 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 和 { {1}}。同意?这如何通过 API 平台实现?如果 /mice 注释仅应用于 Animal,我会得到这个单一的所需 URL,但不会在 OpenAPI Swagger 文档或实际请求中获取 Dog、Cat 和 Mouse 的子属性。如果 @ApiResource() 注释仅应用于 Dog、Cat 和 Mouse,则无法获得所有动物的组合集合,而且我有多个端点。需要将它应用于所有三个吗?看来 OpenApi 的关键字 @ApiResource()oneOfallOf 可能会提供此 stackoverflow answer 以及此 Open-Api specification 所描述的解决方案。 Api-Platform 是否支持此功能,如果支持,如何支持?

动物

anyOf

<?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",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",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",*     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",*     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;
    }
}

MetaClass 回答的补充信息

下面是我的存储库方法,关键是最具体的类在构造函数中设置实体。

<?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",*     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;
    }
}

我希望遵循“一般而言 REST 的共同偏好是使用单个端点 /animals”,但理解您“为 /dogs、/cats 和 /mice 选择单个端点”的理性。为了克服您的原因,我还考虑使 Animal 变得具体并使用组合来实现多态性,以便 Animal 具有某种动物类型的对象。我想最终仍然需要 Doctrine 继承来允许 Animal 与这个对象建立一对一的关系,但唯一的属性是 PK ID 和鉴别器。我很可能会放弃这个追求。

不确定我是否同意您不使用 denormalizationContext 的方法,但除非情况发生变化并且我需要更多灵活性,否则我会采用您的方法。

我不明白您对标签的使用。起初我认为这是一些唯一的标识符,或者可能是某种暴露鉴别器的方法,但不这么认为。请详细说明。

关于“为了避免在每个具体子类中重复这些属性的定义,我使用 yaml 添加了一些组”,我的方法是为抽象 Animal 类创建属性保护而不是私有,以便 PHP 可以使用反射和使用组抽象动物中的“动物:阅读”和各个具体类中的“鼠标:阅读”等分组,并得到了我想要的结果。

是的,请了解您关于限制列表与详细信息结果的观点。

我最初认为 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 可以解决递归问题,但无法使其正常工作。然而,有效的是使用 @MaxDepth

我发现 API-Platform 生成的 Swagger 规范在 SwaggerUI 中显示 @ApiProperty(readableLink=false) 的一些情况,但同意 API-Platform 似乎并不真正支持 oneOf、allOf 或 anyOf。然而,不知何故,是否需要实现这一点?例如,动物 ID 在其他一些表中,文档需要一个猫、狗或老鼠,不是吗?或者是否使用了由每个序列化组组合产生的这一长长的类型列表?

解决方法

我认为没有关于此主题的可靠来源,但我确实拥有 long experience with frameworks,abstract user interfaces and php 并创建了 MetaClass Tutorial Api Platform,因此我会尝试自己回答您的问题。

本教程旨在涵盖大多数 CRUD 和搜索应用程序的共同点,适用于 api 平台 api 和使用 api 平台客户端生成器生成的 react 客户端。本教程不涉及继承和多态,因为我认为它不会出现在许多 CRUD 和搜索应用程序中,但它解决了许多方面的问题,有关概述,请参阅 readme of the master branch 中的章节列表。 Api Platform 为此类应用程序的 api 提供了许多开箱即用的通用功能,只需为特定资源和操作进行配置。在 React 分支中,这导致重复模式和重构为通用组件,并最终导致 extended react client generator 伴随教程。这个答案中的序列化组方案更通用一些,因为我对这个主题的理解随着时间的推移而有所提高。

您的类在 Api Platform 2.6 上开箱即用,但未包含的存储库类除外。我从注释中删除了它们,因为现在似乎没有调用它们的特定方法。您可以随时在需要时再次添加它们。

与 REST 通常使用单个端点 /animals 不同,我为 /dogs、/cats 和 /mice 选择了单个端点,因为:

  1. Api 平台通过引用这些特定端点的 iri 来识别资源类的实例,并在这些实例被序列化时将它们作为 @id 的值包含在内。客户端生成器,我想管理客户端也依赖于这些端点来处理 crud 操作,
  2. 使用 Api 平台,特定的后期操作可以通过学说 orm 开箱即用。端点 /animals 需要一个自定义的 Denormalizer 来决定要实例化哪个具体类。
  3. 使用序列化组,特定端点可以更好地控制序列化。没有它,就很难让序列化与本教程第 4 章中的方式兼容,
  4. 在 Api Platform 的许多 extension points 中,很容易使事情适用于特定资源,并且文档中的所有示例都利用了这一点。使它们特定于手头对象的实际具体子类是没有记录的,并且可能并非总是可行。

我只包含 /animals get 集合操作,因为这允许客户端在单个请求中检索、搜索和排序多态动物集合。

根据教程的第 4 章,我删除了写入注释组。 Api Platforms 反序列化已经允许客户端只包含那些带有 post、put 和 patch 的属性,这些属性保存数据并打算设置,因此反序列化组的唯一目的可以是禁止通过(的某些操作)设置某些属性api 或允许通过嵌套文档创建相关对象。当我尝试通过将其作为鼠标的 $ateByCat 的值发布来添加新猫时,出现错误“不允许使用属性“ateByCat”的嵌套文档。请改用 IRI。通过 Dog::$catsChased 添加一个也发生了同样的情况,因此在没有写入注释组的情况下,授予某些角色的操作的安全性似乎不会受到损害。对我来说似乎是默认的声音。

我向 Animal 添加了一个 ::getLabel 方法,以用单个字符串(注释为 http://schema.org/name)来表示每个方法。基本 CRUD 和搜索客户端主要向用户显示单一类型的实体,并以这种方式表示相关实体。拥有特定的 schema.org/name 属性对客户端来说更方便,并且使其成为派生属性更灵活,然后根据实体类型添加不同的属性。 label 属性是唯一添加到“相关”组的属性。该组被添加到每个类型的规范化上下文中,以便对于 Cat、Doc 和 Mouse 的“获取”操作,它是唯一为相关对象序列化的属性:

{
  "@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"
}

为了得到这个结果,我必须使特定于具体子类的继承属性的序列化组。为了避免在每个具体子类中重复这些属性的定义,我使用 yaml 添加了一些组(添加在此答案的底部)。为了使它们工作,将以下内容添加到 api/config/packages/framework.yaml:

serializer:
    mapping:
        paths: ['%kernel.project_dir%/config/serialization']

yaml 配置与注释很好地融合在一起,并且只会覆盖来自 Animal 类的那些。

根据教程的第 4 章,我还添加了列表组,以便在获取集合操作的结果中包含一组更有限的属性。当实体集合呈现给用户时,信息量很快就会变得过大和/或覆盖屏幕,即使有分页也是如此。如果 api 开发人员清楚客户端的目的,则在 api 中进行选择将加快数据传输,尤其是在忽略多对多关系的情况下。这会导致一系列老鼠的序列化,如下所示:

{
  "@context": "/contexts/Mouse","@id": "/mice","@type": "hydra:Collection","hydra:member": [
    {
      "@id": "/mice/3","ateByCat": {
        "@id": "/cats/1","label": "Felix"
      },"label": "2021-01-13","name": "mimi","birthday": "2021-01-13T00:00:00+00:00","color": "grey"
    }
  ],"hydra:totalItems": 1
}

get /animals 序列化的配置是一种妥协。如果我包括所有子类的列表组:

 *     collectionOperations={
 *         "get"={
 *             "normalization_context"={"groups"={"cat:list","dog:list","mouse:list","related"}}
 *         },*     },

我得到了一个很好的多态响应,但相关对象还包含其类型列表组的所有属性,而不仅仅是标签:

{
  "@context": "/contexts/Animal","@id": "/animals","hydra:member": [
    {
      "@id": "/cats/1","label": "Felix"
    },{
      "@id": "/dogs/2","playsFetch": true,"name": "Bella","birthday": "2019-03-13T00:00:00+00:00","color": "brown","label": "Bella"
    },{
      "@id": "/mice/3","hydra:totalItems": 3
}

这对于手头的例子来说很好,但是如果关系越多,它就会变得有点大,所以对于一般的妥协,我只包括“animal:list”和“referred”,从而导致较小的响应:

{
  "@context": "/contexts/Animal","hydra:totalItems": 3
}

如您所见,多态性仍然存在(ateByCat),并且问题确实变小了,但并没有消失。这个问题不能用序列化组解决,因为从序列化上下文来看,猫吃老鼠的关系是递归的。一个更好的解决方案可能是装饰 api_platform.serializer.context_builder 为一对一递归关系的属性添加一个 custom callback,但是序列化递归关系的问题不是特定于继承的,因此超出了范围这个问题所以现在我不详细说明这个解决方案。

Api Platform 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://localhost/docs.json 检索整个 openapi 方案

代码

<?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",*     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",*         "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",*     normalizationContext={"groups"={"dog:read","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",*     normalizationContext={"groups"={"mouse:read",* )
 * @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 生成的一长串类型很难看,但我想它会是 适用于想要自动验证属性值和抽象用户界面(如管理客户端)的客户端。对于设计/搭建客户端和自定义抽象用户界面来说,它们可能会很麻烦,所以如果 Api Platform 能够自动适当地使用它们会很好,但对于大多数开发团队来说,我不认为对改进 OpenApi docs factory 进行投资会赚回来的。换句话说,手动调整客户端通常会减少工作量。所以现在我不会花任何时间在这上面。

更成问题的是,在 JsonLD 文档中,使用“output”= 指定的操作类型的属性被合并到资源本身的类型中。但这与继承无关。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...