Skip to content

engineer: add EnforceFormRequestToDtoRule (queue #55 instance 2, Phase-2 promotion)#33

Merged
Goosterhof merged 4 commits into
mainfrom
engineer/enforce-form-request-to-dto
Jun 12, 2026
Merged

engineer: add EnforceFormRequestToDtoRule (queue #55 instance 2, Phase-2 promotion)#33
Goosterhof merged 4 commits into
mainfrom
engineer/enforce-form-request-to-dto

Conversation

@Goosterhof

Copy link
Copy Markdown
Contributor

Mission

Extract the FormRequest instance of war-room enforcement queue #55 (opt-in convention enforcement — "arch test detects misuse but not omission") into a canonical PHPStan rule. Sister of PR #20 (EnforceResourceDataValidatorOptInRule, queue #55 instance 3) — same opt-in-omission pedagogy, same parameterized-base shape. War-room board item: WR-0066.

Source of truth

entreezuil's reflection-based Pest arch test tests/Arch/FormRequestsTest.php ("form requests with mutation actions define toDto method") — the stronger omission-detecting form. kendo carries only the weaker misuse-only shape; the entreezuil semantic is what ships here.

The rule

EnforceFormRequestToDtoRule — flags concrete classes extending Illuminate\Foundation\Http\FormRequest that neither declare nor inherit a toDto() method. Doctrine: ADR-0012 (FormRequest → DTO Flow). Identifier: enforceFormRequestToDto.missingToDtoMethod.

  • FQCN ancestor traversal via PHPStan reflection — short-name collisions in unrelated namespaces do not fire (fixture-proven).
  • Abstract classes exempt — the per-territory BaseFormRequest intermediate is exempt by shape, not by name.
  • Inherited / trait-provided toDto() satisfies the contractmethod_exists() parity with the source-of-truth Pest matcher.
  • Parameterized base: formRequestBaseClass PHPStan parameter (default Illuminate\Foundation\Http\FormRequest), wired via the parameters: + parametersSchema: + arguments: triplet — the PR engineer: add EnforceResourceDataValidatorOptInRule (queue #55 Phase-2 promotion) #20 convention.
  • Per-territory exemptions (entreezuil's LoginRequest precedent, read-only filter requests) migrate to consumer phpstan.neon ignoreErrors — never by name in rule code, per package convention. README documents the recipe.

Versioning

Per ADR-0021 §Versioning: candidate Major bump — the rule surfaces new errors in already-clean code wherever a consumer has a concrete FormRequest without toDto() (read-only/query requests included until suppressed). Pre-cascade audit required across ISMS, kendo, emmie, entreezuil, ublgenie, brick-inventory, codebook before tagging; flagged in the CHANGELOG [Unreleased] entry. Release-PR work is out of scope here.

Implementation note worth reviewer attention

The constructor default uses FormRequest::class (compile-time constant, never autoloads) instead of an FQCN string literal. Pint's class_keyword fixer calls class_exists() on class-shaped string literals, and the Pint phar bundles a real Illuminate\Foundation\Http\FormRequest whose ValidatesWhenResolvedTrait dependency is not bundled — a bare 'Illuminate\Foundation\Http\FormRequest' string literal anywhere in this package's PHP source makes composer format fatal with "Trait not found" (minimal reproduction confirmed against an isolated temp file). The use import is alias-only; consumers analysing non-Laravel trees are unaffected because ::class resolution requires no autoload. Documented in the rule docblock.

Gates (all local, fresh composer install, PCOV on — CI parity)

  • composer test — 91 tests / 142 assertions green (6 new)
  • composer phpstan — level max clean (10 rule services registered)
  • composer format:check — Pint clean
  • composer test:coverage + composer coverage:check — 88.24% (570/646) ≥ 83% gate
  • composer mutation:ci — MSI 82.94% / Covered MSI 82.94% (both ≥ 75 gate); zero escaped mutants on the new rule
  • composer audit — clean

Scope

Out of scope per orders: the routes-->can() sister (queue #55 instance 1, WR-0067), the issue #14 ratchet (WR-0064), cross-territory adoption, and the release PR.

🤖 Generated with Claude Code

Goosterhof and others added 2 commits June 10, 2026 10:19
…e-2 promotion)

Flags concrete FormRequest subclasses that neither declare nor inherit a
toDto() method. Doctrine: ADR-0012 (FormRequest -> DTO Flow). Identifier:
enforceFormRequestToDto.missingToDtoMethod. Promoted from entreezuil's
reflection-based Pest arch test (tests/Arch/FormRequestsTest.php) — the
second instance of the 'arch test detects misuse but not omission' shape
under war-room enforcement queue #55 (WR-0066). Sister of
EnforceResourceDataValidatorOptInRule (instance 3, PR #20): same
parameterized-base shape via the formRequestBaseClass PHPStan parameter
(default Illuminate\Foundation\Http\FormRequest).

Abstract classes exempt (BaseFormRequest intermediates); inherited and
trait-provided toDto() satisfy the contract (method_exists parity with the
source-of-truth Pest matcher); short-name collisions do not match (FQCN
ancestor traversal). Per-territory legitimate exemptions (LoginRequest)
migrate to consumer phpstan.neon ignoreErrors per package convention.

Implementation note: constructor default uses FormRequest::class instead
of an FQCN string literal — Pint's class_keyword fixer class_exists()'s
class-shaped string literals and the Pint phar bundles a real FormRequest
with an unbundled trait dependency, fataling composer format.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CHANGELOG [Unreleased] Added entry with candidate-Major versioning flag and
pre-cascade audit demand; CLAUDE.md rules-shipped table row + Phase 2
narrative (third Phase 2 addition); README rules table row + configurable
base class and exemptions section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Goosterhof Goosterhof added the Agent Review Requested Requesting review of specialized AI review agents. label Jun 10, 2026

@Goosterhof Goosterhof left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Concerns

0 blockers · 2 concerns · 1 nit · 1 praise · 3 inline

Adds EnforceFormRequestToDtoRule (queue #55 instance 2) on the PR #20 parameterized-base template — rule logic is sound, the FQCN-traversal and abstract-exemption shapes are fixture-proven, CI is green on both PHP lanes, and the PR body matches the diff. Two test-coverage/API-currency concerns keep this short of approve-worthy; neither blocks.

Findings (detail inline)

  • ⚠️ tests/Rules/EnforceFormRequestToDtoRuleTest.php:49 — trait-provided toDto() and the violating-transitive path are claimed but untested
  • ⚠️ src/Rules/EnforceFormRequestToDtoRule.php:120ClassReflection::isSubclassOf(string) is deprecated in the vendored PHPStan 2.2.2
  • nit: src/Rules/EnforceFormRequestToDtoRule.php:102
  • praise: the FormRequest::class constructor default (src/Rules/EnforceFormRequestToDtoRule.php:178) dodging the Pint-phar class_exists() fatal is a genuinely non-obvious correctness move — and backing it with a minimal reproduction plus a docblock explaining why the use import is alias-only is exactly how a landmine should be documented.

Out-of-scope declarations (routes-->can() sister WR-0067, issue #14 ratchet WR-0064, release PR) are acknowledged and stay out of scope.

Automated war-room agent review — posted because this PR carries the Agent Review Requested label.

Comment thread tests/Rules/EnforceFormRequestToDtoRuleTest.php
Comment thread src/Rules/EnforceFormRequestToDtoRule.php
Comment thread src/Rules/EnforceFormRequestToDtoRule.php
@jasperboerhof

Copy link
Copy Markdown
Contributor

Review Loop · 3/10 · FAIL — 🛑 1 🔴 1 🟡 1

phpstan-warroom-rules #33 · AC anchor: none

Caution

3 finding(s) posted inline — see the review comments on the changed lines. A BLOCKER must be resolved before merge.

Action

escalate — BLOCKERs present

1 similar comment
@jasperboerhof

Copy link
Copy Markdown
Contributor

Review Loop · 3/10 · FAIL — 🛑 1 🔴 1 🟡 1

phpstan-warroom-rules #33 · AC anchor: none

Caution

3 finding(s) posted inline — see the review comments on the changed lines. A BLOCKER must be resolved before merge.

Action

escalate — BLOCKERs present

Comment thread extension.neon Outdated
Comment thread tests/Rules/EnforceFormRequestToDtoRuleTest.php
Comment thread extension.neon Outdated
Comment thread tests/Rules/EnforceFormRequestToDtoRuleTest.php
Comment thread tests/Rules/EnforceFormRequestToDtoRuleTest.php
@jasperboerhof

Copy link
Copy Markdown
Contributor

PR Fixer · claimed

…-request-to-dto

# Conflicts:
#	CLAUDE.md
#	README.md
#	extension.neon
@jasperboerhof

Copy link
Copy Markdown
Contributor

PR Fixer · pushed

  • Target: phpstan-warroom-rules
  • PR: engineer: add EnforceFormRequestToDtoRule (queue #55 instance 2, Phase-2 promotion) #33 — engineer: add EnforceFormRequestToDtoRule (queue #55 instance 2, Phase-2 promotion)
  • Branch: engineer/enforce-form-request-to-dto
  • Mode: base-sync
  • Pushed: 1db6822
  • Worktree: removed (was ~/Code/agent-worktrees/phpstan-warroom-rules/pr-33)
  • Merged: origin/main → engineer/enforce-form-request-to-dto
  • Auto-resolved conflicts: 3 hunks across extension.neon, README.md, CLAUDE.md, tier=additive-list (HEAD adds EnforceFormRequestToDtoRule entry, BASE adds EnforceCurrentUserAttributeRule entry — disjoint additions vs ancestor in registration list / rule tables; no rule logic in src/ touched by any hunk)
  • Structured-file validation: YAML duplicate-key check WARN-skipped (ruamel.yaml not installed; both workflow .yml files auto-merged from one side only); no JSON/TOML touched
  • Local gates: composer format:check PASS · composer phpstan PASS (after composer dump-autoload + clear-result-cache — bootstrap-time classmap predated main's _stubs.php; not a merge regression) · composer test PASS (103 tests, 159 assertions)

…taBaseClass defaults

The shipped extension.neon parameter defaults used double-backslash
single-quoted FQCNs. NEON only unescapes \\ inside double-quoted strings;
in single quotes the backslashes stay literal, so the value decoded to a
class name matching nothing and ClassReflection::isSubclassOf() returned
false for every analysed class — both EnforceFormRequestToDtoRule and
(pre-existing, since PR #20) EnforceResourceDataValidatorOptInRule were
silent no-ops for any consumer using the default. CI stayed green because
every test constructs the rule directly via the PHP ::class constructor
default, never exercising the NEON registration path.

Fixes (PR #33 review — jasperboerhof BLOCKER + MAJOR + MINOR, General-review concerns):
- extension.neon: single-backslash defaults + inline NEON-quoting warning.
- container-resolved regression test per rule (getAdditionalConfigFiles +
  getByType) pinning the NEON default and %parameter% wiring; both confirmed
  to fail when the double-backslash defect is reintroduced.
- TraitProvidedToDtoRequest fixture + test (trait-provided toDto() satisfies
  the contract via hasNativeMethod trait flattening).
- TransitiveViolatorRequest fixture + test (concrete leaf extends abstract
  base without toDto() — transitive omission must fire at the leaf).

Gates: composer test (107), phpstan, format:check, coverage (88.06% >= 83) green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Goosterhof

Copy link
Copy Markdown
Contributor Author

Review addressed — ef0c5b1

Thanks for the BLOCKER — verified end-to-end before fixing, and it traced one level deeper than the new rule.

Addressed (4) · deferred with rationale (2)

🛑 BLOCKER — NEON \\ no-op ✅ Fixed. Reproduced the A/B independently: shipped 'Illuminate\\Foundation\\Http\\FormRequest'[OK] No errors on ViolatorRequest; single-backslash → fires at line 9. Both defaults now single-backslash, with an inline NEON-quoting warning comment guarding the pattern.

Scope note (worth a look): I did not defer the sibling resourceDataBaseClass (extension.neon:6) to a follow-up issue. I confirmed empirically that it has the identical defect — EnforceResourceDataValidatorOptInRule has been a silent no-op for every default-configured consumer since it shipped in PR #20 / v0.3.0. Filing a follow-up for a one-character fix on the line directly above the one I was already editing, while leaving a live architecture rule dead, didn't hold up. Fixed both in the same commit. Versioning ripple flagged in the CHANGELOG: restoring enforcement on the resource-data rule means ^0.3 consumers without a parameter override may surface previously-undetected violators — the pre-cascade audit must now treat that rule as effectively un-enforced until this ships.

🔴 MAJOR — neon wiring untested ✅ Fixed, for both rules. Added testRuleResolvesFromExtensionNeonAndFires per rule — resolves the rule from the PHPStan container via getAdditionalConfigFiles() + getByType(), exercising the NEON default and %parameter% wiring rather than the PHP ::class constructor default. Both confirmed to fail when the double-backslash defect is reintroduced (not a vacuous gate).

🟡 MINOR — trait toDto() unpinned ✅ Fixed. Added TraitProvidedToDtoRequest fixture + clean-expectation test. Also added TransitiveViolatorRequest (raised as a separate General-review concern) — concrete leaf extending the abstract base with no toDto() in the chain, must fire at the leaf; pins the inverse of the existing inherited-compliant case.

Deferred (both reviews agreed these belong elsewhere):

  • ClassReflection::isSubclassOf(string) deprecation (rule:120 + sister rule:139) — not mechanical (isSubclassOfClass() takes a ClassReflection, must resolve the FQCN via ReflectionProvider first, preserving the unknown-base no-op the string form gives for free). Filed as a package-wide enforcement-queue sweep covering both rules, not folded in here.
  • Nit (message hardcodes extends FormRequest under override) — declined for now to keep message shape identical to the sister rule. As the General review itself noted, it's "consistent either way"; diverging only this rule's message buys exactness at the cost of family consistency. Revisit as a both-rules change if it recurs.

Gates green locally: composer test (107), phpstan, format:check, coverage 88.06% ≥ 83.

@jasperboerhof

Copy link
Copy Markdown
Contributor

Review Loop · 9/10 · PASS

phpstan-warroom-rules #33 · AC anchor: No anchor (no issue_key / plan_dir / PR AC section) — surface anchor: package CLAUDE.md + ADR-0012/0021 + war-room queue #55 · head ef0c5b1d9b

Tip

No findings — clean against the review checklist.

Action

merge-ready

@jasperboerhof jasperboerhof self-requested a review June 12, 2026 11:09
@Goosterhof Goosterhof merged commit e9ef319 into main Jun 12, 2026
2 checks passed
@Goosterhof Goosterhof deleted the engineer/enforce-form-request-to-dto branch June 12, 2026 13:04
Goosterhof added a commit that referenced this pull request Jun 15, 2026
The `## War Room ADR Projections` section was last synced 2026-05-08 and
mis-described what the package distributes. Reconciled every projection
bullet against the 11 rules shipping on `main`, mapping each by its
docblock "Doctrine source" line per ADR-0021.

- ADR-0009: rule released in v0.3.0 (was stale `Phase 2, [Unreleased]`).
- ADR-0011: add `ForbidEloquentMutationInControllersRule` (PR #28).
- ADR-0012: was "no HTTP surface" — now ships `EnforceFormRequestToDtoRule` (PR #33).
- ADR-0019: add `ForbidEloquentMutationInControllersRule`; retire stale
  `EnforceExplicitHydrationRule` Phase-2 candidate note.
- ADR-0029: ADDED — ships `EnforceAuditTransactionScopeRule` (PR #27).
- ADR-0001: add `LogBuilderTruncateRule` to the §Append-only bullet.
- New subsection for war-room Architectural-Principle rules with no
  published ADR: `ForbidAbortHelperRule` + `EnforceCurrentUserAttributeRule`
  (PR #26, Principle #9).
- Bump Last synced to 2026-06-15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Agent Review Requested Requesting review of specialized AI review agents.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants