Symfony: обработка запросов в API

  • Tutorial
image

Я думаю, для многих не секрет, что компонент Form плохо подходит для работы в API,
каждый изобретает свой велосипед на замену, одним из таких велосипедов я решил поделиться. На звание “лучшего решения” я не претендую, но если мое решение кому-нибудь окажется полезно, либо я получу новые знания – будет очень здорово.

В нашей API каждый запрос обрабатывается с помощью модели, не важно это сущность доктрины или просто класс. Поэтому решение построено вокруг аннотаций внутри этих моделей.

Модель может выглядеть примерно так:

Листинг модели
<?php
namespace Common\Model;

use Common\Constraint as AppAssert;
use Symfony\Component\Validator\Constraints as Assert;
use Troytft\DataMapperBundle\Annotation\DataMapper;

class PostsFilter
{
    /**
     * @DataMapper(type="string")
     */
    protected $query;

    /**
     * @DataMapper(type="entity", options={"class": "CommonBundle:City"})
     * @Assert\NotBlank
     */
    protected $city;

    /**
     * @return mixed
     */
    public function getCity()
    {
        return $this->city;
    }

    /**
     * @param mixed $value
     */
    public function setCity($value)
    {
        $this->city = $value;

        return $this;
    }

    /**
     * @return string
     */
    public function getQuery()
    {
        return $this->query;
    }

    /**
     * @param string $value
     */
    public function setQuery($value)
    {
        $this->query = $value;
        
        return $this;
    }
}


Аннотация принимает следующие параметры:
name (не обязательный параметр, имя поле в запросе)
type (не обязательный параметр, тип поля, возможные значения: string, integer, float, boolean, timestamp, array, entity, array_of_entity)
groups (не обязательный параметр, scope запроса, нужно, если одна и та же модель используется в разных местах, но с разным набором полей)

А теперь то, как это выглядит в контроллере:

/** @var Request $request */
$request = $this->get('request');
$data = $request->getRealMethod() == 'GET' ? $request->query->all() : $request->request->all();

/** @var DataMapperManager $manager */
$manager = $this->get('data_mapper.manager');

$model = $manager
    ->setGroups($groups)
    ->setValidationGroups($validationGroups)
    ->setIsClearMissing($clearMissing)
    ->setIsValidate(true)
    ->handle($model, $data);


Менеджер сам смаппит все данные на модель, запустит валидацию и если она не пройдет – выбросит исключение.

Если же говорить про реальное использование, то весь код контроллера выносится в базовый контроллер, и внутри реальных экшенов кода становится крайне мало:

public function createAction()
{
    $user = $this->getUser();
    $entity = $this->save($this->handleRequest(new Common\Entity\Blog\Post($this->getUser())));

    $this->getNotificationManager()->notifyModerators($entity);

    return $entity;
}


Код проекта:
— github: github.com/Troytft/data-mapper
— packagist: packagist.org/packages/troytft/data-mapper-bundle
Share post

Comments 19

    0
    В нашей API каждый запрос обрабатывается с помощью модели

    Зачем же вам контроллер, если запрос моделью обрабатывается?
      0
      Имеется ввиду, что все входные данные читаются не напрямую, а через модель. Это отсекает все лишние проверки, вся бизнес логика построена уже на основе «чистых» данных из модели.
        0
        Тобишь фильтры в модели?
          0
          Если совсем грубо говоря, то да.

          На самом деле фильтрация происходит на разных уровнях:
          1) JSON, который приходит в body конвертируется в привычный для Symfony вид, например вот так https://github.com/FriendsOfSymfony/FOSRestBundle/blob/2.0/EventListener/BodyListener.php
          Грубо говоря на выходе мы получаем ассоциативный массив

          2) В дело вступает первая прослойка data mapper`а: каждое значение этого массива приводится к ожидаемому виду, будь то строка, число, массив или что-то более сложное.

          3) Модель получает ВСЕГДА ожидаемое значение, если ожидается строка, а прислали массив – до модели оно не дойдет, отскочит с эксепшеном уровнем выше. Можно сказать, что данные, которые приходят в модель – уже относительно чистые. По-сути data mapper это замена формам.
          А дальше модель валидируется на основе Constraint`ов записаных в ней, в случае ошибки – так же улетает эксепшен

          После всех этих шагов – мы спокойно работаем с моделью, которая внутри себя содержит все необходимые данные и они нам точно подходят
      0
      А почему не вынести обработку в сервисы, SOA-way?
        0
        Обработку чего именно? Вся бизнес логика внутри сервисов, data-mapper тоже сервис, так что нет разницы где и как его использовать
          0
          Но что-то мне подсказывает что сущности анемичны.
            0
            Извиняюсь за возможно глупый вопрос, но не могли бы вы объяснить значение этого медицинского термина в данном контексте?
              0
              Это когда наши сущности это не полноценные объекты с поведением, а тупые структуры из геттеров и сеттеров. [AnemicDomainModel]
        0
        Сталкивался с этой задачей. Пришлось крутить свой велосипед: https://github.com/leoza/entities-bundle
          +1
          Рекомендую к просмотру: Marco Pivetta — Doctrine ORM Good Practices and Tricks
            0
            Отличное выступление, спасибо большое за ссылку на видео.
            Я решал немного иную задачу, но многие моменты пересекаются с теми вопросами, что рассматривал Марко, поэтому теперь надо хорошо подумать )
          0
          symfony.com/doc/current/components/serializer.html как альтернатива data_mapper.
            0
            Он отлично подходит для сериализации объекта в json, но для обратного действия он умеет слишком мало, не может и половину фич data mapper`а
            0
            Много чего перепробовал (формы, сериализацию, чистые данные)
            Везде есть минусы. Если API нагружено хоть немного то формы и сериализаторы сильно негативно влияют на производительность.

            Я остановился на DTO который создается из request с помощью OptionsResolver
            В DTO описывают параметры и валидация.

            1. Реквест преобразуется в DTO
            2. В сервис бизнес логики передается DTO (Тайп хинтинг появляется :) )
            3. Сервис возвращает данные которые передаются в Formatter
            4. Форматированные данные уходят на респонс

              0
              В статье примерно тоже самое
                0
                ну чтож, осталось переименовать DTO запросов в команды, сделать для каждой команды отдельные сервисы-хэндлеры, которые не возвращают ничего, и отдельные сервисы которые получают нужное состояние. И получится почти CQRS.

                p.s. сам так же делал еще относительно недавно, сейчас все больше и больше нравится подход полного разделения ответственности на чтение и запись. Такой код выходит проще всего поддерживать.
                  0
                  Fesor т.е. ты ушел от DTO? Можно где нибудь на github`е посмотреть примеры?
                    0
                    команды можно воспринимать как DTO.

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

              Only users with full accounts can post comments. Log in, please.