如何在 api 平台中通过过滤实现自定义项目获取端点?

How to implement custom item get endpoint with filtering in api platform?

我正在开发一个 symfony/api 平台应用程序,允许用户跟踪体育比赛。我的实体看起来像这样(为简洁起见缩短):

User.php

class User implements UserInterface
{
    // ...

    /**
     * @ORM\OneToMany(targetEntity=MatchPlayer::class, mappedBy="user")
     */
    private $matches;

    // ...
}

MatchPlayer.php

class MatchPlayer
{
    // ...

    /**
     * @ORM\ManyToOne(targetEntity=User::class, inversedBy="matches")
     * @ORM\JoinColumn(onDelete="SET NULL")
     */
    private $user;

    /**
     * @ORM\ManyToOne(targetEntity=Match::class, inversedBy="players")
     */
    private $playedMatch;

    /**
     * @ORM\ManyToOne(targetEntity=Position::class, inversedBy="matches")
     */
    private $position;

    // ...
}

Match.php

class Match
{
    // ...

    /**
     * @ORM\Column(type="smallint")
     * @Groups({"match:read"})
     */
    private $outcome;

    /**
     * @ORM\ManyToOne(targetEntity=Sport::class, inversedBy="matches")
     */
    private $sport;

    /**
     * @ORM\OneToMany(targetEntity=MatchPlayer::class, mappedBy="playedMatch", cascade={"persist", "remove"})
     */
    private $players;

    // ....
}

所以在我的模型中,一个用户可以与许多比赛相关联,而一个比赛可以通过胶水与许多用户相关联 table,这也保存了用户所玩的位置。

现在我想使用 api 平台(例如 /api/users/{id}/statistics/api/statistics/{userId} 公开一个端点,该端点动态获取数据并显示用户在何种运动中进行了多少场比赛位置,以及用户有多少匹配 won/tied/lost。理想情况下,端点将允许按运动进行过滤,例如 /api/users/{id}/statistics?sport[]=football&sport[]&outcome=win

因为这些统计数据不会作为一个实体保存到数据库中,所以我尝试了一种类似于 Expose a model without any routes documentation page 的方法。我创建了一个 Statistics 实体,如下所示:

/**
 * @ApiResource(
 *     collectionOperations={},
 *     itemOperations={
 *          "get"={
 *              "controller"=NotFoundAction::class,
 *              "read"=false,
 *              "output"=false,
 *          },
 *     }
 * )
 */
class Statistic
{
    /**
     * @var User
     * @ApiProperty(identifier=true)
     */
    public $user;

    /**
     * @var Position[]|null
     */
    public $position = [];

    /**
     * @var Sport[]|null
     */
    public $maps = [];

    /**
     * @var int
     */
    public $wins = 0;

    /**
     * @var int
     */
    public $ties = 0;

    /**
     * @var int
     */
    public $losses = 0;
}

并向 User 实体添加了自定义操作:

 * @ApiResource(
 *    ...
 *     itemOperations={
 *          "get_statistic"={
 *              "method"="GET",
 *              "path"="/users/{id}/statistics",
 *          }
 *     },
 *    ...
 */

但是我不确定如何按运动、位置和 wins/ties/losses 实施过滤。据我所知,“普通”过滤器不起作用,因为它仅适用于 collections.

上的获取操作

如果这甚至可能,我将如何在我的 api 中实现它?我已经尝试过自定义数据提供程序和控制器,但我无法在任一解决方案中获取过滤器查询参数,并且“普通”过滤器(如 SearchFilter 中内置的 api 平台)将不起作用,因为它仅适用于获取操作collections,我正在处理一件物品。

这绝对有可能,但根据您的选择,您需要做更多的工作才能获得所需的结果。

我将使用自定义操作,因为这更容易解释,而且我已经有了一些代码示例。

要获得过滤所需的信息,您需要采用较低级别的方法。您错过的关键部分是 API 平台构建在 Symfony 之上,因此您可以只使用 Request(用于自定义操作)或 RequestStack(用于数据provider) 获取过滤器。

还要确保 API 平台知道如何序列化您正在输出的数据(Statistics 对象),您需要使用 DTO.

代码如下所示:

在您的实体上,我们添加自定义操作 class 并将输出指定为统计数据 class:

 * @ApiResource(
 *    ...
 *     itemOperations={
 *          "get_statistics"={
 *              "method"="GET",
 *              "path"="/users/{id}/statistics",
 *              "controller"=UserStatsAction::class,
 *              "input"=Statistics::class
 *          }
 *     },
 *    ...
 */

自定义操作代码示例:

final class UserStatsAction
{
    private $em;


    public function __construct(EntityManagerInterface $em)
    {
        $this->em = $em;
    }

    public function __invoke(Request $request)
    {
        $id = $request->get('id');
        $repository = $this->em->getRepository(User::class);
        if(!($user = $repository->find($id))) {
            throw new NotFoundHttpException();
        }

        $sports = $request->query->get('sport', []);
        $outcome = $request->query->get('outcome');

        // Optional: validate your filter data
        $validator = Validation::createValidator();
        $context = $validator->startContext();
        $context->atPath('sports')->validate($sports, [
            new Assert\Choice([
                'choices' => ['football', 'basketball'],
            ]),
        ]);
        $context->atPath('outcome')->validate($outcome, [
            new Assert\Choice([
                'choices' => ['win', 'loose', 'tie'],
            ]),
        ]);
        $violations = $context->getViolations();

        if (0 !== count($violations)) {
            throw new ValidationException($violations);
        }

        // I'll assume you are hnadiling empty/nulls value properly inside this method
        // and return all the stats if 
        $results = $repository->getStatistics($sports, $outcome);

        // For this to work, you'll need to set a DTO for your stats
        return $results;
    }
}

我使用 Request 作为自定义操作的参数,而不是 User 实体。我的示例中有一些您可能不需要的代码,例如从存储库中获取用户或验证过滤器(不过我 鼓励用户输入 cleanup/validation)。

一个重要的提及:API 平台不鼓励自定义操作,您将失去对 GraphQL 的支持。如果您需要 GraphQL,可以使用 DataProvider 实现相同的结果,但这是一个更高级的设置,我需要模拟您应用的某些部分才能弄明白。

希望这对您有所帮助。

更新:

要使过滤器正常工作,您还需要更新 OpenAPI/Swagger 配置,正如 在下面的评论中指出的那样。

您可以使用 PHP 并创建规范化器来做到这一点,如文档的 Override the OpenAPI speficiation 部分所述。

这也可以通过扩展 APIResource 注释来完成,这里有一个例子:

 * @ApiResource(
 *     ...
 *     collectionOperations={
 *          "post",
 *          "get"={
 *              "openapi_context"={
 *                  "parameters"={
 *                      {
 *                          "name": "<query_string_param_name>",
 *                          "type": "string",
 *                          "in": "query",
 *                          "required": false,
 *                          "description": "description",
 *                          "example": ""
 *                      }
 *                  }
 *              }
 *          }
 *     }
 *     ...
 *  })

我发现这种方法更易于使用,但没有记录在案。我根据我的 OpenAPI 规范知识和官方文档中的 Configuring the Entity Receiving the Uploaded File 示例推断了这一点。