Community写作与编辑github.com

gplanchat/durable-skill

Claude Code 技能:在 PHP 中编写 Durable 工作流、活动和 Nexus 操作。

durable-skill 是什么?

该技能为在 PHP 中构建基于 Durable 引擎的工作流、活动(Activity)和 Nexus 操作提供了全面的指导和约定。它强调工作流的主体必须具有确定性,因为引擎会基于日志重放执行。技能文档清晰区分了三种核心代码形状:工作流类(带有 `#[AsWorkflow]` 和 `#[AsWorkflowMethod]` 属性)、活动接口及其实现(通过 `#[AsActivityMethod]` 定义名称)、以及 Nexus 服务接口(通过 `#[AsNexusService]` 和 `#[AsNexusOperation]` 实现跨团队调用)。文档特别警告不要在工作流体内使用 `rand()`、`time()`、直接调用数据库或 HTTP 请求等非确定性操作,而应使用环境提供的方法(如 `sideEffect`、`activityStub`、`sleep`)或通过活动委托副作用。它还详细说明了如何通过 `Awaitable` 实现并行等待(`all`、`any`、`some`),以及如何参考独立的引用文件来了解信号、更新、重试策略、迁移等高级主题。支持 Symfony/Sylius(自动配置)和 Magento(`di.xml` 声明)等平台。

兼容平台✓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 类。其签名中不需要基类、接口或框架类型。使其具备 durable(持久)特性的关键在于引擎会根据日志重放执行,因此唯一必须遵守的规则就是确定性。

三种形状

#[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;
}

永远不要在调用点将活动名称写成字符串。 存根会读取契约的属性,因此名称只存在于一个地方。这是破坏现有工作流最常见的方式:重命名方法后,保留属性,正在运行的执行仍能找到其活动。

必须遵守的规则:工作流体会被重放

#[AsWorkflowMethod] 中的所有代码都会从顶部重新执行,每次引擎恢复执行时都会发生——可能在另一个进程中,也可能在几天后。因此,工作流体在被重放时必须给出相同的结果:

工作流体中禁止使用的操作代替方案
rand()、uniqid()、time()、new \DateTime()$environment->sideEffect(fn() => …) —— 结果只记录一次
sleep()、usleep()$environment->sleep($duration, '等待原因')
数据库查询、HTTP 调用、文件写入活动
使用 if (getenv(…)) 根据部署环境分支$environment->version($changeId, $min, $max)

活动 是非确定性代码的归属地。每次尝试它只会运行一次,其结果被写入日志;重放时,日志返回结果而不是代码本身。

⚠ 活动可能会被重试;请确保第二次尝试是安全的。 引擎保证 结果 被记录一次,但不保证你的副作用只执行一次。重复扣款是业务事故,而不是框架缺陷。

等待

所有可调度操作都返回一个 Awaitable。在你等待它之前什么也不会发生——这也是实现并行的方法:

$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、版本控制以及如何启动一个执行(不要在 Web 请求中内联启动)。
  • 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 中列出)。
  • 每个活动调用都通过基于契约构建的存根进行——没有字符串名称。
  • 工作流体中没有非确定性代码。
  • 每个定时器都有摘要说明:它是在观测时间线中标识该行的名称,TimerScheduled 在凌晨 3 点向操作员传递的信息非常有限。
  • 工作流不会捕获它无法补偿的异常。未处理的失败会结束执行并可见;被吞掉的异常会让订单处于半处理状态且悄无声息。

相关技能