refactor(bundle): DurableExtension's registrations move into @internal loaders (#342) - #543
Conversation
The whole container DurableExtension builds, one line per id, for twelve variants (six configurations, profiler on and off). The next commit adds the test that reads it; together they pin the container before any registration moves out of the extension. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…gisters (#342) Every definition, alias and durable.* parameter, with class, visibility, factory, arguments keyed as written (references as @id, @?id, @!id), and tags with their attributes. Twelve variants take each configuration branch of the extension on both sides: backends, the Messenger activity transport, activity contracts, the Guzzle and PSR-18 clients, the legacy mixed stores, and an application's own redactor and observer aliases. Moving a registration must leave the fixture byte-identical; a commit that means to change the container shows it as a fixture diff. Regenerate with DURABLE_UPDATE_SNAPSHOT=1. Checked: raising the lock middleware's priority, or dropping the cache warmer, fails it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…st (#342, review) Review of C0 (antoine): $before held the aliases variant A pre-registers, so the dump left out exactly the ids A exists to watch. An extension that overwrote the application's observer alias, or registered the default redactor over the application's, passed unseen. The application's aliases now stay out of $before, and their final state is in the fixture: two lines, both still the application's. Overwriting either fails it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ervices (#342) registerWorkflowDefinitionLoader, registerActivityExecutor, registerRuntime, registerParentChildCoordinator and registerActivityContractResolver move verbatim into an @internal loader class, as public statics with their own scope and docblocks; load() calls them at the same place. isTemporalNative() and aliasObserver() become @internal public statics, shared with the loaders. The container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…registrations join CoreServices (#342) registerEngine, registerActivityContractCacheWarmer, registerWorkflowQueryRunner and registerWorkflowBackend move verbatim; load() calls them at the same place. The container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…CoreServices (#342) registerWorkflowControlHandlers moves verbatim; load() calls it at the same place. The container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r\MessengerServices (#342) Moved verbatim; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ssengerServices (#342) Moved verbatim; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ntStores (#342) Moved verbatim; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…342) The DSN branch of registerEventStore() moves out as EventStores::registerTemporalEventStore(); moved whole, the method would exceed the 200-line commit limit. Its body is unchanged, the early return stays in the caller, and the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…Stores (#342) Moved verbatim; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gplanchat
left a comment
There was a problem hiding this comment.
Incremental review (antoine), C0–C4, through 7bb18d4. Verdict: OK, no finding.
C0 (snapshot): OK after d5d65a3. The dump now keeps the aliases an application set before load(). Overwriting the app's observer alias, or registering the default redactor over the app's alias, both fail the snapshot.
Per-commit check. I checked out each of the 8 commits alone and ran DurableContainerSnapshotTest there:
| Commit | Lines | Fixture | Snapshot |
|---|---|---|---|
| 32dfbdf | 179 | untouched | OK |
| 5bfb590 | 138 | untouched | OK |
| ba576aa | 122 | untouched | OK |
| af02c26 | 91 | untouched | OK |
| bd90e03 | 182 | untouched | OK |
| 85621b8 | 46 | untouched | OK |
| 0c5aa4c | 173 | untouched | OK |
| 7bb18d4 | 46 | untouched | OK |
Verbatim. For each commit I listed the added lines that do not appear among its removed lines, whitespace ignored. The only ones left are new method signatures, their call sites, and self::isTemporalNative() becoming DurableExtension::isTemporalNative(). Every moved body is unchanged.
0c5aa4c, the one extraction. registerTemporalEventStore() uses only its parameters ($container, $temporalConfig, $dsn, $journal) and locals it defines itself ($fromAssembly, whose closure parameters are $id, $class, $method, $public and $arguments, plus $client, $guzzleClient and $psr18), so no outer variable is lost. The early return; stays in the caller, right after the call. The T and N variants (Temporal, with guzzle_client / psr18_client) run through it and stay byte-identical.
At the head: tests/unit/DurableBundle 211 OK; PHPStan, Psalm and cs:check 0. DurableExtension.php is 564 lines so far; load() under 150 lines is the Done-when still to come.
…DbalStores (#342) Moved verbatim; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tores (#342) Moved verbatim; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ader\EventStores (#342) Moved verbatim; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oader\Observability (#342) Moved verbatim; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ventStores (#342) Moved verbatim; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nds() into Loader\CoreServices (#342) The heartbeat sender, ActivityMessageProcessor and its Messenger handler are not commands; CoreServices::registerActivityProcessor() takes them, called where they were. Body unchanged; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#342) Moved verbatim; the container snapshot is byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…event_store.inner (#342) The DBAL journal replaces the in-memory one, and without a DSN nothing reads it. The snapshot shows the one line: the id leaves variants D and L, and stays on in_memory and wherever a Temporal DSN is set. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he other backends (#342) The in-memory metadata store is registered as durable.workflow_metadata_store.inner from the start, with the interface aliased to it. The in-memory and DBAL paths no longer rename a definition to put their decorator in front. The snapshot shows Temporal only: the public definition becomes a public alias to the private .inner. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gplanchat
left a comment
There was a problem hiding this comment.
Incremental review (antoine), the push after 7bb18d4, through 694a136. Verdict: OK on the code, with one small "Done when" gap to close.
Per-commit check. I checked out each commit alone and ran DurableContainerSnapshotTest there. It is 8 commits, not 9: the eight you listed.
| Commit | Lines | Fixture | Snapshot |
|---|---|---|---|
| 7202ad0 | 130 | untouched | OK |
| b2ce084 | 193 | untouched | OK |
| 9b47d3c | 121 | untouched | OK |
| f944589 | 139 | untouched | OK |
| 49706b4 | 149 | untouched | OK |
| 13f5905 | 96 | untouched | OK |
| 65b00e6 | 169 | untouched | OK |
| 694a136 | 21 | changed (intended) | OK |
Verbatim. In the seven moves, the only added lines that do not appear among the removed lines (whitespace ignored) are method signatures, call sites and self:: becoming DurableExtension:: calls.
13f5905, the extraction. registerActivityProcessor() uses $container, $config, $isTemporalNative (its parameters) and two locals it assigns itself ($activityTransportConfig, $activityTransportName). The rest of registerCommands() uses $activityTransport, a separate variable computed on its own, so nothing the caller needed was taken away. It is called in place, and the snapshot is identical.
C5, 694a136. The fixture diff is exactly the one line (IDTNLAidtnla becoming I.TN.Ai.tn.a on def durable.event_store.inner), and UPGRADE.md covers it. I looked for a reader on D or L:
- In the tree, every reference to the id is on a path that keeps it: the Temporal read-through (
EventStores.php:134, variants T and N) and the in-memory catalog and projecting journal (:193,:201, variants I and A). The reference inDurableRunCatalogWiringTestis the in-memory case. - In the fixture, every line referencing
@durable.event_store.innercarriesI....Ai....aor..TN....tn..; none carries D, L, d or l.
I found no reader to miss.
"Done when" gap, small: DurableContainerSurfaceTest pins the public ids for one backend, not three
testThePublicSurfaceStaysReachable() loads a single configuration: the legacy event_store: dbal, workflow_metadata: dbal. The box says "pins the public ids for the three backends".
The snapshot does record that the three are public on every variant, with one detail worth pinning. On Temporal, WorkflowMetadataStore is a public definition, not an alias. The fixture has no alias line for it on T and t.
Fix: make that data provider backend × id: in_memory, dbal and temporal (DSN), crossed with the three ids. Assert hasAlias() || hasDefinition() and that the resolved service is public, so the Temporal metadata case passes as a definition.
At the head: DurableExtension.php is 118 lines and load() is 40. tests/unit/DurableBundle 211 OK, the full suite 2,321 OK; PHPStan, Psalm and cs:check 0.
…lass ids kept as aliases (#342) Engine, runtime, activity executor, definition loader, contract resolver, uuid generator, parent/child coordinator, query runner, workflow backend, timer dispatcher, activity message processor, heartbeat sender. Each definition goes private; its class or interface id stays, as an alias with the visibility it had, so autowiring and ->get() are unchanged. Test edited: DurableDeclaredWiringTest getDefinition->findDefinition on ActivityContractResolver, the id became an alias, same assertions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… on every backend (#342) The public-ids test loaded one configuration, the legacy DBAL keys. It now runs in_memory, the legacy keys, backend: dbal and temporal, each against the three ids, with the same assertions. Before 515f17c the temporal metadata row fails: WorkflowMetadataStore was a definition there, not an alias. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ity transport move to durable.* ids (#342) Each definition goes private; its class or interface id stays, as an alias with the visibility it had. Test edited: DurableProfilerWiringTest getDefinition->findDefinition on WorkflowResumeDispatcher (two lines), the id became an alias, same assertions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…urable.* ids (#342) durable.handler.{resume_workflow,fire_workflow_timers,deliver_signal, deliver_update,activity_run} (the Temporal signal and update handlers share the ids of their journal counterparts; the branches are exclusive), durable.command.{diagnose,worker,setup}, durable.data_collector. Class ids stay as aliases with the visibility they had. Tests edited, the ids became aliases, same assertions: - getDefinition->findDefinition: SetupCommandTest:34, DurableWorkerCommandWiringTest (5 lines), DurableDiagnoseCommandWiringTest:47, DurableRunCatalogWiringTest:87; - SetupCommandTest:33 hasDefinition->has: hasDefinition() on an alias is always false, which would have made the assertion vacuous. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cursor move to durable.temporal.* ids (#342) durable.temporal.{client,workflow_task_runner,history_cursor} and durable.temporal.rpc.{activity,execution,nexus}, each built by the assembly as before; class ids stay as aliases with the visibility they had. WorkflowTaskProcessor keeps its class id: DurableContainerSurfaceTest pins it as a private definition. Test edited: DurableTemporalAssemblyWiringTest:44 getDefinition->findDefinition, the ids became aliases, same assertions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tions (#342) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…342) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gplanchat
left a comment
There was a problem hiding this comment.
Review (antoine), 694a136 → fc91654, C11–C13 and the Done-when fix. Verdict: OK, no finding.
No visibility flip. I parsed the fixture before (694a136) and after (fc91654) per variant and per id:
- Public to private or vanished: 0 of the 228 public (variant, id) pairs. Every class id that was public is still public, now as an alias.
- Private to public: 0.
- New ids: 32, all
durable.*, all private. There are 35 def-to-alias conversions. C11's TemporalWorkflowMetadataStore, among others, aliases an id that already existed. - Every alias resolves: all 56 aliases have a target defined in the same variant, with no dangling durable target.
Nothing reaches a renamed id by definition outside the loaders. I read every getDefinition / hasDefinition in src/:
ActivityHandlerPassresolves an alias before itshasDefinition()(ActivityExecutor->durable.activity_executor, a definition).RequireLockFactoryPass,NexusHandlerPassandTemporalReceiversPassusedurable.*ids, which were not renamed.WorkflowPasswalks tagged ids.- The plugin test's
RunDashboardbelongs to the plugin extension.
Test edits, not weakenings.
getDefinitionbecomesfindDefinitionin 7 files. It resolves the alias, and the same assertion then runs on the same definition.SetupCommandTest:33,hasDefinitionbecomeshas. It is stronger:hasDefinition()on what became an alias would always be false, and theassertFalsewould have gone vacuous.
The Done-when gap is closed (5a1f4e8). The public ids are pinned on 4 configs × 3 ids. With C11's WorkflowMetadataStore alias on Temporal, no "definition or alias" allowance is needed.
Head: full PHPUnit 2,330 OK (109 skips); PHPStan, Psalm and cs:check 0. From my side, #342's Done-when is met: load() 40 lines, the public ids pinned on every backend, and UPGRADE entries for the ids that changed.
…waits for the user (#342) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Refs #342 (finding M20, M12 residue). C0–C13 of the plan on the issue are in. C14 (the public ids) waits for the user's decision.
What is here
DurableContainerSnapshotTestdumps every definition, alias and parameter the extension registers intoFixtures/container-snapshot.txt.DURABLE_UPDATE_SNAPSHOT=1rewrites the fixture and still asserts.DurableExtensioninto@internalclasses underDependencyInjection/Loader/.CoreServices,MessengerServices,EventStores,DbalStores,Observability,Commands.DurableExtension.phpgoes from 979 to 118 lines, andload()is 40.durable.event_store.inner.WorkflowMetadataStoreis an alias, as on the other backends.durable.workflow_metadata_store.innerfrom the start.DurableContainerSurfaceTest::testThePublicSurfaceStaysReachableruns in_memory, the legacy DBAL keys,backend: dbaland temporal against the three ids. Cases were only added. Before C11, the temporal metadata row fails.durable.*ids (af10746, 9d08094, 9562dab, 16cf789).WorkflowTaskProcessorkeeps its class id, becauseDurableContainerSurfaceTestpins it as a private definition.findDefinition()andhas().Deviation from the issue's "Fix"
The loaders are
@internalclasses, not config files; a config-file form can follow if wanted. Verbatim closure files shared one scope, so their local variables collided. The classes keep each method byte-for-byte, and the snapshot proves the container is unchanged.Two commits are extractions rather than verbatim moves, to stay under the 200-line commit limit. Both bodies are unchanged, and the snapshot stays byte-identical.
registerEventStore().registerCommands().Tests edited
Each id below became an alias, and each edit keeps the same assertion.
DurableDeclaredWiringTest:getDefinition→findDefinition(ActivityContractResolver, 2 lines).DurableProfilerWiringTest:getDefinition→findDefinition(WorkflowResumeDispatcher, 2 lines).SetupCommandTest:34:getDefinition→findDefinition.SetupCommandTest:33:hasDefinition→has. On an alias,hasDefinition()is always false, so keeping it would have made the assertion vacuous.DurableWorkerCommandWiringTest:getDefinition→findDefinition(5 lines).DurableDiagnoseCommandWiringTest:47,DurableRunCatalogWiringTest:87,DurableTemporalAssemblyWiringTest:44:getDefinition→findDefinition.DurableContainerSurfaceTest: cases added (see above).C14 (the public ids) waits for the user's decision and is not part of this PR.
Possible follow-up, outside #342: on dbal with a Temporal DSN (variant N),
durable.event_store.temporalhas no reader, becauseEventStoreInterfacepoints at DBAL. The compiler drops it as an unused private service.Done when
DurableExtension::load()under 150 lines (40);DurableContainerSurfaceTestpins the public ids for the three backends.UPGRADE.mdfor any removed public id. No public id is removed here, so this box waits for C14.Reviewer: antoine, per commit. Check that the fixture is untouched by each move commit and that the snapshot is green at each one.