Releases: calebdw/phpstan-laravel
Release list
v1.5.0
v1.5.0 teaches the analyser about Eloquent primary keys, and relaxes the PHPStan constraint so you can pin around the 2.2.9 regression.
Model key inference
getKey() and modelKeys() used to come back as mixed and array<int, int|string> no matter how the model was configured. They now resolve against the model's own key type:
$model->getKey(); // int|string|null
$user->getKey(); // int|null
$uuidModel->getKey(); // string|null
$users->modelKeys(); // array<int, int|null>
$uuidModels->modelKeys(); // array<int, string|null>
User::query()->modelKeys(); // list<int> (Laravel 13)
UuidModel::query()->modelKeys(); // list<string> (Laravel 13)The key type is read from getKeyType() on an instantiated model rather than from the class, so it sees $keyType, HasUuids, HasUlids, and anything else a trait or the boot sequence contributes. A model whose key type cannot be determined falls back to int|string.
Three details worth knowing, since they are the reason the three results differ:
getKey()and collectionmodelKeys()includenull, because an in-memory model may be unsaved and have no key yet.- Builder
modelKeys()describes a query projection, so it stays non-null, and it is alistbecause the underlyingpluck()is unkeyed. - Collection
modelKeys()keeps the collection's own key type, matching thearray_map()the framework runs over its items.
Builder modelKeys() only exists on Laravel 13, so that inference applies there only.
PHPStan constraint relaxed
phpstan/phpstan is now ^2.2.8 rather than ^2.2.9 (#9).
PHPStan 2.2.9 has a regression affecting root-namespace facade aliases (\Log, \Cache, \Str, and friends) registered by Laravel's alias loader, and the old lower bound left no version combination that avoided it. Nothing here requires 2.2.9, so you can hold at 2.2.8 until PHPStan ships the fix. See phpstan/phpstan#15102.
If you would rather move off the aliases entirely, rector-laravel has a rule that rewrites global facades to their fully qualified equivalents.
Fixes
ModelHelperno longer attempts to instantiate a class that is not an Eloquent model. Key resolution walks the class reflections behind a receiver type, which can include non-models, and that path could otherwise construct an unrelated class during analysis.
Full Changelog: v1.4.0...v1.5.0
v1.4.0
v1.4.0 improves Eloquent scalar inference and makes moving an existing Larastan project much easier.
Eloquent scalar value inference
Eloquent builder value(), soleValue(), and valueOrFail() calls now resolve the selected model property instead of returning mixed. Schema types, casts, accessors, and explicit model property annotations all carry through.
User::query()->value('id'); // int|null
User::query()->soleValue('id'); // int
User::query()->valueOrFail('id'); // int
User::query()->valueOrFail('email_verified_at'); // Carbon|nullvalue() includes null because the query may find no row. soleValue() and valueOrFail() throw when no row is available, but still preserve null when the selected column itself is nullable. Dynamic and raw expressions continue to resolve to mixed.
Agent-assisted Larastan migration
The package now ships a Laravel Boost skill named phpstan-laravel-larastan-migration. Ask a Boost-enabled coding agent to migrate a project from larastan/larastan or calebdw/larastan, and it can perform the migration as an ordered, reviewable workflow:
- Record the existing PHPStan result before changing anything.
- Swap the Composer package and extension include.
- Rename and reorganize configuration options.
- Rewrite Larastan identifiers in baselines and inline ignores.
- Verify the final analysis against the original result.
The skill updates existing suppressions in place rather than blindly regenerating the baseline, so newly exposed analysis errors remain visible for review.
Documentation
- Documented Laravel IDE Helper compatibility. Generated
@propertyannotations written directly to models override phpstan-laravel's schema, cast, and accessor inference; facade/meta generation and IDE-only external model metadata remain usable. - Expanded and corrected the Larastan migration guide, including configuration and error-identifier renames.
- Simplified the project README and directed detailed usage guidance to the documentation site.
Full Changelog: v1.3.1...v1.4.0
v1.3.1
Fix higher order collection ->map type inference
Full Changelog: v1.3.0...v1.3.1
v1.3.0
Calling ->values()->all() on an Enumerable now returns a list<...> type instead of array<int, ...>
Full Changelog: v1.2.0...v1.3.0
v1.2.0
PHPStan Laravel 1.2.0
PostgreSQL schema dumps are finally a first-class source of Eloquent model properties in PHPStan Laravel.
This is a major expansion of schema support. PostgreSQL projects can now analyse the plain-text pg_dump files produced by Laravel's schema:dump command instead of generating replacement migrations, maintaining parallel PHPDoc metadata, or asking a MySQL-focused parser to make sense of PostgreSQL SQL.
First-class PostgreSQL schema support
Install the new optional parser alongside PHPStan Laravel:
composer require --dev calebdw/phpstan-laravel:^1.2 calebdw/pg-schema-parser:^1.0When it is the only SQL parser installed, the default auto driver selects it automatically. If the project also has a MySQL parser installed, select PostgreSQL explicitly:
parameters:
laravel:
sqlParser: postgresPHPStan Laravel then reads the normal connection-named dumps under database/schema, including files such as pgsql-schema.sql, and uses them to infer model properties without connecting to the database.
The PostgreSQL integration understands much more than basic CREATE TABLE statements:
- PostgreSQL's canonical integer, floating-point, boolean, character, date/time, UUID, JSON, network, binary, and other built-in type names
- PostgreSQL enums as precise literal-string unions
- Domains resolved to their underlying types
- PostgreSQL arrays as the string literals returned by raw PDO attributes
- Nullable and
NOT NULLcolumns - Tables in
publicas ordinary Laravel table names - Tables outside
publicwith their schema-qualified names preserved - Unknown, extension-provided, and application-defined types with a safe
stringfallback
Model casts continue to apply on top of the inferred database type. For example, an uncast jsonb attribute reflects PDO's raw string value, while an Eloquent array, collection, object, or custom cast produces the corresponding cast type.
Plain-text dumps are supported. PostgreSQL custom and directory-format dumps are not SQL text and cannot be scanned.
PostgreSQL enums retain their values
A database enum is no longer reduced to an unhelpful string:
CREATE TYPE public.account_status AS ENUM (
'active',
'suspended',
'closed'
);
CREATE TABLE public.accounts (
status public.account_status NOT NULL
);The resulting model property is inferred as:
$account->status;
// 'active'|'closed'|'suspended'If the model applies a backed-enum cast, normal Eloquent cast inference takes over and returns the PHP enum type instead.
Schema dumps and later migrations work together
Migration discovery now follows Laravel's own directory traversal: only migration files directly inside each configured directory are scanned. Nested directories such as database/migrations/archive are not replayed implicitly over a current schema dump.
This configuration scans application and modular migrations while leaving nested archives alone:
parameters:
laravel:
migrationDirectories:
- database/migrations
- app/Domain/*/database/migrationsProjects that intentionally organize active migrations into nested directories can opt in with another wildcard path:
parameters:
laravel:
migrationDirectories:
- database/migrations
- database/migrations/*The first path scans direct migrations; the second scans direct migrations in each immediate child directory.
The directory documentation now also makes replacement semantics explicit. A configured migrationDirectories or schemaDirectories list replaces its conventional default. Include database/migrations or database/schema in the list when adding locations rather than replacing them.
More reliable schema parser failures
The phpmyadmin driver now wraps both lexer and parser failures in PHPStan Laravel's SqlParserFailure. Previously, input the lexer could not tokenize, including PostgreSQL array syntax or PostgreSQL 18's \restrict meta-command, could escape as a vendor exception instead of identifying the unreadable schema dump.
SQL-standard CHARACTER and CHARACTER VARYING spellings are also recognized as strings rather than falling through to mixed.
Upgrade notes
- PostgreSQL projects should install
calebdw/pg-schema-parserand usesqlParser: postgreswhen another SQL parser is also present. - Migration directories are no longer recursive by default. Add an explicit wildcard path if nested migration directories contain active migrations.
- More accurate schema types and nullability may reveal real type mismatches or make old baseline entries and inline ignores unmatched.
- Projects without squashed schema dumps still need no SQL parser; parser dependencies remain optional and are resolved only when a dump is read.
Documentation
- Added PostgreSQL installation and configuration guidance.
- Added PostgreSQL schema support to the comparison with Larastan.
- Clarified effective migration and schema directory defaults.
- Added separate examples for replacing and extending default directories.
- Documented explicit opt-in for nested migration directories.
- Added an FAQ entry for enum-keyed collections.
Full changelog: v1.1.0...v1.2.0
v1.1.0
PHPStan Laravel 1.1.0
PHPStan Laravel 1.1.0 improves array-shape and model-property inference, fixes several schema helper types, and understands Laravel's conventionally static utility macros.
No configuration changes are required when upgrading.
Precise Arr::only() shapes
Arr::only() now preserves and narrows array shapes instead of returning a generic array:
/** @var array{id: int, name: string, email: string} $row */
Arr::only($row, ['id', 'name']);
// array{id: int, name: string}
Arr::only($row, 'id');
// array{id: int}
Arr::only($row, ['id', 'missing']);
// array{id: int}Unknown keys preserve the original shape with optional entries, since Arr::only() can remove values but cannot add them:
Arr::only($row, $keys);
// array{id?: int, name?: string, email?: string}Generic maps retain their value type while the requested keys are narrowed:
/** @var array<string, int> $counts */
Arr::only($counts, ['open', 'closed']);
// array<'open'|'closed', int>The inference follows Laravel's runtime behavior, including optional and integer-like keys. Arr::only() uses top-level keys and does not support dot notation.
See the Arr::only() guide for more examples.
The model-property guide now also documents the existing shape inference for Model::only(), including columns, casts, accessors, missing attributes, and dotted keys:
$user->only(['name', 'blocked']);
// array{name: string, blocked: bool}
$user->only(['name', 'missing']);
// array{name: string, missing: null}Static utility macros
Laravel utility classes expose a conventionally static API even when a macro is registered with a normal closure. These calls are now understood without suppressing method.staticCall:
Str::macro('initials', function (string $name): string {
return collect(explode(' ', $name))
->map(fn (string $part): string => $part[0])
->join('');
});
Str::initials('Taylor Otwell'); // stringStatic-facing macros are enabled by default for:
Illuminate\Support\ArrIlluminate\Support\StrIlluminate\Support\NumberIlluminate\Support\BenchmarkIlluminate\Validation\Rule
The exception applies only to dynamically discovered macros. Native instance methods still receive PHPStan's normal static-call checks, and macro closure parameters and return types remain available to analysis.
Projects can add their own static-facing macro classes with staticMacroClasses. Entries apply to subclasses, so Eloquent's static forwarding can be enabled for every model when that matches the project's conventions:
parameters:
laravel:
staticMacroClasses:
- Illuminate\Database\Eloquent\ModelUse staticMacroClasses! to replace the defaults instead of extending them:
parameters:
laravel:
staticMacroClasses!: []See the new macros guide for discovery rules, static and instance macros, facades, and configuration.
Fixes #2.
Correct schema helper types
Migration-based model-property inference now matches Laravel's schema helpers more closely:
Schema::table('users', function (Blueprint $table): void {
$table->rememberToken();
$table->year('birth_year');
$table->year('graduation_year')->nullable();
$table->timestampTz('published_at');
$table->timeTz('opens_at');
$table->softDeletesDatetime('deleted_at');
$table->softDeletesTz('deleted_at_tz');
});The resulting model properties are inferred as:
$user->remember_token; // string|null
$user->birth_year; // int
$user->graduation_year; // int|null
$user->published_at; // string
$user->opens_at; // string
$user->deleted_at; // string|null
$user->deleted_at_tz; // string|nullThis release also correctly recognizes spatialIndex() as an index operation rather than a column declaration.
Fixes #4.
Collection variance guidance
New troubleshooting and FAQ documentation explains PHPStan errors where the expected and returned collection types appear identical, followed by a TValue is not covariant tip.
Laravel collections are mutable, so their value type remains invariant. Use call-site covariance when a method exposes a read-only projection of a wider type:
/** @return Collection<int, covariant string|null> */
function names(): Collection
{
return User::query()->pluck('name');
}This widens the public return contract without making every mutable Collection globally covariant. Unsafe writes through the projected type remain rejected by PHPStan.
See the collection troubleshooting guide for details.
Addresses #1.
Documentation
- Added a complete guide to macro discovery, call forms, facades, and
staticMacroClasses. - Added detailed
Arr::only()andModel::only()inference examples. - Added collection variance troubleshooting and FAQ entries.
- Added a PHP compatibility badge to the README.
Upgrade
composer require --dev calebdw/phpstan-laravel:^1.1This release has no intended breaking configuration changes. More precise inferred types may reveal previously hidden type mismatches or cause obsolete baseline entries to become unmatched.
Thanks to Sander Muller for the detailed reports and reproductions behind #1, #2, and #4.
Full changelog: v1.0.1...v1.1.0
v1.0.1
update docs
Full Changelog: v1.0.0...v1.0.1
v1.0.0
Full Changelog: https://github.com/calebdw/phpstan-laravel/commits/v1.0.0