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 = $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
властивості:
id: # integer
type: # ArticleType
name: # string
body: # text
publish: # boolean
publishAt: # datetime

Визначає поля і зв'язку:

schema:
class: AppBundle\Entity\Movie
db_driver: orm
властивості:
...

tags: # AppBundle\Entity\Tag
join: left
властивості:
id: # integer

Додаткові умови:

schema:
class: AppBundle\Entity\Article
db_driver: orm
conditions: ["{{alias}}.publish = true"]
властивості:
....

Так само можливо визначати додаткові умови для зв'язків:

schema:
class: AppBundle\Entity\Movie
db_driver: orm
властивості:
...

articles: # AppBundle\Entity\Article
conditions: ["{{alias}}.publish = true"]
join: none
властивості:
....

DataSchema так само реалізує функцію довантаження вкладених сутностей, якщо вони не були налаштовані як join:

schema:
class: AppBundle\Entity\Movie
db_driver: orm
властивості:
...

articles: # AppBundle\Entity\Article
join: none
властивості:
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 Користувач. Група: користувачі. Логін: пароль: user-1, пароль: qwerty
  4. Користувач 2. Група: користувачі. Логін: пароль: 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"]


Висновок
Для бажаючих ознайомитися з тим як це реалізовано в коді — посилання на гитхаб.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.