feat(rules): configurable controllerNamespacePrefixes on the three controller-scoped rules (WR-0283)#48
Conversation
…ntroller-scoped rules — ForbidEloquentMutationInControllers, EnforceCurrentUserAttribute, ForbidResourceWrappedInJsonResponse (WR-0283) Replaces the hardcoded `private const CONTROLLER_NAMESPACE_PREFIX = 'App\Http\Controllers'` gate on all three controller-scoped rules with a shared constructor-injected `array $controllerNamespacePrefixes` (default `['App\Http\Controllers']`). A class is in scope when its namespace str_starts_with ANY configured prefix, so canonical sub-namespaces (kendo's `App\Http\Controllers\Central\*`) still match under the default byte-for-byte. Wired through extension.neon mirroring the formRequestBaseClass precedent: `controllerNamespacePrefixes` parameter default + `listOf(string())` schema + `%controllerNamespacePrefixes%` argument on all three rules' service registrations. Consumers with divergent controller namespaces (emmie's `App\Http\Client\Controllers` / `App\Http\Admin\Controllers`) opt them in from phpstan.neon — no consumer namespace hardcoded in a rule body. Resolves the ForbidEloquentMutationInControllersRule docblock's standing "lift this into a controllerNamespacePrefixes parameter" TODO, applied uniformly so one knob governs the whole controller surface. Default-unchanged invariant pinned end-to-end by a container-resolved test per rule (testRuleResolvesFromExtensionNeonAndFiresOnDefaultPrefix) plus every pre-existing fixture/test. New sub-namespaced-controller fixture per rule proves the emmie shape is CLEAN under the default and FLAGGED once the prefix is configured. Backward-compatible MINOR (default reproduces current behaviour; zero new errors in any consumer at the default). Not tagged. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_016BD7TLVaCmFLakYWkxaudp
Town Crier Review · 9/10 · PASS · 🔎 Independentphpstan-warroom-rules #48 · AC anchor: PR description (no reachable board) · head Tip Reviewed a contract-package feature that lifts the hardcoded No findings — clean against the review checklist. |
jasperboerhof
left a comment
There was a problem hiding this comment.
Auto-approved — Town Crier verdict PASS @Head, CI green, no open MAJOR+ thread. Our approval is our independent vote (approve-alongside): a peer's review / CHANGES_REQUESTED never withholds it — we verify every blocker ourselves, and a real one drops our own verdict below PASS. See the verdict comment + inline notes.
Goosterhof
left a comment
There was a problem hiding this comment.
✅ Approve-worthy
0 blockers · 0 concerns · 1 nit · 2 praise · 0 inline
CI green (8.4/8.5). Bus thread: dispatch posted an independent PASS at 14:16 (no findings, correctly flagged the standard NEON-list-replace semantics as documented, not a defect); Jasper's harness auto-approved on that verdict. I independently re-derived the same result — confirming, not just deferring to, dispatch's read.
Lifts the hardcoded App\Http\Controllers gate on the three controller-scoped rules (ForbidEloquentMutationInControllersRule, EnforceCurrentUserAttributeRule, ForbidResourceWrappedInJsonResponseRule) into a shared configurable controllerNamespacePrefixes parameter, mirroring the formRequestBaseClass/resourceDataBaseClass precedent — closes the emmie sub-namespace blind spot (WR-0283).
Verification
- Default-preservation claim confirmed: all three rules replace
str_starts_with($ns, self::CONST)with a foreach-any-prefix loop over a single-element default array — byte-identical match set at the default. - "No fourth rule still hardcodes the prefix" claim confirmed by direct grep of
src/Rules/*.php—CONTROLLER_NAMESPACE_PREFIXis fully gone from the tree; the only other namespace-scoped rules (EnforceActionTransactionsRule,ForbidDatabaseManagerInActionsRule) gate onApp\Actions, unaffected. extension.neonwiring confirmed on all three rules' service definitions +parametersSchema: listOf(string()).- Container-resolution tests (
testRuleResolvesFromExtensionNeonAndFiresOnDefaultPrefix) correctly close the gap a constructor-default-only test would miss — a NEON single/double-backslash quoting slip would silently no-op the whole rule for every default consumer, and only an extension.neon-loaded test catches that class of regression. - Constructor shape (
private array $controllerNamespacePrefixes = ['App\Http\Controllers']) matches the existingformRequestBaseClass/resourceDataBaseClasssingle-param precedent style — no stylistic drift.
Nit
- The constructor property is typed bare
array(phpdoc-onlylist<string>) on all three rules — matches existing precedent ($formRequestBaseClassis typedstring, so there's no establishedlist<string>-native pattern here either), so this isn't a regression, just worth tightening toarray<int, string>/list<string>via a phpstan-level self-hint if the package ever raises its own bar on iterable value types.
Praise
- The
testRuleResolvesFromExtensionNeonAndFiresOnDefaultPrefixgate per rule — pinning the shipped NEON default, not the PHP constructor default, closes exactly the failure class this kind of config-lift usually ships with. - CHANGELOG entry states the versioning call explicitly (backward-compatible MINOR, contrasted against the v0.5.0 candidate-major shape) — keeps the release-decision reasoning auditable instead of asserted.
Automated war-room agent review — posted because this PR carries the Agent Review Requested label.
What & why
The three controller-scoped rules —
ForbidEloquentMutationInControllersRule,EnforceCurrentUserAttributeRule,ForbidResourceWrappedInJsonResponseRule— each hardcodedprivate const CONTROLLER_NAMESPACE_PREFIX = 'App\Http\Controllers'. emmie ships controllers underApp\Http\Client\Controllers(4) +App\Http\Admin\Controllers(9), so their inline Eloquent mutations /$request->user()/ resource-wraps were rule-invisible (surfaced by the emmie WR-0275 migration pilot).ForbidEloquentMutationInControllersRule's own docblock already prescribed the fix: "lift this into acontrollerNamespacePrefixes."Fleet survey (2026-07-02): emmie is the only consumer with sub-namespaced controllers — kendo/ublgenie/entreezuil/codebook use canonical
App\Http\Controllers, already fully covered.Change
controllerNamespacePrefixes(default['App\Http\Controllers']), wired throughextension.neonon all three rules — mirrors theformRequestBaseClassprecedent. The namespace gate now matches when the class namespace starts with any configured prefix.phpstan.neon; everyone else is unaffected.Versioning — backward-compatible MINOR
Default
['App\Http\Controllers']reproduces the priorstr_starts_withbehaviour byte-for-byte (kendo'sApp\Http\Controllers\Central\*still flags under default) ⇒ zero new errors in any consumer at the default. New violations appear only when a consumer opts a divergent namespace in. Same shape as v0.6.0'sformRequestToDtoExemptClasses.Tests / gates
%controllerNamespacePrefixes%wiring (the guard against a NEON double-backslash no-op regression).Central\*still flag under default).composer test133/199 · phpstan level max clean · Pint clean · coverage 88.61% · Infection MSI 84.72% (zero escaped mutants in the new gate code) · audit clean.Downstream
emmie opt-in tracked as WR-0291 (enroll its two sub-namespaces + baseline-absorb the ~32 surfaced mutations, folding into the WR-0275 migration campaign).
EnforceCurrentUserAttributesurfaces 0 on emmie (already#[CurrentUser]);ForbidResourceWrappeda small surface.🤖 Generated with Claude Code