You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
5.8 KiB
5.8 KiB
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:
// 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:
- Specification: Evaluates conditions against objects
- 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, orNOTcontaining other specifications
Simple Example: Finding Expired Items
// 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:
// 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:
// 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
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:
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:
$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
- Consistent business rules: Item expiration logic is defined once
- Clean code: No duplicated conditions across repositories, services, etc.
- Efficiency: Can be used both for in-memory filtering and SQL queries
- Flexibility: If business rules change (e.g., items expire 3 days after their date), you only update one place