Yii multi page form wizard best practice

24,676

Solution 1

There are a couple of ways to approach this. I see you posted in the Yii forum so I assume you've searched around there too but in case you haven't:

What I have done is (just for a simple 2-step ActiveRecord form) taken a single action and divided it up into conditional blocks based on the button name, which Yii POSTs on a form submit (note: doesn't work with ajax submits). Then, depending on which button was hit I render the correct form and set the correct scenario on my model for validation purposes.

A hidden "step" field like you have could serve the same purpose as the checking the submitButton name. I would perhaps save the "step" into the form state instead of adding a hidden field though, but either would be fine.

Some people use the stateful activeForm attribute to save the data from a single step in the wizard, or you can use the session, or even save to a temp database table. In my completely untested example below I am using a the stateful form functionality.

Here is an example of what I basically did for an ActiveRecord form. This goes in the "actionCreate":

<?php if (isset($_POST['cancel'])) {
  $this->redirect(array('home'));
} elseif (isset($_POST['step2'])) {
  $this->setPageState('step1',$_POST['Model']); // save step1 into form state
  $model=new Model('step1');
  $model->attributes = $_POST['Model'];
  if($model->validate())
    $this->render('form2',array('model'=>$model));
  else {
    $this->render('form1',array('model'=>$model));
  }
} elseif (isset($_POST['finish'])) {
  $model=new Model('finish');
  $model->attributes = $this->getPageState('step1',array()); //get the info from step 1
  $model->attributes = $_POST['Model']; // then the info from step2
  if ($model->save())
    $this->redirect(array('home'));
  else {
    $this->render('form2',array('model'=>$model));
} else { // this is the default, first time (step1)
  $model=new Model('new');
  $this->render('form1',array('model'=>$model));
} ?>

The forms would look something like this:

Form1:

<?php $form=$this->beginWidget('CActiveForm', array(
    'enableAjaxValidation'=>false,
    'id'=>'model-form',
    'stateful'=>true,
));
<!-- form1 fields go here -->
echo CHtml::submitButton("Cancel",array('name'=>'cancel');
echo CHtml::submitButton("On to Step 2 >",array('name'=>'step2');
$this->endWidget(); ?>

Form 2:

<?php $form=$this->beginWidget('CActiveForm', array(
    'enableAjaxValidation'=>false,
    'id'=>'model-form',
    'stateful'=>true,
));
<!-- form2 fields go here -->
echo CHtml::submitButton("Back to Step 1",array('name'=>'step1');
echo CHtml::submitButton("Finish",array('name'=>'finish');
$this->endWidget(); ?>

I hope that is helpful!

Solution 2

Yii provides a feature called page states to implement things like a multi step / multi page form wizard.

Lets have a look at the Yii docs first:

A page state is a variable that is persistent across POST requests of the same page. In order to use persistent page states, the form(s) must be stateful which are generated using {@link CHtml::statefulForm}.

So the forms of every step / page need to be stateful forms. To render a stateful form you just need to set the CActiveForm::stateful property to true when you start the ActiveForm-widget. Within your controller you can get and set your page states with CController::getPageState() or CController::setPageState().

So these are the basics that work quite well if the implementation of your multi page form wizard is made in the traditional style without AJAX requests.

If however you want to use AJAX calls to submit step data and display the next step, Yii's page states are not usable.

Why? All the page states are transported through HTTP-POST within a hidden input field. The input field gets filled by Yii while the so called output processing. The output processing starts after the rendering and will replace parts of the output. So Yii's page states feature requires the output processing. AJAX responses on the other hand may become corrupted by it because the output processing may also add <link> or <script> tags at the beginning of the output to load required JS and CSS files.

In the end I implemented my own version of stateful forms. I am able to get my stateful data with the static function call ActiveFormWidget::getRequestMultiStepData() every time I need it.

Notice: There is one disadvantage in my implementation: all stateful data needs to be collected before the form widget will be initialized. But I never had a problem with it until now. However here is the code:

class ActiveFormWidget extends CActiveForm
{
    public static $inputNameMultiStepData = '_multiStepData';

    public $multiStep = false;
    public $multiStepData = array();

    public function init()
    {
        parent::init();

        # Hidden-Fields
        if ($this->multiStep) {
            echo Html::hiddenField(static::$inputNameMultiStepData, static::encodeInputData($this->multiStepData));
        }
    }

    /**
     * Gets all multi step data sent.
     * @return array|mixed
     */
    public static function getRequestMultiStepData()
    {
        return isset($_REQUEST[static::$inputNameMultiStepData]) ? static::decodeInputData($_REQUEST[static::$inputNameMultiStepData]) : array();
    }


    /**
     * Encodes form data like Yii does for stateful forms.
     * @param $data
     * @return string
     */
    public static function encodeInputData($data)
    {
        $data = Yii::app()->getSecurityManager()->hashData(serialize($data));

        return base64_encode($data);
    }

    /**
     * Decodes form data like Yii does for stateful forms.
     * @param $data
     * @return bool|mixed
     */
    public static function decodeInputData($data)
    {
        $data = base64_decode($data);
        $data = Yii::app()->getSecurityManager()->validateData($data);
        if ($data !== false) {
            return unserialize($data);
        } else {
            return false;
        }
    }
}
Share:
24,676
Stinky Tofu
Author by

Stinky Tofu

Updated on July 09, 2022

Comments

  • Stinky Tofu
    Stinky Tofu almost 2 years

    I am trying to build a multi-page form with Yii, but am quite new to PHP and Yii and am wondering what the best practice is for writing a multi page form. So far, what I am planning to do is to add a hidden field named 'step' which contains the current step the user is on in the form (the form is broken into 3 steps/pages). So with that in mind, this is how I plan to handle the user clicking on previous/next buttons in the Controller:

    public function actionCreate()
     {
      $userModel = new User;
    
      $data['activityModel'] = $activityModel; 
      $data['userModel'] = $userModel; 
    
      if (!empty($_POST['step']))
      {
       switch $_POST['step']:
        case '1':
         $this->render('create_step1', $data);
         break;
    
        case '2':
         $this->render('create_step2', $data);
         break;
    
      }else
      {
       $this->render('create_step1', $data);
      }
     }
    

    Does this approach make sense? Or am I way off base and there is a much better and more optimized way of doing this in Yii/PHP?

    Thanks!