Search by

brnrajoriya / laravel-queryflow

brnrajoriya

QueryFlow: turn any Eloquent model into a safe, filterable, sortable, searchable and paginated REST list endpoint - driven by a per-model whitelist.

Package info

github.com/brnrajoriya/laravel-queryflow

pkg:composer/brnrajoriya/laravel-queryflow

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-09-25 04:44 UTC

This package is auto-updated.

Last update: 2026-09-25 04:58:49 UTC


README

Tests Latest Version License: MIT

QueryFlow turns any Eloquent model into a complete REST list endpoint - pagination, sorting, keyword search, filters, relations, counts and aggregates - from one whitelist on the model.

#[Queryable(
    filterable: ['id', 'status', 'author_id', 'views', 'created_at'],
    sortable:   ['id', 'title', 'views', 'created_at'],
    searchable: ['title', 'body', 'author.name'],
    includable: ['author', 'comments'],
)]
class Post extends Model
{
    use HasQueryFlow;
}
public function index(QueryFlowRequest $request)
{
    return Post::flow()->apply($request->queryFlow())->get();
}
GET /posts?keyword=laravel&filter[status]=published&with=author&with_count=comments&order_by=views&per_page=20
GET /posts?return_type=count&group_by=status          → [{"status":"draft","count":4}, ...]

Created by Bhaskar Rajoriya. QueryFlow started in 2020 as the addOperationsInQuery() helper used across production Laravel projects, and is now a tested, open-source package.

Why QueryFlow

  • One declaration per model - every list endpoint in your API behaves the same way.
  • Safe by default - only whitelisted columns and relations are accepted; unknown input is a 422, never raw SQL.
  • Your constraints stay yours - client filters are wrapped in one group, so or_where can never escape a where('user_id', $me) you added.
  • Fast - simple / cursor pagination for big tables, with_count instead of loading relations, stable ordering, capped page sizes.
  • Portable - tested on SQLite, MySQL 8.4 and PostgreSQL 17, with Laravel 12 and 13.

Installation

composer require brnrajoriya/laravel-queryflow
php artisan vendor:publish --tag=queryflow-config   # optional

Requires PHP 8.3+ and Laravel 12.40+ or 13.

Declaring what a model exposes

With the attribute:

use BrnRajoriya\QueryFlow\Attributes\Queryable;
use BrnRajoriya\QueryFlow\Concerns\HasQueryFlow;

#[Queryable(filterable: [...], sortable: [...], searchable: [...], includable: [...])]
class Post extends Model
{
    use HasQueryFlow; // optional: adds Post::flow() and $post->loadFlow()
}

Or with properties:

class Post extends Model
{
    protected array $filterable = ['id', 'status', 'created_at'];
    protected array $sortable   = ['id', 'title'];
    protected array $searchable = ['title', 'author.name'];
    protected array $includable = ['author', 'comments'];
}
List Used by Default
filterable filter, operations, select, group_by, aggregates key + fillable + timestamps, minus $hidden
sortable order_by filterable
searchable keyword (relation.column searches a relation) none
includable with, with_count, where_has, has, doesnt_have none

Using it

use BrnRajoriya\QueryFlow\QueryFlow;
use BrnRajoriya\QueryFlow\Params;

// From a FormRequest (validation runs before the controller, API doc tools can read the rules)
$result = Post::flow()->apply($request->queryFlow())->get();

// From any array (validates too)
$result = QueryFlow::for(Post::class)->apply($request->query())->get();

// Keep your own constraints - client filters cannot escape them
$result = QueryFlow::for(Post::query()->where('user_id', $request->user()->id))
    ->apply($request->queryFlow())
    ->get();

// Relations work too
$result = QueryFlow::for($user->posts())->apply($params)->get();

// Single record: ?with=author&with_count=comments
$post->loadFlow($request->query());

get() returns:

