derafu/query
Expressive path-based query builder for PHP from Derafu. Build queries using readable, JSONPath-like paths for nested data structures and collections. Lightweight package with full documentation at derafu.dev/docs/data/query.
Installation:
composer require derafu/query
Requires PHP 8.5+ and Laravel 10+ (for Eloquent integration).
First Use Case: Basic Filtering Query an Eloquent model with nested relationships:
use Derafu\Query\Query;
use App\Models\User;
$users = User::all()->toArray();
$query = Query::from($users)
->where('address.city', 'eq', 'Berlin')
->where('posts[*].status', 'eq', 'published')
->get();
Where to Look First:
src/QueryBuilder.php: Core logic for path resolution and SQL generation.tests/: Example use cases for filtering, projections, and aggregations.Workflow: Use Derafu for complex filters, then hydrate results back to Eloquent.
$rawData = User::with('posts')->get()->toArray();
$filtered = Query::from($rawData)
->where('posts[*].views > 100')
->get();
// Convert back to Eloquent collection if needed
Workflow: Parse frontend query params into path conditions.
$filters = [
'user.address.city' => 'Berlin',
'posts[*].status' => 'published',
];
$query = Query::from($users)
->where(...array_map(fn($val) => ["$val[0]", 'eq', $val[1]], $filters))
->get();
Pattern: Use Eloquent for relationships, Derafu for post-filtering.
$query = User::query()
->whereHas('posts', fn($q) => $q->where('status', 'published'))
->get()
->filter(fn($user) => Query::from($user->toArray())
->where('address.city', 'eq', 'Berlin')
->get()['users'][0] ?? false);
Workflow: Replace Doctrine queries with path-based syntax.
$em = $entityManager;
$users = $em->getRepository(User::class)->findAll();
$filtered = Query::from($users)
->where('address.city', 'eq', 'Paris')
->get();
Pattern: Use Derafu for pre-processing before Laravel aggregations.
$grouped = Query::from($users)
->groupBy('address.city')
->get();
// Pass to Laravel for further processing
Extension Point: Extend the query builder with domain-specific operators.
Query::extend('contains', function($query, $path, $value) {
return $query->where("{$path} LIKE '%{$value}%'");
});
// Usage:
$query->where('user.bio', 'contains', 'PHP');
SQL Injection Risks:
where('user.$id.address', ...)).// UNSAFE: User-controlled path
$path = $_GET['path'];
$query->where($path, 'eq', 'value'); // ❌
Performance with Deep Paths:
user.address.city.country) generate complex SQL.select() to limit fields:
$query->select('user.name', 'user.address.city');
Eloquent Hydration Issues:
->toArray() carefully:
// ❌ Breaks relationships
$users = User::all()->toArray();
$filtered = Query::from($users)->where(...)->get();
// ✅ Fix: Re-fetch from DB or hydrate manually
Circular References:
user.posts[*].author.user) cause infinite recursion.Case Sensitivity:
User.Name ≠ user.name).Inspect Generated SQL: Enable logging for raw SQL:
Query::enableLogging();
$query->where(...)->get(); // Logs SQL to storage/logs/derafu.log
Validate Paths:
Use Query::validatePath($path) to check syntax before execution.
Test Edge Cases:
where('user.address.city', 'eq', null)where('user.tags[*]', 'exists')where('user.metadata', 'type', 'string')Path Auto-Completion:
/**
* @property string $address->city
* @property Post[] $posts
*/
class User {}
Laravel Service Provider:
Query::macro('users', fn() => Query::from(User::all()->toArray()));
Caching:
$cachedUsers = cache()->remember('users', now()->addHour(), fn() => User::all());
$filtered = Query::from($cachedUsers)->where(...)->get();
Custom Data Sources:
Override Query::from() for non-array inputs (e.g., Doctrine collections):
Query::extendSource('doctrine', function($collection) {
return array_map(fn($entity) => $entity->toArray(), $collection);
});
// Usage:
Query::from($doctrineCollection, 'doctrine')->where(...);
Operator Aliases: Add shorthand for common operations:
Query::alias('gt', '>');
// Usage:
$query->where('user.age', 'gt', 25);
Path Normalization: Transform paths before execution (e.g., snake_case to camelCase):
Query::normalizePaths(fn($path) => Str::of($path)->snake()->__toString());
Query Monitoring: Hook into the query lifecycle:
Query::on('beforeExecute', fn($query) => logger()->info('Executing:', $query->getPath()));
How can I help you explore Laravel packages today?