资讯详情

swagger-php 扩展点深度指南:从 Translator 到 Merger 定制 Spec 流水线

📅 2026/10/13 1:57:30 | 华诺云谱 👁 阅读
swagger-php 扩展点深度指南:从 Translator 到 Merger 定制 Spec 流水线
后端API设计文档代码生成【免费下载链接】swagger-phpA php swagger annotation and parsing library项目地址https://gitcode.com/gh_mirrors/sw/swagger-php点击查看免费下载swagger-php 的 Spec 流水线Assembler → Specification → Resolver → Augmenters → Compiler并非一条写死的链路而是一组可以替换、追加的扩展点。本文围绕仓库中的 扩展点参考 与 扩展点指南 展开逐一讲解 Translator、子类化 spec 属性、withSpecification()贡献、Resolver、Augmenter、Merger 与 Compiler 的作用机制、内置默认实现和自定义接入方式并结合 Builder 源码 与各契约接口说明底层调用关系。读完你既能理解每个钩子何时被调用、以什么顺序被调用也能直接写出可运行的框架接入代码。管道总览每个扩展点在哪里按什么顺序被触达官方 Spec 流水线架构 给出的完整链路是Source files → Assembler → Specification → Resolver → Augmenters → Compiler → OpenAPI document对应到 Builder::doBuildSpec() 的实现实际执行顺序是Assembler从反射对象读取属性、解析嵌套关系把根属性投入Specification的各个桶bucketwithSpecification()钩子若注册在 Resolver 运行之前拿到已装配的Specification并向其add()属性Resolver找出被引用但没有对应组件的 FQCN交给 resolver 链逐个尝试Augmenters以分组管道形式运行阶段顺序为resolve → reduce → augment见 Group 枚举Compiler按目标版本默认3.1.0可由#[OpenApi(version: …)]或setVersion()覆盖把Specification编译为文档。扩展点“按管道到达顺序”大致是Translators装配期每个 reflector 一次→ 子类化属性静态声明→withSpecification()贡献装配后、解析前→ Resolvers解析期→ Augmenters增强期→ Mergers贯穿 reduce 阶段→ Compiler输出期。Translators把“非 spec 属性”翻译成 spec 属性机制与运行时机Translator 在装配期间运行每个 reflector 调用一次。契约见 AttributeTranslatorInterface实现只需回答两个问题getAttributes()从该 reflector 上读取哪些原始 PHP 属性translate()返回装配器应当视为“已声明”的属性列表。translate()收到两个参数$created是本次读取到的原始属性实例$attributes是此前 translator 链已经解析出的累积结果。它可以向其中追加、替换或原样放行顺序遵循注册顺序。属性工厂会做最后一轮过滤只保留实现AttributeInterface的实例见 AttributeFactory::readAttributes()。每个 translator 还需要实现reset()装配器在每次顶层收集单元例如每个类开始时调用它用于清理跨层累积的状态。内置默认 TranslatorAttributeFactory 构造时注册了两个默认 translatorTranslator职责DefaultAttributeTranslator默认实现处理原生 OpenApi 属性用ReflectionAttribute::IS_INSTANCEOF读取所有实现AttributeInterface的属性OptionalPropertyAttributeTranslator为只写了OA\Schema或OA\Encoding的 schema 属性补上必需的OA\Property第二个 translator 正是“隐式OA\Property快捷写法”的实现来源当ReflectionProperty或构造函数参数上只有OA\Schema而无OA\Property时它在最前面插入一个new OA\Property()只有OA\Encoding时则插入new OA\Property\Encoded()见 源码第 52-56 行。OA\MediaType的快捷方式则由Augmenter\Shortcuts增强器负责OA\Parameter的快捷方式则是普通子类——三者的实现路径各不相同。自定义 Translator把框架路由属性变成 Operation指南 route_attribute.php 给出了一个对 swagger-php 一无所知的框架属性#[\Attribute(\Attribute::TARGET_METHOD)] final class Route { public function __construct(public string $path, public string $method get) { } }配套的 route_translator.php 把它翻译成 spec 属性final class RouteTranslator implements AttributeTranslatorInterface { public function reset(): void { } public function getAttributes( \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\ReflectionClassConstant $reflector, ): array { return $reflector-getAttributes(Route::class); } public function translate( array $attributes, array $created, \ReflectionClass|\ReflectionMethod|\ReflectionProperty|\ReflectionParameter|\ReflectionClassConstant $reflector, ): array { foreach ($created as $route) { if ($route instanceof Route) { $attributes[] new OA\Operation( path: $route-path, method: $route-method, operationId: $reflector-getName(), ); } } return $attributes; } }“把外来属性翻译成 spec 属性”只是 Translator 的一种用法另一种是补写一个没人写的原生属性——swagger-php 自己就是通过DefaultAttributeTranslator与OptionalPropertyAttributeTranslator这么用的。注册位置withAttributeFactory()内嵌withTranslators()Translator 注册在属性工厂上所以withTranslators()必须嵌套在withAttributeFactory()之内$builder-withAttributeFactory(fn (AttributeFactory $factory) $factory-withTranslators( fn (TypedList $translators) $translators-add(new RouteTranslator()) ));注意withAttributeFactory()是立即执行的它针对 builder 已持有的工厂实例调用回调重复调用配置的是同一个实例见 Builder::withAttributeFactory()。子类化 spec 属性用反射推导构造函数参数spec 属性不是final的可以被子类化。子类通过反射自己推导构造函数参数把重复样板从被注解的代码里移走。指南 dto_attribute.php 演示了一个继承自OA\Schema的Dto#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] final class Dto extends OA\Schema { public function __construct(string $of) { $class new \ReflectionClass($of); parent::__construct( schema: $class-getShortName(), required: array_map( static fn (\ReflectionProperty $property): string $property-getName(), $class-getProperties(\ReflectionProperty::IS_PUBLIC), ), ); } }于是#[Dto(of: Pet::class)]一次就完成了两件事把 schema 命名为类短名Pet并把所有 public 属性标记为required两处都不用手写。对管道的其余部分而言这个子类就是一个OA\Schema——合并、嵌套containment和编译都一视同仁。Contributing to the Specification在解析前把属性“放进”文档Builder::withSpecification()在Resolver 运行之前把装配好的Specification交给一个 callable见 Builder 文档。向它add()的属性会像扫描所得一样走完后续全部管道。指南 contribution.php 演示了从路由表构建文档的场景——没有任何属性可扫描纯命令式注册function buildFromRoutes(array $routes): Result { return (new Builder()) -setMode(Mode::SPEC) -addSource(new \ReflectionClass(Pet::class)) -withSpecification(function (Specification $specification) use ($routes): void { $specification-add(new OA\Info(title: Registered routes, version: 1.0.0)); foreach ($routes as $path $model) { $specification-add(new OA\Operation\Get(path: $path, responses: [ new OA\Response(response: 200, description: OK, content: [ new OA\MediaType\Json(schema: new OA\Schema(ref: $model)), ]), ])); } }) -build(); }产物contribution-3.1.0.yamlopenapi: 3.1.0 info: title: Registered routes version: 1.0.0 paths: /pets: get: operationId: a258179ad504a1eacaafc2ef43cbd7ba responses: 200: description: OK content: application/json: schema: $ref: #/components/schemas/Pet components: schemas: Pet: type: object properties: name: type: string age: type: integer required: - name - age关键语义有三条管道读取的是 reflector不是源文件——属性从哪里来无关紧要。贡献的属性如果携带 reflector就会被当作装配器自己的属性处理给了ReflectionMethod的 Operation 会从方法取 summary、description 和 operationId给了ReflectionParameter的 Parameter 会取名称、类型和是否必填。一个知道[UserController::class, index]对应某条路由的框架可以把这一切都交接过去。没有任何 reflector 的东西实体注册表、序列化器配置只能以“裸”属性形式进来——withSpecification()是它进入文档的唯一途径。Augmenter 也能add()但它运行在解析之后所以它添加的属性里的$ref不会被解析。经验法则用 Augmenter 丰富已有的内容用withSpecification()放置新内容。用setMeta()标记贡献供下游识别一旦添加贡献的属性就和扫描属性没有区别。如果后续步骤需要区分它们——比如 Merger 决定优先级、某个 Augmenter 想跳过它们——就在添加时用setMeta()在你自己拥有的键名下做标记之后用getMeta()读回。meta按写入者键名隔离swagger-php 核心不读写它见 AbstractAttribute::setMeta() 与 AttributeInterface。Resolvers补全被引用但从未收集的类从 reflector 播种意味着文档可以点名一个从未作为源出现的类添加一个控制器它某个$ref指向一个 DTO而没有任何人收集过这个 DTO。每个这样的名字都会进入 resolver 步骤Resolver.php 的实现会对未解析 FQCN 循环收敛上限MAX_ITERATIONS 50超限会抛出“resolver loop did not converge”异常。默认 ResolverResolver\ReflectionResolver\Reflection 覆盖普通场景类含接口、枚举存在且携带 spec 属性 → 用正在使用的装配器collect()收集它引用即解析类完全不存在class_exists/interface_exists/enum_exists均失败→ 返回false交给链上下一个 resolver类收集后没有产出任何组件→ 同样返回false未解析的引用被报告而非静默丢弃。“FQCN 视为已解析”的定义是收集后buildComponentIndex()-find($fqcn)能找到一个AttributeInterface实例见 源码第 30-33 行。这带来一个实用收益把相关类全部列为 builder 源变成可选的——只要被引用链上的类都携带 spec 属性只添加一个控制器就够。注册自定义 ResolverBuilder::withResolver()把 resolver 列表交给 callable。resolver 按顺序尝试第一个成功者获胜所以在默认 resolver 之后添加的 resolver接手的是默认 resolver 未能解析的类。insert()则把新 resolver 放到默认项之前。一个常见的自定义场景从类的 public 属性和 PHP 类型推导 schema让不带任何 spec 属性的 DTO 也能出现在文档里。契约极简ResolverInterfaceinterface ResolverInterface { public function resolve(string $fqcn, Assembler $assembler): bool; }resolver 拿到的是构建本 specification 的那个Assembler用其collect()收集 reflector 会把结果直接并入当前 specification返回true表示已处理停止该 FQCN 的链。Augmenters装配后对整棵 Specification 的增强Augmenter 在装配之后、对整个Specification运行负责补齐属性里没写的内容。它不强制实现任何接口——任何接收Specification并返回它的 callable 都可以PipeInterface只是用来声明阶段不实现它的管道会落入默认组augment。阶段resolve → reduce → augmentBuilder 用 Augmenter\Group 枚举给管道分三组阶段用途Resolve从 PHP 反射和跨桶关系推导数据Reduce过滤或移除条目Augment追加派生元数据同一阶段内的管道按注册顺序运行见 Pipeline::ordered() 的分桶实现。内置管道的完整清单与每个阶段的用途见 Augmenters 参考Builder::getDefaultAugmenters() 里可以看到内置管道是从Inheritance到EnumDescriptions的完整列表且最后还追加了第二个Augmenter\Merge把迟注册增强器新增的内容也做一次归约。一个最小自定义 Augmenter指南 tag_augmenter.php 演示了按控制器给操作打 tagfinal class TagFromController implements PipeInterface { public function __invoke(mixed $specification): mixed { foreach ($specification-operations as $operation) { $reflector $operation-getReflector(); if ($reflector instanceof \ReflectionMethod) { $controller $reflector-getDeclaringClass()-getShortName(); $operation-tags ?? [preg_replace(/Controller$/, , $controller) . s]; } } return $specification; } public function group(): string|\BackedEnum { return Group::Augment; } }注册方式withAugmenters(fn (Pipeline $augmenters) $augmenters-add(new TagFromController()))。withAugmenters()同样针对 builder 已持有的同一管道实例执行。Specification 提供的三个视图Specification源码是扁平容器每个根属性类型一个桶openapi、info、servers、tags、pathItems、operations、schemas、responses、parameters、requestBodies、headers、securitySchemes、links、examples、mediaTypes、attachables。Augmenter 需要的三个视图都从这里拿getWalker()遍历整棵装配树buildComponentIndex()解析某个$ref对应的组件buildPathItemHierarchy()找出管辖某个类的PathItem层级。路径级元数据必须通过层级读取一个类如果没有自己的PathItem它就受祖先类的PathItem管辖——直接看类本身的属性会漏掉。Mergers决定同一键下的两个属性谁活下来问题背景两个属性可能争抢同一个键同一路径同一方法的两个 Operation、两个都叫Pet的 schema——扫描找到一个、withSpecification()钩子贡献另一个、继承克隆造出第三个。在 Merger 介入之前Augmenter\Merge会先把每个根桶归约到“每键一项”Augmenter/Merge.php否则编译器只能“碰巧”保留最后写入的那一个。Merger 契约MergerInterface由三个方法组成supports(string $class): bool——按注册顺序被询问先声明类型的先处理identity(AttributeInterface $attribute): ?string——什么让两个属性“是同一个”返回null表示该属性永不合并、直接放行merge(AttributeInterface $earlier, AttributeInterface $later): AttributeInterface——幸存者是谁。$earlier与$later按生产者顺序排列这是管道唯一保证的顺序所以return $later就是 last-wins。超出该顺序的优先级是核心不持有的策略由注册的 merger 自行决定。默认 MergerMerge\LastWins万能兜底LastWins 是兜底 mergersupports()恒为true每键存活一个、保留后者并通过日志警告两个来源位置。它做的是归约而非折叠两个进、一个出编译器永远看不到冲突它自己的偶然规则也就不再参与决策。把两个属性的字段合并起来是类型专属 merger 的职责要注册在它前面。身份判定由共享的 IdentityTrait 定义按集合归属属性类型身份键Operation路径 方法或 webhook 方法components 桶里的组件组件键component:…路径绑定的 PathItem路径path:…Tag名称tag:…Servers、安全要求、外部文档无身份按位置排列重复合法直接放行Builder 的默认 merger 列表只有[LastWins]getDefaultMergers()测试 OperationsTest::testIsNotADefaultMerger() 验证了Merge\Operations不在默认列表里。自定义 Merger识别自己的属性并决定优先级指南给出了一个完整示例扫描已描述的操作对抗一个由钩子贡献并标记为“自己的”操作时保留前者use OpenApi\Contracts\AttributeInterface; use OpenApi\Contracts\MergerInterface; use OpenApi\Spec as OA; final class MyOperationMerger implements MergerInterface { public function supports(string $class): bool { return is_a($class, OA\Operation::class, true); } public function identity(AttributeInterface $attribute): ?string { return $attribute-path ! null $attribute-method ! null ? $attribute-method . . $attribute-path : null; } public function merge(AttributeInterface $earlier, AttributeInterface $later): AttributeInterface { return $later-getMeta(self::class, false) ? $earlier : $later; } }注册把它插到兜底 merger之前因为链上第一个声明处理某类型的 merger 负责该类型而LastWins声称处理一切、必须垫底use OpenApi\Merge; use OpenApi\Utils\TypedList; $builder-withMergers(fn (TypedList $mergers) $mergers-insert( new MyOperationMerger(), Merge\LastWins::class, ));“身份与折叠同居一对象”是有意设计消费者对自有类型换一种键控方式时只需改动一处。需要识别自己产物的包在创建属性时用setMeta(MyOperationMerger::class, true)打标在merge()里读回即可核心既不写也不读这些 meta。可选 MergerMerge\Operations字段级折叠Merge\Operations源码是可选的不是 Builder 默认 merger。当同一个路径和方法的操作被多处描述例如withSpecification()贡献的路由表 属性补充的 responses时把它注册到LastWins之前就能逐字段折叠两个操作而非整体二选一$builder-withMergers(fn ($mergers) $mergers-insert(new Merge\Operations(), Merge\LastWins::class));身份沿用兜底 merger 的路径方法或 webhook方法——注册折叠改变的是两个操作如何合并而不是哪两个是同一个。折叠规则与参考文档一致测试 OperationsTest.php 逐条验证只有一方设置的字段总是被采用双方都设置的字段由构造时的Mode决定默认Mode::Last折进后者、折进前者或只取不重叠的部分Mode也可以是接收两者并返回一个的 callable让选择跟随生产者用setMeta()留下的标记而非到达顺序字段冲突会被报告记录被保留的值双方设置相同的不会parameters按“名称 位置in”键控responses按状态码键控在First/Last下双方都描述的参数或响应会向内折叠一层参数连同其schema因此路由的pattern能保留下来对抗属性的type在NonOverlapping下则整体取前者的requestBody、externalDocs、security、servers、callbacks各是一个整体决策从模式保留的那一方整取显式security: []会被设置并构成“退出”tags与attachables取并集x扩展字段在相同模式下逐键合并幸存者是一个新 Operation前者的副本再折入后者两边的贡献都不被改动它保留前者的 reflector、源码位置和meta前者没有 reflector 时回退到后者的。配置项参考文档明确给出设置类型默认值说明modeOpenApi\Merge\Mode\|callableMode::Last同一身份的属性重叠时的处理方式一个Mode或一个接收 earlier/later 并返回一个的 callableMerge\Mode枚举的三个取值Mode.phpFirst冲突字段保留前者、Last保留后者默认、NonOverlapping只取独有部分双方都设的字段保留前者且不折叠。用 callable 按对选模式是“生产者打标决定优先级”的免写 merger 方案use OpenApi\Contracts\AttributeInterface; use OpenApi\Merge; new Merge\Operations( fn (AttributeInterface $earlier, AttributeInterface $later): Merge\Mode $later-getMeta(MyProducer::class) ? Merge\Mode::First : Merge\Mode::Last, );一个细节只含ref的参数能与同名同位置的 inline 参数折叠因为Parameters增强器会先给组件参数补上名称和位置折叠后存活的是那个引用。合并通过Augmenter\Merge执行合并这一遍由 Augmenter\Merge 执行它被注册两次第一次作为reduce 阶段的首个管道此时所有身份都已存在——PathItems在 resolve 阶段解析了路径Names推断出组件键第二次作为最后一个管道把迟增强器新增的内容也归约一遍。Augmenter\Merge::reduce()只处理根集合——键冲突来自不同来源扫描、钩子、resolver、继承克隆的地方同一属性内部的两个重复键如一个操作里两个200响应是同一作者同一处写的两条记录留给编译器做 last-write-wins 并报告。Compilers替换输出编译器Builder::setCompiler()替换把Specification变成文档的编译器。否则编译器会从目标版本自动解析——resolveCompiler() 依次询问OpenApi30Compiler、OpenApi31Compiler、OpenApi32Compiler都不支持就抛出No compiler available for version ...。因此这个钩子只服务于“出厂编译器产不出的输出”。Classic 逃生舱withGenerator()Builder::withGenerator()配置经典Generator在 classic 模式下它是整条管道在 hybrid 模式下它是扫描那一遍。callable 收到一个默认Generator可以就地配置或返回另一个。Spec 模式没有Generator该钩子永远不会被调用。hybrid 模式下Generator到达时带着空的处理器管道——在这里添加的 processor 是唯一会运行的且它看到的是“刚扫描完”的注解属性还没挂到 schema 上继承也还没产生任何allOf。桥接HybridBridge和 augmenters 之后才做这些工作见 doHybridAssemble()。所以只读取或改动作者所写内容的 processor 在 hybrid 下可用依赖上述结构的 processor 应该写成 augmenter。组合示例一个完整的定制构建指南 wiring.php 把上面的RouteTranslator与TagFromController装配到一个以 reflector 而非目录为源的 builder 上function buildSpec(): Result { return (new Builder()) -setMode(Mode::SPEC) -addSource(new \ReflectionClass(PetController::class)) -withAttributeFactory(fn (AttributeFactory $factory) $factory-withTranslators( fn (TypedList $translators) $translators-add(new RouteTranslator()) )) -withAugmenters(fn (Pipeline $augmenters) $augmenters-add(new TagFromController())) -build(); }给定携带#[Route(path: /pets)]与#[OA\Response]的控制器pet_controller.php产物wiring-3.1.0.yamlopenapi: 3.1.0 info: title: Pets version: 1.0 paths: /pets: get: tags: - Pets operationId: list responses: 200: description: OKoperationId: list来自 Translator 用$reflector-getName()生成tags: [Pets]来自TagFromController增强器对PetController类名的处理path: /pets来自框架的Route属性——三个扩展点各司其职。边界哪些地方刻意不是扩展点指南最后一节划定了边界每一条都有理由和替代方案属性类型不会为下游便利而放宽。强类型正是 DTO 的价值所在。只对某个集成有意义的元数据若在源码中与被描述属性相邻声明就放Attachable若由代码沿途附加就放setMeta()——两者都不会进入生成的文档也不该放进放宽后的$ref: string|object。没有框架专属代码也没有此计划。Translators、贡献、augmenters 和 attachables 就是契约框架需要的一切都可以基于它们在本仓库之外构建。没有事件、没有监听器。管道是确定性的、自上而下顺序读取这保证一个错误文档能追溯到产出它的步骤。事件顺序读代码看不出来也更难测试。装配器内部保持内部。collect()是契约它如何解析嵌套可以自由变化也确实变化过。延伸阅读Builder 参考每个钩子的独立说明与签名Augmenters 参考内置管道与其阶段架构文档各阶段如何拼装Spec 管道内部slot map、解析过程以及新属性必须遵循的规则各默认实现的直接源码DefaultAttributeTranslator与OptionalPropertyAttributeTranslator在 src/Assembler/、Resolver\Reflection在 src/Resolver/Reflection.php、LastWins与Operations在 src/Merge/、Augmenter\Merge在 src/Augmenter/Merge.php合并行为有 tests/Merge/OperationsTest.php 与 tests/Augmenter/MergeTest.php 逐条验证。赞分享后端API设计文档代码生成【免费下载链接】swagger-phpA php swagger annotation and parsing library项目地址https://gitcode.com/gh_mirrors/sw/swagger-php点击查看免费下载相关推荐swagger-php Spec Attributes 完全指南OpenApi\Spec 类型化属性与 spec 流水线实战swagger php Spec Attributes 完全指南OpenApi\Spec 类型化属性与 spec 流水线实战 导读 本文聚焦 swagger后端API设计文档代码生成Tiny RDM连接Redis Sentinel哨兵模式与Cluster集群模式高可用部署实战指南Tiny RDM连接Redis Sentinel哨兵模式与Cluster集群模式高可用部署实战指南 Tiny RDMTiny Redis Desktop M后端API设计文档代码生成DeepSpeed 流水线并行深度指南PipelineModule、PipelineEngine 与可扩展调度器全解析DeepSpeed 流水线并行深度指南PipelineModule、PipelineEngine 与可扩展调度器全解析 流水线并行Pipeline Para推理引擎大模型上一篇揭秘MiMo-7B-RL的MTP技术让推理速度提升2倍的核心引擎下一篇NodeGui 中 ScrollPhase 枚举详解通过 QWheelEvent 识别滚轮事件的滚动阶段创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