Community编程与开发github.com

Redocly/php-generator

已导出的 Redocly `php` 客户端生成器的设计文档。在修改 `generators/php/` 之前,请先阅读并更新此文档。

php-generator 是什么?

该技能详细描述了 Redocly `php` 客户端生成器的设计规范和实现约束。它定义了生成器输出的 PHP 代码结构,包括使用构造函数属性提升的 final 模型类、带有类型化方法的 Client 类,以及无需 Composer 依赖的嵌入式运行时(PHP ≥ 8.1,cURL)。文档涵盖了一系列必须遵守的设计决策,例如模型类的不可变性、Client 类的非 final 性以支持测试模拟、参数命名空间的唯一性处理、通过类常量存储操作映射以避免 OPcache 预加载问题(文件级常量被丢弃)、枚举与区分联合的实现方式、错误处理模式(异常)、日期时间类型映射、流式请求体的直接传递(不读入内存)、分页与 SSE 支持、运行时嵌入机制以及可选的遥测功能。它还提供了从基于服务的 SDK 迁移的指南,并描述了修改迭代流程:先编辑此技能,再匹配生成器代码,最后通过 `redocly generate-client` 检查 diff。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Redocly/redocly-cli/tree/HEAD/packages/client-generator/eject-assets/skills/php-generator

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

php 生成器 — 其技能

此文件是您已导出的 php 生成器(generators/php/)的设计文档: 要修改生成器,请先编辑此技能,然后使代码与之匹配 — 对 generators/php/ 的更改如果没有此文件中的对应说明,则视为不完整。

其输出内容

一个自包含的 <stem>.php 文件:使用构造函数属性提升的模型类,一个包含每个操作的类型化方法的 Client 类,以及嵌入式运行时。PHP ≥ 8.1,通过 cURL 扩展进行 HTTP 通信 — 零 Composer 依赖。命名空间源自 API 标题(identifierFor(title, pascal) — 例如 CafeOrders)。

