Skip to content

Custom PHPDoc types

All custom types that are specific to this extension are listed here. Types that are defined by PHPStan can be found on their website.

view-string

The view-string type is a subset of the string type. Any string that passes the view()->exists($string) test is also a valid view-string.

Example:

/**
 * @phpstan-param view-string $view
 * @param string $view
 * @return \Illuminate\View\View
 */
public function renderView(string $view): View
{
    return view($view);
}
Now, whenever you call renderView, this extension will try to check whether the given string is a valid blade view.

If the string is not an existing blade view, the following error will be displayed by this extension.

Parameter #1 $view of method TestClass::renderView() expects view-string, string given.  

The view, html, text, and markdown constructor arguments of Illuminate\Mail\Mailables\Content are also view-string|null, including named arguments. htmlString is rendered HTML and is not checked as a view name.

When working with packages, all vendor-prefixed paths like acme::example may fail. As packages don't contain a Laravel app, the default skeleton from orchestra/testbench is used. This instance doesn't know about the package so views are not registered. Create a testbench.yaml file to register your service provider to solve this issue.

providers:
    - Acme\AcmeServiceProvider

model-property

model-property extends the built-in string type and acts like a string in the type level. But during the analysis if this extension finds that an argument of the method or a function has a model-property<ModelName>, it'll try to check that the given argument value is actually a property of the model.

All of the Laravel core methods have this type thanks to the stubs. So whenever you use a Eloquent builder, relation or a model method that expects a column, it'll be checked by this extension if the column actually exists. But you can also typehint any argument with model-property in your code.

The type is only active when modelPropertyType is enabled. With it off, model-property<Model> behaves as a plain string and nothing is checked. There is no rule behind it: once the type is active, the mismatches are reported by PHPStan's ordinary argument checks, so they carry core identifiers such as argument.type. See checking column names for a worked example.

builder-of

The builder-of<Model> type resolves to the Eloquent builder for that model. It uses a custom newEloquentBuilder() implementation, #[UseEloquentBuilder], or the model's static $builder property, in that order. Otherwise it is Illuminate\Database\Eloquent\Builder<Model>.

A union of models becomes a union of their builders. Generic arguments, static, $this, and intersections on the model are kept. Model query methods, Collection::toQuery(), and relation query builders use it so custom builders are retained.

use App\User;
use App\Post;
use Illuminate\Database\Eloquent\Builder;

/**
 * @phpstan-return builder-of<User>
 */
function getActiveUsers(): Builder
{
    return User::query()->where('active', true);
}

/**
 * @phpstan-param builder-of<Post> $postQuery
 */
function publishPosts(Builder $postQuery): void
{
    $postQuery->where('foo', 'bar')->get()->each(fn ($post) => $post->publish());
}

It also works with generic templates:

use Illuminate\Database\Eloquent\Builder;

/**
 * @template TModel of \Illuminate\Database\Eloquent\Model
 */
class ModelRepository
{
    /**
     * @phpstan-param class-string<TModel> $modelClass
     * @phpstan-return builder-of<TModel>
     */
    public function getQuery(string $modelClass): Builder
    {
        return $modelClass::query();
    }
}

An optional second argument selects a relationship on the first model:

/** @param builder-of<User, 'accounts'> $query */
function filterAccounts(Builder $query): void
{
    $query->where('active', true);
}

If User::accounts() relates to Account, this resolves to that model's builder, including a custom builder. Dotted paths such as builder-of<User, 'posts.comments'> resolve to the final related model. Unions of models or relation names become unions of builders. Paths that cannot resolve are discarded; if none resolve, the type falls back to builder-of<User>. If a path cannot be followed because a relation lost its related model type, it instead falls back to Builder<Model>.

relation-of

The relation-of<Model, 'name'> type resolves to the relationship object returned by the model's relationship method, including its concrete relation class and generic model types. Dotted paths resolve to the final relationship object, rather than its query builder.

/** @param relation-of<\App\User, 'posts.comments'> $comments */
function filterComments(\Illuminate\Database\Eloquent\Relations\Relation $comments): void
{
    $comments->where('approved', true);
}

Unions of models or relationship names resolve to unions of relationship objects. Unresolvable names are discarded when another name resolves; when none resolve, the type falls back to Relation<Model, Model> with the original model as the declaring model. Generic model and relationship name templates resolve once their types are known.

eager-load-of

The eager-load-of<Model, TRelations> type is the with() argument with each closure typed by its key: the closure under 'posts' receives relation-of<Model, 'posts'>. The builder's with() / withOnly() and the Eloquent collection's load() / loadMissing() bind TRelations to their argument with a template, whose bound checks the argument. A wrapper does the same:

/**
 * @template TRelations of array<array-key, array<mixed>|\Closure|string>|string
 * @param eager-load-of<\App\User, TRelations> $relations
 */
function loadUsers(array|string $relations): void
{
    \App\User::query()->with($relations)->get();
}

loadUsers(['posts.comments' => fn ($query) => $query->latest()]);
// $query: MorphMany<App\Comment, App\Post>

Other values, and arrays whose keys are not all literals, are checked against the bound.

collection-of

The collection-of<Model> type resolves to the Eloquent collection used by that model. It uses a custom newCollection() implementation, #[CollectedBy], or the model's static $collectionClass property, in that order. Laravel 13's inherited #[CollectedBy] behavior is also respected. Otherwise it is Illuminate\Database\Eloquent\Collection<int|string, Model>.

Model unions become unions of their collections, and generic templates are resolved when their model type becomes known. Like array<TKey, TValue>, an optional key type can be given first:

/** @param collection-of<string, \App\User> $users */
function usersByEmail(\Illuminate\Database\Eloquent\Collection $users): void
{
}

factory-of

The factory-of<Model> type resolves to the concrete factory selected for the model. It follows the same precedence as Model::factory(): a model's custom newFactory() method, static $factory property, #[UseFactory], then Laravel's factory naming convention. Model unions become unions of their factories.

/**
 * @template TModel of \Illuminate\Database\Eloquent\Model
 * @param class-string<TModel> $model
 * @return factory-of<TModel>
 */
function factoryFor(string $model)
{
    return $model::factory();
}