创建文档

编写文档的方式有很多种。没有一种正确的方法,而且通常是由已在使用的工具(如资源库服务或公司维基)预先决定的。不过,还是有一些技巧和窍门可以帮助你编写和维护文档,我们希望在本节中向你介绍这些技巧和窍门。

文本文档

让我们首先关注典型的人工编写文本文档。经典的方法是建立维基,因为维基的最大优势是,即使技术水平不高的人也可以访问和使用。这使它们成为公司的最佳选择。现代维基,无论是自托管还是软件即服务(SaaS),都提供了很多保证和有用的功能,如内联注释或版本管理。它们还可以与许多外部工具(如票据系统)连接。

另一种方法是将文档添加到版本库中,使其与代码保持紧密联系—​例如,添加到子文件夹中。这也是一种有效的方法,尤其适用于较小的团队或开源项目。不过,你不应该使用 Word 或便携文档格式(PDF)等臃肿的格式,而应该专注于基于文本的格式,如 Markdown。它们的篇幅要小很多,而且通过版本控制历史记录很容易跟踪对它们的修改。

手动编写文档的关键在于保持更新。文本文件或维基很有耐心,而且不会忘记,随着时间的推移,许多页面的文档会无形中堆积在它们的存储空间中。如果不清楚哪些文档是正确的,哪些是过时的,就会出现问题。一旦有了疑问,就完全不可信了。

解决这个问题的唯一办法就是建立一个确保文件得到更新的流程。在上一章中,我们已经介绍了一种可行的方法:结合 "完成定义"(DoD)进行代码审查。这样,每当我们要在代码库中添加新代码或更改代码时,我们就会收到一份检查清单,提醒我们在必要时更新文档。

特别是,系统和软件架构是通过图表来记录的。因此,在下一节中,我们将向你展示如何有效地创建这些图表。

图表

一个好的图表通常比长篇文字更能说明问题。有许多免费使用的图表制作工具,你可以选择手动绘制图表或根据文本定义生成图表。

手动绘制图表

创建图表的传统方法是使用图表工具手动绘制。这些工具专门用于在创建过程中提供帮助—​例如,提供模板和图标集,或者在移动对象时保持对象之间的连接箭头。

本章要介绍的一款多功能工具是 diagrams.net(https://www. diagrams.net)。事实上,我们也用它为本书制作插图。它提供了一个元素库,例如,可用于创建统一建模语言 (UML) 图和流程图等图表。它还提供了最流行的云计算提供商的图标,如 Google Cloud Platform (GCP)、Amazon Web Services (AWS) 和 Microsoft Azure。

如果你打算使用它,我们建议你将图表保存为可扩展矢量图形(SVG)。SVG 基于可扩展标记语言 (XML),虽然 XML 相当冗长,但它比便携式网络图形 (PNG) 等图形格式占用的磁盘空间更少。

更重要的是,它可以在编辑器中反复加载和修改,因此每次系统发生变化时都不必重新开始。大多数集成开发环境(IDE)和所有浏览器都可以将 SVG 文件显示为图形图像,甚至可以无限缩放,如有必要,还可以轻松导出为最常用的图像格式。

根据定义生成图表

不过,并不是每个人都喜欢使用繁琐的编辑器来绘制图表。幸运的是,有多种图表绘制工具可以根据定义生成图表。为了演示这些工具的一般工作原理,我们选择了 Mermaid.js (https://mermaid-js.github.io) 作为示例。它是用 JavaScript 编写的,使用受 Markdown 启发的语言来定义图表。

在了解这种方法的优势之前,让我们先来看一个简单的流程图示例:

graph LR
    A{Do you know how to write great PHP code?} --> B[No]
    A --> C[Yes]
    C --> E(Awesome!)
    B --> D{Did you read Clean Code in PHP?} --> F[No]
    D --> G[Yes]
    G --> H(Please read it again)
    F --> I(Please read it)

前面的代码将呈现一个图表,如下所示:

image 2023 11 12 22 49 37 960
Figure 1. Figure 13.1: Mermaid diagram example

图表生成工具可帮助你创建多种图表类型,如序列图、甘特图,甚至是众所周知的饼图。你无需考虑如何设计图表样式或排列图表。主要工作由图表制作工具完成。当然,Mermaid.js 提供了许多影响生成图表外观的方法。

由于图表定义是简单的文本块,因此可以添加到代码库中。对它们的修改可以通过版本历史轻松追踪。美人鱼图表与 Markdown 文档的集成度特别高,因为最流行的集成开发环境可以通过附加扩展直接在文档中显示这些图表。

最后,如果你只是想体验一下美人鱼的各种可能性,你可以使用美人鱼实时编辑器(https://mermaid.live)来更好地了解它是如何工作的。

Mermaid 替代品

其它值得一提的图表工具还有 PlantUML (https://plantuml.com) 和 Diagrams ( https://diagrams.mingrammer.com ),前者提供了更多实用的图表类型来记录软件架构,后者则在记录系统架构方面表现出色。

文档生成器

最好的文档可能是我们不需要自己创建的文档,而且还能像人类撰写的内容一样有用。遗憾的是,尽管我们不知道机器学习(ML)未来会将我们带向何方,但这目前仍将是一个梦想。

现在,我们已经可以使用工具从代码中创建文档。至少,我们可以用它们来汇总分散在项目多个类中的信息。

API 文档

在本节中,我们将以 API 文档为例,向你展示如何从代码中创建文档。如果你的应用程序提供了应用程序接口(API),那么为其提供最新的文档是非常重要的。编写此类文档是一个耗时且容易出错的过程,但我们至少可以让它变得简单一些。

有许多方法可以用来编写 API 文档。在本书中,我们将向你介绍一种越来越流行的格式: OpenAPI。这种格式的前身是 Swagger,它在 YAML Ain’t Markup Language (YAML) 文档中描述了 API 的所有方面,看起来就像这样:

openapi: 3.0.0
info:
    title: 'Product API'
    version: '0.1'
paths:
    /product:
        get:
            operationId: getProductsUsingAnnotations
            parameters:
                -
                  name: limit
                  in: query
                  description: 'How many products to return'
                  required: false
                  schema:
                    type: integer
            responses:
              '200':
                description: 'Returns the product data'

乍一看,这些信息可能有点多。不过不用担心,这并不复杂。简而言之,前面的 YAML 描述了 0.1 版本的产品应用程序接口(Product API),它提供了一个端点,即 /product。该端点可以使用超文本传输协议(HTTP)动词 GET 调用,并接受可选参数 limit,该参数为整数类型,必须写入 URL 查询中(例如,/product?limit=50)。如果一切顺利,端点将返回 HTTP 代码 200。

OpenAPI documentation

OpenAPI 格式非常广泛,因此我们无法在本书中介绍。如果你有兴趣了解更多,请查看官方文档: https://oai.github.io/Documentation

一个值得欢迎的好处是,PhpStorm 等集成开发环境在编写这些 YAML 文件时默认会通过检查模式的有效性来提供支持。举例来说,如果你写的是 operation 而不是 operationId,集成开发环境就会突出显示错误的用法。

你既可以手动编写 YAML 文件,也可以让系统生成 YAML 文件。我们想仔细研究一下后一种用例。为此,我们需要一个名为 swagger-php 的 Composer 软件包 (https://github.com/zircote/swagger-php) 的帮助。请参考软件包文档了解如何安装。

当然,该软件包不能神奇地凭空创建文档。相反,swaggerphp 会解析直接写在 PHP 代码中的元信息,这些元信息可以是 DocBlock 注释,也可以是 PHP 8.1 中的属性。换句话说,在生成 YAML 文件之前,我们需要确保元信息已经存在。

这些信息是什么样的呢?让我们看看第一个使用注解的例子:

/**
 * @OA\Info(
 *      title="Product API",
 *      version="0.1"
 * )
 */
class ProductController
{
    /**
     * @OA\Get(
     *      path="/product",
     *      operationId="getProducts",
     *      @OA\Parameter(
     *          name="limit",
     *          in="query",
     *          description="How many products to return",
     *          required=false,
     *          @OA\Schema(
     *              type="integer"
     *          )
     *      ),
     *      @OA\Response(
     *          response="200",
     *          description="Returns the product data"
     *      )
     * )
     */
    public function getProducts(): array
    {
        // ...
    }
}

根据 DocBlocks 中的信息,swagger-php 将以 YAML 文件的形式返回我们的 API 文档,该文件看起来与前面的示例一模一样。但是,既然我们可以直接编写 YAML 文件,为什么还要使用 swagger-php呢?

事实上,并不是每个人都希望在代码中包含大块的文档,而且根据你希望记录的详细程度,这些文档可能会比上例中的大得多。不过,如果你想到一个有许多端点的应用程序接口,这些端点分散在代码中的各个控制器中,你可能已经意识到了这样做的好处:所有需要的元信息都存储在代码附近,因此,如果对端点进行了更改,开发人员只需修改 DocBlock 注释,比在其它文档或 wiki 中进行更改要容易得多。由于注释是代码的一部分,因此更改也已经在版本控制之下。最后,使用 swagger-php 的决定权在你或团队手中。

在本章的内联文档部分,我们将讨论为什么 DocBlocks 不是存储元信息的最佳位置。幸运的是,自 PHP 8.0 以来,我们有了一个更好的地方来存储元信息,那就是属性,我们在第 6 章 "PHP 在进化—​贬值和革命 "中已经谈到了属性。

在讨论为什么属性是更好的选择之前,让我们先看看如何使用属性来记录我们的端点:

use OpenApi\Attributes as OAT;

#[OAT\Info(
    version: '0.1',
    title: 'Product API',
)]
class ProductController
{
    #[OAT\Get(
        path: '/v2/product',
        operationId: 'getProducts',
        parameters: [
            new OAT\Parameter(
                name: 'limit',
                description: 'How many products to return',
                in: 'query',
                required: false,
                schema: new OAT\Schema(
                    type: 'integer'
                ),
            ),
        ],
        responses: [
            new OAT\Response(
                response: 200,
                description: 'Returns the product data',
            ),
        ]
    )]
    public function getProducts(): array
    {
        // ...
    }
}

诚然,属性语法看起来可能有点陌生。不过,我们建议使用属性而不是注释,因为属性有很多方便的优点。首先,它们是真正的代码;它们会被 PHP 解释器解析,而且你的集成开发环境也能支持你编写它们。在前面例子的第一行,你可以看到我们需要导入 OpenApi\Attributes 命名空间来使这个例子生效。

在这个命名空间中,你会发现这里引用的实际类。这些文件位于项目的供应商文件夹中。这样,你就可以使用自动完成等功能,如果有不正确的地方,你将立即从集成开发环境中得到反馈,从而使编写此类文档变得更加容易。

最后一步是根据代码生成 YAML 文件。当然,这一步可以在我们在第 11 章 "持续集成 "中介绍的 CI 管道中自动完成。你可以在本书的 Git 仓库中找到使用示例。

你可能会问:我能用这些 API 文档做什么?当然,它已经可以作为其它开发人员的文档,但它的作用远不止于此。例如,你可以将其导入 Insomnia 或 Postman 等 HTTP 客户端。这样,你就可以立即开始与 API 进行交互,而无需查找确切的模式。

另一个用例是帮助你为 API 编写功能测试。有一些软件包,如 PHP Swagger Test (https://github.com/byjg/php-swagger-test) 或 Spectator ( https://github.com/hotmeteor/spectator ),可以帮助你根据 OpenAPI Specification (OAS) 编写测试,OAS 也可以被视为一种契约。例如,你可以测试特定 HTTP 状态代码返回的对象是否与合约中指定的一致。

最后,也可能是最重要的用例,是将 OAS 规范与 Swagger UI (https://github.com/swagger-api/swagger-ui) 结合使用,后者是 API 的可视化交互式文档。

下面的截图显示了我们的示例 API 的样子:

image 2023 11 12 23 03 23 259
Figure 2. Figure 13.2: Swagger UI

探讨 OpenAPI 和 Swagger UI 的所有可能性将超出本书的范围。如果你想了解更多,我们建议你查看这两种工具。

OpenAPI 替代方案

你还可以使用其它格式,如 RESTful API Modeling Language (RAML) (https://raml.org) 或 API Blueprint (https://apiblueprint.org ),我们对任何解决方案都不持异议。