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.
composer require --dev spaze/phpstan-disallowed-calls
With phpstan/extension-installer present, nothing else is needed. Otherwise add the include to phpstan.neon:
includes:
- vendor/spaze/phpstan-disallowed-calls/extension.neon
Fastest first win: include a bundled ruleset (see docs/configuration-bundled.md for the full list — disallowed-dangerous-calls.neon and disallowed-loose-calls.neon are the usual starters) and run PHPStan once:
includes:
- vendor/spaze/phpstan-disallowed-calls/extension.neon
- vendor/spaze/phpstan-disallowed-calls/disallowed-dangerous-calls.neon
- vendor/spaze/phpstan-disallowed-calls/disallowed-loose-calls.neon
Then add one hand-written rule so you learn the config shape, e.g. ban var_dump() with a helpful message:
parameters:
disallowedFunctionCalls:
-
function: 'var_dump()'
message: 'Use the app logger (or ray/dd in local only), never var_dump()'
Where to look next, in order: docs/custom-rules.md (all supported item types and keys — function/method/static calls, constants, enums, properties, namespaces, classes, attributes, keywords, superglobals), docs/configuration-bundled.md, then the docs/allow-*.md pages. The strict config schema (since v3.0) validates keys, so an unknown key fails loudly rather than silently doing nothing.
First real use case in a Laravel app: ban dd(), dump(), var_dump(), print_r(), ray() and env() outside config/, all in one config, and let CI catch the dd() someone forgot to delete.
Start from bundled, then customize by re-declaring. A config entry replaces any previous entry for the same disallowed item (for calls, allowExceptParams values must match too). So to keep var_dump() disallowed everywhere except your own debug wrapper, don't try to "add" to the bundled rule — re-declare it with the extra allow directive and message:
parameters:
disallowedFunctionCalls:
-
function: 'var_dump()'
message: 'Use App\Support\Debug::dump()'
allowIn:
- 'App\Support\Debug::dump()'
Escape hatches are per-entry, and combine. allowIn (function/method names), path-based allows, parameter conditions (allowExceptParams family and their InAllowed variants), allowInInstanceOf, attribute-based allows, and PHPStan's own ignoreErrors. Directives are evaluated in order, and the item is allowed as soon as one matches. allowExcept* / disallow-in-style directives are not allow directives: when their pattern matches the item is reported, and later directives are never consulted.
Path-scope env() instead of banning it outright:
-
function: 'env()'
message: 'Read config via config(), env() only belongs in config/*.php'
allowInPaths:
- 'config/*'
- 'tests/*'
Parameter-conditional rules for noisy APIs. Rather than banning DB::raw() (sometimes legitimately needed), ban the sharp edges or ban-with-exception on a specific argument, e.g. allow app() only when called with no arguments, or disallow ->whereRaw() unless the first argument is a constant string in your repository layer. Parameter conditions accept scalars, PHPDoc typeString ('foo'|'bar', array{}) since v3.0, and positions/names.
Wrapper-first workflow. Pick a small number of "never call directly" APIs — the framework logger facade, a mailer, a cache, exec(), shell_exec(), eval(), unserialize(), $_REQUEST/$_GET/$_POST via disallowedSuperglobals — and pair every disallowed entry with a sanctioned wrapper that is allowed via allowIn/allowInInstanceOf. This keeps the rule list short and gives developers a clear alternative, which is what actually stops the bad call.
Type-hint carve-outs. For disallowedClasses/disallowedNamespaces, allow in type-hint positions (param/return types) where a boundary class is unavoidable, while still blocking new/static usage. Applies only to those two key types.
Legacy codebase rollout: run with --generate-baseline once, commit the baseline, and keep the rule set as-is; new violations fail CI. Alternatively use default error identifiers in ignoreErrors for a handful of known files. Note the baseline approach hides re-introduced violations in the same lines, so prefer wrapper + allowIn* where you can.
Audit mode (no other rules). Per docs/phpstan-custom-ruleset.md, run this extension with a minimal ruleset against vendor/ to audit a third-party package for eval, exec, superglobals, etc., without adopting a full PHPStan level:
vendor/bin/phpstan analyse -c phpstan-disallowed-only.neon vendor/acme/package
Keep disable_functions / disable_classes in sync. Use the provided generator (docs/disallow-disabled-functions-classes.md) to produce a config snippet from production's php.ini, so code that calls a disabled function is flagged in CI rather than fatalling in prod.
CI integration. Standard vendor/bin/phpstan analyse in the pipeline is enough — violations are ordinary PHPStan errors, so exit code 1 fails the build. Add --error-format=github for inline annotations. Use errorTip (a string or, since v4.8, an array) to put the "why" next to the error instead of burying it in docs.
Typical Laravel rule set to build toward (each with an allowed wrapper/config location):
| Disallow | Allow where |
|---|---|
dd(), dump(), ray(), var_dump(), print_r() |
App\Support\Debug only |
env() |
config/*, tests/* |
$_REQUEST, $_GET, $_POST, $_COOKIE |
request/service layer only |
exec(), shell_exec(), eval(), unserialize(), phpinfo() |
nowhere (or a hardened service) |
Log:: facade (generic logger) |
your logging wrapper via allowIn |
DB::raw(), ->whereRaw() |
repository classes via allowInInstanceOf |
print_r catches print_R); constants, paths and filenames are case-sensitive. Path typos in allowInPaths silently never match.sodium_* won't fire on a machine where the extension is missing — don't treat this as a security control (the README is explicit: it's a second pair of eyes, obfuscatable by design).., ? or !. Don't put trailing punctuation logic in your head — and don't write ignoreErrors regexes anchored on a period you didn't write.docs/.var_dump() replaces the first (and the bundled one); the later entry wins, so restate the message and every allow directive you still want. Since v3.0 you can add multiple rules for the same call to get different messages per parameter set — but that changed the pre-3.0 "first rule wins by order" behaviour.else if with a space throws. It parses as else + if; disallow elseif instead. Also, parameter matching inside (...) doesn't work for control structures: you can disallow all declare() but cannot re-allow declare(strict_types=1).disallowedEnums uses the constant machinery internally; a v4.7.1 fix removed template types from the declaring-class name in messages for PHPStan 2.1.38+, so message-matching ignoreErrors regexes may need adjusting after PHPStan upgrades.disallowedNamespaces and disallowedClasses respect the type-hint-position allow; asking for the same on constants or calls won't do anything.extension.neon is actually included (phpstan --debug / -v shows included configs), re-run with --clear-result-cache after config edits, and check the rule against a deliberately bad file in an analyzed path. If the file isn't matched by paths/excludePaths, PHPStan never sees it — the most common "rule doesn't work" cause.allow* directives, errorTip, custom error identifiers, and the bundled-config generator. If you need detection of a construct not covered in tests/src, that's an upstream issue/PR — the README says plainly that untested constructs are probably not detected.make lint target so dd() is caught before commit, not after review.How can I help you explore Laravel packages today?