Escrevendo código durável
Um workflow é uma classe PHP normal. Sem classe base, sem interface, sem tipo de framework em sua assinatura. O que o torna durável é que o motor o reproduz a partir de um diário, então a única regra que realmente importa é o determinismo.
As três formas
#[AsWorkflow('checkout')] // o nome que o cluster conhece
final class CheckoutWorkflow
{
public function __construct(
private readonly WorkflowEnvironment $environment,
) {}
#[AsWorkflowMethod] // exatamente um por classe de workflow
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), 'resfriando');
return $this->environment->await($orders->notify($receipt));
}
}
interface OrderActivities // o CONTRATO carrega os nomes
{
#[AsActivityMethod(name: 'shop.order.charge')]
public function charge(string $orderId): string;
}
final class DoctrineOrderActivities implements OrderActivities // o MANIPULADOR faz o trabalho
{
public function charge(string $orderId): string { /* efeitos colaterais reais aqui */ }
}
#[AsNexusService('billing')] // outra equipe, outro deploy
interface BillingContract
{
#[AsNexusOperation('check')]
public function check(string $order, int $amountInCents, string $currency): array;
}
Nunca escreva um nome de activity como string no local de chamada. O stub lê os atributos do contrato, portanto o nome vive em exatamente um lugar. Esta é a forma mais comum de quebrar um workflow existente: renomear o método, manter o atributo e as execuções em andamento ainda encontram sua activity.
A regra que vincula: o corpo do workflow é reproduzido
Tudo dentro de um método #[AsWorkflowMethod] é executado novamente, do início, toda vez que o motor
retoma a execução — possivelmente em outro processo, dias depois. Portanto, o corpo deve dar
as mesmas respostas quando reproduzido:
| Nunca no corpo do workflow | Use em vez disso |
|---|---|
rand(), uniqid(), time(), new \DateTime() | $environment->sideEffect(fn() => …) — o resultado é registrado uma vez no diário |
sleep(), usleep() | $environment->sleep($duration, 'por que esperamos') |
| Uma consulta ao banco, uma chamada HTTP, uma escrita em arquivo | uma activity |
if (getenv(…)) para bifurcar com base no deploy | $environment->version($changeId, $min, $max) |
Uma activity é onde o não-determinismo pertence. Ela é executada uma vez por tentativa, seu resultado é escrito no diário e, na reprodução, o diário responde em vez do código.
⚠ Uma activity é tentada novamente; escreva-a de forma que uma segunda tentativa seja segura. O motor garante que o resultado seja registrado uma vez, não que seu efeito colateral o seja. Cobrar um cartão duas vezes é um incidente de negócio, não um bug do framework.
Esperando
Tudo que pode ser agendado retorna um Awaitable. Nada acontece até que você o aguarde —
que também é como você obtém paralelismo:
$a = $stub->reserve($order); // agendado, não aguardado
$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() aceita um prazo opcional: await($awaitable, Duration::minutes(5)).
Arquivos de referência
Leia o que você precisa, não todos os três:
references/workflows.md— sinais, atualizações, child workflows, timers,continueAsNew, versionamento e como uma execução é iniciada (nunca inline em uma requisição web).references/activities.md—ActivityOptions, política de retry, os quatro timeouts, exceções não-repetíveis, heartbeats, cancelamento.references/nexus.md— Contratos Nexus, por que um contrato se divide em dois quando um workflow cumpre uma operação, e a armadilha de payload que falha silenciosamente.references/migrations.md— leia isto antes de tocar em código existente. Saindo do Temporal PHP SDK, ou movendo de uma versão do Durable para a próxima:gplanchat/durable-rectorfaz a reescrita, e a referência é principalmente sobre as cinco coisas que suas regras se recusam a adivinhar — duas das quais falham silenciosamente, em um servidor que já está em execução.
Antes de devolver o código
- Um
#[AsWorkflowMethod]por classe de workflow, e a classe é declarada ao host (Symfony/Sylius autoconfiguram; Magento lista emdi.xml). - Toda chamada de activity passa por um stub construído a partir de um contrato — sem nomes em string.
- Nada não-determinístico no corpo do workflow.
- Todo timer tem um resumo: é o que nomeia a linha na linha do tempo de observação, e
TimerSchedulednão diz nada a um operador às 3h da manhã. - O workflow não captura o que não pode compensar. Uma falha não tratada encerra a execução e é visível; uma engolida deixa um pedido meio processado e silencioso.