Form with many optional fields. Meta scenario idea

I’m curious what is your way of handling forms with many optional group of fields. Lets take a contact form with some standard fields like name, surname, mail, phone. Additionally, there are 3 checkboxes. Checking any of them unreveals different, additional group of 4, 5 fields to fill.

My first attempt to solve that was to declare a scenario for every combination of checkboxes turned on. In this example it would look like that:

  • mandatory fields are without scenario so they are always validated
  • there must be a different scenario for each optional field group: scenarioA, scenarioB, scenarioC
  • and we have to prepare a scenario for every combination of checkboxes: scenarioAB, scenarioAC, scenarioABC, scenarioBC

Next all these scenarios have to be assigned to fields which may be optionally filled. Now here is my question. What if I had 5 or 6 optional fields group? That would give me a huge amount of scenarios to prepare. Am I missing something, or there is no better way to solve that using tools build in Yii ?

What if there was an option to create a meta scenarios, or set many active scenarios before validation?

EDIT

I have created a sample behavior which can create metaScenario on the fly. Basically it allows to assign few scenarios to single validation run.

Attached is MetaScenario behaviour. Put it into extensions directory.

Usage is simple. In model add following to configure behaviour




   public function behaviors(){

           return array(

               'MetaScenario' => array(

                   'class' => 'application.extensions.MetaScenario',     //path to class file

                   'mark' => 'META'    // trigger which will run meta scenario generator

               ),

           );

       }

   

and modify rules and safe attributes functions




   $rules = array(

               //your rules set. Example:

   

               // default rules

               array('name, surname, mail', 'required', 'on'=>'main'),

               array('mail', 'email', 'on'=>'main'),

               array('verifyCode', 'captcha', 'on'=>'main'),

               // scenario 1 rules

               array('from, to, nights, persons', 'required', 'on'=>'scenario1'),

               array('nights, persons', 'numerical','integerOnly'=>true, 'on'=>'scenario1'),

               // scenario 2 rules

               array('carFrom, carTo', 'required', 'on'=>'scenario2'),

           );

           $this->createMetaRule(&$rules,$this->getScenario());

           return $rules;

   




   public function safeAttributes()

       {

           $scenario1 = array('from', 'to', 'nights', 'persons');

           $scenario2 = array('carFrom', 'carTo');

           $main = array_diff($this->attributeNames(), $scenario1, $scenario2);  //This is formModel example and I want all attributes not used in other scenarios to be considered safe

           $attributes = array(

           'main'=>implode(', ',$main),

           'scenario1'=>implode(', ',$scenario1),

           'scenario2'=>implode(', ',$scenario2),

           );

           $this->createMetaSafeAttributes(&$attributes,$this->getScenario());

           return $attributes;

       }

   

finally in controller set the scenario name in format yourMarkKeyword_defaultScenario_Scenario1_Scenario2_(…)_ScenarioN

for example:




               $contact = new ContactForm;

               $scenario = 'META_main';

               $scenario .= ($_POST['ContactForm']['hotel']=='rent')?'_scenario1':'';

               $scenario .= ($_POST['ContactForm']['car']=='rent')?'_scenario2':'';

               $contact->setScenario($scenario);

   

Thats all. behavior will create another rule and safe Attributes set containing rules and safe attributes from all scenarios separated with " _ ".

Note that scenarios names I used are just examples. You may use any names as long as they don’t contain " _ " symbol as it is used as separator. Alse keep in mind that methods createMetaRules and createMetaSafeAttributes take reference to an array not an array.

Comments are welcome.

edit in first post

