Community寫作與編輯github.com

gplanchat/durable-skill

Claude Code 技能:使用 PHP 編寫 Durable 工作流程、活動與 Nexus 操作

durable-skill 是什麼?

此技能提供完整的指引,協助開發者使用 PHP 建立可靠的 Durable 工作流程(Workflows)、活動(Activities)與 Nexus 操作。其核心是「確定性(determinism)」原則:工作流程本體由引擎反覆重播(replay),因此必須避免任何非確定性行為(如隨機數、時間函數、外部 I/O)。技能透過 `#[AsWorkflow]`、`#[AsActivityMethod]` 等屬性(Attribute)來標記程式碼結構,並將非確定性操作隔離至活動(Activity)中,活動的結果會寫入日誌,重播時直接取用。此外,技能也說明如何透過 `環境(WorkflowEnvironment)` 的 await、sleep、sideEffect 等方法進行等候與平行處理,並提供參考檔案(如遷移、活動選項、Nexus 合約)以處理進階場景。支援的框架包括 Symfony、Sylius 與 Magento,並且特別強調避免在呼叫點以字串寫入活動名稱,以維護執行中的相容性。

相容平台✓Claude Code~Codex CLI~Cursor
npx skills add gplanchat/durable-skill

Installed? Explore more 寫作與編輯 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

撰寫 Durable 程式碼

工作流程是一個純 PHP 類別。其簽章中沒有基底類別、介面或框架型別。使其具備持久性的關鍵在於引擎會根據日誌重播它,因此唯一真正的約束就是確定性。

三種形態

#[AsWorkflow('checkout')]                       // 叢集辨識的名稱
final class CheckoutWorkflow
{
    public function __construct(
        private readonly WorkflowEnvironment $environment,
    ) {}

    #[AsWorkflowMethod]                          // 每個工作流程類別剛好一個
    public function run(string $orderId): string
    {
        $orders = $this->environment->activityStub(OrderActivities::class);

        $receipt = $this->environment->await($orders->charge($orderId));
        $this->environment->sleep(Duration::seconds(30), '冷卻中');

        return $this->environment->await($orders->notify($receipt));
    }
}
interface OrderActivities                        // 合約承載名稱
{
    #[AsActivityMethod(name: 'shop.order.charge')]
    public function charge(string $orderId): string;
}

final class DoctrineOrderActivities implements OrderActivities   // 處理器執行實際工作
{
    public function charge(string $orderId): string { /* 真正的副作用在這裡 */ }
}
#[AsNexusService('billing')]                     // 另一個團隊、另一個部署
interface BillingContract
{
    #[AsNexusOperation('check')]
    public function check(string $order, int $amountInCents, string $currency): array;
}

絕對不要在呼叫點將活動名稱寫成字串。 Stub 會讀取合約的屬性,因此名稱只存在於一個地方。這是破壞現有工作流程最常見的方式:重新命名方法、保留屬性,正在執行的執行個體仍然能找到其活動。

約束規則:工作流程本體會被重播

每次引擎恢復執行時——可能在另一個行程中、數天之後——#[AsWorkflowMethod] 內的所有內容都會從頭到尾重新執行。因此,本體在重播時必須給出相同的答案:

絕對不要在 workflow 本體中使用請改用
rand(), uniqid(), time(), new \DateTime()$environment->sideEffect(fn() => …) — 結果只會被記錄一次
sleep(), usleep()$environment->sleep($duration, '為什麼要等待')
資料庫查詢、HTTP 呼叫、檔案寫入一個 activity
if (getenv(…)) 根據部署分支$environment->version($changeId, $min, $max)

Activity 才是非確定性應該存在的地方。它每次嘗試執行一次,其結果會寫入日誌,而在重播時日誌會回答而非執行程式碼。

⚠ Activity 會被重試;請將其撰寫成第二次嘗試是安全的。 引擎保證結果只會被記錄一次,但不保證你的副作用只發生一次。重複扣款是商業事故,不是框架錯誤。

等待

所有可排程的東西都會回傳一個 Awaitable。在你 await 它之前什麼都不會發生——這也是實現平行處理的方式:

$a = $stub->reserve($order);                     // 已排程,未等待
$b = $stub->quote($order);
[$reserved, $quoted] = $this->environment->await($this->environment->all($a, $b));

$first = $this->environment->await($this->environment->any($a, $b));
$twoOfThree = $this->environment->await($this->environment->some(2, $a, $b, $c));

await() 接受一個可選的截止時間:await($awaitable, Duration::minutes(5))。

參考檔案

讀取你需要的檔案,而非全部:

  • references/workflows.md — 訊號、更新、子工作流程、計時器、continueAsNew、版本控制,以及如何啟動一個執行個體(永遠不要在網路請求中內嵌)。
  • references/activities.md — ActivityOptions、重試策略、四個逾時、不可重試的例外、心跳、取消。
  • references/nexus.md — Nexus 合約、為何當工作流程履行操作時合約會一分為二,以及會默默失敗的 payload 陷阱。
  • references/migrations.md — 在碰觸現有程式碼之前請先閱讀此文件。 從 Temporal PHP SDK 遷移,或從一個 Durable 版本升級到下一個:gplanchat/durable-rector 負責改寫,而參考文件主要說明其規則拒絕猜測的五件事——其中兩項會在已經運行的伺服器上默默失敗。

交回程式碼前

  • 每個工作流程類別一個 #[AsWorkflowMethod],且該類別已向宿主宣告(Symfony/Sylius 會自動配置;Magento 在 di.xml 中列出)。
  • 每個活動呼叫都透過從合約建立的 stub 進行——沒有字串名稱。
  • 工作流程本體中沒有非確定性的東西。
  • 每個計時器都有一個摘要:它定義了觀測時間軸中的行名稱,而 TimerScheduled 在凌晨三點對操作人員毫無幫助。
  • 工作流程不要捕捉它無法補償的錯誤。未處理的失敗會結束執行並可見;被吞掉的錯誤則會讓訂單處於半處理狀態且無聲無息。

相關技能