Skip to content

B5 and B6 — the first workflow registers, and the walkthrough leads somewhere - #276

Open
gplanchat wants to merge 2 commits into
mainfrom
docs/premier-workflow-qui-marche
Open

B5 and B6 — the first workflow registers, and the walkthrough leads somewhere#276
gplanchat wants to merge 2 commits into
mainfrom
docs/premier-workflow-qui-marche

Conversation

@gplanchat

@gplanchat gplanchat commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Documentation only, six files, two languages. No code.

B5 — the guide's first workflow did not register

The guide put #[AsActivity(name: …)] on the implementation class. Checked against the code: that attribute is read by ActivityContractResolver::resolveViaReflection() on the contract, as an optional naming prefix. On an implementation, nobody reads it.

Worse, it took the place of #[AsActivityHandler(contract: …)] — the only one of the two that DurableBundle::build() autoconfigures. Readers followed the guide to the letter and their class was never registered on the activity executor.

The real model, now the guide's:

Attribute Where What it does
#[AsActivity(name:)] on the contract prefixes activity names — optional
#[AsActivityMethod(name:)] on the contract's methods names an activity
#[AsActivityHandler(contract:)] on the implementation registers it — autoconfigured

Three pages carried the same error, and the activities page spelled it out: "a concrete class (often annotated with #[AsActivity] for its name)". Fixed in both languages.

The packages page also promised that "classes carrying #[AsWorkflow] and #[AsActivity] register themselves". That is wrong for both: #[AsWorkflow] is not autoconfigured to date (that is the subject of #255) and #[AsActivity] is not a registration attribute. The page now states the real situation, and points at the durable.workflow tag the guide actually has you write.

B6 — the walkthrough led to no result

The guide stopped at a dispatchNewWorkflowRun() that returns void. Nothing said a consumer had to be running, nor which one — and the only section that mentioned workers was conditioned on Temporal, hence absent from the path the guide prescribes by default.

A step 5 now says it, with the command:

php bin/console messenger:consume durable_workflows durable_activities

Those two names are the transports the reader has just declared in their own messenger.yaml — which is why no document can hand you this command without that file existing first, and why one looks for the missing piece everywhere except in one's own configuration.

The two profiles the guide was mixing

This is the substance of B6, and it was written nowhere. The guide prescribed in-memory:// "for tests and local dev", then showed a dispatch from a controller. The two together cannot work:

  • One process — tests. An in-memory transport does not outlive its process. Dispatching from a web request to consume in a separate worker is impossible, and so is 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.

The configuration given for the second profile is not invented: event_store.type, workflow_metadata.type and child_workflow.parent_link_store.type do accept dbal, checked in Configuration.php before being written.

Neighbourhood

Step 5 answers #263 ("nothing says a worker must run, nor which queues it consumes") on the documentation side; the issue also asks for a first-class durable:worker command, which remains to be done. The profile distinction states what #259 observes from another angle: routing durable work where a separate worker cannot read it makes the workflow replay inside the request that started it.

Verification

Final check across the six files: no implementation carries #[AsActivity] any more, each carries #[AsActivityHandler], and every remaining #[AsActivity] is on an interface.


Addendum — 2026-09-04, after cross-review

B5 was only half closed. The offending attribute is indeed fixed everywhere — that was verified. But the guide had readers tag all of ../src/Workflow/ without an exclude, and placed the activities underneath it, in App\Workflow\Activity. WorkflowPass filters on nothing but a backslash in the class name, WorkflowRegistry::registerClass() calls the loader without a guard, and WorkflowDefinitionLoader:144 throws "must have exactly one #[AsWorkflowMethod], found 0". A reader following the guide to the letter still ended up with a container that would not build — for a different reason, in the paragraph next to the one this branch was rewriting. The repository's own test bench (symfony/config/services.yaml:23-29) already showed the shape that works: the tagged folder holds workflows and nothing else.

The published routing sent FireWorkflowTimersMessage to sync. The timer is then replayed inside the very process that scheduled it. The bench carries that exact line in its base configuration — and overrides it in when@dev, when@prod and when@test, with the reason in a comment: timer wake-ups carry a DelayStamp, sync:// runs them immediately and ignores the delay, so the sample workflows never finish. The guide had copied the line without the override. It now routes to durable_workflows.

activity_contracts named App\Workflow\Activity\OrderActivities, which the guide never creates: cache:warmup died on a ReflectionException before the first workflow. It is GreetingActivities, the contract of the following steps.

The example controller answered 200 to an asynchronous dispatch that had executed nothing. It answers 202.

The "several processes" profile named doctrine.dbal.default_connection without ever having you install either the DBAL bridge or DoctrineBundle. The missing command is there.

Line-by-line parity between the two languages is preserved: the sixteen headings remain on the same line numbers in _index.md and _index.fr.md.

Still open, out of scope: a bench that follows the guide and goes red when it stops working. The repository has the machinery — the "Magento module (it actually boots)" job goes as far as "a workflow runs in it". That is one more job, not an architecture, and it is what would have made this branch a red regression rather than a review finding.

gplanchat and others added 2 commits September 4, 2026 00:16
…elque part

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) <noreply@anthropic.com>
…r ne boucle plus

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) <noreply@anthropic.com>
@gplanchat gplanchat changed the title B5 et B6 — le premier workflow s'enregistre, et le parcours mène quelque part B5 and B6 — the first workflow registers, and the walkthrough leads somewhere Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant