使用 Dingo API 快速构建 RESTful API(十二)—— 生成 API 文档

为 API 编写文档和编写 API 接口功能同样重要,因为 API 大多是给别人用的,有了具备可读性的文档别人才知道怎么调用,以及如何处理返回结果。为了让文档编写过程更简单,Dingo 扩展包允许你在控制器中添加注解,然后通过 Artisan 命令根据这些注释生成 API 文档。

下面我们就来演示下如何在 Dingo 路由控制器中添加注解并通过这些注解生成相应的 API 文档。

注:通过闭包函数定义的 API 无法通过这种方式生成 API 文档。另外,注解看起来是以注释的方式呈现,但它是用于描述源代码的元数据,可以参与代码的编译和执行,而注释通常只是代码功能、参数和返回的描述而已,不参与代码的编译和执行。

Resource

Dingo API 中控制器通常以资源控制器的形式来表示,我们可以通过 @Resource 注解定义一个资源,打开 Api\TaskController.php,在控制器类声明的前面添加注解如下:

/**
 * Task Resource Controller
 * @package App\Http\Controllers\Api
 * @Resource("Tasks")
 */
class TaskController extends ApiController
{
    ...
}

该注解的第一个参数用于描述资源的名称,还可以传递第二个参数定义资源路由的 base URI:

@Resource("Tasks", uri="/tasks")

Action

所谓 Action 指的是 HTTP 请求方法,目前 Dingo 支持 GET、POST、PUT、PATCH、DELETE 这五种请求方法,我们可以这样定义 TaskController 资源控制器 index 方法的 Action 注解:

/**
 * Display a listing of the resource.
 * @param Request $request
 * @return \Illuminate\Http\Response
 * @GET("/{?page,limit}"
 */
public function index(Request $request)
{
    ...
}

由于 index 方法支持分页,所以我们传递了额外的 pagelimit 可选参数到 URI 中。如果是 store 方法的话,对应的 Action 注解如下:

/**
 * Store a newly created resource in storage.
 *
 * @param  CreateTaskRequest $request
 * @return \Illuminate\Http\Response
 * @POST("/")
 */
public function store(CreateTaskRequest $request)
{
    ...
}

依次类推,后面的其它方法就不一一演示了,学院君会在最后给出最终的所有方法注解代码。

Version

Dingo API 有版本号的概念,我们可以在方法声明中通过 @Versions 注解添加该方法支持的版本:

/**
 * Display a listing of the resource.
 * @param Request $request
 * @return \Illuminate\Http\Response
 * @GET("/{?page,limit}"
 * @Versions({"v3"})
 */
public function index(Request $request)
{
    ...
}

你可以传递多个版本号到集合参数中,如果不设置该注解,则表示支持所有版本的 Dingo API 接口。

Parameter

我们可以通过 @Parameters@Parameter 注解来定义请求 URL 中的查询字符串参数,@Parameters 是一个集合,可以包含多组不同的参数组合,具体每一个参数则通过 @Parameter 来定义,比如我们可以通过这两个注解定义上面的 index 方法 URL 参数如下:

/**
 * Display a listing of the resource.
 * @param Request $request
 * @return \Illuminate\Http\Response
 * @GET("/{?page,limit}")
 * @Versions({"v3"})
 * @Parameters({
 *     @Parameter("page", description="page number", type="integer", required=false, default=1),
 *     @Parameter("limit", description="task item number per page", type="integer", required=false, default=10)
 * })
 */
public function index(Request $request)
{
    ...
}

每个 @Parameter 注解可以通过额外的属性指定参数说明,通过参数名语义就可以看出相应的含义,这里就不展开介绍了。

Request

上面介绍的参数声明是针对 URL 查询字符串的,如果是通过 HTTP 请求实体传递的参数怎么声明呢?这些请求实体通常是 POST、PUT、PATCH 这些请求方法传递参数的方式,以 POST 为例,如果是通过表单传递的参数,可以这么定义注解:

/**
 * Store a newly created resource in storage.
 *
 * @param  CreateTaskRequest $request
 * @return \Illuminate\Http\Response
 * @POST("/")
 * @Versions({"v3"})
 * @Request("text=test&is_completed=1", contentType="application/x-www-form-urlencoded")
 */
public function store(CreateTaskRequest $request)
{
    ...
}

当然,这种定义方式将字段值写死了,对使用者来说不够明确,我们可以像下面这样为每个字段属性进行定义:

@Request("text={text}&is_completed={is_completed}", contentType="application/x-www-form-urlencoded", attributes={
    @Attribute("text", type="string", required=true, description="the body of task", sample="test task"),
    @Attribute("is_completed", type="boolean", required=true, description="task is completed or not", sample=1)
})

如果是通过 JSON 格式传递请求参数的话,可以这么定义注解:

@Request({"text":"test task", "is_completed":1}, attributes={
    @Attribute("text", type="string", required=true, description="the body of task", sample="test task"),
    @Attribute("is_completed", type="boolean", required=true, description="task is completed or not", sample=1)
})

由于 contentType 属性值默认就是 application/json,所以不需要额外设置了,在 @Request 注解中,还可以通过 headers 属性设置额外的请求头,比如需要认证后才能访问 store 方法对应路由,这个时候需要在请求头中通过 Authorization 字段传递 Bearer Token 值,可以这样定义 @Request 注解实现:

@Request({"text":"test task", "is_completed":0}, headers={
    "Authorization": "Bearer {API Access Token}"
}, attributes={
    @Attribute("text", type="string", required=true, description="the body of task", sample="test task"),
    @Attribute("is_completed", type="boolean", required=false, description="task is completed or not", sample=0)
})

Response

有了请求注解,自然也有与之对应的响应注解,我们可以通过 Dingo 扩展包提供的 @Response 注解声明响应状态码、内容类型、响应实体和响应头等信息:

/**
 * Store a newly created resource in storage.
 *
 * @param  CreateTaskRequest $request
 * @return \Illuminate\Http\Response
 * @POST("/")
 * @Versions({"v3"})
 * @Request({"text":"test task", "is_completed":0}, headers={
 *     "Authorization": "Bearer {API Access Token}"
 * }, attributes={
 *     @Attribute("text", type="string", required=true, description="the body of task", sample="test task"),
 *     @Attribute("is_completed", type="boolean", required=true, description="task is completed or not", sample=0)
 * })
 * @Response(200, body={"data":{"id":4,"text":"Test Task 4","completed":"no","link":"http://todo.test/dingoapi/task/4"}})
 */
public function store(CreateTaskRequest $request)
{
    $request->validate([
        'text' => 'required|string'
    ]);

    $task = Task::create([
        'text' => $request->post('text'),
        'user_id' => auth('api')->user()->id,
        'is_completed' => Task::NOT_COMPLETED
    ]);

    return $this->response->item($task, new TaskTransformer());
}

@Request 注解一样,内容类型默认是 application/json,无需额外指定,我们还可以通过 headers 属性设置额外的响应头,通过 attributes 属性描述每个响应字段的含义:

@Response(200, body={"data":{"id":1,"text":"Test Task 1","completed":"no","link":"http://todo.test/dingoapi/task/1"}}, attributes={
    @Attribute("id", type="integer", description="the id of task", sample=1),
    @Attribute("text", type="string", description="the body of task", sample="test task"),
    @Attribute("completed", type="string", description="task is completed or not", sample="no"),
    @Attribute("link", type="string", description="task link", sample="http://todo.test/dingoapi/task/1")
})

Transaction

最后,如果某个方法会返回多个不同的响应(比如处理异常状况),或者可以支持多组不同的请求/响应,可以通过一个事务注解(@Transaction)将多组请求和响应囊括进来,每个组合里面支持一个请求和至少一个响应注解,比如我们可以通过这种方式定义 update 方法如下:

