Static analysis with Larastan
With Larastan, the package checks query definitions before they run. Add its extension after Larastan in phpstan.neon:
includes:
- vendor/larastan/larastan/extension.neon
- vendor/myprogs/query-builder/extension.neonWhat is checked
PHPStan reports, on the line of the declaration:
| Problem | Identifier |
|---|---|
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-one | queryBuilder.toOne |
Relationship::to() or a Relationship::morphTo() type whose definition describes another model | queryBuilder.definition |
| a column the table does not have: fields, virtual field dependencies, filter and sort columns, aggregate columns | queryBuilder.column |
Filter::scope() without such a scope on the model | queryBuilder.scope |
| a default sort that is not among the sorts | queryBuilder.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:
parameters:
queryBuilder:
checkColumns: falseWhat 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)andQuery::for(User::query())areQuery<User>.- A definition names its model with
@extends QueryDefinition<User>, or the extension reads it frommodel()when it returnsUser::class. Without either, the query is aQuery<Model>. - An override of
query()may return the definition's own builder type (Builder<User>).