如何编写基于 Swagger-PHP 的 API 文档

分享 maxincai ⋅ 于 6个月前 ⋅ 最后回复由 to-xmc 2天前 ⋅ 3723 阅读

学习 Swagger 过程中发现大部份的文档都是基本 JSON 或 YAML 的,基于 PHP 注解语法的较少,所以将 Swagger从入门到精通 这本书上的 YAML 格式用 PHP 注解的方式写了一下,希望能对大家有帮助。
原 YAML 版本链接:https://www.gitbook.com/book/huangwenchao/swagger/details

前言

编写目的

本文介绍如何使用Swagger编写API文档。通过阅读本文,你可以:

  • 了解swagger是什么
  • 掌握使用swagger编写API文档的基本方法

第 1 章 简介

1.1 Swagger

Swagger(丝袜哥)给人第一印象就是【最(hen)流(niu)行(bai)】,不懂Swagger咱就out了。它的官方网站是http://swagger.io/。

Swagger是一个简单但功能强大的API表达工具。它具有地球上最大的API工具生态系统,数以千计的开发人员,使用几乎所有的现代编程语言,都在支持和使用Swagger。使用Swagger生成API,我们可以得到交互式文档,自动生成代码的SDK以及API的发现特性等。

现在,Swagger已经帮助包括Apigee, Getty图像, Intuit, LivingSocial, McKesson, 微软, Morningstar和PayPal等世界知名企业建立起了一套基于RESTful API的完美服务系统。

2.0版本已经发布,Swagger变得更加强大。值得感激的是,Swagger的源码100%开源在github。

1.2 OpenAPI 规范

OpenAPI规范是Linux基金会的一个项目,试图通过定义一种用来描述API格式或API定义的语言,来规范RESTful服务开发过程。OpenAPI规范帮助我们描述一个API的基本信息,比如:

  • 有关该API的一般性描述
  • 可用路径(/资源)
  • 在每个路径上的可用操作(获取/提交...)
  • 每个操作的输入/输出格式

目前V2.0版本的OpenAPI规范(也就是SwaggerV2.0规范)已经发布并开源在github上。该文档写的非常好,结构清晰,方便随时查阅。关于规范的学习和理解,本文最后还有个彩蛋。

1.3 为啥要使用 OpenAPI 规范?

  • OpenAPI 规范这类 API 定义语言能够帮助你更简单、快速的表述 API,尤其是在 API 的设计阶段作用特别突出
  • 根据 OpenAPI 规范编写的二进制文本文件,能够像代码一样用任何 VCS 工具管理起来
  • 一旦编写完成,API文档可以作为:
    • 需求和系统特性描述的根据
    • 前后台查询、讨论、自测的基础
    • 部分或者全部代码自动生成的根据
    • 其他重要的作用,比如开放平台开发者的手册...

1.4 如何编写 API 文档?

1.4.1 语言 JSON vs YAML

1.4.2 基于 PHP 注释的方式

第 2 章 从零开始

这一章主要介绍 API 的基本组成部分,包括提供给 API 消费者(所有可能访问API的个体,下简称“消费者”)的的不同 HTTP 请求方法、路径,请求和消息体中的参数,以及返回给消费者的不同 HTTP 状态及响应消息体。

<?php

/**
 * @SWG\Swagger(
 *    swagger="2.0",
 *     schemes={"https"},
 *     host="tcmzapi.emao.com",
 *     basePath="/",
 *     @SWG\Info(
 *         version="1.0.0",
 *         title="淘车猫中转库 Api 文档",
 *         description="淘车猫中转库 Api 文档"
 *     )
 * )
 */

这个文档的内容分成四部分,下面分别来说明。

2.1.1 OpenAPI 规范的版本号

首先我们要通过一个 swagger 属性来声明 OpenAPI 规范的版本。

<?php
/**
 * @SWG\Swagger(
 *     swagger="2.0"
 * )
 */

你没看错,是 swagger,上面已经介绍了,OpenAPI 规范是基于 Swagger的,在未来的版本中,这个属性可能会换成别的。 目前这个属性的值,暂时只能填写为 2.0

2.1.2 API 描述信息

然后我们需要说明一下API文档的相关信息,比如API文档版本(注意不同于上面的规范版本)、API文档名称已经可选的描述信息。

<?php
/**
 * @SWG\Info(
 *     version="1.0.0",
 *     title="淘车猫中转库 Api 文档",
 *     description="淘车猫中转库 Api 文档"
 * )
 */
  • version:API 文档版本
  • title:API 文档标题
  • description:API文档描述

2.1.3 API 的 URL

作为web API,一个很重要的信息就是用来给消费者使用的根URL,可以用协议(http或者https)、主机名、根路径来描述:

<?php
/**
 * @SWG\Swagger(
 *     schemes={"https"},
 *     host="tcmapi.emao.com",
 *     basePath="/v1"
 * )
 */

这这个例子中,消费者把 https://tcmapi.emao.com/v1 作为根节点来访问各种 API。因为和具体环境有关,不涉及 API 描述的根本内容,所以这部分信息是可选的。

  • schemes:协议,可以是多个
  • host:主机名
  • basePath:根路径

2.2 定义一个 API 操作

如果我们要展示一组用户信息,可以这样描述:

<?php
/**
 * @SWG\Swagger(
 *     swagger="2.0"
 *     schemes={"https"},
 *     host="tcmapi.emao.com",
 *     basePath="/v1.0",
 *     @SWG\Info(
 *         version="1.0.0",
 *         title="淘车猫 Api 文档",
 *         description="淘车猫 Api 文档"
 *     )
 * )
 * @SWG\Get(
 *     path="/persons",
 *     summary="获取一些人",
 *     description="返回包含所有人的列表。",
 *     @SWG\Response(
 *         response=200,
 *         description="一个用户列表",
 *         @SWG\Schema(
 *             type="array",
 *             @SWG\Items(
 *                  required={"username"}
 *                  @SWG\Properties()
 *             )
 *         ),
 *     ),
 * )
 */
  • Get:请求的 HTTP 方法,支持 GET/POST/PUT/DELETE 等 HTTP 标准请求方法
  • path:请求的路径
  • summary:接口简介
  • tags:接口标签,可以是多个
  • description:接口描述,支持 Markdown 语法
  • operationId:操作的 ID,需要唯一

2.2.1 添加一个 路径 (path)