必须保持的设计决策

  • 模型是 final class,使用构造函数属性提升,必需参数在前,可选参数可为 null (= null)。每个类在编译时生成 hydration 方法:fromArray(array $data): self 和 toArray(): array(内联使用网络名称;序列化时跳过 null) — 无反射。omit 模式通过其基类进行 hydration/序列化。类型为区分联合(DISCRIMINATED union)的属性或响应通过联合的 unmarshalX 调度器进行 hydration,因此消费者可以使用 instanceof 进行类型收窄;其类型包含 array,因为具有未知鉴别器的值将保留为原始数组。任何其他联合将保留为解码后的 JSON,因此其类型与 JSON 一致:类成员为 array,枚举成员为其底层标量,日期成员为 string。
  • 文件通过 PHPStan 级别 5。 它最终位于用户自己的项目中,因此其分析器会读取它。操作表是一个 PHPStan 完全了解的常量,因此方法仅在表中有对应条目时读取 $op['security'],否则传递 []。
  • writeOnly 属性是可选的,即使在规范中标记为必需:请求和响应共享同一个类,而响应永远不会携带该属性。它为 nullable (= null),因此 fromArray 可以在没有该属性的情况下 hydration 响应,而 toArray 在请求中仍会发送它。
  • Client 类不是 final — PHP 测试套件会模拟具体类 (createMock(Client::class)),而 final 会迫使每个消费者创建一个包装器接口。模型类保持 final。
  • 每个参数都是独立的参数,因此它们的名称与方法自身声明的参数($body、$headers、$idempotencyKey)共享一个命名空间。 使用 uniqueIdentifiers(..., { taken: … }) 构建它们:OpenAPI 允许一个操作在两个位置使用相同的名称(路径中的 id 和查询中的 id),而 PHP 会直接拒绝重复的参数定义。网络名称保持不变,因此请求不变。
  • 操作映射是一个类常量 Client::OPERATIONS,通过 self::OPERATIONS[...] 读取,而不是文件级别的 const。OPcache 预加载会将类与函数保留在共享内存中,但会丢弃文件级常量,并且一旦 Client 类存在,自动加载器就不会再次包含该文件 — 因此文件级映射在第一次请求后的每次请求中都将是未定义的。类常量随类一起存储。
  • 命名: 类使用 PascalCase,属性/方法使用 camelCase,通过 identifierFor(..., RESERVED_WORDS.php) 实现;保留字后添加下划线。
  • 枚举是原生支持的枚举(string/int);其他标量保留为别名。区分联合是基于 match 的 unmarshalX(array $data) 调度器。每个分支像处理该类型的属性一样 hydration 其映射目标,因此没有自己类的目标(纯 oneOf)保持为原始数组;allOf 被展平。
  • 联合类型在 PHP 8.1 可以表达的地方保留其类型。 标量、枚举、类或数组的联合变为原生联合类型(int|string、string|array),而不是塌缩为 mixed — 富列表过滤是常见情况,丢失其类型就失去了类型化 SDK 的意义。仅当某个成员没有自己的 PHP 类型(内联对象、交集、unknown)时才回退到 mixed,因为 mixed 不能出现在联合内部。可空性在联合中表示为 |null(PHP 禁止混用 ? 和 |),对于单一类型表示为 ?T。
  • 错误: 异常是错误模式(ApiError/TimeoutError 继承 \RuntimeException);errorMode 不会改变输出(生成器声明 errorModes: ['throw'],因此 result 会快速失败)。
  • 日期: dateType: Date 将 format: date/date-time 类型化为 \DateTimeImmutable;hydration 使用 new \DateTimeImmutable(...),序列化使用 \DateTimeInterface::ATOM(date-time)或 'Y-m-d'(date),包括查询参数。
  • 方法参数: 必需路径参数为位置参数,JSON 请求体紧随其后,可选查询参数为可空的命名参数(NAMED arguments),然后是 ?array $headers,以及修改方法上的 ?string $idempotencyKey。
  • 非 JSON 成功响应体(PDF、图片、八位字节流)返回原始体作为 string — 二进制下载绝不能降级为 void。
  • PHPDoc 承载签名无法表达的信息。 PHP 的 array 和 \Generator 会擦除其元素类型,因此 docblock 需要声明:集合返回使用 @return Customer[],<op>Pages()/<op>Items() 使用 @return \Generator<int, Customer>。静态分析和阅读器依赖这些注释;没有注解的 hydration 返回值看起来是未类型化的。
  • 响应头: 声明了成功响应头的操作会获得一个 <op>WithHeaders() 变体,返回一个 Envelope(data、headers — 强制转换为 int/bool/string,键名使用 camelCase,缺失或无法解析的值被省略 — 以及 status)。没有声明头的操作不会获得变体,基础方法保持仅返回体(PHP 无法根据标志改变返回类型)。
  • 服务器: 当描述声明服务器时,会生成一个 Servers 类,每个服务器对应一个静态方法;服务器变量成为命名的字符串参数,默认使用规范的默认值(Servers::production(organizationId: 'org_x')),因此模板化的基础 URL 无需手动字符串构建。客户端的固定默认值保持为 servers[0],并替换变量默认值。
  • 流式请求体直接传递。 对于非类型化的 multipart/form-data 操作(来自工具包的 isUntypedMultipartBody:既不是对象也不是 $ref 的模式)或二进制操作(isBinaryContentType),请求体类型为 mixed,由运行时处理(multipartBody / binaryBody):流 resource 通过 cURL 分块上传(CURLOPT_UPLOAD + CURLOPT_INFILE,方法由 CURLOPT_CUSTOMREQUEST 保持),从不读入字符串,无论重试策略如何,仅尝试一次,因为已消费的流无法重播。CURLOPT_INFILESIZE 仅在流可寻址且 fstat 返回数组(从当前位置开始的剩余字节数)时设置;php://input、管道和不支持 stream_stat 的包装器使用分块传输。任一操作的 string 类型请求体直接发送,不进行 json_encode;二进制操作的其他值仍会 json_encode;非类型化 multipart 操作的 array 类型请求体保持 toMultipart 路径。调用者提供的 Content-Type 优先级高于描述或 multipart 边界的,在 send 中不区分大小写匹配,因此该标头永远不会发送两次;此规则适用于所有操作。整个请求的 cURL 超时仍然适用(timeout: null 禁用)。类型化的 multipart 请求体不受影响。
  • 对等特性: 认证、带有 Retry-After 和抖动退避的重试、每次尝试的 cURL 超时、中间件可调用对象、分页(<op>Pages() / <op>Items() 作为 \Generator)、SSE(通过 curl_multi 泵的 iterSse)、multipart。
  • 运行时在此文件夹的 runtime/runtime.php 中手动编写(通过 php -l 检查),并在准备时嵌入。curl_close 从不调用(自 PHP 8.5 起已弃用,自 8.0 起为无操作)。在 --runtime module 模式下,它作为一个 runtime.php 文件编写,客户端通过 require_once 引入,其命名空间被重写为客户端的命名空间,因此一个命名空间跨越两个文件。
  • onSettled 每个发送的调用仅触发一次。 send() 在重试循环之后,在抛出或返回之前,依次调用每个 Config->onSettled 可调用对象,然后调用每个 Config::$registeredOnSettled 可调用对象,传递 operationId、status(无响应时为 0)、timedOut、attempts 和 durationMs。每个钩子都会执行。对于成功调用,抛出第一个钩子错误;对于失败调用,原始错误优先,钩子错误写入 error_log。SSE 流在 send() 外部打开,因此不触发任何钩子。
  • 遥测在生成时可选择启用。 使用 client.telemetry: on 时,生成器还会在客户端文件旁边写入 <client>.telemetry.php(来自此文件夹中的 telemetry/telemetry.php,在准备时嵌入),两种运行时模式均可,并使用客户端的命名空间并嵌入生成器版本。客户端文件不变。应用程序需要引入它,并在 Config->onSettled 中传递 new UsageTelemetry(...)。SDK 所有者改为调用 register(),该方法将实例添加到 Config::$registeredOnSettled,因此每个客户端都会统计其调用次数;UsageTelemetry::flushRegistered() 刷新每个已注册的实例。PHP 没有计时器:发现窗口已过期的调用会先发送,而关闭函数会发送打开的窗口。常量存在于类上,因为 OPcache 预加载会丢弃文件级常量。类名对每个 SDK 都是保留的,因为该文件与模型共享命名空间。
  • 仅使用中性工具包编写 — 否则 dogfooding 保护会失败。

