NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Packagist · #2168 most downloaded on Packagist
Psalm plugin for Laravel
Last release 2 days ago
15 Sep 2026
Ships fairly regularly
a new release about every 9 days
Some releases are documented
notes for 16 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
8 years old
183 releases · first in 2019
v3.16.0 adds taint-analysis support for the laravel/ai package: prompt-injection detection plus tracking of LLM output into SQL, shell, and HTML sinks
v3.16.0 adds taint-analysis support for the laravel/ai package: prompt-injection detection plus tracking of LLM output into SQL, shell, and HTML sinks. It also extends MissingView to every view-name-bearing API and ships several new type-narrowing features.
The plugin now understands laravel/ai (>=0.11.0 <1.0.0, #937). The integration loads only when Composer reports the package installed; apps without it get no extra stubs or handlers. Prompt-input findings are enabled automatically; <findPromptInjection value="false" /> turns them off while keeping model-output findings.
Untrusted data (request input, HTTP responses, tool arguments, retrieved documents) reaching an LLM prompt is reported:
final class SupportAgent
{
use \Laravel\Ai\Promptable;
}
(new SupportAgent())->prompt($request->input('question'));
// 🔴 TaintedCustom: Detected tainted llm_promptLLM output is treated as a taint source, so it is followed into the existing SQL, shell, file, and HTML sinks:
function run(\Laravel\Ai\Responses\AgentResponse $response): void {
DB::select($response->text);
}
// 🔴 TaintedSql: raw model output interpolated into a queryMissingView beyond view() and Factory::make() to Factory::first()/renderWhen()/renderEach(), response()->view(), Route::view(), MailMessage::view()/markdown(), and assertViewIs() (#1407)
Route::view('/welcome', 'welcom');
// 🔴 MissingView: View 'welcom' not found in any of the registered view pathsArr::get() value type from known array shapes, including dot-notation keys and $default handling (#1402)
/** @param array{a: int} $data */
$value = Arr::get($data, 'a');
-// before: mixed
+// now: intManager::driver() to its create*Driver() return type, including the no-argument default-driver form (#1410)
final class ShippingManager extends Manager
{
public function getDefaultDriver(): string { return 'ups'; }
public function createUpsDriver(): Shipper { return new UpsShipper(); }
}
$manager->driver('ups'); // Shipper
-$manager->driver(); // was: mixed
+$manager->driver(); // now: ShippergetCollection() to a model's custom Eloquent collection (#1408)
#[CollectedBy(WorkOrderCollection::class)]
class WorkOrder extends Model {}
WorkOrder::query()->paginate()->getCollection();
-// before: Eloquent\Collection<int, WorkOrder>
+// now: WorkOrderCollection<int, WorkOrder>TaintedHtml for response()->make() bodies explicitly served as attachments (#1348)
response()->make($csv, 200, ['Content-Disposition' => 'attachment; filename="export.csv"']);
-// before: TaintedHtml
+// now: no issue — the response is a download, not rendered HTMLnew Illuminate\Http\Response(...), and safe literal Content-Type values; HTML, XML, SVG, and script types stay reported (#1417)PersonalAccessToken binding for models using HasApiTokens without an explicit @use annotation, fixing spurious MissingTemplateParam (#1425)HasFactory bindings from newFactory(), static $factory, and #[UseFactory] without suppressing unrelated MissingTemplateParam diagnostics on the model (#1423)handle() methods, Eloquent relations) so --find-unused-code stops flagging them as dead (#1422)Full Changelog: v3.15.7...v3.16.0
One column per quarter.
Taint accuracy for named arguments, plus three Eloquent and Collection narrowings.
Taint accuracy for named arguments, plus three Eloquent and Collection narrowings.
// sinkNamed(string $path = 'safe', string $label = 'x'), file sink on $path
sinkNamed(label: (string) $request->input('page'));
-// TaintedFile (before): the taint landed on $path, which never saw the request
+// no issue: the argument is attributed to $labelAbstractPaginator::getCollection() and AbstractCursorPaginator::getCollection() to Eloquent\Collection when the paginator holds models (#1397). The 33 methods that exist only on the Eloquent collection (load*, find, fresh, modelKeys, toQuery, ...) are now reachable. $users = User::paginate();
$users->getCollection()->load('posts');
-// UndefinedMethod (before): Support\Collection<int, User> has no load()
+// now resolves: Eloquent\Collection<int, User>select() stubs for Collection, LazyCollection, and Eloquent\Collection (#1399), including the select(null) branch and the variadic form. $rows = collect([['a' => 1, 'b' => 2]])->select('a');
-// Collection<array-key, mixed>&static (before); select('a', 'b') reported TooManyArguments
+// Collection<array-key, array<array-key, mixed>>Auth::guard() and Auth::shouldUse() when the guard name is a string-backed enum case (#1405). Previously any non-literal argument dropped all narrowing back to the bare contract. Auth::guard(Guards::Admin)->user();
-// Illuminate\Contracts\Auth\Guard (before)
+// now resolves through SessionGuard to the configured user model<experimental value="true" /> now also enables findSerializedQueuedModels, unless the project sets that flag explicitly (#1400). experimental is the early-access switch for rules that are off by default and on their way to becoming default.Full Changelog: v3.15.6...v3.15.7
Two new inference sources for Eloquent-backed code, one new opt-in queue-safety rule, and three false-positive fixes across migrations, model attribut
Two new inference sources for Eloquent-backed code, one new opt-in queue-safety rule, and three false-positive fixes across migrations, model attribute helpers, and pipelines.
SerializedQueuedModel rule for ShouldQueue classes that hold an Eloquent model without reaching Illuminate\Queue\SerializesModels, where the whole model is written into the queue payload instead of a ModelIdentifier (#1385). Enable with <findSerializedQueuedModels value="true" />. Detection resolves the flattened __serialize() / __sleep() rather than matching the trait name, so framework bases that already pull the trait in (Illuminate\Foundation\Queue\Queueable, Illuminate\Notifications\Notification) and classes that hand-write their own serialization stay silent. final class ReconcileLedger implements ShouldQueue
{
public function __construct(private Customer $customer) {}
- // silent: the entire Customer row is written into the queue payload
+ // SerializedQueuedModel: $customer will be serialized whole into the queue payload
}Arr::pluck() value and key types from the element model's @property annotations, matching what Collection::pluck() and Builder::pluck() already do (#1383). /** @param list<Customer> $rows */ // Customer has @property string $id
-Arr::pluck($rows, 'id'); // array<array-key, mixed>
+Arr::pluck($rows, 'id'); // list<string>
-Arr::pluck($rows, 'id', 'id'); // array<array-key, mixed>
+Arr::pluck($rows, 'id', 'id'); // array<string, string>$schema = Schema::connection(...) in migrations, so columns declared through a variable-held builder (#1382).Model::getAppends() and Model::getMutatedAttributes() to list<string>, so callers no longer have to re-assert the element type (#1381).Pipeline::then() from its destination closure instead of leaving it mixed (#1376).Full Changelog: v3.15.5...v3.15.6
Taint precision and Eloquent type narrowing. Two taint fixes remove a duplicate finding and close a missed SQL injection, and four type fixes sharpen
Taint precision and Eloquent type narrowing. Two taint fixes remove a duplicate finding and close a missed SQL injection, and four type fixes sharpen inference on facades, collections, and the query builder.
@method tags, so templated facade methods resolve their real return type (#1370) $value = Cache::remember('key', 60, fn (): User => User::first());
-// mixed — the generated @method tag outranked the plugin's stub
+// UsergroupBy() and keyBy() keys for model columns instead of widening to array-key (#1369) $byId = User::all()->keyBy('id');
-// Collection<array-key, User>
+// Collection<int, User>chunkById() callback like chunk(), so the chunk is a templated collection rather than mixed (#1373) Article::query()->chunkById(100, function ($chunk) { ... });
-// $chunk: mixed
+// $chunk: Collection<int, Article>$fetchUsing argument on DB::select(), DB::selectResultSets(), DB::cursor(), and their Connection counterparts, while keeping the three-argument signature an error on Laravel 12 (#1374) DB::cursor('select * from users', [], true, [PDO::FETCH_ASSOC]);
-// TooManyArguments — the stub declared three parameters
+// accepted on Laravel 13; still TooManyArguments on Laravel 12Row types follow the fetch mode, so a custom $fetchUsing no longer claims stdClass:
$rows = DB::cursor('select 1'); // Generator<int, stdClass>
$assoc = DB::cursor('select 1', [], true, [PDO::FETCH_ASSOC]);
-// Generator<int, stdClass>
+// Generator<int, mixed>TaintedInclude only, not also TaintedFile (#1358). A view name selects which template executes; it cannot reach an arbitrary path, because the view finder rewrites . to / and appends a fixed extensionWhereColumnTaintHandler's removal bridge on the AST node itself rather than spl_object_id, closing a missed SQL injection (#1366). Psalm frees foreign ASTs mid-file, so a reissued object handle could hit a stale record and strip sql taint from an unrelated expressionFull Changelog: v3.15.4...v3.15.5
Extends taint reporting to the call forms Laravel applications actually write — facade statics, response() on its contract, and view() names — and cle
Extends taint reporting to the call forms Laravel applications actually write — facade statics, response() on its contract, and view() names — and clears the false positives that surfaced alongside it.
response() contract methods, and view() names (#1318). A facade's surface is @method pseudo-methods resolved through __callStatic, whose parameters have no docblock to carry a sink, so the static form was silent while the chained form fired. Redirect::to($request->input('next'));
-// silent: the facade pseudo-parameter carried no sink
+// TaintedHeader: Detected tainted header$with and $withCount (#1321). Supports dotted paths, column selectors, and the $withCount alias grammar; defers when an intermediate related model cannot be resolved. class Post extends Model
{
protected $with = ['auther'];
- // silent: eager-load defaults were never validated
+ // UndefinedModelRelation: relation 'auther' is not defined on Post
}@psalm-taint-escape on closure validation rules (#1352), in inline validate() arrays, in FormRequest rules(), and when the closure is the field's whole rule. Previously the only way to assert a rule made a value safe was to extract it into a dedicated Rule class. $request->validate([
'path' => ['required', /** @psalm-taint-escape file */ static fn ($attr, $value, $fail) => /* ... */],
]);
-// TaintedFile: a closure body is opaque, so no rule could assert safety
+// cleanTaintedSql false positives on where() array values for nullable, template-bounded, and intersection builder receivers (#1338, #1350). Values in the map form are PDO-bound, but a receiver typed Builder|null, @template T of Builder, or T&Builder declined the strip. /** @param Builder|null $query */
$query->where(['status' => $request->input('status')]);
-// TaintedSql: Detected tainted SQL
+// cleanTaintedFile on uploaded-file extensions (#1324, #1325). getClientOriginalExtension() is the tail after the final dot of a normalized basename and cannot introduce a path segment, and clientExtension() returns a value from Symfony's MIME registry rather than raw client input. All other taint kinds, including include, are unchanged. Storage::putFileAs('uploads', $file, Str::ulid() . '.' . $file->getClientOriginalExtension());
-// TaintedFile: Detected tainted file handling
+// cleanSuppress cross-class taint flow through the Dispatchable traits (#1334). Psalm conflated taint nodes from the shared trait bodies, so an argument dispatched to one job appeared to reach an unrelated job's constructor sink. Genuine Bus and Event taint is still reported.
Fingerprint the migration schema cache on file contents instead of modification times (#1346). A git clone stamps every file with the checkout time, so the fingerprint changed on every CI run and the cache never hit even when the restored schema was still valid.
Full Changelog: v3.15.3...v3.15.4
Taint-analysis precision for static where() / whereNot() calls and redirect responses (Psalm 6 line), ported from the paired v4.15.3 release.
Taint-analysis precision for static where()/whereNot() calls and redirect responses (Psalm 6 line), ported from the paired v4.15.3 release.
TaintedSql false positives and a false negative on where()/whereNot() static receivers and nested-condition array values (#1300). class ConcreteOverrideModel extends Model {
/** @psalm-taint-sink sql $column */
public static function where(mixed $column): void {}
}
$term = (string) $request->input('term');
-ConcreteOverrideModel::where([['name', '=', $term]]); // silent (false negative, own sink stripped)
+ConcreteOverrideModel::where([['name', '=', $term]]); // TaintedSql
Article::where([['name', '=', $request->keyword]]);
-// TaintedSql (false positive) -- mixed value failed the outer position's scalar gate
+// clean -- inner nested-condition value strips unconditionally on a real Model receiverTaintedSSRF false positive on redirect() and Redirect/ResponseFactory redirect responses. A redirect is a header/open-redirect sink, not a server-side request, so it now reports the correct sink kind (#1313).-return redirect($request->input('next')); // TaintedSSRF (wrong sink kind)
+return redirect($request->input('next')); // TaintedHeaderFull Changelog: v3.15.2...v3.15.3
Taint-analysis precision for the query where() family (Psalm 6 line). Resolves the TaintedSql inconsistency from #1300 (the same safe query reported o
Taint-analysis precision for the query where() family (Psalm 6 line). Resolves the TaintedSql inconsistency from #1300 (the same safe query reported or stayed silent depending on how it was written), and closes two SQL source/sink gaps.
Request::__get() as user input, so $request->term carries taint like $request->input('term') (#1305). DB::table('t')->whereRaw((string) $request->input('term')); // TaintedSql
-DB::table('t')->whereRaw((string) $request->term); // silent
+DB::table('t')->whereRaw((string) $request->term); // TaintedSqlwhereColumn() / orWhereColumn() on all three identifier positions, which the grammar emits raw (#1308).-$builder->whereColumn((string) $request->input('c'), '=', 'other'); // silent
+$builder->whereColumn((string) $request->input('c'), '=', 'other'); // TaintedSqlwhere() sql-taint strip on a Laravel builder receiver, so a non-builder where(array $parts) that interpolates raw SQL keeps its report (#1311).where() array forms raising a false TaintedSql on PDO-bound value positions; the strip now walks the array literal element-wise and keeps the sink only on raw-identifier positions (#1302, fixes #1300). $term = (string) $request->input('term');
-Model::where([['name', 'LIKE', "%{$term}%"]]); // TaintedSql (false positive)
+Model::where([['name', 'LIKE', "%{$term}%"]]); // clean — value is PDO-boundwhereLike-family $value param to mixed, matching where() and the PDO-bound runtime, so idiomatic calls stop reporting false positives (#1312).-Model::whereLike('name', $request->query('q')); // PossiblyInvalidArgument
+Model::whereLike('name', $request->query('q')); // cleanFull Changelog: v3.15.1...v3.15.2
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →