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 Forbidden Laravel Package

rajmundtoth0/phpstan-forbidden

View on GitHub
Deep Wiki
Context7

Getting Started

This is a PHPStan extension, not a runtime package — nothing ships to production. Treat it as a lint rule that runs in CI and pre-commit, alongside Pint and Larastan.

composer require --dev rajmundtoth0/phpstan-forbidden

Add it to your existing phpstan.neon (you almost certainly already have Larastan wired up in a Laravel app):

includes:
    - vendor/larastan/larastan/extension.neon
    - vendor/rajmundtoth0/phpstan-forbidden/extension.neon   # verify filename in the package README

parameters:
    level: 6
    paths:
        - app
        - routes
        - database

Then confirm the rule is actually loaded before you trust it — write a deliberate violation and check it fails:

# temporarily add dd('x'); to a controller
vendor/bin/phpstan analyse --debug app/Http/Controllers

Where to look first:

  1. vendor/rajmundtoth0/phpstan-forbidden/extension.neon — tells you the exact parameter name the rule reads and which node types it hooks (FuncCall, StaticCall, MethodCall, ConstFetch, New_).
  2. The rule class itself in src/ — read it. With 7 stars and 0 dependents, the source is the documentation, and it is short.
  3. vendor/spaze/phpstan-disallowed-calls — worth reading as the mature reference implementation so you know what "should" exist vs. what this package actually covers.

First use case: ban the debugging helpers and env() outside config/. That single rule pays for itself the first time someone commits a dd().

parameters:
    forbidden:
        functions:
            - dd
            - dump
            - var_dump
            - ray

If your ban list needs path awareness (allow env() in config/, ban it everywhere else), set that up now — retrofitting path scoping across a large ban list is the annoying part.

Implementation Patterns

Keep the ban list in a committed phpstan.neon.dist

phpstan.neon.dist is committed and CI reads it; phpstan.neon is the local escape hatch. Ban lists are team policy, so they belong in the .dist file. Local developers who need to temporarily unban something override in phpstan.neon without polluting the shared config.

Scope bans by path with multiple config files

PHPStan has no per-path rule configuration, but includes gives you the same effect as long as you run analysis in separate invocations:

vendor/bin/phpstan analyse app routes        # strict ban set
vendor/bin/phpstan analyse config            # env() allowed here
vendor/bin/phpstan analyse tests database    # dd() allowed in tests

Create phpstan-app.neon, phpstan-config.neon, etc., each including the base file and adding its own forbidden list. Wire them into composer:

"scripts": {
    "lint:static": [
        "@php vendor/bin/phpstan analyse -c phpstan-app.neon",
        "@php vendor/bin/phpstan analyse -c phpstan-config.neon",
        "@php vendor/bin/phpstan analyse -c phpstan-tests.neon"
    ]
}

Note that each invocation is a separate PHPStan process, so expect a few extra seconds of container boot per run. For a monorepo this is fine; for a small app consider consolidating to two configs (app vs. everything-else).

Ban the Laravel footguns, not just dd()

The high-value targets in a typical Laravel codebase:

  • Debug/dev leakage: dd, dump, ray, var_dump, sleep, exit, die
  • Config bypass: env() in app/ — forces everything through config(), which is what makes config:cache work in production
  • Mass-assignment escapes: Model::unguard(), Model::reguard()
  • Query safety: DB::raw(), DB::statement(), DB::unprepared() — ban outright or confine to app/Repositories with a narrow path-scoped config
  • Auth shortcuts: Auth::loginUsingId(), auth()->loginUsingId() outside tests
  • Facade service location: app(SomeService::class) inside controllers — pushes the team toward constructor injection
  • Destructive infra: Redis::flushall(), Artisan::call('migrate:fresh')

Start with the debug helpers (zero false positives), then add env() (path-scoped), then the opinionated ones like DB::raw() (expect pushback, expect legitimate exceptions).

Use bans to drive a refactor, then lock it in

The most effective workflow is incremental and one-directional:

  1. Identify a legacy service, helper, or global you want gone.
  2. Add it to the ban list. Do not add the resulting errors to a baseline.
  3. Add narrow ignoreErrors entries for the remaining call sites with count: set to the current number.
  4. Each PR removes a few call sites and decrements the count:.
  5. When count: hits zero, delete the ignoreError entry.
