From f3fed0012f7c0eb1c345b9a4e6b8c830c9fd017b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Planchat?= Date: Fri, 4 Sep 2026 00:16:57 +0200 Subject: [PATCH 1/2] =?UTF-8?q?docs(guide):=20le=20premier=20workflow=20s'?= =?UTF-8?q?enregistre,=20et=20le=20parcours=20m=C3=A8ne=20quelque=20part?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux défauts du même chemin, celui que suit quelqu'un qui découvre Durable. **Le premier workflow ne s'enregistrait pas.** Le guide posait `#[AsActivity]` sur la classe d'implémentation. Or cet attribut est lu par `ActivityContractResolver` sur le **contrat**, comme préfixe de nommage optionnel — sur une implémentation, personne ne le lit. Et il y remplaçait `#[AsActivityHandler]`, le seul des deux que le bundle autoconfigure. Le lecteur suivait le guide à la lettre et rien ne se branchait. Corrigé dans les quatre pages qui portaient l'erreur, deux langues comprises. La page des activités annonçait la même chose en toutes lettres — « souvent annotée d'un `#[AsActivity]` pour son nom » — et la page des paquets promettait que `#[AsWorkflow]` et `#[AsActivity]` s'enregistrent seuls, ce qui est faux des deux : le premier n'est pas autoconfiguré à ce jour, le second n'est pas un attribut d'enregistrement. **Le parcours ne menait à aucun résultat.** Il s'arrêtait sur un `dispatchNewWorkflowRun()` qui rend `void`, sans dire qu'un consommateur doit tourner ni lequel. Une étape 5 le dit, avec la commande — dont les noms de transports viennent du `messenger.yaml` que le guide fait écrire, ce qui explique qu'aucun document ne puisse la donner sans ça. Et elle distingue les deux profils que le guide mélangeait : `in-memory://` avec des magasins en mémoire vaut pour un test qui envoie et draine dans un seul processus, mais un transport en mémoire ne survit pas à son processus — envoyer depuis une requête web pour consommer dans un worker séparé ne peut pas marcher, et le rejeu non plus. Le profil multi-processus demande de vrais transports **et** un magasin durable, les deux, sinon le worker prend une entrée nommant un workflow dont il ne voit pas le journal. Refs: B5 et B6 de documentation/audit/ Co-Authored-By: Claude Opus 5 (1M context) --- documentation/user/activities/_index.fr.md | 8 ++- documentation/user/activities/_index.md | 8 ++- .../user/getting-started/_index.fr.md | 68 ++++++++++++++++++- documentation/user/getting-started/_index.md | 67 +++++++++++++++++- documentation/user/packages/_index.fr.md | 7 +- documentation/user/packages/_index.md | 6 +- 6 files changed, 150 insertions(+), 14 deletions(-) diff --git a/documentation/user/activities/_index.fr.md b/documentation/user/activities/_index.fr.md index 981d8dbf..82eff594 100644 --- a/documentation/user/activities/_index.fr.md +++ b/documentation/user/activities/_index.fr.md @@ -10,7 +10,7 @@ Cette page résume comment on **écrit** des activités en Durable. Le détail n ## Deux pièces 1. **L'interface de contrat d'activité** — les méthodes que le workflow a le droit d'appeler, chacune marquée d'un **`#[AsActivityMethod]`**. Depuis le workflow, on passe par un **`ActivityStub`** (**ActivityInvoker** dans les ADR). -2. **La classe d'implémentation** — une classe concrète (souvent annotée d'un **`#[AsActivity]`** pour son nom) qui **implémente** le contrat et fait le vrai travail. +2. **La classe d'implémentation** — une classe concrète portant **`#[AsActivityHandler]`**, qui nomme le contrat qu'elle implémente. C'est cet attribut qui l'enregistre : le bundle l'autoconfigure, et sans lui le workflow ne trouve aucun gestionnaire à l'exécution. ## Exemple : contrat et implémentation @@ -22,15 +22,19 @@ L'**interface** énumère les méthodes que le workflow peut planifier. Chaque m declare(strict_types=1); use Gplanchat\Durable\Attribute\AsActivity; +use Gplanchat\Durable\Attribute\AsActivityHandler; use Gplanchat\Durable\Attribute\AsActivityMethod; +// `#[AsActivity]` est optionnel et se pose sur le **contrat** : il préfixe le nom des activités +// déclarées en dessous. Sur une classe d'implémentation, personne ne le lit. +#[AsActivity(name: 'order-activities')] interface OrderActivities { #[AsActivityMethod(name: 'charge-order')] public function charge(string $orderId): string; // type de retour synchrone, côté worker } -#[AsActivity(name: 'order-activities')] +#[AsActivityHandler(contract: OrderActivities::class)] final class OrderActivitiesHandler implements OrderActivities { public function __construct( diff --git a/documentation/user/activities/_index.md b/documentation/user/activities/_index.md index 7181b52c..82139075 100644 --- a/documentation/user/activities/_index.md +++ b/documentation/user/activities/_index.md @@ -10,7 +10,7 @@ This page summarizes how you **author** activities in Durable. Normative detail ## Two pieces 1. **Activity contract interface** — methods the workflow may call, each marked with **`#[AsActivityMethod]`**. From the workflow you interact through **`ActivityStub`** (**ActivityInvoker** in ADRs). -2. **Activity implementation class** — concrete class (often annotated with **`#[AsActivity]`** for naming) that **implements** the contract and performs real work. +2. **Activity implementation class** — concrete class carrying **`#[AsActivityHandler]`**, naming the contract it implements. That attribute is what registers the class: the bundle autoconfigures it, and without it the workflow finds no handler at run time. ## Example: activity contract and implementation @@ -22,15 +22,19 @@ The **interface** lists methods the workflow may schedule. Each exposed method c declare(strict_types=1); use Gplanchat\Durable\Attribute\AsActivity; +use Gplanchat\Durable\Attribute\AsActivityHandler; use Gplanchat\Durable\Attribute\AsActivityMethod; +// `#[AsActivity]` is optional and belongs on the **contract**: it prefixes the names of the +// activities declared below. On an implementation class nothing reads it. +#[AsActivity(name: 'order-activities')] interface OrderActivities { #[AsActivityMethod(name: 'charge-order')] public function charge(string $orderId): string; // synchronous return type on the worker } -#[AsActivity(name: 'order-activities')] +#[AsActivityHandler(contract: OrderActivities::class)] final class OrderActivitiesHandler implements OrderActivities { public function __construct( diff --git a/documentation/user/getting-started/_index.fr.md b/documentation/user/getting-started/_index.fr.md index 0566b3f3..3caa4fec 100644 --- a/documentation/user/getting-started/_index.fr.md +++ b/documentation/user/getting-started/_index.fr.md @@ -163,7 +163,7 @@ App\Workflow\: ### Déclarer les implémentations d'activité -Les classes d'implémentation d'activité sont des services Symfony ordinaires (l'autowiring s'applique). Si vous posez `#[AsActivityHandler]` sur la classe, le bundle les ramasse tout seul dès que le service est marqué. +Rien à écrire. Une classe portant `#[AsActivityHandler]` est ramassée par l'autoconfiguration du bundle dès qu'elle est un service — ce qu'avec l'`autoconfigure: true` par défaut d'une application Symfony elle est déjà. C'est là que les workflows ci-dessus diffèrent : eux ont encore besoin de la balise. --- @@ -178,8 +178,11 @@ declare(strict_types=1); namespace App\Workflow\Activity; +use Gplanchat\Durable\Attribute\AsActivity; use Gplanchat\Durable\Attribute\AsActivityMethod; +// Optionnel : préfixe le nom des activités déclarées en dessous. +#[AsActivity(name: 'greeting-activities')] interface GreetingActivities { #[AsActivityMethod(name: 'greet')] @@ -196,9 +199,10 @@ declare(strict_types=1); namespace App\Workflow\Activity; -use Gplanchat\Durable\Attribute\AsActivity; +use Gplanchat\Durable\Attribute\AsActivityHandler; -#[AsActivity(name: 'greeting-activities')] +// C'est cet attribut qui enregistre la classe ; le bundle l'autoconfigure. +#[AsActivityHandler(contract: GreetingActivities::class)] final class GreetingActivitiesHandler implements GreetingActivities { public function greet(string $name): string @@ -265,6 +269,64 @@ final class GreetController } ``` +### 5 — Faire tourner un consommateur, sinon rien n'arrive + +`dispatchNewWorkflowRun()` rend `void` et fait exactement ce que son nom dit : il *envoie*. Le +workflow s'exécute quand quelque chose consomme les transports configurés plus haut. D'ici là +l'exécution attend en file — un tableau de bord la dira `RUNNING`, ce qui est vrai et inutile : ça +veut dire *pas terminée*, pas *quelqu'un s'en occupe*. + +```bash +php bin/console messenger:consume durable_workflows durable_activities +``` + +Ces deux noms sont les transports que **vous** avez déclarés dans `messenger.yaml`. Aucun document +ne peut vous donner cette commande sans que vous ayez écrit ce fichier d'abord — c'est ce qui fait +qu'on cherche la pièce manquante partout sauf dans sa propre configuration. + +Pour voir ce que le moteur retient d'une exécution : + +```bash +php bin/console durable:execution:diagnose greet-abc123 +``` + +#### Dans quel profil êtes-vous ? + +Deux configurations fonctionnent. Les mélanger est le faux pas habituel, et il échoue en silence. + +**Un seul processus — les tests.** Transports `in-memory://` et magasins en mémoire. Envoi, reprise +et activité se passent dans un même processus PHP, donc un test envoie et draine d'un seul geste. Un +transport en mémoire **ne survit pas à son processus** : y envoyer depuis une requête web pour +consommer dans un worker séparé ne peut pas marcher, et le rejeu non plus — le journal dont le +worker aurait besoin vit dans la mémoire du processus web. + +**Plusieurs processus — développement local et production.** De vrais transports **et** un magasin +durable. Les deux, sinon le worker prend en file une entrée nommant un workflow dont il ne voit pas +le journal : + +```yaml +durable: + event_store: + type: dbal + workflow_metadata: + type: dbal + child_workflow: + parent_link_store: + type: dbal + dbal: + connection: doctrine.dbal.default_connection +``` + +```dotenv +MESSENGER_DURABLE_WORKFLOW_DSN=doctrine://default +MESSENGER_DURABLE_ACTIVITY_DSN=doctrine://default +``` + +La règle derrière les deux profils : **une exécution survit exactement à ce à quoi survivent son +journal et sa file.** Routez `ResumeWorkflowMessage` ou `ActivityMessage` vers un transport qu'un +worker séparé ne peut pas lire, et le workflow rejoue dans la requête web qui l'a démarré puis meurt +avec le processus — précisément la panne que l'exécution durable existe pour supprimer. + --- ## Démarrer les workers Temporal (production / mode dev) diff --git a/documentation/user/getting-started/_index.md b/documentation/user/getting-started/_index.md index 1703ec75..a9ba5b25 100644 --- a/documentation/user/getting-started/_index.md +++ b/documentation/user/getting-started/_index.md @@ -163,7 +163,7 @@ App\Workflow\: ### Register activity implementations -Activity implementation classes are registered as normal Symfony services (autowiring applies). If you use `#[AsActivityHandler]` on the class, the bundle picks them up automatically when the service is tagged. +Nothing to write. A class carrying `#[AsActivityHandler]` is picked up by the bundle's autoconfiguration as soon as it is a service — which, with the default `autoconfigure: true` of a Symfony application, it already is. This is where workflows above differ: those still need the tag. --- @@ -178,8 +178,11 @@ declare(strict_types=1); namespace App\Workflow\Activity; +use Gplanchat\Durable\Attribute\AsActivity; use Gplanchat\Durable\Attribute\AsActivityMethod; +// Optional: prefixes the names of the activities declared below. +#[AsActivity(name: 'greeting-activities')] interface GreetingActivities { #[AsActivityMethod(name: 'greet')] @@ -196,9 +199,10 @@ declare(strict_types=1); namespace App\Workflow\Activity; -use Gplanchat\Durable\Attribute\AsActivity; +use Gplanchat\Durable\Attribute\AsActivityHandler; -#[AsActivity(name: 'greeting-activities')] +// This attribute is what registers the class; the bundle autoconfigures it. +#[AsActivityHandler(contract: GreetingActivities::class)] final class GreetingActivitiesHandler implements GreetingActivities { public function greet(string $name): string @@ -265,6 +269,63 @@ final class GreetController } ``` +### 5 — Run a consumer, or nothing happens + +`dispatchNewWorkflowRun()` returns `void`, and does exactly what its name says: it *dispatches*. The +workflow runs when something consumes the transports you configured above. Until then the execution +sits in a queue — a dashboard will call it `RUNNING`, which is true and unhelpful: it means *not +finished*, not *someone is working on it*. + +```bash +php bin/console messenger:consume durable_workflows durable_activities +``` + +Those two names are the transports **you** declared in `messenger.yaml`. No document can hand you +this command without you having written that file first, which is why it is easy to look for the +missing piece everywhere except in your own configuration. + +To see what the engine holds for one run: + +```bash +php bin/console durable:execution:diagnose greet-abc123 +``` + +#### Which profile are you in? + +Two configurations work. Mixing them is the usual first stumble, and it fails silently. + +**One process — tests.** `in-memory://` transports with the in-memory stores. Dispatch, resume and +activity all happen inside a single PHP process, so a test can dispatch and drain in one go. An +in-memory transport **does not outlive its process**: dispatching from a web request and consuming +in a separate worker cannot work here, and neither can replay — the journal the worker would need +lives in the web process's memory. + +**Several processes — local dev and production.** Real transports **and** a durable store. Both, or +the worker picks up a queue entry naming a workflow whose journal it cannot see: + +```yaml +durable: + event_store: + type: dbal + workflow_metadata: + type: dbal + child_workflow: + parent_link_store: + type: dbal + dbal: + connection: doctrine.dbal.default_connection +``` + +```dotenv +MESSENGER_DURABLE_WORKFLOW_DSN=doctrine://default +MESSENGER_DURABLE_ACTIVITY_DSN=doctrine://default +``` + +The rule behind both profiles: **an execution survives exactly what its journal and its queue +survive.** Route `ResumeWorkflowMessage` or `ActivityMessage` to a transport a separate worker +cannot read, and the workflow replays inside the web request that started it and dies with the +process — the very failure durable execution exists to remove. + --- ## Start Temporal workers (production / dev mode) diff --git a/documentation/user/packages/_index.fr.md b/documentation/user/packages/_index.fr.md index 919e2bdf..b1047e80 100644 --- a/documentation/user/packages/_index.fr.md +++ b/documentation/user/packages/_index.fr.md @@ -72,8 +72,11 @@ composer require gplanchat/durable-bundle Ce qu'il fait, et que vous écririez autrement à la main : -- **L'autoconfiguration.** Les classes portant `#[AsWorkflow]` et `#[AsActivity]` s'enregistrent seules ; - vous ne les listez pas dans un fichier de conteneur. +- **L'autoconfiguration.** Les classes portant `#[AsActivityHandler]` s'enregistrent seules sur + l'exécuteur d'activités ; vous ne les listez pas dans un fichier de conteneur. `#[AsActivity]` est + un attribut de **nommage** posé sur le contrat, pas d'enregistrement, et `#[AsWorkflow]` n'est + **pas** autoconfiguré à ce jour — une classe de workflow a encore besoin de la balise + `durable.workflow`, que le guide de démarrage montre. - **Le câblage Messenger.** Reprises de workflow et envois d'activité sont routés vers les transports que vous nommez dans `durable.yaml`, si bien qu'un workflow qui se suspend reprend par vos files existantes. diff --git a/documentation/user/packages/_index.md b/documentation/user/packages/_index.md index b6917bfd..c7024db3 100644 --- a/documentation/user/packages/_index.md +++ b/documentation/user/packages/_index.md @@ -68,8 +68,10 @@ composer require gplanchat/durable-bundle What it does that you would otherwise write by hand: -- **Autoconfiguration.** Classes carrying `#[AsWorkflow]` and `#[AsActivity]` are registered on their - own; you do not list them in a container file. +- **Autoconfiguration.** Classes carrying `#[AsActivityHandler]` register themselves on the activity + executor; you do not list them in a container file. `#[AsActivity]` is a naming attribute on the + contract, not a registration one, and `#[AsWorkflow]` is **not** autoconfigured today — a workflow + class still needs the `durable.workflow` tag, which the getting-started guide shows. - **Messenger wiring.** Workflow resumes and activity dispatches are routed to the transports you name in `durable.yaml`, so a workflow that suspends resumes through your existing queues. - **One console command.** `durable:execution:diagnose ` prints what the engine holds From 9a6a7e5cd23ed89e95a13c7e934c4e37e6e6ebd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Planchat?= Date: Fri, 4 Sep 2026 01:53:33 +0200 Subject: [PATCH 2/2] =?UTF-8?q?docs(guide):=20le=20premier=20workflow=20d?= =?UTF-8?q?=C3=A9marre=20pour=20de=20vrai,=20et=20son=20minuteur=20ne=20bo?= =?UTF-8?q?ucle=20plus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La relecture croisée a montré que B5 n'était fermé qu'à moitié. L'attribut fautif est bien corrigé partout — `#[AsActivityHandler]` sur les implémentations, `#[AsActivity]` sur les seuls contrats — mais le guide fait baliser `../src/Workflow/` sans `exclude` et range ses activités dessous, en `App\Workflow\Activity`. `WorkflowPass` ne filtre rien d'autre qu'un antislash dans le nom de classe, `WorkflowRegistry::registerClass()` appelle le chargeur sans garde, et `WorkflowDefinitionLoader:144` lève « must have exactly one #[AsWorkflowMethod], found 0 ». Le lecteur qui suivait le guide à la lettre obtenait toujours un conteneur qui ne se construit pas — pour une autre raison, dans le paragraphe voisin de celui que cette branche réécrivait. Le banc du dépôt (`symfony/config/services.yaml:23-29`) montrait déjà la forme qui marche : au dossier balisé, ses seuls workflows. Le routage publié envoyait `FireWorkflowTimersMessage` en `sync`. Le minuteur est alors rejoué dans le processus qui vient de le programmer. Le banc porte exactement cette ligne dans sa configuration de base — et l'écrase dans `when@dev`, `when@prod` et `when@test`, avec la raison en commentaire : « les réveils timer portent DelayStamp ; sync:// les exécute tout de suite et ignore le délai — les workflows sample (Query 2s, Periodic 0.2s) ne terminent jamais ». Le guide avait copié la ligne sans la surcharge. Il route désormais vers `durable_workflows`. `activity_contracts` nommait `App\Workflow\Activity\OrderActivities`, que le guide ne crée jamais : `cache:warmup` mourait sur une `ReflectionException` avant le premier workflow. C'est `GreetingActivities`, le contrat des étapes suivantes. Le contrôleur d'exemple répondait `200` à un envoi asynchrone qui n'a rien exécuté. `202`. Le profil « plusieurs processus » nommait `doctrine.dbal.default_connection` sans jamais faire installer ni le pont DBAL ni DoctrineBundle. La commande manquante est là. La parité ligne à ligne des deux langues est conservée : les seize titres restent aux mêmes numéros de ligne dans `_index.md` et `_index.fr.md`. Reste ouvert, hors périmètre : un banc qui suive le guide et rougisse quand il cesse de marcher. Le dépôt en a la machine — le job « Module Magento (il démarre pour de vrai) » va jusqu'à « Un workflow tourne dedans ». C'est un job de plus, pas une architecture, et c'est ce qui aurait fait de cette branche une régression rouge plutôt qu'une relecture. Co-Authored-By: Claude Opus 5 (1M context) --- .../user/getting-started/_index.fr.md | 25 +++++++++++++++---- documentation/user/getting-started/_index.md | 24 +++++++++++++++--- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/documentation/user/getting-started/_index.fr.md b/documentation/user/getting-started/_index.fr.md index 3caa4fec..e28b5ff5 100644 --- a/documentation/user/getting-started/_index.fr.md +++ b/documentation/user/getting-started/_index.fr.md @@ -84,7 +84,7 @@ durable: activity_contracts: cache: cache.app contracts: - - App\Workflow\Activity\OrderActivities # listez ici vos interfaces d'activité + - App\Workflow\Activity\GreetingActivities # listez ici vos interfaces d'activité ``` Basculez sur Temporal à l'exécution en définissant `DURABLE_DSN` dans votre environnement : @@ -110,7 +110,7 @@ framework: routing: Gplanchat\Durable\Transport\ResumeWorkflowMessage: durable_workflows Gplanchat\Durable\Transport\ActivityMessage: durable_activities - Gplanchat\Durable\Transport\FireWorkflowTimersMessage: sync + Gplanchat\Durable\Transport\FireWorkflowTimersMessage: durable_workflows Gplanchat\Durable\Transport\DeliverWorkflowSignalMessage: sync Gplanchat\Durable\Transport\DeliverWorkflowUpdateMessage: sync ``` @@ -158,9 +158,15 @@ Toute classe portant `#[AsWorkflow]` dans votre espace de noms de workflows est # config/services.yaml App\Workflow\: resource: '../src/Workflow/' + exclude: '../src/Workflow/Activity/' tags: [durable.workflow] ``` +L'`exclude` compte. La balise ne filtre rien : chaque service qu'elle attrape est passé au registre +des workflows, qui exige exactement un `#[AsWorkflowMethod]` et lève sinon. Baliser un dossier qui +porte aussi vos gestionnaires d'activité, et le conteneur cesse de se construire sur une erreur +nommant une classe que vous n'enregistriez pas exprès. Au dossier balisé, ses seuls workflows. + ### Déclarer les implémentations d'activité Rien à écrire. Une classe portant `#[AsActivityHandler]` est ramassée par l'autoconfiguration du bundle dès qu'elle est un service — ce qu'avec l'`autoconfigure: true` par défaut d'une application Symfony elle est déjà. C'est là que les workflows ci-dessus diffèrent : eux ont encore besoin de la balise. @@ -264,7 +270,9 @@ final class GreetController $executionId = 'greet-'.uniqid(); $this->dispatcher->dispatchNewWorkflowRun($executionId, 'greet', ['name' => $name]); - return new JsonResponse(['executionId' => $executionId]); + // 202 : le run est en file, pas terminé. Répondre 200 ici est la première chose qui + // fait attendre un résultat qu'aucun consommateur n'a encore produit. + return new JsonResponse(['executionId' => $executionId], JsonResponse::HTTP_ACCEPTED); } } ``` @@ -301,8 +309,15 @@ consommer dans un worker séparé ne peut pas marcher, et le rejeu non plus — worker aurait besoin vit dans la mémoire du processus web. **Plusieurs processus — développement local et production.** De vrais transports **et** un magasin -durable. Les deux, sinon le worker prend en file une entrée nommant un workflow dont il ne voit pas -le journal : +durable, sinon le worker prend une entrée nommant un workflow dont il ne voit pas le journal. + +Ce profil demande deux paquets que la prise en main ci-dessus n'installe pas — le journal DBAL, et +DoctrineBundle pour le service `doctrine.dbal.default_connection` qu'il nomme : + +```bash +composer require gplanchat/durable-bridge-dbal doctrine/doctrine-bundle +``` + ```yaml durable: diff --git a/documentation/user/getting-started/_index.md b/documentation/user/getting-started/_index.md index a9ba5b25..47bd3d54 100644 --- a/documentation/user/getting-started/_index.md +++ b/documentation/user/getting-started/_index.md @@ -84,7 +84,7 @@ durable: activity_contracts: cache: cache.app contracts: - - App\Workflow\Activity\OrderActivities # list your activity interfaces here + - App\Workflow\Activity\GreetingActivities # list your activity interfaces here ``` Switch to Temporal at runtime by setting `DURABLE_DSN` in your environment: @@ -110,7 +110,7 @@ framework: routing: Gplanchat\Durable\Transport\ResumeWorkflowMessage: durable_workflows Gplanchat\Durable\Transport\ActivityMessage: durable_activities - Gplanchat\Durable\Transport\FireWorkflowTimersMessage: sync + Gplanchat\Durable\Transport\FireWorkflowTimersMessage: durable_workflows Gplanchat\Durable\Transport\DeliverWorkflowSignalMessage: sync Gplanchat\Durable\Transport\DeliverWorkflowUpdateMessage: sync ``` @@ -158,9 +158,15 @@ Any class annotated with `#[AsWorkflow]` in your workflow namespace is auto-regi # config/services.yaml App\Workflow\: resource: '../src/Workflow/' + exclude: '../src/Workflow/Activity/' tags: [durable.workflow] ``` +The `exclude` matters. The tag is not a filter: every service it matches is handed to the workflow +registry, which requires exactly one `#[AsWorkflowMethod]` and throws otherwise. Tag a folder that +also holds your activity handlers and the container stops building, with an error naming a class you +never meant to register. Keep the tagged folder to workflows, or exclude what is not one. + ### Register activity implementations Nothing to write. A class carrying `#[AsActivityHandler]` is picked up by the bundle's autoconfiguration as soon as it is a service — which, with the default `autoconfigure: true` of a Symfony application, it already is. This is where workflows above differ: those still need the tag. @@ -264,7 +270,9 @@ final class GreetController $executionId = 'greet-'.uniqid(); $this->dispatcher->dispatchNewWorkflowRun($executionId, 'greet', ['name' => $name]); - return new JsonResponse(['executionId' => $executionId]); + // 202: the run is queued, not done. Answering 200 here is the first thing that makes a + // caller poll for a result that no consumer has produced yet. + return new JsonResponse(['executionId' => $executionId], JsonResponse::HTTP_ACCEPTED); } } ``` @@ -301,7 +309,15 @@ in a separate worker cannot work here, and neither can replay — the journal th lives in the web process's memory. **Several processes — local dev and production.** Real transports **and** a durable store. Both, or -the worker picks up a queue entry naming a workflow whose journal it cannot see: +the worker picks up a queue entry naming a workflow whose journal it cannot see. + +This profile needs two packages the quick start above does not install — the DBAL journal, and +DoctrineBundle for the `doctrine.dbal.default_connection` service it names: + +```bash +composer require gplanchat/durable-bridge-dbal doctrine/doctrine-bundle +``` + ```yaml durable: