happyr/doctrine-specification
Reusable Doctrine query Specifications for PHP. Replace messy repositories and huge QueryBuilder methods with small, composable, testable spec classes. Reduce duplication, avoid methods with many arguments, and extend queries cleanly as your app grows.
Specification classes, improving maintainability and testability.andX, orX, notX) for building complex queries dynamically, which is valuable for Laravel’s dynamic filtering needs (e.g., API filters, admin panels).match() method in repositories, though this requires additional abstraction.Specification objects into Eloquent query constraints.match(Specification $spec)) to bridge the gap.Spec class uses DQL (Doctrine Query Language), which is similar but not identical to Eloquent’s query syntax. Example:
// Doctrine DQL (package output)
Spec::eq('user.status', 'active', 'u')
// Eloquent equivalent
whereHas('user', fn($q) => $q->where('status', 'active'))
This mismatch requires manual mapping or a query builder adapter.BaseSpecification classes) unfamiliar compared to Eloquent’s fluent syntax.?status=active&role=admin). Assess whether the composability outweighs the integration effort.spatie/laravel-query-builder).fractal/api for dynamic filtering).// Before: Eloquent QueryBuilder
$query = User::query()->where('active', true);
if ($role) $query->where('role', $role);
// After: Specification
$spec = Spec::andX(
Spec::eq('active', true),
Spec::eq('role', $role) // Optional, dynamically added
);
$results = $this->userRepo->match($spec);
SpecificationRepository) to standardize match() usage.class EloquentSpecificationAdapter {
public static function toEloquent(Specification $spec): \Closure {
return fn($query) => /* map DQL to Eloquent */;
}
}
join() syntax differs from DQL’s JOIN ... ON.COUNT(DISTINCT ...) vs. Eloquent’s selectRaw().context for dynamic aliases; Eloquent uses table names directly.happyr/doctrine-specification package’s roadmap.$spec->getSpec()->getQuery()->getDQL(); // Log this for complex queries
doctrine.dbal.logging_enabled: true) to verify queries.SELECT *) can.CONCAT without proper escaping.Company object to OwnedByCompany spec.IS EMPTY).HAVING) may not translate cleanly.How can I help you explore Laravel packages today?