framework icon indicating copy to clipboard operation
framework copied to clipboard

[12.x] Introduce `ScopeAwareRule` contract to provide relative contextual array item data to validation rules

Open lucasacoutinho opened this issue 4 days ago • 0 comments

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}.");
        }
    }
}

lucasacoutinho avatar Dec 10 '25 23:12 lucasacoutinho