我们添加一个 /persons路径,用来访问一组用户信息

2.2.2 在路径中添加一个 HTTP 方法

在每个路径中,我们可以添加任意的HTTP动词,如GET/POST/PUT/DELETE等来操作所需要的资源。

比如需要展示一组用户信息,我们可以在/person 路径中添加 get 方法,同时还可以填写一些简单的描述信息(summary)或者说明该主主法的一段长篇大论(description)

<?php
/**
 * @SWG\Get(
 *     path="/persons",
 *     summary="获取一些人",
 *     description="返回包含所有人的列表。"
 * )
 */

这样一来,我们调 get https://tcmapi.emao.com/v1/persons 方法就能获取一个用户信息列表了。

2.2.3 定义响应 (response) 类型

对于每个方法(或操作),我们都可以在响应(responses)中添加任意的 HTTP状态码(比如 200 OK 或者 404 Not Found等)。这个例子中我们添加上 200 的响应:

<?php
/**
 * @SWG\Response(
 *     response=200,
 *     description="一个用户列表"
 * )
 */

2.2.4 定义响应内容

get /persons这个接口返回一组用户信息,我们通过响应消息中的模式(schema)属性来描述清楚具体的返回内容。

一组用户信息就是一个用户信息对象的数组(array),每一个数组元素则是一个用户信息对象(object),该对象包含三个string类型的属性:姓氏名字用户名,其中用户名必须提供(required)。

<?php
/**
 * @SWG\Schema(
 *     type="array",
 *     @SWG\Items(
 *          required={"username"},
 *          @SWG\Property(
 *              property="firstName",
 *              type="string",
 *              description="firstName"
 *          ),
 *          @SWG\Property(
 *              property="lastName",
 *              type="string",
 *              description="lastName"
 *          ),
 *          @SWG\Property(
 *              property="username",
 *              type="string",
 *              description="username"
 *          )
 *     )
 * )
 */

2.3 定义 请求参数 (query parameteers)

用户太多,我们不想一股脑全部输出出来。这个时候,分页输出是个不错的选择,我们可以通过添加请求参数来实现。

<?php
/**
 * @SWG\Parameter(
 *     name="pageSize",
 *     in="query",
 *     description="Number of persons returned",
 *     type="integer"
 * ),
 * @SWG\Parameter(
 *     name="pageNumber",
 *     in="query",
 *     description="Page number",
 *     type="integer"
 * )
 */

2.3.1 在 get 方法中增加请求参数

<?php
/**
 * @SWG\Get(
 *     path="/persons",
 *     summary="获取一些人",
 *     description="返回包含所有人的列表。",
 *     @SWG\Parameter()
 * )
 */

2.3.2 添加分页参数

在参数列表中,我们添加两个名字(name)分别叫做 pageSizepageNumber的整型(integer)参数,并作简单描述:

<?php
/**
 * @SWG\Get(
 *     path="/persons",
 *     summary="获取一些人",
 *     description="返回包含所有人的列表。",
 *     @SWG\Parameter(
 *          name="pageSize",
 *          in="query",
 *          description="Number of persons returned",
 *          type="integer"
 *     ),
 *     @SWG\Parameter(
 *          name="pageNumber",
 *          in="query",
 *          description="Page number",
 *          type="integer"
 *     )
 * )
 */

这样一来,消费者就可以通过 get /persons?pageSize=20&pageNumber=2 来访问第2页的用户信息(不超过20条)了。

2.4 定义 路径参数 (path parameter)

有时候我们想要根据用户名来查找用户信息,这时我们需要增加一个接口操作,比如可以添加一个类似 /persons/{username} 的操作来获取用户信息。注意,{username} 是在请求路径中的参数。

<?php
/**
 * @SWG\Get(
 *     path="/persons/{username}",
 *     summary="获取一些人",
 *     description="返回包含所有人的列表。",
 *     @SWG\Parameter(
 *          name="username",
 *          in="path",
 *          required="true"
 *          description="The person's username",
 *          type="string"
 *     )
 * )
 */

2.4.1 添加一个 get /persons/{username} 操作

首先我们在 /persons 路径后面,增加一个 /persons/{username} 的路径,并定义一个 get (操作)方法。

<?php
/**
 * @SWG\Get(
 *     path="/persons/{username}",
 *     summary="Gets a person",
 *     description="Returns a single person for its username"
 * )
 */

2.4.2 定义路径参数 username

因为 {username} 是路径参数,我们需要先像请求参数一样将它添加到 parameters 属性中,注意名称应该同上面大括号( { } ) 里面的名称一致。并通过 in 这个属性,来表示它是一个路径(path)参数。

<?php
/**
 * @SWG\Parameter(
 *     name="username",
 *     in="path",
 *     required="true"
 *     description="The person's username",
 *     type="string"
 * )
 */

定义路径参数时很容易出现的问题就是忘记:required: true,Swagger的自动完成功能中没有包含这个属性定义。 如果没有写 require 属性,默认值是 false,也就是说 username 参数时可选的。可事实上,作为路径参数,它是必需的。

2.4.3 定义响应消息

别忘了获取单个用户信息也需要填写 200 响应消息,响应消息体的内容就是之前描述过的用户信息(用户信息列表中的一个元素):

<?php
/**
 * @SWG\Response(
 *     response=200,
 *     description="A Person",
 *     @SWG\Schema(
 *         required={"username"},
 *         @SWG\Property(
 *              property="firstName",
 *              type="string"
 *         ),
 *         @SWG\Property(
 *              property="lastName",
 *              type="string"
 *         ),
 *         @SWG\Property(
 *              property="username",
 *              type="string"
 *         )
 *     )
 * )
 */

当然,API的提供者会对 username 进行校验,如果查无此人,应该返回 404 的异常状态。所以我们再加上 404 状态的响应:

 <?php
 /**
 * @SWG\Response(
 *     response=404,
 *     description="The Person does not exists."
 * )
 */

2.5 定义 消息体参数 (body parameter)

当我们需要添加一个用户信息时,我们需要一个能够提供 post /persons 的API操作。

