Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Joi's avatar
Level 1

Nesting Rule::foreach with closure

I have following sample data

{
  "items": [
    {"id": 2, "qty": 20},
    {"id": 3, "qty": 10}
  ]
}

The kind of validation I am trying to achieve:

  • the id is required
  • the qty is required
  • the id exists on a table
  • the qty for each row does not exceed certain table field.

Here is my validation rules


 return [
    'items' => 'array|required|min:1',
    'items.*' => Rule::forEach(function () {
        return [
            function ($attribute, $value, $fail) {
                $model = Product::findOrFail($value['id']);
                if ($model->balance < $value['qty']) {
                    return $fail($attribute . '.qty exceeds existing balance');
                }
                return true;
            },
            'id' => 'bail|required|exists:App\Models\Product,id',
            'qty' => 'bail|required|integer|numeric|min:0',
        ];

    })
];

However, the closure returned inside foreach is never executed. The id and qty rules are processed as expected.

Is there a way to access both id and qty for each row simultaneously?

P.S following rules structure will skip qty and id validations

[
'items' => 'array|required|min:1',
'items.*.id' => 'bail|required|exists:App\Models\Product,id',
'items.*.qty' => 'bail|required|integer|numeric|min:0',
'items.*' => Rule::forEach(function () {
    return [
        function ($attribute, $value, $fail) {
            $model = Product::findOrFail($value['id']);
            if ($model->balance < $value['qty']) {
                return $fail($attribute . '.qty exceeds existing balance');
            }
            return true;
        },
    ];

})
]
0 likes
3 replies
Joi's avatar
Level 1

thank you @tykus. But the short docs doesn't explicitly state that the key should not be nested array.

But it seems it doesn't support my logic so I have opted to following approach.

 return [
    'items' => 'array|required|min:1',
    'items.*' => Rule::forEach(function () {
        return [
            function ($attr, $value, $fail) {
                
                $subValidator = Validator::make($value, [
                    'id' => 'bail|required|exists:App\Models\Product,id',
                    'qty' => 'bail|required|integer|numeric|min:0'
                ], ['id.exists' => ':attribute does not exists']);

                if ($subValidator->fails()) {
                    return $fail($subValidator->errors()->first());
                }
                //rest of validation logic
                }
              ]
             })
  ]

1 like
Thunderson's avatar

you should use "Rule::forEach" on 'items' and not on 'items.*', like this, then it will work

return [
    'items' => ['array','required', 'min:1',Rule::forEach(function () {
        return [
            function ($attribute, $value, $fail) {
                $model = Product::findOrFail($value['id']);
                if ($model->balance < $value['qty']) {
                    return $fail($attribute . '.qty exceeds existing balance');
                }
                return true;
            },
            'id' => 'bail|required|exists:App\Models\Product,id',
            'qty' => 'bail|required|integer|numeric|min:0',
        ];
    })],
];

Please or to participate in this conversation.