spaze/phpstan-disallowed-calls
PHPStan rules to detect disallowed functions, methods, constants, namespaces, attributes, variables, and properties without running code. Catch leftover debug calls like var_dump() and enforce custom logging or API usage in production.
What this actually is. spaze/phpstan-disallowed-calls is not a Laravel package — it ships no service provider, no config publish, no runtime code. It is a PHPStan rule extension: a set of static-analysis rules plus a NEON configuration schema that flags disallowed functions, methods, constants, namespaces, attributes, keywords/control structures, superglobals, and (since 4.9.0) properties and enum properties. It belongs in the require-dev block and the CI pipeline, not in composer.json's runtime dependencies. That distinction drives everything below: the "integration" is a policy-as-code exercise, not an application change.
Architecture fit. Strong fit for any Laravel codebase already doing static analysis. Laravel shops typically run Larastan (PHPStan + Laravel-aware extensions), so this ruleset composes cleanly on top of an existing phpstan.neon. It has zero coupling to application code — no proxies, no bootstrapping, no runtime overhead — so it is trivially removable and cannot cause a production incident. The extension is intentionally not a security boundary; the README states it is "another pair of eyes" against your own mistakes and explicitly not a defense against hostile developers. A TPM should position it as a code-hygiene and standards-enforcement gate, never as a security control.
Integration feasibility. Low technical effort, moderate policy effort. Install is a one-line Composer dev requirement; phpstan/extension-installer auto-registers, or a two-line includes: block does it manually. The genuinely valuable part for a TPM is the allow-list model, which is unusually mature for a package this size: re-allow by path, by enclosing method/function, by parameter value or PHPDoc typeString, by flag, by instanceof / interface implementation, by class or method attributes, and by type-hint position. That means exceptions can be expressed at the right granularity instead of forcing a coarse global ignore, which is the usual reason these gates rot. The disallowExceptIn* family inverts the default (report when matched, allow otherwise) and short-circuits later directives — a subtlety worth documenting internally before teams write rules. Bundled configs exist for dangerous/loose calls, and a generator converts PHP's disable_functions / disable_classes into a snippet, which is genuinely useful for keeping staging/prod parity checks in sync.
Technical risk.
$fn(), call_user_func, variable static calls, container-resolved calls — is largely outside what a PHPStan rule can see. In a Laravel codebase with heavy container and facade indirection, this is a real coverage gap. In 4.9.2, unknown functions without available reflection are deliberately not reported, which is the right trade-off (fewer false positives) but further widens the gap between "configured" and "enforced."--prefer-lowest accidentally pulling PHPStan 2.x instead of 1.x, array_values() depending on a bleeding-edge 1.10.59 rule). This is a fast-moving host platform; every PHPStan major or minor bump is a potential support ticket. Pin both.Key questions to resolve before committing.
dd, dump, var_dump, env() outside config/, DB::raw, Model::unguard) versus taste (better left to Pint/PHP_CodeSniffer)?app(), resolve(), Artisan::call) that the package cannot detect statically — and if so, do we accept that gap explicitly?allowIn* directive?Stack fit. Laravel 10/11/12 + PHP 8.2–8.5 + PHPStan 2.x (with Larastan) + Composer + GitHub Actions is the well-trodden path; all of it is exercised by the package's own CI matrix, including PHP 8.5 e2e runs. If the team uses PHP_CodeSniffer/Pint for style, keep the boundary clean: style rules stay in Pint, usage prohibitions live here. There is no official Laravel ruleset (Nette and Symfony have one each), so we author and own our own phpstan-disallowed-calls.neon — treat that file as a first-class reviewed artifact, versioned in the repo and code-owned by the platform team.
Migration path. Land it in observe-only mode first. Add the dev dependency, include extension.neon (or rely on extension-installer), include the bundled disallowed-dangerous-calls / disallowed-loose-calls configs, and run analysis in CI with continue-on-error: true for one sprint. Collect the violation set, size it, and decide per rule whether to fix, baseline, or drop. Then flip the gate.
Compatibility. Requirements to pin explicitly: the PHPStan major/minor, the extension's minor (its release notes routinely name the PHPStan version they were tested against), and PHP minor in CI. Note the v3.0.0 BC surface — structured parameters, removal of the auto-appended "because reasons" message, rule ordering for duplicate targets, and default error identifiers (introduced in 3.4.0, which matters if you ignore by identifier rather than by message). Error identifiers are the right ignore currency; ignoring by message text will break the moment a message key changes.
Sequencing.
| Phase | Action | Exit criterion |
|---|---|---|
| 0 | Inventory current PHPStan/Larastan setup, level, existing ignores | Baseline documented |
| 1 | Install as dev dep, include bundled configs, run non-blocking | Violation count known per rule |
| 2 | Triage: fix / baseline / reject rule; agree org policy list | Signed-off rule catalog |
| 3 | Author custom disallowed*.neon with org rules and errorTip guidance (arrays supported since 4.8.0) |
Rules reviewed and merged |
| 4 | Enable as blocking gate; baseline frozen for legacy, strict for new code | CI green on main |
| 5 | Add disable_functions generator output per environment |
Dev/prod parity check in CI |
| 6 | Quarterly rule review; re-evaluate on each PHPStan major | Drift controlled |
Sequence phase 5 after 4 — matching disable_functions is a nice-to-have that only pays off once the gate is trusted.
Maintenance. Low-to-moderate, dev-only. No runtime surface, no migrations, no deploy risk. The real maintenance load is the configuration, not the code: every new allow-exception is a reviewed diff, and every PHPStan bump is a compatibility check. Budget roughly a half-day per PHPStan minor upgrade and one planning day per major. Pin the extension to a caret range and let Renovate/Dependabot open PRs so the changelog's PHPStan-compatibility notes get read by a human.
Support. MIT-licensed, GitHub-issues support model, strong README plus a docs/ tree covering bundled configs, custom rules, and each allow directive individually — documentation quality is the package's standout asset and materially reduces onboarding cost. No commercial SLA; if the org needs one, the fallback is vendoring a pinned fork or switching to ekino/phpstan-banned-code, both of which are cheap because nothing at runtime depends on it.
Scaling. Analysis cost scales with codebase size and rule count, but PHPStan's result cache absorbs most of it; the incremental cost of a few dozen disallowed-call rules is negligible next to the Larastan baseline. The scaling problem is organizational, not computational: in a monorepo or multi-repo estate, N repos means N copies of the NEON config drifting apart. Solve this early with a shared internal package or a reusable CI workflow that injects the canonical ruleset. Rule count also has a human ceiling — beyond roughly 40–60 prohibitions, developers stop reading the failures and start blanket-ignoring.
Failure modes.
| Mode | Symptom | Mitigation |
|---|---|---|
| False positive blocks a merge | Team frustration, pressure to disable the gate | Rich allow directives (allowIn*, params, attributes) + errorTip telling devs the sanctioned alternative |
| Silent miss on dynamic calls | Confidence exceeds actual coverage | Document the limitation in the ADR; pair with code review and, where it matters, runtime checks |
| Config typo silently ineffective | Rule believed active but isn't (pre-v3) or hard error (post-v3) | Structured schema now validates; add a negative test that asserts a known-bad fixture fails |
| PHPStan upgrade breaks rules | CI red after a dependency bump | Pin both, upgrade deliberately, run the package's own e2e as a smoke test |
| Baseline rot | Legacy ignore list grows unbounded | Ratchet: no new entries; quarterly review with an owner |
| Message-based ignores break | Ignores stop matching after a wording change | Ignore by error identifier only (default identifiers available since 3.4.0) |
| Wrongly cited as a security control | Compliance gap | Explicit non-goal in the ADR; security review stays separate |
Ramp-up. Roughly half a day for the platform engineer who owns the PHPStan config and authors the org ruleset; about fifteen minutes for an application developer, who only needs to learn three things: what the failure means, where the errorTip points, and how to request a scoped exception. The allow-list taxonomy is the only genuinely non-obvious surface — allowExceptIn* semantics and the "first matching allow directive wins" rule should be written into the internal runbook, since a misread here produces rules that quietly allow far more than intended. Net assessment: high leverage per unit of effort, best adopted as a scoped, well-documented standards gate on top of an existing PHPStan investment rather than as a standalone initiative.
How can I help you explore Laravel packages today?