Всем привет! Поговорим о сборке решения REST API на FOSRestBundle + JMSSerializerBundle.
Наш путь к освоению REST API начался около четырех лет назад (мы — это ГЛАВВЕБ). Первой попыткой было написание собственного велосипеда на Yii фреймворке. Получилось. И в дальнейшем, с небольшими доработками, мы применили это решение на нескольких небольших проектах. Так как я не сторонних собственных «велосипедов», в следующих проектах мы уже использовать один из restful экстеншенов Yii фреймворка, периодически допиливая его.
Затем в нашу компанию пришел Symfony. Новых проектов на Yii мы уже не брали. Начали смотреть, какие существуют REST решения для Symfony. Конечно же первое с чем мы начали экспериментировать — стандартная сборка FOSRestBundle + JMSSerializer (+ NelmioApiDocBundle для генерации документации). Если кому интересно, документация здесь. Что тут понравилось, так это отсутствие магии с контроллерами (я имею ввиду динамическую генерацию роутов исходя из моделей и обработку всех запросов в базовом контроллере) и присутствие магии с генерацией документации.
Решение основанное на FOSRestBundle + JMSSerializer неплохо себя зарекомендовало в наших проектах. Но прежде чем разрабатывать проект больше чем собственный бложек, нужно определиться со следующими вопросами:
Давайте подробнее по каждому из них.
Как реализовать систему управления правами доступа?
У симфони из коробки есть пару решений на этот счет:
— использовать ACL, можно почитать тут;
— организовать систему разделения прав доступа на основе ролей + вотеров (voter), читать тут.
Для себя мы выбрали второй вариант.
Как организовать фильтрацию списков?
Сперва, как наверно и большинство разработчиков, мы создали базовый контроллер. В нем реализовали основные методы для фильтрации, создания и обновления сущностей. Метод реализующий фильтрацию динамически генерировал кверибилдер исходя из переданных параметров в запросе. Из проекта в проект мы переносили этот контроллер. Где-то его дорабатывали по надобности. В конечном счете, в разных проектах этот базовый контроллер имел существенные различия.
Далее мы решили это немного оформить. Вынесли фильтрацию в специальный сервис, логику с добавлением и обновлением сущностей в отдельный класс (паттерн экшен). Так родился GlavwebRestBundle. В то время он выглядел примерно так.
Как определить границы вложенности сущностей друг в дуга?
И имею ввиду, ту ситауцию когда одна сущность содержит коллекцию других. Для решения этой задачи у JMSSerializer есть атрибут «MaxDepth», в сущностях это выглядит примерно так:
Но тут есть подводные камни. Глубина считается с начала json объекта получившегося в результате, а не исходя из сущности. Т.е. если наш объект вложен в коллекцию, то глубина должны быть 3, а если мы возвращаем наш объект в единственном экземпляре, то глубина = 2. Когда сущности вложены друг в друга много раз, получаются страшные вещи типа: JMS\MaxDepth(depth=7). Ниже я покажу как мы избавились от MaxDepth.
Как возвращать только определенные поля сущности?
Допустим мы имеем сущность «пользователь», пользователь содержит ряд полей, в том числе и пароль который мы не хотим показывать в апи. В этом нам поможет стратегия ExclusionPolicy и атрибут «Expose» в JMSSerializer.
Для класса определяем стратегию ExclusionPolicy:
И Expose указываем для тех полей которые нам нужны в апи, все остальные будут пропущены JMSSerializer-ом
Как возвращать определенный набор полей в зависимости от запроса?
Часто для списка объектов нам нужен ограниченный набор данных, а для просмотра конкретного объекта — полный. Это возможно реализовать с помощью атрибута «Groups» в JMSSerializer. Для каждой сущности мы определили как минимум две группы: entity_list и entity_view.
В контроллере с через параметры запроса мы получаем необходимые значения и передаем их в SerializerContext сериализера.
Это решило проблему с вложенностью, нам больше не нужно указывать MaxDepth для полей. Теперь клиент обращаясь к апи мог сам конфигурировать необходимую ему вложенность и выбирать один из двух наборов полей (list или view).
Как видоизменять возвращаемые значения?
Тут тоже на помощь приходит JMSSerializer, определяем листенер и в нем меняем вывод как нам хочется.
Как организовать загрузку файлов в PUT?
Т.к. метод PUT не позволяет отправлять форму, были варианты для обновления файлов использовать POST либо файлы кодировать в base64. Ни тот ни другой вариант нас не устроил. Приняли решение загрузку и удаления файлов реализовать с помощью отдельных запросов к апи для каждого поля. Допустим, у пользователя есть поле «avatar», соответственно необходимо реализовать два дополнительных метода: POST /api/user/{user}/avatar для загрузки нового аватара (передаем форму с одним полем file) и DELETE /api/user/{user}/avatar для удаления существующего аватара.
Как тестировать REST API?
Очень важный вопрос, по крайней мере для нас. Здесь достаточно нюансов, я опишу их подробнее в одной из следующих статей. Если коротко, то мы использовали LiipFunctionalTestBundle + фикстуры в связке с AliceBundle. И написали собственный класс в котором реализовали необходимые нам функции. Этот компонент так же был определен в GlavwebRestBundle.
Как показала практика, решение FOSRestBundle + JMSSerializer в целом рабочее. Но мир диктует все больше требований. Это вынудило нас на пересмотр концепции реализации REST API на Symfony. Об этом поговорим в следующей статье.
Немного нашей истории.
Наш путь к освоению REST API начался около четырех лет назад (мы — это ГЛАВВЕБ). Первой попыткой было написание собственного велосипеда на Yii фреймворке. Получилось. И в дальнейшем, с небольшими доработками, мы применили это решение на нескольких небольших проектах. Так как я не сторонних собственных «велосипедов», в следующих проектах мы уже использовать один из restful экстеншенов Yii фреймворка, периодически допиливая его.
Затем в нашу компанию пришел Symfony. Новых проектов на Yii мы уже не брали. Начали смотреть, какие существуют REST решения для Symfony. Конечно же первое с чем мы начали экспериментировать — стандартная сборка FOSRestBundle + JMSSerializer (+ NelmioApiDocBundle для генерации документации). Если кому интересно, документация здесь. Что тут понравилось, так это отсутствие магии с контроллерами (я имею ввиду динамическую генерацию роутов исходя из моделей и обработку всех запросов в базовом контроллере) и присутствие магии с генерацией документации.
Итак, FOSRestBundle + JMSSerializerBundle
Решение основанное на FOSRestBundle + JMSSerializer неплохо себя зарекомендовало в наших проектах. Но прежде чем разрабатывать проект больше чем собственный бложек, нужно определиться со следующими вопросами:
- как реализовать систему управления правами доступа?
- как организовать фильтрацию списков?
- как определить границы вложенности сущностей друг в дуга?
- как возвращать только определенные поля сущности?
- как возвращать определенный набор полей в зависимости от запроса?
- как видоизменять возвращаемые значения?
- как организовать загрузку файлов в PUT?
- как тестировать REST API?
Давайте подробнее по каждому из них.
Как реализовать систему управления правами доступа?
У симфони из коробки есть пару решений на этот счет:
— использовать ACL, можно почитать тут;
— организовать систему разделения прав доступа на основе ролей + вотеров (voter), читать тут.
Для себя мы выбрали второй вариант.
Как организовать фильтрацию списков?
Сперва, как наверно и большинство разработчиков, мы создали базовый контроллер. В нем реализовали основные методы для фильтрации, создания и обновления сущностей. Метод реализующий фильтрацию динамически генерировал кверибилдер исходя из переданных параметров в запросе. Из проекта в проект мы переносили этот контроллер. Где-то его дорабатывали по надобности. В конечном счете, в разных проектах этот базовый контроллер имел существенные различия.
Далее мы решили это немного оформить. Вынесли фильтрацию в специальный сервис, логику с добавлением и обновлением сущностей в отдельный класс (паттерн экшен). Так родился GlavwebRestBundle. В то время он выглядел примерно так.
Как определить границы вложенности сущностей друг в дуга?
И имею ввиду, ту ситауцию когда одна сущность содержит коллекцию других. Для решения этой задачи у JMSSerializer есть атрибут «MaxDepth», в сущностях это выглядит примерно так:
/**
* @JMS\MaxDepth(depth=2)
*/
private $groups;
Но тут есть подводные камни. Глубина считается с начала json объекта получившегося в результате, а не исходя из сущности. Т.е. если наш объект вложен в коллекцию, то глубина должны быть 3, а если мы возвращаем наш объект в единственном экземпляре, то глубина = 2. Когда сущности вложены друг в друга много раз, получаются страшные вещи типа: JMS\MaxDepth(depth=7). Ниже я покажу как мы избавились от MaxDepth.
Как возвращать только определенные поля сущности?
Допустим мы имеем сущность «пользователь», пользователь содержит ряд полей, в том числе и пароль который мы не хотим показывать в апи. В этом нам поможет стратегия ExclusionPolicy и атрибут «Expose» в JMSSerializer.
Для класса определяем стратегию ExclusionPolicy:
use JMS\Serializer\Annotation as JMS;
/**
* @JMS\ExclusionPolicy("all")
*/
class MedicalEscortType {
И Expose указываем для тех полей которые нам нужны в апи, все остальные будут пропущены JMSSerializer-ом
/**
* @JMS\Expose
* @var integer
*/
private $name;
Как возвращать определенный набор полей в зависимости от запроса?
Часто для списка объектов нам нужен ограниченный набор данных, а для просмотра конкретного объекта — полный. Это возможно реализовать с помощью атрибута «Groups» в JMSSerializer. Для каждой сущности мы определили как минимум две группы: entity_list и entity_view.
В контроллере с через параметры запроса мы получаем необходимые значения и передаем их в SerializerContext сериализера.
$scopes = array_map('trim', explode(',', $request->get('_scope')));
$serializationContext = SerializationContext::create()
->setGroups(array_merge($scopes, [GroupsExclusionStrategy::DEFAULT_GROUP]))
;
$view = $this->view($data, $statusCode, $headers);
$view->setSerializationContext($serializationContext)
return $view;
Это решило проблему с вложенностью, нам больше не нужно указывать MaxDepth для полей. Теперь клиент обращаясь к апи мог сам конфигурировать необходимую ему вложенность и выбирать один из двух наборов полей (list или view).
Как видоизменять возвращаемые значения?
Тут тоже на помощь приходит JMSSerializer, определяем листенер и в нем меняем вывод как нам хочется.
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
/**
* Class SerializationListener
* @package AppBundle\Listener
*/
class SerializationListener implements EventSubscriberInterface
{
/**
* @var UploaderHelper
*/
private $uploaderHelper;
/**
* @param UploaderHelper $uploaderHelper
*/
public function __construct(UploaderHelper $uploaderHelper)
{
$this->uploaderHelper = $uploaderHelper;
}
/**
* @inheritdoc
*/
static public function getSubscribedEvents()
{
return array(
array('event' => 'serializer.post_serialize', 'class' => 'AppBundle\Entity\User', 'method' => 'onPostSerializeUserAvatar')
);
}
/**
* @param ObjectEvent $event
*/
public function onPostSerializeUserAvatar(ObjectEvent $event)
{
$url = $this->uploaderHelper->asset($event->getObject(), 'avatarFile');
$event->getVisitor()->addData('avatarUrl', $url);
}
Как организовать загрузку файлов в PUT?
Т.к. метод PUT не позволяет отправлять форму, были варианты для обновления файлов использовать POST либо файлы кодировать в base64. Ни тот ни другой вариант нас не устроил. Приняли решение загрузку и удаления файлов реализовать с помощью отдельных запросов к апи для каждого поля. Допустим, у пользователя есть поле «avatar», соответственно необходимо реализовать два дополнительных метода: POST /api/user/{user}/avatar для загрузки нового аватара (передаем форму с одним полем file) и DELETE /api/user/{user}/avatar для удаления существующего аватара.
Как тестировать REST API?
Очень важный вопрос, по крайней мере для нас. Здесь достаточно нюансов, я опишу их подробнее в одной из следующих статей. Если коротко, то мы использовали LiipFunctionalTestBundle + фикстуры в связке с AliceBundle. И написали собственный класс в котором реализовали необходимые нам функции. Этот компонент так же был определен в GlavwebRestBundle.
Заключение
Как показала практика, решение FOSRestBundle + JMSSerializer в целом рабочее. Но мир диктует все больше требований. Это вынудило нас на пересмотр концепции реализации REST API на Symfony. Об этом поговорим в следующей статье.