撰寫 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在凌晨三點對操作人員毫無幫助。 - 工作流程不要捕捉它無法補償的錯誤。未處理的失敗會結束執行並可見;被吞掉的錯誤則會讓訂單處於半處理狀態且無聲無息。