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

Olindn's avatar

Validating complex array with dynamic content

Hey everyone!

I'm currently learning Laravel, working on a hobby project where leaders of a guild can vote on their guild members. Running PHP 8.1 & the latest version of Laravel Jetstream with Inertia.

I've hit a wall however trying to validate a complex array. The hack I've come up with, feels dirty and the validation rules are not being returned properly. I'd love to know if there is a better approach.

UI for creating new conditions UI for creating new conditions, with these conditions a new vote can be created and a player will show up in the voting list if the conditions are matched

As you can see in the image above, there can be multiple conditions which each have different options. For example:

The condition "if hours online" has the following options: "Is greater than", "Is less than", "Is equal to". However the condition "if is in current rank" has other options like "for more than". So for each condition different validation rules have to be applied, which makes it a bit tricky.

//Here is an example how the server receives the request data

[
  {
    "rankId": 17,
    "promote": [
      {
        "condition": 2,
        "option": "is greater than",
        "firstValue": "30",
        "timeValue": "30"
      },
      {
        "condition": 5,
        "option": "for more than",
        "firstValue": "30",
        "timeValue": null
      }
    ],
    "demote": []
  },
  //...
  {
    "rankId": 18,
    "promote": [],
    "demote": []
  }
]

So I created the following FormRequest

//VoteSettingsRequest.php

public function rules(): array
    {
        return [
            '*.promote.*' => [
                'required',
                new VoteSettingsConditionRule,
            ],
            '*.demote.*' => [
                'required',
                new VoteSettingsConditionRule,
            ],
        ];
    }

And created a custom Validation Rule

//VoteSettingsConditionRule.php

class VoteSettingsConditionRule implements Rule
{
    /** @var \Illuminate\Validation\ValidationException  */
    private $errorMessage;

    public function passes($attribute, $value): bool
    {
        try {
            $validatedCondition = match($value['condition']) {
                1, 2 => Validator::make($value, [
                    'condition' => ['required', 'numeric'],
                    'option' => ['required', 'string', 'in:is greater than,is lower than,equals to'],
                    'firstValue' => ['required', 'numeric'],
                    'timeValue' => ['required', 'numeric']
                ]),
                3 => Validator::make($value, [
                    'condition' => ['required', 'numeric'],
                    'option' => ['required', 'string', 'in:equals to'],
                    'firstValue' => ['required', 'in:Premium account,Free account'],
                ]),
                4 => Validator::make($value, [
                    'condition' => ['required', 'numeric'],
                    'option' => ['required', 'string', 'in:is greater than,is lower than,equals to'],
                    'firstValue' => ['required', 'numeric']
                ]),
                5 => Validator::make($value, [
                    'condition' => ['required', 'numeric'],
                    'option' => ['required', 'string', 'in:for more than'],
                    'firstValue' => ['required', 'numeric'],
                ]),
                default => false
            };

            if(! $validatedCondition->validate()) {
                $this->errorMessage = $validatedCondition->messages();
                return false;
            }
        } catch (\Exception $exception) {
            $this->errorMessage = $exception;
            return false;
        }

        return true;
    }

    public function message(): array
    {
        return collect($this->errorMessage->validator->getMessageBag()->getMessages())
            ->flatten()
            ->toArray();
    }
}

With this approach however the error bag that gets returned to the frontend does not contain all the fields required to display the right validation.

Sure I might get it to work eventually but it doesn't feel elegant or like a Laravel way... Hope someone can get me heading in the right direction!

//Returned Error Bag after a failed validation

{
    "errorBags": {
        "default": {
            "0.promote.0": [
                "The first value field is required.",
                "The time value field is required."
            ]
        }
    },
    "errors": {
        "0.promote.0": "The first value field is required."
    },
}
0 likes
7 replies
bobbybouwmann's avatar
Level 88

To get the proper validation array back you should build up the rules before you run the validation. It doesn't work when you build up this array in one of the validation rules. There are a ton of options to do this, so I think you can figure this out yourself ;)

3 likes
Olindn's avatar

@bobbybouwmann Thanks so much! I went back to the documentation and found a way. It's difficult to know if you are on the right track.

I think I was browsing 8.x documentation, which didn't had the Rule::forEach() option. Lesson learned to browse latest docs 🤓

Ended up with the following working piece of logic. Is there anything you'd do differently?

//VoteSettingsRequest.php

public function rules(): array
{
    return [
        '*.promote.*' => [
            Rule::forEach(function ($value): array {
                return $this->getValidationRules($value);
            }),
        ],
        '*.demote.*' => [
            Rule::forEach(function ($value): array {
                return $this->getValidationRules($value);
            }),
        ]
    ];
}

private function getValidationRules($value)
{
    return match($value['condition'] ?? null) {
        1, 2 => [
            'condition' => ['required', 'numeric'],
            'option' => ['required', 'string', 'in:is greater than,is lower than,equals to'],
            'firstValue' => ['required', 'numeric'],
            'timeValue' => ['required', 'numeric']
        ],
        3 => [
            'condition' => ['required', 'numeric'],
            'option' => ['required', 'string', 'in:equals to'],
            'firstValue' => ['required', 'in:Premium account,Free account'],
        ],
        4 => [
            'condition' => ['required', 'numeric'],
            'option' => ['required', 'string', 'in:is greater than,is lower than,equals to'],
            'firstValue' => ['required', 'numeric']
        ],
        5 => [
            'condition' => ['required', 'numeric'],
            'option' => ['required', 'string', 'in:for more than'],
            'firstValue' => ['required', 'numeric'],
        ],
        default => [
            'condition' => ['required', 'numeric', 'max:5']
        ]
    };
}
1 like
Tray2's avatar

I also suggest the you simplify the code so you don't send those complex arrays to your back end. Use the KISS principle for this, and the code will be much easier to read and maintain.

1 like
kokoshneta's avatar

@vincent15000 Not “Keep It Simple and Stupid”, but “Keep It Simple, Stupid” (you’re not meant to keep it stupid – the principle is calling you stupid for making things too complicated).

1 like
Olindn's avatar

@Tray2 I'm not familiair with KISS, found some video's explaining it but did not found any resource that gave me the aha moment. Do you have any recommended resource I can learn more about KISS?

Keep It Simple, Stupid with just the name of KISS in mind, I was thinking to make it more simple instead of submitting one big array with all ranks and their conditions I can split it up and submit only the current rank. This would change the UX, and even make it a bit better since the user is editing the current rank and saves it directly after they're done.

So then the request data would look like this:

{
  "rankId": 17,
  "promote": [
    {
      "condition": 2,
      "option": "is greater than",
      "firstValue": "30",
      "timeValue": "30"
    },
    {
      "condition": 5,
      "option": "for more than",
      "firstValue": "30",
      "timeValue": null
    }
  ],
  "demote": []
}

My reasoning for sending on big array was to just store a json object in the database, that would become messy fast, so storing it it separately makes sense.

If I would try to simplify the request data even more the only thing I can come up with would be to create a save button at each condition which would make the UX not so great.

Please or to participate in this conversation.