<?php
/**
 * @SWG\Post(
 *     path="/persons",
 *     summary="Creates a person",
 *     description="Adds a new person to the persons list.",
 *     @SWG\Parameter(
 *          name="person",
 *          in="body",
 *          required="true"
 *          description="The person to create.",
 *          @SWG\Schema(
 *              required={"username"},
 *              @SWG\Property(
 *                  property="firstName",
 *                  type="string"
 *              ),
 *              @SWG\Property(
 *                   property="lastName",
 *                   type="string"
 *              ),
 *              @SWG\Property(
 *                   property="username",
 *                   type="string"
 *              )
 *          )
 *     ),
 *     @SWG\Response(
 *          response="200",
 *          description="Persons succesfully created."
 *     ),
 *     @SWG\Response(
 *          response="400",
 *          description="Persons couldn't have been created."
 *     )
 * )
 */

2.5.1 添加一个 post /persons 操作

首页在 /persons 路径下添加一个 Post 操作:

<?php
/**
 * @SWG\Post(
 *     path="/persons",
 *     summary="Creates a person",
 *     description="Adds a new person to the persons list.",
 * )
 */

2.5.2 定义消息体参数

接下来我们给 post 方法添加参数,通过 in 属性显式说明参数是在 body 中的。参数的定义参考 get /persons/{username} 的 200 响应消息体参数,也就是包含用户的姓氏、名字、用户名。

<?php
/**
 * @SWG\Parameter(
 *     name="person",
 *     in="body",
 *     required="true"
 *     description="The person to create.",
 *     @SWG\Schema(
 *         required={"username"},
 *         @SWG\Property(
 *             property="firstName",
 *             type="string"
 *         ),
 *         @SWG\Property(
 *              property="lastName",
 *              type="string"
 *         ),
 *         @SWG\Property(
 *              property="username",
 *              type="string"
 *         )
 *     )
 * )
 */

2.5.3 定义响应消息

最后不要忘记定义 post 操作的响应消息。

<?php
/**
 * @SWG\Response(
 *     response="200",
 *     description="Persons succesfully created."
 * ),
 * @SWG\Response(
 *     response="400",
 *     description="Persons couldn't have been created."
 * )
 */

第 3 章 文档瘦身

现在我们已经学会了编写API文档的基本方法。不过上面的例子中存在一些重复,这对于程序员的嗅觉来说,就是代码的“坏味道”。这一章我们一起学习如何通过抽取可重用的定义(definitions)来简化API文档。

3.1 简化数据模型

我们认真观察第2章最后输出的API文档,很容易发现 Person 的定义出现了三次,非常的不 DRY☹。

现在,我们通过可重用的 定义 (definition)来重构这个文档:

<?php
/**
 * @SWG\Definition(
 *     definition="Person",
 *     type="object",
 *     required={"username"},
 *     @SWG\Property(
 *         property="firstName",
 *         type="string"
 *     ),
 *     @SWG\Property(
 *         property="lastName",
 *         type="string"
 *     ),
 *     @SWG\Property(
 *         property="username",
 *         type="string"
 *     )
 * )
 */

/**
 * @SWG\Definition(
 *     definition="Persons",
 *     type="array",
 *     @SWG\Items(ref="#/definitions/Person")
 * )
 */

文档简化了很多。这得益于OpenAPI规范中关于定义(definition)的章节中允许我们“一处定义,处处使用”。

3.1.1 添加 定义 (definitions) 项

我们首先在API文档的尾部添加一个 定义 (definitions)项(其实它也可以放在文档的任意位置,只不过大家习惯放在文档末尾):

<?php
/**
 * @SWG\Definition()
 */

3.1.2 增加一个可重用的(对象)定义

然后我们增加一个 Person 对象的定义:

<?php
/**
 * @SWG\Definition(
 *     definition="Person",
 *     type="object",
 *     required={"username"},
 *     @SWG\Property(
 *         name="firstName",
 *         type="string"
 *     ),
 *     @SWG\Property(
 *         name="lastName",
 *         type="string"
 *     ),
 *     @SWG\Property(
 *         name="username",
 *         type="string"
 *     )
 * )
 */

3.1.3 引用一个 定义 来增加另一个 定义

在定义项中,我们可以立即引用刚才定义好的 Person 来增加另一个 定义,Persons。Persons 是一个 Person 对象的数组。与之前直接定义的不同之处是,我们增加了一个 引用(reference)属性,也就是 ref 来引用 Person 。

<?php
/**
 * @SWG\Definition(
 *     definition="Persons",
 *     type="array",
 *     @SWG\Items(ref="#/definitions/Person")
 * )
 */

3.1.4 在响应消息中使用 定义

一旦定义好了 Person ,我们可以把原来在响应消息中相应的定义字段替换掉。

3.1.4.1 get/persons

原来:

<?php
/**
 * @SWG\Response(
 *     response=200,
 *     description="A list of Person",
 *     @SWG\Schema(
 *         type="array",
 *         @SWG\Items(
 *              required={"username"},
 *              @SWG\Property(
 *                  property="firstName",
 *                  type="string"
 *              ),
 *              @SWG\Property(
 *                  property="lastName",
 *                  type="string"
 *              ),
 *              @SWG\Property(
 *                  property="username",
 *                  type="string"
 *              )
 *         )
 *     )
 * )
 */

现在:

<?php
/**
 * @SWG\Response(
 *     response=200,
 *     description="A list of Person",
 *     @SWG\Schema(
 *         type="array",
 *         @SWG\Items(ref="#/definitions/Persons")
 *     )
 * )
 */
3.1.4.2 get/persons/{username}

原来:

<?php
/**
 * @SWG\Response(
 *     response=200,
 *     description="A Person",
 *     @SWG\Schema(
 *          required={"username"},
 *          @SWG\Property(
 *              property="firstName",
 *              type="string"
 *          ),
 *          @SWG\Property(
 *              property="lastName",
 *              type="string"
 *          ),
 *          @SWG\Property(
 *              property="username",
 *              type="string"
 *          )
 *     )
 * )
 */

现在:

<?php
/**
 * @SWG\Response(
 *     response=200,
 *     description="A Person",
 *     @SWG\Schema(ref="#/definitions/Person")
 * )
 */

3.1.5 在参数中使用 定义

不仅仅在消息中可以使用 定义,在参数中也可以使用。

3.1.5.1 post /persons

原来:

