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

Bhavil Jain's avatar

Validating a Complex, 3 Level Nested, Form Data with conditional requirements

[WARNING : LONG POST]

Hey everyone,

I'm using Laravel 10, Inertia and VueJS stack, to build building a rather complex form (survey builder) and I'm having troubles with validating the data.

I have, in total, 7 types of questions out of which sets of 2 have overlapping data structures

  • single , multiple (multichoice question. you can either select 1 option or multiple optiosn)
  • nps, rating (rating can have custom scale, nps has a fixed scale of 10)
  • likert, matrix (mostly similar, only the 'scoring' and usage of these questions is different)
  • personal_info

I have Never had the need to write a custom validator or rule till date, validation using the '|' symbol has usually sufficed my needs. so if the answer is custom validators, I would really appreciate it if someone could help me build one in the context of my data.

I know i can iterate on a array using the * symbol

I thought of building the $rules array dynamically by running a loop and testing each question's type and adding the conditional validation as needed, but quickly realized that this would not work as thge rules would only be added and not 'validated' during the loop and hence the rules would get overwritten in each iteration.

I searched the web and read something about the current() function that returns the current iterator during validation, but havent been able to find much info about it, nor was i able to find anything in the docs regarding this. chat gpt has failed me as well in this quest.

the data I receive on submitting is as follows (the needed Validation is in comments)