Request Result
default LengthAwarePaginator
pagination=simple Paginator (no COUNT(*) query)
pagination=cursor CursorPaginator (fastest for large tables / infinite scroll)
return_type=count int
return_type=sum|avg|min|max + aggregate_column int|float
any aggregate + group_by [['status' => 'draft', 'count' => 4], ...]

Extend QueryFlowRequest to add your own rules:

class IndexRequest extends QueryFlowRequest
{
    public function rules(): array
    {
        return [...parent::rules(), 'status' => ['sometimes', 'string']];
    }
}

Query parameters

Parameter Example Notes
page 2
per_page 25 default 25, max 100 (configurable)
pagination paginate | simple | cursor cursor uses the cursor parameter
order_by views or views,title sortable only; key is added as tie breaker
order_type desc or desc,asc one value for all columns, or one per column
keyword laravel case-insensitive; % and _ are literal
filter[col] filter[status]=published equals; filter[status][]=a&filter[status][]=b = in
operations[] see below up to 25, nested up to 3 levels
select title,views or select[]=title key and needed foreign keys are added automatically
with author,comments or author.company includable only
with_count comments adds comments_count
trashed with | only models using SoftDeletes
return_type data | count | sum | avg | min | max
aggregate_column views required for sum / avg / min / max
group_by status only with an aggregate return_type

Every name can be changed in config/queryflow.php ('order_by' => 'sort_by') to match an existing frontend.

Operations

// e.g. with qs.stringify(params)
{
  operations: [
    { code: 'where', parameters: { column: 'status', operator: '=', value: 'published' } },
    { code: 'where_between', parameters: { column: 'created_at', values: ['2026-01-01', '2026-12-31'] } },
    { code: 'or_group', operations: [                                   // ... OR (views > 100 AND body IS NULL)
      { code: 'where', parameters: { column: 'views', operator: '>', value: 100 } },
      { code: 'where_null', parameters: { column: 'body' } },
    ]},
    { code: 'where_has', relation: 'author', parameters: { column: 'name', operator: 'like', value: 'Jo%' } },
  ],
}
Parameters Codes
column, operator, value where, or_where, where_date, where_month, where_day, where_year, where_time
column, values[] where_in, where_not_in, or_where_in, or_where_not_in
column, values[2] where_between, or_where_between, where_not_between, or_where_not_between
column where_null, where_not_null, or_where_null, or_where_not_null
column_1, operator, column_2 where_column, or_where_column
relation (+ optional column/operator/value) where_has, or_where_has, has, doesnt_have
operations[] group, or_group

Operators: =, !=, <>, <, <=, >, >=, like, not like (configurable).

Existing projects can keep calling addOperationsInQuery($query, $operations).

Security

  • Column, relation and operator names are checked against whitelists; values are always bindings.
  • Relation names are only resolved after the whitelist check, so a request can never call arbitrary model methods (with=delete).
  • $hidden columns (e.g. password) are never filterable by default.
  • Limits on page size, operations, nesting depth, relations and selected columns (config).

Performance tips

  • Add database indexes for the columns in filterable / sortable you actually filter and sort on.
  • Use pagination=cursor (or simple) for large or append-only tables.
  • Prefer with_count over with when the client only shows a number.
  • %keyword% search cannot use a normal index; for large tables consider Laravel Scout / full-text search.

Testing

composer test                       # SQLite
DB_DRIVER=mysql DB_PORT=3306 composer test
DB_DRIVER=pgsql DB_PORT=5432 DB_USERNAME=postgres composer test

Credits & license

QueryFlow is created and maintained by Bhaskar Rajoriya (LinkedIn).

Released under the MIT License: free to use in personal and commercial projects. Please keep the copyright notice. If QueryFlow helps your project, a mention of "QueryFlow by Bhaskar Rajoriya" or a GitHub star is appreciated - GitHub's "Cite this repository" button uses CITATION.cff.

A ready-to-use API that uses QueryFlow: Laravel API Boilerplate.