rjapi / raml-json-api
Laravel 框架的 PHP 代码生成器(基于 OAS),完全支持 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-08-29 04:31:10 UTC
README
基于 OAS 的 Laravel 框架 PHP 代码生成器,完全支持 JSON-API 数据格式
- 入门演示
- CRUD 演示
- 关系链接演示
- 迁移演示
- API 文档演示
- 安装
- Open API 类型与声明
- Open API 文档生成器
- 生成的文件内容
- 关系
- 批量扩展
- 查询参数
- 安全
- 缓存
- 软删除
- 树形结构
- 有限状态机
- 拼写检查
- 位掩码
- 自定义 SQL
- 自定义业务逻辑
- 重新生成
通过 composer 安装
首先 - 如果您还没有这样做,请创建 Laravel 项目
composer create-project --prefer-dist laravel/laravel your_app
然后在您的项目目录中运行
composer require solidry/api-generator
它将自动通过添加控制台命令 api:generate
(您应在运行 php artisan
时看到它)来注册 Laravel ServiceProvider,并发布 "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 的控制器/表单请求/模型+Pivot、路由(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
ID,例如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 *Controllers基于文档是默认生成的,因此您不需要手动创建它。让我们看看一些例子
所有生成的方法(展开后)将看起来像这样: 这其中没有任何魔法 - 只需查看您的生成控制器,其中为每个方法预生成了注释,例如。
<?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和整数类型的任何范围。例如,整数可以设置为无符号的小整数,通过设置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
-> 文章表中的主题表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。
default
值应精确等于 ' ' - 空字符串(包含空格)。
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 服务器实例,如果它们有集群或副本集。
另一种选择是使您的服务对 Cache Stampede(或狗群攻击)具有抵抗力,通过将相应的 stampede 属性应用于 cache
实体,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(); // ... }); } // ... }
这将自动应用于删除请求,并且模型不会被收集到视图/索引中。
关闭JSON API支持
如果您愿意禁用Laravel应用程序中的JSON API规范映射(例如 - 您需要生成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
。请参阅wiki页面获取使用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
要安装额外的语言数据库运行
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,
请记住,您可以始终隐藏例如permissions
字段在索引/视图GET请求中,如果需要的话。
自定义 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
数组,以便填充对象(请参阅批量赋值规则),例如:
protected $fillable = [ 'id', 'title' ];
注意:您需要一个id
字段,因为json-api序列化器需要它。
自定义业务逻辑
您可以根据需要添加任何业务逻辑,将自定义代码放在预生成的控制器中是最佳选择,例如,要为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属性。
在下一章中,您将了解如何将自定义代码放置在模型/表单请求中,并保护其免受代码生成重写。
重新生成
根据生成的历史类型和当前OpenApi文档的状态重新生成代码是一项重要功能,这可以通过运行以下命令轻松实现
php artisan api:generate oas/openapi.yaml --migrations --regenerate --merge=last
此命令将合并来自.gen
目录的文档的最后状态/快照和当前文档(在这种情况下来自oas/openapi.yaml
),然后创建模型和表单请求文件,将其与用户在生成的属性和方法之间添加的内容合并。此外,它还会在新生成的迁移文件中添加新列及其索引。
控制器状态
<?php namespace Modules\V2\Http\Controllers; class ArticleController extends DefaultController { private $prop = 'foo'; // >>>props>>> // <<<props<<< public function myMethod() { return true; } }
重生成的表单请求示例
<?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包提供,特别是对于输出,请参阅http://fractal.thephpleague.com/
祝您编码愉快 ;-)
PS:此仓库的目的是防止一遍又一遍地做同样的事情,期望得到不同的结果。(感谢阿尔伯特·爱因斯坦)
支持者感激之情