Écrire du code durable
Un workflow est une classe PHP simple. Pas de classe parente, pas d'interface, pas de type du framework dans sa signature. Ce qui le rend durable, c'est que le moteur le rejoue depuis un journal, donc la seule règle qui compte vraiment est le déterminisme.
Les trois formes
#[AsWorkflow('checkout')] // le nom connu du cluster
final class CheckoutWorkflow
{
public function __construct(
private readonly WorkflowEnvironment $environment,
) {}
#[AsWorkflowMethod] // exactement un par 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), 'refroidissement');
return $this->environment->await($orders->notify($receipt));
}
}
interface OrderActivities // le CONTRAT porte les noms
{
#[AsActivityMethod(name: 'shop.order.charge')]
public function charge(string $orderId): string;
}
final class DoctrineOrderActivities implements OrderActivities // le GESTIONNAIRE fait le travail
{
public function charge(string $orderId): string { /* vrais effets de bord ici */ }
}
#[AsNexusService('billing')] // autre équipe, autre déploiement
interface BillingContract
{
#[AsNexusOperation('check')]
public function check(string $order, int $amountInCents, string $currency): array;
}
N'écrivez jamais un nom d'activité sous forme de chaîne au point d'appel. Le stub lit les attributs du contrat, donc le nom vit à un seul endroit. C'est la manière la plus courante de casser un workflow existant : renommer la méthode, garder l'attribut, et les exécutions en cours trouvent toujours leur activité.
La règle qui lie : le corps du workflow est rejoué
Tout dans une méthode #[AsWorkflowMethod] s'exécute à nouveau, depuis le début, à chaque fois que le moteur reprend l'exécution — peut-être dans un autre processus, des jours plus tard. Le corps doit donc donner les mêmes réponses lorsqu'il est rejoué :
| Jamais dans un corps de workflow | Utilisez plutôt |
|---|---|
rand(), uniqid(), time(), new \DateTime() | $environment->sideEffect(fn() => …) — le résultat est journalisé une fois |
sleep(), usleep() | $environment->sleep($duration, 'pourquoi on attend') |
| Une requête base de données, un appel HTTP, une écriture fichier | une activité |
if (getenv(…)) pour bifurquer selon le déploiement | $environment->version($changeId, $min, $max) |
Une activité est l'endroit où le non-déterminisme a sa place. Elle s'exécute une fois par tentative, son résultat est écrit dans le journal, et lors de la reprise, le journal répond à la place du code.
⚠ Une activité est réessayée ; écrivez-la pour qu'une deuxième tentative soit sûre. Le moteur garantit que le résultat est enregistré une fois, pas que votre effet de bord l'est. Débiter une carte deux fois est un incident métier, pas un bug du framework.
Attendre
Tout ce qui peut être planifié retourne un Awaitable. Rien ne se produit tant que vous ne l'attendez pas — c'est aussi ainsi que vous obtenez du parallélisme :
$a = $stub->reserve($order); // planifié, non attendu
$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() accepte une échéance optionnelle : await($awaitable, Duration::minutes(5)).
Fichiers de référence
Lisez celui dont vous avez besoin, pas les trois :
references/workflows.md— signaux, mises à jour, workflows enfants, minuteurs,continueAsNew, versioning, et comment une exécution est démarrée (jamais en ligne dans une requête web).references/activities.md—ActivityOptions, politique de réessai, les quatre délais d'attente, exceptions non réessayables, battements de cœur, annulation.references/nexus.md— contrats Nexus, pourquoi un contrat se divise en deux lorsqu'un workflow remplit une opération, et le piège de la charge utile qui échoue silencieusement.references/migrations.md— lisez ceci avant de toucher au code existant. En venant du SDK Temporal PHP, ou en passant d'une version de Durable à la suivante :gplanchat/durable-rectorfait la réécriture, et la référence porte surtout sur les cinq choses que ses règles refusent de deviner — deux d'entre elles échouent silencieusement, sur un serveur qui tourne déjà.
Avant de rendre le code
- Un seul
#[AsWorkflowMethod]par classe de workflow, et la classe est déclarée à l'hôte (Symfony/Sylius l'auto-configurent ; Magento la liste dansdi.xml). - Chaque appel d'activité passe par un stub construit à partir d'un contrat — pas de noms sous forme de chaîne.
- Rien de non déterministe dans le corps du workflow.
- Chaque minuteur a un résumé : c'est ce qui nomme la ligne dans la chronologie d'observation, et
TimerScheduledne dit rien à un opérateur à 3h du matin. - Le workflow n'attrape pas ce qu'il ne peut pas compenser. Un échec non géré termine l'exécution et est visible ; un échec avalé laisse une commande à moitié traitée et silencieuse.