yassinedabbous / laravel-dynamic-query
Build query dynamicaly with your API
Package info
github.com/YassineDabbous/laravel-dynamic-query
pkg:composer/yassinedabbous/laravel-dynamic-query
Requires
- php: >=7.4
- laravel/framework: ^5.5|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0
Requires (Dev)
- orchestra/testbench: ^9.0
README
Build powerful, dynamic API queries from URL parameters.
Let your API consumers select fields, filter, sort, group, paginate, and compute statistics — all from a single query string.
Table of Contents
- Features
- Requirements
- Installation
- Quick Start
- Usage Overview
- All-in-One Scopes
- Configuration
- Detailed Documentation
- Security
- Testing
- License
Features
| Feature | Description |
|---|---|
| Field Selection | Clients choose which columns, relations, and appends to include in the response |
| Filtering | 30+ operators including like, between, in, null, JSON operators, full-text search, and relation existence checks |
| Sorting | Multi-column sorting with ascending/descending via - prefix |
| Grouping | Group by columns or date macros (year, month, day, hour) with timezone support |
| Statistics | Aggregate metrics (count, sum, avg, min, max), custom SQL metrics, cumulative/growth transforms, and period-over-period comparison |
| Pagination | Configurable paginate / simplePaginate / get all with per-page limits |
| Smart Joins | Automatic JOIN generation from dot-notation (user.email) on filters, sorts, and groups |
| Date Presets | Semantic date ranges like today, this_week, last_30_days, ytd |
| Auto Relations | Discover model relations via PHP Reflection — zero configuration |
| Security | Strict whitelist-based filtering, SQL alias sanitization, and configurable defaults |
Requirements
- PHP >= 8.0
- Laravel 8.x / 9.x / 10.x / 11.x
Installation
composer require yassinedabbous/laravel-dynamic-query
Publish the configuration file (optional):
php artisan vendor:publish --tag=dynamic-query-config
The service provider is auto-discovered by Laravel.
Quick Start
1. Add the trait to your model
use YassineDabbous\DynamicQuery\HasDynamicQuery; class Product extends Model { use HasDynamicQuery; public function dynamicColumns(): array { return ['id', 'name', 'price', 'category_id', 'created_at']; } public function dynamicFilters(): array { return [ 'name' => ['=', 'like%'], 'price' => ['=', '>', '<', '>=', '<=', 'between'], 'category_id' => ['=', 'in'], 'created_at' => null, // all operators ]; } public function dynamicSorts(): array { return ['id', 'name', 'price', 'created_at']; } }
2. Use it in your controller
class ProductController extends Controller { public function index() { // All-in-one: select, filter, sort, group, paginate, append return Product::dynamicAPI(); // Or step-by-step: return Product::dynamicSelect() ->dynamicFilter() ->dynamicSort() ->dynamicPaginate(); } }
3. Call your API
GET /api/products?_fields=id,name,price&name=Widget&_sort=-price&per_page=10
Response includes only id, name, price — filtered by name, sorted by price descending, 10 per page.
Usage Overview
Dynamic Fields
Select specific columns, relations, and nested relation fields:
GET /api/products?_fields=id,name,category:id|name
Declare what's available on your model:
public function dynamicColumns(): array { return ['id', 'name', 'price', 'category_id']; } public function dynamicRelations(): array { return [ 'category' => 'category_id', // depends on category_id column 'reviews' => null, // no column dependency ]; } public function dynamicAppends(): array { return [ 'full_name' => ['first_name', 'last_name'], // depends on columns 'status_label' => 'status', // depends on status column ]; } public function dynamicAggregates(): array { return [ 'reviews_count' => fn($q) => $q->withCount('reviews'), ]; }
Dynamic Filtering
Filter with any of 30+ operators:
GET /api/products?price=>=100&name=Widget&_operators[name]=like%
Negate with ! prefix:
GET /api/products?!category_id=5 # category_id != 5
GET /api/products?status=active&_logic=or # OR logic across filters
Supports: =, !=, <, >, <=, >=, like, %like%, in, between, null, has, full_text, json_contains, json_overlaps, and more.
📖 Full Filtering Documentation →
Dynamic Sorting
Sort by one or more columns. Prefix with - for descending:
GET /api/products?_sort=price # ASC
GET /api/products?_sort=-price,name # price DESC, then name ASC
GET /api/products?_sort=user.name # sort by related column (auto-join)
📖 Full Sorting Documentation →
Dynamic Grouping
Group results with optional date macros:
GET /api/products?_group=category_id
GET /api/orders?_group=created_at:month&_timezone=America/New_York
GET /api/orders?_group=created_at:year,status
Supported macros: year, month, day, hour.
📖 Full Grouping Documentation →
Dynamic Statistics
Compute metrics with grouping, transforms, comparisons, and caching:
GET /api/orders?_metric=sum:total&_group=created_at:month
GET /api/orders?_metric=avg:total&_transform=growth
GET /api/orders?_metric=count&_compare=previous_period&created_at[]=2024-01-01&created_at[]=2024-03-31
📖 Full Statistics Documentation →
Dynamic Pagination
GET /api/products?per_page=25 # 25 items per page
GET /api/products?_get_all=true # return all (if enabled)
GET /api/products?_simple=true # use simplePaginate
📖 Full Pagination Documentation →
All-in-One Scopes
| Scope | Description |
|---|---|
dynamicQuery() |
Applies Select + Filter + Sort + Group |
dynamicAPI() |
Applies Select + Filter + Sort + Group + Paginate + Append |
// In a controller: return Product::dynamicAPI(); // With explicit input (e.g. in a job or test): return Product::dynamicAPI(['_fields' => 'id,name', '_sort' => '-price']);
Configuration
After publishing, edit config/dynamic-query.php:
return [ 'defaults' => [ 'per_page' => 15, 'max_per_page' => 100, 'allow_get_all' => false, 'max_get_all' => 1000, 'cache_ttl' => 600, 'timezone' => 'UTC', ], 'settings' => [ 'enable_stats_cache' => false, 'relation_guess' => true, // Auto-discover relations via Reflection 'strict_filtering' => true, // Whitelist-only filtering 'clean_response' => true, // Limit response to requested fields ], 'params' => [ 'fields' => '_fields', 'sort' => '_sort', 'logic' => '_logic', // ... and more ], ];
📖 Full Configuration Reference →
Detailed Documentation
| Document | Topics |
|---|---|
| Installation | Install, publish config, service provider |
| Configuration | All config options, parameter names, defaults |
| Fields & Selection | Columns, relations, appends, aggregates, deep fields, response cleaning |
| Filtering | All operators, negation, OR logic, smart joins, named scopes, date presets, JSON operators |
| Sorting | Multi-column, direction prefix, related-column sorting |
| Grouping | Standard grouping, date macros, timezone, SQL generation |
| Statistics & Metrics | Aggregates, custom metrics, transforms, period comparison, caching, Stats API |
| Pagination | Per-page, get-all, simple paginate, limits |
| Advanced | Smart joins, auto-relation discovery, DynamicQueryable contract, morph model resolution, programmatic input |
Security
This package follows a whitelist-first security approach:
- Strict Filtering — Only filters defined in
dynamicFilters()are applied (enabled by default). - Column Whitelisting — Only columns listed in
dynamicColumns()can be selected. - SQL Sanitization — All dynamically generated aliases are sanitized via regex.
- Custom Metrics Safety —
dynamicMetrics()values are developer-defined SQL, never user input. - Pagination Limits —
max_per_pageandmax_get_allprevent resource exhaustion.
Testing
composer test
License
The MIT License (MIT). See LICENSE for details.