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

Комментарии (14)


  1. Delphinum
    18.04.2016 17:54

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

    Зачем же вам контроллер, если запрос моделью обрабатывается?


    1. lowadka
      18.04.2016 17:57

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


      1. Delphinum
        18.04.2016 18:30

        Тобишь фильтры в модели?


        1. lowadka
          18.04.2016 18:44

          Если совсем грубо говоря, то да.

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

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

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

          После всех этих шагов – мы спокойно работаем с моделью, которая внутри себя содержит все необходимые данные и они нам точно подходят


  1. cawakharkov
    18.04.2016 17:58

    А почему не вынести обработку в сервисы, SOA-way?


    1. lowadka
      18.04.2016 18:01

      Обработку чего именно? Вся бизнес логика внутри сервисов, data-mapper тоже сервис, так что нет разницы где и как его использовать


      1. Fesor
        18.04.2016 19:55

        Но что-то мне подсказывает что сущности анемичны.


        1. lowadka
          19.04.2016 00:53

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


          1. Fesor
            19.04.2016 01:49

            Это когда наши сущности это не полноценные объекты с поведением, а тупые структуры из геттеров и сеттеров. [AnemicDomainModel]


  1. LeonidZ
    18.04.2016 21:30

    Сталкивался с этой задачей. Пришлось крутить свой велосипед: https://github.com/leoza/entities-bundle


    1. Fesor
      18.04.2016 22:22
      +1

      Рекомендую к просмотру: Marco Pivetta — Doctrine ORM Good Practices and Tricks


      1. LeonidZ
        19.04.2016 05:26

        Отличное выступление, спасибо большое за ссылку на видео.
        Я решал немного иную задачу, но многие моменты пересекаются с теми вопросами, что рассматривал Марко, поэтому теперь надо хорошо подумать )


  1. bighoc
    19.04.2016 00:49

    symfony.com/doc/current/components/serializer.html как альтернатива data_mapper.


    1. lowadka
      19.04.2016 00:51

      Он отлично подходит для сериализации объекта в json, но для обратного действия он умеет слишком мало, не может и половину фич data mapper`а