使用 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
方法支持分页,所以我们传递了额外的 page
、limit
可选参数到 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 文档将保存到项目根目录下,生成的文档效果图如下:
还可以通过点击 Github 项目文件查看对应 API 文档。基于 Dingo 扩展包实现的待办任务项目 API 源码都可以在 Github 项目上查看:nonfu/todoapp。
1 Comment
注解方法需要首字母大写,其他小写,如,正确:@Get,错误:@GET,后面写法会报找不到方法;artisan需要加--use-version参数指定版本,如,--use-version v3(这里值对应@Versions中的值)。 完整命令: php artisan api:docs --name TodoApp --output-file apidocs.md --use-version v3