feat: add EnforceCurrentUserAttributeRule#26
Conversation
PR Reviewer · claimed
|
PR Reviewer · 10/10 · PASS
Findings
Actionmerge-ready |
jasperboerhof
left a comment
There was a problem hiding this comment.
Auto-approved by /review-open-prs — review verdict is PASS. See the verdict comment for the per-reviewer breakdown.
Goosterhof
left a comment
There was a problem hiding this comment.
Two blockers, two concerns, one nit, one praise.
The rule is well-constructed mechanically — clean CallLike registration, three detection shapes that mirror the established LogRule pattern, and a thorough fixture matrix that exercises every out-of-scope context (FormRequest, middleware, Action, job, top-level). The detection logic for the three call shapes is correct. The problem is not the detection — it is the scope gate, and it is load-bearing.
Blockers
1. The Controller-ancestry gate does not match the actual fleet — the rule is silent on most real controllers. insideControllerMethod() (src/Rules/EnforceCurrentUserAttributeRule.php:156-169) fires only when $classReflection->isSubclassOf('Illuminate\Routing\Controller'). But across the consumer territories, the overwhelming majority of controllers do not extend any base class at all — modern Laravel scaffolds controllers without a base Controller:
- kendo: 59 controllers, 0 extend a base. The PR positions kendo as "clean with 30 adopted sites" getting a "constraint bump only — regression protection." There is no regression protection: a future
$request->user()added to any of those 59 controllers fires nothing, because the ancestry gate returnsfalsefor all of them. - ublgenie: 12 controllers, 0 extend. Recon claims 6 violations — the rule catches 0.
- entreezuil: 7 controllers, 0 extend. Recon claims 3 violations.
- emmie is the only territory where the gate works: 88 of 105 controllers extend a local
abstract class Controller extends Illuminate\Routing\Controller(app/Http/Controllers/Controller.php:9,11). The other 17 escape.
So the rule's claimed cross-territory yield (50+ violations) is almost entirely unreachable as written. The fixtures pass because every controller fixture (RequestUserInController.php:10, AuthFacadeInController.php:10, etc.) was authored with extends Controller — they validate the gate against a controller shape three of the four territories don't actually use. The test suite is green against a strawman, and the kendo "regression protection" the PR explicitly promises does not exist.
2. The sibling rule already solved this, and this rule diverges from it. ForbidEloquentMutationInControllersRule scopes "controller" by namespace prefix (App\Http\Controllers, ForbidEloquentMutationInControllersRule.php:140-146) and its docblock explicitly documents why it avoids ancestry: $scope->getClassReflection() is unreliable, and namespace gating catches sub-namespaces like kendo's App\Http\Controllers\Central\* "naturally." Shipping a second controller-scoped rule in the same package with a contradictory, weaker definition of "controller" is an internal inconsistency that will confuse every consumer — one rule fires on a controller, the sibling doesn't, for the same class. Fix path: replace the ancestry gate with $scope->getNamespace() + str_starts_with($namespace, 'App\Http\Controllers'), copy the controllerNamespacePrefixes parameterization hook the sibling already carries. The FormRequest exclusion survives this unchanged — FormRequests live in App\Http\Requests, verified across all four territories, so a namespace gate keeps them silent without the ancestry crutch.
The two together: the rule as written will pass CI in every territory and protect almost nothing. Re-anchoring the gate to namespace is the correction, and it makes the existing fixture matrix more honest (drop the extends Controller from the controller fixtures so they reflect real scaffolding, and add a fixture for a sub-namespace controller).
Concerns
1. getName() === self::CONTROLLER_BASE_FQCN early-return (:164-166) is dead weight under the current gate and unmotivated. It exists to avoid flagging the base Illuminate\Routing\Controller itself, but that class lives in vendor/ and won't be analyzed, and isSubclassOf is already strict-ancestor in the relevant direction. No fixture exercises it. If the gate moves to namespace this branch disappears anyway; if it stays, justify it with a fixture or drop it.
2. The auth() AST-shape match is namespace-naive (receiverIsAuthHelper, :232-243). It matches any FuncCall named auth regardless of namespace resolution — a user-defined App\Support\auth() in a controller would false-positive. Low probability given the helper's ubiquity, but the sibling rules lean on $scope->resolveName() for exactly this discrimination on the facade branch (:206) while the helper branch skips it. Worth a one-line note in the docblock that this is a deliberate AST-only match accepting that collision, or a function_exists-style guard if you want to be strict.
Nits
The CHANGELOG entry is a single ~1900-character paragraph carrying versioning strategy, pre-cascade audit plan, and per-territory rollout. That's campaign-report and release-PR content, not a changelog line. Trim to the rule's behavior and identifier; the rollout plan already lives in the deployment order and PR body.
Praise
The TopLevelAuthCall.php fixture (:453-468) and its test (:699-712) are a genuine catch — exercising the null-getClassReflection() path to kill the FalseValue mutant on the no-class-reflection guard is the kind of mutation-driven coverage that prevents a real crash on top-level code, not a coverage-percentage chase. Named in the PR body with the specific mutant it kills. That's load-bearing.
Verdict: this is a COMMENT, not an approval. The detection engine is sound and the test discipline is real, but the scope gate is anchored to a controller shape three of the four consumer territories don't use — as written it would ship green and enforce almost nothing, including the kendo regression protection the PR explicitly promises. Re-anchor the gate to the namespace prefix the sibling rule already uses, re-author the controller fixtures to drop the artificial extends Controller, and the rule does what it claims. I'd want to re-review the gate change and the fixture re-authoring before merge.
Flags authenticated-user resolution via Request::user() / Auth::user() /
auth()->user() inside controller methods. The canonical fix is Laravel's
#[\Illuminate\Container\Attributes\CurrentUser] container attribute on the
method parameter — eliminates the implicit-nullable-then-assert dance.
Detection branches on three call shapes via CallLike registration:
- MethodCall on Illuminate\Http\Request subtype receiver (type-based via
ObjectType::isSuperTypeOf())
- MethodCall whose receiver is a FuncCall('auth') (AST-shape match — the
helper's return type is unloaded in stub-only environments)
- StaticCall resolving to Illuminate\Support\Facades\Auth (FQCN comparison
via \$scope->resolveName())
Scoped to Controller descendants via ClassReflection::isSubclassOf(
Illuminate\Routing\Controller). FormRequest descendants, middleware, services,
Actions, jobs, and console commands are silent by design.
Identifier: enforceCurrentUserAttribute.useAttributeInsteadOfRequestUser
Doctrine: war-room §Architectural Principles — Explicit over implicit
Origin: war-room cross-territory recon 2026-05-22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 10 fixtures and 10 test methods covering every detection branch and
every legitimate-skip case:
Fire (4):
- RequestUserInController — shape 1 (Request subtype receiver)
- AuthFacadeInController — shape 3 (Auth::user() static call)
- AuthHelperInController — shape 2 (auth() helper receiver)
- RequestUserInControllerWithAssert — exact PR #263 shape; fires once
on the \$request->user() call (assert is downstream noise)
Silent (5):
- UsesCurrentUserAttribute — already-canonical #[CurrentUser] adoption
- RequestUserInFormRequest — FormRequest descendant, by-design exclusion
- AuthInMiddleware — not a Controller descendant
- AuthInAction — not a Controller descendant
- AuthInJob — not a Controller descendant
Mutation-killing:
- TopLevelAuthCall — top-level call outside any class (kills FalseValue
mutant on the null-class-reflection branch)
The _stubs.php file provides bracketed-namespace stubs for
Illuminate\Routing\Controller, Illuminate\Http\Request,
Illuminate\Foundation\Http\FormRequest, App\Models\User, and the global
auth() helper — illuminate/routing / illuminate/http are not vendor-installed
on this package, mirroring the AuditSnapshotOnRetry _stubs.php pattern.
60 tests pass, 101 assertions. Coverage 86.00% (≥83 gate); MSI 82.47%
(≥75 gate); Covered MSI 82.47% (≥75 gate). Two LogicalOr mutants on the
Identifier-name guard clauses escape — equivalent-mutant precedent already
accepted on LogRule lines 84/99.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ributeRule
- CHANGELOG.md [Unreleased] § Added — new entry capturing detection shape,
scope gate, doctrine source, identifier, origin recon yield, versioning
posture (candidate Major bump per ADR-0021 §Versioning), pre-cascade
audit demand, multi-guard ergonomics note, and v1 out-of-scope items
(Auth::guard('name')->user()).
- README.md ## Rules table — new row in the canonical seven-rule table.
- CLAUDE.md ## Rules shipped table — new row matching the rules-shipped
format (rule | doctrine | identifier).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0fc4ce3 to
c2dff7f
Compare
|
Restacked for v0.4.0 — re-review requested. Rebased onto the restacked #28; base retargeted to |
Summary
EnforceCurrentUserAttributeRuleflaggingRequest::user()/Auth::user()/auth()->user()calls inside classes extendingIlluminate\Routing\Controller. The canonical fix is Laravel's#[\Illuminate\Container\Attributes\CurrentUser]container attribute on the method parameter — eliminates the implicit-nullable-then-assert dance ($user = $request->user(); assert($user instanceof User);) introduced by emmie PR #263 and recurring across the war-room fleet.$this->user()is canonical because container-attribute injection does not apply toFormRequest::rules()/toDto()/authorize()invocations), middleware, services, Actions, jobs, and console commands are silent by design.CallLikeregistration (mirrorsLogRulev0.3.0 expansion shape): type-based receiver match onIlluminate\Http\Requestsubtype (ObjectType::isSuperTypeOf()), AST-shape match onFuncCall('auth')receiver, FQCN match via$scope->resolveName()onAuth::class. Containing-class gate viaClassReflection::isSubclassOf(Controller::class).Origin
War-room cross-territory recon (2026-05-22) — ~50+ violations across codebook (~40+), ublgenie (6), entreezuil (3), emmie (2); kendo already clean with 30 adopted sites. Doctrine source: war-room §Architectural Principles — Explicit over implicit. Identifier:
enforceCurrentUserAttribute.useAttributeInsteadOfRequestUser.Versioning
Per ADR-0021 §Versioning, this is a candidate Major bump (new errors in already-clean code wherever consumer territories carry un-migrated controllers). The release PR will decide whether this collapses into the existing v0.3.0
[Unreleased]block (already a Major, currently gated on PR #25 recovery) or cuts as v0.4.0 after v0.3.0 ships. CHANGELOG entry added to[Unreleased]per package convention.Pre-cascade audit verified at filing time across the 5 Laravel territories (recon counts above). Post-release adoption plan:
Out of scope v1
Auth::guard('name')->user()and other guard-specific resolution paths — rare, substitution is more nuanced (#[CurrentUser(guard: 'name')]), do not appear in the recon yield. Track as follow-up issue if a future recon surfaces them.#[CurrentUser] Client $clientresolves via client guard,#[CurrentUser] User $uservia user guard — verified in emmie'sClientController::me. Laravel dispatches by typed parameter.Mutation-test notes
TopLevelAuthCall.php) to kill theFalseValuemutant oninsideControllerMethod()'s null-class-reflection branch.LogicalOrmutants remain alive on the early-return guards (lines 118 and 140) — equivalent-mutant precedent already accepted onLogRulelines 84/99 (same defensive-guard shape, same family).Test plan
composer test— 60 tests pass, 101 assertions (10 new test methods on the new rule)composer phpstan— level max self-analysis clean (9 services discovered)composer format:check— Pint cleancomposer test:coverage+composer coverage:check— 86.00% line coverage (381/443) ≥ 83% gatecomposer mutation:ci— MSI 82.47%, Covered MSI 82.47% (both ≥ 75% gate)Doctrine artifacts
orders/phpstan-warroom-rules/enforce-current-user-attribute-rule-engineer-deployment.mdreports/phpstan-warroom-rules/execution/2026-05-22-engineer-enforce-current-user-attribute-rule.md🤖 Generated with Claude Code