- 后端
- API设计
- 文档
- 代码生成
【免费下载链接】swagger-php
A php swagger annotation and parsing library
本文是 swagger-php 项目维护者与贡献者编写文档、Pull Request 描述与提交信息时遵循的完整规范,涵盖「事实只说一次」「细节只放一处」「复用而非复制」等核心写作原则,以及文档结构、拼写、语气与逐项审查清单。文中同时结合 docs/dev/docs-toolchain.md 与tools/目录下的真实生成器源码、tests/DocsAccuracyTest.php 等测试佐证,说明这些规范如何在 swagger-php 中落地为可自动校验的工程机制。读完本文,你将掌握一套可以直接复用到任何开源 PHP 项目文档流程中的编写与校验方法。
这些规则在哪里适用
本规范的大多数内容讨论的是精确与克制,这两点不会因为写作载体不同而改变。它们同时适用于:
docs/目录下的手写页面;src/中的 docblock 注释;- Pull Request 描述与提交信息(commit message)。
docblock 会被拼接到生成的参考页面中(例如docs/reference/annotations.md由src/Annotations/的 docblock 生成),所以一个不精确的 docblock 会以正式发布文档的形式对外呈现。换句话说,注释质量直接影响发布质量。
以下三个部分只针对文档页面,不适用于 PR 描述和提交信息:
- 结构(Structure);
- 拼写(Spelling);
- 审查清单中与生成页面相关的条目。
Pull Request 描述与提交信息的互补约束
PR 描述描述的是这次变更,只有在变更无法被理解时才补充上下文。工作是如何被发现的、还调查过什么、可能引出什么后续工作,都不属于 diff 的一部分。CONTRIBUTING.md 中给出了模板和标题格式(type(Scope): subject,例如feat(Spec): add encoding shortcut)。
提交信息正文则相反,它记录的是这个 diff 做了什么,而不是它为什么存在。解释动机的句子属于 PR 描述。常规验证("已通过测试")也不是关于 diff 的事实——变更经过测试是默认假设,在正文里写它等于什么都没说。
事实只说一次
不要重复页面自身结构已经确立的内容。例如 spec 管线的 beta 状态,已经由以下多个载体共同承担:
- docs/guide/modes.md 中的Status行;
- 章节标题中的
(beta); ::: warning Beta提示块;- 版本时间线;
- 页面标题中的 🧪 图标。
此时再写一句"这是唯一没有标记 beta 的模式"就毫无增量,而且会随着上述那些真实载体独立腐烂。
判断标准只有一个:读者是否能从这句话中学到原本看不到的信息。"仅仅读起来通顺"是不够的。这条原则适用于成熟度、版本要求、该用哪个命名空间、某个模式做什么等所有内容。
营销填充与复述填充是同一种缺陷。把"稳定的、生产就绪的模式"换成"唯一没有标记 beta 的模式"并不算改进。
第三种形式是解释"事实为什么重要"的从句,例如 "which is what makes X possible"、"which matters because…"、"which is the point"。这种从句本身不携带任何事实。直接陈述事实然后停止即可。如果一个事实的重要性无法从事实本身看出来,那说明这个事实本身就是错的。
细节只属于一个地方
导览(orientation)可以出现在多个页面,因为不同的人会从不同入口到达同一主题,常常需要重复介绍。但具体细节(detail)绝不能被重复展开。同一份细节写两遍,两份副本就会各自漂移,而且没有任何机制能指出哪一份已经过时。
有效的分工方式是按深度切分,而不是按主题切分:
- 面向用户的页面说明"某个东西做什么、为什么重要",然后链接到更深处;
- 面向贡献者的页面承载机制、规则和精确清单;
- 两者互不重复对方的另一半。
docs/reference/architecture.md与docs/dev/pipeline.md就是现成的范例。参考页面用一段话描述 Assembler 后链接到内部文档;内部文档则独占 slot map、解析算法和根属性清单。在这次拆分之前,两个页面用不同措辞各自描述了 slot map、augmenter 排序和类层次结构。
当你要第二次写某个东西时,那就是应该改成链接的信号。
复用内容,不要复制内容
当同一段文字需要在多个输出中出现时,应该把它提取出来再引入。复制是腐烂的根源。swagger-php 已经在使用两种机制:
docs/snippets/preamble_*.md:markdown 片段,由参考文档生成器拼接到对应页面中。从源码看,DocGenerator::snippetContent() 读取docs/snippets/preamble_{type}.md,找不到文件时返回null,找到时把内容追加到Renderer::preamble()生成的页面框架之后(见 Renderer::preamble())。编辑片段即可更新生成页面。仓库中现存六个片段:preamble_annotations.md、preamble_attributes.md、preamble_spec-attributes.md、preamble_processors.md、preamble_augmenters.md、preamble_extension-points.md,均位于 docs/snippets/。<<< @/snippets/path.php:把真实且经过测试执行的源文件转写到页面中,因此被记录的示例永远不会与真实运行的代码脱节。例如Renderer::exampleSection()就通过这种转写把docs/examples/specs/{name}/attributes|annotations|spec/{file}三份实现分别注入<codeblock>的三个模板槽(at/an/spec)中(见 Renderer.php)。
第三种机制<!--@include: ./path.md-->由 Vitepress 支持,但本仓库尚未使用。它适合在手写页面之间共享散文。
优先提取片段而不是粘贴。凡是内容可以派生出来的地方,优先生成而不是复制——docs/dev/docs-toolchain.md 说明了哪些内容已经由生成器产出。
写主题本身,不要写"关于文档"的注脚
一个页面描述的是它的主题。除非文档本身就是主题(如本页或工具链笔记),页面不需要解释文档是如何产生的、为什么某个交叉引用是可信的。
任何页面——无论哪种类型——都不需要宣布自己的读者群或为自己的存在辩护。标题和把读者带到这里来的链接已经完成了这个工作。
原文档给出了三个真实的反面案例(均出自同一页面):
The Augmenters reference —generated from the pipeline itself, so it cannot fall out of step with the code
see Spec pipeline internals,which is written for people changing the pipeline rather than using it
This page stays at the level of "what happens and in what order"
这三句话各自都属实,但没有任何一句回答了读者唯一的问题:这个东西是怎么工作的。某个参考页是否是生成的,只对维护它的人有意义;读者只需要跟着链接走。
最常见的标志是挂在链接后面解释链接的从句。让链接文字自己完成工作:see [Spec pipeline internals]就已经完整了,逗号之后的任何内容都是页面在谈论自己。
只枚举封闭集合
一个需要别人记得去更新的列表,注定会出错。只有当集合是封闭的——即它在代码中某处被定义,且只随那段代码变化而变化——才值得枚举。
生成页面就是封闭集合:tools/docgen.php 中的$outputMap明确列出了七组输出目标(reference/annotations.md、reference/attributes.md、reference/spec-attributes.md、reference/processors.md、reference/augmenters.md、reference/extension-points.md、guide/examples.md),因此一张列出这些页面的表格可以一直成立,直到有人编辑生成器。"docs/下其他所有内容"则是开放集合——任何人添加一个页面它就变大,所以它应当被描述而不是被列举。
同样的检验也适用于挂在列表上的计数。集合会变,数字就会变,所以当"六个页面"这种计数所概括的枚举被删掉时,这个计数也要一并删掉。
不要手写可以被捕获的内容
命令输出、默认配置、API 清单都应该从真实的东西复制,而不是凭想象编写。原文档记录过一个教训:某次手写的openapi -h代码块长期携带了错误的格式、错误的--mode描述、一个并不存在的--version默认值,以及四个缺失的选项——只因为一直没有人真正运行过它。
正确做法是捕获真实输出:
./bin/openapi -h # capture this ./bin/openapi --mode spec -D src这一规范已经被固化为自动化测试。DocsAccuracyTest::testCliHelpOutputMatchesDocs()(见 tests/DocsAccuracyTest.php)从docs/guide/generating-openapi-documents.md中提取-h帮助块,实际执行bin/openapi -h,并断言两者中命令行自定义选项部分完全一致。Symfony Console 自动追加的内置选项(从-h, --help开始)会被ownHelpOptions()剥离,因为它们的措辞随 Symfony 版本变化,文档中仍展示完整输出。这条测试确保手写的帮助文本一旦漂移,CI 会立即失败。
不要引用行号
Class::method()能扛住重构,Foo.php:110不能。ADR-001 中曾有三处行号引用,其中两处已经腐烂。
这一原则与仓库内部的"引用规范"相互印证:DocsAccuracyTest::testResultMethodListingMatchesDocs()(tests/DocsAccuracyTest.php)用正则从docs/reference/builder.md提取所有$result->xxx(方法引用,再与Builder\Result类的全部公有实例方法逐一比对——它验证的正是"方法名"这一层引用,而非行号。
不要声称你未验证过的东西
如果某个页面要对比 spec 模式与 classic 模式,那就把两者都运行一遍。像"classic 模式可能会输出数组形式"这种模糊措辞,是"基于推理而非实测"的信号,读者无法据此行动。要么验证后直说,要么只记录你确知的行为。
这条规则在DocsAccuracyTest中有直接的机器化版本:testCompilerTableMatchesDocs()(tests/DocsAccuracyTest.php)对文档中的每个 Compiler 条目都实际实例化对应的类、验证它实现CompilerInterface、并断言getVersion()以文档标注的版本号开头。
不要与所在页面自相矛盾
两个来自同一页面的例子:
- 属性被描述为"不可变(immutable)",而紧接着的 augmenter 章节却在描述向它们写入数据;
- "所有模式产生相同输出",紧跟着却是一张模式差异表。
如果某个声明需要读者无视下一段才能成立,那就删掉这个声明。
语气(Voice)
短句、平实、一个句子一个意思。一句话携带三个限定条件时,拆成两三句。
- 不用破折号(em dash)。改用逗号、括号、冒号或另起一句;
- 不用分号连接从句。拆分句子;
- 不用缩写形式:写 "does not"、"it is";
- 克制。功能由"它做什么"来描述,而不是"它有多好";
- 解释描述代码:如 "
swagger-phpwill automatically register the@OAalias"、"can be used to"。指示性语句以 "you" 称呼读者; - 不用 "not X but Y" 句式,也不要在结尾写 "In short" 式的总结。
拼写(Spelling)
代码库的拼写是混合的:normalise与serialization并存,behavio(u)r两种拼写在src/和docs/中都出现。没有单一约定可循。
规则是:匹配你正在编辑的文件已有的拼写,其他的一律不动。批量修正造成的改动远多于它修复的问题。
结构(Structure)
docs/snippets/下的代码片段由测试套件执行。具体见 docs/dev/testing.md:DocSnippetsTest会运行每个docs/snippets/*_an.php,并对照对应的-3.1.0.yaml期望输出(跨适用的模式与实现);缺失的实现会被跳过而非判失败,但缺失或过期的-3.1.0.yaml是硬失败。新增一个 snippet 意味着要同时提供foo_an.php(annotations)、foo_at.php(attributes)、foo_spec.php(spec attributes)与foo-3.1.0.yaml四件套。仓库中现存的完整示例是 minimal_api_an.php(@OA注解风格)、minimal_api_at.php(属性风格)、minimal_api_spec.php(OpenApi\Spec风格)及其 minimal_api-3.1.0.yaml 期望输出。<codeblock>组件把注解(annotation)、属性(attribute)和 spec 变体渲染为标签页。三种实现必须保持同步。这也解释了为什么Renderer::exampleSection()要为每个示例文件同时生成at/an/spec三个模板槽。内部链接使用站点绝对路径(
/reference/architecture),而不是相对文件路径,这样页面在不同渲染深度下依然有效。在本文(仓库文档语境)中,这类链接的仓库相对等价物如 docs/reference/architecture.md、docs/dev/pipeline.md 均指向实际存在的文件。描述
classic与spec模式行为差异的散文,放在::: info 🧪 Mode difference容器中,这样当两条管线趋同后这些差异可以再次被集中列出。以模式为主题的页面会把这种区分体现在自身结构里,而不需要逐段标记。
审查文档变更(Reviewing documentation changes)
这是一套可以跑在文档 diff——无论自己的还是别人的——以及 PR 描述、提交信息上的检查清单。原文档特别说明:下面每一项都至少在这个代码库中抓住过真实缺陷。
检查失败时,修复源头而不是修复症状:错误的生成页面意味着错误的 docblock 或生成器,而不是需要手工编辑的页面。
可以直接运行的检查
- 生成页面未被改动。
composer docs:gen在 docs/dev/docs-toolchain.md 列出的页面上不应留下任何 diff。该脚本在 composer.json 中定义为@php tools/docgen.php;tools/docgen.php还支持按名称只跑部分生成器(如php tools/docgen.php aug proc)。 - 文中提到的每个
composer <script>都真实存在于composer.json。本仓库定义的文档相关脚本包括:docs:check(运行DocsAccuracyTest)、docs:gen、docs:dev(进入docs/运行vitepress dev)、docs:build(先docs:gen再构建静态站点)。 - 每个代码引用都能解析。对照源码检查类名、
Class::method()、文件路径、CLI 标志与配置键。这能抓住最有害的一类错误:自信的散文描述了一个已被改名或从未存在过的 API。 - 命令输出是捕获的,不是编写的。任何
--help文本、默认配置导出或 API 清单都应来自真实运行。这一点已由DocsAccuracyTest::testCliHelpOutputMatchesDocs()自动化。 - 没有行号引用。
Foo.php:123会静默腐烂,应引用Class::method()。 - 链接可解析。相对链接指向存在的文件。站点构建会对失效的内部链接报错,因此
composer docs:build覆盖了其余情况。 - 章节锚点已验证,或直接删掉。
#heading-slug链接只有在标题为纯单词时才安全,标点与连字符会使生成的 slug 有歧义。如果无法确认,就链接到页面本身。 - 没有易变值。计数("六个页面"、"约 27 个测试文件")、端口号、版本号等任何不编辑文档就会漂移的内容。要么从源头推导,要么定性描述,要么在工具自己声明时直接引用工具的声明。开发服务器自己打印 URL 时,应记录为"打印其 URL",而不是记录一个端口号。
- 版本与依赖要求与composer.json 一致。
- 没有营销或套话填充:"production-ready"、"seamless"、"powerful"、"clean"、"simply"、"robust"、"comprehensive"、"leverage"、"crucial"、"genuinely"。
- diff 中没有重复的套话。同一措辞出现两次,其中一次就是填充。
- 没有破折号,也没有连接从句的分号。两者都能在 diff 搜索中出现。
需要阅读才能完成的检查
- 没有枚举开放集合。列表只留给代码中定义、且仅随代码变化而变化的集合。任何会随有人添加文件而增长的东西,用描述代替列举。
- 细节只出现一次。两个页面可以介绍同一主题,但只有一页承载具体细节。当你第二次写某个东西时,改成链接。
- 声明是验证过的,不是推理出来的。"may"、"should"、"is expected to" 这类含糊词通常标志着没人测试过的声明。先运行,再直说,或者删掉。
- 页面没有自相矛盾。一个需要读者在两段之后无视它的声明,比没有声明更糟。
- 层级匹配读者。面向用户的页面说"做什么"并链向深处;面向贡献者的页面承载机制。任何一方都不该持有另一方的另一半。
- 没有关于文档本身的评论,除非文档就是页面主题。包括页面如何生成、链接为何可信。警惕挂在链接后面解释链接的从句。
- 没有页面宣布自己的读者群或目的。"This page is for…"、"these notes exist because…"。这条同样适用于贡献者页面。
- 没有解释前句重要性的从句,如 "which is what makes X possible" 或 "which matters because…"。陈述事实然后停止。
- 没有复述页面结构已显示的内容。状态表、标题或提示块已经告诉了读者,再写一句重复它只会腐烂。
- 拼写跟随所在文件。两种拼写都不值得批量修正。
- 共享内容被复用而非复制。见前文"复用内容"一节。
- 一个句子一个意思。带三个限定条件的句子要拆分。
- 解释描述代码。指示性语句以 "you" 称呼读者。
规范背后的工具链:docs:gen本身就是漂移检查
本规范的绝大部分约束,最终都落在一套可运行的生成与校验机制上,理解它才能理解"为什么这些规范能长期生效"。
生成页面:不要手工编辑
以下是 docs/dev/docs-toolchain.md 列出的生成页面及其来源,它们都由composer docs:gen写入:
| 页面 | 由什么生成 |
|---|---|
docs/reference/annotations.md | src/Annotations/docblock +docs/snippets/preamble_annotations.md |
docs/reference/attributes.md | src/Attributes/docblock +docs/snippets/preamble_attributes.md |
docs/reference/spec-attributes.md | src/Spec/docblock +docs/snippets/preamble_spec-attributes.md |
docs/reference/processors.md | src/Processors/docblock +docs/snippets/preamble_processors.md+ProcessorGenerator中的散文 |
docs/reference/augmenters.md | src/Augmenter/docblock +docs/snippets/preamble_augmenters.md+AugmenterGenerator中的散文 |
docs/guide/examples.md | 示例源码 + 各示例的docs/examples/specs/*/Readme.md |
要修改这些页面,就修改它们的源头然后重新运行composer docs:gen。直接编辑页面是白费功夫:下一次docs:gen会覆盖你的改动,composer docs:build(先跑docs:gen再构建)同样会覆盖。
注意部分散文活在内置于生成器的字符串字面量里,而不在任何 markdown 文件中。例如ProcessorGenerator::renderConfigSection()(见 tools/src/Docs/Reference/ProcessorGenerator.php)与AugmenterGenerator::renderConfigSection()(见 tools/src/Docs/Reference/AugmenterGenerator.php)中 "Configuration" 章节的-c与-D说明文字就是代码内的 heredoc 字符串。而DocGenerator::renderConfiguration()(tools/src/Docs/DocGenerator.php)负责统一渲染这些章节的公共框架,两个生成器只传入名词、模式标志与示例列表。
所有生成器都经由tools/src/Docs/Sections/渲染:Renderer只构建页面框架(prelude、标题、示例块),参数渲染为定义列表。描述是带自有段落断行的散文,类型经过 HTML 转义,两者都需要以 markdown 解析器不会再次读取的方式到达页面(ConfigSettingsSection的实现见 tools/src/Docs/Sections/ConfigSettingsSection.php)。
docs/下其余一切均为手写,包括顶层docs/examples/Readme.md——因为只有各示例自己的 Readme 才喂给生成页面。
docs:gen的正确用法
生成器是确定性的,且提交的输出应当与源头一致,因此:
composer docs:gen && git status --porcelain docs/任何输出都意味着已提交页面发生了漂移,重新生成的版本才是正确版本。值得在打开 PR 之前运行。在干净的树上运行,或者把git status限定在生成页面范围内——docs/reference/也含手写页面,全目录检查会把你自己对参考页的编辑也报成漂移。
命令速查
| 命令 | 说明 |
|---|---|
composer docs:gen | 重新生成上表列出的页面 |
composer docs:build | 先运行docs:gen,再构建静态站点 |
composer docs:dev | 本地预览,长时间运行。启动时打印其 URL 且不返回 |
什么才算"被文档记录的配置项"
docs/reference/augmenters.md与docs/reference/processors.md列出-c name.option=value可接受的选项。一个配置项够格的条件是:它是非对象类型的构造函数参数:
- 构造函数参数按约定就是公开配置契约;
- 对象类型参数(工厂、解析器、生成器)是协作者(collaborators)而非配置项,无法用 CLI 值表达。
这套逻辑在 DocGenerator::configurableParameters() 中实现了一次,并且镜像了Utils\Pipeline::getConfig()在运行时的报告。从源码注释看,该方法的判定规则是:若任何构造函数参数携带#[Config]属性,则这些参数即为可配置项(显式选择加入,与Pipeline::getConfig()完全一致);否则回退到"非对象类型构造函数参数"的启发式规则,目前src/Processors/下所有尚未迁移到#[Config]的类走此回退。二者靠手工对齐而非构造上保证:改了一个就要检查另一个。验证手段是./bin/openapi --mode spec -D src列出的配置项,必须与参考页面记录的完全一致。
值得知道的 CLI 行为
-D/--defaults打印解析后的默认配置,但仍然要求paths参数:./bin/openapi --mode spec -D src,而不是./bin/openapi --mode spec -D;--version设置的是目标OpenAPI版本,不是工具版本;- 帮助输出是标准 Symfony Console 格式,来自
./bin/openapi -h; - 未知的
-c键在spec模式下通过Pipeline::configure()报告为警告;在classic模式下则被静默忽略,因为配置走的是Generator::setConfig()。classic 模式下-c键拼写错误会无声失败。
已知粗糙点
AugmenterGenerator::renderConfigSection()与ProcessorGenerator::renderConfigSection()本质上是同一个方法写了两次,只差名词、模式标志与程序化示例。对其中一处的修复通常也要应用到另一处。
文档是测试套件的一部分
理解 docs/dev/testing.md 中的这一论断,就能明白为什么本规范不是空谈:两个测试直接验证文档与真实输出的一致性,只要添加文档而不同时添加对应物,两者都会失败。
DocSnippetsTest运行每个docs/snippets/*_an.php并与匹配的-3.1.0.yaml对比,跨适用的模式与实现。模式/实现配对是刻意的:spec 模式只跑 spec snippet,classic 模式从不跑它们,hybrid 模式跑全部三种;ExamplesTest对docs/examples/specs/*做同样的事,对照按版本区分的*-3.0.0.yaml/*-3.1.0.yaml/*-3.2.0.yaml期望文件。
这正是"改变管线输出会表现为一面墙的文档测试失败"的原因——那是测试套件在正常工作:期望的 YAML 文件就是管线输出规格本身,也是发布文档展示的内容。
实践建议:把这套规范移植到你的项目
swagger-php 的这套文档治理体系可以按以下优先级逐步移植到任何 PHP 项目:
- 先定规则:把"事实只说一次"、"细节只放一处"、"复用而非复制"、"不引用行号"、"不写未验证声明"写进
CONTRIBUTING.md,并把 PR 描述与提交信息分开约束; - 再建生成:凡是由源码 docblock、配置或命令输出派生的文档,一律走生成器(参考本仓库的
tools/docgen.php+tools/src/Docs/结构),手工页面只保留导览性内容; - 最后上校验:为每一条可机检的规则写一条测试——本仓库的
DocsAccuracyTest(帮助输出对比、编译器表格对比、Concerns 表对比、Result 方法清单对比)就是可复制的范本,composer docs:check一键运行。
如此,文档质量就从"靠自觉"变成了"靠 CI 兜底",这正是 docs/dev/writing-docs.md 这套规范在 swagger-php 中真实运转的方式。
- 后端
- API设计
- 文档
- 代码生成
【免费下载链接】swagger-php
A php swagger annotation and parsing library
相关推荐
文档标题
文档标题 概述 简要说明文档目的和范围 功能特性 功能点1:详细描述 功能点2:详细描述 安装指南 系统要求 Windows版本要求 硬件要求 安装步骤 1.
桌面应用系统编程EmDash 文档写作规范拆解:为 Astro CMS 编写准确、自然、可检索的技术文档
EmDash 文档写作规范拆解:为 Astro CMS 编写准确、自然、可检索的技术文档 本文基于 EmDash 仓库中 skills/writing emda
CMS后端前端插件系统Duktape 内部文档 reStructuredText 写作规范与排版约定
Duktape 内部文档 reStructuredText 写作规范与排版约定 导读 Duktape 是一个以可移植性和紧凑体积为目标的嵌入式 JavaScri
语言运行时嵌入式解释器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考