<?php
/**
 * @SWG\Post(
 *     path="/persons",
 *     summary="Creates a person",
 *     description="Adds a new person to the persons list.",
 *     @SWG\Parameter(
 *          name="person",
 *          in="body",
 *          required="true"
 *          description="The person to create.",
 *          @SWG\Schema(
 *              required={"username"},
 *              @SWG\Property(
 *                  property="firstName",
 *                  type="string"
 *              ),
 *              @SWG\Property(
 *                   property="lastName",
 *                   type="string"
 *              ),
 *              @SWG\Property(
 *                   property="username",
 *                   type="string"
 *              )
 *          )
 *     )
 * )
 */

现在:

<?php
/**
 * @SWG\Post(
 *     path="/persons",
 *     summary="Creates a person",
 *     description="Adds a new person to the persons list.",
 *     @SWG\Parameter(
 *          name="person",
 *          in="body",
 *          required="true"
 *          description="The person to create.",
 *          @SWG\Schema(ref="#/definitions/Person")
 *     )
 * )
 */

3.2 简化响应消息

我们看到了 引用 ($ref)的作用,接下来我们再把它用到响应消息的定义中:

3.2.1 定义可重用的HTTP 500 响应

发生HTTP 500错误时,假如我们希望每一个API操作都返回一个带有错误码(error code)和描述信息(message)的响应,我们可以这样做:

<?php
/**
 * @SWG\Response(
 *     response="500",
 *     description="An unexpected error occured.",
 *     @SWG\schema(
 *         @SWG\Property(
 *             property="code",
 *             type="string"
 *         ),
 *         @SWG\Property(
 *             property="message",
 *             type="string"
 *         )
 *     )
 * )
 */

3.2.2 增加一个 Error 定义

按照“一处定义、处处引用”的原则,我们可以在定义项中增加 Error 的定义:

<?php
/**
 * @SWG\Definition(
 *     definition="Error",
 *     @SWG\Property(
 *         property="code",
 *         type="string"
 *     ),
 *     @SWG\Property(
 *         property="message",
 *         type="string"
 *     )
 * )
 */

而且我们也学会了使用 引用($ref),所以我们可以这样写:

<?php
/**
 * @SWG\Response(
 *     response="500",
 *     description="An unexpected error occured.",
 *     @SWG\schema(ref="#/definitions/Error")
 * )
 */

3.2.3 定义一个可重用的响应消息

上面的文档中,还是有一些重复的内容。我们可以根据 OpenAPI 规范中的 responses 章节的描述,通过定义一个可重用的响应消息,来进一步简化文档。

<?php
/**
 * @SWG\Response(
 *     response="Standard500ErrorResponse",
 *     description="An unexpected error occured.",
 *     @SWG\schema(ref="#/definitions/Error")
 * )
 */

注意:响应消息中引用了 Error 的定义。

3.2.4 使用已定义的响应消息

我们还是通过 引用($ref)来使用一个已经定义好的响应消息,比如:

3.2.4.1 get /users
<?php
/**
 * @SWG\Response(
 *     response="200",
 *     description="A list of Person",
 *     @SWG\schema(ref="#/definitions/Persons")
 * ),
 * @SWG\Response(
 *     response="500",
 *     ref="#/responses/Standard500ErrorResponse"
 * )
 */

略去一部份不重要的。

第 4 章 深入了解一下

通过前面的练习,我们可以写出一篇结构清晰、内容精炼的API文档了。可是 OpenAPI 规范还给我们提供了更多的便利和惊喜,等着我们去了解和掌握。这一章主要介绍用于定义属性和数据模型的高级方法。

4.1 私人定制

使用 JSON Schema Draft 4,我们可以定义任意类型的各种属性,举例说明。

4.1.1 定符串 (strings) 长度和格式

当定义个字符串属性时,我们可以定制它的长度及格式:

属性 类型 描述
minLength number 字符串最小长度
maxLength number 字符串最大长度
pattern string 正则表达式

如果我们规定用户名是长度介于8~64,而且只能由小写字母和数字来构成,那么我们可以这样写:

<?php
/**
 * @SWG\Property(
 *     property="username",
 *     type="string",
 *     pattern="[a-z0-9]{8,64}",
 *     minLength="8",
 *     maxLength="64"
 * )
 */

4.1.2 日期和时间

日期和时间的处理参考 RFC 3339 ,我们唯一要做的就是写对格式:

格式 属性包含内容 属性示例
date ISO8601 full-date 2016-04-01
dateTime ISO8601 date-time 2016-04-16T16:06:05Z

如果我们在 Person 的定义中增加 生日上次登录时间 时间戳,我们可以这样写:

<?php
/**
 * @SWG\Property(
 *     property="dateOfBirth",
 *     type="string",
 *     format="date"
 * ),
 * @SWG\Property(
 *     property="lastTimeOnline",
 *     type="string",
 *     format="dateTime"
 * )
 */

4.1.3 数字类型与范围

当我们定义一个数字类型的属性时,我们可以规定它是一个整型、长型、浮点型或者双浮点型。

名称 类型 格式
integer integer int32
long integer int64
float number float
double number double

和字符串一样,我们也可以定义数字属性的范围,比如:

属性 类型 描述
minimum number 最小值
maximum number 最大值
exclusiveMinimum boolean 数值必须 > 最小值
exclusiveMaximum boolean 数值必须 < 最大值
multipleOf number 数值必须是 multipleOf 的整数倍

如果我们规定 pageSize 必须是整数,必须 > 0 且 <=100,还必须是 10 的整数倍,可以这样写:

<?php
/**
 * @SWG\Parameter(
 *     name="pageSize",
 *     in="query"
 *     description="Number of persons returned",
 *     type="integer",
 *     format="int32",
 *     minimum: 0,
 *     exclusiveMinimum: true,
 *     maximum: 100,
 *     exclusiveMaximum: false,
 *     multipleOf: 10
 * )
 */

4.1.4 枚举类型

我们还可以定义枚举类型,比如定义 Error 时,我们可以这样写:

<?php
/**
 * @SWG\Property(
 *     property="code",
 *     type="string",
 *     enum={"DBERR", "NTERR", "UNERR"}
 * )
 */

code 的值只能从三个枚举值中选择。

4.1.5 数值的大小和唯一性

数字的大小和唯一性通过下面这些属性来定义:

属性 类型 描述
minItems number 数值中的最小元素个数
maxItem number 数值中的最大元素个数
uniqueItems boolean 标示数组中的元素是否唯一

