Validating the sum of a property in an array in Laravel

Published November 29th, 2022

Important: The information in this article is over 12 months old, and may be out of date or no longer relevant.

But hey, you're here anyway, so give it a read see if it still applies to you.

I’m currently working on a project in Laravel where I needed to validate the sum of a property in an array adds up to a specific value. For example:

1$data = [
2 'items' => [
3 [
4 'name' => 'Banana',
5 'number' => 24
6 ],
7 [
8 'name' => 'Rama',
9 'number' => 76
10 ]
11 ]
12];
Copied!

I needed to make sure the sum of the number properties added up to 100.

In the past, I wrote a closure in my Form Request, but that shows this is something that is needed more than once, so rather than repeat the logic every time I needed it, why not write a custom rule? Laravel makes it so easy too.

Heads up too: this rule is for Laravel 9.18+ where Invokable validation classes were added.

Invokable rules have a single __invoke method where the validation logic takes place, and if fails, can call the $fail closure that is available.

This rule is designed to be attached to the outer array – not the property in the array. The logic for the validation here is to convert the array to a collection, then using the collection’s sum method, confirm it adds up to the required amount. And if it does not, it fails.

1public function __invoke($attribute, $value, $fail)
2{
3 $array = collect($value);
4 
5 if ($array->sum($this->property) != $this->sum) {
6 $fail(__('The sum of the :property must add up to :sum.', [
7 'property' => $this->property,
8 'sum' => $this->sum
9 ]));
10 }
11}
Copied!

So hang tight… where did $this->property and $this->sum come from? Given the rule is applied to the array itself, you need to tell the rule what property to sum:

1new ExactSumRule('number')
Copied!

Within the Rule’s constructor, the internal property attribute is being set, so the rule will now look for the sum of the “number” property. And sum too.

For my initial needs, I required a total of 100 for my use case, but I can also think of times where a different total is needed. The Rule also accepts a second parameter to change the required sum:

1new ExactSumRule('number', 50)
Copied!

By default, the Rule will sum for 100. But above, it will sum for 50.

This now offers flexibility to check different properties in an array, with different totals, in a reusable Rule.

And passes 7 tests confirming parameters, behaviour and options – I’ve got these written as Pest tests if you’re interested and have read this far: reach out to me if you’re wanting these.

Here’s a look at the entire rule:

1namespace App\Rules;
2 
3use Illuminate\Contracts\Validation\InvokableRule;
4use InvalidArgumentException;
5use TypeError;
6 
7class ExactSumRule implements InvokableRule
8{
9 public function __construct(
10 protected string $property,
11 protected float $sum = 100
12 ) {
13 if ($this->sum < 0) {
14 throw new InvalidArgumentException('The $sum must be a value greater than 0.');
15 }
16 }
17 
18 public function __invoke($attribute, $value, $fail): void
19 {
20 $array = collect($value);
21 
22 try {
23 if ($array->sum($this->property) != $this->sum) {
24 $fail(__('The sum of the :property must add up to :sum.', [
25 'property' => $this->property,
26 'sum' => $this->sum
27 ]));
28 }
29 } catch (TypeError $error) {
30 $fail(__('The values for :property could not be added.', [
31 'property' => $this->property
32 ]));
33 }
34 }
35}
Copied!

In the __invoke method, a try block will catch any TypeErrors thrown, such as when you provide a property that cannot be summed using the collection's sum method, and will fail the validation.

And here’s how we can use it for the number property on the items array:

1// ...
2'items' => [
3 'required',
4 'array',
5 'min:1',
6 new ExactSumRule('number')
7]
8// ...
Copied!

And now to check the number properties add up to 50:

1// ...
2'items' => [
3 'required',
4 'array',
5 'min:1',
6 new ExactSumRule('number', 50)
7]
8// ...
Copied!

Or even 15.5:

1// ...
2'items' => [
3 'required',
4 'array',
5 'min:1',
6 new ExactSumRule('number', 15.5)
7]
8// ...
Copied!

Happy summing!

You may be interested in...