I’ve followed your instructions as described above, but the validation doesn’t seem to work :(

EDIT:

the problem seems to be, that if I do the following:

$contact = new ContactForm;

$scenario = ‘META_main’;

$contact->setScenario($scenario);

and then inside the "rules" function of the model:

Yii::log($this->getScenario(), ‘info’, ‘scenario’);

that “$this->getScenario()” doens’t return any value.

but if i do:

Yii::log($contact->getScenario(), ‘info’, ‘scenario’);

there will be "META_main" in the logfile. seems to be a problem of "getScenario" inside the model class itself.

btw.: in the "safeAttributes" function "$this->getScenario()" returns the expected value. but not in "rules" function"

Can you try to run


Yii::log($this->getScenario(), 'info', 'scenario');

as a first line in rules function? Also please try to disable (comment out) behaviour function, so it will not be attached and check if $this->getScenario() returns any value.

It’s weird because my behaviour doesn’t touch scenario attribute nor its setter or getter. Can you post your rules function ? Maybe you are using some other extension which can manipulate scenarios ?

still the same issue. no, i don’t use other extensions or behaviors.

my rules function looks like this:

public function rules()


{


	Yii::log($this->getScenario(), 'info', 'scenario');


	


	$arrRule = array(


		array('strIdSessionActive','length','max'=>50),


		array('strCardFirstName','length','max'=>100),


		array('strCardLastName','length','max'=>100),


		array('strShippingFirstName','length','max'=>100),


		array('strShippingLastName','length','max'=>100),


		array('strShippingStreet','length','max'=>150),


		array('strShippingZipcode','length','max'=>10),


		array('strShippingCity','length','max'=>100),


		array('strShippingCountry','length','max'=>10),


		array('strUsername','length','max'=>100),


		array('strUserpass','length','max'=>100),


		array('strFirstName','length','max'=>100),


		array('strLastName','length','max'=>100),


		array('strStreet','length','max'=>150),


		array('strZipcode','length','max'=>10),


		array('strCity','length','max'=>100),


		array('strCountry','length','max'=>10),


		array('strEmail','length','max'=>200),


		array('strEmailUpdate','length','max'=>200),


		array('strBankOwner','length','max'=>200),


		array('strBankCode','length','max'=>200),


		array('strBankNumber','length','max'=>200),


		array('strBankName','length','max'=>200),


		array('strCompany','length','max'=>100),


		array('strMobile','length','max'=>50),


		array('strTelephone','length','max'=>50),


		array('strIpAddress','length','max'=>15),


		array('strContractName','length','max'=>255),


		array('strCardNumber','length','max'=>255),


		array('strDistributorNumber','length','max'=>255),


		array('strEmail', 'compare', 'compareAttribute' => 'strEmailRepeat', 'on' => 'step1'),


		array('idSalutation, strFirstName, strLastName, strStreet, strZipcode, strCity, strCountry, strEmail, strEmailRepeat', 'required', 'on' => 'step1'),


		array('strUsername, strUserpass, strUserpassRepeat, intBirthDay, intBirthMonth, intBirthYear', 'required', 'on' => 'step2'),


		array('idShippingTitle, strShippingFirstName, strShippingLastName, strShippingStreet, strShippingZipcode, strShippingCity', 'required', 'on' => 'shipping'),


		array('strCardFirstName, strCardLastName', 'required', 'on' => 'present'),


		array('strBankOwner, strBankCode, strBankNumber, strBankName', 'required', 'on' => 'debit'),


		array('strCountry', 'default', 'value' => 'DE'),


		array('strShippingCountry', 'default', 'value' => 'DE'),


		array('idPromotionGirl, idShippingTitle, booPresent, booBox, idSalutation, idRefere, idCoupon, intStatus, intJoypoint, intReminderLevel', 'numerical', 'integerOnly'=>true),


	);


	


	//$this->createMetaRules(&$arrRule, $this->getScenario());


	


	return $arrRule;


}

Sorry bitmatix, but I can’t find any reason why it would not work. Its strange because it’s not working even if you omit 3rd party code.

Just a few ideas. Maybe you updated framework recently and not all files were overwritten? Can you try to generate a new, very simple model and verify if getScenario() is working flawlessly there? Maybe you can debug code starting from validate() in your controller and see at which point scenario attribute is deleted (I assume its deleted at some point as you said its working in controller and in attributes function in model).

EDIT:

Do you pass any variables to validate() method ? Its the only way i found that could overwrite model->scenario attribute.

It doens’t even work with a new generated model. i’ve completly checked out the whole framework from svn that doesn’t help, too.

do you use the current revision from the svn?

I’m using 1.09 version from here http://www.yiiframework.com/download/

really strange. I’ve tried it with the same version (as you do), but it doesn’t change anything.