从基于服务的 SDK 迁移

  • 按资源划分的服务($client->customers()->get($id))映射为以 operationId 命名的扁平方法($client->getCustomer($id));可选的查询参数保持其命名参数风格(filter:、sort:、limit:)。

  • 公开分页响应头(getTotalItems()、getLimit())的集合包装器映射到 <op>WithHeaders() 信封(->headers['paginationTotal']);简单的迭代映射到 <op>Items() / <op>Pages() 生成器。

  • 公开字段错误的专用验证异常类映射到 catch (ApiError $e) + $e->status === 422 + 解码后的 $e->body。

  • 会话/bearer token 流程映射到 auth: ['bearer' => $tokenProvider],并使用可调用对象 — 每个请求解析一次,因此刷新无需重建客户端。

  • 它自身生成文档。 使用 client.docs(或 --docs)时,docs 钩子写入 <stem>.php.md:安全方案,然后是每个操作的一个章节,包含其参数、请求体、响应类型和行为说明。调用代码片段来自此生成器自身的 sample 钩子,因此页面只能显示旁边 SDK 的语法,布局来自创作工具包中的 renderReferencePage — 可通过 @redocly/client-generator 从导出的副本访问。页面上的分页由 paginationRuleFor 决定,与该生成器解析分页时使用的辅助函数相同。

修改循环

  1. 编辑此技能:描述新的行为或决策。
  2. 使 generators/php/ 与之匹配。
  3. 运行 redocly generate-client 并检查生成输出的 git diff — 生成的文件从不手动编辑。

更新的内置版本可通过 redocly eject-generator php --update 合并。

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

相关技能