Я хочу использовать 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. Но как-нибудь понадобится это реализовать? Например, идентификатор животного был в какой-то другой таблице, потребуется документация для кошки, собаки или мыши, не так ли? Или вместо этого используется этот длинный список типов, являющийся результатом каждой комбинации групп сериализации?
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\cat
и\dog
? Спасибо - person user1032531   schedule 01.03.2021oneOf
. Это может быть то, что вы ищете, если сможете заставить его работать. Тем не менее, я склоняюсь к тому, чтобы иметь отдельные конечные точки для ресурсов, которые не имеют одинаковой структуры. - person Jeroen van der Laan   schedule 01.03.2021