比如我们定义一个用户数组 Persons,希望返回的用户信息条数介于10~100之间,而且不能有重复的用户信息,我们可以这样写:

<?php
/**
 * @SWG\Property(
 *     property="Persons",
 *     @SWG\Items(
 *         type="array",
 *         minItems="10",
 *         maxItems="100",
 *         uniqueItems="true",
 *         ref="#/definitions/Person"
 *     )
 * )
 */

4.1.6 二进制数据

可以用 string 类型来表示二进制数据:

格式 属性包含
byte Base64编码字符
binary Base64任意十进制的数据序列字符

比如我们需要在用户信息中增加一个头像属性(avatarBase64PNG)用base64编码的PNG图片来表示,可以这样写:

<?php
/**
 * @SWG\Property(
 *     property="avatarBase64PNG",
 *     type="string",
 *     format="byte"
 * )
 */

4.2 高级数据定义

4.2.1 读写操作同一定义的数据

有时候我们读取资源信息的内容会比我们写入资源信息的内容(属性)更多,这很常见。是不是意味着我们必须专门为读取资源和写入资源分别定义不同的数据模型呢?幸运的是,OpenAPI 规范中提供了 readOnly 字段来帮我们解决整问题。比如:

<?php
/**
 * @SWG\Property(
 *     property="lastTimeOnline",
 *     type="string",
 *     format="dateTime",
 *     readOnly="true"
 * )
 */

上面这个例子中,上次在线时间(lastTimeOnline )是 Person 的一个属性,我们获取用户信息时需要这个属性。但是很明显,在创建用户时,我们不能把这个属性 post 到服务器。于是我们可以把它标记为 readOnly。

4.2.2 组合定义确保一致性

一致性设计是在编写API文档时需要重点考虑的问题。比如我们在获取一组用户信息时,需要同时获取页面信息(totalItems, totalPage, pageSize, currentPage)等,而且这些信息 必须 在根节点上。

怎么办呢?首先想到的做法就是:

<?php
/**
 * @SWG\Definition(
 *     definition="PagedPersonsV1",
 *     @SWG\Property(
 *          property="items",
 *          type="array",
 *          @SWG\Items(ref="#/definitions/Person")
 *     ),
 *     @SWG\Property(
 *          property="totalItems",
 *          type="integer"
 *     ),
 *     @SWG\Property(
 *          property="totalPages",
 *          type="integer"
 *     ),
 *     @SWG\Property(
 *          property="pageSize",
 *          type="integer"
 *     ),
 *     @SWG\Property(
 *          property="currentPage",
 *          type="integer"
 *     )
 * )
 */

如果其他API操作也需要这些 页面信息,那就意味着这些属性必须一遍又一遍的定义。不仅重复体力劳动,而且还很危险:比如忘记了其中的一两个属性,或者需要添加一个新的属性进来,那就是霰弹式的修改,想想都很悲壮。

稍微好一点的做法,就是根据前面学习的内容,把这几个属性抽取出来,建立一个 Paging 模型,“一处定义、处处使用”:

<?php
/**
 * @SWG\Definition(
 *     definition="PagedPersonsV2",
 *     @SWG\Property(
 *          property="items",
 *          type="array",
 *          @SWG\Items(ref="#/definitions/Person")
 *     ),
 *     @SWG\Property(
 *          property="Paging",
 *          @SWG\Items(ref="#/definitions/Paging")
 *     )
 * ),
 * @SWG\Definition(
 *     definition="Paging",
 *     @SWG\Property(
 *          property="totalItems",
 *          type="integer"
 *     ),
 *     @SWG\Property(
 *          property="totalPages",
 *          type="integer"
 *     ),
 *     @SWG\Property(
 *          property="pageSize",
 *          type="integer"
 *     ),
 *     @SWG\Property(
 *          property="currentPage",
 *          type="integer"
 *     )
 * )
 */

但是,页面属性都不再位于 根节点!与我们前面设定的要求不一样了。怎么破?

JSON Schema v4 property中定义的 allOf,能帮我们解围:

<?php
/**
 * @SWG\Definition(
 *     definition="PagedPersons",
 *     allOf={
            @SWG\Schema(ref="#/definitions/Persons"),
 *          @SWG\Schema(ref="#/definitions/Paging"),
 *     }
 * )
 */

上面这个例子表示,PagedPersons 根节点下,具有将 Persons 和 Paging 展开 后的全部属性。

allOf 同样可以使用行内的数据定义,比如

<?php
/**
 * @SWG\Definition(
 *     definition="PagedCollectingItems",
 *     allOf={
            @SWG\Property(
 *              property="items",
 *              type="array",
 *              minItems="10",
 *              maxItems="100",
 *              uniqueItems="true",
 *              @SWG\Items(ref="#/definitions/CollectingItem")
 *          ),
 *          @SWG\Schema(ref="#/definitions/Paging"),
 *     }
 * )
 */

4.2.3 数据模型的继承(TODO)

目前各工具支持程度不高,待续

第 5 章 输入输出模型

这一章主要介绍如何定义高度精确化的参数和响应消息等。

5.1 高级参数定义

5.1.1 必带参数和可选参数

我们已经知道使用关键字 required 来定义一个必带参数。

5.1.1.1 定义必带参数和可选参数

在一个参数中,required 是一个 boolean 型的可选值。它的默认值是 false 。

比如在某个操作中,username 是必填参数:

<?php
/**
 * @SWG\Parameter(
 *     name="username",
 *     in="path",
 *     required="true"
 *     description="The person's username",
 *     type="string"
 * )
 */
5.1.1.2 定义必带属性和可选属性

根据定义,required 是一个字符串列表,列表中包含各必带参数名。如果某个参数在这张列表中找不到,那就说明它不是必带参数。如果没有定义required ,就说明所有参数都是可选。如果 required 定义在一个HTTP请求上,这说明所有的请求参数都是必填。

在 POST 、persons 中有 Person 的定义,在这里 username 这个属性是必带的,我们可以指定它为 required ,其他非必带字段则不指定:

<?php
/**
 * @SWG\Items(
 *     required={"username"},
 *     @SWG\Property(
 *         property="firstName",
 *         type="string"
 *     ),
 *     @SWG\Property(
 *         property="lastName",
 *         type="string"
 *     ),
 *     @SWG\Property(
 *         property="username",
 *         type="string"
 *     )
 * )
 */

5.1.2 带默认值的参数

