Розробка

Генератор коду для Laravel — на введення RAML, на висновок JSON-API

Можливість створити генератор коду для API, щоб позбавити майбутнє покоління від необхідності постійно створювати одні і ті ж контролери, моделі, роутери, мидлвары, міграції, скелетоны, фільтри, валідації і т. д. вручну (нехай навіть в контексті всіх звичних і зручних фреймворків), здалася мені цікавою.

Вивчив типізацію і тонкощі специфікації raml, мені він сподобався лінійністю і можливістю описувати структуру і типи будь сутності на 1-3 рівня глибиною. Так як на той момент вже був знайомий з Laravel (до нього юзал Yii2, CI але вони були менш популярні), а так само з json-api форматом виводу даних — вся архітектура вляглася в голові зв’язним графом.

Давайте перейдемо до прикладів.

Припустимо, у нас є наступна сутність описана в raml:

ArticleAttributes:
 description: Article attributes description
 type: object
властивості:
title:
 required: true
 type: string
 minLength: 16
 maxLength: 256
facets:
index:
 idx_title: index
description:
 required: true
 type: string
 minLength: 32
 maxLength: 1024
facets:
 spell_check: true
 spell_language: en
url:
 required: false
 type: string
 minLength: 16
 maxLength: 255
facets:
index:
 idx_url: unique
show_in_top:
 description: Show at the top of main page
 required: false
 type: boolean
status:
 description: The state of an article
 enum: ["draft", "published", "postponed", "archived"]
facets:
state_machine:
 initial: ['draft']
 draft: ['published']
 published: ['archived', 'postponed']
 postponed: ['published', 'archived']
 archived: []
topic_id:
 description: ManyToOne Topic relationship
 required: true
 type: integer
 minimum: 1
 maximum: 6
facets:
index:
 idx_fk_topic_id: foreign
 references: id
 on: topic
 onDelete: cascade
 onUpdate: cascade
rate:
 type: number
 minimum: 3
 maximum: 9
 format: double
date_posted:
 type: date-only
time_to_live:
 type: time-only
deleted_at:
 type: datetime

Якщо ми запустимо команду

php artisan raml:generate raml/articles.raml --migrations 

то отримаємо наступні сгенеренние об’єкти:

1) Контролер сутності

<?php
namespace ModulesV1HttpControllers;

class ArticleController extends DefaultController 
{
}

Він вже вміє GET/POST/PATCH/DELETE, за якими буде ходити в таблицю через модель, міграція для якої так само буде сгенерена. DefaultController завжди доступний розробнику, щоб була можливість впровадити функціонал для всіх контролерів.

2) Модель сутності Article

<?php
namespace ModulesV2Entities;

use IlluminateDatabaseEloquentSoftDeletes;
use rjapiextensionBaseModel;

class Article extends BaseModel 
{
 use SoftDeletes;

 // >>>props>>>
 protected $dates = ['deleted_at'];
 protected $primaryKey = 'id';
 protected $table = 'article';
 public $timestamps = false;
 public $incrementing = false;
 // <<<props<<<
 // >>>methods>>>

 public function tag() 
{
 return $this->belongsToMany(Tag::class, 'tag_article');
}
 public function topic() 
{
 return $this->belongsTo(Topic::class);
}
 // <<<methods<<<
}

Як бачите тут з’явилися коментарі // >>>props>>> та // >>>methods>>> — вони потрібні для того, щоб розділяти code-space від user code-space. Є ще відносини tag/topic — belognsToMany/belongsTo відповідно, які будуть зв’язувати сутність Article з тегами/топіки, надаючи можливість отримати до них доступ в relations json-api одним запитом GET або змінювати їх оновлюючи статтю.

3) Міграція суті, з підтримкою rollback (рефлексія/атомарність):

<?php
use IlluminateSupportFacadesSchema;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseMigrationsMigration;

class CreateArticleTable extends Migration 
{
 public function up() 
{
 Schema::create('article', function(Blueprint $table) {
$table->bigIncrements('id');
 $table->string('title', 256);
 $table->index('title', 'idx_title');
 $table->string('description', 1024);
 $table->string('url', 255);
 $table->unique('url', 'idx_url');
 // Show at the top of main page
$table->unsignedTinyInteger('show_in_top');
 $table->enum('status', ["draft","published","postponed","archived"]);
 // ManyToOne Topic relationship
$table->unsignedInteger('topic_id');
 $table->foreign('topic_id', 'idx_fk_topic_id')->references('id')->on('topic')->onDelete('cascade')->onUpdate('cascade');
$table->timestamps();
});
}

