Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phpstan Banned Code Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (one step, dev-only):

composer require --dev ekino/phpstan-banned-code

That's it if you already have phpstan/extension-installer in your project (most Laravel projects do once PHPStan is wired up). If not, add the include manually in phpstan.neon:

includes:
    - vendor/ekino/phpstan-banned-code/extension.neon

Where to look first: phpstan.neon (or phpstan.neon.dist). That single file is the whole configuration surface β€” the extension ships with a sensible default denylist, so the fastest first step is to run PHPStan without touching config and see what it already flags. Then open vendor/ekino/phpstan-banned-code/extension.neon to see the defaults you inherited. The README's "Advanced usage" block is the authoritative config reference; there is nothing else to learn.

First use case (the one you actually ship on day one): wire PHPStan into CI, run it on a branch, and confirm the baseline behavior. In a typical Laravel app you'll get hits on dd(), dump(), and var_dump() immediately β€” that's the package working. Add a script to composer.json:

"scripts": {
    "analyse": "phpstan analyse --memory-limit=1G"
}

Then composer analyse locally and in CI. Ship the ban on debug helpers first; defer the aggressive bans (eval, exit, shell execution) until you've surveyed the existing hits β€” see Gotchas.

Minimal config to start (explicit beats implicit):

parameters:
    banned_code:
        nodes:
            - type: Expr_FuncCall
              functions:
                  - dd
                  - dump
                  - var_dump
                  - print_r
                  - debug_backtrace
        use_from_tests: true

Declaring nodes explicitly makes the policy reviewable in a PR rather than buried in a vendor default you'll forget. Copy the full list from the README when you're ready to expand.


Implementation Patterns

Pattern 1 β€” Tiered rollout via multiple PHPStan configs. PHPStan lets you include multiple config files, so encode policy in layers rather than one monolith:

phpstan.neon                 # includes the two below
phpstan-banned-base.neon     # debug helpers β€” enforced everywhere
phpstan-banned-strict.neon   # eval/exit/shell β€” enforced on app/, not on console/deploy

Because the extension reads parameters.banned_code per-config, you can run phpstan analyse app/ with the strict config and phpstan analyse database/ tests/ with the loose one. This is how you express "legit exec lives in deployment commands but not in controllers."

Pattern 2 β€” Pair it with baseline discipline. This is the package's headline feature and the workflow that makes it stick: non_ignorable: true (the default) means a var_dump cannot be silently absorbed into phpstan-baseline.neon. That's exactly what you want for the ban list. For legacy code where you need grandfathering during a burn-down, flip it deliberately:

parameters:
    banned_code:
        non_ignorable: false

The pattern is: keep defaults non-ignorable for var_dump/dd, and relax only when you're migrating a legacy tree and planning to remove the baseline entries. Never leave non_ignorable: false indefinitely β€” it defeats the control.

Pattern 3 β€” Use it as a migration worklist. During a Laravel upgrade (e.g., 10 β†’ 11, or PHP 8.2 β†’ 8.4), configure the deprecated/removed constructs for that release as banned functions on a branch. Run PHPStan against the app: the output is a prioritized worklist of every call site, generated automatically. Ship the "no new deprecated usage" gate first, burn down existing hits, then promote it to default branch. This converts a scary upgrade into an incremental, measured one.

Pattern 4 β€” Treat use_from_tests as an architectural boundary. Enable it and you get a free gate against use Tests\... imports leaking into production code β€” a class of bug that ships test doubles into prod. Low cost, high signal if your codebase ever mixed namespaces.

Pattern 5 β€” Extend the denylist for Laravel-specific footguns. The default covers debug helpers. Add what your team actually regrets seeing in diffs:

parameters:
    banned_code:
        nodes:
            - type: Expr_FuncCall
              functions:
                  - dd
                  - dump
                  - var_dump
                  - ray            # spatie/ray
                  - print_r
                  - debug_backtrace
                  - env            # force config() β€” env() is null after config:cache
            - type: Expr_Exit
              functions: null      # bans die/exit, including in HTTP controllers

env() outside config/ is a classic Laravel landmine that this rule catches cleanly, and it's a good example of the extension earning its keep beyond debug statements.