/**
 * Update the specified resource in storage.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  int  $id
 * @return \Illuminate\Http\Response
 * @POST("/{id}")
 * @Versions({"v3"})
 * @Parameters({
 *     @Parameter("id", type="integer", description="the ID of the task", required=true)
 * })
 * @Transaction({
 *     @Request({"text":"test task", "is_completed":1}, headers={
 *         "Authorization": "Bearer {API Access Token}"
 *     }, attributes={
 *         @Attribute("text", type="string", required=true, description="the body of task", sample="test task"),
 *         @Attribute("is_completed", type="boolean", required=true, description="task is completed or not", sample=1)
 *     }),
 *     @Response(200, body={"data":{"id":1,"text":"Test Task 1","completed":"no","link":"http://todo.test/dingoapi/task/1"}}, attributes={
 *         @Attribute("id", type="integer", description="the id of task", sample=1),
 *         @Attribute("text", type="string", description="the body of task", sample="test task"),
 *         @Attribute("completed", type="string", description="task is completed or not", sample="no"),
 *         @Attribute("link", type="string", description="task link", sample="http://todo.test/dingoapi/task/1")
 *     }),
 *     @Response(404, body={"message":"404 not found", "status_code": 404})
 * })
 */
public function update(Request $request, $id)
{
    $task = Task::findOrFail($id);
    $updatedTask = tap($task)->update(request()->only(['is_completed', 'text']))->fresh();
    return $this->response->item($updatedTask, new TaskTransformer());
}

由于 update 方法可能会返回 200 和 404 两种响应,所以我们通过 @Transaction 将请求和响应注解都包含到一个集合项中。

生成 API 文档

至此,我们已经完成了 Dingo API 请求和响应从起始行到报文首部,再到报文实体所有 HTTP 报文组成部分的注解描述,接下来我们按照上面介绍的注解命令完成 Api\TaskController 控制器所有方法的注解定义:

/**
 * Task Resource Controller
 * @package App\Http\Controllers\Api
 * @Resource("Tasks", uri="/tasks")
 */
class TaskController extends ApiController
{
    public function __construct()
    {
        $this->middleware('auth:api');
    }

    /**
     * Display a listing of the resource.
     * @param Request $request
     * @return \Illuminate\Http\Response
     * @GET("/{?page,limit}")
     * @Versions({"v3"})
     * @Parameters({
     *     @Parameter("page", description="page number", type="integer", required=false, default=1),
     *     @Parameter("limit", description="task item number per page", type="integer", required=false, default=10)
     * })
     * @Request(headers={
     *         "Authorization": "Bearer {API Access Token}"
     * })
     * @Response(200, body={"data":{{"id":1,"text":"Test Task 1","completed":"no","link":"http://todo.test/dingoapi/task/1"}},"meta":{"pagination":{"total":4,"count":1,"per_page":1,"current_page":1,"total_pages":4,"links":{"next":"http://todo.test/dingoapi/tasks?page=2"}}}})
     */
    public function index(Request $request)
    {
        $limit = $request->input('limit') ? : 10;
        // 获取认证用户实例
        $user = $request->user('api');
        $tasks = Task::where('user_id', $user->id)->paginate($limit);
        return $this->response->paginator($tasks, new TaskTransformer());
    }

