Skip to content

Releases: script-development/phpstan-warroom-rules

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 01 Jul 13:14
1e37859

Release-as-a-whole: backward-compatible MINOR — one new optional PHPStan parameter (formRequestToDtoExemptClasses, default []) on EnforceFormRequestToDtoRule. Default-empty ⇒ zero new errors in existing consumers; a consumer adopts on its own ^0.5 → ^0.6 pin bump. Seed: war-room enforcement queue #131 — a class-keyed exemption surface so a territory can retire a duplicate local FormRequestsTest arch test and consolidate its exempt list into package config.

Added

  • EnforceFormRequestToDtoRule — new optional formRequestToDtoExemptClasses PHPStan parameter (default []): a list of fully-qualified class names to exempt from the toDto() contract, matched by exact FQCN. This is the class-keyed alternative to the existing per-file phpstan.neon ignoreErrors suppression path (identifier + path keyed) — predictable across file moves and, crucially, it ports a retiring local FormRequest→DTO Pest arch test's FQCN exemption list into package config 1:1. Wired through extension.neon (formRequestToDtoExemptClasses: [] parameter + listOf(string()) schema + exemptClasses: %formRequestToDtoExemptClasses% on the rule's service registration), mirroring the formRequestBaseClass precedent. No class name is ever hardcoded in the rule body — a consumer-supplied FQCN list is config, not a rule-body literal, so the package's "never by name inside the rule" convention is preserved. Seed: war-room enforcement queue #131 — EnforceFormRequestToDtoRule now duplicates territory-local FormRequestsTest arch tests (entreezuil first live case, PR #274), enforcing the same invariant twice with two exemption ledgers to keep in sync; the class-keyed param lets a territory retire its local arch test and consolidate its exempt list into package config (that retirement is a separate follow-up, not this change). README documents the param + a "Retiring a local FormRequest→DTO arch test" migration recipe. Versioning: backward-compatible MINOR (new optional parameter, default empty ⇒ zero new errors in existing consumers — NOT a candidate-major; the default-empty list means no consumer sees behaviour change until it opts in). Released as v0.6.0.

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 26 Jun 10:38
2b793a6

Release-as-a-whole: candidate MAJOR (pre-1.0 minor bump — ^0.4 → ^0.5) — ships two new rules (ForbidHttpExceptionInActionsRule, ForbidResourceWrappedInJsonResponseRule) from the Commander's review of emmie PR #481 (war-room enforcement queue #123 + #124). Both surface new errors in already-clean code wherever a consumer violates, so each consumer adopts on its own ^0.4 → ^0.5 bump PR (the ^0.{minor} caret means ^0.4 excludes 0.5.0 — tagging auto-adopts nobody). Blast radius (surveyed 2026-06-25 origin/development): ZERO violators on emmie + kendo for both rules (the #481 offender is on its branch, not merged) — the per-territory bump is expected clean save for un-merged branch work.

Added

  • ForbidHttpExceptionInActionsRule — flags throw statements inside App\Actions\* classes whose thrown expression's type is a subtype of Symfony\Component\HttpKernel\Exception\HttpExceptionInterface (the HttpException family — HttpException itself plus every subclass: NotFoundHttpException, AccessDeniedHttpException, UnprocessableEntityHttpException, …). HTTP status concerns belong to the HTTP layer; an Action that throws a 422 has reached past its boundary into transport. A uniqueness rule belongs in the FormRequest; a domain failure throws a custom domain exception the renderer maps to a status. Identifier: forbidHttpExceptionInActions.httpExceptionInAction. Doctrine: war-room §Architectural Principles — Explicit over implicit (#1) + Form Request → DTO → Action pipeline (#3). Type-aware sibling of ForbidAbortHelperRule (which bans the abort() helper family whose own message recommends throw new HttpException — correct for controllers, wrong for Actions): this rule closes the matching gap on the direct throw new HttpException(...) form inside Actions, catching subclass throws, fully-qualified throws with no use import, and typed-value throws that an import-checking arch test would miss. Illuminate\Validation\ValidationException is explicitly OUT of scope — Actions legitimately throw new ValidationException($validator) for stateful / cross-field validation that cannot live in a static FormRequest; it is not a member of the Symfony HttpException family, so the type gate never fires on it. Action-namespace gate mirrors ForbidDatabaseManagerInActionsRule (App\Actions prefix via $scope->getNamespace() + str_starts_with). Out of scope: non-App\Actions\* namespaces (controllers, FormRequests, exception renderers, middleware all legitimately raise HTTP-layer exceptions); the abort() helper family (covered by ForbidAbortHelperRule). Seed: Commander review of emmie PR #481 — CreateLocationEmailAction threw HttpException(422, …) for an "override already exists" uniqueness check (war-room enforcement queue #123). Blast radius (surveyed 2026-06-25 origin/development): ZERO raw-HttpException-in-Action on emmie + kendo (the #481 offender is on its branch, not merged) — the rule lands green and red-flags #481 at CI once enabled. Versioning: per ADR-0021 §Versioning, candidate Major bump (the rule surfaces new errors in already-clean code wherever a consumer territory has an Action throwing an HTTP-layer exception). Pre-cascade audit required per consumer at Phase-B bump (^0.4 → ^0.5); the emmie + kendo survey shows zero current violators, so the cascade is expected clean fleet-wide save for any un-merged branch work.
  • ForbidResourceWrappedInJsonResponseRule — flags response()->json($payload, …) and new JsonResponse($payload, …) inside App\Http\Controllers\* classes only when $payload's resolved type is a subtype of Illuminate\Http\Resources\Json\JsonResource. A JsonResource is already a Responsable — Laravel serializes it to a JSON response on its own; wrapping one in an explicit JSON response double-wraps the payload and discards the resource's own response shaping. Return the resource directly instead: return XxxResource::fromModel($model); (HTTP 200). Identifier: forbidResourceWrappedInJsonResponse.resourceWrapped. Doctrine: war-room §Architectural Principles — Explicit over implicit (#1) + ADR-0009 (Unified ResourceData Pattern — resources own their own response serialization). Type-awareness is mandatory: a blanket string-ban on response()->json(...) would false-positive on the overwhelmingly common legitimate sites that wrap a plain array / DTO / scalar / message envelope, and on response()->json(null, 204) (a 2026-06-25 fleet survey sized ~24 emmie + ~43 kendo response()->json/JsonResponse sites, almost all legitimate non-Resource payloads — a blanket ban would be ~67 false positives). Two AST shapes inspected: (1) MethodCall named json whose receiver is the response() helper FuncCall (AST-shape match — the helper's ResponseFactory return type is unloaded in stub-only analysis environments, mirroring how EnforceCurrentUserAttributeRule matches the auth() helper); (2) New_ of Illuminate\Http\JsonResponse (FQCN via $scope->resolveName()). Named-envelope edge (decided: EXCLUDE): a resource (or resource collection) nested under a named array key — e.g. emmie RegistrationBroadcastController:28's ['registrations' => …Resource::collect(...)] — is a deliberate response envelope, not a bare double-wrap; the first argument is an Array_ whose type is array<…>, not a JsonResource subtype, so the type gate naturally lets it through. The response()->json($resource, 201) status-override form still fires (the wrap is the violation, not the status; resource-with-non-200 is an unrelated future investigation). Controller-namespace gate mirrors ForbidEloquentMutationInControllersRule. Out of scope: non-App\Http\Controllers\* namespaces; non-JsonResource payloads; resources nested in any envelope. Seed: Commander review of emmie PR #481 — EmailController::store returned response()->json(EmailResource::fromModel($email), 201) while sibling update() correctly returned the Resource directly (war-room enforcement queue #124). Versioning: per ADR-0021 §Versioning, candidate Major bump (the rule surfaces new errors in already-clean code wherever a controller wraps a resource in an explicit JSON response). Pre-cascade audit required per consumer at Phase-B bump (^0.4 → ^0.5); the 2026-06-25 survey found ~0 current violators on emmie + kendo development (the #481 offender is on its branch).

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 25 Jun 13:03
d6a3fb6

Release-as-a-whole: MAJOR — ships four new rules (EnforceCurrentUserAttributeRule, ForbidEloquentMutationInControllersRule, EnforceAuditTransactionScopeRule, EnforceFormRequestToDtoRule) plus two folded no-op fixes (the NEON double-backslash defect that silenced EnforceFormRequestToDto + EnforceResourceDataValidatorOptIn for default-configured consumers — so EnforceResourceDataValidatorOptInRule, shipped in v0.3.0, has been a no-op on ^0.3, and its real surface also appears on bump). Unlike v0.3.0 (audited clean fleet-wide before tag), v0.4.0 is tagged known-dirty. The per-rule "pre-cascade audit" notes below move to per-territory Phase-B bump time, not before-tag: the ^0.{minor} caret means ^0.3 excludes 0.4.0, so tagging auto-adopts nobody — each consumer remediates and goes green on its own ^0.3 → ^0.4 bump PR (each carrying baseline-absorb for the other rules plus any login-handler ignoreErrors; see README §EnforceCurrentUserAttributeRule — false positives). Current-user remediation is already in review on its four territories: ublgenie #341, entreezuil #226, emmie #413, codebook #380. Phase B follows as per-territory war-room dispatches at our own pace.

Added

  • Tests: direct type-inference coverage for ConnectionTransactionReturnTypeExtension via a PHPStan\Testing\TypeInferenceTestCase (tests/Type/ConnectionTransactionReturnTypeExtensionTest.php + fixture tests/Fixtures/ConnectionTransactionReturnType/transaction-return-type.php). The extension previously had no direct test — it was only exercised implicitly by audit-snapshot rule fixtures, none of which asserted the resolved return type. The new fixture loads extension.neon (same config consumers register) and assertType()s the inferred type of $connection->transaction(...) for closures returning a constant scalar, an object/DTO, a nullable, an array shape, and a widened (non-constant) scalar — pinning that the extension forwards the closure acceptor's return type rather than mixed. Test-only; no consumer-facing surface. Closes Quartermaster F-2. Versioning: none (internal test coverage).
  • EnforceCurrentUserAttributeRule — flags calls to Request::user() / Auth::user() / auth()->user() inside classes in the App\Http\Controllers namespace. 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 (EMMIE-0197) and recurring across the war-room fleet. Doctrine: war-room §Architectural Principles — Explicit over implicit. Identifier: enforceCurrentUserAttribute.useAttributeInsteadOfRequestUser. Detection branches on three call shapes via CallLike registration (mirrors LogRule v0.3.0 shape): 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 controllers via the App\Http\Controllers namespace prefix ($scope->getNamespace() + str_starts_with) — mirrors ForbidEloquentMutationInControllersRule and the canonical "controllers are identified by the App\Http\Controllers namespace" convention. FormRequest (App\Http\Requests, where $this->user() is canonical because container-attribute injection does not apply to FormRequest::rules() / toDto() / authorize() invocations), middleware (App\Http\Middleware), services, Actions (App\Actions), jobs, and console commands are silent because their namespaces do not start with the controller prefix. Origin: war-room cross-territory recon 2026-05-22 (50+ violations across codebook, ublgenie, entreezuil, emmie; kendo already clean with 30 adopted sites). Versioning: per ADR-0021 §Versioning, candidate Major bump (the rule surfaces new errors in already-clean code wherever a consumer territory has un-migrated controllers). The release PR will determine whether this collapses into the existing v0.3.0 [Unreleased] block (already a Major) or cuts as a separate Major (v0.4.0) after v0.3.0 ships. Pre-cascade audit required at per-territory Phase-B bump (v0.4.0 tags known-dirty — see the [0.4.0] release header) — consuming territories will need either Medic dispatches to migrate (ublgenie 6 sites, entreezuil 3 sites, emmie 2 sites) or PHPStan-baseline staging (codebook ~40+ sites — safer than mass-edit on lightly-staffed AVG/NEN-7510-downstream territory). Kendo gets a constraint bump only (zero violations). Multi-guard ergonomics (#[CurrentUser] Client $client resolves via client guard, #[CurrentUser] User $user via user guard — verified in emmie's ClientController::me) work as expected: Laravel dispatches by typed parameter. 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.
  • ForbidEloquentMutationInControllersRule — bans Eloquent persistence APIs (save, update, delete, create, destroy, forceDelete, forceFill, push, restore, touch, and their *OrFail / *Quietly / *OrCreate variants — 24-method blocklist) on Illuminate\Database\Eloquent\Model subclasses and Illuminate\Database\Eloquent\Builder chains when the call site is inside an App\Http\Controllers\* class (including sub-namespaces like kendo's App\Http\Controllers\Central\*, matched via str_starts_with). Reads (find, where, get, first, paginate, pluck, count, exists, query) are deliberately permitted — route-model binding, ResourceData hydration, and policy checks need controller-level Model access; the doctrine line is "Controllers may READ Models, but MUST NOT mutate them." Identifier: forbidEloquentMutationInControllers.eloquentMutationInController. Doctrine: ADR-0011 (Action Class Architecture) — Actions are the chokepoint for mutations — combined with ADR-0019 (Explicit Model Hydration) — Model::create() / fill() / forceFill() / update() banned application-wide; this rule enforces the controller surface where the violations have been historically common. Algorithm: namespace gate (App\Http\Controllers) → recursively walk every ClassMethod body collecting MethodCall + StaticCall nodes → for MethodCall, fire if ObjectType::isSuperTypeOf() against Model OR Builder matches the receiver type and the method name is in the blocklist; for StaticCall, fire if Scope::resolveName() resolves to a Model subclass FQCN and the method name is in the blocklist. Builder coverage is type-aware (User::query()->where(...)->update([...]) fires) — the generic parameter is not unwrapped because ObjectType matches Builder<User> as a subtype of the unparameterized Builder cleanly, no brittle generic introspection needed. Supersedes the consumer-side string-match Pest arch tests in kendo (backend/tests/Arch/ControllersTest.php controllers must not call Eloquent write methods directly), ublgenie + entreezuil (tests/Arch/ControllersTest.php of the same shape), and the bridge subset in ISMS (backend/tests/Architecture/ControllerCurrentUserTest.php from PR #10, 2026-05-28). The string-match shape catches ->save(, ->update([, ->delete(, ->forceDelete( but cannot discriminate Model::create() from Response::create(), Collection::push() from Model::push(), or ->update($vars) without an inline array literal — the type-aware AST inspection here closes those gaps. Cross-territory cascade post-merge: consumer Pest tests deleted; emmie + brick-inventory-orchestrator pick up coverage automatically on next composer update. Out of scope: non-App\Http\Controllers\* namespaces (Actions/Services/Jobs/Middleware are allowed to call persistence APIs), non-Eloquent receivers, dynamic method names ($model->{$var}() — value-flow analysis), variable class names in static calls ($class::create(...)). Closes war-room enforcement queue #87. Versioning: per ADR-0021 §Versioning, candidate Major bump (the rule surfaces new errors in already-clean code wherever a consumer territory has a controller calling Eloquent persistence APIs directly — the three territories currently running the string-match Pest test caught the bulk of these, but the type-aware shape will surface additional violations the string-match shape missed: Model::create(), Model::destroy(), chained Builder mutations, *Quietly variants, etc.). Pre-cascade audit required across ISMS, kendo, emmie, entreezuil, ublgenie, brick-inventory at per-territory Phase-B bump (v0.4.0 tags known-dirty — see the [0.4.0] release header) — three territories' Pest tests already closed the string-match-visible violators; the type-aware additional surface (Builder chains, Model::create(), *Quietly variants) may carry undetected violators.
  • EnforceAuditTransactionScopeRule — enforces ADR-0029 (Audit Row Durability Contract) §Decision rule 3. Flags non-transactional state mutations (StatefulGuard / Session / Cache / Bus / Queue / Mailer / Notification / Broadcaster / Filesystem and their Illuminate\Support\Facades\* counterparts, mutation methods only) inside transaction(...) closures in App\Actions\* classes. Identifier: enforceAuditTransactionScope.nonTransactionalMutationInClosure. Doctrine: ADR-0029 §Decision rule 3. Seed: ISMS-0003 PR #7 commit f1d357b (2026-05-28) — three Auth Actions (AuthenticateWorkerAction, VerifyTwoFactorChallengeAction, LogoutWorkerAction) mutated StatefulGuard + Session state inside the transaction closure before the audit row write; an audit-write failure would have rolled back the audit row while leavi...
Read more

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 29 May 13:58
e3572b9

Release-as-a-whole: MAJOR — collapses three rule-level contractual widenings into a single Major bump per ADR-0021 §Versioning. Each rule's pre-cascade audit returned 0 violators across all 5 consumer territories (kendo, entreezuil, emmie, ublgenie, brick-inventory-orchestrator), so the Major represents the contract change, not empirical violation count. Consumers upgrading from ^0.2 to ^0.3 accept the broader rule contracts whether or not their existing code trips them. Phase A pin sweep (^0.1.x^0.2) closed pre-release — all four laggard consumers (kendo, entreezuil, emmie, BIO) bumped between 2026-05-06 and 2026-05-08 via independent dispatches; verified by 4-territory Medic wave 2026-05-13 (all no-op, all composer phpstan clean against EnforceAuditSnapshotOnRetryRule). Phase B pin sweep (^0.2^0.3) follows post-tag as a separate war-room dispatch.

Added

  • EnforceResourceDataValidatorOptInRule — flags classes extending App\Http\Resources\ResourceData that declare a non-empty EAGER_LOAD_COUNT or EAGER_LOAD_SUM constant but do not call validateRelationsLoaded() anywhere in their method bodies. Without the call, missing eager-load aggregates fail open as 0 / null instead of throwing — silently re-introducing the silent-zero bug closed by kendo PR #1079 (KD-0494). Doctrine: ADR-0009 §EAGER_LOAD validator opt-in. Identifier: enforceResourceDataValidatorOptIn.missingValidatorCall. Promoted from kendo PR #1084's Pest arch test (Armorer, merged 2026-05-07 at db20ea9cf) — the third instance of the "arch test detects misuse but not omission" enforcement shape, dispositioned for Phase-2 promotion under war-room enforcement queue #55 by the Commander on 2026-05-07. Inheritance is matched via PHPStan reflection (FQCN ancestor traversal) — short-name collisions in unrelated namespaces do NOT match. The base FQCN is parameterizable via the resourceDataBaseClass PHPStan parameter (default: App\Http\Resources\ResourceData); territories whose ResourceData lives elsewhere can override per consumer phpstan.neon. Compliant call shapes: self::validateRelationsLoaded($model), static::validateRelationsLoaded($model), $this->validateRelationsLoaded($model) (instance form accepted for liberal compatibility with the source-of-truth Pest matcher, even though the base method is protected static). Empty-array constants (EAGER_LOAD_COUNT = []) do NOT fire — they are no-ops. Versioning: Minor-at-rule-level, collapses into the bundled v0.3.0 Major per ADR-0021 §Versioning. Cross-territory cascade audit (2026-05-08): 0 violators across emmie, kendo, entreezuil, ublgenie, brick-inventory-orchestrator — campaign report at campaigns/phpstan-warroom-rules/2026-05-08-pre-cascade-audit-resource-data-validator-opt-in.md. Side observations: emmie uses App\Http\Resources\DTOResource (non-default base, rule non-applicable absent resourceDataBaseClass override); entreezuil has not adopted the ResourceData pattern (still on JsonResource despite ADR-0009 in CLAUDE.md, latent adoption debt); BIO operates dual-base (ResourceData + ComputedResourceData<TSource> per BIO sovereign ADR-0010). Sister extractions for the FormRequest toDto() omission shape (queue #55 instance #2) and the routes ->can() middleware omission shape (queue #55 instance #1) are deferred to separate dispatches.

Security

  • Pinned all GitHub Actions references in ci.yml and release.yml to commit SHAs with # v<MAJOR> comments for Dependabot tag-tracking. Closes Sapper M1 Finding #3 (supply-chain forward-compatibility before potential Packagist OIDC migration). Versioning: none (CI workflow change, no consumer-facing surface).

Changed

  • Doctrine: corrected publish-channel framing in CLAUDE.md (L11 and the Release process section) and the release.yml header comment. Public packagist.org has no OIDC Trusted Publishing option today — OIDC is a Private Packagist–only feature (packagist/artifact-publish-github-action, GA February 2026). The package's actual publish channel is the standard https://packagist.org/api/github push-event webhook (dev-* aliases on branch push, versioned releases on tag push via release.yml). Migration to Private Packagist would change ally-side Composer consumption (private repo URL + token in composer.json) and is a commercial decision; tracking continues on Issue #11. Closes Sapper M1 Finding #2 (doctrine drift on publish channel) and resolves Issue #11 audit. Versioning: none (doctrine alignment, no consumer-visible behaviour).
  • Governance: added .github/CODEOWNERS routing all changes to @script-development/phpstan-warroom-admins. A separate rule-authors team is intentionally not split out today — the admins team and the rule-design reviewer set are identical at the current shop size; revisit if the contributor base grows or rule-design review becomes a distinct concern from operational repo administration. Pairs with branch-protection update enabling require_code_owner_reviews=true. Closes Sapper M1 Finding #5 (no CODEOWNERS file). Versioning: none (governance change, no consumer-visible surface).
  • LogRule (BREAKING): extended to cover the static-call shapes Model::destroy(...) and Model::forceDestroy(...) on Log-named classes. getNodeType() broadened from MethodCall::class to CallLike::class and processNode branches on MethodCall vs StaticCall. Both shapes emit the same logRule.logModification identifier so consumer phpstan.neon ignoreErrors entries cover the whole rule with one identifier (the previous rule's compliance teeth depended on delete/forceDelete instance shapes; on a non-soft-delete log model Model::destroy([1]) purges and Model::forceDestroy([1]) always purges — both slipped through). Versioning: Major-at-rule-level per ADR-0021 §Versioning; ships as v0.3.0. Cross-territory cascade audit (2026-05-13): 0 violators across kendo, entreezuil, emmie, ublgenie, brick-inventory-orchestrator — campaign report at campaigns/phpstan-warroom-rules/2026-05-13-pre-cascade-audit-log-rule-static-call.md. The static-call shape proved cleaner than v0.2.0's instance-call expansion (which surfaced 1 operational-log false positive in ublgenie); no consumer-side ignoreErrors migrations required. Resolves issue #4.
  • LogBuilderTruncateRule (BREAKING): new sibling rule to LogRule, sharing the logRule.logModification identifier so consumer phpstan.neon ignoreErrors entries cover the whole append-only doctrine with one entry. Flags Builder->truncate() calls where the fluent chain's most recent table() invocation targets a Log-named table (string-literal first argument containing 'log' / 'logs', case-insensitive substring match). Covers DB::table('logs')->truncate(), DB::connection('central')->table('logs')->truncate(), and $this->db->table('logs')->truncate() (instance-injected ConnectionInterface). Receiver detection is type-based (Illuminate\Database\Query\Builder OR Illuminate\Database\Eloquent\Builder subtype via ObjectType::isSuperTypeOf()) — mirrors EnforceAuditSnapshotOnRetryRule's ConnectionInterface pattern. The Eloquent\Builder receiver branch covers the rare-but-coherent $eloquentBuilder->table('logs')->truncate() shape; Eloquent chains that set the table via the Model's $table property (AuditLog::query()->truncate()) or via Eloquent's from() vocabulary (AuditLog::query()->from('logs')->truncate()) are an acceptable miss in the same family as variable table names — the table name does not appear as a table()-call string-literal in the chain. Doctrine: ADR-0001 §Append-only — truncate() is the bluntest delete and bypasses Eloquent events, observers, and audit triggers entirely. Out of scope: variable table names ($t = 'logs'; DB::table($t)->truncate()), Eloquent from('logs') chains, and Model-property-driven tables — all would need value-flow or model-graph inspection; acceptable misses, rely on reviewer + consumer-side phpstan.neon ignoreErrors. Versioning: Major-at-rule-level per ADR-0021 §Versioning; collapses into the bundled v0.3.0 Major alongside the LogRule static-call expansion. Cross-territory cascade audit (2026-05-13): 0 violators across kendo, entreezuil, emmie, ublgenie, brick-inventory-orchestrator — campaign report at campaigns/phpstan-warroom-rules/2026-05-13-pre-cascade-audit-log-builder-truncate.md. The truncate() shape proved genuinely uncommon across the fleet (~6 calls total across 2,500+ scanned PHP files; none against log-named tables); no consumer-side ignoreErrors migrations required. Resolves issue #8.
  • CI: added PHP 8.5 to the ci.yml and release.yml test matrices alongside 8.4 (['8.4']['8.4', '8.5']). PHP 8.5.0 was released 2025-11-20; the war-room dev environment already runs 8.5.5 locally, so PRs were getting ad-hoc 8.5 coverage during pre-push but no CI signal. Adding (rather than replacing) keeps 8.4 — the composer.json ^8.4 contractual minimum — covered. shivammathur/setup-php@v2 supports 8.5 since GA. Resolves issue #5.
  • CI: added line-coverage measurement and a threshold gate. ci.yml switches coverage: nonecoverage: pcov on both 8.4 and 8.5 matrix legs (PCOV is line-coverage-only and faster than Xdebug — debugger features aren't needed). New composer scripts: test:coverage (runs PHPUnit with --coverage-clover=build/logs/clover.xml --coverage-text) and coverage:check (runs bin/coverage-check.php, a standalone clover parser — no extra runtime dependency added to a static-analysis package for a single CI gate). Two new CI steps replace the Tests step: Tests with coverage and Coverage threshold gate. Clover XML is uploaded as a per-leg artifact (clover-php-${{ matrix.php }}, 14-day retention) so reviewers can inspect uncovered lines without spelunking through workflow logs. Initial threshold: 83% — the...
Read more

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 04 May 09:31

Added

  • EnforceAuditSnapshotOnRetryRule — flags App\Actions\* classes whose constructor injects an entity audit logger and whose $connection->transaction(...) calls do not begin with an in-memory state reset ($model->refresh(), fresh fetch via ->newQuery()->findOrFail(...) / ->fresh(), or fresh instantiation via new ... / ->newInstance()). Doctrine: ADR-0001 §Snapshot-on-Retry Safety. Identifier: enforceAuditSnapshotOnRetry.firstStatementMustResetState. Promoted from cross-territory Pest arch tests (emmie PR #187, entreezuil PR #139, ublgenie PR #166, kendo PR #1029). Receiver detection is type-based (Illuminate\Database\ConnectionInterface subtype) — replaces territory-specific property-name matching ($this->db vs $this->connection). Escape hatch: // @audit-snapshot-retry-safety: <rationale> marker preceding the transaction call.

Changed

  • PHP constraint: bumped composer.json php from ^8.3 to ^8.4. The package's Pint config (mb_str_functions: true) normalizes ltrim/trim calls to mb_ltrim/mb_trim, which are PHP 8.4+ functions. The new rule introduced the first mb_ltrim/mb_trim callsites; aligning the constraint with the formatter's actual output. All consuming territories already run PHP 8.4 — no real-world impact.
  • LogRule (BREAKING): extended FORBIDDEN_METHODS from ['delete', 'update'] to ['delete', 'forceDelete', 'forceDeleteQuietly', 'update']. On a SoftDeletes-bearing model ->delete() is a no-op against the underlying row and ->forceDelete() is the only call that actually purges; the rule's compliance teeth previously rested on the migration-time convention that audit-log models never adopt SoftDeletes. Static-call shapes (Model::destroy(), Model::forceDestroy(), DB::table('logs')->truncate()) remain out of scope — getNodeType() returns MethodCall::class, and static-call coverage is tracked as issue #4. Origin: issue #1, surfaced by ally review on Back-to-code/ublgenie-app#163. Pre-cascade audit across emmie, kendo, entreezuil, ublgenie surfaced one new violation: ublgenie/app/Actions/DeleteBranch.php:56 (InvoiceLog::query()->whereIn(...)->forceDelete()) — operational/processing log, not an audit log; migrates to consumer-side phpstan.neon ignoreErrors per package convention. Versioning: per ADR-0021 §Versioning, this is a Major bump (new errors in code that previously passed); within 0.x this ships as v0.2.0.

v0.1.1

Choose a tag to compare

@github-actions github-actions released this 29 Apr 11:25

Changed

  • Compatibility: widened illuminate/* constraints from ^11.0 || ^12.0 to ^11.0 || ^12.0 || ^13.0 across the five required packages (database, contracts, cache, filesystem, log, mail). Surfaced during ADR-0021 cascade onto entreezuil (Laravel 13). No behavioral change — the package's PHPStan rules reason about class names that are stable across Laravel 11/12/13. Forward-looking: removes the constraint as a future cascade blocker.

v0.1.0

Choose a tag to compare

@github-actions github-actions released this 29 Apr 08:27

Added

  • EnforceActionTransactionsRule — flags App\Actions\* classes whose execute() performs ≥2 writes without ->transaction(). Doctrine: ADR-0011.
  • ForbidDatabaseManagerInActionsRule — flags App\Actions\* constructors that inject Illuminate\Database\DatabaseManager. Doctrine: ADR-0021 §Why ConnectionInterface.
  • ForbidAbortHelperRule — flags abort(), abort_if(), abort_unless() function calls. Doctrine: war-room §Explicit over implicit.
  • LogRule — flags update() / delete() calls on classes whose name contains "Log" or "logs". Doctrine: ADR-0001 §Append-only.
  • ConnectionTransactionReturnTypeExtension — resolves $connection->transaction(fn () => $foo) to the closure's return type instead of mixed.

Notes

  • Rules ported from emmie's backend/app/PHPStan/. The territory-specific Terminology exception in LogRule was dropped — per-territory false positives are now suppressed via consumer phpstan.neon ignoreErrors.
  • Test coverage is smoke-level for v0.1.0; full matrix for EnforceActionTransactionsRule (non-DB property exclusions, nested closure transaction detection, full 18-method write list) lands in a follow-up.
  • Action namespace assumption: rules that scope to Actions match App\Actions\*. Lift to a parameter when a non-conforming territory onboards.