From 51bb1d32395d4b3140bd32bc6efd457f8938ed50 Mon Sep 17 00:00:00 2001 From: Conduction Release Bot Date: Sat, 22 Aug 2026 09:45:00 +0200 Subject: [PATCH] docs(hydra-gates): the contract opt-in must cover the analysers too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The opt-in section told adopters how to make the interface resolvable AT RUNTIME and stopped there. That is how I missed it: psalm and phpstan never run the test bootstrap — they resolve types through the composer autoload map — so the guarded require does nothing for them. If the interface appears in PRODUCTION code rather than only in tests, removing the psr-4 prefix turns every mention into an undefined class. Measured on adoption of v1.9.0: 204 errors on decidiq, 213 on stackiq, all the same class, and both went red in CI. Adds the two seams that fix it — psalm ``, phpstan `scanDirectories` — with the reason they are stubs rather than autoload entries: the class exists at runtime because OpenRegister supplies it, and putting it back into the autoloader is exactly the defect #531 removed. Also records why phpstan's entry belongs in the APP's config and not the shared base: openregister owns the real lib/Contract/, so scanning the vendored copy there would declare the same class twice. The migration was verified against PHPUnit in both directions and the analysers were never run. Check lib/, not just tests/. --- hydra-gates/README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/hydra-gates/README.md b/hydra-gates/README.md index b2aa053b..b2e4b684 100644 --- a/hydra-gates/README.md +++ b/hydra-gates/README.md @@ -190,6 +190,43 @@ in the bootstrap `phpunit.xml` actually loads: several apps ship two or three. registration order across independently loaded apps is exactly the thing nobody controls. Asking whether the interface is resolvable is order-independent. +### The bootstrap is not enough if your `lib/` typehints the contract + +**Psalm and PHPStan never run the test bootstrap.** They resolve types through +the composer autoload map, so the guarded `require` above is invisible to them. +If the interface appears in *production* code — a constructor promotion, a +parameter type, a `::class` fetch in `lib/` — the analyser will now report it as +undefined, and the count is not small: 204 errors on decidiq, 213 on stackiq, +all the same class. + +That is analysis-only. The class still exists at runtime, because OpenRegister +supplies it. So the fix is a stub, not an autoload entry — putting it back into +the autoloader is precisely the defect this change removed. + +**Psalm** — in `psalm.xml`: + +```xml + + + + +``` + +**PHPStan** — in the app's own `phpstan.neon`: + +```neon +parameters: + scanDirectories: + - %currentWorkingDirectory%/vendor/conduction/hydra-gates/hydra-gates/contracts +``` + +Not in the shared base: openregister owns the real `lib/Contract/`, and scanning +the vendored copy there would declare the same class twice. + +This was missed when the prefix was removed, because the migration was verified +against PHPUnit in both directions and the analysers were never run. Two apps +went red on adoption. Check `lib/`, not just `tests/`. + --- ## What it needs at runtime