Я думаю, для многих не секрет, что компонент 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)
cawakharkov
18.04.2016 17:58А почему не вынести обработку в сервисы, SOA-way?
lowadka
18.04.2016 18:01Обработку чего именно? Вся бизнес логика внутри сервисов, data-mapper тоже сервис, так что нет разницы где и как его использовать
Fesor
18.04.2016 19:55Но что-то мне подсказывает что сущности анемичны.
lowadka
19.04.2016 00:53Извиняюсь за возможно глупый вопрос, но не могли бы вы объяснить значение этого медицинского термина в данном контексте?
Fesor
19.04.2016 01:49Это когда наши сущности это не полноценные объекты с поведением, а тупые структуры из геттеров и сеттеров. [AnemicDomainModel]
LeonidZ
18.04.2016 21:30Сталкивался с этой задачей. Пришлось крутить свой велосипед: https://github.com/leoza/entities-bundle
Fesor
18.04.2016 22:22+1Рекомендую к просмотру: Marco Pivetta — Doctrine ORM Good Practices and Tricks
LeonidZ
19.04.2016 05:26Отличное выступление, спасибо большое за ссылку на видео.
Я решал немного иную задачу, но многие моменты пересекаются с теми вопросами, что рассматривал Марко, поэтому теперь надо хорошо подумать )
bighoc
19.04.2016 00:49symfony.com/doc/current/components/serializer.html как альтернатива data_mapper.
lowadka
19.04.2016 00:51Он отлично подходит для сериализации объекта в json, но для обратного действия он умеет слишком мало, не может и половину фич data mapper`а
Delphinum
Зачем же вам контроллер, если запрос моделью обрабатывается?
lowadka
Имеется ввиду, что все входные данные читаются не напрямую, а через модель. Это отсекает все лишние проверки, вся бизнес логика построена уже на основе «чистых» данных из модели.
Delphinum
Тобишь фильтры в модели?
lowadka
Если совсем грубо говоря, то да.
На самом деле фильтрация происходит на разных уровнях:
1) JSON, который приходит в body конвертируется в привычный для Symfony вид, например вот так https://github.com/FriendsOfSymfony/FOSRestBundle/blob/2.0/EventListener/BodyListener.php
Грубо говоря на выходе мы получаем ассоциативный массив
2) В дело вступает первая прослойка data mapper`а: каждое значение этого массива приводится к ожидаемому виду, будь то строка, число, массив или что-то более сложное.
3) Модель получает ВСЕГДА ожидаемое значение, если ожидается строка, а прислали массив – до модели оно не дойдет, отскочит с эксепшеном уровнем выше. Можно сказать, что данные, которые приходят в модель – уже относительно чистые. По-сути data mapper это замена формам.
А дальше модель валидируется на основе Constraint`ов записаных в ней, в случае ошибки – так же улетает эксепшен
После всех этих шагов – мы спокойно работаем с моделью, которая внутри себя содержит все необходимые данные и они нам точно подходят