rollun-com/rollun-openapi

rollun骨架与openapi生成代码之间的桥梁


README

该库包含一个PHP脚本,用于从openapi规范中生成客户端或服务器端的代码。该脚本通过openapi-generator工具运行。

Openapi规范是一个特定结构的文档,用于描述HTTP API:URL路径、请求/响应数据格式、认证等。更多详情可以在这里阅读这里

基于此规范可以生成客户端或服务器端的代码。

对于客户端部分,生成的API客户端可以用来向该API发送请求。

生成的服务器端代码将包含需要实现的控制器模板。

客户端和服务器都将包含验证以及数据对象与HTTP请求/响应之间的序列化和反序列化。

因此,您需要在项目的composer.json文件中包含此库,因为它包含用于运行生成代码所需的类。

快速开始

什么是openapi

Openapi是一种文件格式,用于描述http API:请求/响应格式和端点。

一个简单的openapi文件示例,描述一个返回用户名数组的API端点/users,如下所示

openapi: 3.0.0
info:
  title: Sample API
  description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
  version: 0.1.9
servers:
  - url: http://api.example.com/v1
    description: Optional server description, e.g. Main (production) server
  - url: http://staging-api.example.com
    description: Optional server description, e.g. Internal staging server for testing
paths:
  /users:
    get:
      summary: Returns a list of users.
      description: Optional extended description in CommonMark or HTML.
      responses:
        '200':    # status code
          description: A JSON array of user names
          content:
            application/json:
              schema: 
                type: array
                items: 
                  type: string

更多关于openapi规范格式的信息可以在swagger.io文档中找到

Openapi生成器

此文件可以用各种工具使用,例如swagger ui生成具有交互式文档的界面。

Swagger ui

存在另一个工具openapi-generator,它可以根据openapi文档(规范)生成代码。此代码可能包含请求/响应对象、它们的验证、不同媒体类型的序列化/反序列化、测试等。

代码生成器可以分为两类:服务器端和客户端。它们根据您的程序是作为客户端还是服务器,或者两者都是(代理服务器)来使用。

服务器端生成器生成控制器模板,程序员需要实现以获得可接收请求的有效服务器。

客户端生成器生成代码,允许您方便地向服务器发送请求并处理响应。

实际上,我们的生成器由两个生成器组成:客户端和服务器端,分别通过generate:clientgenerate:server命令启动。

编写规范

通常规范由实现API的人或需要API的人编写。

为了编写规范,我们使用部署在我们的服务器上的swagger-editor,可通过以下链接访问:swagger-editor.rollun.net。此编辑器与github上的rollun-com/openapi-manifests存储库集成,其中存储所有我们的规范,并允许在存储库中打开或保存规范。请确保将规范保存在此存储库中,因为它可能被我们的程序用于搜索规范。有关保存规范的说明可以在rollun-com/openapi-manifests存储库中找到。

为了简化规范的编写,我们提供了一个名为skeleton的规范模板。您可以通过在swagger-editor中点击“打开规范”按钮找到它。

Swagger editor open manifests button

然后,将打开一个选择规范的窗口,其中您可以找到skeleton规范。

Swagger editor choosing skeleton manifests

关于创建清单的规则,可以阅读manifests.md

启动生成器

将库安装到您的项目(微服务)中

composer require rollun-com/rollun-openapi

重要 在 composer 处理完毕后,请检查 /config/config.php 文件中是否存在配置提供者 \OpenAPI\ConfigProvider::class,以及它是否在 \Zend\Expressive\Router\FastRouteRouter\ConfigProvider::class\Mezzio\Router\FastRouteRouter\ConfigProvider::class 之后加载,否则将无法工作。

之后,您需要通过 php 运行该库中的脚本 ./bin/openapi-generator,使用命令 generate:server 生成服务器端代码,如果需要生成客户端代码,则使用 generate:client。脚本将自动询问清单路径,但也可以通过参数 -m 直接指定。

如果通过 composer 将此库安装到您的项目中,则该脚本将位于 ./vendor/bin/openapi-generator 路径,而不是 ./bin/openapi-generator

重要 为避免错误,此脚本必须在已安装 openapi-generator 的环境中运行。这可以通过以下两种方式实现

  1. 按照其网站上的说明,在本地系统中安装 openapi-generator
  2. 使用 docker,并在容器内运行此脚本。

通过 docker 启动生成

docker run --rm \
  -v $PWD:/var/www/app \
  maxrollundev/php-openapi-generator:8.0 \
  php vendor/bin/openapi-generator generate:server \
  -m openapi.yaml

其中

  • -v $PWD:/var/www/app - 创建从主机当前目录到容器 /var/www/app 目录的卷(此路径很方便使用,因为对于该容器来说,它默认是工作目录)
  • maxrollundev/php-openapi-generator:8.0 - 容器名称(8.0 - php 版本)
  • php vendor/bin/openapi-generator generate:server - 直接运行生成器脚本(对于客户端,将 generate:server 替换为 generate:client
  • -m openapi.yaml - 清单路径(可以是 URL)

如果您在项目中使用 docker-compose,则可以在 services 部分添加生成器服务

services:
  # ...
  
  php-openapi-generator:
    image: maxrollundev/php-openapi-generator:8.0
    volumes:
      - ./:/var/www/app

然后可以通过以下方式启动生成器

docker-compose run --rm php-openapi-generator \
  php vendor/bin/openapi-generator generate:server \
  -m openapi.yaml

不使用 docker 启动生成

  1. 安装 openapi-generator 版本低于 5(不包括 5)。要进行检查,请执行命令

    openapi-generator version,如果已安装 openapi-generator,您将看到生成器的版本。

    生成器版本必须低于第五版。 这是因为在第五版中 移除了 我们使用的生成器,它已被重命名并改为使用 Laminas 代替 Zend。

  2. 要生成代码,请执行命令

    php vendor/bin/openapi-generator generate:serverphp vendor/bin/openapi-generator generate:client

    该命令支持参数。参数以 --name=value 的形式传递。目前已实现通过路径或 URL 指定清单(manifest)参数。例如

    php vendor/bin/openapi-generator generate:client --manifest=openapy.yaml

生成后的设置

必须将生成的类添加到 composer 的自动加载器中。

"autoload": {
  "psr-4": {
    "SomeModule\\": "src/SomeModule/src/"
  }
},

其中,SomeModule 是清单的标题

如果出现错误

  1. 请检查容器中是否存在 rollun\logger\LifeCycleToken

    在容器中,此名称下应存在一个包含当前应用程序生命周期标识符的字符串。

    建议的方法是安装 rollun-com/rollun-logger 库。该库包含 LifeCycleToken。有关如何在容器中安装它的说明,请参阅库的文档

使用生成的服务器

服务器生成器生成需要程序员实现的控制器模板。控制器模板位于路径 src/{ManifestTitle}/src/OpenaAPI/{ManifestVersion}/Server/Rest。例如 User.php(manifest)openapi.yaml

<?php

namespace HelloUser\OpenAPI\V1\Server\Rest;

use Articus\DataTransfer\Service as DataTransferService;
use OpenAPI\Server\Rest\Base7Abstract;
use Psr\Log\LoggerInterface;
use rollun\dic\InsideConstruct;

/**
 * Class User
 */
class User extends Base7Abstract
{
	public const CONTROLLER_OBJECT = 'User1Controller';

	/** @var object */
	protected $controllerObject;

	/** @var LoggerInterface */
	protected $logger;

	/** @var DataTransferService */
	protected $dataTransfer;


	/**
	 * User constructor.
	 *
	 * @param mixed $controllerObject
	 * @param LoggerInterface|null logger
	 * @param DataTransferService|null dataTransfer
	 *
	 * @throws \ReflectionException
	 */
	public function __construct($controllerObject = null, $logger = null, $dataTransfer = null)
	{
		InsideConstruct::init([
		    'controllerObject' => static::CONTROLLER_OBJECT,
		    'logger' => LoggerInterface::class,
		    'dataTransfer' => DataTransferService::class
		]);
	}


	/**
	 * @inheritDoc
	 *
	 * @param \HelloUser\OpenAPI\V1\DTO\User $bodyData
	 */
	public function post($bodyData = null)
	{
		if (method_exists($this->controllerObject, 'post')) {
		    $bodyDataArray =$this->dataTransfer->extractFromTypedData($bodyData);

		    return $this->controllerObject->post($bodyDataArray);
		}

		throw new \Exception('Not implemented method');
	}


	/**
	 * @inheritDoc
	 */
	public function getById($id)
	{
		if (method_exists($this->controllerObject, 'getById')) {
		    return $this->controllerObject->getById($id);
		}

		throw new \Exception('Not implemented method');
	}
}

该类中的 postgetById 方法将在处理请求时被调用。如所示,该类将方法委托给某个 controllerObject。这个 controllerObject 是程序员需要创建的类,在其中编写所有必要方法(在本例中是 postgetById)的实现。例如 UserController。之后,将该类放置在依赖注入容器中,名称为常量 CONTROLLER_OBJECT,在本例中为 'User1Controller'。这最简单的方法是在配置中添加别名:示例

使用生成的客户端

客户端使用起来更简单,在生成后程序员不需要进行任何额外操作。类似于服务器,在目录 src/{ManifestTitle}/src/OpenaAPI/{ManifestVersion}/Client/Rest 中生成 API 客户端类,允许发送请求。

可以从容器中获取所需类,它已经准备好使用。

日期和时间格式

根据 OpenApi 规范,日期和时间应以格式 OpenApi 返回,格式为 RFC 3339, section 5.6。例如:"2017-07-21T17:32:28Z","2020-12-11T15:04:02.255Z"。需要注意的是,PHP 的 \DateTime::RFC3339 ('Y-m-d\TH:i:sP') 格式并不完全符合当前的 RFC 3339 格式,即 PHP 的 \DateTime::RFC3339 不允许字符串末尾有 Z,也没有支持可选的毫秒数。

到版本 6.1.0,不支持毫秒数,日期时间的验证格式为 'Y-m-d\TH:i:s\Z'.

从版本 6.1.0 开始,验证器被添加以完全符合规范 RFC 3339, section 5.6。但是,必须重新生成代码,以更改生成的 DTO 注解中的日期格式。

是否将库放入 require-dev 部分?

不是的,这个库中的几乎所有类都需要在生产环境中使用:路由、DTO 序列化等。生成代码使用的是 ./bin 目录中的命令、template 模板以及 nette/php-generator 包。目前,这些依赖项仍然保留在包中,并在生产中拉取。

服务器部分实现文档

服务器部分实现文档

客户端部分实现文档

客户端部分实现文档

主机切换

从版本 3.1.0 开始,Rest 类实现了接口 OpenAPI\Client\Rest\ClientInterface,该接口包含接口 OpenAPI\Client\Rest\HostSelectionInterface,允许切换主机。

为了使用这个功能,将 OpenAPI\Server\Rest\RestInterface 替换为 OpenAPI\Client\Rest\ClientInterface,它也包含 RestInterface,因此不会破坏任何东西。

<?php

namespace OpenAPI;

use HelloUser\OpenAPI\V1\Client\Rest\Hello;
use OpenAPI\Client\Rest\ClientInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use rollun\dic\InsideConstruct;use Zend\Diactoros\Response;

class TestHandler implements RequestHandlerInterface
{
    /**
     * @var ClientInterface|null
     */
    private $rest;

    public function __construct(ClientInterface $rest = null)
    {
        InsideConstruct::init(['rest' => Hello::class]);
    }

    public function handle(ServerRequestInterface $request): ResponseInterface
    {
        $this->rest->setHostIndex(1);
        $result = $this->rest->getById('10');
        return new Response\JsonResponse($result);
    }
}

composer install 停滞

可能是因为 "rollun-com/rollun-callback" 库的问题。尝试将其从 composer.json 中删除并重新运行安装。如果一切顺利,则通过 composer require 单独安装此库。

用户操作和端点

除了标准的 CRUD 操作外,现在还可以生成代码以发送客户端和服务器处理的用户方法,这些方法将按您的要求运行端点。

之前我们只能生成8个方法,因此也只能有8个路径,例如对于某个实体“订单”。

现在可以生成任何其他PHP方法和任何其他路径。例如,需要生成一个处理POST请求的路径为/order/{id}/user的方法。首先,需要将所需路径添加到清单中。接下来有两种方式可以将该路径绑定到您的PHP方法。

选项1. 您可以不指定任何其他内容(除了路径)。

paths:
  /order/{id}/user:
    post:
      tags:
        - Order
      parameters:
        - name: id
          in: path
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'

在这种情况下将生成OrderIdUserPost方法。也就是说,类中的方法(Handler、Rest、Api)将根据以下模式自动生成 - 路径 + http方法(全部使用camelCase)。在控制器中需要描述一个具有相同名称的方法。该方法将接受2个参数 - $id和$bodyData,因此控制器的大致样子如下

public function orderIdUserPost(string $id, User $bodyData)
{
    // code
}

选项2. 在清单中指定operationId。

如果您不想以这种方式生成方法,即如果方法需要设置一个逻辑上易于理解的名称,可以在清单中指定operationId。

paths:
  /order/{id}/user:
    post:
      tags:
        - Order
      operationId: setOrderUser
      parameters:
        - name: id
          in: path
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'

在这种情况下,所有方法都将命名为setOrderUser。相应地,在控制器中也需要描述一个具有相同名称的方法。

public function setOrderUser(string $id, User $bodyData)
{
    // code
}

关于路径长度的注意事项。建议路径不要超过两级。也就是说,允许路径/order/{id}/user,但不允许/order/{id}/user/roles等。

启动测试

使用docker

需要确保系统中已安装

  • docker
  • docker-compose
  • make工具

只需首先运行make up即可启动应用程序。之后,为了执行测试,请运行make test。要停止应用程序,请运行make down

不使用docker

可以通过composer test运行测试。在某些测试中会启动内置的php服务器并监听端口8081,因此非常重要确保该端口是可用的。

常见问题

  • 清单结构支持不足,无法进行无准备的开发。该包包含.yml示例。
  • 响应结构必须严格匹配清单结构。否则,服务器将返回发生意外错误