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

    Всем привет! Поговорим о сборке решения 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. Об этом поговорим в следующей статье.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 14

      0

      TL;DR Не используйте JMS Serializer

        0
        Собственно об этом будет следующая статья. Но все же, интересно услышать ваше мнение, почему вы считаете, что именно не стоит использовать JMS Serializer.
          0
          Не первый раз сталкиваюсь с тем, что знающие люде рекомендуют обойтись без JMS Serializer.
          Подскажите альтернативы? И пару слов о том, чем он плох. Буду рад услышать. Спасибо.

          Nilov_A, когда ожидается следующий материал?
            0
            Через неделю, 15 июня.
              +1
              Я бы предложил fractal как альтернативу.
                0
                Вторая статья по REST API здесь
                +1

                Это удобно для очень простеньких проектиков. Описал мэппинги и все круто. Но как только задачи становятся посложнее мы даже заметить не успеем как у нас появится свой листенер который фиксит какой-нибудь баг.


                Словом я использовал jms serializer с перерывами где-то 2 года, пока мне не надоело. В итоге на мэппинги я тратил примерно сктолько же времени сколько бы потратил напиши все руками. Причем частенько возникали проблемы при возникновении циклических ссылок на двусторонних связях в сущностях, чего можно было бы избежать явно подготовив данные.


                А как только появляется необходимость суппортить несколько версий API — жизнь превращается в боль. И вот ты уже вынужден делать слой DTO между сущностями и сериализатором что бы все было хорошо. А при таком раскладе проще взять fractal какой и не париться.


                Как альтернатива мне нравится symfony/serializer. Он использует намного более простой подход, используя php как промежуточный формат + нормализаторы. Это позволяет делать все тоже самое но намного проще и более гибко. Можно задавать свои нормализаторы для типов, все явно. Для простых проектов или при наличии DTO оно местами даже проще чем JMS.

                  0
                  Спасибо за ответ.
              0
              А в чём конкретно выражалось «урааа» от перехода с Yii на Symfony? Я полагаю, что речь шла о Yii первой версии, а не второй?
                0
                Наверное стоит удалить это предложение из текста. Иначе это породит бессмысленные холивары.
                  0
                  Да нет же. Просто я занимаюсь довольно плотно разработкой на Yii2 и присматриваюсь к Symfony. Просто интересуюсь мнением человека, который сделал этот переход, без подтекста холивара.
                    +3

                    На самом деле в Symfony нет ничего особенного. Во всяком случае в контексте топика (http api), тут любой фреймворк предоставляющий абстракцию от SAPI и имеющий нормальную реализацию IoC будет неплох. Для меня ключевым является Doctrine ORM, которая реально крутая штука и разбираясь в которой я постоянно испытывал восторг и восхищение. Правда опять же это не тот инструмент который подходит в 100% случаев.


                    В случае с Yii замена любого компонента на что-то получше или просто более подходящее к ситуации вызовет боль. Слишком большая связанность у фреймворка. Попробуйте выкинуть active record и посмотрим что останется. Хотя во второй версии получше, да. В симфони я могу делать что захочу, и при этом без каких-либо кастылей. Многим правда эта гибкость наоборот не нравится. Им хочется что бы все что нужно было из коробки.

                      0
                      Спасибо за мнение.

                      Можно ведь обсуждать фреймворки и без холиваров :D
                        +1
                        Грубо, в Симфони есть всё из коробки, но можно всё это менять на сторонние или свои реализации и это всё равно останется Симфони.

                Only users with full accounts can post comments. Log in, please.