通过关键字 default,我们可以定义一个参数的默认值。当这个参数不可得(请求未带或者服务器未返回)时,这个参数就取默认值。因此设定了某个参数的默认值后,它是否 required 就没意义了。

5.1.2.1 定义参数的默认值

我们定义参数 pageSize 的默认值为 20 ,那么如果请求时没有填写 pageSize ,服务器也会默认返回 20 个元素。

<?php
/**
 * @SWG\Parameter(
 *     name="pageSize",
 *     in="query"
 *     description="Number of persons returned",
 *     type="integer",
 *     format="int32",
 *     minimum="0",
 *     exclusiveMinimum="true",
 *     maximum="100",
 *     exclusiveMaximum="false",
 *     multipleOf="10",
 *     default="20"
 * )
 */
5.1.2.2 定义属性的默认值

同参数,使用关键字 default 即可。

5.1.3 带空值的参数

在 GET /persons 时,如果我们想添加一个参数来过滤“是否通过实名认证”的用户,应该怎么做呢?首先想到的是这样:GET /persons?page=2&includeVerifiedUsers=true ,问题是 includeVerifiedUsers 语义已经如此清晰,而让 “=true”显得很多余。我们能不能直接用:GET /persons?page=2&includeVerifiedUsers 呢?

要做到这种写法,我们需要一个关键字 allowEmptyValue 。我们定义 includeVerifiedUsers 时允许它为空。那么如果我们请求 GET /persons?page=2&includeVerifiedUsers 则表示需要过滤“实名认证”用户,如果我们直接请求 GET /persons?page=2 则表示不过滤:

<?php
/**
 * @SWG\Parameter(
 *     name="includeNonVerifiedUsers",
 *     in="query"
 *     type="boolean",
 *     default="false",
 *     allowEmptyValue="true"
 * )
 */

5.1.4 参数组

设计API的时候,我们经常会遇到在 GET 请求中需要携带一组请求参数的情况。如何在API文档章呈现呢?很简单,我们只需要设定 参数类型(type)array,并选择合适的 组合格式(collectionFormat) 就行了。

组合格式 描述
csv (default value) Comma separated values(逗号分隔) foo,bar
ssv Space separated values(空格分隔) foo bar
tsv Tab separated values(反斜杠分隔) foo\bar
pipes Pipes separated values(竖线分隔) foo|bar
multi 单属性可以取多个值,比如 foo=bar&foo=baz. 只适用于查询参数和表单参数。

比如我们想根据多种参数(username , firstname , lastname , lastTimeOnline )等来对 Person 进行带排序的查询。我们需要一个这样的API请求: GET /persons?sort=-lastTimeOnline|+firtname|+lastname 。用于排序的参数是 sort ,+表示升序,-表示降序。

相应的API文档,可以这样写:

<?php
/**
 * @SWG\Parameter(
 *     name="sort",
 *     in="query"
 *     type="array",
 *     uniqueItems=true,
 *     minItems=1,
 *     maxItems=3,
 *     collectionFormat="pipes",
 *     @SWG\items(
 *         type="string",
 *         pattern="[-+](username|lastTimeOnline|firstName|lastName)",
 *     )
 * )
 */

现在我们就能搞定 GET /persons?sort=-lastTimeOnline|+firtname|+lastname 这种请求了。当然,我们还可以指定排序的默认值,锦上添花。

<?php
/**
 * @SWG\Parameter(
 *     name="sort",
 *     in="query"
 *     type="array",
 *     uniqueItems=true,
 *     minItems=1,
 *     maxItems=3,
 *     collectionFormat="pipes",
 *     @SWG\items(
 *         type="string",
 *         pattern="[-+](username|lastTimeOnline|firstName|lastName)",
 *     ),
 *     default={"-lastTimeOnline", "+username"}
 * )
 */

5.1.5 消息头 (Header) 参数

参数,按照位置来分,不仅仅包含路径参数、请求参数和消息体参数,还包括消息头参数和表单参数等。比如我们可以在HTTP请求的消息头上加一个 User-Agent (用于跟踪、调试或者其他),可以这样定义它:

<?php
/**
 * @SWG\Parameter(
 *      parameter="userAgent",
 *      name="User-Agent",
 *      type="string",
 *      in="header",
 *      required=true
 *  )
 */

然后像使用其他参数一样使用它:

<?php
/**
 * @SWG\Parameter(ref="#/parameters/userAgent")
 */

5.1.6 表单参数

有些 js-less-browser 的老浏览器不支持 POST JSON数据,比如在创建用户时,只能够以这样个格式请求:

POST /js-less-persons

username=apihandyman&firstname=API&lastname=Handyman

没有问题,丝袜哥可以搞定。我们只需要把各个属性的 in 关键字定义为formData ,然后设置 consumes 的媒体类型为 application/x-www-form-urlencoded 即可。

<?php
/**
 * @SWG\Post(
 *     path="/persons",
 *     summary="Creates a person",
 *     description="For JS-less partners",
 *     consumes={"application/x-www-form-urlencoded"},
 *     produces={"text/html"},
 *     @SWG\parameter(
 *         name="username",
 *         in="formData",
 *         required="true",
 *         pattern="[a-z0-9]{8,64}",
 *         minLength=8,
 *         maxLength=64,
 *         type="string"
 *
 *      )
 * )
 */

5.1.7 文件参数

当我们要处理一个请求,输入类型是 文件 时,我们需要:

  • 使用 multipart/form-data 媒体类型;

  • 设置参数的 in 关键字为 formData;

  • 设置参数的 类型(type) 为 file。

比如:

<?php
/**
 * @SWG\Post(
 *     path="/images",
 *     summary="Uploads an image",
 *     consumes={"multipart/form-data"},
 *     @SWG\Parameter(
 *         name="image",
 *         in="formData",
 *         type="file"
 *     )
 *     @SWG\Response(
 *         response="200",
 *         description="Image's ID",
 *         @SWG\Schema(
 *             @SWG\Property(
 *                  property="imageId",
 *                  type="string"
 *             )
 *         )
 *     )
 * )
 */

有时候我们想限定输入文件的类型(后缀),很不幸的是,根据现在V2.0的规范暂时还做不到☹

5.1.8 参数的媒体类型

