Codegenerator für Laravel - für OAS-Eingabe, für JSON-API-Ausgabe

Die Möglichkeit, einen Codegenerator für die API zu erstellen, um die zukünftige Generation vor der Notwendigkeit zu bewahren, ständig dieselben Controller, Modelle, Router, Middleware, Migrationen, Skelette, Filter, Validierungen usw. zu erstellen. manuell (auch im Kontext vertrauter und praktischer Frameworks) schien es mir interessant.

Ich habe die Typisierung und Feinheiten der OpenAPI-Spezifikation untersucht. Ich mochte sie durch ihre Linearität und die Fähigkeit, die Struktur und die Typen von Entitäten auf 1 bis 3 Ebenen zu beschreiben. Da ich zu dieser Zeit bereits mit Laravel (Yii2 verwendete es früher, CI, aber sie waren weniger beliebt) sowie mit dem json-api-Datenausgabeformat vertraut war, ging mir die gesamte Architektur mit einem verbundenen Diagramm in den Kopf.



Kommen wir zu den Beispielen.

Angenommen, wir haben die folgende in OAS beschriebene Entität:

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 

Wenn wir den Befehl ausführen

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

dann erhalten wir die folgenden generierten Objekte:

1) Entity Controller

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

Er kennt bereits GET / POST / PATCH / DELETE, für das er über das Modell an die Tabelle geht, für das auch die Migration generiert wird. DefaultController steht dem Entwickler immer zur Verfügung, so dass Funktionen für alle Controller implementiert werden können.

2) Artikelentitätsmodell

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


Wie Sie sehen können, wurden hier Kommentare angezeigt // >>> Requisiten >>> und // >>> Methoden >>> - sie werden benötigt, um den Code-Raum vom Benutzer-Code-Raum zu trennen. Es gibt auch eine Tag / Topic-Beziehung - belognsToMany / ZugehörigkeitTo -, die die Artikelentität mit Tags / Themes verbindet und die Möglichkeit bietet, mit einer einzigen GET-Anforderung auf sie in den Beziehungen json-api zuzugreifen oder sie durch Aktualisieren des Artikels zu ändern.

3) Entitätsmigration mit Rollback-Unterstützung (Reflexion / Atomizität):

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

Der Migrationsgenerator unterstützt alle Arten von Indizes (einschließlich zusammengesetzter Indizes).

4) Router für Bildlaufanforderungen:

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

Routen wurden nicht nur für grundlegende Abfragen erstellt, sondern auch für Beziehungen zu anderen Entitäten, die von 1 Abfrage und Erweiterungen als Massenoperationen abgerufen werden, um Daten in Stapeln erstellen / aktualisieren / löschen zu können.

5) FormRequest zur Vorverarbeitung / Validierung von Anfragen:

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

Hier ist alles einfach - die Regeln zum Validieren von Eigenschaften und Beziehungen werden generiert, um die Hauptentität mit Entitäten in der Beziehungsmethode zu verknüpfen.

Der beste Teil sind schließlich die Abfragebeispiele:

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

Zeigen Sie einen Artikel an, ziehen Sie alle Tags in Relationen und Paginierung auf Seite 2 mit einem Limit von 10 fest und sortieren Sie sie nach Alter.

Wenn wir nicht alle Felder des Artikels anzeigen müssen:

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

Nach mehreren Feldern sortieren:

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

Filtern (oder was auch immer in die WHERE-Klausel fällt):

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

Ein Beispiel für die Erstellung einer Entität (in diesem Fall Artikel):

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

Die Antwort lautet:

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

Siehe den Link in links-> self? Sie können sofort
 GET http://laravel.loc/article/1 
oder speichern Sie es zum späteren Nachschlagen.

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

Ich habe eine Liste von Objekten zurückgegeben, in denen der Typ dieses Objekts, seine ID, der gesamte Satz von Attributen, dann ein Link zu mir selbst, Beziehungen, die in der URL über include = tag angefordert werden, angegeben sind. Es gibt keine Einschränkungen für das Einbeziehen von Beziehungen, d. H. Include = tag, Thema, Stadt und alle von ihnen werden in den Beziehungsblock aufgenommen , und ihre Objekte werden in eingeschlossen gespeichert.

Wenn wir 1 Artikel und alle seine Beziehungen / Beziehungen erhalten möchten:

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


Und hier ist ein Beispiel für das Hinzufügen von Beziehungen zu einer vorhandenen Entität - eine Anforderung:

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

Die Antwort lautet:

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

Sie können zusätzliche Optionen auf den Konsolengenerator übertragen:

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

Auf diese Weise teilen Sie dem Generator mit, dass Sie einen Code mit Migrationen erstellen (Sie haben ihn bereits gesehen), den Code neu generieren und ihn mit den neuesten Änderungen aus dem gespeicherten Verlauf speichern, ohne die benutzerdefinierten Abschnitte des Codes zu beeinflussen, sondern nur diejenigen, die automatisch generiert wurden (dh nur diese) , die durch spezielle Blöcke mit Kommentaren im Code hervorgehoben werden). Es ist möglich, Schritte zurück anzugeben, zum Beispiel: --merge = 9 ( Zurücksetzen der Generation 9 Schritte zurück), die Codegenerierungsdaten in der Vergangenheit --merge = "2017-07-29 11:35:32" .

Einer der Benutzer der Bibliothek schlug vor, Funktionstests für Abfragen zu generieren. Durch Hinzufügen der Option --tests können Sie Tests ausführen, um sicherzustellen, dass Ihre API fehlerfrei funktioniert.

Darüber hinaus können Sie viele Optionen verwenden (alle werden flexibel über den Konfigurator konfiguriert, der im generierten Modul enthalten ist - Beispiel: /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, ], ], ]; 

Natürlich können alle Konfigurationen bei Bedarf ein- und ausgeschaltet werden. Weitere Informationen zu zusätzlichen Funktionen des Codegenerators finden Sie unter den folgenden Links. Beiträge sind immer willkommen.

Vielen Dank für Ihre Aufmerksamkeit, kreativen Erfolg.

Artikel Ressourcen:

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


All Articles