Friday, 11 October 2013

Easy Form Wizards with Yii Framework

Yii is a PHP5 framework founded by previously Prado developers. It’s not as famous as Symfony or Zend Framework, but, like them, it follows to the required acronyms like OOP, MVC, DRY, ORM… and because NoSQL is not an acronym, Yii doesn’t support currently any of those data-stores.
More seriously, Yii has a good doc, an easy-to-learn API and has some interesting concepts, like we will see further in this article.

Installation

Grab the last release and uncompress it. I suggest that you put it at the same level than your webserver’s root (but not in it, for the sake of the security)
If you’re under Unix, you may find fine to create a symlink like this, so the updates will be easy, even if you have a lot of Yii apps.
$ ln -s yii-1.1.3.r2247 yii-stable
Then move into your webroot and type:
$ php yii-1.1.3.r2247/framework/yiic webapp demo
You will be prompted to create a new app called demo. Answer Yes and the yiic command-line utility will create a whole directory structure for you.
Opposite to symfony, a yii project contains only one application, so there’s only one webcontroller, index.php. This file essentially find the framework.
The interesting parts are located in the protected folder. Note that Yii has security in mind and has add a .htaccess file in it, so the protected folder can not be opened from the webserver.
Assuming your webserver is correctly configured to serve files from your webroot, you should view the default page when browsing to http://localhost/demo/.

The Yii “form” component

Unlike to symfony (and django) forms, Yii doesn’t have a dedicated component to handle forms. Every model class can be a form, from the business logic point of view. So the validation is not done in a separate class, and has not to be done twice, like sometimes you are lead to with symfony or django. This is the reason why django introduces in the 1.2 release the concept of validator, which can be used in a model and in a form.
The CForm class only is the glue between a view and the model. Yii is probably more MVC than symfony/django here, because we don’t create a class where we mix the validation and the displaying of the widgets.
For more details, see the form builder documentation.

Create a form wizard

A nice functionnality I’ve recently came into is the ability of Yii to easily creates form wizards.
All model’s classes in Yii can be used in different contexts, called scenarii. A scenario will trigger a dedicated validation rule. For exemple, a LoginForm can be used on registration or when login:
  • In the first case, we require the name and the 2 passwords fields to be filled in, and the 2 passwords to match.
  • In the second case, we require the name and the password fields to be filled in, and we add an option to allow the app to remember the user.
More on this here.
This functionality can be used to create wizards.
Create in protected/models/ a file called WizardForm.php. Paste the code below in it:
<?php
class WizardForm extends CFormModel
{
        public $step ;
        public $name ;
        public $body ;
        public $pub_date ;
        public $authors ;

        public static function getPubDates() {
                return array('' => '----',
                             '-1' => 'yesterday',
                             '0' => 'today',
                             '1' => 'tomorrow') ;
        }

        public static function getAuthors() {
                return array('' => '----',
                             'me' => 'Me',
                             'myself' => 'Myself') ;
        }

        public function preview() {
                $pub_dates = $this->getPubDates() ;
                $authors = $this->getAuthors() ;
                return strtr('<h2>%name%</h2><div>%body%</div><hr/><span>Published %pub_date% by %authors%</span>',
                             array('%name%' => $this->name,
                                   '%body%' => nl2br($this->body),
                                   '%pub_date%' => $pub_dates[$this->pub_date],
                                   '%authors%' => $authors[$this->authors])) ;
        }

        public function rules() {
                return array(
                        // name, email, subject and body are required
                        array('step, name, body', 'required', 'on' => 'step1'),
                        array('step, name, body, pub_date, authors', 'required', 'on' => 'step2'),
                        array('step, name, body, pub_date, authors', 'required', 'on' => 'step3'),
                        ) ;
        }
}
?>
This model defines 3 rules, which will be our wizard steps: In the first step, we display only the name and body fields. The step field is hidden and contains … the current step! On submit, if the validation passes, we display the second step, where the pub_date and the authors are shown. The name and body fields are hidden and filled with the values of the previous step.
In the DefaultController file, and replaces the code of the actionIndex method with the one below:
<?php
public function actionIndex() {
            // get the step. If a previous step has been posted, the step is
    // found in this previous form's fields. Otherwise we set it to
    // 1
    $step = isset($_POST['WizardForm']) && isset($_POST['WizardForm']['step']) ?
        $_POST['WizardForm']['step'] : 1 ;
    $model = new WizardForm('step' . $step) ;

    $form = new CForm('application.views.site.wizard_step' . $step,
              $model) ;

    // validate the previous step
    if(isset($_POST['WizardForm'])) {
        $model->attributes=$_POST['WizardForm'] ;
        if ($form->validate()) {
            // The previous step has validated
            if ($step < 3) {
                // build the current step form if we're
                // not at the last step
                $step++ ;
                $model = new WizardForm('step' . $step) ;
                $model->attributes=$_POST['WizardForm'] ;
                $form = new CForm('application.views.site.wizard_step' . $step,
                          $model) ;
            }
        }
    }
    $preview = $step == 3 ? $model->preview() : '' ;

    $this->render('wizard', array('form' => $form,
                      'preview' => $preview,
                      'step' => $step)) ;
}
This action uses a template called wizard.php and located in views/site/wizard.php. So create this file and add the following content:
<h1>Step <?php echo $step ; ?></h1>

<div class="form">
        <?php echo $form ; ?>
</div><!-- form -->
<?php if ($preview) : ?>
        <?php echo $preview ; ?>
<?php endif; 
The view is dead-simple because the actual content is in the form “templates”. For each step, we have a dedicated one:
// views.site.wizard_step1.php
<?php return array(
        'elements' => array(
                'step' => array(
                        'type' => 'hidden',
                        'value' => 1,
                ),
                'name' => array(
                        'type' =>'text'
                ),
                'body' => array(
                        'type' => 'textarea'
                )
        ),
        'buttons' => array(
                'submit' => array('type' => 'submit',
                                  'value' => Yii::t('default', 'Step2'))
        )
) ;

// views.site.wizard_step2.php
<?php return array(
        'elements' => array(
                'step' => array(
                        'type' => 'hidden',
                        'value' => 2,
                ),
                'name' => array(
                        'type' =>'hidden'
                ),
                'body' => array(
                        'type' => 'hidden'
                ),
                'pub_date' => array(
                        'type' => 'dropdownlist',
                        'items' => WizardForm::getPubDates()
                ),
                'authors' => array(
                        'type' => 'dropdownlist',
                        'items' => WizardForm::getAuthors()
                )
        ),
        'buttons' => array(
                'submit' => array('type' => 'submit',
                                  'value' => Yii::t('default', 'Publish'))
        )
) ;

// views.site.wizard_step3.php
<?php return array(
        'elements' => array(
                'step' => array(
                        'type' => 'hidden',
                        'value' => 2,
                ),
                'name' => array(
                        'type' =>'hidden'
                ),
                'body' => array(
                        'type' => 'hidden'
                ),
                'pub_date' => array(
                        'type' => 'hidden',
                        #'values' => array('yesterday', 'today', 'tomorrow')
                ),
                'authors' => array(
                        'type' => 'hidden'
                )
        )
) ;
And that’s all ! You can see your wizard working in your browser.
The code is available on github bitbucket if you’re interested.

No comments:

Post a Comment