parameters:
    ignoreErrors:
        -
            message: '#Forbidden function LegacyHelper::normalize#'
            path: app/Legacy/*
            count: 12

count: is the key detail — it makes the ignore a ratchet. New violations fail immediately; the number can only go down.

Narrow exceptions with ignoreErrors, never with a blanket path exclusion

excludePaths is a blunt instrument — it disables all PHPStan rules for that directory, including null-safety and type checks. If you need to allow DB::raw() in app/Repositories/, use a path-scoped ignoreErrors entry targeting the specific forbidden message, so the rest of PHPStan still runs.

Enforce in CI, not just locally

Add PHPStan to your pipeline so a banned call can't merge:

- run: composer lint:static

Commit hooks (Husky / captainhook) are a nice fast loop, but treat CI as the authority — hooks are skippable and drift between machines. Also make sure the PHPStan result cache isn't masking a newly added ban: bump a cache key or run --clear-result-cache in CI if your ban list changes frequently, since stale cache can suppress changed-config errors.

Test your ban list like you test code

A one-line Pest/PHPUnit test that shells out to PHPStan against a fixture file containing dd() and asserts a non-zero exit code is cheap insurance. It catches the failure mode where the extension silently stops loading after a phpstan.neon edit or a package major-version bump — which is otherwise indistinguishable from "no violations found."

Gotchas and Tips

It's static analysis only — it stops nothing at runtime. call_user_func('dd'), $fn = 'dd'; $fn(), (new $class)->$method(), and variable functions generally aren't resolved to a static name, so they sail straight past. If a ban is genuinely security-relevant (DB::raw() with user input), pair it with an architecture test or a runtime guard; don't treat the ban list as a control.

Larastan is a prerequisite for anything method-chain based. Without larastan/larastan, PHPStan can't resolve Facade proxy types or dynamic model methods, so banning DB::raw() or Auth::loginUsingId() may produce zero hits — silently. Verify every new ban with a deliberate violation before trusting it.

Never let these errors into a baseline. Running phpstan --generate-baseline after adding bans will happily swallow all of them into phpstan-baseline.neon, and you'll believe the bans are enforced when they aren't. If you regenerate a baseline for other reasons, re-check that forbidden-node errors didn't get swept up. reportUnmatchedIgnoredErrors: true (the default — keep it) helps here.

@phpstan-ignore comments are the hole in the fence. Anyone can silence a banned call inline. PHPStan 1.11+ warns on unused ignores; consider setting reportUnusedIgnoredErrors: true and reviewing @phpstan-ignore additions in code review the same way you'd review a // eslint-disable.

Exclude generated code from analysis. _ide_helper.php, _ide_helper_models.php, bootstrap/cache/*, and storage/framework/* routinely appear in paths and generate noise. Also remember vendor code is never analysed by default — bans apply to your source only, so you can't forbid a package's internals this way.

Pin the version and isolate the config. With 7 stars, a single maintainer, and a small dependents count, treat the config surface as unstable across releases. Two mitigations: (a) "rajmundtoth0/phpstan-forbidden": "1.0.*" rather than ^1.0, and (b) put all forbidden: parameters in one small phpstan-forbidden.neon that your main configs include. Swapping to spaze/phpstan-disallowed-calls (which has broader node coverage, active maintenance, and a larger user base) then becomes a one-file change instead of a hunt through every config. Honestly, for anything beyond a handful of function bans, evaluate spaze/phpstan-disallowed-calls first — it's the more battle-tested option and the migration cost is lower now than later.

The parameter key may not match what you expect. Extension parameter names have shifted in this package's history and the README is the only contract. If your forbidden: block produces no errors, the first debugging step is not "is my list wrong" — it's vendor/bin/phpstan analyse --debug and inspecting the loaded config with -vvv to confirm the parameter is actually being read rather than silently ignored. An unknown parameter in a neon file is not an error by default.

Performance is a non-issue, but config sprawl isn't. The rule is a few array lookups per node; you won't measure it. What you will feel is three or four overlapping phpstan-*.neon files where a ban added to one isn't in the others. Keep the base list in a single included file and only layer path-specific additions on top.

Extension point if coverage falls short: write a plain PHPStan Rule in app/Support/PHPStan/ implementing PHPStan\Rules\Rule<FuncCall> and register it in phpstan.neon under services:. It's ~30 lines, needs no package, and gives you full control over the message and the nodeType — often the right answer for one very specific ban that the package doesn't model.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin