API 平台资源的多个密钥标识符

问题描述

我有一个 Chart 对象,其中包含 Serie 对象的集合,而这些 Serie 对象包含 Data 对象的集合。与其要求 REST API 通过代理主键来识别 Serie 和 Data 对象,我希望根据它们在各自集合中的位置来这样做。

数据库架构如下。最初我考虑制作 serie 的 chart_id/position 和 data 的 chart_id/serie_id/position 复合主键,但是,Doctrine 只能为 one level(即 Serie)这样做,并且为了保持一致,我对所有表使用代理键.

chart
- id (PK autoincrement)
- name

serie
- id (PK autoincrement)
- position (int with unique constraint with chart_id)
- name
- chart_id (FK)

data
- id (PK autoincrement)
- position (int with unique constraint with serie_id)
- name
- serie_id (FK)
- value

/charts/1 的完全水合响应将返回如下所示的 JSON。例如,要定位名称Series1Data1 的数据对象,url 将是 /charts/1/series/0/datas/1,或者如果需要 /datas/chart=1;seriePosition=0;dataPosition=1 也可以工作。

{
    "id": 1,"name": "chart1"
    "series": [{
            "chart": "/chart/1","position": 0,"name": "series0.chart1","datas": [{
                    "serie": "/charts/1/series/0","name": "datas0.series0.chart1"
                },{
                    "serie": "/series/chart=1;position=0","position": 1,"name": "datas1.series0.chart1"
                }
            ]
        },{
            "chart": "/chart/1","name": "series1.chart1","datas": []
        }
    ]
}

为了更改标识符,我使用 @ApiProperty 将 Serie 和 Data 的主键 identifier$id 设置为 false 以及 Serie 的 $chart$position 和数据的 $serie$position 为真。

实体/图表.PHP

<?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 Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

/**
 * @ORM\Entity()
 * @ApiResource()
 */
class Chart
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\OnetoMany(targetEntity=Serie::class,mappedBy="chart")
     */
    private $series;

    /**
     * @ORM\Column(type="string",length=45)
     */
    private $name;
    
    public function __construct()
    {
        $this->series = new ArrayCollection();
    }

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

    public function getSeries(): Collection
    {
        return $this->series;
    }

    public function addSeries(Serie $series): self
    {
        exit(sprintf('Chart::addSeries() $this->series->contains($series): %s',$this->series->contains($series)?'true':'false'));
        if (!$this->series->contains($series)) {
            $this->series[] = $series;
            $series->setChart($this);
        }

        return $this;
    }

    public function removeSeries(Serie $series): self
    {
        if ($this->series->removeElement($series)) {
            if ($series->getChart() === $this) {
                $series->setChart(null);
            }
        }

        return $this;
    }

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

        return $this;
    }

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

实体/Serie.PHP

<?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 Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

/**
 * @ORM\Entity()
 * @ORM\Table(uniqueConstraints={@ORM\UniqueConstraint(name="unique_position_serie",columns={"chart_id","position"}),@ORM\UniqueConstraint(name="unique_name_serie","name"})})
 * @ApiResource(
 *   collectionoperations={
 *     "get1" = {  
 *       "method" = "get",*     },*     "get2" = {  
 *       "method" = "get",*       "path" = "/charts/{id}/series",*       "requirements" = {
 *         "id" = "\d+",*       },*       "openapi_context" = {
 *         "parameters" = {
 *           {
 *             "name" = "id",*             "in" = "path",*             "required" = true,*             "description" = "Chart ID",*             "schema" = {
 *               "type" = "integer"
 *             }
 *           }
 *         }
 *       }
 *     },*     "post1" = {  
 *       "method" = "post",*     "post2" = {  
 *       "method" = "post",*             "schema" = {
 *               "type" = "integer"
 *             }
 *           }
 *         }
 *       }
 *     }
 *   },*   itemOperations={
 *     "get1" = {  
 *       "method" = "get",*       "path" = "/charts/{id}/series/{position}",*         "position" = "\d+",*       "openapi_context" = {
 *         "parameters" = {
 *           {
 *             "name" = "position",*             "description" = "Series position in chart",*     "put1" = {  
 *       "method" = "put",*     "put2" = {  
 *       "method" = "put",*     "patch1" = {  
 *       "method" = "patch",*     "patch2" = {  
 *       "method" = "patch",*     "delete1" = {  
 *       "method" = "delete",*     "delete2" = {  
 *       "method" = "delete",*             "schema" = {
 *               "type" = "integer"
 *             }
 *           }
 *         }
 *       }
 *     }
 *   }
 * )
 */

class Serie
{
    /**
    * @ORM\Id
    * @ORM\GeneratedValue(strategy="IDENTITY")
    * @ORM\Column(type="integer")
    * @ApiProperty(identifier=false)
    */
    private $id;

    /**
    * @ORM\ManyToOne(targetEntity=Chart::class,inversedBy="series")
    * @ORM\JoinColumn(nullable=false,onDelete="CASCADE")
    * @ApiProperty(identifier=true)
    * ApiProperty(push=true)
    */
    private $chart;

    /**
    * @ORM\Column(type="integer")
    * @ApiProperty(identifier=true)
    * ApiProperty(push=true)
    */
    private $position;

    /**
    * @ORM\OnetoMany(targetEntity=Data::class,mappedBy="serie")
    */
    private $data;

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

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

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

    public function getChart(): ?Chart
    {
        return $this->chart;
    }

    public function setChart(?Chart $chart): self
    {
        $this->chart = $chart;

        return $this;
    }

    public function getData(): Collection
    {
        return $this->data;
    }

    public function addData(Data $data): self
    {
        if (!$this->data->contains($data)) {
            $this->data[] = $data;
            $data->setSerie($this);
        }

        return $this;
    }

    public function removeData(Data $data): self
    {
        if ($this->data->removeElement($data)) {
            if ($data->getSerie() === $this) {
                $data->setSerie(null);
            }
        }

        return $this;
    }

    public function setPosition(int $position)
    {
        $this->position = $position;

        return $this;
    }

    public function getPosition()
    {
        return $this->position;
    }

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

        return $this;
    }

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

实体/数据.PHP

<?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 Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity()
 * @ORM\Table(uniqueConstraints={@ORM\UniqueConstraint(name="unique_name_data",columns={"serie_id","name"}),@ORM\UniqueConstraint(name="unique_position_data","position"})})
 * @ApiResource(
 *   collectionoperations={
 *     "get1" = {  
 *       "method" = "get",*       "path" = "/charts/{id}/series/{position}/datas",*             "schema" = {
 *               "type" = "integer"
 *             }
 *           },*           {
 *             "name" = "position",*       "path" = "/charts/{id}/series/{series_position}/datas/{position}",*         "series_position" = "\d+",*       "openapi_context" = {
 *         "parameters" = {
 *           {
 *             "name" = "series_position",*             "description" = "Datas position in chart",*             "schema" = {
 *               "type" = "integer"
 *             }
 *           }
 *           }
 *       }
 *     },*             "schema" = {
 *               "type" = "integer"
 *             }
 *           }
 *           }
 *       }
 *     }
 *   }
 * )
 */
class Data
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(type="integer")
     * @ApiProperty(identifier=false)
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity=Serie::class,inversedBy="data")
     * @ORM\JoinColumn(nullable=false,onDelete="CASCADE")
     * @ApiProperty(identifier=true)
     * ApiProperty(push=true)
     */
    private $serie;

    /**
     * @ORM\Column(type="integer")
     * @ApiProperty(identifier=true)
     * ApiProperty(push=true)
     */
    private $position;

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

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

    public function getSerie(): ?Serie
    {
        return $this->serie;
    }

    public function setSerie(?Serie $serie): self
    {
        $this->serie = $serie;

        return $this;
    }

    public function setPosition(int $position)
    {
        $this->position = $position;

        return $this;
    }

    public function getPosition()
    {
        return $this->position;
    }

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

        return $this;
    }

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

API-Platform 能够为图表和系列项目生成 IRI,但不能为数据项目生成 IRI。我怀疑这是因为 Data 的 $serie 标识符属性需要 chartId 和它的位置,但不知道如何解决它。

我查看了 subresources,但是,它们只支持 GET 请求和 subresources will be deprecated in favor of multiple ApiResources(但是,我什至不知道“多个 ApiResources”是什么意思)。另外,可能与装饰 IriConverter 有关,但不确定。

我需要做什么才能允许数据资源通过其在系列集合中的位置来识别,并且 Swagger 文档要反映这样做?

编辑 - 附加内容

我更改了实体以尝试实现两种不同的方法,但不幸的是这两种方法都没有完全奏效。我应该将精力集中在两种方法中的一种吗?

  1. 父代标识符在查询中的位置(即 /datas/chart=1;serie_position=0;data_position=0

  2. 父的标识符在路径中的位置(即/charts/1/series/0/datas/0

如果我想使用占位符 chart_id 而不是 id(即 /charts/{chart_id}/series/{series_position}/datas/{position} 而不是 /charts/{id}/series/{series_position}/datas/{position},我如何删除重命名图表的 id

我是否应该以不同的方式执行此操作,这样我就不需要所有注释,并且可能重命名图表的 id?在某种程度上,我能够让 Swagger 提供字段,但不要认为我做得对。也许在decorating api_platform.openapi.factory之前?

enter image description here

解决方法

遗憾的是,我发现 api-platform 对于设计/实现良好的多级 api(以及通常不符合存储在 ORM 中的实体的 api 资源)来说太天真和非常有限,并导致真正不可读/不可维护的代码快。

对于类似的用例,我使用了一个带有三个必需参数的 api 端点(更不用说 api-platform 在一个半月后从项目中删除了)。