CommunityRédaction et éditiongithub.com

gplanchat/durable-skill

Compétence Claude Code pour écrire des workflows durables, activités et opérations Nexus en PHP

Qu'est-ce que durable-skill ?

Cette compétence guide les développeurs dans l'écriture de code durable avec Temporal en PHP (via le SDK `gplanchat/durable`). Elle explique que les workflows ne sont que des classes PHP sans héritage ni interface, à une exception près : leur corps doit être déterministe car il est rejoué intégralement à chaque reprise. La compétence définit trois « formes » : les workflows (marqués avec `#[AsWorkflow]`), les activités (via des contrats `#[AsActivityMethod]`) et les opérations Nexus (`#[AsNexusOperation]`). Elle insiste sur la règle cruciale de ne jamais passer un nom d'activité sous forme de chaîne au point d'appel, mais de toujours utiliser les attributs du contrat pour garantir la compatibilité lors des renommages. Elle détaille ce qu'il faut éviter dans un workflow (appels non déterministes comme `rand()`, `sleep()`, requêtes DB) et comment utiliser `$environment->sideEffect()`, `$environment->sleep()`, ou déléguer à des activités. Elle couvre également l'attente parallèle, les références aux fichiers de documentation dédiés (workflows, activités, Nexus, migrations), et fournit une checklist de vérification avant de rendre le code, incluant la gestion des échecs et la sécurité des reprises d'activité.

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add gplanchat/durable-skill

Installed? Explore more Rédaction et édition skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

É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 workflowUtilisez 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 fichierune 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-rector fait 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 dans di.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 TimerScheduled ne 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.

Skills associés