Skip to content

Releases: calebdw/phpstan-laravel

v1.5.0

Choose a tag to compare

@calebdw calebdw released this 25 Aug 21:24
1cadbeb

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 collection modelKeys() include null, 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 a list because the underlying pluck() is unkeyed.
  • Collection modelKeys() keeps the collection's own key type, matching the array_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

  • ModelHelper no 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

Choose a tag to compare

@calebdw calebdw released this 25 Aug 19:51
09f55a6

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|null

value() 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 @property annotations 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

Choose a tag to compare

@calebdw calebdw released this 25 Aug 02:42
3907834

Fix higher order collection ->map type inference

Full Changelog: v1.3.0...v1.3.1

v1.3.0

Choose a tag to compare

@calebdw calebdw released this 25 Aug 02:21
b988a37

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

Choose a tag to compare

@calebdw calebdw released this 25 Aug 01:59
1d58a65

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.0

When 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: postgres

PHPStan 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 NULL columns
  • Tables in public as ordinary Laravel table names
  • Tables outside public with their schema-qualified names preserved
  • Unknown, extension-provided, and application-defined types with a safe string fallback

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/migrations

Projects 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-parser and use sqlParser: postgres when 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

Choose a tag to compare

@calebdw calebdw released this 24 Aug 17:44
163298c

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'); // string

Static-facing macros are enabled by default for:

  • Illuminate\Support\Arr
  • Illuminate\Support\Str
  • Illuminate\Support\Number
  • Illuminate\Support\Benchmark
  • Illuminate\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\Model

Use 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|null

This 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() and Model::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.1

This 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

Choose a tag to compare

@calebdw calebdw released this 24 Aug 06:45
1ff7b29

update docs

Full Changelog: v1.0.0...v1.0.1

v1.0.0

Choose a tag to compare

@calebdw calebdw released this 24 Aug 06:07
f995ae5