一个 API 可以消费各种不同的媒体类型,比如说最常见的是 application/json 类型的数据,当然这不是 API 唯一支持的类型。我们可以在 文档的根节点 或者 一个操作的根节点 下添加关键字 consumes,来定义这个操作能够消费的媒体类型。

比如我们的API全部都接受JSON和YAML的数据,那我们可以在文档的根节点下添加:

<?php
/**
 * @SWG\Swagger(
 *     consumes={"application/json", "application/x-yaml"}
 * )
 */

如果某个操作(比如上传图片的操作)很特殊,它可以通过自己添加 consumes来覆盖全局设置:

<?php
/**
 * @SWG\Post(
 *     path="/images",
 *     summary="Uploads an image",
 *     consumes={"multipart/form-data"},
 *     @SWG\Parameter(
 *         name="image",
 *         in="formData",
 *         type="file"
 *     )
 *     @SWG\Response(
 *         response="200",
 *         description="Image's ID",
 *         @SWG\Schema(
 *             @SWG\Property(
 *                  name="imageId",
 *                  type="string"
 *             )
 *         )
 *     )
 * )
 */

5.2 高级响应消息定义

5.2.1 不带消息体的响应消息

不带消息体的响应很常见,比如HTTP 204 状态响应本身就表示服务器返回不带任何消息内容的成功消息。

要定义一个不带消息体的响应很简单,我们只需要写响应状态和描述就行了:

<?php
/**
 * @SWG\Response(
 *     response="204",
 *     description="Person succesfully created."
 * )
 */

5.2.2 响应消息中的必带参数和可选参数

与请求消息中类似,我们使用 required 参数来表示,比如请求一个用户信息时, 服务器必须返回username。

5.2.3 响应消息头

API 的返回结果不仅仅体现下 HTTP 状态和响应消息体,还可以在响应消息头上做文章。比如我们可以限定一个API的使用次数和使用时间段,在响应消息头中,增加一个属性 X-Rate-Limit-Remaining 来表示API可调用的剩余次数,增加另一个属性 X-Rate-Limit-Reset 来表示 API 的有效截止时间。

<?php
/**
 * @SWG\Response(
 *     response="204",
 *     description="Person succesfully created."
 *     @SWG\Header(
 *       header="X-Rate-Limit-Remaining",
 *       type="integer",
 *       format="int32"
 *     ),
 *     @SWG\Header(
 *       header="X-Rate-Limit-Reset",
 *       type="string",
 *       format="date-time"
 *     )
 * )
 */

5.2.4 默认响应消息

我们在定义响应消息时,通常会列举不同的HTTP状态结果。如果有些状态不在我们API文档的定义范围(比如服务器需要返回 993 的状态),该怎么处理呢?这时需要通过关键字 default 来定义一个默认响应消息,用于各种 定义之外 的状态响应,比如:

<?php
/**
 * @SWG\Response(
 *     response="default",
 *     description="default response"
 * )
 */

目前这个配置也不支持“一次定义,处处使用” 。☹

5.2.5 响应消息的媒体类型

与请求消息一样,我们也可以定义响应消息所支持的媒体类型,不同的是我们要用到关键字 produces(与请求消息中的 consumes 相对,由此可见,API 文档描述的主体是服务提供者)。

比如,我们可以在文档的根路径下全局设置:

<?php
/**
 * @SWG\Swagger(
 *     produces={"application/json", "application/x-yaml"}
 * )
 */

也可以在某个操作的根路径下覆盖设置。

5.3 定义某个参数只存在于响应消息中

如前章节4.2.1中已经提到的,定义一个对象,其中某个属性我们只希望在响应消息中携带,而不希望在请求消息中携带,应该用 readOnly 关键字来表示

第 6 章 不要让 API 裸奔

第 7 章 让文档的可读性更好

7.1 分类标签 (Tags)

通过关键字 tags 我们可以对文档中接口进行归类,tags 的本质是一个字符串列表。tags 定义在文档的根路径下。

7.1.1 单标签

比如说 GET /persons 属于用户(Person) 这个分类的,那么我们可以给它贴个标签:

<?php
/**
 * @SWG\Get(
 *     path="/persons",
 *     summary="Gets some persons",
 *     description="Returns a list containing all persons. The list supports paging.",
 *     operationId="searchUsers",
 *     @SWG\Parameter(ref="#/parameters/userAgent"),
 *     tags={"Persons"}
 * )
 */

7.1.2 多标签

一个操作也可以同时贴几个标签,比如:

<?php
/**
 * @SWG\Get(
 *     path="/js-less-consumer-persons",
 *     summary="Creates a person",
 *     description="For JS-less partners",
 *     operationId="createUserJS",
 *     @SWG\Parameter(ref="#/parameters/userAgent"),
 *     deprecated=true
 *     tags={"JSLess", "Persons"}
 * )
 */

贴上标签后,在Swagger Editor和Swagger UI中能够自动归类,我们可以按照标签来筛选接口,试试吧?

7.2 无处不在的描述文字(Descriptions)

description 这个属性几乎是无处不在,为了提高文档的可读性,我们应该在必要的地方都加上描述文字。

7.2.1 安全项的描述

7.2.2 模式 (Schema) 的描述

每一种模式(Schema),都会有一个标题(title)和一段描述,比如:

<?php
/**
 * @SWG\Definition(
 *     definition="Person",
 *     title="Human",
 *     description="A person which can be the user itself or one of his friend"
 * )
 */

7.2.3 属性的描述

比如:

<?php
/**
 * @SWG\Property(
 *     property="firstName",
 *     description="first name",
 *     type="string"
 * )
 */

7.2.4 参数的描述

比如:

/**
 * @SWG\Parameter(
 *      parameter="username",
 *      name="username",
 *      type="string",
 *      in="path",
 *      required=true,
 *     description="The person's username"
 *  )
 */

7.2.5 操作的概述 (summary)、描述和操作 ID (operationId)

一个操作(Operation)通常都会包含概述和描述信息。而且我们还可以添加一个关键字 operationId,这个关键字通常用来指示服务提供者对这个操作的 处理函数 的函数名。比如:

<?php
/**
 * @SWG\Get(
 *     path="/persons",
 *     summary="Gets some persons",
 *     description="Returns a list containing all persons. The list supports paging.",
 *     operationId="searchUsers",
 *     @SWG\Parameter(ref="#/parameters/userAgent"),
 *     tags={"Persons"}
 * )
 */