 public function down() 
{
Schema::dropIfExists('article');
}

}

Генератор міграцій підтримує всі типи індексів (композитні в тому числі).

4) Роутер для прокидывания запитів:

// >>>routes>>>
// Article routes
Route::group(['prefix' => 'v2', 'namespace' => 'Modules\2\Http\Controllers'], function()
{
 // bulk routes
 Route::post('/article/bulk', 'ArticleController@createBulk');
 Route::patch('/article/bulk', 'ArticleController@updateBulk');
 Route::delete('/article/bulk', 'ArticleController@deleteBulk');
 // basic routes
 Route::get('/article', 'ArticleController@index');
 Route::get('/article/{id}', 'ArticleController@view');
 Route::post('/article', 'ArticleController@create');
 Route::patch('/article/{id}', 'ArticleController@update');
 Route::delete('/article/{id}', 'ArticleController@delete');
 // relation routes
 Route::get('/article/{id}/relationships/{relation}', 'ArticleController@relations');
 Route::post('/article/{id}/relationships/{relation}', 'ArticleController@createRelations');
 Route::patch('/article/{id}/relationships/{relation}', 'ArticleController@updateRelations');
 Route::delete('/article/{id}/relationships/{relation}', 'ArticleController@deleteRelations');
});
// <<<routes<<<

Створені роуты не тільки для базових запитів, але і відносин (relations) з іншими сутностями, які будуть витягатися 1 запитом і розширення в якості bulk операцій для можливості створювати/обновляти/видаляти дані «пачками».

5) Middleware для перед-обробки/валідації запитів:

<?php
namespace ModulesV2HttpMiddleware;

use rjapiextensionBaseFormRequest;

class ArticleMiddleware extends BaseFormRequest 
{
 // >>>props>>>
 public $id = null;
 // Attributes
 public $title = null;
 public $description = null;
 public $url = null;
 public $show_in_top = null;
 public $status = null;
 public $topic_id = null;
 public $rate = null;
 public $date_posted = null;
 public $time_to_live = null;
 public $deleted_at = null;
 // <<<props<<<

 // >>>methods>>>
 public function authorize(): bool 
{
 return true;
}

 public function rules(): array 
{
 return [
 'title' => 'required|string|min:16|max:256|',
 'description' => 'required|string|min:32|max:1024|',
 'url' => 'string|min:16|max:255|',
 // Show at the top of main page
 'show_in_top' => 'boolean',
 // The state of an article
 'status' => 'in:draft,published,postponed,archived|',
 // ManyToOne Topic relationship
 'topic_id' => 'required|integer|min:1|max:6|',
 'rate' => '|min:3|max:9|',
 'date_posted' => ",
 'time_to_live' => ",
 'deleted_at' => ",
];
}

 public function relations(): array 
{
 return [
'tag',
'topic',
];
}
 // <<<methods<<<
}

Тут все просто — сгенерены правила валідації властивостей і відносини для зв’язки основної сутності з сутностями в relations методі.

Нарешті, найприємніше — приклади запитів:

http://example.com/v1/article?include=tag&page=2&limit=10&sort=asc

Виведи статтю, підтягни в relations всі її теги, пагинация на сторінку 2, з лімітом 10 і відсортуй по-возрастранию.

Якщо нам не потрібно виводити всі поля статті:

http://example.com/v1/article/1?include=tag&data=["title", "description"]

Соритировка по декільком полям:

http://example.com/v1/article/1?include=tag&order_by={"title":"asc", "created_at":"desc"}

Фільтрація (або те, що потрапляє в умова WHERE):

http://example.com/v1/article?include=tag&filter=[["updated_at", ">", "2018-01-03 12:13:13"], ["updated_at", "<", "2018-09-03 12:13:15"]]

Приклад створення сутності (в даному випадку статті):

POST http://laravel.loc/v1/article
{
 "data": {
"type":"article",
 "attributes": {
 "title":"Foo bar Foo bar Foo bar Foo bar",
 "description":"description description description description description",
 "fake_attr": "attr",
 "url":"title title bla bla bla",
"show_in_top":1
}
}
}

Відповідь:

{
 "data": {
 "type": "article",
 "id": "1",
 "attributes": {
 "title": "Foo bar Foo bar Foo bar Foo bar",
 "description": "description description description description description",
 "url": "title title bla bla bla",
 "show_in_top": 1
},
 "links": {
 "self": "laravel.loc/article/1"
}
}
}

Бачите посилання в links->self? ви відразу ж можете

GET http://laravel.loc/article/1

або зберегти її для використання в подальшому.

GET http://laravel.loc/v1/article?include=tag&filter=[["updated_at", ">", "2017-01-03 12:13:13"], ["updated_at", "<", "2019-01-03 12:13:15"]]
{
 "data": [
{
 "type": "article",
 "id": "1",
 "attributes": {
 "title": "Foo bar Foo bar Foo bar Foo bar 1",
 "description": "The quick brovn fox jumped ower the lazy dogg",
 "url": "http://example.com/articles_feed 1",
 "show_in_top": 0,
 "status": "draft",
 "topic_id": 1,
 "rate": 5,
 "date_posted": "2018-02-12",
 "time_to_live": "10:11:12"
},
 "links": {
 "self": "laravel.loc/article/1"
},
 "relationships": {
 "tag": {
 "links": {
 "self": "laravel.loc/article/1/relationships/tag",
 "related": "laravel.loc/article/1/tag"
},
 "data": [
{
 "type": "tag",
 "id": "1"
}
]
}
}
}
],
 "included": [
{
 "type": "tag",
 "id": "1",
 "attributes": {
 "title": "Tag 1"
},
 "links": {
 "self": "laravel.loc/tag/1"
}
}
]
}

Повернув список об’єктів, у кожному з котрих тип цього об’єкта, його id, весь набір атрибутів, далі посилання на себе, relationships запитані в url через include=tag по специфікації немає обмежень на включення відносин, тобто можна так наприклад include=tag,topic,city і всі вони увійдуть до блоку relationships, а їх об’єкти будуть зберігатися в included.

Якщо ми хочемо отримати 1 статтю і всі її стосунки/зв’язку:

GET http://laravel.loc/v1/article/1?include=tag&data=["title", "description"]
{
 "data": {
 "type": "article",
 "id": "1",
 "attributes": {
 "title": "Foo bar Foo bar Foo bar Foo bar 123456",
 "description": "description description description description description 123456",
},
 "links": {
 "self": "laravel.loc/article/1"
},
 "relationships": {
 "tag": {
 "links": {
 "self": "laravel.loc/article/1/relationships/tag",
 "related": "laravel.loc/article/1/tag"
},
 "data": [
{
 "type": "tag",
 "id": "3"
},
{
 "type": "tag",
 "id": "1"
},
{
 "type": "tag",
 "id": "2"
}
]
}
}
},
 "included": [
{
 "type": "tag",
 "id": "3",
 "attributes": {
 "title": "Tag 4"
},
 "links": {
 "self": "laravel.loc/tag/3"
}
},
{
 "type": "tag",
 "id": "1",
 "attributes": {
 "title": "Tag 2"
},
 "links": {
 "self": "laravel.loc/tag/1"
}
},
{
 "type": "tag",
 "id": "2",
 "attributes": {
 "title": "Tag 3"
},
 "links": {
 "self": "laravel.loc/tag/2"
}
}
]
}

А ось і приклад додавання відносин до вже існуючої сутності — запит:

PATCH http://laravel.loc/v1/article/1/relationships/tag
{
 "data": {
"type":"article",
"id":"1",
 "relationships": {
 "tag": {
 "data": [{ "type": "tag", "id": "2" },{ "type": "tag", "id": "3" }]
}
 } 
}
}

Відповідь:

