solidry/api-generator

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

资助包维护!
Liberapay
Issuehunt
Patreon

安装量: 26 347

依赖项: 0

建议者: 2

安全性: 0

星标: 328

关注者: 14

分支: 60

开放问题: 10

类型:api-generator


README

基于OAS的PHP代码生成器(适用于Laravel框架),完全支持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项目,请创建Laravel项目

composer create-project --prefer-dist laravel/laravel your_app

然后在您的项目目录中运行

composer require solidry/api-generator

它将自动通过添加控制台命令api:generate来注册Laravel ServiceProvider(您应该在运行php artisan时看到它),并发布“laravel-modules”提供者。

自动加载

默认情况下,控制器、实体或存储库不会自动加载。您可以使用psr-4来自动加载您的模块。例如

{
  "autoload": {
    "psr-4": {
      "App\\": "app/",
      "Modules\\": "Modules/"
    }
  }
}

通过运行来刷新更改

composer dump-autoload

可选设置

要提供与Json API兼容的错误处理程序,可以将ErrorHandler特质添加到app/Exceptions/Handler类中,并从标准的Laravel render方法返回return $this->renderJsonApi($request, $exception);Handler类的完整示例如下所示

<?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use SoliDry\Exceptions\ErrorHandler;

class Handler extends ExceptionHandler
{
    use ErrorHandler; 
    
    /**
     * Render an exception into an HTTP response.
     *
     * @param  \Illuminate\Http\Request $request
     * @param  \Exception $exception
     * @return JsonResponse
     */
    public function render($request, Exception $exception): JsonResponse
    {
        return $this->renderJsonApi($request, $exception); // method you should call to return json-api response
    }
}

如您所注意到的,它返回Illuminate\Http\JsonResponse Laravel对象以适当地输出数据,例如。

{
  "errors": [
    {
      "code": 61,
      "message": "Connection refused [tcp://127.0.0.1:6379]",
      "file": "/vendor/predis/predis/src/Connection/AbstractConnection.php",
      "line": 155,
      "uri": "http://laravel.loc/api/v2/article",
      "meta": "#0 /vendor/predis/predis/src/Connection/StreamConnection.php(128): Predis\\Connection\\AbstractConnection->onConnectionError('Connection refu...', 61)\n#1 /vendor/predis/predis/src/Connection/StreamConnection.php(178): Predis\\Connection\\StreamConnection->createStreamSocket(Object(Predis\\Connection\\Parameters), 'tcp://127.0.0.1...', 4)\n#2 /vendor/predis/predis/src/Connection/StreamConnection.php(100): Predis\\Connection\\StreamConnection->tcpStreamInitializer(Object(Predis\\Connection\\Parameters))\n#3 /vendor/predis/predis/src/Connection/AbstractConnection.php(81): Predis\\Connection\\StreamConnection->createResource()\n#4 /vendor/predis/predis/src/Connection/StreamConnection.php(258): Predis\\Connection\\AbstractConnection->connect()\n#5 /vendor/predis/predis/src/Connection/AbstractConnection.php(180): Predis\\Connection\\StreamConnection->connect()\n#6 /vendor/predis/predis/src/Connection/StreamConnection.php(288): Predis\\Connection\\AbstractConnection->getResource()\n#7 /vendor/predis/predis/src/Connection/StreamConnection.php(394): Predis\\Connection\\StreamConnection->write('*2\\r\\n$3\\r\\nGET\\r\\n$4...')\n#8 /vendor/predis/predis/src/Connection/AbstractConnection.php(110): Predis\\Connection\\StreamConnection->writeRequest(Object(Predis\\Command\\StringGet))\n#9 /vendor/predis/predis/src/Client.php(331): Predis\\Connection\\AbstractConnection->executeCommand(Object(Predis\\Command\\StringGet))\n#10 /vendor/predis/predis/src/Client.php(314): Predis\\Client->executeCommand(Object(Predis\\Command\\StringGet))\n#11 /vendor/laravel/framework/src/Illuminate/Redis/Connections/Connection.php(114): Predis\\Client->__call('get', Array)\n#12 /vendor/laravel/framework/src/Illuminate/Redis/Connections/Connection.php(214): Illuminate\\Redis\\Connections\\Connection->command('get', Array)\n#13 /vendor/laravel/framework/src/Illuminate/Redis/RedisManager.php(195): Illuminate\\Redis\\Connections\\Connection->__call('get', Array)\n#14 /vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php(237): Illuminate\\Redis\\RedisManager->__call('get', Array)\n#15 /vendor/solidry/api-generator/src/Extension/CacheTrait.php(95): Illuminate\\Support\\Facades\\Facade::__callStatic('get', Array)\n#16 /vendor/solidry/api-generator/src/Extension/CacheTrait.php(60): SoliDry\\Extension\\ApiController->getXFetched(Object(Illuminate\\Http\\Request), Object(SoliDry\\Helpers\\SqlOptions))\n#17 /vendor/solidry/api-generator/src/Extension/ApiController.php(115): SoliDry\\Extension\\ApiController->getCached(Object(Illuminate\\Http\\Request), Object(SoliDry\\Helpers\\SqlOptions))\n#18 [internal function]: SoliDry\\Extension\\ApiController->index(Object(Illuminate\\Http\\Request))\n#19 /vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): call_user_func_array(Array, Array)\n#20 /vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(45): Illuminate\\Routing\\Controller->callAction('index', Array)\n#21 /vendor/laravel/framework/src/Illuminate/Routing/Route.php(219): Illuminate\\Routing\\ControllerDispatcher->dispatch(Object(Illuminate\\Routing\\Route), Object(Modules\\V2\\Http\\Controllers\\ArticleController), 'index')\n#22 /vendor/laravel/framework/src/Illuminate/Routing/Route.php(176): Illuminate\\Routing\\Route->runController()\n#23 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(680): Illuminate\\Routing\\Route->run()\n#24 /vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(30): Illuminate\\Routing\\Router->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))\n#25 /vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php(41): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))\n#26 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(163): Illuminate\\Routing\\Middleware\\SubstituteBindings->handle(Object(Illuminate\\Http\\Request), Object(Closure))\n#27 /vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))\n#28 /vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php(58): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))\n#29 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(163): Illuminate\\Routing\\Middleware\\ThrottleRequests->handle(Object(Illuminate\\Http\\Request), Object(Closure), 60, '1')\n#30 /vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(53): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}(Object(Illuminate\\Http\\Request))\n#31 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(104): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))\n#32 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(682): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))\n#33 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(657): Illuminate\\Routing\\Router->runRouteWithinStack(Object(Illuminate\\Routing\\Route), Object(Illuminate\\Http\\Request))\n#34 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(623): Illuminate\\Routing\\Router->runRoute(Object(Illuminate\\Http\\Request), Object(Illuminate\\Routing\\Route))\n#35 /vendor/laravel/framework/src/Illuminate/Routing/Router.php(612): Illuminate\\Routing\\Router->dispatchToRoute(Object(Illuminate\\Http\\Request))\n#36 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(176): Illuminate\\Routing\\Router->dispatch(Object(Illuminate\\Http\\Request))\n#37 /vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(30): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}(Object(Illuminate\\Http\\Request))\n#38 /vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(104): Illuminate\\Routing\\Pipeline->Illuminate\\Routing\\{closure}(Object(Illuminate\\Http\\Request))\n#39 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(151): Illuminate\\Pipeline\\Pipeline->then(Object(Closure))\n#40 /vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(116): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter(Object(Illuminate\\Http\\Request))\n#41 /public/index.php(55): Illuminate\\Foundation\\Http\\Kernel->handle(Object(Illuminate\\Http\\Request))\n#42 {main}"
    }
  ]
}

