Skip to content

Static analysis with Larastan ​

With Larastan, the package checks query definitions before they run. Add its extension after Larastan in phpstan.neon:

neon
includes:
    - vendor/larastan/larastan/extension.neon
    - vendor/myprogs/query-builder/extension.neon

What is checked ​

PHPStan reports, on the line of the declaration:

ProblemIdentifier
a relation path with a segment the model does not have (includes(), Relationship, dotted fields and filters, relation and aggregate sorts)queryBuilder.relation
Sort::relation() through a relation that is not to-onequeryBuilder.toOne
Relationship::to() or a Relationship::morphTo() type whose definition describes another modelqueryBuilder.definition
a column the table does not have: fields, virtual field dependencies, filter and sort columns, aggregate columnsqueryBuilder.column
Filter::scope() without such a scope on the modelqueryBuilder.scope
a default sort that is not among the sortsqueryBuilder.defaultSort

Columns come from the migrations and schema dumps Larastan reads, so they are checked only for models whose table Larastan knows. To turn the column checks off:

neon
parameters:
    queryBuilder:
        checkColumns: false

What is skipped ​

Only what is written literally is checked: a method returning [...] with strings and calls of Field, Relationship, Filter and Sort. Entries built at runtime (variables, spreads, array_merge()), methods inherited from a base definition and one-off Query::for() queries are skipped.

A relation is followed when its return type names the related model (@return HasMany<Post, $this>); a morphTo is followed through the types listed with morphTypes() or Relationship::morphTo(). A nested definition is compared with the relation when both files are analysed.

Typed queries ​

Query is generic in its model, so the model's scopes and the builder's methods stay known down a chain such as OrderCommentQuery::for($request)->where('order_id', $id)->owned()->paginate().

  • Query::for(User::class) and Query::for(User::query()) are Query<User>.
  • A definition names its model with @extends QueryDefinition<User>, or the extension reads it from model() when it returns User::class. Without either, the query is a Query<Model>.
  • An override of query() may return the definition's own builder type (Builder<User>).