{
 "data": {
 "type": "article",
 "id": "1",
 "attributes": {
 "title": "Foo bar Foo bar Foo bar Foo bar 1",
 "description": "The quick brovn fox jumped ower the lazy dogg",
 "url": "http://example.com/articles_feed 1",
 "show_in_top": 0,
 "status": "draft",
 "topic_id": 1,
 "rate": 5,
 "date_posted": "2018-02-12",
 "time_to_live": "10:11:12"
},
 "links": {
 "self": "laravel.loc/article/1"
},
 "relationships": {
 "tag": {
 "links": {
 "self": "laravel.loc/article/1/relationships/tag",
 "related": "laravel.loc/article/1/tag"
},
 "data": [
{
 "type": "tag",
 "id": "2"
},
{
 "type": "tag",
 "id": "3"
}
]
}
}
},
 "included": [
{
 "type": "tag",
 "id": "2",
 "attributes": {
 "title": "Tag 2"
},
 "links": {
 "self": "laravel.loc/tag/2"
}
},
{
 "type": "tag",
 "id": "3",
 "attributes": {
 "title": "Tag 3"
},
 "links": {
 "self": "laravel.loc/tag/3"
}
}
]
}

Консольному генератора можна передати доп опції:

php artisan raml:generate raml/articles.raml --migrations --regenerate --merge=last

Таким чином, ви повідомляєте генератора — створи код з міграціями (це ви вже бачили) і перегенери код, смерджив його з останніми змінами збереженої історії, не зачіпаючи кастомних ділянок коду, а тільки ті, що були сгенерены автоматом (тобто якраз ті, які виділені спец-блоками коментарями в коді). Є можливість вказувати кроки назад, наприклад: –merge=9 (откати генерацію на 9 кроків назад), дати генерації коду в минулому –merge=«2017-07-29 11:35:32».

Один з користувачів бібліотеки запропонував генерувати функціональні тести для запитів — додавши опцію –tests ви можете нагенерить тести, щоб бути впевненими в тому, що ваш API працює без помилок.

Додатково можна скористатися безліччю опцій (всі вони гнучко настроюються через конфігуратор, який лежить в генерованому модулі — приклад: /Modules/V2/Config/config.php):

<?php
return [
 'name' => 'V2',
 'query_params'=> [ // параметри запитів використовуються за умовчанням
 'limit' => 15,
 'sort' => 'desc',
 'access_token' => 'db7329d5a3f381875ea6ce7e28fe1ea536d0acaf',
],
 'дерева'=> [ // сутності виводяться у вигляді дерев 
 'menu' => true,
],
 'jwt'=> [ // jwt авторизація
 'enabled' => true,
 'table' => 'user',
 'activate' => 30,
 'expires' => 3600,
],
 'state_machine'=> [ // finite state machine
 'article'=> [
 'status'=> [
 'enabled' => true,
 'states'=> [
 'initial' => ['draft'],
 'draft' => ['published'],
 'published' => ['archived', 'postponed'],
 'postponed' => ['published', 'archived'],
 'archived' => ["],
],
],
],
],
 'spell_check'=> [ // обробка спелчекером орфографії тексту конкретних полів
 'article'=> [
 'description'=> [
 'enabled' => true,
 'language' => 'en',
],
],
],
 'bit_mask'=> [ // бітова маска для поля permissions (виводить true/false вкл/викл для кожного)
 'user'=> [
 'permissions'=> [
 'enabled' => true,
 'flags'=> [
 'publisher' => 1,
 'editor' => 2,
 'manager' => 4,
 'photo_reporter' => 8,
 'admin' => 16,
],
],
],
],
 'cache'=> [ // які сутності кешувати
 'tag'=> [
 'enabled' => false, // кеш для сутності tag відключений
 'stampede_xfetch' => true,
 'stampede_beta' => 1.1,
 'ttl' => 3600,
],
 'article'=> [
 'enabled' => true, // кеш для сутності article включений
 'stampede_xfetch' => true,
 'stampede_beta' => 1.5,
 'ttl' => 300,
],
],
];

Природно всі конфігурації можна точково вкл/викл при необхідності. Більш детальну інформацію про додаткові можливості генератора коду можете подивитися за посиланнями нижче. Контрибьюты завжди вітаються.

Дякую за увагу, творчих успіхів.

Ресурси статті:
генератор — github.com/RJAPI/raml-json-api
специфікація json-api — jsonapi.org/format
специфікація raml — github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md

Related Articles

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Close