iwastenot/laravel-api-query-helper

A helper class for building API queries in Laravel

Maintainers

Package info

gitlab.com/iwastenot/apiqueryhelper

Issues

pkg:composer/iwastenot/laravel-api-query-helper

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 1

0.3.0 2026-07-14 14:45 UTC

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

ComponentSupported versions
PHP>= 7.4
Laravel / Illuminate8.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 request
  • docs/how-to/filters.md โ€” filter patterns + suffix list
  • docs/how-to/relations.md โ€” nested _with, _fields, _sort, filters
  • docs/how-to/scout.md โ€” Scout search and _q
  • docs/ApiQueryHelper.md โ€” reference for helper methods
  • docs/ApiModel.md โ€” reference for model helpers
  • docs/glossary.md โ€” glossary of query terms
  • docs/faq.md โ€” common questions and troubleshooting
  • docs/migration-notes.md โ€” behavior changes between releases
  • docs/developer-guide.md โ€” contributor setup notes

โš™๏ธ Supported Parameters

ParameterTypeDescription
_withcomma-separated listEager-load relationships (with())
_fieldscomma-separated listRestrict selected columns
_sortcomma-separated listSort fields, prefix with - for descending; dotted paths (category-name) sort by related columns
_qstringModel-directed search across the model's $searchable fields
_or[N][...]bracketed groupsOr-groups: entries or-combine, groups and-combine with everything else
all othersfiltersTranslated 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.

ExampleEffectMethod
status-eq=activestatus = 'active'where()
price-gte=100price >= 100where()
deleted-nulldeleted IS NULLwhereNull()
deleted-not-nulldeleted IS NOT NULLwhereNotNull()
id-in=1,2,3id IN (1,2,3)whereIn()
role-not-in=admin,staffrole NOT IN (...)whereNotIn()
created-between=2024-01-01,2024-12-31Date rangewhereBetween()
gte=price,100price >= 100where()
status=activestatus = 'active'where()
title-lk=%a%\|%b%title LIKE '%a%' OR title LIKE '%b%'grouped orWhere()
category-name-lk=%x%parent rows with a matching categorywhereHas()
_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 ... like
  • not-lk, not-like => where ... not like
  • gte => where ... >=
  • lte => where ... <=
  • gt => where ... >
  • lt => where ... <
  • eq => where ... =
  • not, not-eq => where ... !=
  • null => whereNull
  • not-null => whereNotNull
  • in => whereIn
  • not-in => whereNotIn
  • between => whereBetween
  • not-between => whereNotBetween
  • date => whereDate
  • year => whereYear
  • month => whereMonth
  • day => whereDay
  • time => whereTime
  • column => 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 _with
  • getKeys() โ€” all known safe fields
  • getIndexKeys() โ€” indexable, visible subset
  • getIndexAttributes() โ€” 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

FileRole
ApiQueryHelper.phpMain query parser and orchestrator
Arr.phpArray filtering utilities for dotted keys
ApiModel.phpBase 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.