# Specification Pattern in PHP for AutoStore The Specification pattern is a way to encapsulate business rules or query criteria in a single place. In AutoStore, this pattern helps us define conditions like "find expired items" without duplicating logic across the codebase. ## Why Do We Need This? Imagine checking if an item is expired in multiple places: ```php // In a repository $expiredItems = array_filter($items, function($item) { return $item->getExpirationDate() <= new \DateTime(); }); // In a service if ($item->getExpirationDate() <= new \DateTime()) { $this->sendOrderNotification($item); } // In SQL query $stmt = $pdo->prepare("SELECT * FROM items WHERE expirationDate <= :expirationDate"); $stmt->execute(['expirationDate' => (new DateTime())->format('Y-m-d H:i:s')]); $items = $stmt->fetchAll(PDO::FETCH_ASSOC); ``` This creates problems: - **Duplication**: The same rule appears multiple times - **Maintenance**: If the rule changes, you need to update it everywhere - **Database queries**: In-memory filtering doesn't translate to SQL WHERE clauses ## How This Implementation Works The implementation has two main classes: 1. **Specification**: Evaluates conditions against objects 2. **Spec**: Helper class with constants and methods to build specifications ### Basic Structure A specification can be: - A simple comparison: `[field, operator, value]` (e.g., `['expirationDate', '<=', $today]`) - A logical group: `AND`, `OR`, or `NOT` containing other specifications ### Simple Example: Finding Expired Items ```php // Create a specification for expired items $expiredSpec = new Specification( Spec::lte('expirationDate', new \DateTimeImmutable()) ); // Use it to check if an item is expired if ($expiredSpec->match($item)) { echo "This item is expired!"; } // Or filter an array of items $expiredItems = array_filter($items, function($item) use ($expiredSpec) { return $expiredSpec->match($item); }); ``` ### Creating Complex Conditions You can combine conditions using `and`, `or`, and `not`: ```php // Find active items that are expiring in the next 7 days $aboutToExpireSpec = new Specification( Spec::and([ Spec::eq('status', 'active'), Spec::and([ Spec::gte('expirationDate', new \DateTimeImmutable()), Spec::lte('expirationDate', new \DateTimeImmutable('+7 days')) ]) ]) ); ``` ## Available Operators ### Comparison Operators | Method | Description | Example | |--------|-------------|---------| | `Spec::eq(field, value)` | Equals | `Spec::eq('name', 'Milk')` | | `Spec::neq(field, value)` | Not equals | `Spec::neq('status', 'deleted')` | | `Spec::gt(field, value)` | Greater than | `Spec::gt('quantity', 10)` | | `Spec::gte(field, value)` | Greater than or equal | `Spec::gte('price', 5.99)` | | `Spec::lt(field, value)` | Less than | `Spec::lt('weight', 2.5)` | | `Spec::lte(field, value)` | Less than or equal | `Spec::lte('expirationDate', $today)` | | `Spec::in(field, [values])` | Value is in list | `Spec::in('category', ['dairy', 'meat'])` | | `Spec::nin(field, [values])` | Value is not in list | `Spec::nin('status', ['deleted', 'archived'])` | ### Logical Operators | Method | Description | Example | |--------|-------------|---------| | `Spec::and([specs])` | All conditions must be true | `Spec::and([$spec1, $spec2])` | | `Spec::or([specs])` | At least one condition must be true | `Spec::or([$spec1, $spec2])` | | `Spec::not(spec)` | Negates the condition | `Spec::not($spec1)` | ## Best Practice: Domain-Specific Specifications For important business rules, create dedicated classes: ```php // src/Domain/Specifications/ItemExpirationSpec.php class ItemExpirationSpec { public function isExpired(Item $item, DateTimeImmutable $currentTime): bool { return $this->getSpec($currentTime)->match($item); } public function getSpec(DateTimeImmutable $currentTime): Specification { return new Specification( Spec::lte('expirationDate', $currentTime->format('Y-m-d H:i:s')) ); } } ``` ## Using Specifications with Repositories ### For In-Memory Repositories ```php public function findWhere(Specification $specification): array { $result = []; foreach ($this->items as $item) { if ($specification->match($item)) { $result[] = $item; } } return $result; } ``` ### For SQL Repositories The SQL renderer in the example converts specifications to WHERE clauses: ```php public function findWhere(Specification $specification): array { $params = []; $sqlRenderer = new SqlRenderer(); $whereClause = $sqlRenderer->render($spec->getSpec(), $params); $sql = "SELECT * FROM items WHERE $whereClause"; $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return $this->mapToItems($stmt->fetchAll(\PDO::FETCH_ASSOC)); } ``` ## Example: SQL Renderer Usage The commented example in the code shows how to convert a specification to SQL: ```php $spec = Spec::and([ Spec::eq('status', 'active'), Spec::or([ Spec::gt('score', 80), Spec::in('role', ['admin', 'moderator']) ]), Spec::not(Spec::eq('deleted', true)) ]); $params = []; $sqlRenderer = new SqlRenderer(); $whereClause = $sqlRenderer->render($spec, $params); // Results in SQL like: // (status = ? AND ((score > ?) OR (role IN (?,?))) AND NOT (deleted = ?)) // with params: ['active', 80, 'admin', 'moderator', true] ``` ## Benefits for AutoStore 1. **Consistent business rules**: Item expiration logic is defined once 2. **Clean code**: No duplicated conditions across repositories, services, etc. 3. **Efficiency**: Can be used both for in-memory filtering and SQL queries 4. **Flexibility**: If business rules change (e.g., items expire 3 days after their date), you only update one place