script-development/phpstan-warroom-rules
Custom PHPStan rules for Laravel projects that catch script-development conventions and common pitfalls. Drop-in config, clear error messages, and fast static analysis for CI pipelines.
Architecture fit. This is a PHPStan rule extension for Laravel codebases, distributed as a dev-only Composer package — no runtime footprint, no production dependency, no migration surface in the application itself. That makes it an unusually cheap thing to adopt from an architecture standpoint. More importantly, the design decisions in this release are the ones a TPM should care about: detection is shape-keyed (left-hand call resolving to a first-party namespace, a required mixed parameter, a nullable-scalar return) rather than maintained against a helper-name inventory. That removes the perennial "who updates the list when a helper is renamed" maintenance tax and makes the rule's behaviour predictable across territories. Configuration is a single narrowingHelperNamespacePrefixes list (default ['App']) matched on namespace boundaries, so per-territory namespace roots are tunable without forking. The stated ADR-0021 posture — false negatives acceptable, false positives are not — is the correct bias for a rule that gates merges, and the release is explicit about where it trades coverage for precision.
Integration feasibility. Low effort, medium coordination. Adoption is a single ^0.11 pin bump plus CI green, with the escape hatch being an inline @phpstan-ignore naming the rule identifier. There is no data migration, no API contract change, no deploy step. The real work is remediation of newly-surfaced findings, not integration.
Technical risk. The central risk is that this is a 0.x MINOR that is consumer-impacting: ^0.10 excludes it, so nobody is auto-adopted, but every territory that bumps will get new errors in previously clean code. That is desirable (real bugs were found: a sentinel-seeded array that unlocked a forceDelete(), a gender persisted as an empty SET, and a missing node parsed as now() written as a date of birth) but it means a pin bump is a remediation project, not a version bump. Secondary risks:
mixed key returning a nullable scalar is indistinguishable from a boundary reader by signature. The per_page default hits in lokalekeuze are flagged in the notes as likely this shape. Unmanaged, this converts the rule into ignore-noise.ForbidCredentialDerivedCacheKeyRule is silent on a consumer with no encrypted cast. A clean run there is not evidence the rule works — the notes explicitly require a planted credential-keyed cache as a positive control. That is an operational requirement, not a nice-to-have.?? null whose null is swallowed one call later by Carbon::parse) are explicitly out of scope. A green build must not be read as "data-integrity verified."TypeCombinator, per-member scalar probes, union-receiver branch resolution, parent::casts() override order). Correct, but it means PHPStan upgrades are a compatibility event for this package.ForbidCredentialDerivedCacheKeyRule / CacheKeyTaintCollector description, so that rule's full behaviour and its consumer audit demands are not yet assessable.Key questions.
App, or do any need a custom narrowingHelperNamespacePrefixes?Stack fit. The package sits in the existing PHPStan/CI lane of a Laravel stack. It requires a Composer dev dependency, a PHPStan config include, and — where non-default — the namespace-prefix parameter. Nothing else in the stack is touched.
Migration path. Per-territory, not fleet-wide. The pre-1.0 caret convention is the migration mechanism: each repository opens its own ^0.11 pin-bump PR, absorbs the new errors, remediates, and goes green on its own schedule. Remediation is a closed set of three patterns — skip the write, fail closed (throw), or handle null explicitly (?? null and pass it on) — plus the documented escape hatch for genuine lookup helpers. That is learnable in a single review cycle.
Compatibility. Two compatibility obligations to encode now: (a) ignore identifiers are part of consumer config, so any upstream rename of forbidSentinelFallbackOnNarrowingHelper.sentinelFallback or forbidCredentialDerivedCacheKey.keyFromEncryptedAttribute breaks baselines and must be treated as a breaking change; (b) because every 0.x minor is consumer-impacting by convention, each future bump carries the same audit cost — budget for recurrence, not a one-off.
Sequencing.
FormatLogbookEntriesAction, including a CarbonImmutable::parse(… ?? 'now') shape) and lokalekeuze's to be partly the known lookup false positive, split between real fixes and documented inline ignores with rationale.Maintenance. Modest but recurring. Each 0.x bump is a per-territory audit against a single documented identifier, plus config upkeep for namespace prefixes. The shape-keyed design means no helper inventory to maintain — the largest ongoing cost is ignore hygiene: every inline suppression needs an owner and a reason or it becomes permanent debt.
Support. Two support surfaces. Internally: engineers hitting a new error need a decision tree (is this a real sentinel? a lookup? → fix, or ignore with justification) and reviewers need to be able to tell a legitimate escape hatch from a silenced bug. Externally: the residual false positive is a known upstream limitation, so there should be a clear channel for reporting shapes that match by signature but aren't boundary readers.
Scaling. Cost is roughly linear in territories (one PR each) and constant in rule complexity, which is favourable. The scaling concern is CI time and signal quality: the per-node cost is deliberately cheap on non-matching expressions (one instanceof), but the rule set is growing, and every false positive that gets ignored rather than fixed erodes the rule's authority across all twelve-plus territories.
Failure modes.
encrypted cast, or on a codebase whose boundary helpers don't match the shape, reads as "we're safe." Mitigation: positive controls in CI, and explicit communication that the out-of-scope cases (sink-swallowed null, laundered variables, non-literal fallbacks) are not covered by a green build.Ramp-up. Low for individuals (three remediation patterns, one config knob, one ignore syntax), moderate for the organisation — the habit to build is "a new static-analysis error is a finding to triage, not a blocker to suppress," and the discipline to build is positive-control verification so clean results mean something.
How can I help you explore Laravel packages today?