7.2.6 响应消息的描述

<?php
/**
 * @SWG\Response(
 *     response=200,
 *     description="A list of Person"
 * )
 */

7.2.7 响应消息头的描述

7.2.8 标签的描述

我们在 API 文档的根路径下添加了 tags 的定义,对于其中的每一个标签,我们都可以添加描述信息:

<?php
/**
 * @SWG\Tag(
 *     names="Persons",
 *     description="Everything you need to handle users and friends"
 * )
 */

7.3 在描述中使用 Markdown 语法

在绝大部份的 description 中,我们可以使用 GFM (Github Flavored Markdown)语法。

7.3.1 多行描述

使用符号 | 然后在新行中打一个tab(注意:YAML的tab是两个空格 ),就可以编辑多行描述,比如:

<?php
/**
 * @SWG\Tag(
 *     names="Persons",
 *     description="|
 *   Returns a list containing all items this person is looking for.
The list supports paging.
 *     "
 * )
 */

7.3.2 简单使用GFM

比如我们要强调,可以这样写:

7.3.3 带信息组的描述

7.3.4 带代码的描述

7.4 示例数据 (Examples)

我们已经知道了用Schema来描述参数和属性,有的时候,用示例数据更有表现了。我们可以使用关键字example来给原子属性或者对象添加示例数据。

7.4.1 原子属性的示例数据

<?php
/**
 * @SWG\Property(
 *     property="firstName",
 *     description="first name",
 *     type="string",
 *     example="John"
 * )
 */

7.4.2 对象属性的示例数据

待补充

7.4.3 定义的示例数据

待补充

7.4.4 响应消息的示例数据

待补充

7.4.5 示例数据的优先级

如果我们在各个级别(比如参数、对象、定义、响应消息)都添加了示例数据。支持 OpenAPI 规范的各解析工具 都是最高级别 的定义为准。

7.5 标记为弃用

我们可以通过关键字 deprecated 置为 true 来标记接口的 弃用 状态,比如:

<?php
/**
 * @SWG\Get(
 *     path="/js-less-consumer-persons",
 *     summary="Creates a person",
 *     description="For JS-less partners",
 *     operationId="createUserJS",
 *     @SWG\Parameter(ref="#/parameters/userAgent"),
 *     deprecated=true
 *     tags={"JSLess", "Persons"}
 * )
 */

7.6 链接到外部文档

一般来说,项目中不光只有一篇API文档,还应该有些描述application key,测试用例,操作链以及其他内容的文档,这些文档一般是单独成篇的。如果在描述某个接口时,我们想链接这些文档,可以通过关键字externalDoc 来添加,例如:

<?php
/**
 * @SWG\ExternalDocumentation(
 *     description="Complete documentation describing how to use this API",
 *     url="http://doc.simple.api/"
 * )
 */

第 8 章 分而治之

根据前面几张的知识,我们已经可以轻松的构建一个复杂的API文档了。可是作为一个学过 Clean Code 的程序员,我们并不希望所有的接口、定义都在一个大而全的上帝文件里。这一章我们一起来学习拆分文件。

本文章首发在 Laravel China 社区
本帖已被设为精华帖!
本帖由 Summer 于 6个月前 加精
回复数量: 17
  • maxincai
    6个月前

    文档中有部份省略,待补充。

  • YuxiangDong 生命不息,挖坑不止 1
    6个月前

    虽然我感觉这种东西挺好,写代码的时候就把接口文档写了,但是在团队里推广这个实在太难了,学习成本有点高

  • Such丶 a programer on the way
    6个月前

    @YuxiangDong 其实还好,我们内部就在用这个,这套东西不光是开发,包括后期的自动化测试,都可以基于swagger的自动生成

  • wujunze talk is cheap show me the code
    6个月前

    @Such丶 基于 swagger 的自动生成自动测试 有什么工具推荐吗?

  • klgd
    6个月前

    以前就关注过,但..... 还要再去学一门注释语言
    最好是能直接基于 phpdoc,可惜没有

  • Such丶 a programer on the way
    6个月前

    @wujunze 我们是这么做的:用swagger做mock数据,用单元测试来保证接口返回字段的完整性

  • Yu
    5个月前

    辛苦了 很有帮助

  • Yu
    5个月前

    文中的 大部分地方

    required="true"

    应该为

    required=true,
  • chenBJ
    5个月前

    我们用的gitbook,在写api文档还有存放一些知识点时很不错的一个软件

  • 颜⑧
    5个月前

    好长啊,且值得收藏。之前看有些部门代码没有一行注释,发现互联网公司很多不写注释不写文档但都能做事情,好奇怪。

  • ADKi 大鹏一日同风起,抟摇直上九万里。 假令风歇时下来,犹能簸却沧溟水。
    5个月前

    这是不是 注释比代码多系列

  • Kurisu
    4个月前

    @ADKi bingo

  • xflyhack
    3个月前

    为什么不结合laravel的框架做例子的讲解呢?我把注解放到框架里面各种报错,完全没法操作。还是懵逼中

  • to-xmc
    3周前
     *     @SWG\Parameter(
     *         name="query",
     *         in="query",
     *         description="查询参数",
     *         type="string",
     *         default={"ajax":"config"}
     *     ),

    file
    这怎么解决 单引号,双引号都不可以

  • Alexanderwmc Je pense, donc je suis
    3周前

    @YuxiangDong 不光是学习成本,感觉这东西在编码环节就是增加工作量,也许后期维护会好点吧

  • Alexanderwmc Je pense, donc je suis
    3周前

    有个问题想问下,如果在api接口那里想要复用model层定义的swagger属性,也就是在资源控制器中的方法的返回结果里用ref的方式引用model那里的定义,如果有涉及到关联关系的怎么定义?

  • to-xmc
    2周前

    fields[]='a'&fields[]='b'&fields[]='c'
    这种参数如何写?

暂无评论~~
您需要登陆以后才能留下评论!

Composer 中国全量镜像

Top 100 扩展包

Lumen 中文文档

Laravel 速查表

Laravel 中文文档

Laravel 项目开发规范

Laravel 开发环境部署

Elasticsearch-PHP 中文文档

Lumen 中文文档

GraphQL PHP 中文文档

社区文档撰写指南

TDD 构建 Laravel 论坛笔记

PHP PSR 标准规范

PHP 设计模式全集

Dingo API 中文文档