ekino/phpstan-banned-code
PHPStan extension that detects banned code in your project, like var_dump, exit, eval, print, shell execution, and other unwanted functions. Configure the exact nodes and functions to block, enforce rules in CI, and keep your codebase clean.
Architecture fit
require-dev, and ships zero code into the runtime artifact. Blast radius is therefore limited to CI and developer machines.phpstan.neon config. Adoption cost is one Composer line plus a NEON block, not a new tool in the pipeline.Expr_FuncCall, Stmt_Echo, Expr_Eval, Expr_Exit, Expr_Print, Expr_ShellExec), with a function denylist per node. That is a clean, declarative model —non-authors can review the banned list, and policy lives in version control rather than in a wiki.use_from_tests (flags use Tests\... imports in non-test files —a cheap architectural boundary that maps directly onto Laravel's test-namespace conventions) and non_ignorable: true by default, which prevents violations from being absorbed into a PHPStan baseline. The second is what turns a lint suggestion into an enforceable control.Integration feasibility
composer require --dev ekino/phpstan-banned-code; with phpstan/extension-installer present it auto-registers, otherwise add extension.neon to includes. Configuration goes under parameters.banned_code.nikic/php-parser majors exists because configuration references AST node type strings. A parser major bump can rename or restructure nodes, and the failure mode is silent non-detection rather than a red build. That is the main technical risk to plan for.Technical risk
echo/print in Blade-adjacent or legacy templates, exec in deployment console commands); a flat denylist has no per-directory allowlist, only multiple NEON configs.call_user_func('shell_exec', ...), variable functions, or reflection. It is one control among several and must not be presented as injection prevention.Key questions
exit/die, or shell functions reaching shared branches? If not, we are buying a control without a problem statement.non_ignorable default blocks baseline bypass; confirm that is the intended policy before rollout.Stack fit Assume Laravel 10/11/12 on PHP 8.2+, PHPStan 2.x and Larastan already installed for at least a subset of repos. The extension loads into the same analysis pass —no new CI stage, no new container, negligible added runtime. Where Larastan is present, resolve version constraints together; do not let Composer negotiate them independently.
Migration path
var_dump, dd, dump, debug_backtrace, exit/die. Low debate, high signal. Tier 2 (next release): eval, shell_exec, exec, passthru, system, proc_open, phpinfo, backtick shell execution. Tier 3 (evaluate per repo): echo, print, print_r —noisy wherever templates or legacy output paths exist.non_ignorable: true. Fix or explicitly grandfather existing hits via a scoped config include before flipping the gate, so the first day of enforcement is not a mass suppression event.Compatibility
phpstan/extension-installer (or explicit include) on every target repo; pin the extension version alongside PHPStan in composer.json.var_dump in a fixture and asserts a failure, so a silent parser/node-type drift is caught by CI rather than by an incident.Sequencing
Dependency add → config + dry-run metrics → Tier 1 blocking → shared config rollout → Tier 2 → optional Tier 3 and use_from_tests. Tie the PHPStan-major-upgrade workstream to this package's version so both move in one PR.
Maintenance Low, ongoing: the banned list is stable and changes rarely; the config diff is reviewable by non-authors. Concentration risk is a single small maintainer plus a 1:1 PHPStan-major dependency. Budget roughly a half-day per PHPStan major upgrade, and keep a fallback: reimplementing the rule in-house is a small, well-understood task if the project stalls.
Support
No vendor, no SLA. Documentation is a README with a single advanced-usage example; node-type discovery ("what string do I put in type?") is the one support question developers will ask. Mitigate with an internal one-pager listing our approved node types and banned functions. The upgrade escape hatch is strong because the failure surface is contained to CI.
Scaling Config scales linearly and cheaply —it rides the existing PHPStan run, so incremental cost is a rounding error even across many repos. Policy complexity does not scale as well: the model is a flat denylist. Per-directory allowlists can be approximated with multiple NEON configs; severity levels, auto-fix, and architectural rules (dependency direction, forbidden class usage) are out of scope and require custom PHPStan rules.
Failure modes
nikic/php-parser or PHPStan major upgrade —the rule stops firing and nobody notices. Highest-severity failure; mitigated by the fixture smoke test.non_ignorable: true means a legitimate exec in a deployment command becomes a hard blocker. Without a directory-scoped config prepared in advance, the team will demand the feature be turned off wholesale.non_ignorable: false "temporarily," the control quietly stops being a policy. Treat that flag as governed config.disable_functions, container hardening, and SAST/DAST but does not replace them. State this in the rollout comms.Ramp-up An engineer already fluent in PHPStan needs 1–2 hours to wire the config and read the violation report; the only unfamiliar concept is AST node types in the NEON file. Teams new to PHPStan should sequence this after PHPStan itself is established, otherwise onboarding conflates two changes. Plan a short internal note covering our node-type choices, the tiered denylist, and the exemption process —that note, not the upstream README, will be what most of the org actually uses.
How can I help you explore Laravel packages today?