psalm/plugin-laravel
Psalm plugin for Laravel that adds deep framework-aware static analysis plus taint-based security scanning. Detects SQL injection, XSS, SSRF, shell injection, file traversal, and open redirects by tracking user input flows across functions and services.
| Dependency | v3 | v4 |
|---|---|---|
| PHP | ^8.2 | ^8.3 |
| Laravel | 11, 12 | 12, 13 |
| Psalm | 6, 7 (beta) | 7 only |
Laravel 11 and Psalm 6 are no longer supported. If you need them, stay on v3.
v4 requires vimeo/psalm ^7.0.0-beta17 or later. If your project still uses Psalm 6, upgrade Psalm first:
composer require --dev vimeo/psalm:^7.0.0-beta17
Psalm 7 is still in beta. You may need to add this to your project's composer.json:
{
"minimum-stability": "dev",
"prefer-stable": true
}
Psalm 7 introduces new issue types that may surface in your codebase. These catch real design problems – fixing them improves your code, but you can suppress them during the upgrade and address them later:
MissingPureAnnotation -- a method has no side effects but lacks [@psalm-pure](https://github.com/psalm-pure). Adding it lets Psalm verify the method stays side-effect-free and enables callers to use it in pure contexts.MissingAbstractPureAnnotation -- an abstract method should be declared [@psalm-pure](https://github.com/psalm-pure) so all implementations are guaranteed pure.MissingImmutableAnnotation -- a class has no mutable state but lacks [@psalm-immutable](https://github.com/psalm-immutable). Marking it immutable prevents accidental mutation in future changes.MissingInterfaceImmutableAnnotation -- an interface should be [@psalm-immutable](https://github.com/psalm-immutable) so all implementations are guaranteed immutable.<issueHandlers>
<MissingPureAnnotation errorLevel="suppress" />
</issueHandlers>
All Eloquent relation stubs gained additional template parameters. If your codebase has [@psalm-return](https://github.com/psalm-return) (or [@return](https://github.com/return)) annotations with relation generics, they must be updated:
| Relation type | v3 signature | v4 signature |
|---|---|---|
BelongsTo |
BelongsTo<TRelated> |
BelongsTo<TRelated, TDeclaringModel> |
HasOne |
HasOne<TRelated> |
HasOne<TRelated, TDeclaringModel> |
HasMany |
HasMany<TRelated> |
HasMany<TRelated, TDeclaringModel> |
BelongsToMany |
BelongsToMany<TRelated> |
BelongsToMany<TRelated, TDeclaringModel> |
MorphOne |
MorphOne<TRelated> |
MorphOne<TRelated, TDeclaringModel> |
MorphMany |
MorphMany<TRelated> |
MorphMany<TRelated, TDeclaringModel> |
MorphTo |
MorphTo<TRelated> |
MorphTo<TRelated, TDeclaringModel> |
MorphToMany |
MorphToMany<TRelated> |
MorphToMany<TRelated, TDeclaringModel> |
HasOneThrough |
HasOneThrough<TRelated> |
HasOneThrough<TRelated, TIntermediate, TDeclaring> |
HasManyThrough |
HasManyThrough<TRelated> |
HasManyThrough<TRelated, TIntermediate, TDeclaring> |
Collection, EloquentCollection, and Builder are unchanged.
In Psalm 6 you had to pass --taint-analysis as a separate flag.
Psalm 7 combines type analysis and taint analysis into a single run by default.
No flags needed — just run ./vendor/bin/psalm.
Plugin issues (suppressible via <PluginIssue>):
InvalidConsoleArgumentName -- argument() references an undefined name in the command's $signatureInvalidConsoleOptionName -- option() references an undefined name in the command's $signatureNoEnvOutsideConfig -- env() called outside the config/ directory (env() returns null when the config is cached)<issueHandlers>
<PluginIssue name="InvalidConsoleArgumentName" errorLevel="suppress" />
<PluginIssue name="InvalidConsoleOptionName" errorLevel="suppress" />
<PluginIssue name="NoEnvOutsideConfig" errorLevel="suppress" />
</issueHandlers>
Psalm built-in issues (new detections via taint analysis):
TaintedSql -- where(), orWhere(), and other query builder methods now have [@psalm-taint-sink](https://github.com/psalm-taint-sink) sql annotations, catching SQL injection via dynamic column names#[Scope] attribute support -- Laravel 12+ scope detection alongside the traditional scope method prefixcasts() method without executing it)pseudo_property_set_types) for model propertiesAttribute<TGet, TSet> accessor templatesafter() closures, Blueprint::rename(), addColumn(), and more migration methods supportedloadMigrationsFrom()# 1. Update PHP to 8.3+ and Laravel to 12+ if needed
# 2. Upgrade Psalm to v7
composer require --dev vimeo/psalm:^7.0.0-beta17
# 3. Upgrade the plugin
composer require --dev psalm/plugin-laravel:^4.0
# 4. Update relation generic annotations (add declaring model parameter)
# BelongsTo<Foo> → BelongsTo<Foo, self>
# HasMany<Foo> → HasMany<Foo, self>
# HasOne<Foo> → HasOne<Foo, self>
# BelongsToMany<Foo> → BelongsToMany<Foo, self>
# MorphOne<Foo> → MorphOne<Foo, self>
# MorphMany<Foo> → MorphMany<Foo, self>
# HasManyThrough<Foo> → HasManyThrough<Foo, Intermediate, self>
# HasOneThrough<Foo> → HasOneThrough<Foo, Intermediate, self>
#
# Quick sed for the common cases (run from project root):
find app -name '*.php' -exec grep -l '[@psalm-return](https://github.com/psalm-return) \(BelongsTo\|HasMany\|HasOne\|BelongsToMany\|MorphOne\|MorphMany\|MorphTo\|MorphToMany\)<' {} \; \
| xargs sed -i 's/[@psalm-return](https://github.com/psalm-return) \(BelongsTo\|HasMany\|HasOne\|BelongsToMany\|MorphOne\|MorphMany\|MorphTo\|MorphToMany\)<\([^>]*\)>/[@psalm-return](https://github.com/psalm-return) \1<\2, self>/g'
# HasManyThrough / HasOneThrough need manual edits (add intermediate model).
# 5. Run Psalm and update your baseline
./vendor/bin/psalm --set-baseline=psalm-baseline.xml
# 6. Review new issues
# - InvalidConsoleArgumentName / InvalidConsoleOptionName are real bugs — fix them
# - NoEnvOutsideConfig — move env() calls into config files
# - TaintedSql on Builder::where() — review for actual SQL injection risk
How can I help you explore Laravel packages today?