默认情况下,Laravel安装具有api前缀的API路由。如果您想通过前缀访问生成的json api路由,例如/v2/article/myshop/basket,您需要从您的RouteServiceProvider中的mapApiRoutes()方法中删除前缀。

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;

class RouteServiceProvider extends ServiceProvider
{
    // ...

    protected function mapApiRoutes()
    {
        // Route::prefix('api') // you don't need prefixes then
        Route::middleware('api')
             ->namespace($this->namespace)
             ->group(base_path('routes/api.php'));
    }
}

运行生成器

在控制台中运行

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

此命令为您创建整个环境,以便您可以继续构建基于OAS/Laravel/JSON API的复杂API,特别是:模块化应用程序的目录、支持完整MVC的控制器/表单请求/模型+枢轴、路由(与JSON API兼容)甚至迁移以帮助您创建RDBMS结构。

oas/openapi.yaml - 项目根目录中的oas目录中的文件,应在准备就绪之前准备就绪,或者您可能希望仅通过复制示例来尝试,例如mkdir oas && curl 'https://raw.githubusercontent.com/SoliDry/api-generator/master/tests/functional/oas/openapi.yaml' > /oas/openapi.yaml,并可能根据您的需求进行重写。

如果需要或对您的环境更方便,您也可以使用.json扩展/格式来产生相同的结果。

选项

--migrations 是一个选项,用于为每个实体(create_entityName_table)以及如果有多对多关系则为其生成迁移。

--regenerate 如果需要重写之前生成的所有文件,请使用此选项。默认情况下,生成的文件会被保留以防止覆盖新增或修改的内容。

输出将类似于以下内容

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 IDs 添加到您想要的实体中,例如 SID 可以放置在 Article 实体中,如下所示 id: SID - api-generator 将相应地生成迁移、关系和模型。

  ID:
    type: integer
    required: true
    # it will be BIGINT UNSIGNED in migration Schema if maximum > 10
    maximum: 20
  SID:
    type: string
    required: true
    maxLength: 128    
  Type:
    type: string
    required: true
    minLength: 1
    maxLength: 255
  DataObject:
    type: object
    required: true
  DataArray:
    type: array
    required: true

或者以 json 格式

"ID": {
    "type": "integer",
    "required": true,
    "maximum": 20
},
"SID": {
    "type": "string",
    "required": true,
    "maxLength": 128
},
"Type": {
    "type": "string",
    "required": true,
    "minLength": 1,
    "maxLength": 255
},
"DataObject": {
    "type": "object",
    "required": true
},
"DataArray": {
    "type": "array",
    "required": true
}

特殊数据类型 RelationshipsDataItem - !required

  RelationshipsDataItem:
    type: object
    properties:
      id: ID
      type: Type

或者以 json 格式

"RelationshipsDataItem": {
    "type": "object",
    "properties": {
        "id": "ID",
        "type": "Type"
    }
}

在每种自定义关系类型中定义

属性 *Attributes 为每个自定义对象定义,例如

  ArticleAttributes:
    description: Article attributes description
    type: object
    properties:
      title:
        required: true
        type: string
        minLength: 16
        maxLength: 256
        facets:
          index:
            idx_title: index
      description:
        required: true
        type: string
        minLength: 32
        maxLength: 1024
      url:
        required: false
        type: string
        minLength: 16
        maxLength: 255
        facets:
          index:
            idx_url: unique        
      show_in_top:
        description: Show at the top of main page
        required: false
        type: boolean
      status:
        description: The state of an article
        enum: ["draft", "published", "postponed", "archived"]
      topic_id:
        description: ManyToOne Topic relationship
        required: true
        type: integer
        minimum: 1
        maximum: 6
        facets:
          index:
            idx_fk_topic_id: foreign
            references: id
            on: topic
            onDelete: cascade
            onUpdate: cascade        
      rate:
        type: number
        minimum: 3
        maximum: 9
        format: double     

或者以 json 格式