[
  "id" => "04ed076c-6923-47d4-9b7e-e38d4d3e7723"  // required|uuid
  "title" => "Form Title" // required|string
  "subTitle" => "Form SubTitle" // required|string
  "description" => null // required|string
  "sections" => [  // required|min:1
    0 => [
      "id" => "80bc1f5f-8978-4ddf-9201-57f85a50e611" // required|uuid
      "title" => "Section #1"   // required|string
      "questions" => [ // required|min:1
        0 =>[
          "id" => "9d6fcb2e-13e9-4cc0-834a-9b3390e2a09c" // required|uuid
          "questionNumber" => 1 // required|integer
          "type" => "single" // required|exists:question_types,type 
          "statement" => "Type Your Question"    // required|string
          "subTitle" => "Question Subtitle"   // required|string
          "uniqueAttribute" => null   // required|exists:unique_attributes,id 
          "required" => false  // required|boolean
          "comment" => false  // required|boolean
          "image" => false  // required|boolean
		  "hasConditions" => true  // required|boolean
          "conditions" =>[ // required_if:hasConditions,true
			// conditions below should only be added to the $rules array if the validation in the previous line is true (i.e if hasConditions is true)
            "ask_if_relation" => "all" // required|in:all,any 
            "skip_to_relation" => "all" // required|in:all,any
            "ask_if" => [] // required
            "skip_to" => [ //required
              0 => [
                "id" => "b6c9f0c9-d963-4792-bffd-5e72a8fa5c17"   // required|uuid
                "conditionType" => "skip_to"  // required|in:skip_to,ask_if
                "questionID" => "9d6fcb2e-13e9-4cc0-834a-9b3390e2a09c" // required| !should be same as the question id of [0] !
                "parentQuestionID" => "9d6fcb2e-13e9-4cc0-834a-9b3390e2a09c" required| !should be same as the question id of [0] (for skip_to type of condition only)!
                "isSubQuestion" => false // required|boolean
                "subQuestionID" => null // ! required_if: Parent Question is a sub question
                "comparisonOperator" => "is_selected" // required|in:....
                "requiredAnswerID" => "7dd011fa-aed4-473b-a4f9-7c2261f962c5" // required_if:comparisionOperator,!=,between | !should exist in the 'options' array below!
                "requiredAnswerVal" => "Option 1" //  !should exist in the 'options' array below!
                "requiredMinAnswerID" => // required_if:comparisionOperator,between | !should exist in the 'options' array below!
                "requiredAnswerMinVal" => null // required_if:comparisionOperator,between | !should exist in the 'options' array below!
                "requiredMaxAnswerID" => null // required_if:comparisionOperator,between | !should exist in the 'options' array below!
                "requiredAnswerMaxVal" => null // required_if:comparisionOperator,between | !should exist in the 'options' array below!
                "skipToQuestionID" => "577e5526-5b71-4ff5-9338-664ff32dca78" // required|uuid| !should be the id of a question that has a question number higher than this question (parent question)
              ]
            ]
          ]
          "options" => [  // required_if:type,single|min:2
            0 => [
              "id" => "7dd011fa-aed4-473b-a4f9-7c2261f962c5" // required|uuid
              "value" => "Option 1" // required|string
              "score" => 0 // required|integer
              "comment" => null // string
            ]
            1 => [
              "id" => "15203f3d-8362-4dfd-8cf8-08695f454c8d"
              "value" => "Option 2"
              "score" => 0
              "comment" => null
            ]
          ]
          "other" => false // required|boolean
        ]
		
		
		....
		// Mored Questions
		....
		
		
		// Most of the validation will be the same as above, mentioning only the ones that are different
        3 => [
          "id" => "14815520-993e-4f72-a391-8931b9b302df"
          "questionNumber" => 4
          "type" => "rating"
          "statement" => "Type Your Question"
          "subTitle" => "Question Subtitle"
          "statementPlaceholder" => "Type Your Question"
          "subTitlePlaceholder" => "Question Subtitle"
          "uniqueAttribute" => null
          "required" => false
          "comment" => false
          "image" => false
          "conditions" => [
            "ask_if_relation" => "all"
            "skip_to_relation" => "all"
            "ask_if" => [
              0 => array:14 [▼
                "id" => "832fa8a8-6574-4b0b-8324-50d64306356a"
                "conditionType" => "ask_if"
                "questionID" => "14815520-993e-4f72-a391-8931b9b302df"
                "parentQuestionID" => "d2f6bd9c-1b2b-4549-8a32-937d685ea0a0" // ! This id should belong to a question where the question number is less than this question's question no i.e. it appears/listed before this question!
                "isSubQuestion" => false
                "subQuestionID" => null
                "comparisonOperator" => "is_not_selected"
                "requiredAnswerID" => "27567d5f-63fd-4989-a206-b02998a4d928"
                "requiredAnswerVal" => "Option 2"
                "requiredMinAnswerID" => null
                "requiredAnswerMinVal" => null
                "requiredMaxAnswerID" => null
                "requiredAnswerMaxVal" => null
                "skipToQuestionID" => null
              ]
            ]
            "skip_to" => []
          ]
          "hasConditions" => true
          "maxRating" => 5 // required_if:type,rating,nps
          "selectedRating" => 0 // required_if:type,rating,nps
          "lowText" => "Low" // required_if:type,rating,nps
          "midText" => "Medium" // required_if:type,rating,nps
          "highText" => "High" // required_if:type,rating,nps
        ]
        
		
		....
		// Mored Questions
		....
		
		
		// Most of the validation will be the same as above, mentioning only the ones that are different
        6 => array:17 [▼
          "id" => "57ded9f3-acb4-4587-b8d6-bade4ed26806"
          "questionNumber" => 7
          "type" => "matrix"
          "statement" => "Type Your Question"
          "subTitle" => "Question Subtitle"
          "statementPlaceholder" => "Type Your Question"
          "subTitlePlaceholder" => "Question Subtitle"
          "uniqueAttribute" => null
          "required" => false
          "comment" => false
          "image" => false
          "conditions" => array:4 [▼
            "ask_if_relation" => "all"
            "skip_to_relation" => "all"
            "ask_if" => []
            "skip_to" => []
          ]
          "hasConditions" => false
          "subQuestions" => [  // required|min:2  
            0 => [
              "id" => "69c65afc-8392-4727-904a-0da431ecc1a1" // required|uuid
              "subQuestionNumer" => 1 // required|integer
              "statement" => "Please Type A Statement" / required|string
              "type" => "subQuestion" // required
              "comment" => false // required|boolean
              "uniqueAttribute" => null  // required|exists:unique_attributes,id
            ]
            1 => array:6 [▼
              "id" => "08fa8db7-1bdb-450e-842b-07824e866d37"
              "subQuestionNumer" => 2
              "statement" => "Please Type A Statement"
              "type" => "subQuestion"
              "comment" => false
              "uniqueAttribute" => null
            ]
          ]
          "optionsCount" => 5 // required|min:2,max:5
          "options" => array:5 [▼
            0 => array:3 [▼
              "id" => "e4517468-dbf9-4627-a6ba-ac0796330608" // required|uuid
              "value" => "Option" // required|string
              "score" => 1 //
            ]
            1 => array:3 [▼
              "id" => "265369cb-6640-42d8-aec1-0934d724c6ee"
              "value" => "Option"
              "score" => 2
            ]
            2 => array:3 [▼
              "id" => "7dfd66a6-3e0d-4b13-9c9b-9a5386449969"
              "value" => "Option"
              "score" => 3
            ]
            3 => array:3 [▼
              "id" => "f205cba4-e282-43ab-8bc3-079e11176ec2"
              "value" => "Option"
              "score" => 4
            ]
            4 => array:3 [▼
              "id" => "91ba6b90-016b-44b0-be37-390f372184fb"
              "value" => "Option"
              "score" => 5
            ]
          ]
          "subQuestionCounter" => 2 // required|min:2
        ]
        7 => array:16 [▼
          "id" => "59c6c302-bd3f-496d-adb6-a5db15f9b32b"
          "questionNumber" => 8
          "name" => array:6 [▼
            "text" => "Name"
            "type" => "text"
            "options" => []
            "value" => null
            "ask" => false
            "required" => false
          ]
          "email" => array:6 [▼
            "text" => "Email ID"
            "type" => "email"
            "options" => []
            "value" => null
            "ask" => false
            "required" => false
          ]
          "birthday" => array:6 [▼
            "text" => "Date Of Birth"
            "type" => "date"
            "options" => []
            "value" => null
            "ask" => false
            "required" => false
          ]
          "gender" => array:6 [▼
            "text" => "Gender"
            "type" => "select"
            "options" => array:4 [▶]
            "value" => null
            "ask" => false
            "required" => false
          ]
          "anniversary" => array:6 [▼
            "text" => "Anniversary"
            "type" => "date"
            "options" => []
            "value" => null
            "ask" => false
            "required" => false
          ]
          "mobile" => array:6 [▼
            "text" => "Mobile"
            "type" => "number"
            "options" => []
            "value" => null
            "ask" => false
            "required" => false
          ]
          "pinCode" => array:6 [▼
            "text" => "Pincode"
            "type" => "number"
            "options" => []
            "value" => null
            "ask" => false
            "required" => false
          ]
          "type" => "personal_info"
          "statement" => "Please input your information"
          "subTitle" => "Question Subtitle"
          "statementPlaceholder" => "Please input your information"
          "subTitlePlaceholder" => "Question Subtitle"
          "conditions" => array:4 [▼
            "ask_if_relation" => "all"
            "skip_to_relation" => "all"
            "ask_if" => []
            "skip_to" => []
          ]
          "hasConditions" => false
        ]
      ]
      "position" => -9
    ]
  ]
  "creator" => "NoOneInParticular" // required|exists:users,id
  "note" => null
  "isPersonalInfoQuestionAsked" => true // required|boolean
]

my validation looks like this currently (I'm stuck)

public function rules(): array
    {
        $rules = [
            "id"=>"required|uuid",
            "title"=>"required|string",
            "subtitle"=>"required|string",
            "description"=>"string",
            "creator"=>"required", // ! Should Be User ID, Need To add avalidation for the same
            "note"=>"string",
            "isPersonalInfoQuestionAsked"=>"required|boolean",

            // Sections
            "sections"=>"min:1",
            "sections.*.id"=>"required|uuid",
            "sections.*.title"=>"required|string",
            "sections.*.position"=>"required|integer",

            // Questions
            "sections.*.questions"=>"min:1",
            "sections.*.questions.*.id"=>"required|uuid",
            "sections.*.questions.*.questionNumber"=>"required|integer",
            "sections.*.questions.*.typeId"=>"required|exists:question_types,id",
            "sections.*.questions.*.statement"=>"required|string",
            "sections.*.questions.*.subtitle"=>"required|string",
            "sections.*.questions.*.uniqueAttributeId"=>"exists:unique_attributes,id",
            "sections.*.questions.*.required"=>"required|boolean",
            "sections.*.questions.*.comment"=>"required|boolean",
            "sections.*.questions.*.image"=>"required|boolean",
            "sections.*.questions.*.hasConditions"=>"required|boolean",
            "sections.*.questions.*.options"=>"required_if:sections.*.questions.*.typeId,1,2"
        ];
        return $rules;
    }
0 likes
5 replies
krisi_gjika's avatar

@Bhavil Jain your idea of looping the data and building the $rules dynamically is correct, I don't see what issue you had with it. Keep in mind array rules don't have to apply to every item of the array.

Ex: sections.0.questions.2.statement -> apply the rule to the third question of the first section

Bhavil Jain's avatar

@krisi_gjika Firstly... thanks for responding...

I think I missed that class... 😅😅

Should I proceed like this?

public function rules(): array
    {
        $rules = [
            .......
            // Questions
			foreach($this->sections as $sectionIndex=>$sections)
			{
				foreach($section["questions"] as $questionIndex=>$question)
				{
						if($question->type == 1) //Single Choice Question
						{
								$rules["sections[".$sectionIndex."]['questions'][.$questionIndex."]['options']] => "required|min:2|array";
						}
				}
			}
            .......
        ];

Is this what you meant?

Unable to find any assistance here... I did the following... it seems to work as of now

public function rules(): array
    {
        $rules = [
            .......
			"sections.*.questions.*" => "min:1",
            "sections.*.questions.*.id" => "required|uuid",
            "sections.*.questions.*.questionNumber" => "required|integer",
            "sections.*.questions.*.type" => "required|exists:question_types,id",
            "sections.*.questions.*.statement" => "required|string",
            "sections.*.questions.*.subTitle" => "required|string",
            "sections.*.questions.*.uniqueAttributeId" => "nullable|exists:unique_attributes,id",
            "sections.*.questions.*.required" => "required_unless:sections.*.questions.*.type,8|boolean",
            "sections.*.questions.*.comment" => "required_unless:sections.*.questions.*.type,8|boolean",
            "sections.*.questions.*.image" => "required_unless:sections.*.questions.*.type,8|boolean",
            "sections.*.questions.*.hasConditions" => "required|boolean",

			// Single and Multiple Questions (Other & Options)
            "sections.*.questions.*.other" => "required_if:sections.*.questions.*.type,1,2|boolean",
            "sections.*.questions.*.options" => "required_if:sections.*.questions.*.type,1,2|array|min:2",
            "sections.*.questions.*.options.*.id" => "required_if:sections.*.questions.*.type,1,2|uuid",
            "sections.*.questions.*.options.*.value" => "required_if:sections.*.questions.*.type,1,2|string",
            "sections.*.questions.*.options.*.score" => "required_if:sections.*.questions.*.type,1,2|integer",
            "sections.*.questions.*.options.*.comment" => "nullable|string",
        	.......
        ];

Which approach would be better?

Also, is there any way that I could get access to the iterator value? i.e. the value of the '*' that we are putting in? it would be helpful in validating the conditions array, as the validation for it would depend on other elements in the questions array. I would like to pass the iterator value to a custom Rule

krisi_gjika's avatar
Level 14

@Bhavil Jain something like:

public function rules(): array
{
        $rules = [....]; // base rules

        if (!$this->filled('sections') || !is_array($this->input('sections'))) {
            return $rules;
        }
	    
        foreach($this->sections as $sectionIndex => $section) {
                if (empty($section['questions']) || !is_array($section['questions'])) {
					continue;
				}
				foreach($section["questions"] as $questionIndex => $question) {
						$baseKey = "sections.{$sectionIndex}.questions.{$questionIndex}";
						if ($question['type'] == 1) {
							$rules = array_merge($rules, $this->singleChoiceRules($baseKey));
						} ... other question types
				}
		}
		
		// dd($rules); you can dd the rules array to verify it's what you expect
		return $rules;
}

protected function singleChoiceRules(string $baseKey)
{
	return [
		"{$baseKey}.questionNumber" => 'required|integer',
		... other rules specific to this question type
	];
}
1 like

Please or to participate in this conversation.