solidry / api-generator
基于OAS的PHP代码生成器(适用于Laravel框架),完全支持JSON-API数据格式
Requires
- php: >=7.1
- ext-json: *
- ext-pdo: *
- illuminate/container: >=5.8
- lcobucci/jwt: ^3.2
- league/fractal: ~0.14
- nwidart/laravel-modules: >=3.1
- predis/predis: ^1.1
- raml-org/raml-php-parser: ^4.1
Requires (Dev)
- codeception/codeception: >=2.4
- darkaonline/l5-swagger: >=5.8
- fzaninotto/faker: ^1.7
- laravel/framework: >=5.8
- mockery/mockery: ~1.0
- phpunit/phpunit: >=6.5
- dev-master
- 2.6.15
- 2.6.14
- 2.6.13
- 2.6.12
- 2.6.11
- 2.6.10
- 2.6.9
- 2.6.8
- 2.6.7
- 2.6.6
- 2.6.5
- 2.6.4
- 2.6.3
- 2.6.2
- 2.6.1
- 2.6.0
- 2.5.11
- 2.5.10
- 2.5.9
- 2.5.8
- 2.5.7
- 2.5.6
- 2.5.5
- 2.5.4
- 2.5.3
- 2.5.2
- 2.5.1
- 2.5.0
- 2.4.4
- 2.4.3
- 2.4.2
- 2.4.1
- 2.4.0
- 2.3.41
- 2.3.7
- 2.3.6
- 2.3.5
- 2.3.4
- 2.3.3
- 2.3.2
- 2.3.1
- 2.3.0
- 2.2.1
- 2.1.2
- 2.1.1
- 2.1.0
- 2.0.1
- 1.9.41
- 1.9.4
- 1.9.3
- 1.9.2
- 1.9.1
- 1.8.6
- 1.8.5
- 1.8.4
- 1.8.3
- 1.8.2
- 1.8.1
- 1.8.0
- 1.7.2
- 1.7.1
- 1.7.0
- 1.6.6
- 1.6.5
- 1.6.4
- 1.6.3
- 1.6.2
- 1.6.1
- 1.5.1
- 1.4.2
- 1.4.1
- 1.3.2
- 1.3.1
- 0.12.5
- 0.12.4
- 0.12.3
- 0.12.2
- 0.12.1
- 0.11.5
- 0.11.4
- 0.11.3
- 0.11.2
- 0.11.1
- 0.10.2
- 0.10.1
- 0.9.6
- 0.9.5
- 0.9.4
- 0.9.3
- 0.9.2
- 0.9.1
- 0.8.1
- 0.7.1
- 0.6.3
- 0.6.2
- 0.6.1
- 0.5.2
- 0.5.1
- 0.4.1
- 0.3.1
- 0.2.3
- 0.2.2
- 0.2.1
- 0.1.1
- dev-composer-issues
- dev-develop
This package is auto-updated.
Last update: 2024-09-08 22:56:25 UTC
README
基于OAS的PHP代码生成器(适用于Laravel框架),完全支持JSON-API数据格式
- 入门演示
- CRUD演示
- 关系链接演示
- 迁移演示
- API文档演示
- 安装
- Open API类型和声明
- Open API文档生成器
- 生成文件内容
- 关系
- 批量扩展
- 查询参数
- 安全性
- 缓存
- 软删除
- 树结构
- 有限状态机
- 拼写检查
- 位掩码
- 自定义SQL
- 自定义业务逻辑
- 重新生成
通过composer安装
首先 - 如果还没有创建Laravel项目,请创建Laravel项目
composer create-project --prefer-dist laravel/laravel your_app
然后在您的项目目录中运行
composer require solidry/api-generator
它将自动通过添加控制台命令api:generate
来注册Laravel ServiceProvider(您应该在运行php artisan
时看到它),并发布“laravel-modules”提供者。
自动加载
默认情况下,控制器、实体或存储库不会自动加载。您可以使用psr-4
来自动加载您的模块。例如
{ "autoload": { "psr-4": { "App\\": "app/", "Modules\\": "Modules/" } } }
通过运行来刷新更改
composer dump-autoload
可选设置
要提供与Json API兼容的错误处理程序,可以将ErrorHandler
特质添加到app/Exceptions/Handler
类中,并从标准的Laravel render
方法返回return $this->renderJsonApi($request, $exception);
,Handler
类的完整示例如下所示
<?php namespace App\Exceptions; use Exception; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Http\JsonResponse; use SoliDry\Exceptions\ErrorHandler; class Handler extends ExceptionHandler { use ErrorHandler; /** * Render an exception into an HTTP response. * * @param \Illuminate\Http\Request $request * @param \Exception $exception * @return JsonResponse */ public function render($request, Exception $exception): JsonResponse { return $this->renderJsonApi($request, $exception); // method you should call to return json-api response } }
如您所注意到的,它返回Illuminate\Http\JsonResponse
Laravel对象以适当地输出数据,例如。
{ "errors": [ { "code": 61, "message": "Connection refused [tcp://127.0.0.1:6379]", "file": "/vendor/predis/predis/src/Connection/AbstractConnection.php", "line": 155, "uri": "http://laravel.loc/api/v2/article", "meta": "#0 /vendor/predis/predis/src/Connection/StreamConnection.php(128): Predis\\Connection\\AbstractConnection->onConnectionError('Connection refu...', 61)\n#1 /vendor/predis/predis/src/Connection/StreamConnection.php(178): Predis\\Connection\\StreamConnection->createStreamSocket(Object(Predis\\Connection\\Parameters), 'tcp://127.0.0.1...', 4)\n#2 /vendor/predis/predis/src/Connection/StreamConnection.php(100): Predis\\Connection\\StreamConnection->tcpStreamInitializer(Object(Predis\\Connection\\Parameters))\n#3 /vendor/predis/predis/src/Connection/AbstractConnection.php(81): Predis\\Connection\\StreamConnection->createResource()\n#4 /vendor/predis/predis/src/Connection/StreamConnection.php(258): Predis\\Connection\\AbstractConnection->connect()\n#5 /vendor/predis/predis/src/Connection/AbstractConnection.php(180): Predis\\Connection\\StreamConnection->connect()\n#6 /vendor/predis/predis/src/Connection/StreamConnection.php(288): Predis\\Connection\\AbstractConnection->getResource()\n#7 /vendor/predis/predis/src/Connection/StreamConnection.php(394): Predis\\Connection\\StreamConnection->write('*2\\r\\n$3\\r\\nGET\\r\\n$4...')\n#8 /vendor/predis/predis/src/Connection/AbstractConnection.php(110): Predis\\Connection\\StreamConnection->writeRequest(Object(Predis\\Command\\StringGet))\n#9 /vendor/predis/predis/src/Client.php(331): Predis\\Connection\\AbstractConnection->executeCommand(Object(Predis\\Command\\StringGet))\n#10 /vendor/predis/predis/src/Client.php(314): Predis\\Client->executeCommand(Object(Predis\\Command\\StringGet))\n#11 /vendor/laravel/framework/src/Illuminate/Redis/Connections/Connection.php(114): Predis\\Client->__call('get', Array)\n#12 /vendor/laravel/framework/src/Illuminate/Redis/Connections/Connection.php(214): Illuminate\\Redis\\Connections\\Connection->command('get', Array)\n#13 /vendor/laravel/framework/src/Illuminate/Redis/RedisManager.php(195): Illuminate\\Redis\\Connections\\Connection->__call('get', Array)\n#14 /vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php(237): Illuminate\\Redis\\RedisManager->__call('get', Array)\n#15 /vendor/solidry/api-generator/src/Extension/CacheTrait.php(95): Illuminate\\Support\\Facades\\Facade::__callStatic('get', Array)\n#16 /vendor/solidry/api-generator/src/Extension/CacheTrait.php(60): SoliDry\\Extension\\ApiController->getXFetched(Object(Illuminate\\Http\\Request), Object(SoliDry\\Helpers\\SqlOptions))\n#17 /vendor/solidry/api-generator/src/Extension/ApiController.php(115): SoliDry\\Extension\\ApiController->getCached(Object(Illuminate\\Http\\Request), Object(SoliDry\\Helpers\\SqlOptions))\n#18 [internal function]: SoliDry\\Extension\\ApiController->index(Object(Illuminate\\Http\\Request))\n#19 /vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): call_user_func_array(Array, Array)\n#20 /vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(45): Illuminate\\Routing\\Controller->callAction('index', Array)\n#21 /vendor/laravel/framework/src/Illuminate/Routing/Route.php(219): Illuminate\\Routing\\ControllerDispatcher->dispatch(Object(Illuminate\\Routing\\Route), Object(Modules\\V2\\Http\\Controllers\\ArticleController), 'index')\n#22 /vendor/laravel/framework/src/Illuminate/Routing/Route.php(176): Illuminate\\Routing\\Route->runController()\n#23 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(680): Illuminate\\Routing\\Route->run()\n#24 /vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(30): Illuminate\\Routing\\Router->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))\n#25 /vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php(41): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))\n#26 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(163): Illuminate\\Routing\\Middleware\\SubstituteBindings->handle(Object(Illuminate\\Http\\Request), Object(Closure))\n#27 /vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))\n#28 /vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php(58): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))\n#29 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(163): Illuminate\\Routing\\Middleware\\ThrottleRequests->handle(Object(Illuminate\\Http\\Request), Object(Closure), 60, '1')\n#30 /vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))\n#31 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(104): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))\n#32 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(682): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))\n#33 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(657): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))\n#34 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(623): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))\n#35 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(612): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))\n#36 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(176): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))\n#37 /vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(30): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))\n#38 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(104): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))\n#39 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(151): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))\n#40 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(116): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))\n#41 /public/index.php(55): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))\n#42 {main}" } ] }
默认情况下,Laravel安装具有api
前缀的API路由。如果您想通过前缀访问生成的json api路由,例如/v2/article
或/myshop/basket
,您需要从您的RouteServiceProvider
中的mapApiRoutes()
方法中删除前缀。
<?php namespace App\Providers; use Illuminate\Support\Facades\Route; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; class RouteServiceProvider extends ServiceProvider { // ... protected function mapApiRoutes() { // Route::prefix('api') // you don't need prefixes then Route::middleware('api') ->namespace($this->namespace) ->group(base_path('routes/api.php')); } }
运行生成器
在控制台中运行
php artisan api:generate oas/openapi.yaml --migrations
此命令为您创建整个环境,以便您可以继续构建基于OAS/Laravel/JSON API的复杂API,特别是:模块化应用程序的目录、支持完整MVC的控制器/表单请求/模型+枢轴、路由(与JSON API兼容)甚至迁移以帮助您创建RDBMS结构。
oas/openapi.yaml
- 项目根目录中的oas目录中的文件,应在准备就绪之前准备就绪,或者您可能希望仅通过复制示例来尝试,例如mkdir oas && curl 'https://raw.githubusercontent.com/SoliDry/api-generator/master/tests/functional/oas/openapi.yaml' > /oas/openapi.yaml
,并可能根据您的需求进行重写。
如果需要或对您的环境更方便,您也可以使用.json
扩展/格式来产生相同的结果。
选项
--migrations
是一个选项,用于为每个实体(create_entityName_table)以及如果有多对多关系则为其生成迁移。
--regenerate
如果需要重写之前生成的所有文件,请使用此选项。默认情况下,生成的文件会被保留以防止覆盖新增或修改的内容。
输出将类似于以下内容
之后,你可以在项目中看到以下目录和文件模块结构
Docker 仓库
安装和尝试 api-generator(以沙箱模式)的另一种方法是使用 https://github.com/SoliDry/laravel-api。克隆仓库并运行
docker-compose up -d
你可以在那里看到 Dockerfile
和 docker-compose.yml
文件。
PS 欢迎贡献以构建更好的容器。
Open API类型和声明
OAS(Open API Specification)是由两组开发者(他们厌倦了相互争论 😄)合并 Swagger 和 RAML 规范开发的,因此它变得非常受欢迎,并且已经实现了 api-generator。
openapi: 3.0.1 info: This api provides access to articles servers: - url: https://{environment}.example.com:{port}/{basePath} description: Production server variables: environment: default: api description: An api for devices at Google dot com port: enum: - 80 - 443 default: 80 basePath: default: v3 # this version will be used as Modules subdirectory and base path uri in routes e.g. /Modules/V2/ and /v2/articles # to declare globally which files to include with other components declarations uses: topics: oas/topic.yaml
或者以 json 格式
{ "openapi": "3.0.2", "info": { "title": "Articles", "description": "This api provides access to articles", "version": "v3" }, "servers": [ { "url": "https://{environment}.example.com:{port}/{basePath}", "description": "Production server", "variables": { "environment": { "default": "api", "description": "An api for devices at Google dot com" }, "basePath": { "default": "v3" }, "port": { "enum": [ "80", "443" ], "default": "80" } } } ], "uses": { "topics": "oas/topic.json" } }
您可以将多个服务器和多个文件设置到主 openapi.yaml
中,因此将为每个服务器模块生成代码,例如:Modules/v2、Modules/v3、Modules/v4,并且将有来自不同文件的其他类型。
基本和自定义类型声明在以下位置
components: schemas:
或者以 json 格式
"components": { "schemas": {
类型 ID, Type, DataObject/DataArray
是特殊的辅助类型 - !required
您可以轻松地将 string
IDs 添加到您想要的实体中,例如 SID
可以放置在 Article
实体中,如下所示 id: SID
- api-generator 将相应地生成迁移、关系和模型。
ID: type: integer required: true # it will be BIGINT UNSIGNED in migration Schema if maximum > 10 maximum: 20 SID: type: string required: true maxLength: 128 Type: type: string required: true minLength: 1 maxLength: 255 DataObject: type: object required: true DataArray: type: array required: true
或者以 json 格式
"ID": { "type": "integer", "required": true, "maximum": 20 }, "SID": { "type": "string", "required": true, "maxLength": 128 }, "Type": { "type": "string", "required": true, "minLength": 1, "maxLength": 255 }, "DataObject": { "type": "object", "required": true }, "DataArray": { "type": "array", "required": true }
特殊数据类型 RelationshipsDataItem
- !required
RelationshipsDataItem: type: object properties: id: ID type: Type
或者以 json 格式
"RelationshipsDataItem": { "type": "object", "properties": { "id": "ID", "type": "Type" } }
在每种自定义关系类型中定义
属性 *Attributes
为每个自定义对象定义,例如
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 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"] 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
或者以 json 格式
"ArticleAttributes": { "description": "Article attributes description", "type": "object", "properties": { "title": { "type": "string", "required": true, "minLength": 16, "maxLength": 256 }, "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" } } }
关系自定义类型定义语义 *Relationships
TagRelationships: description: Tag relationship description type: object properties: data: type: DataArray items: type: RelationshipsDataItem
"TagRelationships": { "description": "Tag relationship description", "type": "object", "properties": { "data": { "type": "DataArray", "items": { "type": "RelationshipsDataItem" } } } }
完整的复合对象看起来像这样
Article: type: object properties: type: Type id: ID attributes: ArticleAttributes relationships: type: TagRelationships[] | TopicRelationships
"Article": { "type": "object", "properties": { "type": "Type", "id": "SID", "attributes": "ArticleAttributes", "relationships": { "type": "TagRelationships[] | TopicRelationships" } } }
这就是 api-generator 所需的全部内容,以提供在 Laravel 框架内直接使用的代码结构,其中可以应用任何业务逻辑。
要使用多个文件处理,请添加(作为根元素)
uses: topics: oas/openapi.yaml otherfile: oas/otherFile.yaml yetanother: oas/yetanother.yaml
所有文件都将生成,就像它们是一个复合对象一样。
要为 GET 查询参数设置默认值,请设置 QueryParams 如此
QueryParams: type: object properties: page: type: integer required: false default: 10 description: page number limit: type: integer required: false default: 15 description: elements per page sort: type: string required: false pattern: "asc|desc" default: "desc" access_token: type: string required: true example: db7329d5a3f381875ea6ce7e28fe1ea536d0acaf description: sha1 example default: db7329d5a3f381875ea6ce7e28fe1ea536d0acaf
它将在类似下面的请求中使用:http://example.com/api/v1/article?include=tag
其中没有传递任何参数。
生成器完成后,完整的目录结构将如下
Modules/{ModuleName}/Http/Controllers/ - contains Controllers that extends the DefaultController (descendant of Laravel's Controller) Modules/{ModuleName}/Http/FormRequest/ - contains forms that extends the BaseFormRequest (descendant of Laravel's FormRequest) and validates input attributes (that were previously defined as *Attributes) Modules/{ModuleName}/Entities/ - contains mappers that extends the BaseModel (descendant of Laravel's Model) and maps attributes to RDBMS Modules/{ModuleName}/Routes/api.php - contains routings pointing to Controllers with JSON API protocol support Modules/{ModuleName}/Database/Migrations/ - contains migrations created with option --migrations
Open API文档生成器
OAS 基于控制器文档是默认生成的,因此您不需要手动创建,让我们看看几个示例
所有生成的方法(展开时)将看起来像这样:[图片链接] 这里没有任何魔法 - 只需查看您的生成控制器,每个方法都有预生成的注解,例如。
<?php namespace Modules\V3\Http\Controllers; class ArticleController extends DefaultController { // >>>props>>> // <<<props<<< // >>>methods>>> /** * @OA\Get( * path="/v3/article", * summary="Get Articles ", * tags={"ArticleController"}, * @OA\Parameter( * in="query", * name="include", * required=false, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="query", * name="page", * required=false, * @OA\Schema( * type="integer", * ), * ), * @OA\Parameter( * in="query", * name="limit", * required=false, * @OA\Schema( * type="integer", * ), * ), * @OA\Parameter( * in="query", * name="sort", * required=false, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="query", * name="data", * required=false, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="query", * name="filter", * required=false, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="query", * name="order_by", * required=false, * @OA\Schema( * type="string", * ), * ), * @OA\Response( * response="200", * description="", * ), * ) */ /** * @OA\Get( * path="/v3/article/{id}", * summary="Get Article", * tags={"ArticleController"}, * @OA\Parameter( * in="query", * name="include", * required=false, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="query", * name="data", * required=false, * @OA\Schema( * type="string", * ), * ), * @OA\Response( * response="200", * description="", * ), * ) */ /** * @OA\Post( * path="/v3/article", * summary="Create Article", * tags={"ArticleController"}, * @OA\Response( * response="201", * description="", * ), * ) */ /** * @OA\Patch( * path="/v3/article/{id}", * summary="Update Article", * tags={"ArticleController"}, * @OA\Response( * response="200", * description="", * ), * ) */ /** * @OA\Delete( * path="/v3/article/{id}", * summary="Delete Article", * tags={"ArticleController"}, * @OA\Response( * response="204", * description="", * ), * ) */ /** * @OA\Get( * path="/v3/article/{id}/{related}", * summary="Get Article related objects", * tags={"ArticleController"}, * @OA\Parameter( * in="query", * name="data", * required=false, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="path", * name="id", * required=true, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="path", * name="related", * required=true, * @OA\Schema( * type="string", * ), * ), * @OA\Response( * response="200", * description="", * ), * ) */ /** * @OA\Get( * path="/v3/article/{id}/relationships/{relations}", * summary="Get Article relations objects", * tags={"ArticleController"}, * @OA\Parameter( * in="query", * name="data", * required=false, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="path", * name="id", * required=true, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="path", * name="relations", * required=true, * @OA\Schema( * type="string", * ), * ), * @OA\Response( * response="200", * description="", * ), * ) */ /** * @OA\Post( * path="/v3/article/{id}/relationships/{relations}", * summary="Create Article relation object", * tags={"ArticleController"}, * @OA\Parameter( * in="path", * name="id", * required=true, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="path", * name="relations", * required=true, * @OA\Schema( * type="string", * ), * ), * @OA\Response( * response="201", * description="", * ), * ) */ /** * @OA\Patch( * path="/v3/article/{id}/relationships/{relations}", * summary="Update Article relation object", * tags={"ArticleController"}, * @OA\Parameter( * in="path", * name="id", * required=true, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="path", * name="relations", * required=true, * @OA\Schema( * type="string", * ), * ), * @OA\Response( * response="200", * description="", * ), * ) */ /** * @OA\Delete( * path="/v3/article/{id}/relationships/{relations}", * summary="Delete Article relation object", * tags={"ArticleController"}, * @OA\Parameter( * in="path", * name="id", * required=true, * @OA\Schema( * type="string", * ), * ), * @OA\Parameter( * in="path", * name="relations", * required=true, * @OA\Schema( * type="string", * ), * ), * @OA\Response( * response="204", * description="", * ), * ) */ /** * @OA\Post( * path="/v3/article/bulk", * summary="Create Article bulk", * tags={"ArticleController"}, * @OA\Response( * response="201", * description="", * ), * ) */ /** * @OA\Patch( * path="/v3/article/bulk", * summary="Update Article bulk", * tags={"ArticleController"}, * @OA\Response( * response="200", * description="", * ), * ) */ /** * @OA\Delete( * path="/v3/article/bulk", * summary="Delete Article bulk", * tags={"ArticleController"}, * @OA\Response( * response="204", * description="", * ), * ) */ // <<<methods<<< }
生成文件内容
模块配置
<?php return [ 'name' => 'V1', 'query_params'=> [ 'limit' => 15, 'sort' => 'desc', 'access_token' => 'db7329d5a3f381875ea6ce7e28fe1ea536d0acaf', ], 'trees'=> [ 'menu' => true, ], 'jwt'=> [ 'enabled' => true, 'table' => 'user', 'activate' => 30, 'expires' => 3600, ], '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'=> [ 'user'=> [ 'permissions'=> [ 'enabled' => true, 'hide_mask' => true, 'flags'=> [ 'publisher' => 1, 'editor' => 2, 'manager' => 4, 'photo_reporter' => 8, 'admin' => 16, ], ], ], ], 'cache'=> [ 'tag'=> [ 'enabled' => true, 'stampede_xfetch' => false, 'stampede_beta' => 1.1, 'ttl' => 3600, ], 'article'=> [ 'enabled' => true, 'stampede_xfetch' => true, 'stampede_beta' => 1.5, 'ttl' => 300, ], ], ];
控制器
实体控制器示例
<?php namespace Modules\V1\Http\Controllers; class ArticleController extends DefaultController { }
默认情况下,每个控制器都使用任何 GET - index/view、POST - create、PATCH - update、DELETE - delete 方法。因此,您在这里不需要实现任何特殊的内容。
默认控制器示例
<?php namespace Modules\V1\Http\Controllers; use SoliDry\Extension\BaseController; class DefaultController extends BaseController { }
为了提供针对所有控制器的一定逻辑的开发者(用户空间)实现。
表单请求
Validation BaseFormRequest 示例
<?php namespace Modules\V2\Http\Requests; use SoliDry\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<<< }
模型
BaseModel 示例
<?php namespace Modules\V2\Entities; use Illuminate\Database\Eloquent\SoftDeletes; use SoliDry\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<<< }
路由
路由将创建在 /Modules/{ModuleName}/Routes/api.php
文件中,为 yaml 中定义的每个实体创建
// >>>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/relationships/{relations}', 'ArticleController@relations'); Route::post('/article/relationships/{relations}', 'ArticleController@createRelations'); Route::patch('/article/relationships/{relations}', 'ArticleController@updateRelations'); Route::delete('/article/relationships/{relations}', 'ArticleController@deleteRelations'); }); // <<<routes<<<
如您所注意到的,有支持关系 API 调用和批量扩展查询。
迁移
生成的迁移将类似于 Laravel 中的标准迁移
<?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'); } }
注意,您可以通过minLength/maxLength和minimum/maximum分别设置varchar、integer类型的任何范围。例如,可以将integer设置为无符号smallint,其中minimum: 1
(任何大于0的数字)和maximum: 2
(任何小于等于3的数字,以适应smallint数据库类型范围)。
如果使用double/float类型,则最大值用于显示长度(或M),最小值用于SQL中的精度(或D),例如:DOUBLE(M, D)
特定模块的所有迁移都将放在Modules/{ModuleName}/Database/Migrations/
要执行它们所有 - 运行:php artisan module:migrate
此外,值得注意的是 - Laravel使用table_id约定通过外键链接表。因此,您可以选择默认方式 - 在yaml中添加一个与表名匹配的id(就像在示例中一样:topic_id
-> 在article表中对应于topic表的id
,参见OAS类型和声明中的ArticleAttributes
)或创建自己的外键并将其添加到生成的BaseModel实体中的hasMany/belongsTo -> $foreignKey
参数。
此外,要指定特定列的索引,您可以添加一个如下的facets
属性
# regular index facets: index: idx_title: index # unique key facets: index: idx_url: unique # foreign key facets: index: idx_fk_topic_id: foreign references: id on: topic onDelete: cascade onUpdate: cascade
到现有列。
然而,有些情况下,您必须创建组合索引
last_name: required: false type: string minLength: 16 maxLength: 256 facets: composite_index: index: ['first_name', 'last_name'] # can be unique, primary
外键的一个示例可能如下
facets: composite_index: foreign: ['first_column', 'second_column'] references: ['first_column', 'second_column'] on: first_second onDelete: cascade onUpdate: cascade
测试
为了提供一个方便的集成/功能测试的方式,可以通过提供--tests
命令选项生成测试,例如
php artisan api:generate oas/openapi.yaml --migrations --tests
在命令输出中,您将看到以下文件已被创建
tests/functional/ArticleCest.php created ... tests/functional/TagCest.php created
有关如何在Laravel中设置功能测试环境的更多信息 - 请参阅https://codeception.com/for/laravel
关系特定品质
为了让生成器知道要应用哪种特定关系(例如:ManyToMany、OneToMany、OneToOne),在实体中设置relationships
属性,例如 - 例如,让我们看看如何设置Article和Tag实体之间的ManyToOne关系。
定义Article,如下所示,具有关系
relationships:
type: TagRelationships[]
和Tag,如下所示,具有关系
relationships:
type: ArticleRelationships
这样,您告诉生成器:“在Article和Tag之间创建从Article到Tag的OneToMany关系”这个想法适用于您需要的任何关系 - 例如,ManyToMany:TagRelationships[] -> ArticleRelationships[]
,OneToOne:TagRelationships -> ArticleRelationships
您还可以将多个关系绑定到一个实体,例如 - 您有一个Article实体,它必须绑定到TagRelationships和TopicRelationships,这可以这样做
relationships:
type: TagRelationships[] | TopicRelationships
或反之亦然
relationships:
type: TopicRelationships | TagRelationships[]
生成器将独立检测实体之间的所有关系。
查询参数
您可能想要使用额外的查询参数来获取包含项和/或分页,例如
http://example.com/api/v1/article?include=tag,topic&page=2&limit=10&sort=asc
您可能不想拖动所有属性/字段
http://example.com/api/v1/article/1?include=tag&data=["title", "description"]
注意:数据数组项必须用双引号设置。
或者您可能想要按不同的方向对多个列进行ORDER BY排序
http://example.com/api/v1/article?include=tag&order_by={"title":"asc", "created_at":"desc"}
此外,您还可以这样过滤结果
http://example.com/api/v1/article?include=tag&filter=[["updated_at", ">", "2017-01-03 12:13:13"], ["updated_at", "<", "2017-01-03 12:13:15"]]
这些数组将放入Laravel的where子句,并相应地受到参数绑定的保护。
类似于:v1、v2的动态模块名称将在运行时作为config/module.php
文件中的数组最后一个元素被提取,如果您由于某种奇怪的原因想使用以前的模块,只需将之前注册的模块之一设置为数组最后一个元素即可。
自动生成的config/module.php
示例
<?php return [ 'modules'=> [ 'v1', ] ];
为了在运行时获取配置参数,生成器将在Modules/{ModuleName}/Config/config.php
文件中创建内容
<?php return [ 'name'=>'V1', 'query_params'=> [ // default settings 'limit' => 15, 'sort' => 'desc', // access token to check via global FormRequest 'access_token' => 'db7329d5a3f381875ea6ce7e28fe1ea536d0acaf', ], ];
批量扩展
可以通过向表示资源集合的URL发送POST请求来创建多个资源。
POST /photos Content-Type: application/vnd.api+json; ext=bulk Accept: application/vnd.api+json; ext=bulk { "data": [{ "type": "photos", "title": "Ember Hamster", "src": "http://example.com/images/productivity.png" }, { "type": "photos", "title": "Mustaches on a Stick", "src": "http://example.com/images/mustaches.png" }] }
可以通过向表示它们所属的资源集合的URL发送PATCH请求来更新多个资源。
PATCH /articles Content-Type: application/vnd.api+json; ext=bulk Accept: application/vnd.api+json; ext=bulk { "data": [{ "type": "articles", "id": "1", "title": "To TDD or Not" }, { "type": "articles", "id": "2", "title": "To cache or not" }] }
可以通过向表示它们所属的资源集合的URL发送DELETE请求来删除多个资源。
DELETE /articles Content-Type: application/vnd.api+json; ext=bulk Accept: application/vnd.api+json; ext=bulk { "data": [ { "type": "articles", "id": "1" }, { "type": "articles", "id": "2" } ] }
请求完全成功或失败(在一个“事务”中)。
因此,任何涉及多个操作请求只有在所有操作都成功执行时才会成功。如果任何单个操作失败,则请求不会更改服务器的状态。
安全性
静态访问令牌
在 QueryParams
中,您可以声明 access_token
属性,该属性将被放置到 Modules/{ModuleName}/Config/config.php
。生成器将创建 Modules\{ModuleName}\Http\Requests\ApiAccessToken.php
FormRequest。
要激活在每个请求上执行此检查 - 将 ApiAccessToken FormRequest 添加到 app/Http/Kernel.php
,例如:
class Kernel extends HttpKernel { /** * The application's global HTTP middleware stack. * * These middleware are run during every request to your application. * * @var array */ protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Modules\V2\Http\Requests\ApiAccessToken::class, ];
生成的配置部分
'query_params'=> [ 'limit' => 15, 'sort' => 'desc', 'access_token' => 'db7329d5a3f381875ea6ce7e28fe1ea536d0acaf', ],
JWT (Json Web Token)
要支持 JWT 检查,您需要向任何用户、员工、客户等表中添加 jwt
和 password
属性
password: description: user password to refresh JWT (encrypted with password_hash) required: true type: string maxLength: 255 jwt: description: Special field to run JWT Auth via requests required: true type: string minLength: 256 maxLength: 512 default: ' '
参数 maxLength
非常重要,因为将创建 varchar 类型的 sql 字段,长度为 512。
默认值应精确等于 ' ' - 包含空格的空字符串。
JWT 特定配置将由生成器附加到 Modules/{ModuleName}/Config/config.php
'jwt'=> [ 'enabled' => true, 'table' => 'user', 'activate' => 30, 'expires' => 3600, ],
您可以根据需要更改这些 activate
和 expires
时间设置。
为了保护 JWT 令牌中的密钥验证 - 将 JWT_SECRET
变量放置到 .env 配置文件中,并分配密钥值(密钥可以是任何长度的任何字符串,但请明智地使用强大的密钥,例如:使用 sha1/sha2 等散列)。
然后将该值放置到全局配置文件 config/app.php
中,我们需要这样做以应用缓存配置环境的最佳实践。
'jwt_secret' => env('JWT_SECRET', 'secret'),
对于任何标准的 Laravel 中间件,在 app/Http/Kernel.php
中进行注册。
/** * The application's route middleware. * * These middleware may be assigned to groups or used individually. * * @var array */ protected $routeMiddleware = [ 'jwt' => \SoliDry\Extension\BaseJwt::class,
只需在需要定义的中定义中间件即可在任何请求中使用此中间件,例如,在 Modules/{ModuleName}/Routes/api.php
中:
仅声明特定路由的 JWT 检查
Route::get('/article', 'ArticleController@index')->middleware('jwt');
声明路由组的 JWT 检查
Route::group(['middleware' => 'jwt',
JWT 将在 POST 请求和更新实体的 PATCH 请求上创建,例如,如果您向 http://example.com/api/v1/user
发送以下内容的 POST 请求:
{ "data": { "type":"user", "attributes": { "first_name":"Alice", "last_name":"Hacker", "password":"my123Password" } } }
响应将类似于
{ "data": { "type": "user", "id": "7", "attributes": { "first_name": "Alice", "last_name": "Hacker", "password": null, "jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjU4ODljOGY2NzE3YjIifQ.eyJpc3MiOiJsYXJhdmVsLmxvYyIsImF1ZCI6ImxhcmF2ZWwubG9jIiwianRpIjoiNTg4OWM4ZjY3MTdiMiIsImlhdCI6MTQ4NTQyNDg4NiwibmJmIjoxNDg1NDI0OTE2LCJleHAiOjE0ODU0Mjg0ODYsInVpZCI6N30.JnC7OhlUIBoMTlu617q0q2nCQ4SqKh19bXtiHfBeg9o", "attributes": null, "request": null, "query": null, "server": null, "files": null, "cookies": null, "headers": null }, "links": { "self": "laravel.loc/user/7" } } }
注意如果 JWT enabled=true
,则密码将使用 password_hash
散列并保存在密码字段中。不要担心 "password": null,
属性,在输出之前已取消设置,以确保安全。您可以在模型上添加额外的密码或其他字段的检查,例如:长度、强度等,在 before/afterSave 事件中。
JWT 刷新的示例 - http://example.com/api/v1/user/4
{ "data": { "type":"user", "attributes": { "password":"myPassword123", "jwt":true } } }
请注意,密码和 jwt 设置为 true 是必需的。
响应
{ "data": { "type": "user", "id": "4", "attributes": { "first_name": "Alice", "last_name": "Hacker", "jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjU4ODcwNGU1NTRjNzkifQ.eyJpc3MiOiJsYXJhdmVsLmxvYyIsImF1ZCI6ImxhcmF2ZWwubG9jIiwianRpIjoiNTg4NzA0ZTU1NGM3OSIsImlhdCI6MTQ4NTI0MzYyMSwibmJmIjoxNDg1MjQzNjUxLCJleHAiOjE0ODUyNDcyMjEsInVpZCI6NH0.GD96ewc1dhbpz9grNaE2070Qy30Mqkh3B0VpEb7h3mQ", ...
带有 JWT 的常规请求将类似于
http://example.com/api/v1/article?include=tag&jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjU4ODVmYmM0NjUyN2MifQ.eyJpc3MiOiJsYXJhdmVsLmxvYyIsImF1ZCI6ImxhcmF2ZWwubG9jIiwianRpIjoiNTg4NWZiYzQ2NTI3YyIsImlhdCI6MTQ4NTE3NTc0OCwibmJmIjoxNDg1MTc1ODA4LCJleHAiOjE0ODUxNzkzNDgsInVpZCI6M30.js5_Fe5tFDfeK88KJJpSEpVO6rYBOG0UFAaVvlYYxcw
签名令牌的算法是 HS256,它可以在未来的版本中通过额外的用户定义选项进行更改,以便开发人员可以选择另一个。然而,HMAC SHA-256 是目前最受欢迎的。
缓存
API 随带缓存能力(通过 Redis)出厂自带,您需要做的只是声明缓存设置
Redis: type: object
并将 cache
属性设置在任何自定义实体中,例如:
Article: type: object properties: ... cache: type: Redis properties: stampede_xfetch: type: boolean default: true stampede_beta: type: number default: 1.5 ttl: type: integer default: 300
可以设置多个 Redis 服务器实例,如果它们具有集群或 replica-set。
另一个选项是通过将相应的 stampede 属性应用于 cache
实体来使您的服务对 Cache Stampede(或狗群)具有抵抗力(或狗群):stampede_xfetch
启用 xfetch 实现,并且 stampede_beta
应该在 0.5<=beta<=2.0 之间(其中 > 1.0 提前安排重新计算,< 1.0 晚些时候安排重新计算),在这种情况下还需要 ttl 属性。
生成的配置输出将类似于
'cache'=> [ 'article'=> [ 'enabled' => true, 'stampede_xfetch' => true, 'stampede_beta' => 1.5, 'ttl' => 300, ], ],
所有特定设置,包括主机/端口/密码、复制、集群等都可以通过 Laravel 标准的 Redis 缓存设置轻松配置。有关更多信息,请参阅此处 - Redis Laravel 配置
配置缓存设置后 - index
和 view
请求(例如:/api/v1/article/1?include=tag&data=["title", "description"]
或 /api/v1/article?include=tag&filter=...
)会将结果数据以指定 uri 的哈希键放入缓存中,从而提供唯一的键=值存储机制。
在 Redis 数据库实例中,您将看到具有以下键的序列化对象:
index:fa006676687269b5d1b12583ac1a8b64
...
view:f2d62a3c2003dcc0d89ef7d6746b6444
软删除
当模型进行软删除时,它们实际上并没有从您的数据库中删除。相反,模型上设置了一个 deleted_at
属性并将其插入数据库。如果一个模型有一个非空的 deleted_at
值,则该模型已被软删除。
要为模型启用软删除,只需在您需要的任何自定义类型上添加 deleted_at
属性,例如:
ArticleAttributes: description: Article attributes description type: object properties: ... deleted_at: type: datetime
对于指定的类型,将在 Entities/
文件夹中生成特殊的属性/特性,同时也会创建相关的迁移字段。
模型示例
<?php namespace Modules\V2\Entities; use Illuminate\Database\Eloquent\SoftDeletes; use SoliDry\Extension\BaseModel; class Article extends BaseModel { use SoftDeletes; // >>>props>>> protected $dates = ['deleted_at']; // ... }
迁移示例
<?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->softDeletes(); // ... }); } // ... }
它将自动应用于删除请求,并且模型不会在 view/index 中被收集。
关闭 JSON API 支持
如果您愿意禁用 json api 规范映射到 Laravel 应用程序中(例如 - 您需要在 laravel-module 中生成 MVC 结构并创建自己的 json 架构,或任何其他输出格式),只需在 DefaultController 中将 $jsonApi
属性设置为 false。
<?php namespace Modules\V1\Http\Controllers; use SoliDry\Extension\BaseController; class DefaultController extends BaseController { protected $jsonApi = false; }
因为此类由所有控制器继承 - 您无需在每个控制器类中添加此属性。默认情况下,JSON API 已开启。
树结构
您可以通过将其声明为 Trees
自定义类型来轻松构建树结构
Trees: type: object properties: menu: type: boolean default: true catalog: type: boolean default: false
并将 parent_id
添加到目标表中,例如:
MenuAttributes: type: object properties: title: required: true type: string rfc: type: string default: / parent_id: description: mandatory field for building trees type: integer minimum: 9 maximum: 10 default: 0
整个树将被放置在 meta
json-api 根元素中,而所有父元素(存储为 parent_id=0)将位于数据根元素中。这样做是为了保持稳定的 json-api 结构及其关系。
元数据响应示例
"meta": { "menu_tree": [ { "id": 1, "title": "ttl1", "rfc": "/", "parent_id": 0, "created_at": null, "updated_at": null, "children": [ { "id": 3, "title": "ttl21", "rfc": "/", "parent_id": 1, "created_at": null, "updated_at": null, "children": [] }, { "id": 2, "title": "ttl2", "rfc": "/", "parent_id": 1, "created_at": null, "updated_at": null, "children": [ { "id": 4, "title": "ttl3", "rfc": "/", "parent_id": 2, "created_at": null, "updated_at": null, "children": [] } ] } ] } ] }
子元素位于每个父元素的 children
属性数组中,如果没有子元素,则为空。
要获取最顶层祖先的子树 - 简单地执行对项目的 GET 请求,例如:http://example.com/api/v1/menu/1
。请参阅 Postman 中的真实世界示例。
有限状态机
要将有限状态机添加到实体(表)的字段(列)中 - 将定义添加到您的 OAS 文件中,如下所示:
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: []
state_machine
声明中唯一必需的特定项是状态机的 initial
值。
生成过程通过后,您将在 config.php
中获得以下内容:
'state_machine'=> [ 'article'=> [ 'status'=> [ 'enabled'=>true, 'states'=> [ 'initial' => ['draft'], 'draft' => ['published'], 'published' => ['archived', 'postponed'], 'postponed' => ['published', 'archived'], 'archived' => [''], ], ], ], ],
它将分别处理 POST
和 PATCH
请求。您可以通过将 enabled
设置为 false
来轻松禁用状态机。您可以在不同的表中添加状态机。
拼写检查
安装
由强大的 Linux 库 GNU aspell
和其作为 PHP 扩展的词典提供的拼写检查功能。
在 Linux(例如 Ubuntu)上安装扩展
apt-get install php-pspell
要安装额外的语言 db 运行
apt-get install aspell-fr
使用方法
您可能希望将拼写检查设置在特定的字段/列上
description: required: true type: string minLength: 32 maxLength: 1024 facets: spell_check: true spell_language: en
生成器输出在 Modules/{VersionName}/Config/config.php
中看起来如下:
'spell_check'=> [ 'article'=> [ 'description'=> [ 'enabled'=>true, 'language' => 'en', ], ], ],
与其他设置一样,可以通过将 enabled
设置为 false 来禁用拼写检查。如果没有预先设置有关语言的信息,则默认使用 en
。
在 POST/PATCH(创建/更新)方法的响应中,您将获得带有填充的失败检查数组的 meta
内容。
{ "data": { "type": "article", "id": "21", "attributes": { "title": "Quick brown fox", "description": "The quick brovn fox jumped ower the lazy dogg", "url": "http://example.com/articles/21/tags", "show_in_top": "0", "status": "draft" }, "links": { "self": "example.com/article/21" } }, "meta": { "spell_check": { "description": [ "brovn", "ower", "dogg" ] } } }
位掩码
要使用带有自动标志碎片化/重新组合的位掩码,您可以将额外的方面定义为整数字段,如下所示:
permissions: type: integer required: false maximum: 20 facets: bit_mask: publisher: 1 editor: 2 manager: 4 photo_reporter: 8 admin: 16
因此,将生成并使用在请求运行时处理数据的 bit_mask
配置实体。
生成的配置片段
'bit_mask'=> [ 'user'=> [ 'permissions'=> [ 'enabled' => true, 'flags'=> [ 'publisher' => 1, 'editor' => 2, 'manager' => 4, 'photo_reporter' => 8, 'admin' => 16, ], ], ], ],
请求/响应将是
{ "data": { "type":"user", "attributes": { "publisher": false, "editor": true, "manager": false, "photo_reporter": true, "admin": true } } }
{ "data": { "type": "user", "id": "1", "attributes": { "first_name": "Alice", "last_name": "Hacker", "permissions": 26, "publisher": false, "editor": true, "manager": false, "photo_reporter": true, "admin": true,
请记住,您可以通过隐藏索引/视图 GET 请求中的 permissions
字段来执行操作。
自定义SQL
如果出于任何原因您需要使用自定义 SQL 查询,只需在 Modules/V1/Config/config.php
中定义它。
'custom_sql' => [ 'article' => [ 'enabled' => true, 'query' => 'SELECT id, title FROM article a INNER JOIN tag_article ta ON ta.article_id=a.id WHERE ta.tag_id IN ( SELECT id FROM tag WHERE CHAR_LENGTH(title) > :tag_len ) ORDER BY a.id DESC', 'bindings' => [ 'tag_len' => 5, ] ], ],
如您所见,对于所需实体有 query
、bindings
(已传递安全参数绑定的值)和enabled
参数。只有对于 index
API 方法,才会执行自定义 SQL 查询,因此如果您需要例如 delete
或 update
特定额外的行,请使用之前选择的 ID 调用这些方法。
别忘了添加 Laravel 特定的 $fillable
或 $guarded
数组,以便填充对象(mass-assignment 规则),例如:
protected $fillable = [ 'id', 'title' ];
注意:由于 json-api 序列化器,您需要一个 id
字段。
自定义业务逻辑
您可以为需要的任何业务逻辑添加代码,在预生成的控制器中添加自定义代码的最佳位置是,例如,为 ArticleController
中的字段添加特定的清理程序以及修改后的输出,您可以像这样覆盖 create
方法
<?php namespace Modules\V1\Http\Controllers; use Illuminate\Http\Request; class ArticleController extends DefaultController { public function create(Request $request) { // any business logic here for input pre-processing data parent::create($request); // any business logic here for output pre-processing data } }
可能存在需要在该类型索引/查看/创建/更新/删除的特定方法或初始化逻辑中添加回退的情况,这可以通过将代码放置在 DefaultController
中轻松实现,就像为任何其他控制器一样。专门为这些目的构建的继承模型将优雅地执行任何操作/之后等。例如
<?php namespace Modules\V1\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Routing\Route; use SoliDry\Extension\BaseController; class DefaultController extends BaseController { public function __construct(Route $route) { // specific code before init parent::__construct($route); // specific code after init } public function index(Request $request) { // specific code before index execution parent::index($request); // specific code after index execution } }
如您所注意到的,您可以访问 Route 和 Request 属性。
在下一章中,您将了解如何将自定义代码放置在 Models/FormRequest 中,以保护它免受代码生成覆盖。
重新生成
根据生成的类型历史和当前 OpenApi 文档的状态重新生成您的代码是一个重要功能,这可以通过运行以下命令轻松实现
php artisan api:generate oas/openapi.yaml --migrations --regenerate --merge=last
此命令将合并来自 .gen
目录的最后状态/快照和当前文档(在这种情况下来自 oas/openapi.yaml
),然后为模型和 FormRequest 创建文件,将其与用户添加的内容合并,这些内容在生成的属性和方法之间。此外,它将在新创建的迁移文件中添加新的列及其索引。
控制器状态
<?php namespace Modules\V2\Http\Controllers; class ArticleController extends DefaultController { private $prop = 'foo'; // >>>props>>> // <<<props<<< public function myMethod() { return true; } }
重新生成的 FormRequest 示例
<?php namespace Modules\V2\Http\Requests; use SoliDry\Extension\BaseFormRequest; class TagFormRequest extends BaseFormRequest { public $userPropOne = true; // >>>props>>> public $id = null; // Attributes public $title = null; // <<<props<<< public $userPropTwo = 123; public function userDefinedMethod(): int { return 1; } // >>>methods>>> public function authorize(): bool { return true; } public function rules(): array { return [ "title" => "string|required|min:3", ]; } public function relations(): array { return [ "article", ]; } // <<<methods<<< public function anotherUserDefinedMethod(): bool { return false; } }
如您所见,所有用户内容都得到了保留,并与重新生成的合并。当存在 --regenerate
选项时,自定义业务逻辑内容会保存其状态,无论是带有还是不带其他选项。
对于 Eloquent 模型也是一样。
<?php namespace Modules\V1\Entities; use SoliDry\Extension\BaseModel; class Article extends BaseModel { public $userPropOne = true; // >>>props>>> protected $primaryKey = "id"; protected $table = "article"; public $timestamps = false; // <<<props<<< public $userPropTwo = 123; public function userDefinedMethod(): int { return 1; } // >>>methods>>> public function tag() { return $this->belongsToMany(Tag::class, 'tag_article'); } public function topic() { return $this->belongsTo(Topic::class); } // <<<methods<<< public function anotherUserDefinedMethod(): bool { return false; } }
重新生成的迁移示例
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class AddColumnLastNameToUser extends Migration { public function up() { Schema::table('user', function(Blueprint $table) { $table->string('last_name', 256); $table->index(['first_name', 'last_name']); $table->unsignedBigInteger('permissions'); }); } public function down() { Schema::table('user', function(Blueprint $table) { $table->dropColumn('last_name'); $table->dropColumn('permissions'); }); } }
如果您不想在每次运行时保存历史记录,请添加 --no-history
选项。
您还可以做更多关于回滚历史记录的事情
- 通过传递选项例如
--merge=9
,生成器将回退 9 步 --merge="2017-07-29 11:35:32"
生成器通过时间在历史记录中获取到具体的文件
尽管如此,如果您需要完全回滚系统的状态,请使用与合并相同的键的 --rollback
选项。
==== Infection 代码覆盖率 ====
指标
Mutation Score Indicator (MSI): 81%
Mutation Code Coverage: 86%
Covered Code MSI: 93%
==========
HTTP 请求/响应示例可以在 WiKi 页面找到 - https://github.com/SoliDry/api-generator/wiki
带有生成文件的 Laravel 项目示例可以在这里找到 - https://github.com/SoliDry/laravel-api
要深入了解 Open API
规范 - https://swagger.org.cn/specification/
要深入了解 JSON-API
规范 - http://jsonapi.org/format/ JSON-API 支持由 Fractal 包提供,尤其是对于输出,由 Fractal 包提供 - http://fractal.thephpleague.com/
编码愉快 ;-)
PS 此存储库的目的是防止重复做同样的事情,期望不同的结果。(感谢阿尔伯特·爱因斯坦)
支持者的感激之情