Как использовать унаследованные классы с API-платформой

Я хочу использовать API-платформу для выполнения операций CRUD с классами иерархии объектов. Я нашел немного написанного при использовании унаследованных классов с API-платформой и некоторых, но не намного, при использовании с сериализатором Symfony, и я ищу лучшее направление в отношении того, что нужно реализовать по-другому специально для унаследованных классов.

Скажем, у меня есть Dog, Cat и Mouse, унаследованные от Animal, где 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-платформа не обсуждает CTI или STI, и единственная соответствующая ссылка, которую я нашел в документации, касалась MappedSuperclass. Нужно ли использовать MappedSuperclass в дополнение к CLI или STI? Обратите внимание, что я попытался применить MappedSuperclass к Animal, но получил ожидаемую ошибку.

На основании этого сообщения, а также других, кажется, что предпочтительная реализация RESTful заключается в использовании одной конечной точки /animals вместо отдельных /dogs, /cats и /mice. Согласен? Как это можно реализовать с помощью API-платформы? Если аннотация @ApiResource() применяется только к Animal, я получаю этот единственный желаемый URL, но не получаю ни дочерних свойств для Dog, Cat и Mouse в документации OpenAPI Swagger, ни фактического запроса. Если аннотация @ApiResource() применяется только к собакам, кошкам и мышам, то нет возможности получить объединенную коллекцию всех животных, и у меня есть несколько конечных точек. Нужно ли его применять ко всем трем? Похоже, что ключевые слова OpenApi oneOf, allOf и anyOf могут предоставить решение, описанное в этом ответ stackoverflow, а также этот Спецификация Open-Api. Поддерживает ли это 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, но при этом понимайте, почему вы выбираете отдельные конечные точки для / собак, / кошек и / мышей. Чтобы преодолеть ваши причины, я также подумал о том, чтобы сделать Animal конкретным и использовать композицию для полиморфизма, чтобы у Animal был какой-то объект типа животного. Я полагаю, что в конечном итоге наследование Doctrine все равно потребуется, чтобы позволить Animal иметь однозначные отношения с этим объектом, но единственными свойствами будут идентификатор PK и дискриминатор. Скорее всего, я откажусь от этой погони.

Не уверен, согласен ли я с вашим подходом к отказу от использования denormalizationContext, но воспользуюсь вашим подходом, если обстоятельства не изменятся, и мне понадобится больше гибкости.

Я не понимаю, как вы используете этикетку. Сначала я подумал, что это какой-то уникальный идентификатор или, может быть, какое-то средство для раскрытия дискриминатора, но не думаю, что это так. Пожалуйста, дополните.

Что касается того, чтобы избежать повторения определений этих свойств в каждом конкретном подклассе, я добавляю некоторые группы, используя yaml, мой подход заключался в том, чтобы сделать свойства для абстрактного класса Animal защищенным, а не частным, чтобы PHP мог использовать отражение, и использовал группы животных: читать в абстрактном Животное и групповая мышь: читал и т. Д. В отдельных конкретных классах и получил желаемые результаты.

Да, обратите внимание на вашу точку зрения об ограничении результатов для списка по сравнению с деталями.

Первоначально я думал, что @MaxDepth решит рекурсивные проблемы, но не мог заставить его работать. Что действительно работало, так это использование @ApiProperty(readableLink=false).

Я обнаружил несколько случаев, когда сгенерированная API-платформой спецификация чванства отображалась anyOf в SwaggerUI, но согласен, что API-платформа, похоже, действительно не поддерживает oneOf, allOf или anyOf. Но как-нибудь понадобится это реализовать? Например, идентификатор животного был в какой-то другой таблице, потребуется документация для кошки, собаки или мыши, не так ли? Или вместо этого используется этот длинный список типов, являющийся результатом каждой комбинации групп сериализации?


