rjapi / raml-json-api

Laravel 框架的 PHP 代码生成器(基于 OAS),完全支持 JSON-API 数据格式

资助包维护!
Liberapay
Issuehunt
Patreon

安装次数: 416

依赖者: 0

建议者: 0

安全: 0

星标: 328

关注者: 14

分支: 60

类型:api-generator


README

基于 OAS 的 Laravel 框架 PHP 代码生成器,完全支持 JSON-API 数据格式 Tweet

Build Status Scrutinizer Code Quality Total Downloads Latest Stable Version Code Intelligence Status codecov License: MIT

alt OAS logo alt Laravel logo alt JSON API logo

通过 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如果您需要重写之前生成的所有文件,请使用此选项。默认情况下,生成的文件被保留以防止覆盖已添加/修改的内容。

输出将类似于以下内容

Console output

之后,您可以在您的项目中看到以下目录和文件模块结构

Dirs and files

Docker仓库

安装并使用api-generator(以沙盒方式)的另一种方法是使用https://github.com/SoliDry/laravel-api。克隆仓库并运行

docker-compose up -d

您可以在那里看到Dockerfiledocker-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基于文档是默认生成的,因此您不需要手动创建它。让我们看看一些例子

OAS API docs generated

所有生成的方法(展开后)将看起来像这样:OAS API docs generated all methods 这其中没有任何魔法 - 只需查看您的生成控制器,其中为每个方法预生成了注释,例如。

<?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 检查,您需要将 jwtpassword 属性添加到任何用户、员工、客户等表中。

  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,
    ],

您可以按需更改这些 activateexpires 时间设置。

为了保护 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 配置

配置缓存设置后 - indexview 请求(例如:/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' => [''],
                ],
            ],
        ],
    ],

它将在POSTPATCH请求上分别进行处理。您可以通过将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,
            ]
        ],
    ],  

正如您所看到的,对于所需的实体,存在querybindings(已传递安全参数绑定值的地方)和enabled参数。只有针对index API方法,才会执行自定义SQL查询,因此如果您需要例如deleteupdate特定的额外行,请使用先前选定的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:此仓库的目的是防止一遍又一遍地做同样的事情,期望得到不同的结果。(感谢阿尔伯特·爱因斯坦)

支持者感激之情

JetBrains logo