【问题标题】:How to use inherited classes with API-Platform如何通过 API 平台使用继承的类
【发布时间】: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 的关键字oneOfallOfanyOf 似乎可以提供如stackoverflow answerOpen-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 中,CatDog 资源公开的数据被概括为具有类似的多态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


【解决方案1】:

我认为在这个主题上没有可靠的来源,但我确实有 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 平台为此类应用程序的 api 提供了许多开箱即用的通用功能,只需要针对特定​​资源和操作进行配置。在 react 分支中,这导致了重复出现的模式和重构为通用组件,并最终导致 extended react client generator 伴随本教程。这个答案中的序列化组方案更通用一些,因为我对这个主题的理解随着时间的推移而有所提高。

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

与一般对 REST 的普遍偏好相反,我为 /dogs、/cats 和 /mice 选择单个端点 /animals,因为:

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

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

根据教程的第 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 的“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"
}

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

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

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

根据本教程的第 4 章,我还添加了列表组,以便将一组更有限的属性包含在 get 集合操作的结果中。当向用户呈现实体集合时,即使有分页,信息量也会很快变得过大和/或填满屏幕。如果 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 的序列化配置是一种妥协。如果我包括所有子类的列表组:

 *     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
}

这对于手头的示例来说很好,但如果关系越多,它可能会变得有点大,因此对于一般的折衷方案,我只包括“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
}

正如您所见,多态性仍然是可能的(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", "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 生成的长长的类型列表很难看,但我想它会是 适用于希望自动验证属性值和抽象用户界面的客户端,如管理客户端。对于设计/搭建客户端和自定义抽象用户界面,它们可能会很麻烦,所以如果 Api 平台能够适当地自动使用它们会很好,但对于大多数开发团队来说,我不认为投资于改进 OpenApi 文档工厂会赚回来的。换句话说,手动调整客户端通常会减少工作量。所以现在我不会花时间在这上面。

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

【讨论】:

  • 感谢 MetaClass。我会多次阅读您的回答以确保我有一个很好的理解,然后希望您愿意回答几个问题。在过去的几天里,我了解到如果 Animal 的属性受到保护而不是私有,事情会变得更好!此外,将我的存储库添加到我的原始帖子中,这样您就可以看到我如何获得特定端点以仅返回特定类(以前,/cats 返回所有动物)。理想情况下,如果发现有价值的内容,您可以将其添加到您的答案中,这样它会更加全面。
  • 你好MetaClass,我在原问题的最后添加了一些与你的回答相关的内容,希望你能看看和评论。另外,什么是“查看主分支自述文件中的列表op chapters”?谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-12-26
  • 2013-05-14
  • 1970-01-01
  • 1970-01-01
  • 2018-05-25
  • 1970-01-01
相关资源
最近更新 更多