person user1032531    schedule 27.02.2021    source источник
comment
В главном ответе, в котором утверждается, что для каждой конечной точки требуется одна конечная точка для каждого ресурса, данные, предоставляемые как Cat, так и Dog ресурсами, обобщаются и имеют аналогичную полиморфную структуру Animal. Оба они рассматриваются как Animal ресурсы, предоставляемые через одну конечную точку /animals. В вашем варианте использования требуется различать структуры подтипов Animal в одной /animals конечной точке. Но Cat - это не Dog. Подобно тому, что function(Dog | Cat $animal) не совпадает с подписью funtion(Animal $animal).   -  person Jeroen van der Laan    schedule 01.03.2021
comment
@JeroenvanderLaan Согласен. Было несколько комментариев к этому ответу с вопросом о том, о чем я спрашиваю, но реального решения не было, и оно явно не зависело от api-платформы. Указывает ли ваш ответ на то, что у меня должны быть отдельные конечные точки \cat и \dog? Спасибо   -  person user1032531    schedule 01.03.2021
comment
Я не пробовал настраивать платформу api для документирования конечной точки с помощью функции OAS v3 oneOf. Это может быть то, что вы ищете, если сможете заставить его работать. Тем не менее, я склоняюсь к тому, чтобы иметь отдельные конечные точки для ресурсов, которые не имеют одинаковой структуры.   -  person Jeroen van der Laan    schedule 01.03.2021


Ответы (1)


Я не думаю, что по этой теме доступен авторитетный источник, но у меня есть длительный опыт работы с фреймворками, абстрактными пользовательскими интерфейсами и php и создал MetaClass Tutorial Api Платформа, поэтому я сам постараюсь ответить на ваш вопрос.

Учебное пособие призвано охватить общие черты большинства приложений CRUD и Search как для api платформы api, так и для клиента реакции, созданного с помощью генератора клиентов платформы api. В руководстве не рассматриваются наследование и полиморфизм, потому что я не думаю, что это происходит во многих приложениях CRUD и Search, но в нем рассматриваются многие аспекты, которые имеют место. Для обзора см. Список глав в readme основной ветки. Платформа Api предлагает множество общих функций для api таких приложений из коробки, которые необходимо настроить только для определенных ресурсов и операций. В ответных ветках это привело к повторяющимся шаблонам и рефакторингу в общие компоненты и, в конечном итоге, к расширенному генератору клиента реакции для сопровождения учебника. Схема групп сериализации в этом ответе немного более общая, потому что мое понимание предмета со временем улучшилось.

Ваши классы работали из коробки на Api Platform 2.6, за исключением классов репозитория, которые не были включены. Я удалил их из аннотации, так как сейчас ни один из их конкретных методов не вызывается. Вы всегда можете добавить их снова, когда они вам понадобятся.

В отличие от общего предпочтения REST в целом использовать одну конечную точку / животные, я выбрал отдельные конечные точки для / собак, / кошек и / мышей, потому что:

  1. Платформа Api идентифицирует экземпляры классов ресурсов с помощью iri, которые ссылаются на эти конкретные конечные точки, и включает их как значения @id всякий раз, когда эти экземпляры сериализуются. Генератор клиентов и, я полагаю, клиент администратора тоже зависят от этих конечных точек для работы с грубыми операциями,
  2. С Api Platform определенные почтовые операции работают из коробки с doctrine orm. Конечная точка / животные потребуют настраиваемого денормализатора, который может решить, какой конкретный класс создать.
  3. С группами сериализации определенные конечные точки дают больше контроля над сериализацией. Без этого трудно добиться совместимости сериализации с тем, как это делается в главе 4 руководства,
  4. Во многих точках расширения платформы Api легко заставить все работать для специальный ресурс, и все примеры в документации используют это. Сделать их специфичными для конкретного конкретного подкласса рассматриваемого объекта недокументировано и не всегда возможно.

Я включаю только операцию сбора / animals get, потому что она позволяет клиенту извлекать, искать и сортировать полимофическую коллекцию животных за один запрос.

В соответствии с главой 4 учебника я удалил группы аннотаций записи. Десериализация платформ Api уже позволяет клиенту включать в post, put и patch только те свойства, которые содержат данные и предназначены для установки, поэтому единственной целью групп десериализации может быть запрет на установку определенных свойств через (определенные операции) api или разрешить создание связанных объектов через вложенные документы. Когда я попытался добавить нового кота, разместив его как значение $ ateByCat мыши, я получил ошибку. Вложенные документы для атрибута ateByCat не допускаются. Вместо этого используйте IRI. То же самое произошло с добавлением одного через Dog :: $ catsChased, поэтому безопасность при работе с определенными предоставленными ролями, похоже, не будет нарушена без групп аннотаций записи. Для меня это звучит по умолчанию.

