Anyone?
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;
}
@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
];
}
Please or to participate in this conversation.