Encapsulating business rules as composable objects that evaluate whether a candidate satisfies them — separating rules from entity code.
Explanation
The Specification pattern encapsulates a business rule as a class with an isSatisfiedBy($candidate): bool method. Specifications are composable: AndSpecification, OrSpecification, NotSpecification combine them with boolean logic. Example: ActiveCustomerSpecification->and(HasValidEmailSpecification) produces a compound rule. Benefits: business rules are named, reusable, and testable in isolation; they can be translated to query criteria (Doctrine Criteria or SQL WHERE clauses) for database-side filtering. PHP libraries: beberlei/specification, or implement the interface yourself (typically 10 lines). The pattern shines when the same rule must filter both in-memory collections and database queries — a Doctrine-aware specification generates DQL expressions while the core logic remains testable without a database.
Common Misconception
✗ The specification pattern is just a fancy way to write WHERE clauses. Specifications encapsulate business rules as composable, reusable objects — they can be combined with and/or/not, used in both queries and in-memory filtering, and named to express domain concepts explicitly.
Why It Matters
The Specification pattern encapsulates business rules as composable objects — complex query conditions become named, testable, reusable specifications rather than SQL fragments scattered across repositories.
Common Mistakes
Specifications that leak SQL — they should express business rules, not WHERE clauses.
Not making specifications composable with AND, OR, NOT — the pattern's core value is composition.
Over-using specifications for simple, single-use queries — a repository method is simpler.
Specifications that perform I/O — they should be pure predicates, not data fetchers.
Code Examples
✗ Vulnerable
// Raw conditions scattered in repository:
public function findEligibleCustomers(): array {
return $this->db->query(
'SELECT * FROM customers WHERE active = 1 AND balance > 100 AND age >= 18'
)->fetchAll();
}
// 'Eligible' means different things in different contexts — use a specification:
// $eligible = new ActiveSpec()->and(new MinBalanceSpec(100))->and(new AdultSpec());
✓ Fixed
interface Specification {
public function isSatisfiedBy(mixed $candidate): bool;
}
class ActiveCustomer implements Specification {
public function isSatisfiedBy(mixed $customer): bool {
return $customer->isActive() && !$customer->isBanned();
}
}
Encapsulate business rules as Specification objects with isSatisfiedBy($candidate) — combine with and()/or()/not() to build complex rules without giant if-statement blocks