Я добавил метод :: getLabel в Animal, чтобы представить каждый из них одной строкой (помеченной как http://schema.org/name). Базовые клиенты CRUD и поиска в первую очередь показывают пользователю один тип сущностей и таким образом представляют связанные сущности. Наличие определенного свойства schema.org/name более удобно для клиента, а превращение его в производное свойство более гибкое, чем добавление различных свойств в зависимости от типа объекта. Свойство метки - единственное свойство, которое добавляется в связанную группу. Эта группа добавляется в контекст нормализации каждого типа, так что для операций получения 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",
      "@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 и указано, что приводит к меньшему ответу:

{
  "@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, чтобы добавить настраиваемый обратный вызов для свойств взаимно однозначных рекурсивных отношений, но проблема сериализации рекурсивных отношений не является специфической для наследования и, следовательно, выходит за рамки этого вопроса, поэтому сейчас я не буду вдаваться в подробности это решение.

Платформа Api 2.6 не поддерживает oneOf, allOf или anyOf. Вместо этого он создает довольно длинный список типов в результате каждой комбинации используемых групп сериализации, каждая со всеми включенными свойствами в плоский список. Полученный json IMHO слишком велик, чтобы включать его здесь, поэтому я включаю только список имен типов:

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 и выполните описанную конфигурацию, вы сможете получить всю схему openapi с https: //localhost/docs.json.

Код

<?php
// api/src/Entity/Animal.php
namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     collectionOperations={
 *         "get"={
 *             "normalization_context"={"groups"={"animal:list", "related"}}
 *         },
 *     },
 *     itemOperations={},
 * )
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="type", type="string", length=32)
 * @ORM\DiscriminatorMap({"dog" = "Dog", "cat" = "Cat", "mouse" = "Mouse"})
 * @ORM\Entity()
 */
abstract class Animal
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"animal:list"})
     */
    private $name;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $sex;

    /**
     * @ORM\Column(type="integer")
     * @ApiProperty(
     *     attributes={
     *         "openapi_context"={
     *             "example"=1000
     *         }
     *     }
     * )
     */
    private $weight;

    /**
     * @ORM\Column(type="date")
     * @ApiProperty(
     *     attributes={
     *         "openapi_context"={
     *             "example"="2020/1/1"
     *         }
     *     }
     * )
     */
    private $birthday;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"animal:list"})
     */
    private $color;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    public function getSex(): ?string
    {
        return $this->sex;
    }

    public function setSex(string $sex): self
    {
        $this->sex = $sex;

        return $this;
    }

    public function getWeight(): ?int
    {
        return $this->weight;
    }

    public function setWeight(int $weight): self
    {
        $this->weight = $weight;

        return $this;
    }

    public function getBirthday(): ?\DateTimeInterface
    {
        return $this->birthday;
    }

    public function setBirthday(\DateTimeInterface $birthday): self
    {
        $this->birthday = $birthday;

        return $this;
    }

    public function getColor(): ?string
    {
        return $this->color;
    }

    public function setColor(string $color): self
    {
        $this->color = $color;

        return $this;
    }

    /**
     * Represent the entity to the user in a single string
     * @return string
     * @ApiProperty(iri="http://schema.org/name")
     * @Groups({"related"})
     */
    function getLabel() {
        return $this->getName();
    }

}

<?php
// api/src/Entity/Cat.php
namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     collectionOperations={
 *         "get"={
 *             "normalization_context"={"groups"={"cat:list", "related"}}
 *         },
 *         "post"
 *     },
 *     itemOperations={"get", "put", "patch", "delete"},
 *     normalizationContext={"groups"={"cat:read", "cat:list", "related"}}
 * )
 * @ORM\Entity()
 */
class Cat extends Animal
{
    /**
     * @ORM\Column(type="boolean")
     * @Groups({"cat:list"})
     */
    private $likesToPurr;

    /**
     * #@ApiSubresource()
     * @ORM\OneToMany(targetEntity=Mouse::class, mappedBy="ateByCat")
     * @MaxDepth(2)
     * @Groups({"cat:read"})
     */
    private $miceEaten;

    /**
     * #@ApiSubresource()
     * @ORM\ManyToMany(targetEntity=Dog::class, inversedBy="catsChased")
     * @MaxDepth(2)
     * @Groups({"cat:read"})
     */
    private $dogsChasedBy;

    public function __construct()
    {
        $this->miceEaten = new ArrayCollection();
        $this->dogsChasedBy = new ArrayCollection();
    }

    public function getLikesToPurr(): ?bool
    {
        return $this->likesToPurr;
    }

    public function setLikesToPurr(bool $likesToPurr): self
    {
        $this->likesToPurr = $likesToPurr;

        return $this;
    }

    /**
     * @return Collection|Mouse[]
     */
    public function getMiceEaten(): Collection
    {
        return $this->miceEaten;
    }

    public function addMiceEaten(Mouse $miceEaten): self
    {
        if (!$this->miceEaten->contains($miceEaten)) {
            $this->miceEaten[] = $miceEaten;
            $miceEaten->setAteByCat($this);
        }

        return $this;
    }

    public function removeMiceEaten(Mouse $miceEaten): self
    {
        if ($this->miceEaten->removeElement($miceEaten)) {
            // set the owning side to null (unless already changed)
            if ($miceEaten->getAteByCat() === $this) {
                $miceEaten->setAteByCat(null);
            }
        }

        return $this;
    }

    /**
     * @return Collection|Dog[]
     */
    public function getDogsChasedBy(): Collection
    {
        return $this->dogsChasedBy;
    }

    public function addDogsChasedBy(Dog $dogsChasedBy): self
    {
        if (!$this->dogsChasedBy->contains($dogsChasedBy)) {
            $this->dogsChasedBy[] = $dogsChasedBy;
        }

        return $this;
    }

    public function removeDogsChasedBy(Dog $dogsChasedBy): self
    {
        $this->dogsChasedBy->removeElement($dogsChasedBy);

        return $this;
    }
}

<?php
// api/src/Entity/Dog.php
namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     collectionOperations={
 *         "get"={
 *             "normalization_context"={"groups"={"dog:list", "related"}}
 *         },
 *         "post"
 *     },
 *     itemOperations={"get", "put", "patch", "delete"},
 *     normalizationContext={"groups"={"dog:read", "dog:list", "related"}},
 * )
 * @ORM\Entity()
 */
class Dog extends Animal
{
    /**
     * @ORM\Column(type="boolean")
     * @Groups({"dog:list"})
     */
    private $playsFetch;

    /**
     * @ORM\Column(type="string", length=255)
     * @Groups({"dog:read"})
     * @ApiProperty(
     *     attributes={
     *         "openapi_context"={
     *             "example"="red"
     *         }
     *     }
     * )
     */
    private $doghouseColor;

    /**
     * #@ApiSubresource()
     * @ORM\ManyToMany(targetEntity=Cat::class, mappedBy="dogsChasedBy")
     * @MaxDepth(2)
     * @Groups({"dog:read"})
     */
    private $catsChased;

    public function __construct()
    {
        $this->catsChased = new ArrayCollection();
    }

    public function getPlaysFetch(): ?bool
    {
        return $this->playsFetch;
    }

    public function setPlaysFetch(bool $playsFetch): self
    {
        $this->playsFetch = $playsFetch;

        return $this;
    }

    public function getDoghouseColor(): ?string
    {
        return $this->doghouseColor;
    }

    public function setDoghouseColor(string $doghouseColor): self
    {
        $this->doghouseColor = $doghouseColor;

        return $this;
    }

    /**
     * @return Collection|Cat[]
     */
    public function getCatsChased(): Collection
    {
        return $this->catsChased;
    }

    public function addCatsChased(Cat $catsChased): self
    {
        if (!$this->catsChased->contains($catsChased)) {
            $this->catsChased[] = $catsChased;
            $catsChased->addDogsChasedBy($this);
        }

        return $this;
    }

    public function removeCatsChased(Cat $catsChased): self
    {
        if ($this->catsChased->removeElement($catsChased)) {
            $catsChased->removeDogsChasedBy($this);
        }

        return $this;
    }
}

<?php
// api/src/Entity/Mouse.php
namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiSubresource;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Serializer\Annotation\MaxDepth;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ApiResource(
 *     collectionOperations={
 *         "get"={
 *             "normalization_context"={"groups"={"mouse:list", "related"}}
 *         },
 *         "post"
 *     },
 *     itemOperations={"get", "put", "patch", "delete"},
 *     normalizationContext={"groups"={"mouse:read", "mouse:list", "related"}},
 * )
 * @ORM\Entity()
 */
