Replies: 5 comments 6 replies
-
|
In the docs I read someting about the DataAwareRule interface. See |
Beta Was this translation helpful? Give feedback.
-
|
any updates? i faced the same issue unfortunately 😞 |
Beta Was this translation helpful? Give feedback.
-
|
Don't know if there is a "correct way". I used Closure. If your attribute is part of an array you gonna get the index on the second position for example: |
Beta Was this translation helpful? Give feedback.
-
|
Hello! Myself and @Ross2609 just came across this problem! After lots of source code diving, this can be accomplished with This means that when the validator processes the rule and reaches |
Beta Was this translation helpful? Give feedback.
-
|
I’ve recently faced a similar problem when working with nested attributes and complex validation logic where a rule needs access to sibling fields — for example, comparing To make such rules cleaner, I ended up extracting a small utility for path manipulation, which I use across multiple custom rules: <?php
namespace App\Validation\Support;
use Arr;
final readonly class AttributePath
{
/**
* Replaces wildcard segments (`*`) in a pattern path
* with actual segments from a given attribute, while their structure matches.
*
* The method iterates synchronously through both pattern and attribute segments,
* substituting each `*` with the corresponding segment.
* Once the paths diverge (for example, segment names differ),
* substitution stops and the rest of the pattern remains unchanged.
*
* Used to resolve a pattern path into a specific key
* that can be used to access real data.
*
* @param string $pattern The pattern path containing wildcards (e.g., "orders.*.details.discount").
* @param string $attribute The actual attribute path being validated (e.g., "orders.3.details.price").
* @param float|int $depth How many `*` segments should be replaced (default — all).
* @return string The concrete path with replaced wildcards (e.g., "orders.3.details.discount").
*
* Examples:
* <code>
* AttributePath::concretize(pattern: 'orders.*.details.discount', attribute: 'orders.3.details.price');
* // orders.3.details.discount
* </code>
* <code>
* AttributePath::concretize(pattern: 'invoices.*.items.*.price', attribute: 'invoices.1.items.4.total');
* // invoices.1.items.4.price
* </code>
* <code>
* AttributePath::concretize(pattern: 'products.*.name', attribute: 'products.10.details.name.value');
* // products.10.name
* </code>
* <code>
* AttributePath::concretize(pattern: 'orders.attributes.meta', attribute: 'orders.attributes.meta');
* // orders.attributes.meta
* </code>
* <code>
* AttributePath::concretize(pattern: 'invoices.*.items.*.price', attribute: 'invoices.1.items.4.total', depth: 1);
* // invoices.1.items.*.price ← only the first `*` is replaced
* </code>
* <code>
* AttributePath::concretize('orders.*.details.discount.*.value', 'orders.3.details.price.value');
* // → orders.3.details.discount.*.value
* </code>
* <code>
* AttributePath::concretize('orders.*.details.discount.*.value', 'orders.3.items.total.4.test');
* // → orders.3.details.discount.*.value
* </code>
*/
public static function concretize(string $pattern, string $attribute, float|int $depth = INF): string
{
$patternParts = explode('.', $pattern);
$attrParts = explode('.', $attribute);
$resolved = [];
$replacedCount = 0;
while ($patternParts) {
$part = array_shift($patternParts);
$attr = array_shift($attrParts);
// Replace `*` if allowed
if ($part === '*' && $replacedCount < $depth) {
$resolved[] = $attr ?? '*';
$replacedCount++;
continue;
}
// Stop substitution once a mismatch is found
if ($attr !== $part) {
array_push($resolved, $part, ...$patternParts);
break;
}
// Matching segment — keep as is
$resolved[] = $part;
}
return implode('.', $resolved);
}
/**
* Converts a concrete attribute path into a pattern path
* by replacing numeric indices with wildcard segments (`*`).
*
* This is the inverse operation of {@see concretize()} —
* used to derive a generic pattern from a specific attribute path.
*
* Example:
* - input: "orders.3.details.discount"
* - output: "orders.*.details.discount"
*
* @param string $attribute The concrete attribute path, e.g., "orders.3.details.discount".
* @param int|float $depth How many numeric segments to replace (INF — all).
* @return string The generalized path with wildcards.
*
* <code>
* AttributePath::patternize(attribute: 'orders.3.details.discount');
* // orders.*.details.discount
* </code>
*
* <code>
* AttributePath::patternize(attribute: 'invoices.2.items.5.price');
* // invoices.*.items.*.price
* </code>
*
* <code>
* AttributePath::patternize(attribute: 'carts.1.packages.4.products.9.name', depth: 2);
* // carts.*.packages.*.products.9.name
* </code>
*/
public static function patternize(string $attribute, float|int $depth = INF): string
{
$parts = explode('.', $attribute);
$resolved = [];
$replaced = 0;
while ($parts) {
$part = array_shift($parts);
if ($replaced >= $depth) {
array_push($resolved, $part, ...$parts);
break;
}
if (is_numeric($part)) {
$resolved[] = '*';
$replaced++;
continue;
}
$resolved[] = $part;
}
return implode('.', $resolved);
}
/**
* Builds a "sibling" attribute path by replacing one of the segments
* (by default, the last one) with another segment or sequence of segments.
*
* Useful for deriving related attributes, for example,
* getting "discount" from "orders.0.details.price".
*
* @param string $attribute The current attribute path (e.g., "orders.0.details.price").
* @param string|array $replace The new segment(s) to insert.
* A string replaces a single segment;
* an array inserts multiple segments.
* @param string|null $segmentToReplace Optionally, the name of the segment to replace.
* If omitted, the last segment is replaced.
* @return string The resulting sibling attribute path.
*
* <code>
* AttributePath::sibling('orders.0.details.price', 'discount');
* // "orders.0.details.discount"
* </code>
* <code>
* AttributePath::sibling('invoice.details.price', 'total', 'details');
* // "invoice.total.price"
* </code>
* <code>
* AttributePath::sibling('cart.0.summary.value', ['totals', 'currency']);
* // "cart.0.summary.totals.currency"
* </code>
* <code>
* AttributePath::sibling('attributes.2.metadata.5.value', 'code', 'value');
* // "attributes.2.metadata.5.code"
* </code>
*/
public static function sibling(string $attribute, string|array $replace, ?string $segmentToReplace = null): string
{
$parts = explode('.', $attribute);
if ($segmentToReplace) {
$pos = array_search($segmentToReplace, $parts, true);
if ($pos !== false) {
$parts[$pos] = $replace;
}
return implode('.', $parts);
}
// Replace the last segment by default
array_pop($parts);
$parts = array_merge($parts, Arr::wrap($replace));
return implode('.', $parts);
}
}This helper lets a rule dynamically resolve related fields like:$attribute = 'items.3.details.price';
AttributePath::sibling($attribute, 'discount');
// → "items.3.details.discount"
$attribute = 'items.3.details.price';
AttributePath::concretize('items.*.details.discount', $attribute);
// → "items.3.details.discount"
$attribute = 'items.5.details.price';
AttributePath::patternize($attribute);
// → "items.*.details.price"Example usage in a custom ruleHere’s a simple example of a rule that ensures discount is not greater than price for each item. <?php
namespace App\Rules;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\Rule;
use App\Validation\Support\AttributePath;
class DiscountLessThanPrice implements Rule, DataAwareRule
{
protected array $data = [];
public function setData($data)
{
$this->data = $data;
return $this;
}
public function passes($attribute, $value): bool
{
// Build path to the "price" field next to the current "discount"
$pricePath = AttributePath::sibling($attribute, 'price');
$price = data_get($this->data, $pricePath);
// If price is missing — ignore (let other rules handle it)
if ($price === null) {
return true;
}
return (float) $value <= (float) $price;
}
public function message(): string
{
return __('The :attribute must be less than or equal to its corresponding price.');
}
}That way, your validation rules can stay self-contained and expressive, In addition:The other two methods — For example: Both are great for writing context-aware, reusable validation rules that can operate on nested request data |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I recently struggled with a problem where I needed to create a custom validation rule, that was dependent on another field's value, for a nested data structure. Unfortunately, the documentation is not very clear how developers can create custom validation rule objects that are dependent on other fields, if at all possible?
The following illustrates the desired "semantic" I was trying to achieve:
I have searched various forums, issues, laracast, ...etc, without any success. I ended up having to use the
addDependentExtensions()method to add a custom rule. While it worked in my case, I do wonder how the "correct" way of creating dependent rule objects is?Does anyone have a good example of how the above shown example can be achieved?
Beta Was this translation helpful? Give feedback.
All reactions