Two opt-in rules that catch typos in route names and filesystem disks, plus sharper inference for Eloquent relations, aggregate attributes, when()/unless() callbacks, and Collection::filter(). How to enable them in your psalm.xml:
<pluginClass class="Psalm\LaravelPlugin\Plugin">
<findMissingViews value="true" />
<findUnregisteredRouteNames value="true" />
</pluginClass>or enable experimental features:
<pluginClass class="Psalm\LaravelPlugin\Plugin">
<experimental value="true" />
</pluginClass>Features
- Add opt-in
UnregisteredRouteNamerule for route names missing from the booted app's route table:route(),to_route(),URL::route()/signedRoute()/temporarySignedRoute(),Redirect::route(),redirect()->route(),url()->route(). Enable with<findUnregisteredRouteNames value="true" />or<experimental value="true" />(#1573)
return redirect()->route('users.shwo', $user);
+// UnregisteredRouteName: Route name 'users.shwo' is not registered- Add opt-in
UnconfiguredFilesystemDiskrule forStorage::disk()names absent fromfilesystems.disks, with a closest-name suggestion. Enable with<findUnconfiguredFilesystemDisks value="true" />or<experimental value="true" />.\Storage::disk()now also narrows toFilesystemAdapterlike the facade (#1571)
Storage::disk('publik')->put($path, $contents);
+// UnconfiguredFilesystemDisk: Disk 'publik' is not configured in filesystems.disks, did you mean 'public'?- Type
when()/unless()callback parameters from the condition value:when()passes the truthy value,unless()the falsy one, and aClosurecondition passes its return type (#1645)
/** @var ?int $limit */
$query->when($limit, function (Builder $q, string $limit) { /* ... */ });
-// no issue: the callback parameter was never checked
+// InvalidArgument: when expects callable(Builder<...>, int):mixed|null, but Closure(Builder, string):void provided- Infer relation generics for relation methods declared in a trait or delegating to another relation method (
return $this->allRevisions()->where(...)) (#1646)
// HasPaymentSlips trait: public function paymentSlips(): MorphMany { return $this->morphMany(PaymentSlip::class, 'owner'); }
$member->paymentSlips();
-// MorphMany<Model, Model>
+// MorphMany<PaymentSlip, Member>- Narrow
Collection::filter(callable)likewhere(callable), and recognizeis_subclass_of(),is_a(),is_scalar(),is_iterable(),is_resource()and null checks (!is_null($x),$x !== null) in both (#1651)
$items->filter(fn (Countable $x): bool => $x instanceof Stringable);
-// Collection<int, Countable>
+// Collection<int, Countable&Stringable>
$classes->filter(fn (string $c): bool => is_subclass_of($c, Job::class));
-// Collection<int, string>
+// Collection<int, class-string<Job>>Fixes
- Type
{relation}_count/{relation}_existsas nullable unlesswithCount()/withExists()/loadCount()/loadExists()is proven on that model (or the relation is in$withCount), and typewithMin/withMax/withSum/withAvgattributes from the related column's type (#1628)
$post->comments_count ?? $post->loadCount('comments')->comments_count;
-// RedundantCondition: comments_count was always int
+// no issue: int|null until a count is proven loaded- Mark
Modelas@psalm-consistent-constructor, sonew $class()on aclass-string<Model>is no longerUnsafeInstantiation. A model overriding__constructwith an incompatible signature is now reported, since Eloquent's ownnew static($attributes)already breaks on it (#1626)
/** @param class-string<Model> $class */
function make(string $class): Model { return new $class(); }
-// UnsafeInstantiation
+// no issue- Accept any
CollectioninBelongsToMany::sync(),syncWithoutDetaching(),syncWithPivotValues()and their Laravel 13*OrFail()variants (#1644)
$user->roles()->sync($roles->pluck('id'));
-// InvalidArgument / ImplicitToStringCast
+// no issue- Keep
$thisas the declaring model for relation calls inside non-final models, soreturn $this->rel()->where(...)no longer reportsMoreSpecificReturnType/LessSpecificReturnStatement(#1629, #1632) - Resolve scopes and
SoftDeletesmethods per receiver model on a generic custom builder shared by a parent and child model, instead of whichever model was scanned last (#1638) - Drop
@psalm-mutation-freefromRequest::input(), which lazily decodes JSON: aFormRequestoverridinginput()is no longer flagged (#1627) - Stop analysis-time type checks from autoloading analyzed classes, which crashed the run or silently disabled the plugin when a vendor file raised a load-time deprecation (#1661)
- Fix a crash on first-class callables (
$request->input(...),Artisan::command(...),Job::dispatch(...)) withzend.assertions=1(#1659)
Full Changelog: v4.16.6...v4.17.0