class Mouse extends Animal
{
    /**
     * @ORM\Column(type="boolean")
     * @Groups({"mouse:read"})
     */
    private $likesCheese;

    /**
     * #@ApiSubresource()
     * @ORM\ManyToOne(targetEntity=Cat::class, inversedBy="miceEaten")
     * @MaxDepth(2)
     * @Groups({"mouse:list", "animal:list"})
     */
    private $ateByCat;

    public function getLikesCheese(): ?bool
    {
        return $this->likesCheese;
    }

    public function setLikesCheese(bool $likesCheese): self
    {
        $this->likesCheese = $likesCheese;

        return $this;
    }

    public function getAteByCat(): ?Cat
    {
        return $this->ateByCat;
    }

    public function setAteByCat(?Cat $ateByCat): self
    {
        $this->ateByCat = $ateByCat;

        return $this;
    }

    /**
     * Represent the entity to the user in a single string
     * @return string
     * @ApiProperty(iri="http://schema.org/name")
     * @Groups({"related"})
     */
    function getLabel() {
        return $this->getBirthday()->format('Y-m-d');
    }
}

# api/config/serialization/Cat.yaml
App\Entity\Cat:
    attributes:
        name:
            groups: ['cat:list']
        sex:
            groups: ['cat:read']
        weight:
            groups: ['cat:read']
        birthday:
            groups: ['cat:list']
        color:
            groups: ['cat:list']

# api/config/serialization/Dog.yaml
App\Entity\Dog:
    attributes:
        name:
            groups: ['dog:list']
        sex:
            groups: ['dog:read']
        weight:
            groups: ['dog:read']
        birthday:
            groups: ['dog:list']
        color:
            groups: ['dog:list']

# api/config/serialization/Mouse.yaml
App\Entity\Mouse:
    attributes:
        name:
            groups: ['mouse:list']
        sex:
            groups: ['mouse:read']
        weight:
            groups: ['mouse:read']
        birthday:
            groups: ['mouse:list']
        color:
            groups: ['mouse:list']

В ответ на дополнительную информацию

Относительно использования метки см. Главу 4 руководства (readmes обеих веток). Метод :: getLabel также обеспечивает инкапсуляцию: представление может быть изменено без изменения api.

Что касается oneOf, allOf или anyOf: длинный список типов, которые генерирует Apip, уродлив, но я думаю, он будет работать для клиентов, которые хотят автоматически проверять значения свойств и абстрактные пользовательские интерфейсы, такие как клиент администратора. Для проектирования / построения клиента и для настройки абстрактного пользовательского интерфейса они могут быть проблематичными, поэтому было бы неплохо, если бы платформа Api автоматически использовала их надлежащим образом, но для большинства команд разработчиков я не думаю, что инвестиции в улучшение фабрики документов OpenApi будут заработаны обратно. Другими словами, адаптация клиентов вручную обычно требует меньше усилий. Так что пока я не трачу на это время.

Более проблематично то, что в JsonLD docs свойства типов из операций, указанных с помощью output =, объединяются с типом самого ресурса. Но это не связано с наследованием.

person MetaClass    schedule 15.03.2021
comment
Спасибо, MetaClass. Я прочитаю ваш ответ несколько раз, чтобы убедиться, что у меня хорошее понимание, а потом надеюсь, что вы ответите на пару вопросов. За последние несколько дней я узнал, что если свойства Animal будут защищены, а не частными, все станет намного лучше! Кроме того, я добавил свои репозитории в свой исходный пост, чтобы вы могли видеть, как я получил определенные конечные точки, чтобы возвращать только определенный класс (ранее /cats возвращал всех животных). В идеале, если будет выявлен ценный контент, вы можете добавить его к своему ответу, чтобы он был еще более исчерпывающим. - person user1032531; 15.03.2021
comment
Здравствуйте, MetaClass, я добавил контент, связанный с вашим ответом, в конец моего исходного вопроса и надеюсь, что вы сможете взглянуть и прокомментировать. Кроме того, что видеть список op chapters в файле readme основной ветки? Спасибо! - person user1032531; 20.03.2021