"ArticleAttributes": {
    "description": "Article attributes description",
    "type": "object",
    "properties": {
        "title": {
            "type": "string",
            "required": true,
            "minLength": 16,
            "maxLength": 256
        },
        "description": {
            "required": true,
            "type": "string",
            "minLength": 32,
            "maxLength": 1024,
            "facets": {
                "spell_check": true,
                "spell_language": "en"
            }
        },
        "url": {
            "required": false,
            "type": "string",
            "minLength": 16,
            "maxLength": 255,
            "facets": {
                "index": {
                    "idx_url": "unique"
                }
            }
        },
        "show_in_top": {
            "description": "Show at the top of main page",
            "required": false,
            "type": "boolean"
        },
        "status": {
            "description": "The state of an article",
            "enum": [
                "draft",
                "published",
                "postponed",
                "archived"
            ],
            "facets": {
                "state_machine": {
                    "initial": [
                        "draft"
                    ],
                    "draft": [
                        "published"
                    ],
                    "published": [
                        "archived",
                        "postponed"
                    ],
                    "postponed": [
                        "published",
                        "archived"
                    ],
                    "archived": []
                }
            }
        },
        "topic_id": {
            "description": "ManyToOne Topic relationship",
            "required": true,
            "type": "integer",
            "minimum": 1,
            "maximum": 6,
            "facets": {
                "index": {
                    "idx_fk_topic_id": "foreign",
                    "references": "id",
                    "on": "topic",
                    "onDelete": "cascade",
                    "onUpdate": "cascade"
                }
            }
        },
        "rate": {
            "type": "number",
            "minimum": 3,
            "maximum": 9,
            "format": "double"
        },
        "date_posted": {
            "type": "date-only"
        },
        "time_to_live": {
            "type": "time-only"
        },
        "deleted_at": {
            "type": "datetime"
        }
    }
}

关系自定义类型定义语义 *Relationships

  TagRelationships:
    description: Tag relationship description
    type: object
    properties:
      data:
        type: DataArray
        items:
          type: RelationshipsDataItem
"TagRelationships": {
    "description": "Tag relationship description",
    "type": "object",
    "properties": {
        "data": {
            "type": "DataArray",
            "items": {
                "type": "RelationshipsDataItem"
            }
        }
    }
}

完整的复合对象看起来像这样

  Article:
    type: object
    properties:
      type: Type
      id: ID
      attributes: ArticleAttributes
      relationships:
        type: TagRelationships[] | TopicRelationships     
"Article": {
    "type": "object",
    "properties": {
        "type": "Type",
        "id": "SID",
        "attributes": "ArticleAttributes",
        "relationships": {
            "type": "TagRelationships[] | TopicRelationships"
        }
    }
}

这就是 api-generator 所需的全部内容,以提供在 Laravel 框架内直接使用的代码结构,其中可以应用任何业务逻辑。

要使用多个文件处理,请添加(作为根元素)

uses:
  topics: oas/openapi.yaml
  otherfile: oas/otherFile.yaml
  yetanother: oas/yetanother.yaml

所有文件都将生成,就像它们是一个复合对象一样。

要为 GET 查询参数设置默认值,请设置 QueryParams 如此

  QueryParams:
    type: object
    properties:
      page:
        type: integer
        required: false
        default: 10
        description: page number
      limit:
        type: integer
        required: false
        default: 15
        description: elements per page
      sort:
        type: string
        required: false
        pattern: "asc|desc"
        default: "desc"
      access_token:
        type: string
        required: true
        example: db7329d5a3f381875ea6ce7e28fe1ea536d0acaf
        description: sha1 example
        default: db7329d5a3f381875ea6ce7e28fe1ea536d0acaf        

它将在类似下面的请求中使用:http://example.com/api/v1/article?include=tag 其中没有传递任何参数。

生成器完成后,完整的目录结构将如下

Modules/{ModuleName}/Http/Controllers/ - contains Controllers that extends the DefaultController (descendant of Laravel's Controller)
Modules/{ModuleName}/Http/FormRequest/ - contains forms that extends the BaseFormRequest (descendant of Laravel's FormRequest) and validates input attributes (that were previously defined as *Attributes)
Modules/{ModuleName}/Entities/ - contains mappers that extends the BaseModel (descendant of Laravel's Model) and maps attributes to RDBMS
Modules/{ModuleName}/Routes/api.php - contains routings pointing to Controllers with JSON API protocol support
Modules/{ModuleName}/Database/Migrations/ - contains migrations created with option --migrations

Open API文档生成器

OAS 基于控制器文档是默认生成的,因此您不需要手动创建,让我们看看几个示例

OAS API docs generated

所有生成的方法(展开时)将看起来像这样:[图片链接] 这里没有任何魔法 - 只需查看您的生成控制器,每个方法都有预生成的注解,例如。

<?php
namespace Modules\V3\Http\Controllers;

class ArticleController extends DefaultController 
{
    // >>>props>>>
    // <<<props<<<
    // >>>methods>>>
    /**
    * @OA\Get(
    *     path="/v3/article",
    *     summary="Get Articles ",
    *     tags={"ArticleController"},
    *     @OA\Parameter(
    *         in="query",
    *         name="include",
    *         required=false,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="query",
    *         name="page",
    *         required=false,
    *         @OA\Schema(
    *             type="integer",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="query",
    *         name="limit",
    *         required=false,
    *         @OA\Schema(
    *             type="integer",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="query",
    *         name="sort",
    *         required=false,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="query",
    *         name="data",
    *         required=false,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="query",
    *         name="filter",
    *         required=false,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="query",
    *         name="order_by",
    *         required=false,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Response(
    *         response="200",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Get(
    *     path="/v3/article/{id}",
    *     summary="Get Article",
    *     tags={"ArticleController"},
    *     @OA\Parameter(
    *         in="query",
    *         name="include",
    *         required=false,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="query",
    *         name="data",
    *         required=false,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Response(
    *         response="200",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Post(
    *     path="/v3/article",
    *     summary="Create Article",
    *     tags={"ArticleController"},
    *     @OA\Response(
    *         response="201",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Patch(
    *     path="/v3/article/{id}",
    *     summary="Update Article",
    *     tags={"ArticleController"},
    *     @OA\Response(
    *         response="200",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Delete(
    *     path="/v3/article/{id}",
    *     summary="Delete Article",
    *     tags={"ArticleController"},
    *     @OA\Response(
    *         response="204",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Get(
    *     path="/v3/article/{id}/{related}",
    *     summary="Get Article related objects",
    *     tags={"ArticleController"},
    *     @OA\Parameter(
    *         in="query",
    *         name="data",
    *         required=false,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="path",
    *         name="id",
    *         required=true,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="path",
    *         name="related",
    *         required=true,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Response(
    *         response="200",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Get(
    *     path="/v3/article/{id}/relationships/{relations}",
    *     summary="Get Article relations objects",
    *     tags={"ArticleController"},
    *     @OA\Parameter(
    *         in="query",
    *         name="data",
    *         required=false,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="path",
    *         name="id",
    *         required=true,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="path",
    *         name="relations",
    *         required=true,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Response(
    *         response="200",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Post(
    *     path="/v3/article/{id}/relationships/{relations}",
    *     summary="Create Article relation object",
    *     tags={"ArticleController"},
    *     @OA\Parameter(
    *         in="path",
    *         name="id",
    *         required=true,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="path",
    *         name="relations",
    *         required=true,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Response(
    *         response="201",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Patch(
    *     path="/v3/article/{id}/relationships/{relations}",
    *     summary="Update Article relation object",
    *     tags={"ArticleController"},
    *     @OA\Parameter(
    *         in="path",
    *         name="id",
    *         required=true,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="path",
    *         name="relations",
    *         required=true,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Response(
    *         response="200",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Delete(
    *     path="/v3/article/{id}/relationships/{relations}",
    *     summary="Delete Article relation object",
    *     tags={"ArticleController"},
    *     @OA\Parameter(
    *         in="path",
    *         name="id",
    *         required=true,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Parameter(
    *         in="path",
    *         name="relations",
    *         required=true,
    *         @OA\Schema(
    *             type="string",
    *         ),
    *     ),
    *     @OA\Response(
    *         response="204",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Post(
    *     path="/v3/article/bulk",
    *     summary="Create Article bulk",
    *     tags={"ArticleController"},
    *     @OA\Response(
    *         response="201",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Patch(
    *     path="/v3/article/bulk",
    *     summary="Update Article bulk",
    *     tags={"ArticleController"},
    *     @OA\Response(
    *         response="200",
    *         description="",
    *     ),
    * )
    */

    /**
    * @OA\Delete(
    *     path="/v3/article/bulk",
    *     summary="Delete Article bulk",
    *     tags={"ArticleController"},
    *     @OA\Response(
    *         response="204",
    *         description="",
    *     ),
    * )
    */

    // <<<methods<<<
}

生成文件内容

模块配置

<?php
return [
    'name' => 'V1',
    'query_params'=> [
        'limit' => 15,
        'sort' => 'desc',
        'access_token' => 'db7329d5a3f381875ea6ce7e28fe1ea536d0acaf',
    ],
    'trees'=> [
        'menu' => true,
    ],
    'jwt'=> [
        'enabled' => true,
        'table' => 'user',
        'activate' => 30,
        'expires' => 3600,
    ],
    'state_machine'=> [
        'article'=> [
            'status'=> [
                'enabled' => true,
                'states'=> [
                    'initial' => ['draft'],
                    'draft' => ['published'],
                    'published' => ['archived', 'postponed'],
                    'postponed' => ['published', 'archived'],
                    'archived' => [''],
                ],
            ],
        ],
    ],
    'spell_check'=> [
        'article'=> [
            'description'=> [
                'enabled' => true,
                'language' => 'en',
            ],
        ],
    ],
    'bit_mask'=> [
        'user'=> [
            'permissions'=> [
                'enabled' => true,
                'hide_mask' => true,
                'flags'=> [
                'publisher' => 1,
                'editor' => 2,
                'manager' => 4,
                'photo_reporter' => 8,
                'admin' => 16,
                ],
            ],
        ],
    ],
    'cache'=> [
        'tag'=> [
            'enabled' => true,
            'stampede_xfetch' => false,
            'stampede_beta' => 1.1,
            'ttl' => 3600,
        ],
        'article'=> [
            'enabled' => true,
            'stampede_xfetch' => true,
            'stampede_beta' => 1.5,
            'ttl' => 300,
        ],
    ],    
];

控制器

实体控制器示例

<?php
namespace Modules\V1\Http\Controllers;

class ArticleController extends DefaultController 
{
}

默认情况下,每个控制器都使用任何 GET - index/view、POST - create、PATCH - update、DELETE - delete 方法。因此,您在这里不需要实现任何特殊的内容。

默认控制器示例

<?php
namespace Modules\V1\Http\Controllers;

use SoliDry\Extension\BaseController;

class DefaultController extends BaseController 
{
}

为了提供针对所有控制器的一定逻辑的开发者(用户空间)实现。

表单请求

Validation BaseFormRequest 示例

<?php
namespace Modules\V2\Http\Requests;

use SoliDry\Extension\BaseFormRequest;

class ArticleFormRequest extends BaseFormRequest 
{
    // >>>props>>>
    public $id = null;
    // Attributes
    public $title = null;
    public $description = null;
    public $url = null;
    public $show_in_top = null;
    public $status = null;
    public $topic_id = null;
    public $rate = null;
    public $date_posted = null;
    public $time_to_live = null;
    public $deleted_at = null;
    // <<<props<<<

    // >>>methods>>>
    public function authorize(): bool 
    {
        return true;
    }

    public function rules(): array 
    {
        return [
            'title' => 'required|string|min:16|max:256|',
            'description' => 'required|string|min:32|max:1024|',
            'url' => 'string|min:16|max:255|',
                // Show at the top of main page
            'show_in_top' => 'boolean',
                // The state of an article
            'status' => 'in:draft,published,postponed,archived|',
                // ManyToOne Topic relationship
            'topic_id' => 'required|integer|min:1|max:6|',
            'rate' => '|min:3|max:9|',
            'date_posted' => '',
            'time_to_live' => '',
            'deleted_at' => '',
        ];
    }

    public function relations(): array 
    {
        return [
            'tag',
            'topic',
        ];
    }
    // <<<methods<<<
}

模型

BaseModel 示例

<?php
namespace Modules\V2\Entities;

use Illuminate\Database\Eloquent\SoftDeletes;
use SoliDry\Extension\BaseModel;

class Article extends BaseModel 
{
    use SoftDeletes;

    // >>>props>>>
    protected $dates = ['deleted_at'];
    protected $primaryKey = 'id';
    protected $table = 'article';
    public $timestamps = false;
    public $incrementing = false;
    // <<<props<<<
    // >>>methods>>>

    public function tag() 
    {
        return $this->belongsToMany(Tag::class, 'tag_article');
    }
    public function topic() 
    {
        return $this->belongsTo(Topic::class);
    }
    // <<<methods<<<
}

路由

路由将创建在 /Modules/{ModuleName}/Routes/api.php 文件中,为 yaml 中定义的每个实体创建

// >>>routes>>>
// Article routes
Route::group(['prefix' => 'v2', 'namespace' => 'Modules\\V2\\Http\\Controllers'], function()
{
    // bulk routes
    Route::post('/article/bulk', 'ArticleController@createBulk');
    Route::patch('/article/bulk', 'ArticleController@updateBulk');
    Route::delete('/article/bulk', 'ArticleController@deleteBulk');
    // basic routes
    Route::get('/article', 'ArticleController@index');
    Route::get('/article/{id}', 'ArticleController@view');
    Route::post('/article', 'ArticleController@create');
    Route::patch('/article/{id}', 'ArticleController@update');
    Route::delete('/article/{id}', 'ArticleController@delete');
    // relation routes
    Route::get('/article/relationships/{relations}', 'ArticleController@relations');
    Route::post('/article/relationships/{relations}', 'ArticleController@createRelations');
    Route::patch('/article/relationships/{relations}', 'ArticleController@updateRelations');
    Route::delete('/article/relationships/{relations}', 'ArticleController@deleteRelations');
});
// <<<routes<<<

如您所注意到的,有支持关系 API 调用和批量扩展查询。

迁移

生成的迁移将类似于 Laravel 中的标准迁移

<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateArticleTable extends Migration 
{
    public function up() 
    {
        Schema::create('article', function(Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('title', 256);
            $table->index('title', 'idx_title');
            $table->string('description', 1024);
            $table->string('url', 255);
            $table->unique('url', 'idx_url');
            // Show at the top of main page
            $table->unsignedTinyInteger('show_in_top');
            $table->enum('status', ["draft","published","postponed","archived"]);
            // ManyToOne Topic relationship
            $table->unsignedInteger('topic_id');
            $table->foreign('topic_id', 'idx_fk_topic_id')->references('id')->on('topic')->onDelete('cascade')->onUpdate('cascade');
            $table->timestamps();
        });
    }

    public function down() 
    {
        Schema::dropIfExists('article');
    }

}

注意,您可以通过minLength/maxLength和minimum/maximum分别设置varchar、integer类型的任何范围。例如,可以将integer设置为无符号smallint,其中minimum: 1(任何大于0的数字)和maximum: 2(任何小于等于3的数字,以适应smallint数据库类型范围)。

如果使用double/float类型,则最大值用于显示长度(或M),最小值用于SQL中的精度(或D),例如:DOUBLE(M, D)

特定模块的所有迁移都将放在Modules/{ModuleName}/Database/Migrations/

要执行它们所有 - 运行:php artisan module:migrate

此外,值得注意的是 - Laravel使用table_id约定通过外键链接表。因此,您可以选择默认方式 - 在yaml中添加一个与表名匹配的id(就像在示例中一样:topic_id -> 在article表中对应于topic表的id,参见OAS类型和声明中的ArticleAttributes)或创建自己的外键并将其添加到生成的BaseModel实体中的hasMany/belongsTo -> $foreignKey参数。

此外,要指定特定列的索引,您可以添加一个如下的facets属性

    # regular index
    facets:
      index:
        idx_title: index            
        
    # unique key    
    facets:
      index:
        idx_url: unique             
        
    # foreign key
    facets:
      index:
        idx_fk_topic_id: foreign
        references: id
        on: topic
        onDelete: cascade
        onUpdate: cascade

到现有列。

然而,有些情况下,您必须创建组合索引

      last_name:
        required: false
        type: string
        minLength: 16
        maxLength: 256
        facets:
          composite_index:
            index: ['first_name', 'last_name'] # can be unique, primary

外键的一个示例可能如下

    facets:
      composite_index:
        foreign: ['first_column', 'second_column'] 
        references: ['first_column', 'second_column']
        on: first_second
        onDelete: cascade
        onUpdate: cascade        

测试

为了提供一个方便的集成/功能测试的方式,可以通过提供--tests命令选项生成测试,例如

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

在命令输出中,您将看到以下文件已被创建

tests/functional/ArticleCest.php created
...
tests/functional/TagCest.php created

有关如何在Laravel中设置功能测试环境的更多信息 - 请参阅https://codeception.com/for/laravel

关系特定品质

为了让生成器知道要应用哪种特定关系(例如:ManyToMany、OneToMany、OneToOne),在实体中设置relationships属性,例如 - 例如,让我们看看如何设置Article和Tag实体之间的ManyToOne关系。

定义Article,如下所示,具有关系

relationships:
  type: TagRelationships[]

和Tag,如下所示,具有关系

relationships:
  type: ArticleRelationships

这样,您告诉生成器:“在Article和Tag之间创建从Article到Tag的OneToMany关系”这个想法适用于您需要的任何关系 - 例如,ManyToMany:TagRelationships[] -> ArticleRelationships[],OneToOne:TagRelationships -> ArticleRelationships

您还可以将多个关系绑定到一个实体,例如 - 您有一个Article实体,它必须绑定到TagRelationships和TopicRelationships,这可以这样做

relationships:
    type: TagRelationships[] | TopicRelationships

或反之亦然

relationships:
    type: TopicRelationships | TagRelationships[]

生成器将独立检测实体之间的所有关系。

查询参数

您可能想要使用额外的查询参数来获取包含项和/或分页,例如

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

您可能不想拖动所有属性/字段

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

注意:数据数组项必须用双引号设置。

或者您可能想要按不同的方向对多个列进行ORDER BY排序

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

此外,您还可以这样过滤结果

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

这些数组将放入Laravel的where子句,并相应地受到参数绑定的保护。

类似于:v1、v2的动态模块名称将在运行时作为config/module.php文件中的数组最后一个元素被提取,如果您由于某种奇怪的原因想使用以前的模块,只需将之前注册的模块之一设置为数组最后一个元素即可。

自动生成的config/module.php示例

<?php
return [
    'modules'=> [
        'v1',
    ]
];

为了在运行时获取配置参数,生成器将在Modules/{ModuleName}/Config/config.php文件中创建内容

<?php
return [
    'name'=>'V1',
    'query_params'=> [
        // default settings
        'limit' => 15,
        'sort' => 'desc',
        // access token to check via global FormRequest
        'access_token' => 'db7329d5a3f381875ea6ce7e28fe1ea536d0acaf',
    ],
];

批量扩展

可以通过向表示资源集合的URL发送POST请求来创建多个资源。

POST /photos
Content-Type: application/vnd.api+json; ext=bulk
Accept: application/vnd.api+json; ext=bulk

{
  "data": [{
    "type": "photos",
    "title": "Ember Hamster",
    "src": "http://example.com/images/productivity.png"
  }, {
    "type": "photos",
    "title": "Mustaches on a Stick",
    "src": "http://example.com/images/mustaches.png"
  }]
}

可以通过向表示它们所属的资源集合的URL发送PATCH请求来更新多个资源。

PATCH /articles
Content-Type: application/vnd.api+json; ext=bulk
Accept: application/vnd.api+json; ext=bulk

{
  "data": [{
    "type": "articles",
    "id": "1",
    "title": "To TDD or Not"
  }, {
    "type": "articles",
    "id": "2",
    "title": "To cache or not"
  }]
}

可以通过向表示它们所属的资源集合的URL发送DELETE请求来删除多个资源。

DELETE /articles
Content-Type: application/vnd.api+json; ext=bulk
Accept: application/vnd.api+json; ext=bulk

{
  "data": [
    { "type": "articles", "id": "1" },
    { "type": "articles", "id": "2" }
  ]
}

请求完全成功或失败(在一个“事务”中)。

因此,任何涉及多个操作请求只有在所有操作都成功执行时才会成功。如果任何单个操作失败,则请求不会更改服务器的状态。

安全性

静态访问令牌

QueryParams 中,您可以声明 access_token 属性,该属性将被放置到 Modules/{ModuleName}/Config/config.php。生成器将创建 Modules\{ModuleName}\Http\Requests\ApiAccessToken.php FormRequest。

要激活在每个请求上执行此检查 - 将 ApiAccessToken FormRequest 添加到 app/Http/Kernel.php,例如:

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \Modules\V2\Http\Requests\ApiAccessToken::class,
    ];

生成的配置部分

    'query_params'=> [
        'limit' => 15,
        'sort' => 'desc',
        'access_token' => 'db7329d5a3f381875ea6ce7e28fe1ea536d0acaf',
    ],

JWT (Json Web Token)

要支持 JWT 检查,您需要向任何用户、员工、客户等表中添加 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。

默认值应精确等于 ' ' - 包含空格的空字符串。

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 服务器实例,如果它们具有集群或 replica-set。

另一个选项是通过将相应的 stampede 属性应用于 cache 实体来使您的服务对 Cache Stampede(或狗群)具有抵抗力(或狗群):stampede_xfetch 启用 xfetch 实现,并且 stampede_beta 应该在 0.5<=beta<=2.0 之间(其中 > 1.0 提前安排重新计算,< 1.0 晚些时候安排重新计算),在这种情况下还需要 ttl 属性。

生成的配置输出将类似于

'cache'=> [
    'article'=> [
        'enabled' => true,
        'stampede_xfetch' => true,
        'stampede_beta' => 1.5,
        'ttl' => 300,
    ],
],

所有特定设置,包括主机/端口/密码、复制、集群等都可以通过 Laravel 标准的 Redis 缓存设置轻松配置。有关更多信息,请参阅此处 - Redis Laravel 配置

配置缓存设置后 - 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();
            // ...
        });
    }
    // ...
}

它将自动应用于删除请求,并且模型不会在 view/index 中被收集。

关闭 JSON API 支持

如果您愿意禁用 json api 规范映射到 Laravel 应用程序中(例如 - 您需要在 laravel-module 中生成 MVC 结构并创建自己的 json 架构,或任何其他输出格式),只需在 DefaultController 中将 $jsonApi 属性设置为 false。

<?php
namespace Modules\V1\Http\Controllers;

use SoliDry\Extension\BaseController;

class DefaultController extends BaseController 
{
    protected $jsonApi = false;
}

因为此类由所有控制器继承 - 您无需在每个控制器类中添加此属性。默认情况下,JSON API 已开启。

树结构

您可以通过将其声明为 Trees 自定义类型来轻松构建树结构

  Trees:
    type: object
    properties:
      menu:
        type: boolean
        default: true
      catalog:
        type: boolean
        default: false

并将 parent_id 添加到目标表中,例如:

  MenuAttributes:
    type: object
    properties:
      title:
        required: true
        type: string
      rfc:
        type: string
        default: /
      parent_id:
        description: mandatory field for building trees
        type: integer
        minimum: 9
        maximum: 10
        default: 0

整个树将被放置在 meta json-api 根元素中,而所有父元素(存储为 parent_id=0)将位于数据根元素中。这样做是为了保持稳定的 json-api 结构及其关系。

元数据响应示例

  "meta": {
    "menu_tree": [
      {
        "id": 1,
        "title": "ttl1",
        "rfc": "/",
        "parent_id": 0,
        "created_at": null,
        "updated_at": null,
        "children": [
          {
            "id": 3,
            "title": "ttl21",
            "rfc": "/",
            "parent_id": 1,
            "created_at": null,
            "updated_at": null,
            "children": []
          },
          {
            "id": 2,
            "title": "ttl2",
            "rfc": "/",
            "parent_id": 1,
            "created_at": null,
            "updated_at": null,
            "children": [
              {
                "id": 4,
                "title": "ttl3",
                "rfc": "/",
                "parent_id": 2,
                "created_at": null,
                "updated_at": null,
                "children": []
              }
            ]
          }
        ]
      }
    ]
  }

子元素位于每个父元素的 children 属性数组中,如果没有子元素,则为空。

要获取最顶层祖先的子树 - 简单地执行对项目的 GET 请求,例如:http://example.com/api/v1/menu/1。请参阅 Postman 中的真实世界示例。

有限状态机

要将有限状态机添加到实体(表)的字段(列)中 - 将定义添加到您的 OAS 文件中,如下所示:

      status:
        description: The state of an article
        enum: ["draft", "published", "postponed", "archived"]
        facets:
          state_machine:
            initial: ['draft']
            draft: ['published']
            published: ['archived', 'postponed']
            postponed: ['published', 'archived']
            archived: []

state_machine 声明中唯一必需的特定项是状态机的 initial 值。

生成过程通过后,您将在 config.php 中获得以下内容:

    'state_machine'=> [
        'article'=> [
            'status'=> [
                'enabled'=>true,
                'states'=> [
                    'initial' => ['draft'],
                    'draft' => ['published'],
                    'published' => ['archived', 'postponed'],
                    'postponed' => ['published', 'archived'],
                    'archived' => [''],
                ],
            ],
        ],
    ],

它将分别处理 POSTPATCH 请求。您可以通过将 enabled 设置为 false 来轻松禁用状态机。您可以在不同的表中添加状态机。

拼写检查

安装

由强大的 Linux 库 GNU aspell 和其作为 PHP 扩展的词典提供的拼写检查功能。

在 Linux(例如 Ubuntu)上安装扩展

apt-get install php-pspell

要安装额外的语言 db 运行

apt-get install aspell-fr

使用方法

您可能希望将拼写检查设置在特定的字段/列上

      description:
        required: true
        type: string
        minLength: 32
        maxLength: 1024
        facets:
          spell_check: true
          spell_language: en

生成器输出在 Modules/{VersionName}/Config/config.php 中看起来如下:

    'spell_check'=> [
        'article'=> [
            'description'=> [
                'enabled'=>true,
                'language' => 'en',
            ],
        ],
    ],

与其他设置一样,可以通过将 enabled 设置为 false 来禁用拼写检查。如果没有预先设置有关语言的信息,则默认使用 en

在 POST/PATCH(创建/更新)方法的响应中,您将获得带有填充的失败检查数组的 meta 内容。

{
  "data": {
    "type": "article",
    "id": "21",
    "attributes": {
      "title": "Quick brown fox",
      "description": "The quick brovn fox jumped ower the lazy dogg",
      "url": "http://example.com/articles/21/tags",
      "show_in_top": "0",
      "status": "draft"
    },
    "links": {
      "self": "example.com/article/21"
    }
  },
  "meta": {
    "spell_check": {
      "description": [
        "brovn",
        "ower",
        "dogg"
      ]
    }
  }
}

位掩码

要使用带有自动标志碎片化/重新组合的位掩码,您可以将额外的方面定义为整数字段,如下所示:

  permissions:
    type: integer
    required: false
    maximum: 20
    facets:
      bit_mask:
        publisher: 1
        editor: 2
        manager: 4
        photo_reporter: 8
        admin: 16

因此,将生成并使用在请求运行时处理数据的 bit_mask 配置实体。

生成的配置片段

'bit_mask'=> [
    'user'=> [
        'permissions'=> [
            'enabled' => true,
            'flags'=> [
                'publisher' => 1,
                'editor' => 2,
                'manager' => 4,
                'photo_reporter' => 8,
                'admin' => 16,
            ],
        ],
    ],
],

请求/响应将是

{
  "data": {
    "type":"user",
    "attributes": {
        "publisher": false,
        "editor": true,
        "manager": false,
        "photo_reporter": true,
        "admin": true    	
    }
  }
}
{
  "data": {
    "type": "user",
    "id": "1",
    "attributes": {
        "first_name": "Alice",
        "last_name": "Hacker",
        "permissions": 26,
        "publisher": false,
        "editor": true,
        "manager": false,
        "photo_reporter": true,
        "admin": true,

请记住,您可以通过隐藏索引/视图 GET 请求中的 permissions 字段来执行操作。

自定义SQL

如果出于任何原因您需要使用自定义 SQL 查询,只需在 Modules/V1/Config/config.php 中定义它。

    'custom_sql'    => [
        'article' => [
            'enabled' => true,
            'query'   => 'SELECT id, title FROM article a INNER JOIN tag_article ta ON ta.article_id=a.id 
                          WHERE ta.tag_id IN (
                          SELECT id FROM tag WHERE CHAR_LENGTH(title) > :tag_len
                          ) ORDER BY a.id DESC',
            'bindings' => [
                'tag_len' => 5,
            ]
        ],
    ],  

如您所见,对于所需实体有 querybindings(已传递安全参数绑定的值)和enabled 参数。只有对于 index API 方法,才会执行自定义 SQL 查询,因此如果您需要例如 deleteupdate 特定额外的行,请使用之前选择的 ID 调用这些方法。

别忘了添加 Laravel 特定的 $fillable$guarded 数组,以便填充对象(mass-assignment 规则),例如:

    protected $fillable = [
        'id',
        'title'
    ];

注意:由于 json-api 序列化器,您需要一个 id 字段。

自定义业务逻辑

您可以为需要的任何业务逻辑添加代码,在预生成的控制器中添加自定义代码的最佳位置是,例如,为 ArticleController 中的字段添加特定的清理程序以及修改后的输出,您可以像这样覆盖 create 方法

<?php
namespace Modules\V1\Http\Controllers;

use Illuminate\Http\Request;

class ArticleController extends DefaultController
{
    public function create(Request $request)
    {
        // any business logic here for input pre-processing data
        parent::create($request);
        // any business logic here for output pre-processing data
    }
}

可能存在需要在该类型索引/查看/创建/更新/删除的特定方法或初始化逻辑中添加回退的情况,这可以通过将代码放置在 DefaultController 中轻松实现,就像为任何其他控制器一样。专门为这些目的构建的继承模型将优雅地执行任何操作/之后等。例如

<?php
namespace Modules\V1\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use SoliDry\Extension\BaseController;

class DefaultController extends BaseController 
{
    public function __construct(Route $route)
    {
        // specific code before init 
        parent::__construct($route);
        // specific code after init 
    }
    
    public function index(Request $request)
    {
        // specific code before index execution
        parent::index($request); 
        // specific code after index execution
    }
}

如您所注意到的,您可以访问 Route 和 Request 属性。
在下一章中,您将了解如何将自定义代码放置在 Models/FormRequest 中,以保护它免受代码生成覆盖。

重新生成

根据生成的类型历史和当前 OpenApi 文档的状态重新生成您的代码是一个重要功能,这可以通过运行以下命令轻松实现

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

此命令将合并来自 .gen 目录的最后状态/快照和当前文档(在这种情况下来自 oas/openapi.yaml),然后为模型和 FormRequest 创建文件,将其与用户添加的内容合并,这些内容在生成的属性和方法之间。此外,它将在新创建的迁移文件中添加新的列及其索引。

控制器状态

<?php
namespace Modules\V2\Http\Controllers;

class ArticleController extends DefaultController 
{
    private $prop = 'foo';

    // >>>props>>>
    // <<<props<<<
    
    public function myMethod()
    {
        return true;
    }
}

重新生成的 FormRequest 示例

<?php
namespace Modules\V2\Http\Requests;

use SoliDry\Extension\BaseFormRequest;

class TagFormRequest extends BaseFormRequest
{
    public $userPropOne = true;
    // >>>props>>>
    public $id = null;
    // Attributes
    public $title = null;
    // <<<props<<<
    public $userPropTwo = 123;


    public function userDefinedMethod(): int
    {
        return 1;
    }

    // >>>methods>>>
    public function authorize(): bool 
    {
        return true;
    }

    public function rules(): array 
    {
        return [
            "title" => "string|required|min:3",
        ];
    }

    public function relations(): array 
    {
        return [
            "article",
        ];
    }
    // <<<methods<<<

    public function anotherUserDefinedMethod(): bool
    {
        return false;
    }
}

如您所见,所有用户内容都得到了保留,并与重新生成的合并。当存在 --regenerate 选项时,自定义业务逻辑内容会保存其状态,无论是带有还是不带其他选项。

对于 Eloquent 模型也是一样。

<?php
namespace Modules\V1\Entities;

use SoliDry\Extension\BaseModel;

class Article extends BaseModel 
{
    public $userPropOne = true;
    // >>>props>>>
    protected $primaryKey = "id";
    protected $table = "article";
    public $timestamps = false;
    // <<<props<<<
    public $userPropTwo = 123;

    public function userDefinedMethod(): int
    {
        return 1;
    }

    // >>>methods>>>

    public function tag() 
    {
        return $this->belongsToMany(Tag::class, 'tag_article');
    }
    public function topic() 
    {
        return $this->belongsTo(Topic::class);
    }
    // <<<methods<<<

    public function anotherUserDefinedMethod(): bool
    {
        return false;
    }
} 

重新生成的迁移示例

<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddColumnLastNameToUser extends Migration 
{
    public function up() 
    {
        Schema::table('user', function(Blueprint $table) {
            $table->string('last_name', 256);
            $table->index(['first_name', 'last_name']);
            $table->unsignedBigInteger('permissions');
        });
    }

    public function down() 
    {
        Schema::table('user', function(Blueprint $table) {
            $table->dropColumn('last_name');
            $table->dropColumn('permissions');
        });
    }

}

如果您不想在每次运行时保存历史记录,请添加 --no-history 选项。

您还可以做更多关于回滚历史记录的事情

  • 通过传递选项例如 --merge=9,生成器将回退 9 步
  • --merge="2017-07-29 11:35:32" 生成器通过时间在历史记录中获取到具体的文件

尽管如此,如果您需要完全回滚系统的状态,请使用与合并相同的键的 --rollback 选项。

==== Infection 代码覆盖率 ====

指标

 Mutation Score Indicator (MSI): 81%
 Mutation Code Coverage: 86%
 Covered Code MSI: 93%

==========

HTTP 请求/响应示例可以在 WiKi 页面找到 - https://github.com/SoliDry/api-generator/wiki

带有生成文件的 Laravel 项目示例可以在这里找到 - https://github.com/SoliDry/laravel-api

要深入了解 Open API 规范 - https://swagger.org.cn/specification/

要深入了解 JSON-API 规范 - http://jsonapi.org/format/ JSON-API 支持由 Fractal 包提供,尤其是对于输出,由 Fractal 包提供 - http://fractal.thephpleague.com/

编码愉快 ;-)

PS 此存储库的目的是防止重复做同样的事情,期望不同的结果。(感谢阿尔伯特·爱因斯坦)

支持者的感激之情

JetBrains logo