Forge powerful APIs with advanced filtering, pagination, and field selection for Laravel applications
marcosbrendon/apiforge is a Laravel package for forge powerful apis with advanced filtering, pagination, and field selection for laravel applications.
It currently has 0 GitHub stars and 2 downloads on Packagist.
Install it with composer require marcosbrendon/apiforge.
Discover more Laravel packages by marcosbrendon
or browse all Laravel packages to compare alternatives.
Last updated
Forge powerful APIs with advanced filtering, pagination, and field selection capabilities for Laravel applications. Build sophisticated APIs with minimal configuration and maximum performance.
composer require marcosbrendon/apiforge
use MarcosBrendon\\ApiForge\\Traits\\HasAdvancedFilters;
class UserController extends Controller
{
use HasAdvancedFilters;
protected function getModelClass(): string
{
return User::class;
}
public function index(Request $request)
{
return $this->indexWithFilters($request);
}
}
protected function setupFilterConfiguration(): void
{
$this->configureFilters([
'name' => [
'type' => 'string',
'operators' => ['eq', 'like', 'ne'],
'searchable' => true,
'sortable' => true
],
'email' => [
'type' => 'string',
'operators' => ['eq', 'like'],
'searchable' => true
],
'created_at' => [
'type' => 'datetime',
'operators' => ['gte', 'lte', 'between'],
'sortable' => true
]
]);
}
protected function setupFilterConfiguration(): void
{
// Configure regular filters
$this->configureFilters([
'name' => ['type' => 'string', 'operators' => ['eq', 'like']],
'email' => ['type' => 'string', 'operators' => ['eq', 'like']]
]);
// Configure virtual fields
$this->configureVirtualFields([
'full_name' => [
'type' => 'string',
'callback' => fn($user) => trim($user->first_name . ' ' . $user->last_name),
'dependencies' => ['first_name', 'last_name'],
'operators' => ['eq', 'like'],
'searchable' => true,
'sortable' => true
]
]);
// Configure model hooks
$this->configureModelHooks([
'beforeStore' => [
'generateSlug' => fn($model) => $model->slug = Str::slug($model->title)
],
'afterStore' => [
'sendNotification' => fn($model) => NotificationService::send($model)
]
]);
}
# Basic filtering
GET /api/users?name=John&email=*@gmail.com
# Virtual field filtering
GET /api/users?full_name=John*&fields=id,full_name
# Field selection
GET /api/users?fields=id,name,email
# Pagination
GET /api/users?page=2&per_page=20
# Advanced filtering
GET /api/users?name=John*&created_at=>=2024-01-01&sort_by=name
# Relationship filtering
GET /api/users?fields=id,name,company.name&company.active=true
| Operator | Description | Example |
|----------|-------------|---------|
| eq | Equals | name=John |
| ne | Not equals | name=!=John |
| like | Contains (use * as wildcard) | name=John* |
| not_like | Does not contain | name=!*John |
| gt | Greater than | age=>18 |
| gte | Greater than or equal | age=>=18 |
| lt | Less than | age=<65 |
| lte | Less than or equal | age=<=65 |
| in | In array | status=active,pending |
| not_in | Not in array | status=!=active,pending |
| between | Between values | age=18\\|65 |
| null | Is null | deleted_at=null |
| not_null | Is not null | deleted_at=!null |
Optimize your API responses by selecting only the fields you need:
# Basic field selection
GET /api/users?fields=id,name,email
# Include relationships
GET /api/users?fields=id,name,company.name,company.city
# Use aliases
GET /api/users?fields=user_id,user_name,user_email
The package provides automatic pagination with comprehensive metadata:
{
"success": true,
"data": [...],
"pagination": {
"current_page": 1,
"per_page": 15,
"total": 150,
"last_page": 10,
"from": 1,
"to": 15,
"has_more_pages": true,
"prev_page_url": null,
"next_page_url": "/api/users?page=2"
},
"filters": {
"active": {
"name": "John*",
"status": "active"
},
"sorting": {
"sort_by": "created_at",
"sort_direction": "desc"
}
}
}
Configure filters with detailed metadata:
$this->configureFilters([
'status' => [
'type' => 'enum',
'values' => ['active', 'inactive', 'pending'],
'operators' => ['eq', 'in'],
'description' => 'User account status',
'example' => [
'eq' => 'status=active',
'in' => 'status=active,pending'
]
],
'created_at' => [
'type' => 'datetime',
'operators' => ['gte', 'lte', 'between'],
'format' => 'Y-m-d H:i:s',
'sortable' => true,
'description' => 'User registration date'
]
]);
Add the middleware to automatically validate and sanitize requests:
// In your route group
Route::group(['middleware' => ['api', 'apiforge']], function () {
Route::get('/users', [UserController::class, 'index']);
});
Get filter metadata and examples automatically:
# Get available filters and configuration
GET /api/users/metadata
# Get usage examples
GET /api/users/examples
Virtual fields are computed fields that don't exist in your database but can be filtered, sorted, and selected like regular fields. They're calculated on-demand using custom callback functions.
protected function setupFilterConfiguration(): void
{
$this->configureVirtualFields([
'display_name' => [
'type' => 'string',
'callback' => function($user) {
return $user->name ?: $user->email;
},
'dependencies' => ['name', 'email'],
'operators' => ['eq', 'like'],
'searchable' => true,
'sortable' => true
]
]);
}
'order_count' => [
'type' => 'integer',
'callback' => function($user) {
return $user->orders_count ?? $user->orders->count();
},
'relationships' => ['orders'],
'operators' => ['eq', 'gt', 'gte', 'lt', 'lte'],
'cacheable' => true,
'cache_ttl' => 1800
],
'total_spent' => [
'type' => 'float',
'callback' => function($user) {
return $user->orders->sum('total');
},
'relationships' => ['orders'],
'operators' => ['eq', 'gt', 'gte', 'lt', 'lte', 'between'],
'cacheable' => true,
'cache_ttl' => 3600
]
'customer_tier' => [
'type' => 'enum',
'values' => ['bronze', 'silver', 'gold', 'platinum'],
'callback' => function($user) {
$totalSpent = $user->orders->sum('total');
if ($totalSpent >= 10000) return 'platinum';
if ($totalSpent >= 5000) return 'gold';
if ($totalSpent >= 1000) return 'silver';
return 'bronze';
},
'relationships' => ['orders'],
'operators' => ['eq', 'in', 'ne'],
'cacheable' => true,
'cache_ttl' => 3600
],
'age' => [
'type' => 'integer',
'callback' => function($user) {
return $user->birth_date ?
Carbon::parse($user->birth_date)->age : null;
},
'dependencies' => ['birth_date'],
'operators' => ['eq', 'gt', 'gte', 'lt', 'lte', 'between'],
'nullable' => true
]
| Option | Type | Description |
|--------|------|-------------|
| type | string | Field type (string, integer, float, boolean, enum, datetime) |
| callback | callable | Function to compute the field value |
| dependencies | array | Database fields required for computation |
| relationships | array | Eloquent relationships to eager load |
| operators | array | Allowed filter operators |
| cacheable | boolean | Enable caching for computed values |
| cache_ttl | integer | Cache time-to-live in seconds |
| nullable | boolean | Allow null values |
| default_value | mixed | Default value when computation fails |
| searchable | boolean | Include in search operations |
| sortable | boolean | Allow sorting by this field |
# Filter by virtual fields
GET /api/users?customer_tier=gold&age=>=25
# Sort by virtual fields
GET /api/users?sort_by=total_spent&sort_direction=desc
# Select virtual fields
GET /api/users?fields=id,name,customer_tier,total_spent
# Combine with regular fields
GET /api/users?name=John*&customer_tier=gold,platinum&fields=id,name,total_spent
Model hooks provide lifecycle callbacks that execute custom logic during CRUD operations. They're perfect for audit logging, notifications, data validation, and business rule enforcement.
beforeStore - Before creating a new recordafterStore - After successfully creating a recordbeforeUpdate - Before updating an existing recordafterUpdate - After successfully updating a recordbeforeDelete - Before deleting a record (can prevent deletion)afterDelete - After successfully deleting a recordprotected function setupFilterConfiguration(): void
{
$this->configureModelHooks([
'beforeStore' => [
'generateSlug' => function($model, $context) {
if (empty($model->slug) && !empty($model->title)) {
$model->slug = Str::slug($model->title);
}
},
'validateBusinessRules' => function($model, $context) {
if ($model->type === 'premium' && !$model->user->isPremium()) {
throw new ValidationException('User must be premium');
}
}
],
'afterStore' => [
'sendNotification' => function($model, $context) {
NotificationService::send($model->user, 'ItemCreated', $model);
},
'updateCache' => function($model, $context) {
Cache::forget("user_items_{$model->user_id}");
}
]
]);
}
'afterStore' => [
'criticalNotification' => [
'callback' => function($model, $context) {
// Critical notification logic
},
'priority' => 1 // Higher priority (executes first)
],
'regularNotification' => [
'callback' => function($model, $context) {
// Regular notification logic
},
'priority' => 10 // Lower priority (executes later)
]
]
'beforeStore' => [
'validatePremiumFeatures' => [
'callback' => function($model, $context) {
// Validation logic for premium features
},
'conditions' => [
'field' => 'type',
'operator' => 'eq',
'value' => 'premium'
]
]
]
'beforeDelete' => [
'checkPermissions' => [
'callback' => function($model, $context) {
if (!auth()->user()->canDelete($model)) {
throw new UnauthorizedException('Cannot delete this resource');
}
return true; // Allow deletion
},
'stopOnFailure' => true
],
'checkDependencies' => function($model, $context) {
if ($model->orders()->exists()) {
throw new ValidationException('Cannot delete user with existing orders');
}
return true;
}
]
Hooks receive a context object with useful information:
'afterUpdate' => [
'trackChanges' => function($model, $context) {
// Access request data
$request = $context->request;
// Access the operation type
$operation = $context->operation; // 'store', 'update', 'delete'
// Access additional data
$changes = $context->get('changes', []);
// Log the changes
AuditLog::create([
'model_type' => get_class($model),
'model_id' => $model->id,
'changes' => $model->getDirty(),
'user_id' => auth()->id(),
'ip_address' => $request->ip()
]);
}
]
'beforeUpdate' => [
'auditChanges' => function($model, $context) {
$changes = $model->getDirty();
if (!empty($changes)) {
AuditLog::create([
'model_type' => get_class($model),
'model_id' => $model->id,
'changes' => $changes,
'user_id' => auth()->id()
]);
}
}
]
'beforeStore' => [
'setCreatedBy' => fn($model) => $model->created_by = auth()->id()
],
'beforeUpdate' => [
'setUpdatedBy' => fn($model) => $model->updated_by = auth()->id()
]
'afterStore' => [
'clearCache' => fn($model) => Cache::tags(['users'])->flush()
],
'afterUpdate' => [
'updateCache' => function($model, $context) {
Cache::forget("user_{$model->id}");
Cache::put("user_{$model->id}", $model, 3600);
}
]
'afterDelete' => [
'cleanupFiles' => function($model, $context) {
if ($model->avatar) {
Storage::delete($model->avatar);
}
if ($model->documents) {
foreach ($model->documents as $doc) {
Storage::delete($doc->path);
}
}
}
]
Extend the base controller for common functionality:
use MarcosBrendon\\ApiForge\\Http\\Controllers\\BaseApiController;
class ApiController extends BaseApiController
{
protected function getDefaultPerPage(): int
{
return 25;
}
protected function getMaxPerPage(): int
{
return 500;
}
}
Configure field selection with aliases and validation:
protected function configureFieldSelection(): void
{
$this->fieldSelection([
'selectable_fields' => ['id', 'name', 'email', 'company.name'],
'required_fields' => ['id'],
'blocked_fields' => ['password', 'remember_token'],
'default_fields' => ['id', 'name', 'email'],
'field_aliases' => [
'user_id' => 'id',
'user_name' => 'name',
'company_name' => 'company.name'
],
'max_fields' => 50
]);
}
Enable query caching for better performance:
public function index(Request $request)
{
return $this->indexWithFilters($request, [
'cache' => true,
'cache_ttl' => 3600, // 1 hour
'cache_tags' => ['users', 'api']
]);
}
Configure advanced features in your controller:
protected function setupFilterConfiguration(): void
{
// Regular filters
$this->configureFilters([
'name' => ['type' => 'string', 'operators' => ['eq', 'like']],
'status' => ['type' => 'enum', 'values' => ['active', 'inactive']]
]);
// Virtual fields with caching
$this->configureVirtualFields([
'full_name' => [
'type' => 'string',
'callback' => fn($user) => trim($user->first_name . ' ' . $user->last_name),
'dependencies' => ['first_name', 'last_name'],
'operators' => ['eq', 'like'],
'cacheable' => true,
'cache_ttl' => 3600
],
'customer_tier' => [
'type' => 'enum',
'values' => ['bronze', 'silver', 'gold', 'platinum'],
'callback' => [$this, 'calculateCustomerTier'],
'relationships' => ['orders'],
'cacheable' => true,
'cache_ttl' => 1800
]
]);
// Model hooks with priorities
$this->configureModelHooks([
'beforeStore' => [
'validateData' => [
'callback' => [$this, 'validateBusinessRules'],
'priority' => 1
],
'generateSlug' => [
'callback' => fn($model) => $model->slug = Str::slug($model->title),
'priority' => 5
]
],
'afterStore' => [
'sendNotification' => fn($model) => NotificationService::send($model),
'updateCache' => fn($model) => Cache::forget("users_list")
]
]);
}
private function calculateCustomerTier($user)
{
$totalSpent = $user->orders->sum('total');
if ($totalSpent >= 10000) return 'platinum';
if ($totalSpent >= 5000) return 'gold';
if ($totalSpent >= 1000) return 'silver';
return 'bronze';
}
private function validateBusinessRules($model, $context)
{
if ($model->type === 'premium' && !$model->user->isPremium()) {
throw new ValidationException('User must be premium for this type');
}
}
Virtual fields include several performance optimization features:
'expensive_calculation' => [
'type' => 'float',
'callback' => function($model) {
// Expensive computation
return $this->performComplexCalculation($model);
},
'cacheable' => true,
'cache_ttl' => 3600, // Cache for 1 hour
'relationships' => ['orders', 'payments']
]
'user_summary' => [
'type' => 'string',
'callback' => function($model) {
return "{$model->name} ({$model->email}) - {$model->orders_count} orders";
},
'dependencies' => ['name', 'email'], // Only load these fields
'relationships' => ['orders'] // Eager load with count
]
Virtual fields are automatically computed in batches for better performance:
// This will compute virtual fields for all users in a single batch
GET /api/users?fields=id,name,customer_tier&per_page=100
Virtual fields are only computed when requested:
// Virtual fields not computed (not requested)
GET /api/users?fields=id,name,email
// Virtual fields computed only for selected fields
GET /api/users?fields=id,name,customer_tier,total_spent
Configure performance limits in your config file:
// config/apiforge.php
'virtual_fields' => [
'cache' => [
'enabled' => true,
'default_ttl' => 3600,
'driver' => 'redis' // or 'database', 'file'
],
'performance' => [
'memory_limit' => '256M',
'time_limit' => 30, // seconds
'batch_size' => 100,
'max_virtual_fields' => 20
]
]
Model hooks are designed for minimal performance impact:
'expensiveHook' => [
'callback' => function($model, $context) {
// Only run for specific conditions
$this->performExpensiveOperation($model);
},
'conditions' => [
'field' => 'type',
'operator' => 'eq',
'value' => 'premium'
]
]
'afterStore' => [
'sendEmail' => function($model, $context) {
// Queue for background processing
SendWelcomeEmail::dispatch($model)->delay(now()->addMinutes(5));
}
]
Use field selection to reduce data transfer:
GET /api/users?fields=id,name,email,customer_tier
Enable caching for expensive virtual fields:
'cacheable' => true,
'cache_ttl' => 3600
Optimize database queries with proper indexing:
// Add indexes for fields used in virtual field dependencies
Schema::table('users', function (Blueprint $table) {
$table->index(['first_name', 'last_name']);
$table->index('birth_date');
});
Use relationship counting instead of loading full relationships:
'order_count' => [
'callback' => function($user) {
return $user->orders_count ?? $user->orders()->count();
}
]
Configure appropriate pagination:
'pagination' => [
'default_per_page' => 15,
'max_per_page' => 100,
]
composer test
ApiForge is production-ready with:
Enable caching for better performance:
// config/apiforge.php
'cache' => [
'enabled' => true,
'ttl' => 3600,
],
Use field selection to reduce data transfer:
GET /api/users?fields=id,name,email
Configure appropriate pagination:
'pagination' => [
'default_per_page' => 15,
'max_per_page' => 100,
],
Please see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File for more information.
This package was born from the need to create sophisticated APIs with advanced filtering capabilities while maintaining clean, maintainable code. It combines the power of Laravel's Eloquent ORM with a flexible, configuration-driven approach to API development.
full_name, age, customer_tier that can be filtered, sorted, and selectedbeforeStore, afterStore, beforeUpdate, afterUpdate, beforeDelete, afterDeleteMade with β€οΈ by Marcos Brendon