Немного нашей истории.
Наш путь к освоению 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. Об этом поговорим в следующей статье.
Комментарии (14)
ErickSkrauch
08.06.2016 20:20А в чём конкретно выражалось «урааа» от перехода с Yii на Symfony? Я полагаю, что речь шла о Yii первой версии, а не второй?
Nilov_A
08.06.2016 20:21Наверное стоит удалить это предложение из текста. Иначе это породит бессмысленные холивары.
ErickSkrauch
08.06.2016 20:40Да нет же. Просто я занимаюсь довольно плотно разработкой на Yii2 и присматриваюсь к Symfony. Просто интересуюсь мнением человека, который сделал этот переход, без подтекста холивара.
Fesor
08.06.2016 20:52+3На самом деле в Symfony нет ничего особенного. Во всяком случае в контексте топика (http api), тут любой фреймворк предоставляющий абстракцию от SAPI и имеющий нормальную реализацию IoC будет неплох. Для меня ключевым является Doctrine ORM, которая реально крутая штука и разбираясь в которой я постоянно испытывал восторг и восхищение. Правда опять же это не тот инструмент который подходит в 100% случаев.
В случае с Yii замена любого компонента на что-то получше или просто более подходящее к ситуации вызовет боль. Слишком большая связанность у фреймворка. Попробуйте выкинуть active record и посмотрим что останется. Хотя во второй версии получше, да. В симфони я могу делать что захочу, и при этом без каких-либо кастылей. Многим правда эта гибкость наоборот не нравится. Им хочется что бы все что нужно было из коробки.
VolCh
10.06.2016 07:10+1Грубо, в Симфони есть всё из коробки, но можно всё это менять на сторонние или свои реализации и это всё равно останется Симфони.
Fesor
TL;DR Не используйте JMS Serializer
Nilov_A
Собственно об этом будет следующая статья. Но все же, интересно услышать ваше мнение, почему вы считаете, что именно не стоит использовать JMS Serializer.
abiruba
Не первый раз сталкиваюсь с тем, что знающие люде рекомендуют обойтись без JMS Serializer.
Подскажите альтернативы? И пару слов о том, чем он плох. Буду рад услышать. Спасибо.
Nilov_A, когда ожидается следующий материал?
Nilov_A
Через неделю, 15 июня.
Big_Shark
Я бы предложил fractal как альтернативу.
Nilov_A
Вторая статья по REST API здесь
Fesor
Это удобно для очень простеньких проектиков. Описал мэппинги и все круто. Но как только задачи становятся посложнее мы даже заметить не успеем как у нас появится свой листенер который фиксит какой-нибудь баг.
Словом я использовал jms serializer с перерывами где-то 2 года, пока мне не надоело. В итоге на мэппинги я тратил примерно сктолько же времени сколько бы потратил напиши все руками. Причем частенько возникали проблемы при возникновении циклических ссылок на двусторонних связях в сущностях, чего можно было бы избежать явно подготовив данные.
А как только появляется необходимость суппортить несколько версий API — жизнь превращается в боль. И вот ты уже вынужден делать слой DTO между сущностями и сериализатором что бы все было хорошо. А при таком раскладе проще взять fractal какой и не париться.
Как альтернатива мне нравится symfony/serializer. Он использует намного более простой подход, используя php как промежуточный формат + нормализаторы. Это позволяет делать все тоже самое но намного проще и более гибко. Можно задавать свои нормализаторы для типов, все явно. Для простых проектов или при наличии DTO оно местами даже проще чем JMS.
Nilov_A
Спасибо за ответ.