如何通过 API 平台使用继承类

use*_*531 7 rest symfony doctrine-orm jms-serializer api-platform.com

我希望使用 API 平台对对象层次结构类执行 CRUD 操作。我发现在将继承类与 API 平台一起使用时编写的内容很少,在与 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 吗?请注意,我尝试申请MappedSuperclassAnimal但收到了预期的错误。

根据这篇文章以及其他文章,首选的 RESTful 实现似乎是使用单个端点/animals而不是单独的/dogs/cats/mice。同意?如何通过 API 平台实现这一点?如果@ApiResource()注释仅应用于 Animal,我会获得这个所需的 URL,但不会获得 OpenAPI Swagger 文档中 Dog、Cat 和 Mouse 的子属性,也不会获得实际请求。如果@ApiResource()注释仅应用于狗、猫和小鼠,则无法获得所有动物的组合集合,并且我有多个端点。需要将其应用于所有三个吗?看来 OpenApi 的关键字oneOfallOf、 和可能提供此stackoverflow 答案以及此Open-Api 规范anyOf所描述的解决方案。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;
    }
}
Run Code Online (Sandbox Code Playgroud)

<?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;
    }
}
Run Code Online (Sandbox Code Playgroud)

<?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;
    }
}
Run Code Online (Sandbox Code Playgroud)

老鼠

<?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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

我原本以为这@MaxDepth可以解决递归问题,但无法实现。然而,有效的是使用@ApiProperty(readableLink=false).

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

Met*_*ass 12

我认为在这个主题上没有可靠的来源,但我确实在框架、抽象用户界面和 php 方面拥有长期的经验,并创建了MetaClass 教程 Api 平台,所以我会尝试自己回答您的问题。

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

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

针对 REST 通常使用单个端点 /animals 的普遍偏好,我选择了 /dogs、/cats 和 /mice 的单个端点,因为:

  1. Api 平台通过引用这些特定端点的 iri 来标识资源类的实例,并在这些实例被序列化时将它们作为 @id 的值包含在内。客户端生成器,我想管理客户端也依赖这些端点来进行增删改查操作,
  2. 借助 Api 平台,特定的后期操作可以通过条令 orm 开箱即用。端点 /animals 需要一个自定义的反规范化器来决定要实例化哪个具体类。
  3. 通过序列化组,特定端点可以更好地控制序列化。如果没有这一点,就很难使序列化与本教程第 4 章中的完成方式兼容,
  4. 在 Api 平台的许多扩展点中,很容易使事情适用于特定资源,并且文档中的所有示例都利用了它。使它们特定于当前对象的实际具体子类是没有记录的,并且可能并不总是可能的。

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

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

我向 Animal 添加了一个 ::getLabel 方法,以用单个字符串表示每个方法(注释为http://schema.org/name)。基本 CRUD 和搜索客户端主要向用户显示单一类型的实体,并以这种方式表示相关实体。拥有特定的 schema.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"
}
Run Code Online (Sandbox Code Playgroud)

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

serializer:
    mapping:
        paths: ['%kernel.project_dir%/config/serialization']
Run Code Online (Sandbox Code Playgroud)

yaml 配置与注释很好地融合在一起,并且仅覆盖 Animal 类中的配置。

根据本教程的第 4 章,我还添加了列表组,以将一组更有限的属性包含在获取集合操作的结果中。当实体集合呈现给用户时,即使使用分页,信息量很快就会变得过多和/或占据屏幕。如果 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
}
Run Code Online (Sandbox Code Playgroud)

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

 *     collectionOperations={
 *         "get"={
 *             "normalization_context"={"groups"={"cat:list", "dog:list", "mouse:list", "related"}}
 *         },
 *     },
Run Code Online (Sandbox Code Playgroud)

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

{
  "@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
}
Run Code Online (Sandbox Code Playgroud)

这对于手头的示例来说很好,但是随着关系的增加,它可能会变得有点大,因此对于通用妥协,我只包含“animal:list”和“referred”,从而导致较小的响应:

{
  "@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
}
Run Code Online (Sandbox Code Playgroud)

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

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 
Run Code Online (Sandbox Code Playgroud)

如果您将下面的代码粘贴到 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", "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']
Run Code Online (Sandbox Code Playgroud)

针对补充信息的反应

关于标签的使用,请参阅教程的第 4 章(两个分支的自述文件)。::getLabel 方法还带来了封装性:可以在不改变 api 的情况下修改表示形式。

关于 oneOf、allOf 或 anyOf:Apip 生成的一长串类型很丑陋,但我想这对于想要自动验证属性值和抽象用户界面(如管理客户端)的客户端来说是有用的。对于设计/搭建客户端以及自定义抽象用户界面,它们可能会很麻烦,因此如果 Api 平台能够自动适当地使用它们就好了,但对于大多数开发团队来说,我不认为投资于改进 OpenApi 文档工厂将会被赚回来。换句话说,手动调整客户端通常会减少工作量。所以现在我不会花任何时间在这上面。

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