    /**
     * Store a newly created resource in storage.
     *
     * @param  CreateTaskRequest $request
     * @return \Illuminate\Http\Response
     * @POST("/")
     * @Versions({"v3"})
     * @Request({"text":"test task", "is_completed":0}, headers={
     *     "Authorization": "Bearer {API Access Token}"
     * }, attributes={
     *     @Attribute("text", type="string", required=true, description="the body of task", sample="test task"),
     *     @Attribute("is_completed", type="boolean", required=true, description="task is completed or not", sample=0)
     * })
     * @Response(200, body={"data":{"id":1,"text":"Test Task 1","completed":"no","link":"http://todo.test/dingoapi/task/1"}}, attributes={
     *     @Attribute("id", type="integer", description="the id of task", sample=1),
     *     @Attribute("text", type="string", description="the body of task", sample="test task"),
     *     @Attribute("completed", type="string", description="task is completed or not", sample="no"),
     *     @Attribute("link", type="string", description="task link", sample="http://todo.test/dingoapi/task/1")
     * })
     */
    public function store(CreateTaskRequest $request)
    {
        $request->validate([
            'text' => 'required|string'
        ]);

        $task = Task::create([
            'text' => $request->post('text'),
            'user_id' => auth('api')->user()->id,
            'is_completed' => Task::NOT_COMPLETED
        ]);

        return $this->response->item($task, new TaskTransformer());
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     * @GET("/{id}")
     * @Parameters({
     *     @Parameter("id", type="integer", description="the ID of the task", required=true)
     * })
     * @Versions({"v3"})
     * @Transaction({
     *     @Request(headers={
     *         "Authorization": "Bearer {API Access Token}"
     *     }),
     *     @Response(200, body={"data":{"id":1,"text":"Test Task 1","completed":"no","link":"http://todo.test/dingoapi/task/1"}}, attributes={
     *         @Attribute("id", type="integer", description="the id of task", sample=1),
     *         @Attribute("text", type="string", description="the body of task", sample="test task"),
     *         @Attribute("completed", type="string", description="task is completed or not", sample="no"),
     *         @Attribute("link", type="string", description="task link", sample="http://todo.test/dingoapi/task/1")
     *     }),
     *     @Response(404, body={"message":"404 not found", "status_code": 404})
     * })
     */
    public function show($id)
    {
        $task = Task::findOrFail($id);
        return $this->response->item($task, new TaskTransformer());
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     * @PUT("/{id}")
     * @Parameters({
     *     @Parameter("id", type="integer", description="the ID of the task", required=true)
     * })
     * @Versions({"v3"})
     * @Transaction({
     *     @Request({"text":"test task", "is_completed":1}, headers={
     *         "Authorization": "Bearer {API Access Token}"
     *     }, attributes={
     *         @Attribute("text", type="string", required=true, description="the body of task", sample="test task"),
     *         @Attribute("is_completed", type="boolean", required=true, description="task is completed or not", sample=1)
     *     }),
     *     @Response(200, body={"data":{"id":1,"text":"Test Task 1","completed":"no","link":"http://todo.test/dingoapi/task/1"}}, attributes={
     *         @Attribute("id", type="integer", description="the id of task", sample=1),
     *         @Attribute("text", type="string", description="the body of task", sample="test task"),
     *         @Attribute("completed", type="string", description="task is completed or not", sample="no"),
     *         @Attribute("link", type="string", description="task link", sample="http://todo.test/dingoapi/task/1")
     *     }),
     *     @Response(404, body={"message":"404 not found", "status_code": 404})
     * })
     */
    public function update(Request $request, $id)
    {
        $task = Task::findOrFail($id);
        $updatedTask = tap($task)->update(request()->only(['is_completed', 'text']))->fresh();
        return $this->response->item($updatedTask, new TaskTransformer());
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     * @DELETE("/{id}")
     * @Parameters({
     *     @Parameter("id", type="integer", description="the ID of the task", required=true)
     * })
     * @Versions({"v3"})
     * @Transaction({
     *     @Request(headers={
     *         "Authorization": "Bearer {API Access Token}"
     *     }),
     *     @Response(200, body={"message": "Task deleted"}),
     *     @Response(404, body={"message":"404 not found", "status_code": 404})
     * })
     */
    public function destroy($id)
    {
        $task = Task::findOrFail($id);
        $task->delete();
        return response()->json(['message' => 'Task deleted'], 200);
    }
}

最后通过 Dingo 扩展包提供的 API 文档生成命令来生成对应的文档了,在项目根目下执行如下 Artisan 命令:

php artisan api:docs --name TodoApp --output-file apidocs.md

我们通过 --name 指定文档名称,通过 --output-file 指定文档输出路径,生成的 API 文档将保存到项目根目录下,生成的文档效果图如下:

通过 Dingo API 注解生成 API 文档

还可以通过点击 Github 项目文件查看对应 API 文档。基于 Dingo 扩展包实现的待办任务项目 API 源码都可以在 Github 项目上查看:nonfu/todoapp

上一篇: 使用 Dingo API 快速构建 RESTful API(十一)—— 在应用内部请求 Dingo API

下一篇: 使用 Laravel API 文档生成器扩展包自动为项目生成 API 文档