后端API设计文档代码生成【免费下载链接】swagger-phpA php swagger annotation and parsing library项目地址https://gitcode.com/gh_mirrors/sw/swagger-php点击查看免费下载导读本文以 swagger-php 的官方继承参考文档docs/reference/inheritance.md为骨架结合仓库中 Augmenter/Inheritance、Augmenter/PathItems、Specification/PathItemHierarchy 等源码与对应测试完整讲解两条继承管线Schema 继承通过allOf实现 schema 组合与PathItem 继承路径前缀组合与元数据克隆。读完本文你将掌握swagger-php 对父类、Trait、接口的三种遍历规则及首个带 Schema 祖先即停止的决策逻辑allOf重组与重复$ref去重的底层实现以及如何通过PathItemHierarchy查询管辖链、组合前缀、克隆 tags/security/responses 等元数据从而在自己的控制器层级设计中精准预测最终的 OpenAPI 输出。在 swagger-php 的 spec 管线中继承的语义镜像 PHP 的继承语义具体使用示例可参考 docs/guide/spec-attributes.md#inheritance。一、两条继承管线概览在 swagger-php 中PHP 类继承关系的处理由两个 augmenter 分工完成它们都挂在Resolve阶段源码中group(): string|\BackedEnum均返回Group::ResolveAugmenter类名职责Schema 继承OpenApi\Augmenter\Inheritance对每个有类反射器的 schema遍历类层级展开为allOf组合或内联属性合并PathItem 继承OpenApi\Augmenter\PathItems沿类层级组合路径前缀克隆 tags/security/responses 等元数据到 operation并产出路径级输出值得注意的是Inheritance本身是一个委派型augmenter它内部把工作交给 Inheritance\Schemas负责 schema 组合与 Inheritance\Operations负责把无 PathItem 祖先类的 operation 克隆给子类两个子实现见 src/Augmenter/Inheritance.php。从 tests/Augmenter/InheritanceTest.php 可以看到典型调用链先跑Inheritance再跑Names生成组件名最后跑Refs把$ref解析为组件指针并重组allOf。二、Schema 继承PHP 类层级 → allOf 组合2.1 决策规则Inheritance\Schemas::resolve()遍历$payload-schemas只处理getClassReflector()非空的 schema没有类反射器的 schema 直接跳过。对每个 schema它按固定顺序处理三类关系父类Parents——沿getParentClass()链向上走Trait——类自身的直接 trait以及无 schema 祖先的 trait接口Interfaces——类自身的直接接口。对每个遇到的祖先决策规则如下表祖先带有#[Schema]动作是否继续向上遍历是向该 schema 的allOf追加一个$ref停止仅父类适用否将该祖先的#[OA\Property]成员内联合并继续2.2 父类链遍历遇到带 Schema 的祖先即停止文档给出的示例为class C extends B extends A三种情形若B带 schema →C.allOf得到$ref: B遍历停止若B不带 schema 但A带 → B 的属性合并进 CC.allOf得到$ref: A若两者都不带 schema → B 和 A 的属性都合并进 C。停在第一个带 schema 的祖先这一规则避免了冗余引用——因为 B 的 schema 本身如果需要 A会在它自己的展开过程中组合 A。在 Schemas.php 的 expandParents() 中循环体是$parent $reflector-getParentClass(); while ($parent ! false) { $parentSchema $index-findSchema($parent-getName()); if ($parentSchema instanceof OA\Schema) { $this-addAllOfRef($schema, $parentSchema); break; // 命中带 Schema 的祖先停止 } $this-mergeMembers($parent, $existingProperties, $merged); $parent $parent-getParentClass(); }其中addAllOfRef()生成引用的关键细节src/Augmenter/Inheritance/Schemas.php#L131-L140引用名取$referenced-component ?? $referenced-getShortClassName()再用JsonPointer::ref(components, schemas, $name)构造$ref。之所以不用of()方法是因为of()的 title 兜底逻辑会在Namesaugmenter 运行之前错误地按 title 命名类 schema——这是一个易踩的源码级细节。2.3 Trait 处理Trait 从两个来源收集src/Augmenter/Inheritance/Schemas.php#L82-L110类自身的直接 trait无 schema 祖先的 trait——augmenter 沿父类链向上走直到遇到带 schema 的祖先为止沿途收集每个无 schema 祖先的直接 trait。这样做保证了当一个无 schema 的父类使用了带#[Schema]的 trait 时组合关系依然被捕获。trait 自身同样遵循决策表——带 schema 的 trait 贡献$ref不带 schema 的 trait 内联合并属性。例如 tests/Fixtures/Augmenter/Hierarchy/Spec/TraitWithSchema.php 对应的期望输出tests/Fixtures/Augmenter/Hierarchy/expected.yamlClassUsingTraitWithSchema: allOf: - #/components/schemas/NameTrait properties: - age2.4 接口处理只处理 schema 类自身的直接接口expandInterfaces()中调用attributeFactory-getDirectInterfaces($reflector)见 src/Augmenter/Inheritance/Schemas.php#L117-L129。与父类不同接口没有遇到首个带 schema 即停止的规则——所有带 schema 的直接接口都贡献一个$ref。2.5 属性合并当祖先没有 schema 时它的#[OA\Property]成员被合并进当前 schema。去重按属性名进行如果 schema 已声明同名属性则跳过祖先版本mergeMembers()中用property ?? reflector name兜底属性名见 src/Augmenter/Inheritance/Schemas.php#L147-L160。所有从祖先合并来的属性会作为一个整体块前置到 schema 自身的属性之前顺序为 augmenter 的访问顺序父类从最近的到根→ 类的直接 trait按use顺序→ 各无 schema 祖先的 trait → 接口 → 最后是 schema 自身属性。对应实现是resolve()末尾的$schema-properties [...$merged, ...($schema-properties ?? [])]src/Augmenter/Inheritance/Schemas.php#L49-L51。源码注释特别说明合并块在所有来源收集完毕后一次性前置而不是每处理完一个来源就前置一次否则每个兄弟组尤其是 trait会被逆序排放。这条顺序规则有明确的测试佐证tests/Fixtures/Augmenter/Hierarchy/Spec/ClassUsingOrderedTraits.php 继承无 schema 的PlainParent并按use PlainTrait; use OrderedTrait;顺序使用两个 trait其期望属性顺序为parentProp, traitProp, orderedProp, ownProp见 expected.yaml恰好对应父类 → 直接 traituse 顺序→ 自身属性的访问序。2.6 allOf 重组mergeAllOf展开完成后如果一个 schema同时拥有allOf条目和自身属性Refsaugmenter 的mergeAllOf()会把属性移动到一个专属的allOf条目——一个type: object的匿名 schema使最终结果是纯粹的allOf组合User: allOf: - $ref: #/components/schemas/BaseModel - type: object properties: email: type: string实现位于 src/Augmenter/Refs.php#L171-L179if ($schema-allOf null || $schema-properties null) { return; } $schema-allOf[] new OA\Schema(type: object, properties: $schema-properties); $schema-properties null;从 tests/Fixtures/Augmenter/Hierarchy/expected.yaml 看ChildOfParentWithSchema的期望形态正是allOf: [#/components/schemas/ParentWithSchema] 自身childProp属性验证了这一重组。2.7 重复 $ref 去重dedupAllOfRefs如果你显式声明了一个与 augmenter 将要添加的相同的allOf条目例如既继承了某个类又手动引用它Refsaugmenter 的dedupAllOfRefs()会去重——最终只保留一个$ref。去重在类字符串解析完成之后运行因此ref: Parent::class与#/components/schemas/parent会被视为同一个条目。这是有意的设计Refs在resolveRefRefs()和resolveFQCNRefs()把所有Schema\Ref折叠为字符串、并把 FQCN 改写为组件指针之后才执行去重src/Augmenter/Refs.php#L181-L202这样继承 augmenter 添加的指针与用户手写的 FQCN在字符串层面就一致了。源码注释也提醒resolveRefRefs()只在 ref map 非空时运行因此去重键对Schema\Ref对象形态做了防御性处理。三、PathItem 继承前缀组合与元数据克隆3.1 前缀组合Prefix Composition每个#[OA\PathItem]都可以声明prefix。augmenter 沿类层级向上遍历把各 PathItem 的前缀组合起来#[OA\PathItem(prefix: /api/v1)] class BaseController {} #[OA\PathItem(prefix: /users)] class UserController extends BaseController {}解析时从类走向根按**祖先顺序根在前**收集前缀并拼接/api/v1 /users → /api/v1/users组合后的前缀被前置到每个 operation 的path上。若UserController中某个 operation 声明path: /{id}最终路径为/api/v1/users/{id}。前缀拼接的细节在 PathItemHierarchy::prefixFor()每个前缀先trim($prefix, /)去掉首尾斜杠非空部分按根在前顺序收集再用/ . implode(/, $parts)连接没有前缀时返回空字符串。pathFor()则在此基础上把 operation 声明的路径去掉前导斜杠后拼接src/Specification/PathItemHierarchy.php#L161-L175。实测行为见 tests/Augmenter/PathItemsTest.php 的两个数据组2 层PathItemBaseControllerprefix: /api/v1见 tests/Fixtures/Augmenter/PathItemBaseController.phpPathItemUserControllerprefix: /users声明path: /list与path: /{id}见 tests/Fixtures/Augmenter/PathItemUserController.php→ 期望路径[/api/v1/users/list, /api/v1/users/{id}]3 层PathItemGrandparentController/api→PathItemMiddleController/v2→PathItemLeafController/orders声明path: 与path: /{id}→ 期望路径[/api/v2/orders, /api/v2/orders/{id}]。注意path: 的 operation 最终前缀本身就是完整路径说明空路径被前缀完整覆盖。3.2 管辖 PathItemGoverning PathItem一个 operation 的管辖PathItem是从声明该 operation 的类向上走直到遇到带#[PathItem]的类为止。因此没有#[PathItem]的类中的 operation依然可以继承祖先类的 PathItem。实现上PathItemHierarchy在构造时把 specification 中所有component null即非可复用组件的 PathItem 按类名建立索引src/Specification/PathItemHierarchy.php#L38-L51chainFor()从该类沿getParentClass()收集所有 PathItem 后array_reverse()得到最外层祖先在前的链条src/Specification/PathItemHierarchy.php#L93-L111。3.3 通过 PathItemHierarchy 查询链条链条查询统一由Specification\PathItemHierarchy提供入口是Specification::buildPathItemHierarchy()$hierarchy $specification-buildPathItemHierarchy(); $hierarchy-chain(UserController::class); // 所有管辖 PathItem最外层祖先在前 $hierarchy-governing(UserController::class); // 仅最近的那一个 $hierarchy-forOperation($operation); // operation 声明所在类的链条 $hierarchy-prefixFor($operation); // 该链条组合出的前缀无则 $hierarchy-pathFor($operation); // operation 声明路径加上前缀后的结果 $hierarchy-parametersFor($operation); // 链条声明的参数按 in:name 键控使用要点文档明确说明源码亦有注释印证pathFor()读取的是声明时的路径。PathItemsaugmenter 会把组合后的路径写回 operation见下文所以pathFor()必须在 PathItems augmenter运行之前调用否则前缀会被加两次src/Specification/PathItemHierarchy.php#L153-L160。parametersFor()不包含 operation 自身的参数。当链条中两个 PathItem 声明了同名同位置的参数时更近者胜出。参数键由ComponentIndex::parameterKey()生成所以仅是一个 ref 的参数按其组件的名称与位置键控完全没有身份的参数会被丢弃src/Specification/PathItemHierarchy.php#L190-L205。chain(UserController::class)只接受类字符串通过ClassReflector::tryReflect()反射类加载失败时返回空链而非抛异常。PathItemHierarchy实例是构建时刻 specification 中路径项的快照若中途新增/移除 path item需要重新构建源码类注释。文档特别强调在该链条之外读取任何 PathItem 上的信息既不对任何 operation 生效也不能说明任何问题——因此 augmenter 或集成方读取路径级元数据时应当询问 hierarchy而不是自己用反射去读。3.4 元数据克隆Metadata CloningPathItemsaugmenter 会把 PathItem及其祖先上的元数据克隆到 operation。对每个 operation先通过forOperation()取管辖链把组合后的路径写回$operation-path再调用cloneMetadata()src/Augmenter/PathItems.php#L36-L50。三个元数据字段的合并行为如下表属性合并行为tags从所有祖先累积去重后追加到 operation 已有 tags 之后security从所有祖先累积按 scheme 名去重responses从所有祖先累积按响应码去重——operation 自身声明的响应优先三者都沿层级加法式累积——链条上每个祖先的 PathItem 都会贡献collectMergedMetadata()遍历整条链收集见 src/Augmenter/PathItems.php#L105-L128。对应实现细节src/Augmenter/PathItems.php#L60-L96tagsarray_values(array_unique([...$operation-tags ?? [], ...$merged[tags]]))——operation 自身 tags 在前祖先 tags 追加在后。security先收集 operation 已有的 scheme 名再逐个追加祖先中 scheme 名不重复的 requirement。responses先收集 operation 已有的响应码再追加祖先中响应码不重复的 response。测试印证tests/Augmenter/PathItemsTest.phptestExplicitTagsMergedAdditivelyoperation 已有[Custom]继承后得到[Custom, Users]——自身在前、祖先追加testSecurityClonedToOperationsPathItemBaseControllerPathItemUserController场景下每个 operation 都获得bearerAuth安全要求testSharedResponseNotOverriddenWhenCodeExistsPathItemSharedResponseController见 tests/Fixtures/Augmenter/PathItemSharedResponseController.php的 GET operation 自带 401 响应即使 PathItem 也声明了 401最终保留的是 operation 自己的Custom unauthorized——验证operation 自身响应优先。3.5 路径级参数Path-level ParametersPathItem 的parameters从祖先 PathItem 继承并在 OpenAPI 输出中以路径级path level呈现。去重键是name in组合子类的参数优先于祖先。实现是mergeAncestorParameters()src/Augmenter/PathItems.php#L191-L218取链条去掉自身后的祖先部分逆序近祖先先合并用$param-name . : . ($param-in ?? )作为去重键自身参数天然保留。测试testSharedParametersEmittedAtPathLevel验证PathItemSharedResponseController声明X-Request-Id头参数后所有带路径的 PathItem 均含该参数$pi-parameters[0]-name X-Request-Id。3.6 路径级输出Path-level Output带有parameters、summary、description或servers的 PathItem 会在 OpenAPI 文档中产出路径级输出。augmenter 解析哪些 operation 路径映射到这个 PathItem并为每个路径输出路径级属性resolvePathItemPaths()见 src/Augmenter/PathItems.php#L130-L161跳过可复用组件component ! null的 PathItem只有hasSpecProperties()为真四者任一非空时才产出路径级输出一个 PathItem 管辖的路径不止一条时会克隆 PathItem 并分别设置不同path避免同路径多条路径项冲突。测试testPathItemWithSummaryGetsPathPathItemPlainController的 PathItem 最终产出[/products, /products/{id}]两条带路径的条目。3.7 仅父类与 Schema 继承的刻意差异只沿父类走。Trait 和接口不被跟随——这与上文 Schema 继承父类 trait 接口都处理正好相反。这个差异是有意的前缀链是有序的而 trait/接口的图结构无法提供可组合的顺序。带#[PathItem]的 trait 或接口仍会被索引chainFor()只认类但 trait 上的 PathItem 作为 attribute 依然存在只是永远不会管辖使用它的类。文档中的原文总结值得记住Anything read off aPathItemoutside that chain applies to nothing and says nothing while it does——链外读取的 PathItem 信息既不生效也不说明问题。四、被文档省略但不可或缺的一环Operation 克隆Inheritanceaugmenter 委派的第二个子实现 Inheritance\Operations 解决了一个隐蔽问题无 PathItem 祖先类中的 operation 如何被子类继承。当抽象或未标注父类声明了 operation 时这些 operation 在组装阶段以父类为类上下文被收集。而带 PathItem 的子类期望继承这些 operation以便应用前缀/tag/security 组合。实现分三步findInheritableOperations()找出所有所属类没有 PathItem的 operationsrc/Augmenter/Inheritance/Operations.php#L56-L70cloneToChildren()对每个 PathItem 类沿父链上溯若祖先已组装出 operation 则clone并setReflector($reflector)重新关联到子类否则discoverOperations()直接扫描祖先类的方法并提取 operation同时把原始 operation 标记移除src/Augmenter/Inheritance/Operations.php#L78-L114从 specification 中删除原属无 PathItem 祖先的 operation——因为它们没有自己的 PathItem 上下文。该类注释明确要求必须运行在 PathItems augmenter 之前这样被继承的 operation 才能参与前缀解析。测试覆盖非常完整tests/Augmenter/InheritanceTest.phptestInheritedOperationClonedToChild组装后 operation 的类名从抽象父类变为子类testPathItemPrefixAppliedToInheritedOperation继承的 operation 路径变为/invoicestestTagsClonedToInheritedOperation继承的 operation 获得[Invoices]tagstestDiscoveryWhenOnlyChildAssembled只组装子类时未访问过的祖先中的 operation 也能被发现并关联到子类testDirectOperationsNotDuplicated已收集的 operation 不会被重复克隆。五、组合使用一个完整的控制器继承示例把两条管线结合起来看一个完整的实战场景。基于仓库真实 fixturetests/Fixtures/Augmenter/PathItemUserController.php控制器层设计如下use OpenApi\Spec as OA; #[OA\PathItem(prefix: /api/v1)] class BaseController {} #[OA\PathItem( prefix: /users, tags: [Users], )] #[OA\Security\Requirement(scheme: bearerAuth)] #[OA\Parameter\Path(name: tenant, schema: new OA\Schema(type: string))] class UserController extends BaseController { #[OA\Operation\Get(path: /list)] #[OA\Response(response: 200, description: User list)] public function list() {} #[OA\Operation\Get(path: /{id})] #[OA\Response(response: 200, description: Single user)] public function get(int $id) {} }经过Inheritance→Names→Refs→PathItems等 Resolve 阶段处理后路径前缀/api/v1BaseController/usersUserController组合为/api/v1/users/list与/{id}两个 operation 分别成为/api/v1/users/list与/api/v1/users/{id}tags[Users]克隆到两个 operationsecuritybearerAuth安全要求克隆到两个 operation路径级参数tenant路径参数以路径级出现在输出中。在 schema 侧若BaseModel带#[OA\Schema]而User extends BaseModel也带#[OA\Schema]则User输出为纯allOf组合$ref指向BaseModel 匿名type: object的自身属性块与 docs/guide/spec-attributes.md#inheritance 中的示例完全一致。六、小结swagger-php 的继承机制可以浓缩为三句话Schema 侧父类/直接 trait/直接接口带#[Schema]就进allOf父类遇首个带 schema 祖先即停不带就内联合并属性最终由Refs完成allOf重组与重复$ref去重PathItem 侧只沿父类走前缀按根在前顺序组合后前置到 operation 路径tags/security/responses 加法式累积克隆参数按name in去重并以路径级输出统一入口链条查询全部走Specification::buildPathItemHierarchy()避免各调用方自行反射导致链外读取的无效信息。如果要在自己的项目中进一步验证这些行为可以直接运行仓库测试tests/Augmenter/InheritanceTest.php与tests/Augmenter/PathItemsTest.php其期望输出文件 tests/Fixtures/Augmenter/Hierarchy/expected.yaml 是一份可直接对照的组合结构清单。赞分享后端API设计文档代码生成【免费下载链接】swagger-phpA php swagger annotation and parsing library项目地址https://gitcode.com/gh_mirrors/sw/swagger-php点击查看免费下载相关推荐GyroFlow Windows 启动失败排查手册从闪退、卡死到花屏的四条修复链GyroFlow Windows 启动失败排查手册从闪退、卡死到花屏的四条修复链 GyroFlow 是一款基于陀螺仪数据做视频防抖的稳定化软件但不少用户在视频处理桌面应用音视频TypeGraphQL 继承机制实战用 TypeScript 类继承组合 GraphQL 类型与 ResolverTypeGraphQL 继承机制实战用 TypeScript 类继承组合 GraphQL 类型与 Resolver TypeGraphQL 的核心设计理念是桌面应用SQLAlchemy 映射类继承层次完全指南joined、single 与 concrete 三种继承模式实战SQLAlchemy 映射类继承层次完全指南joined、single 与 concrete 三种继承模式实战 导读 本文以 SQLAlchemy 官方文档《数据库后端ORM上一篇终极指南如何使用LinkedIn API Ruby封装简化职业网络开发下一篇Proselint错误处理与调试解决常见问题和异常情况创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?