Differences from spatie
The spatie-compatible API passes the spatie/laravel-query-builder 7.3.6 test suite, which runs against this package in tests/Compat/Spatie with its MIT notice preserved. The differences below are intentional.
Relation keys are added to the SELECT
When fields[...] is combined with include, the primary and foreign keys of every included relation (including morphTo, hasOneThrough/hasManyThrough and pivot keys) are selected automatically. Included relations never come back empty because a key was left out of the fieldset.
The added keys are ordinary attributes of the loaded models. Hide them in your API resources if they must not be exposed.
Fields and includes are applied lazily
allowedFields() and allowedIncludes() validate the request immediately, but apply the SELECT and the eager loads right before the first call that reads the query: get, paginate, first, firstWhere, toSql, getEloquentBuilder, a macro registered with Builder::macro() such as jsonPaginate(), and so on. The macros a global scope adds, such as withTrashed(), keep the chain open.
So:
- the order of
allowedFields()andallowedIncludes()does not matter, andUnknownIncludedFieldsQueryis never thrown (the class exists for type compatibility only); - filters and sorts are still applied immediately, in call order;
- columns added after
allowedFields()(withCount(),addSelect()in a custom sort) are kept next to the requested fields, and aselect()after it wins over the requested fields of the root, in the same places as in spatie; - a requested aggregate (
postsCount) keeps its place in the select as in spatie, but aselect()orallowedFields()afterallowedIncludes()no longer drops it; - an include naming a relation the model does not have fails while the query is planned, also when no row is found; spatie fails only when it loads the relation.
Table-name fieldsets reach nested includes
With convert_relation_table_name_strategy set, a fieldset keyed by the related table (fields[comments]=body) applies to a nested include such as posts.comments too. Spatie resolves the table of first-level relations only.
Eager loads set beforehand get their keys too
QueryBuilder::for(User::with('posts')) with fields[users]=name also selects the key posts needs, and so does ->with('posts.comments') under an included posts with a fieldset. Such a with() after allowedIncludes() keeps the fieldset of the included relation, which spatie drops. A constraint passed to it (->with(['posts' => fn ($q) => ...])) runs after the fieldset instead of replacing it.
Malformed parameters
A nested array where a string is expected (include[a][]=b) is ignored while the endpoint does not allow that parameter. Once it does, MalformedQueryParameter (400) is thrown instead of a PHP error.
Filter edge cases
- A filter object may be applied to several queries.
- An operator without a value (
filter[views]=<) or a</>filter withnullis skipped. - A
scopefilter through a relation resolves its model parameters on the related model. partial,beginsWithandendsWithlook for the wordstrue/falseinstead of turning them into an empty pattern.
morphTo respects fields
A fieldset for a morphTo target type is applied per type, with the keys the type needs.
Custom filter and sort classes
In an override of AllowedFilter::filter() or AllowedSort::sort(), use $query->getSubject() to reach the underlying builder. getEloquentBuilder() applies the deferred fields and includes, which an override called from allowedFilters() or allowedSorts() must not do.
AllowedInclude::include() is not called
Includes are planned together with the fields, so an override of include() in an AllowedInclude subclass never runs. Use AllowedInclude::custom() with an IncludeInterface instead.
Queries that are left alone
Keys are not added to queries that use groupBy or distinct, or that select from a raw expression. Such queries get exactly the columns the request asked for.
Custom relation classes
The relation types that ship with Laravel are known to the package. Other Relation subclasses are resolved by their key getters. If that fails, register your own Planning\Keys\KeyResolver in Planning\Keys\KeyResolverRegistry through the container.