Gerador de código para o Laravel - para entrada na OEA, para saída JSON-API

A capacidade de criar um gerador de código para a API para salvar a futura geração da necessidade de criar constantemente os mesmos controladores, modelos, roteadores, middleware, migrações, esqueletos, filtros, validações etc. manualmente (mesmo no contexto de estruturas familiares e convenientes), parecia-me interessante.

Estudei a tipificação e sutilezas da especificação OpenAPI, gostei por sua linearidade e capacidade de descrever a estrutura e os tipos de qualquer entidade em 1-3 níveis de profundidade. Como naquela época eu já estava familiarizado com o Laravel (o Yii2 costumava usá-lo, o CI, mas eles eram menos populares), bem como com o formato de saída de dados json-api - toda a arquitetura ficou na minha cabeça com um gráfico conectado.



Vamos seguir para os exemplos.

Suponha que tenhamos a seguinte entidade descrita na OEA:

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 

Se executarmos o comando

 php artisan api:generate oas/openapi.yaml --migrations 

então obtemos os seguintes objetos gerados:

1) Controlador de entidade

 <?php namespace Modules\V1\Http\Controllers; class ArticleController extends DefaultController { } 

Ele já conhece GET / POST / PATCH / DELETE, para o qual irá para a tabela através do modelo, cuja migração também será gerada. O Controlador Padrão está sempre disponível para o desenvolvedor, para que seja possível implementar a funcionalidade para todos os controladores.

2) Modelo de entidade do artigo

 <?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<<< } 


Como você pode ver, os comentários apareceram aqui // >>> props >>> e // >>> métodos >>> - eles são necessários para separar o espaço de código do espaço de código do usuário. Também existe um relacionamento tag / tópico - belognsToMany / belongsTo, respectivamente, que conectará a entidade Article com tags / topics, oferecendo a oportunidade de acessá-los nas relações json-api com uma única solicitação GET ou alterá-los atualizando o artigo.

3) Migração de entidade, com suporte à reversão (reflexão / atomicidade):

 <?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'); } } 

O gerador de migração suporta todos os tipos de índices (incluindo os compostos).

4) Roteador para solicitações de rolagem:

 // >>>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<<< 

As rotas foram criadas não apenas para consultas básicas, mas também para relações com outras entidades, que serão puxadas por 1 consulta e extensões como operações em massa para a capacidade de criar / atualizar / excluir dados em lotes.

5) FormRequest para pré-processamento / validação de solicitações:

 <?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<<< } 

Tudo é simples aqui - as regras para validar propriedades e relações são geradas para vincular a entidade principal às entidades no método de relações.

Por fim, a melhor parte são os exemplos de consulta:

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

Exiba um artigo, aperte todas as suas tags em relações, paginação na página 2, com um limite de 10, e classifique por idade.

Se não precisarmos exibir todos os campos do artigo:

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

Classifique por vários campos:

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

Filtragem (ou qualquer que seja a cláusula 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"]] 

Um exemplo de criação de uma entidade (neste caso, artigos):

 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 } } } 

A resposta é:

 { "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" } } } 

Veja o link em links-> self? você pode imediatamente
 GET http://laravel.loc/article/1 
ou salve-o para referência futura.

 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" } } ] } 

Eu retornei uma lista de objetos, em cada um dos quais o tipo desse objeto, seu ID, todo o conjunto de atributos e, em seguida, um link para mim mesmo, relacionamentos solicitados no URL via include = tag, por especificação, não há restrições para incluir relações, ou seja, por exemplo, include = tag, tópico, cidade e todos eles serão incluídos no bloco de relacionamentos e seus objetos serão armazenados em incluído .

Se queremos obter 1 artigo e todos os seus relacionamentos / relacionamentos:

 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" } } ] } 


E aqui está um exemplo de adição de relacionamentos a uma entidade existente - uma solicitação:

 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" }] } } } } 

A resposta é:

 { "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" } } ] } 

Você pode transferir opções adicionais para o gerador do console:

 php artisan api:generate oas/openapi.yaml --migrations --regenerate --merge=last 

Assim, você diz ao gerador - crie um código com migrações (você já o viu) e gere novamente o código, armazenando-o com as alterações mais recentes do histórico salvo, sem afetar as seções personalizadas do código, mas apenas as que foram geradas automaticamente (ou seja, apenas aquelas , destacados por blocos especiais com comentários no código). É possível especificar as etapas anteriores, por exemplo: --merge = 9 (reverter a geração 9 etapas anteriores), as datas de geração do código no passado --merge = "2017-07-29 11:35:32" .

Um dos usuários da biblioteca sugeriu a geração de testes funcionais para consultas - adicionando a opção --tests, você pode executar testes para garantir que sua API funcione sem erros.

Além disso, você pode usar muitas opções (todas elas são configuradas de forma flexível por meio do configurador, que fica no módulo gerado - por exemplo: /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, ], ], ]; 

Naturalmente, todas as configurações podem ser ativadas / desativadas, se necessário. Para obter mais informações sobre recursos adicionais do gerador de código, consulte os links abaixo. Contribuições são sempre bem-vindas.

Obrigado por sua atenção, sucesso criativo.

Recursos do artigo:

Source: https://habr.com/ru/post/pt427741/


All Articles