Repository navigation
QueryFilter
Pair\Api\QueryFilter applies REST-style filtering/sorting/pagination to ActiveRecord list endpoints.
It is used by CrudController to parse request parameters and transform them into Pair\Orm\Query constraints.
filter[field]=valuefilter[field]=!value-
filter[field]=>=N,<=N,>N,<N -
filter[field]=v1,v2,v3(IN) -
filter[field]=null,!null sort=field,-otherFieldsearch=keywordfields=field1,field2include=rel1,rel2page=NperPage=N
Allowed filters/sorts/search fields come from model API config (ApiExposable pattern).
Internally, Pair v4 reads these values through CrudResourceConfig, even when the application supplied an array.
Common keys:
filterablesortablesearchableincludesperPagemaxPerPagedefaultSort
Returns:
-
query(Pair\Orm\Query) pageperPagefieldsincludes
Builds count query with current filters/search (without sort/pagination) to compute pagination metadata.
GET /api/faqs?filter[status]=published&search=payment&sort=-created&page=2&perPage=20
Effects:
- where status = published
- full-text-like search on configured searchable fields
- order by created DESC
- page 2, size 20
- Disallowed or unknown fields are ignored.
-
perPageis clamped to configured maximum. - Sparse fieldsets (
fields) and includes (include) are parsed but actual response shaping depends onResource/transform logic. -
defaultSortis used only when thesortparameter is absent. Ifsortis present but all requested fields are disallowed, QueryFilter does not fall back todefaultSort; it simply emits no sort clauses.
GET /api/users?filter[active]=1&filter[groupId]=2,3&search=ada&sort=-createdAt,email&page=2&perPage=30&fields=id,email,active&include=group
Typical effects:
active = 1-
group_id IN (2,3)(through model binds) - search across configured searchable fields
- order by
created_at DESC, thenemail ASC - page 2, size 30 (bounded by
maxPerPage) - sparse output fields and allowed includes parsed
/**
* Declare the model's public query contract.
*/
public static function apiConfig(): array
{
return [
'filterable' => ['id', 'email', 'active', 'groupId'],
'sortable' => ['id', 'email', 'createdAt'],
'searchable' => ['email', 'fullName'],
'includes' => ['group'],
'defaultSort' => '-id',
'perPage' => 20,
'maxPerPage' => 100,
];
}The application model must use ApiExposable, which provides getCrudResourceConfig(). Override apiConfig() to declare the model's contract; getApiConfig() and getCrudResourceConfig() return the merged configuration.
$request = new \Pair\Api\Request();
$filter = new \Pair\Api\QueryFilter(\App\Orm\User::class, $request, \App\Orm\User::getCrudResourceConfig());
$total = $filter->count();
$result = $filter->apply();
$query = $result['query'];
$rows = \Pair\Orm\Database::load($query->toSql(), $query->getBindings());- Expecting filters on fields not listed in
filterable. - Assuming
fieldsautomatically enforces DB-level select (it is response shaping). - Forgetting bind names: filters use model property names, not raw DB column names.
- Relying on
sortinput without whitelisting insortable.
See also: API, Request, ApiResponse, CrudResourceConfig, ActiveRecord, Resource.
Home · Setup · Configuration
Build with Pair 4
- Generator
- Controllers and responses
- Read models
- ActiveRecord and queries
- Forms and validation
- PairUI.js and HTML fragments
APIs and authentication
- API guide and CRUD
- Middleware and rate limits
- Users, sessions, and ACL
- Passkeys and social login
- Native mobile setup
Integrate and operate
- Integrations
- Runtime extensions and adapter keys
- Cache and cache stores
- Logs and observability
- PWA and push
- Packages and distribution
- Upgrade to v4