Pattern 6 β€” Wire into pre-commit selectively. Don't run full PHPStan on every commit (it's slow). Run it on changed files:

git diff --cached --name-only --diff-filter=ACM -- '*.php' \
  | xargs -r phpstan analyse --memory-limit=1G --

This gives developers fast feedback on their own changes while keeping the full analysis for CI.

Integration tip β€” extension-installer order. If extension.neon isn't being picked up, verify phpstan/extension-installer is installed and that you haven't disabled it in the project's composer.json config. composer show phpstan/extension-installer is the first diagnostic.


Gotchas and Tips

non_ignorable: true is the default β€” and it will surprise you. If you're used to baselining PHPStan errors freely, expect banned-code errors to refuse baselining. This is a feature, not a bug. But it means you cannot adopt the package on a large legacy codebase without either (a) cleaning all hits first, or (b) opting out with non_ignorable: false during migration. Plan the sequencing: enable, fix the hits, then re-enable non-ignorable.

The denylist is static, not runtime-aware. Banned calls hidden behind call_user_func('shell_exec', ...), $fn = 'eval', reflection, or framework indirection are invisible to this extension. Treat it as one control, not injection prevention. Keep disable_functions, container hardening, and SAST/DAST as separate layers β€” do not let a green PHPStan run become a security assurance.

Function-name normalization matters. v2.0 normalized leading backslashes (\var_dump == var_dump), so write names without a leading \ and don't add both. If a previous version silently missed \shell_exec calls, that's a good reason to be on v3.x.

Version alignment is non-negotiable. v3.x targets PHPStan 2.x. Mixing v2.x of this extension with PHPStan 2, or vice versa, will break at the config level. Pin the extension and PHPStan together and bump them in the same PR. The release history is clear: extension majors track PHPStan majors (v1 β†’ PHPStan 1.x, v3 β†’ PHPStan 2.x).

type is a PHPStan node type string, not a class name. The README's comment "type is the returned value of a node, see the method getType()" is the key detail: values like Expr_FuncCall, Expr_Eval, Stmt_Echo, Expr_Exit, Expr_Print, Expr_ShellExec are the accepted vocabulary. A typo here fails silently (rule simply never fires) rather than erroring loudly β€” so when a ban "doesn't work," verify the type string first by checking PHPStan's node type dump (run PHPStan with --debug or inspect the rule source in vendor/).

functions: null is meaningful. For node types that carry no function name (echo, eval, exit, print, shell exec), you pass null. For Expr_FuncCall you pass an array. Mixing these up (e.g., functions: null on Expr_FuncCall) will over-match or no-op depending on the rule β€” copy the README examples verbatim the first time.

Detection of exit/die in Laravel hits more than controllers. Illuminate\Foundation\Application-style code, queue workers, and command scripts legitimately use exit. Enable Expr_Exit only on paths where it shouldn't appear, or you'll get noise that teaches the team to ignore the gate. Same for echo/print β€” a blade view or a CLI command may be fine.

Backtick shell execution is a separate node. Banning shell_exec doesn't ban `cmd`. If you care about shell execution, include Expr_ShellExec explicitly; this is easy to miss because it's a language construct, not a function call.

Debugging a silent failure:

  1. Confirm PHPStan sees the extension: phpstan analyse -v and check the loaded config.
  2. Confirm the node type string is correct.
  3. Confirm the function name is in the array.
  4. Test in isolation: run against a fixture file containing exactly one var_dump() and see if it fires.
  5. If it fires in isolation but not in the app, you have a path or config-inclusion problem, not a rule problem.

Extension point. If you need a rule this package doesn't cover (e.g., banning a specific class method, not a function), write a small custom PHPStan rule implements \PHPStan\Rules\Rule β€” don't fork the extension. The extension's config is a convenience over PHPStan's rule system, and PHPStan is the substrate. Forking a vendor package for a one-method ban is the wrong trade.

Keep the config file reviewable. Banned lists are policy. Put them in a named config file with a comment explaining why each entry is banned (# force config() so config:cache works), and route changes through code review. A silent addition to a vendor-default denylist is how a gate becomes noise.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr