Всем привет! В прошлой статье я рассказал о нашем опыте в REST API со сборкой на FOSRestBundle + JMSSerializer. Сегодня я поделюсь нашим подходом к разработке REST API на FOSRestBundle + GlavwebDatagridBundle.
С ростом количества REST проектов возрастали требования к нашему апи. Часто пользователями нашего апи являются сторонние разработчики, которые имеют те или иные потребности для своих решений. Так, например, не всем было по душе передавать конфигурацию вложенности объектов в запросах. Возможностей фильтрации из коробки так же не хватало.
Но самым весомым основанием к пересмотру подхода к разработке апи были проблемы с производительностью. Причинами этих проблем были либо не оптимально написанный SQL, либо дополнительные затраты на обработку результата доктриной и сериализером.
Если с SQL понятно в какую сторону оптимизировать, то с потерей производительности в доктрине и сериализере сложнее. Вот, как это выглядит после того, как запрос к БД отработал:
Было решено сократить этот путь до:
Так как мы отказались от гидрации в объекты, пришлось отказаться и от JMSSerializer тоже. JMSSerializer делал много полезного для нашего апи (это я описал в предыдущей статье). Кроме того он выполнял еще одну важную работу — догружал вложенные сущности, если они не были определены в join-ах.
Для того, чтобы закрыть образовавшуюся «дыру» в функциональности, возникшую в результате отказа от jmsserializer, был разработан GlavwebDatagridBundle.
Коротко определить предназначение GlavwebDatagridBundle можно так: получать данные, отформатированные нужным образом, на основе фильтров, лимита и оффсета. Не понятно? Знаю. А теперь обо всем по порядку.
В основе GlavwebDatagridBundle лежат следующие компоненты:
DatagridBuilder формирует QueryBuilder используя DataSchema, фильтры и дополнительные параметры запроса:
Далее возвращает объект Datagrid:
Фильтры позволяют задавать дополнительные условия в запрос.
Datagrid получает QueryBuilder из DatagridBuilder и позволяет вернуть данные как в виде списка:
так и в виде одной строки:
DataSchema определяет набор данных в формате yaml:
Определяет поля и связи:
Дополнительные условия:
Так же возможно определять дополнительные условия и для связей:
DataSchema так же реализует функцию догрузки вложенных сущностей, если они не были сконфигурированы как join:
DataSchema используется для построения запроса в DatagridBuilder и для преобразования данных в Datagrid.
Scope позволяет сузить набор данных, определенных в DataSchema.
Например, с помощью scope можно определить небольшой объем данных для списка записей (article\list.yml):
и полный набор данных для просмотра одной записи (article\view.yml):
Предлагаю вашему вниманию вымышленное апи кинотеатра, созданное специально для демонстрации. Сущности и поля сущностей подобраны таким образом, чтобы максимально раскрыть возможности апи.
Демо проект размещен на гитхабе.
Для того, что бы развернуть проект локально нужно сделать 3 простых действия:
1. Создать проект через композер.
2. Выполнить миграции.
3. Выполнить фикстуры.
В наличии следующие сущности:
Между ними имеем следующие связи:
Дополнительные особенности:
Группы пользователей:
Предустановленный набор пользователей:
Сценарий 1.
Пользователь не авторизован. Ему доступны все фильмы и комментарии, подтвержденные модераторами. Пользователь не может создавать комментарии, у него нет доступа к системе администрирования.
Сценарий 2.
Пользователь авторизован. Принадлежит к группе «Пользователь». Ему доступны так же комментарии подтвержденные модераторами и собственные комментарии. Пользователь может создавать комментарии и прикреплять изображения к комментариям, может редактировать и удалять свои комментарии. Система администрирования не доступна.
Сценарий 3.
Пользователь авторизован. Принадлежит к группе «Модератор». Ему доступны все фильмы и комментарии. Модератор может создавать комментарии и прикреплять изображения к комментариям, может редактировать и удалять любые комментарии. Доступны ограниченные возможности системы администрирования — доступ к комментариям.
Сценарий 4.
Пользователь авторизован. Принадлежит к группе «Администратор». Ему доступны все фильмы и комментарии. Может создавать комментарии и прикреплять изображения к комментариям, может редактировать и удалять любые комментарии. Доступны полные возможности системы администрирования.
Система администрирования доступна админам и модераторам.
URL: /admin
URL к документации апи: /api/doc
_scope
С помощью этого параметра можно определить скопу отличный от дефолтного. Например, получим сокращенный список фильмов состоящий только из id и name.
_oprs
Параметр "_oprs" позволяет определить оператор для переданных в фильтр значений. Т.е. если нет возможности передать оператор первым символом в параметре к фильтру, это можно сделать с помощью параметра "_oprs". Например, это полезно для массивов когда, нужно передать «NOT IN»:
_sort
С помощью этого параметра можно определить порядок сортировки. Например, получаем все статьи сортированные по имени (от последней до первой буквы) и ID (от меньшего к большему):
_offset
С помощью параметра "_offset" можно определить позицию для списка. Например, для того что бы получить все записи начиная с 11-й, передадим "_offset=10":
_limit
Этот параметр определяет лимит для списка. Например, получаем первые 10 статей:
Строковые фильтры
Для строковых фильтров по умолчанию поиск осуществляется по вхождению подстроки.
Если нужно обратное, т.е. найти все записи, в которых нет вхождения данной подстроки, то необходимо передать "!" перед значением:
Если нужно полное сравнение, то необходимо поставить знак "=" перед значением:
Не равно, определяется следующим образом (символы "<>" перед значением):
Enum типы
Enum типы ищуются по полному сравнению (=):
Для того что бы найти статьи с типом news необходимо передать news:
Такой запрос вернет пустой результат:
Массивы
Для того, что бы отфильтровать по массиву значений (IN), необходиом передать массив следующим образом [«name1»,«name2»,«name3'...]. Например, для того, чтобы получить статьи с типом photo_report и news:
Получить статьи все кроме new и photo_report:
Даты
Операторы больше/меньше доступны как для числовых фильтров, так и для фильров дат. Например, получить статьи позже 2016-06-07:
Получить статьи раньше 2016-06-07:
Диапазон дат, можно получить следующим образом:
Для желающих ознакомиться с тем как это реализовано в коде — ссылка на гитхаб.
Новые задачи.
С ростом количества 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. Тэги.
Между ними имеем следующие связи:
- один-ко-многим (сеансы фильма, комментарии);
- многие-к-одному (группа фильмов);
- многие-ко-многим (теги);
- один-к-одному (инфо о фильме).
Дополнительные особенности:
- Не опубликованные комментарии доступны только авторам.
- Администраторам и модераторам доступны все комментарии.
- Комментарий созданный пользователем входящим в группу администраторов или модераторов является опубликованным автоматически.
Пользователи и группы пользователей
Группы пользователей:
- Администраторы. Имеют полный доступ ко всем записям в системе администрировния.
- Модераторы. Имеют ограниченный доступ в системе администрирования (могут только редактировать и удалять комментарии).
- Пользователи. Не имеют доступа к системе администрирования. Через апи приложения могут добавлять новые комментарии, редактировать и удалять только собственные комментарии.
Предустановленный набор пользователей:
- Андминистратор. Группа: администраторы. Логин: admin, пароль: qwerty
- Модератор. Группа: модераторы. Логин: login: moderator, пароль: qwerty
- Пользователь 1. Группа: пользователи. Логин: login: user-1, пароль: qwerty
- Пользователь 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"]
Заключение
Для желающих ознакомиться с тем как это реализовано в коде — ссылка на гитхаб.
Поделиться с друзьями
zcasper
Клёво, за исключением одного НО, в описываемом «демо» API мало общего имеет с REST и больше тянет на RPC (параметры должны идти не GET а в HEAD) так как URL является идентификатором ресурса, а в заголовках описывается/запрашивается содержимое этого ресурса:
GET: /api/articles
type=[«photo_report»,«news»]
Nilov_A
Не согласен. Олег, идентификатор ресурса в REST это путь ("/api/articles"), а то что после "?" называются параметрами.
zcasper
не рекламы для, пруф: http://anton.shevchuk.name/php/create-restful-api/, да и в принципе всегда так делали…
zcasper
сам себе отвечу, и так и так тоже делают…
Fesor
чта? Вы что-то путаете (http verb HEAD и заголовки)
URI является идентификатором ресурса (последняя буква об этом так и кричит), URL это… частный случай URI. По поводу фильтрации все намного проще:
А что до пруфов — перечитайте еще раз. Вы видимо увидили пример с заголовком
Range
и подумали что это ко всему относится. А чуть дальше пример с выборкой отдельных полей — что собственно ломает ваше утверждение.zcasper
Да, всё верно, ниже в той ветке на которую вы ответили, я отписался что ошибался…