REST API на Symfony, FOSRestBundle + GlavwebDatagridBundle

    Всем привет! В прошлой статье я рассказал о нашем опыте в REST API со сборкой на FOSRestBundle + JMSSerializer. Сегодня я поделюсь нашим подходом к разработке REST API на FOSRestBundle + GlavwebDatagridBundle.

    Новые задачи.


    С ростом количества REST проектов возрастали требования к нашему апи. Часто пользователями нашего апи являются сторонние разработчики, которые имеют те или иные потребности для своих решений. Так, например, не всем было по душе передавать конфигурацию вложенности объектов в запросах. Возможностей фильтрации из коробки так же не хватало.

    Но самым весомым основанием к пересмотру подхода к разработке апи были проблемы с производительностью. Причинами этих проблем были либо не оптимально написанный SQL, либо дополнительные затраты на обработку результата доктриной и сериализером.

    Если с SQL понятно в какую сторону оптимизировать, то с потерей производительности в доктрине и сериализере сложнее. Вот, как это выглядит после того, как запрос к БД отработал:

    получение массива данных из PDO -> преобразования массива в объекты (в доктрине, гидрация) -> вызов листенера для каждого поля каждого объекта в jmsserializer -> преобразование объекта в json в сериализере.
    

    Было решено сократить этот путь до:

    получение массива данных из PDO -> преобразования массива в многомерный ассоциативный массив (в доктрине, гидрация) -> по необходимости, преобразование данных сразу в массиве.
    

    Так как мы отказались от гидрации в объекты, пришлось отказаться и от JMSSerializer тоже. JMSSerializer делал много полезного для нашего апи (это я описал в предыдущей статье). Кроме того он выполнял еще одну важную работу — догружал вложенные сущности, если они не были определены в join-ах.

    Для того, чтобы закрыть образовавшуюся «дыру» в функциональности, возникшую в результате отказа от jmsserializer, был разработан GlavwebDatagridBundle.

    В «двух словах» о GlavwebDatagridBundle


    Коротко определить предназначение GlavwebDatagridBundle можно так: получать данные, отформатированные нужным образом, на основе фильтров, лимита и оффсета. Не понятно? Знаю. А теперь обо всем по порядку.

    В основе GlavwebDatagridBundle лежат следующие компоненты:
    • DatagridBuilder;
    • Datagrid;
    • Filter;
    • DataSchema + Scope.

    DatagridBuilder


    DatagridBuilder формирует QueryBuilder используя DataSchema, фильтры и дополнительные параметры запроса:

    $datagridBuilder = $this->get('glavweb_datagrid.doctrine_datagrid_builder');
    $datagridBuilder
        ->setEntityClassName(Entity::class)
        ->setFirstResult(0)
        ->setMaxResults(100)
        ->setOrderings(['id'=>'DESC'])
        ->setOperators(['field1' => '='])
        ->setDataSchema('entity.schema.yml', 'entity/list.yml')
    ;
    

    Далее возвращает объект Datagrid:

    $datagrid = $datagridBuilder->build($paramFetcher->all());
    

    Filter


    Фильтры позволяют задавать дополнительные условия в запрос.

    // Define filters
    $datagridBuilder
        ->addFilter('field1')
        ->addFilter('field2')
    ;
    

    Datagrid


    Datagrid получает QueryBuilder из DatagridBuilder и позволяет вернуть данные как в виде списка:
    $datagrid->getList();
    

    так и в виде одной строки:

    $datagrid->getItem();
    

    DataSchema


    DataSchema определяет набор данных в формате yaml:

    schema:
        class: AppBundle\Entity\Article
        db_driver: orm
        properties:
            id: # integer
            type: # ArticleType
            name: # string
            body: # text
            publish: # boolean
            publishAt: # datetime
    

    Определяет поля и связи:

    schema:
        class: AppBundle\Entity\Movie
        db_driver: orm
        properties:
            ...
    
            tags: # AppBundle\Entity\Tag
                join: left
                properties:
                    id: # integer
    

    Дополнительные условия:

    schema:
        class: AppBundle\Entity\Article
        db_driver: orm
        conditions: ["{{alias}}.publish = true"]
        properties:
            ....
    

    Так же возможно определять дополнительные условия и для связей:

    schema:
        class: AppBundle\Entity\Movie
        db_driver: orm
        properties:
            ...
    
            articles: # AppBundle\Entity\Article
                conditions: ["{{alias}}.publish = true"]
                join: none
                properties:
                    ....
    

    DataSchema так же реализует функцию догрузки вложенных сущностей, если они не были сконфигурированы как join:

    schema:
        class: AppBundle\Entity\Movie
        db_driver: orm
        properties:
            ...
    
            articles: # AppBundle\Entity\Article
                join: none
                properties:
                    id: # integer
    

    DataSchema используется для построения запроса в DatagridBuilder и для преобразования данных в Datagrid.

    Scope


    Scope позволяет сузить набор данных, определенных в DataSchema.

    Например, с помощью scope можно определить небольшой объем данных для списка записей (article\list.yml):

    scope:
        id: 
        name: 
    

    и полный набор данных для просмотра одной записи (article\view.yml):

    scope:
        id: 
        type: 
        name: 
        body: 
        publish: 
        publishAt: 
        movies: # AppBundle\Entity\Movie
            id:
            name:
    


    От слов к демо.


    Предлагаю вашему вниманию вымышленное апи кинотеатра, созданное специально для демонстрации. Сущности и поля сущностей подобраны таким образом, чтобы максимально раскрыть возможности апи.

    Демо проект размещен на гитхабе.

    Для того, что бы развернуть проект локально нужно сделать 3 простых действия:

    1. Создать проект через композер.

    composer create-project glavweb/rest-demo-app
    

    2. Выполнить миграции.

    php bin/console d:m:m -n
    

    3. Выполнить фикстуры.

    php bin/console h:d:f:l -n
    


    Сущности


    В наличии следующие сущности:
    • Movie. Фильмы.
    • MovieDetail. Подробная информация о фильме фильма.
    • MovieSession. Сеансы фильмов.
    • MovieGroup. Группы фильмов.
    • MovieComment. Комментарии к фильмам.
    • Image. Изображения, используются в комментариях к фильмам.
    • Article. Статьи.
    • Tag. Тэги.

    Между ними имеем следующие связи:
    1. один-ко-многим (сеансы фильма, комментарии);
    2. многие-к-одному (группа фильмов);
    3. многие-ко-многим (теги);
    4. один-к-одному (инфо о фильме).

    Дополнительные особенности:
    1. Не опубликованные комментарии доступны только авторам.
    2. Администраторам и модераторам доступны все комментарии.
    3. Комментарий созданный пользователем входящим в группу администраторов или модераторов является опубликованным автоматически.

    Пользователи и группы пользователей


    Группы пользователей:
    1. Администраторы. Имеют полный доступ ко всем записям в системе администрировния.
    2. Модераторы. Имеют ограниченный доступ в системе администрирования (могут только редактировать и удалять комментарии).
    3. Пользователи. Не имеют доступа к системе администрирования. Через апи приложения могут добавлять новые комментарии, редактировать и удалять только собственные комментарии.

    Предустановленный набор пользователей:
    1. Андминистратор. Группа: администраторы. Логин: admin, пароль: qwerty
    2. Модератор. Группа: модераторы. Логин: login: moderator, пароль: qwerty
    3. Пользователь 1. Группа: пользователи. Логин: login: user-1, пароль: qwerty
    4. Пользователь 2. Группа: пользователи. Логин: login: user-2, пароль: qwerty

    Сценарии


    Сценарий 1.

    Пользователь не авторизован. Ему доступны все фильмы и комментарии, подтвержденные модераторами. Пользователь не может создавать комментарии, у него нет доступа к системе администрирования.

    Сценарий 2.

    Пользователь авторизован. Принадлежит к группе «Пользователь». Ему доступны так же комментарии подтвержденные модераторами и собственные комментарии. Пользователь может создавать комментарии и прикреплять изображения к комментариям, может редактировать и удалять свои комментарии. Система администрирования не доступна.

    Сценарий 3.

    Пользователь авторизован. Принадлежит к группе «Модератор». Ему доступны все фильмы и комментарии. Модератор может создавать комментарии и прикреплять изображения к комментариям, может редактировать и удалять любые комментарии. Доступны ограниченные возможности системы администрирования — доступ к комментариям.

    Сценарий 4.

    Пользователь авторизован. Принадлежит к группе «Администратор». Ему доступны все фильмы и комментарии. Может создавать комментарии и прикреплять изображения к комментариям, может редактировать и удалять любые комментарии. Доступны полные возможности системы администрирования.

    Система администрирования


    Система администрирования доступна админам и модераторам.

    URL: /admin

    Примеры запросов к Api


    URL к документации апи: /api/doc

    Специальные параметры


    _scope

    С помощью этого параметра можно определить скопу отличный от дефолтного. Например, получим сокращенный список фильмов состоящий только из id и name.

    GET /api/movies?_scope=list_short
    

    _oprs

    Параметр "_oprs" позволяет определить оператор для переданных в фильтр значений. Т.е. если нет возможности передать оператор первым символом в параметре к фильтру, это можно сделать с помощью параметра "_oprs". Например, это полезно для массивов когда, нужно передать «NOT IN»:

    GET /api/articles?_oprs[type]=<>&type=photo_report
    

    _sort

    С помощью этого параметра можно определить порядок сортировки. Например, получаем все статьи сортированные по имени (от последней до первой буквы) и ID (от меньшего к большему):

    GET /api/articles?_sort[name]=DESC&_sort[id]=ASC
    

    _offset

    С помощью параметра "_offset" можно определить позицию для списка. Например, для того что бы получить все записи начиная с 11-й, передадим "_offset=10":

    GET /api/articles?_offset=10
    

    _limit

    Этот параметр определяет лимит для списка. Например, получаем первые 10 статей:

    GET /api/articles?_limit=10
    

    Примеры фильтров


    Строковые фильтры

    Для строковых фильтров по умолчанию поиск осуществляется по вхождению подстроки.

    GET /api/articles?name=Dolorem
    

    Если нужно обратное, т.е. найти все записи, в которых нет вхождения данной подстроки, то необходимо передать "!" перед значением:

    GET /api/articles?name=!Dolorem
    

    Если нужно полное сравнение, то необходимо поставить знак "=" перед значением:

    GET /api/articles?name==Dolorem+eaque+libero+maxime.
    

    Не равно, определяется следующим образом (символы "<>" перед значением):

    GET /api/articles?name=<>Dolorem+eaque+libero+maxime.
    

    Enum типы

    Enum типы ищуются по полному сравнению (=):

    Для того что бы найти статьи с типом news необходимо передать news:

    GET /api/articles?type=news
    

    Такой запрос вернет пустой результат:

    GET /api/articles?type=new
    

    Массивы

    Для того, что бы отфильтровать по массиву значений (IN), необходиом передать массив следующим образом [«name1»,«name2»,«name3'...]. Например, для того, чтобы получить статьи с типом photo_report и news:

    /api/articles?type=["photo_report","news"]
    

    Получить статьи все кроме new и photo_report:

    /api/articles?_oprs[type]=<>&type=["photo_report",+"news"]
    

    Даты

    Операторы больше/меньше доступны как для числовых фильтров, так и для фильров дат. Например, получить статьи позже 2016-06-07:

    /api/articles?publishAt=>2016-06-07
    

    Получить статьи раньше 2016-06-07:

    /api/articles?publishAt=<2016-06-07
    

    Диапазон дат, можно получить следующим образом:

    GET /api/articles?publishAt=[">2016-03-06","<2016-03-13"]
    


    Заключение


    Для желающих ознакомиться с тем как это реализовано в коде — ссылка на гитхаб.
    • +7
    • 10.1k
    • 6
    Share post

    Comments 6

      –1
      Клёво, за исключением одного НО, в описываемом «демо» API мало общего имеет с REST и больше тянет на RPC (параметры должны идти не GET а в HEAD) так как URL является идентификатором ресурса, а в заголовках описывается/запрашивается содержимое этого ресурса:

      GET: /api/articles
      type=[«photo_report»,«news»]
        0
        Не согласен. Олег, идентификатор ресурса в REST это путь ("/api/articles"), а то что после "?" называются параметрами.
          0
          не рекламы для, пруф: http://anton.shevchuk.name/php/create-restful-api/, да и в принципе всегда так делали…
            0
            сам себе отвечу, и так и так тоже делают…
          0
          параметры должны идти не GET а в HEAD


          чта? Вы что-то путаете (http verb HEAD и заголовки)
          так как URL является идентификатором ресурса


          URI является идентификатором ресурса (последняя буква об этом так и кричит), URL это… частный случай URI. По поводу фильтрации все намного проще:
          GET /articles - коллекция ресурсов представляющих статьи
          GET /articles?tags=photos,news - выборка из коллекции ресурсов, представляет собой новую коллекцию со своим URI.


          А что до пруфов — перечитайте еще раз. Вы видимо увидили пример с заголовком Range и подумали что это ко всему относится. А чуть дальше пример с выборкой отдельных полей — что собственно ломает ваше утверждение.
            0
            Да, всё верно, ниже в той ветке на которую вы ответили, я отписался что ошибался…

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