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

Давайте перейдем к примерам.
Предположим у нас есть следующая сущность описанная в OAS:
Если мы запустим команду
то получим следующие сгенеренные объекты:
1) Контроллер сущности
Он уже умеет GET/POST/PATCH/DELETE, за которыми будет ходить в таблицу через модель, миграция для которой так же будет сген��рена. DefaultController всегда доступен разработчику, чтобы была возможность внедрить функционал для всех контроллеров.
2) Модель сущности Article
Как видите здесь появились комментарии // >>>props>>> и // >>>methods>>> — они нужны для того, чтобы разделять code-space от user code-space. Есть еще отношения tag/topic — belognsToMany/belongsTo соответственно, которые будут связывать сущность Article с тэгами/топиками, предоставляя возможность получить к ним доступ в relations json-api одним запросом GET или изменять их обновляя статью.
3) Миграция сущности, с поддержкой rollback (рефлексия/атомарность):
Генератор миграций поддерживает все типы индексов (композитные в том числе).
4) Роутер для прокидывания запросов:
Созданы роуты не только для базовых запросов, но и отношений (relations) с другими сущностями, которые будут вытягиваться 1 запросом и расширения в качестве bulk операций для возможности создавать/обновлять/удалять данные «пачками».
5) FormRequest для пред-обработки/валидации запросов:
Здесь все просто — сгенерены правила валидации свойств и отношения для связки основной сущности с сущностями в relations методе.
Наконец самое приятное — примеры запросов:
Выведи статью, подтяни в relations все ее тэги, пагинация на страницу 2, с лимитом 10 и отсортируй по-возрастранию.
Если нам не нужно выводить все поля статьи:
Соритировка по нескольким полям:
Фильтрация (или то, что попадает в условие WHERE):
Пример создания сущности (в данном случае статьи):
Ответ:
Видите ссылку в links->self? вы сразу же можете
Вернул список из объектов, в каждом из котрых тип этого объекта, его id, весь набор атрибутов, далее ссылка на себя, relationships запрошенные в url через include=tag по спецификации нет ограничений на включения отношений, то есть можно так например include=tag,topic,city и все они войдут в блок relationships, а их объекты будут храниться в included.
Если мы хотим получить 1 статью и все ее отношения/связи:
А вот и пример добавления отношений к уже существующей сущности — запрос:
Ответ:
Консольному генератору можно передать доп опции:
Таким образом, вы сообщаете генератору — создай код с миграциями (это вы уже видели) и перегенери код, смерджив его с последними изменениями из сохраненной истории, не затрагивая кастомных участков кода, а только те, что были сгенерены автоматом (то есть как раз те, которые выделены спец-блоками комментариями в коде). Есть возможность указывать шаги назад, напрмер: --merge=9 (откати генерацию на 9 шагов назад), даты генерации кода в прошлом --merge=«2017-07-29 11:35:32».
Один из пользователей библиотеки предложил генерить функциональные тесты для запросов — добавив опцию --tests вы можете нагенерить тесты, чтобы быть уверенными в том, что ваш API работает без ошибок.
Дополнительно можно воспользоваться множеством опций (все они гибко настраиваются через конфигуратор, который лежит в генерируемом модуле — пример: /Modules/V2/Config/config.php):
Естественно все конфигурации можно точечно вкл/выкл при необходимости. Более подробную информацию о дополнительных возможностях генератора кода можете посмотреть по ссылкам ниже. Контрибьюты всегда приветствуются.
Благодарю за внимание, творческих успехов.
Ресурсы статьи:
Изучил типизацию и тонкости спецификации OpenAPI, мне он понравился линейностью и возможностью описывать структуру и типы любой сущности на 1-3 уровня глубиной. Так как на тот момент уже был знаком с Laravel (до него юзал Yii2, CI но они были менее популярны), а так же с json-api форматом вывода данных — вся архитектура улеглась в голове связным графом.

Давайте перейдем к примерам.
Предположим у нас есть следующая сущность описанная в OAS:
ArticleAttributes:
description: Article attributes description
type: object
properties:
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 api:generate oas/openapi.yaml --migrations
то получим следующие сгенеренные объекты:
1) Контроллер сущности
<?php
namespace Modules\V1\Http\Controllers;
class ArticleController extends DefaultController
{
}
Он уже умеет GET/POST/PATCH/DELETE, за которыми будет ходить в таблицу через модель, миграция для которой так же будет сген��рена. DefaultController всегда доступен разработчику, чтобы была возможность внедрить функционал для всех контроллеров.
2) Модель сущности Article
<?php
namespace Modules\V2\Entities;
use Illuminate\Database\Eloquent\SoftDeletes;
use rjapi\extension\BaseModel;
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 Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
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\\V2\\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/{relations}', 'ArticleController@relations');
Route::post('/article/{id}/relationships/{relations}', 'ArticleController@createRelations');
Route::patch('/article/{id}/relationships/{relations}', 'ArticleController@updateRelations');
Route::delete('/article/{id}/relationships/{relations}', 'ArticleController@deleteRelations');
});
// <<<routes<<<
Созданы роуты не только для базовых запросов, но и отношений (relations) с другими сущностями, которые будут вытягиваться 1 запросом и расширения в качестве bulk операций для возможности создавать/обновлять/удалять данные «пачками».
5) FormRequest для пред-обработки/валидации запросов:
<?php
namespace Modules\V1\Http\Requests;
use rjapi\extension\BaseFormRequest;
class ArticleFormRequest 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 api:generate oas/openapi.yaml --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',
],
'trees'=> [ // сущности выводимые в виде деревьев
'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,
],
],
];
Естественно все конфигурации можно точечно вкл/выкл при необходимости. Более подробную информацию о дополнительных возможностях генератора кода можете посмотреть по ссылкам ниже. Контрибьюты всегда приветствуются.
Благодарю за внимание, творческих успехов.
Ресурсы статьи:
