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

前言

编写目的

本文介绍如何使用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文档了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容