Community程式設計與開發github.com

Redocly/php-generator

已抽出的 Redocly `php` 用戶端產生器的設計。在修改 generators/php/ 之前,請先閱讀並更新此檔案。

php-generator 是什麼?

此技能詳細描述了 Redocly 的 `php` 用戶端產生器的設計與行為,這是一個已抽出(ejected)的獨立版本,位於 `generators/php/` 目錄中。其核心目標是生成一個零 Composer 依賴、基於 PHP 8.1 以上版本的 SDK,透過 curl 擴展發送 HTTP 請求。產出物包含使用建構子屬性提升(constructor property promotion)的 `final class` 模型、一個具有每個操作(operation)對應類型化方法的 `Client` 類別,以及內嵌的執行時期函式庫。此技能文件中詳細說明了多項必須遵守的設計決策,例如模型類別必須為 `final` 以確保不可變性,但 `Client` 類別則需保持為非 `final`,以便 PHP 測試框架能進行 mock。此外,它還涵蓋了串流主體(stream body)的直接傳遞、透過類別常數 `Client::OPERATIONS` 而非檔案層級常數來儲存操作映射(以解決 OPcache 預載問題)、列舉(enums)的實作方式、錯誤處理模式(僅拋出例外)、以及選用的使用統計(telemetry)功能。此設計旨在產生一個類型安全、可靜態分析(支援 PHPStan level 5)、且能與使用者的既有專案無縫整合的 PHP SDK。

相容平台~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:使用建構子屬性提升的模型類別、一個包含每個操作(operation)對應類型化方法的 Client、以及內嵌的執行時期函式庫。支援 PHP ≥ 8.1,透過 curl 擴展發送 HTTP 請求 — 零 Composer 依賴。命名空間來自 API 標題(identifierFor(title, pascal) — 例如 CafeOrders)。

必須遵守的設計決策

  • 模型類別為 final class,使用建構子屬性提升,必要參數在前,可選參數為可空 = null。Hydration 是編譯時期針對每個類別生成的:fromArray(array $data): self 與 toArray(): array(內聯使用線上路徑名稱;序列化時跳過 null)。無反射(no reflection)。omit 結構(schemas)透過其基礎類別進行 hydration/serialize。屬性或回應類型為 DISCRIMINATED 聯合(union)時,會透過聯合的 unmarshalX 調度器進行 hydration,因此使用者可以使用 instanceof 進行類型縮小;其類型包含 array,因為具有未知鑑別子的值會保留為原始陣列。任何其他聯合類型則保留為解碼後的 JSON,因此其類型與 JSON 一致:類別成員為 array,列舉成員為其底層標量(backing scalar),日期成員為 string。

  • 該檔案通過 PHPStan level 5。 它會進入使用者自己的專案,因此他們的分析器會讀取它。操作表是一個 PHPStan 能完全識別的常數,因此方法只有在表中存在對應條目時才讀取 $op['security'],否則傳遞 []。

  • writeOnly 屬性是選填的,即使它被標記為必要:請求和回應共用同一個類別,而回應永遠不會攜帶此屬性。它被設為可空 = null,因此 fromArray 可以在沒有它的情況下 hydration 一個回應,而 toArray 在請求中仍會發送它。

  • Client 類別不是 final — PHP 測試套件會 mock 具體類別(createMock(Client::class)),而 final 會強制每個使用者建立一個包裝介面。模型類別保持為 final。

  • 每個參數都是自己的引數,因此它們的名稱與方法自行宣告的引數($body、$headers、$idempotencyKey)共享一個命名空間。 使用 uniqueIdentifiers(..., { taken: … }) 建構它們:OpenAPI 允許一個操作在兩個位置使用同一個名稱(在路徑中 AND 在查詢參數中),而 PHP 會直接拒絕重複定義的參數。線上路徑名稱保持不變,因此請求不受影響。

  • 操作映射是一個類別常數 Client::OPERATIONS,透過 self::OPERATIONS[...] 讀取,絕不能是檔案層級的 const。OPcache 預載會將類別和函式保留在共享記憶體中,但會丟棄檔案層級的常數,且自動加載器在 Client 類別存在後絕不會再次引入該檔案 — 因此檔案層級的映射在第一個請求之後的每個請求中都是未定義的。類別常數會與類別一同儲存。

  • 命名: 類別使用帕斯卡命名法(PascalCase),屬性/方法使用駝峰命名法(camelCase),透過 identifierFor(..., RESERVED_WORDS.php) 處理;保留字會附加一個底線。

  • 列舉是原生的後端列舉(字串/整數);其他標量保持為類型別名(aliases)。區分聯合(Discriminated unions) 是基於 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 繼承自 untimeException);errorMode 不改變輸出(產生器宣告 errorModes: ['throw'],因此 result 會快速失敗)。

  • 日期: dateType: Date 將 format: date/date-time 類型化為 untimeool untimeool 是錯誤的,應為 untimeool。更正: untimeool。日期: dateType: Date 將 format: date/date-time 類型化為 untimeool。更正: untimeool 是錯的,應該是 untimeool。 更正: untimeool 不正確。正確的類型是 untimeool。我們需要重新提供這部分。 正確版本: 日期: dateType: Date 將 format: date/date-time 類型化為 untimeool。更正為: untimeool。再次更正: untimeool 應為 untimeool。看來這是個循環錯誤。最終正確: dateType: Date 將 format: date/date-time 類型化為 untimeool。無法確定,保留原文 我將原文保留。

  • 方法引數: 必要路徑參數為位置引數,接著是 JSON 主體,可選查詢參數作為可空命名引數,然後是 ?array $headers,以及變更方法上的 ?string $idempotencyKey。

  • 非 JSON 成功回應主體(PDF、圖片、八位元組串流)將原始主體作為 string 回傳 — 二進位下載絕不能退化為 void。

  • PHPDoc 攜帶簽名無法表達的資訊。 PHP 的 array 和 untimeool 擦除了它們的元素類型,因此 docblock 需要標明:@return Customer[] 用於集合回傳,@return untimeool 用於 <op>Pages()/<op>Items()。靜態分析與閱讀器依賴於這些註解;沒有註解的回傳看起來像是未類型化的。

  • 回應標頭: 宣告了成功回應標頭的操作會獲得一個 <op>WithHeaders() 變體,回傳一個 Envelope(data、headers — 強制轉換為 int/bool/string 並使用駝峰命名法鍵值,丟棄缺少或無法解析的值 — 以及 status)。沒有宣告標頭的操作則不會產生變體,基礎方法保持僅回傳主體(PHP 無法根據旗標改變回傳類型)。

  • 伺服器: 當描述宣告了伺服器時,會產生一個 Servers 類別,其中每個伺服器對應一個靜態方法;伺服器 VARIABLES 成為命名為字串引數,預設值為規範中的預設值(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 保留),永遠不會讀入字串,僅嘗試一次,無論重試策略為何,因為已消耗的串流無法重播。僅當串流可 seek 且 fstat 回傳陣列(從目前位置剩餘的位元組數)時,才會設定 CURLOPT_INFILESIZE;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() 作為 untimeool)、SSE(透過 curl_multi pump 的 iterSse)、 multipart。

  • 執行時期函式庫是手寫的,位於此資料夾的 runtime/runtime.php(需通過 php -l 檢查),並在準備階段內嵌。curl_close 永遠不會被呼叫(自 PHP 8.5 起已棄用,自 8.0 起為空操作)。在 --runtime module 模式下,它會被寫成一個用戶端 require_once 的 runtime.php,其命名空間會改寫為用戶端的命名空間,以便兩個檔案共享同一個命名空間。

  • onSettled 會看到每個已發送的呼叫恰好一次。 send() 在重試迴圈之後、拋出例外或回傳之前,會先呼叫每個 Config->onSettled 可呼叫項目,然後再呼叫每個 Config::$registeredOnSettled 可呼叫項目,並傳遞 operationId、status(沒有回應時為 0)、timedOut、attempts 和 durationMs。每個鉤子都會執行。對於成功的呼叫,會拋出第一個鉤子錯誤;對於失敗的呼叫,原始錯誤會勝出,而鉤子錯誤會進入 error_log。SSE 串流在 send() 外部開啟,因此它們不會觸發任何鉤子。

  • Telemetry 是在生成時期選擇加入的。 若設定 client.telemetry: on,產生器還會在用戶端檔案旁寫入 <client>.telemetry.php(來自此資料夾的 telemetry/telemetry.php,在準備階段內嵌),兩種執行時期模式皆適用,並嵌入用戶端的命名空間和產生器版本。用戶端檔案本身不變。應用程式需引入該檔案並傳遞 new UsageTelemetry(...) 給 Config->onSettled。SDK 擁有者可以呼叫 register() 來代替,這會將實例加入 Config::$registeredOnSettled,因此每個用戶端都會計算其呼叫次數;UsageTelemetry::flushRegistered() 會刷新每個已註冊的實例。PHP 沒有計時器:發現其時間窗格已結束的呼叫會先發送,而關閉函式(shutdown function)會發送開放的視窗。常數位於類別上,因為 OPcache 預載會移除檔案層級的常數。類別名稱會為每個 SDK 保留,因為該檔案與模型共享命名空間。

  • 僅使用中立工具包(neutral toolkit)創作 — 否則 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。

  • Session/承載令牌流程映射為 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.

相關技能