framework
framework copied to clipboard
[12.x] Introduce `ScopeAwareRule` contract to provide relative contextual array item data to validation rules
When writing custom validation rules for array items, accessing sibling data requires manual path manipulation. Consider an API for bulk order creation:
{
"orders": [
{
"customer_id": 123,
"items": [
{
"product_id": 1,
"quantity": 10,
"unit_price": 150.00,
"discount": 2000.00
}
]
}
]
}
To validate that discount doesn't exceed the line total (quantity x unit_price), you currently need:
class DiscountMustNotExceedLineTotal implements ValidationRule, DataAwareRule
{
protected array $data = [];
public function setData(array $data): static
{
$this->data = $data;
return $this;
}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
// $attribute = "orders.0.items.1.discount"
// Need to extract indices and navigate the nested structure
preg_match('/^orders\.(\d+)\.items\.(\d+)\./', $attribute, $matches);
$orderIndex = $matches[1] ?? null;
$itemIndex = $matches[2] ?? null;
if ($orderIndex === null || $itemIndex === null) {
return;
}
$item = $this->data['orders'][$orderIndex]['items'][$itemIndex] ?? [];
$lineTotal = ($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0);
if ($value > $lineTotal) {
$fail("The discount cannot exceed the line total of {$lineTotal}.");
}
}
}
We could make the experience much nicer with a new and more limited ScopeAwareRule interface that automatically receives the current array item's data:
class DiscountMustNotExceedLineTotal implements ValidationRule, ScopeAwareRule
{
protected array $scope = [];
public function setScope(array $scope): static
{
$this->scope = $scope;
return $this;
}
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$lineTotal = ($this->scope['quantity'] ?? 0) * ($this->scope['unit_price'] ?? 0);
if ($value > $lineTotal) {
$fail("The discount cannot exceed the line total of {$lineTotal}.");
}
}
}