rikbruil/specification
Implements the Specification pattern for Laravel, letting you encapsulate query constraints and business rules as reusable, composable classes. Combine specifications with and/or/not operators and apply them directly to Eloquent queries for cleaner, testable filtering logic.
The package implements the classical Specification pattern: you encapsulate a business rule in a class with a single isSatisfiedBy($candidate): bool method, then compose rules with and, or, and not. Start by pulling it in and opening the base interface and any composite classes first — those tell you the exact method names and generic types the package expects before you write your first rule.
composer require rikbruil/specification
Define a concrete rule by implementing the interface (or extending any provided abstract base):
use Rikbruil\Specification\Specification;
final class IsActive implements Specification
{
public function isSatisfiedBy($user): bool
{
return $user->active === true;
}
}
The first use case to try is filtering an in-memory collection before touching repos or queries — it lets you validate the rule and its composites in seconds:
$active = array_filter($users, fn ($u) => (new IsActive())->isSatisfiedBy($u));
From there, promote the rule into a repository or query builder where it actually earns its keep.
Compose, don't inherit. The value of the pattern comes from combining small rules. Build a small library of atomic specs (IsActive, HasVerifiedEmail, CreatedSince) and compose them:
$spec = (new IsActive())
->and(new HasVerifiedEmail())
->or(new IsAdmin());
Check the package’s composite operators (typically and, or, not, and sometimes andNot/xor) and use the short-circuiting variants where the underlying candidate may be expensive to inspect.
Separate "collection" specs from "query" specs. A rule that runs in PHP (isSatisfiedBy) is specification-by-candidate. A rule that produces a query constraint (where, Doctrine Criteria, Eloquent Builder) is specification-by-query. Many packages expose both; if this one only ships the in-memory variant, keep that mental split and translate the same rule into scopes/Criteria on the persistence side so you don’t accidentally hydrate the entire table.
Repository integration. The cleanest day-to-day pattern is to hand a spec to a repository, not to leak isSatisfiedBy to callers:
final class UserRepository
{
public function matching(Specification $spec): array
{
return array_values(array_filter(
$this->all(),
fn ($u) => $spec->isSatisfiedBy($u)
));
}
}
Callers then say $users->matching($spec) and stay ignorant of the evaluation strategy.
Test specs in isolation. Because each spec is a pure predicate on a value object, unit tests are trivial and fast — assert isSatisfiedBy against 3–4 representative candidates (match, no-match, boundary, null). Only the composites need integration tests, and you can usually prove those with a truth-table.
Reuse specs as policy objects. The pattern is a natural fit for authorization (CanPublish, OwnsResource), eligibility (EligibleForDiscount), and DTO validation. Reusing the same spec class across these contexts is where the pattern pays off.
isSatisfiedBy($candidate) will often be called with null when filtering optional relations. Add an explicit early return rather than relying on PHP’s loose comparisons.and/or/not is unreadable and hard to debug. Group sub-specs into named classes (EligibleForCheckout) so the composite tree stays two levels deep.isSatisfiedBy that touches a relation on every candidate silently becomes an N+1 query. If the spec needs related data, preload it, or push the rule down into a query spec instead.__toString (or expose a describe()), you get a readable dump of the tree. If not, add a small helper that walks the composites — debugging and/or failures without this is painful.equals() on specs for query-deduping; if yours does, ensure your leaf specs implement it consistently or you’ll get duplicate predicates in generated queries.nand, xor, or a CountSpecification for collections), implement it as a new composite rather than adding branches inside existing ones — the whole point is that the composite vocabulary is open/closed.composer.json, wrap the package behind your own thin Specification interface, and keep the coupling to one folder. Swapping implementations later then costs a namespace change, not a refactor.How can I help you explore Laravel packages today?