Pull to refresh

Пара слов о REST API на Symfony в связке FOSRestBundle + JMSSerializerBundle

Reading time5 min
Views15K
Всем привет! Поговорим о сборке решения REST API на FOSRestBundle + JMSSerializerBundle.


Немного нашей истории.


Наш путь к освоению 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. Об этом поговорим в следующей статье.
Tags:
Hubs:
Total votes 6: ↑3 and ↓30
Comments14

Articles