iwastenot / laravel-api-query-helper
A helper class for building API queries in Laravel
Requires
- php: >=7.3
- illuminate/support: ^8.0|^9.0|^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^6.0|^7.0|^8.0|^9.0|^10.0
- phpunit/phpunit: ^9.6|^10.5|^11.5|^12.0
This package is not auto-updated.
Last update: 2026-07-14 18:32:22 UTC
README
A structured query parser and Eloquent/Scout integration toolkit for Laravel APIs
The iWasteNot API Query Helper provides a consistent way to transform HTTP query parameters into robust Eloquent (or Laravel Scout) queries.
It standardizes filtering, sorting, field selection, and relationship inclusion for JSON APIs without requiring repetitive controller code.
๐งฑ Core Concept
Write API endpoints like:
GET /api/items?_with=photos&_fields=id,title,price&_sort=-created_at&price-gte=100&status-eq=active
and get this automatically:
Item::with(['photos'])
->select(['id', 'title', 'price'])
->where('price', '>=', 100)
->where('status', '=', 'active')
->orderBy('created_at', 'desc')
->get();
All query parsing, validation, and recursion are handled internally.
โ Compatibility
| Component | Supported versions |
|---|---|
| PHP | >= 7.4 |
| Laravel / Illuminate | 8.x, 9.x, 10.x, 11.x, 12.x |
Notes:
- When PHP is 7.4, only Laravel/Illuminate 8.x is supported.
โก Quick Start
composer install --no-interaction --prefer-dist
vendor/bin/phpunit
๐ Installation
composer require iwastenot/laravel-api-query-helper
Then import the helper where you build your API queries:
use IWasteNot\ApiQueryHelper\Services\ApiQueryHelper;
class ItemController extends Controller
{
public function index(Request $request, ApiQueryHelper $api)
{
$query = Item::query();
$results = $api->indexHelper($query, $request->all())
->paginate();
return new IndexCollection($results);
}
}
๐ Documentation
docs/tutorials/getting-started.mdโ walkthrough from model to requestdocs/how-to/filters.mdโ filter patterns + suffix listdocs/how-to/relations.mdโ nested_with,_fields,_sort, filtersdocs/how-to/scout.mdโ Scout search and_qdocs/ApiQueryHelper.mdโ reference for helper methodsdocs/ApiModel.mdโ reference for model helpersdocs/glossary.mdโ glossary of query termsdocs/faq.mdโ common questions and troubleshootingdocs/migration-notes.mdโ behavior changes between releasesdocs/developer-guide.mdโ contributor setup notes
โ๏ธ Supported Parameters
| Parameter | Type | Description |
|---|---|---|
_with | comma-separated list | Eager-load relationships (with()) |
_fields | comma-separated list | Restrict selected columns |
_sort | comma-separated list | Sort fields, prefix with - for descending; dotted paths (category-name) sort by related columns |
_q | string | Model-directed search across the model's $searchable fields |
_or[N][...] | bracketed groups | Or-groups: entries or-combine, groups and-combine with everything else |
| all others | filters | Translated into where*() clauses based on suffix |
๐ Filter Syntax
Filters are derived from all parameters except _with, _fields, _sort,
_q, and _or. Keys that start with _ are ignored by the filter parser.
| Example | Effect | Method |
|---|---|---|
status-eq=active | status = 'active' | where() |
price-gte=100 | price >= 100 | where() |
deleted-null | deleted IS NULL | whereNull() |
deleted-not-null | deleted IS NOT NULL | whereNotNull() |
id-in=1,2,3 | id IN (1,2,3) | whereIn() |
role-not-in=admin,staff | role NOT IN (...) | whereNotIn() |
created-between=2024-01-01,2024-12-31 | Date range | whereBetween() |
gte=price,100 | price >= 100 | where() |
status=active | status = 'active' | where() |
title-lk=%a%\|%b% | title LIKE '%a%' OR title LIKE '%b%' | grouped orWhere() |
category-name-lk=%x% | parent rows with a matching category | whereHas() |
_or[0][a-lk]=%x%&_or[0][b-lk]=%y% | (a LIKE '%x%' OR b LIKE '%y%') | nested where() |
Supported suffixes
The helper supports these suffixes by default:
lk,like=>where ... likenot-lk,not-like=>where ... not likegte=>where ... >=lte=>where ... <=gt=>where ... >lt=>where ... <eq=>where ... =not,not-eq=>where ... !=null=>whereNullnot-null=>whereNotNullin=>whereInnot-in=>whereNotInbetween=>whereBetweennot-between=>whereNotBetweendate=>whereDateyear=>whereYearmonth=>whereMonthday=>whereDaytime=>whereTimecolumn=>whereColumn
For null / not-null, the value list is ignored and no parameters are passed
to the where method.
Suffixes and aliases are normalized via a configurable $suffixes map.
In the suffix-as-key form (for example, gte=price,100), the first value is
the field name and the remaining values are parameters for the where clause.
In the suffix-in-key form, hyphens in the field portion are interpreted as dots
for nested relations (for example, photos-filename-like=image targets
photos.filename).
๐งฎ Eloquent Integration
Models intended for API use should extend the provided ApiModel class.
Example
use IWasteNot\ApiQueryHelper\Models\ApiModel;
class Item extends ApiModel
{
protected $fillable = ['title', 'price', 'status'];
protected $joinable = ['photos', 'tags'];
protected $details = ['description', 'notes'];
public function photos() { return $this->hasMany(Photo::class); }
public function tags() { return $this->belongsToMany(Tag::class); }
}
This gives ApiQueryHelper introspection capabilities through:
getJoinable()โ whitelist of relationships allowed in_withgetKeys()โ all known safe fieldsgetIndexKeys()โ indexable, visible subsetgetIndexAttributes()โ filtered attributes for display
๐ก Usage Examples
Basic filtering, sorting, and field selection
GET /api/items?_fields=id,title,price&_sort=-created_at&price-lte=100&status-eq=active
Item::select(['id','title','price'])
->where('price', '<=', 100)
->where('status', '=', 'active')
->orderBy('created_at', 'desc')
->get();
Eager loading and nested filtering
GET /api/items?_with=photos,tags&photos.filename-like=image
โ Loads related photos where the filename matches "image".
Nested relation fields and sorts
GET /api/items?_with=photos&photos._fields=id,filename&photos._sort=-created_at
โ Scopes _fields and _sort to the photos relation.
Full-text search with Laravel Scout
$results = $api->indexHelper(Item::class, ['_q' => 'hammer']);
When you call indexHelper() with a model class name, Scout mode is used only
when the params are limited to _q, per_page, and offset. The helper does
not apply pagination itself; those values are for your controller/paginator.
_q searches the fields that your Scout driver indexes for the model. Define
them by overriding toSearchableArray() and reindex after changes.
Custom prefiltering in models
public function indexHelperEloquent($query, $joins, $fields, $sorts, $filters)
{
$filters['status'][] = ['=', 'active'];
return [$query, $joins, $fields, $sorts, $filters];
}
See docs/ApiQueryHelper.md for extension-point details.
Show helper for single models
$item = Item::findOrFail($id);
$item = $api->showHelper($item, request()->all());
โ Loads _with relations without altering the base query.
๐งฉ Internal Structure
| File | Role |
|---|---|
ApiQueryHelper.php | Main query parser and orchestrator |
Arr.php | Array filtering utilities for dotted keys |
ApiModel.php | Base model with introspection hooks |
IndexCollection.php | (optional) Resource wrapper for paginated results |
๐งช Testing
vendor/bin/phpunit
โ ๏ธ Limitations
- Scout filters are not whitelisted via
getKeys(). - Pagination is controlled by your controller (the helper does not paginate).
๐ฉน Troubleshooting
- If Scout returns no results, confirm the driver is configured and the model index is built/rebuilt.
- If filters are ignored, ensure the field is present in
getKeys(). - If nested filters do nothing, confirm the parent relation is in
_with.
Changelog
See CHANGELOG.md for release notes and historical changes.
๐ License
MIT. See LICENSE.