Skip to content

Releases: script-development/phpstan-warroom-rules

v0.3.0

29 May 13:58
e3572b9

Choose a tag to compare

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

04 May 09:31

Choose a tag to compare

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

29 Apr 11:25

Choose a tag to compare

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

29 Apr 08:27

Choose a tag to compare

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.