Specification Pattern
debt(d7/e5/b5/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints indicate automated detection is 'no' and the code pattern is 'complex eligibility or validation rules scattered across service methods.' PHPStan is listed but cannot automatically flag missing or misapplied specification patterns — it takes a reviewer who recognises the anti-pattern of scattered business rules to identify that the Specification pattern is needed or misused. This won't surface in standard linting.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix describes encapsulating business rules into Specification objects with isSatisfiedBy(), and/or/not composition — but common mistakes include SQL leaking into specs, missing composition, and I/O in specs. Correcting misuse means identifying scattered rules across service and repository methods and consolidating them into dedicated Specification classes, touching multiple files across the codebase.
Closest to 'persistent productivity tax' (b5). The pattern applies across web, cli, and queue-worker contexts. When adopted, every future maintainer must understand the Specification abstraction and its composition model. Misuse (e.g., SQL-leaking specs or non-composable specs) can quietly spread, but the pattern itself doesn't reshape the entire architecture — it's scoped to business rule and query logic, making it a persistent but not system-defining tax.
Closest to 'notable trap — a documented gotcha most devs eventually learn' (t5). The misconception field explicitly states developers believe Specifications are 'just a fancy way to write WHERE clauses.' Common mistakes confirm this: specs leaking SQL, not being composable, or performing I/O are well-documented pitfalls. The pattern's dual use in both in-memory filtering and query construction is non-obvious, and the distinction between business rule encapsulation and query construction trips up most developers encountering it for the first time.
Also Known As
TL;DR
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
Why It Matters
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
// 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());
interface Specification {
public function isSatisfiedBy(mixed $candidate): bool;
}
class ActiveCustomer implements Specification {
public function isSatisfiedBy(mixed $customer): bool {
return $customer->isActive() && !$customer->isBanned();
}
}