Как реализовать пользовательский элемент для получения конечной точки с фильтрацией на платформе API? - PullRequest
2 голосов
/ 09 июля 2020

Я работаю над приложением платформы 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;

    // ....
}

Итак, в моей модели пользователь может относиться ко многим совпадениям, а совпадение может относиться ко многим пользователям через клей таблица, которая также сохраняет позицию, которую играл пользователь.

Теперь я хочу открыть конечную точку с платформой api, например /api/users/{id}/statistics или /api/statistics/{userId}, которая динамически извлекает данные и показывает, сколько матчей в каком виде спорта сыграл пользователь. , на какой позиции и сколько матчей пользователь выиграл / сыграл / проиграл. В идеале конечная точка должна позволять фильтрацию по видам спорта и выглядеть, например, примерно как /api/users/{id}/statistics?sport[]=football&sport[]&outcome=win.

Поскольку эта статистика не сохраняется в базе данных как объект, я попробовал подход, аналогичный Выставить модель без каких-либо маршрутов, страница документации . Я создал объект 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",
 *          }
 *     },
 *    ...
 */

Однако я не уверен, как реализовать фильтрация по видам спорта, позициям и победам / ничьям / поражениям. Насколько мне известно, «нормальный» фильтр не работает, поскольку он применяется только к операции get для коллекций.

Если это вообще возможно, как мне реализовать это в моем API? Я уже пробовал настраиваемые поставщики данных и контроллеры, но я не могу получить параметры запроса фильтра ни в одном решении, а «нормальный» фильтр (например, платформы API, встроенные в SearchFilter) не будет работать, поскольку он применяется только к операции получения в коллекциях, и Я имею дело с товаром.

1 Ответ

1 голос
/ 10 июля 2020

Это определенно возможно, но в зависимости от вашего выбора вам нужно проделать больше работы, чтобы получить желаемый результат.

Я go с помощью специальной операции, так как это легче объяснить, и у меня уже есть код примеры.

Чтобы получить информацию, необходимую для фильтрации, вам потребуется go с более низким уровнем доступа. Ключевая часть, которую вы пропустили, заключается в том, что платформа API построена на основе Symfony, поэтому вы можете просто использовать Request (для пользовательской операции) или RequestStack (для поставщика данных), чтобы получить фильтры. .

Также, чтобы убедиться, что платформа API знает, как сериализовать данные, которые вы выводите (Statistics объекты), вам необходимо использовать DTO .

Вот как выглядит код:

В вашей сущности мы добавляем класс настраиваемой операции и указываем вывод как класс статистики:

 * @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 = $this->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. В моем примере есть код, который может вам не понадобиться, например, выборка пользователя из репозитория или проверка фильтров (хотя я делаю поощряю очистку / проверку ввода пользователя).

Одно важное упоминание : пользовательские операции не поддерживаются платформой API, и вы потеряете поддержку GraphQL. Если вам нужен GraphQL, того же результата можно добиться с помощью DataProvider, но это более сложная настройка, и мне нужно будет смоделировать некоторые части вашего приложения, чтобы понять это.

Надеюсь, это поможет.

Обновление:

Чтобы фильтры работали, вам также необходимо обновить конфигурацию OpenAPI / Swagger, как tobias ingold указано в комментарии ниже.

Вы можете сделать это, используя PHP и создав нормализатор, как описано в разделе Переопределить спецификацию OpenAPI документации.

Это также может быть сделано путем расширения аннотации APIResource, вот пример:

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

Я нашел этот подход более простым в использовании, но он не задокументирован. Я экстраполировал это на основе своих знаний об OpenAPI spe c и примера Настройка объекта, получающего загруженный файл в официальной документации.

...