《Yii1.1 Application Development Cookbook》

Chapter 4: Working with Forms

In this chapter, we will cover:

  • Writing your own validators
  • Uploading files
  • Adding CAPTCHA
  • Customizing CAPTCHA
  • Creating a custom input widget with CWidget

Introduction

Yii makes working with forms a breeze and the documentation on it is almost complete. Still, there are some areas that need clarification and examples. We will describe them in this chapter.

Writing your own validators

Yii provides a good set of built-in form validators which cover the most typical of developer needs and are highly configurable. However, in some cases, the developer may face a need to create a custom validator.

A good example would be the website ownership validation. For a few of their services, Google requires you to upload a file with the name and content specified to your website and then checks if it is there. We will do the same.

There are two ways to achieve it. First, you can use a class method as validator, and the second way is to create a separate class.

Getting ready

Create a new application using yiic webapp as described in the official guide.

How to do it...

  1. We will start with the class method approach. First, we need to implement a form model. So, create protected/models/SiteConfirmation.php as follows:
<?php
class SiteConfirmation extends CFormModel {
    public $url;
    
    public function rules()
    {
        return array(
            array('url', 'confirm'),
        );
    }
 
    public function confirm($attribute,$params)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        curl_close($ch);
        if(trim($output)!='code here')
            $this->addError('url','Please upload file first.');
    }
}
  1. Now we will use our model from our test controller. Create protected/ controllers/TestController.php as follows:
<?php
class TestController extends CController
{
    function actionIndex()
    {
        $confirmation = new SiteConfirmation();
        $confirmation->url = 'http://yiicookbook.org/verify.html';
        if($confirmation->validate())
            echo 'OK';
        else
            echo 'Please upload a file.';
    }
}
  1. Now try to run the test controller. You should get OK because there is a file with the code here text available from http://yiicookbook.org/verify.html. If you replace the confirmation URL with another one, then you get Please upload file first as there is no such file uploaded.

How it works...

In the SiteConfirmation model, we define a $url field and add a rules method that defines a single validation rule for this field. As there is no built-in validator named confirm, Yii assumes that we want to describe the validation rule in a method named confirm. In this method, we use standard PHP's CURL to get verify.html file contents from a remote host and compare its content with the code here string. If the file content is different, then we add an error using the addError method.

Optionally, we can use two validation method arguments: $attribute and $params. For example, if we specify the validation rule in the following way:

array('url', 'confirm', 'param1' => 'value1', 'param2' => 'value3'),

then we get the $attribute value set to 'url' and the $params value set to the following:

array (
    'param1' => 'value1'
    'param2' => 'value3'
)

There's more...

As we probably want to reuse this kind of validator, we will move its functionality from a model method to a separate class. So, create a file named protected/components/ RemoteFileValidator.php as follows:

<?php
class RemoteFileValidator extends CValidator
{
    public $content = '';
    protected function validateAttribute($object,$attribute)
    {
        $value=$object->$attribute;
        
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $value);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        curl_close($ch);
        
        if(trim($output)!=$this->content)
            $this->addError($object,$attribute,'Please upload file first.');
    }
}

The custom validator class should extend CValidator and implement its abstract validateAttribute method. Arguments passed on to it are $object, which is an instance of the model validated, and $attribute that contains validated attribute name. Parameters passed are assigned to corresponding public properties of the validator class.

That is it. Now, we will use it. In the SiteConfirmation model, we should change the validation rule to the following:

array('url', 'RemoteFileValidator', 'content' => 'code here'),

Here we have used an external validator name. If there is no method with the same name in the model and no same named built-in validator, then Yii will try to find an external validator class with the name or path alias specified.

The rest of the code stays untouched and now you can reuse the validator in other models.

Further reading

For further information, refer to the following URLs:

  • http://www.yiiframework.com/doc/api/CValidator/
  • http://www.yiiframework.com/doc/api/CModel#rules-detail

Uploading files

Handling file uploads is a pretty common task for a web application. Yii has some helpful classes built-in. Let's create a simple form that will allow uploading ZIP archives and storing them in protected/uploads.

Getting ready

  • Create a fresh application using yiic webapp
  • In your protected directory, create an uploads directory

How to do it...

  1. We will start with the model, so create protected/models/Upload.php as follows:
<?php
class Upload extends CFormModel
{
    public $file;
    
    public function rules()
    {
        return array(
            array('file', 'file', 'types'=>'zip'),
        );
    }
}
  1. Now we will move on to the controller, so create protected/controllers/UploadController.php:
<?php
class UploadController extends Controller
{
    function actionIndex()
    {
        $dir = Yii::getPathOfAlias('application.uploads');
        $uploaded = false;
        
        $model=new Upload();
        
        if(isset($_POST['Upload']))
        {
            $model->attributes=$_POST['Upload'];
            $file=CUploadedFile::getInstance($model,'file');
            if($model->validate()){
                $uploaded = $file->saveAs($dir.'/'.$file->getName());
            }
        }
        
        $this->render('index', array(
            'model' => $model,
            'uploaded' => $uploaded,
            'dir' => $dir,
        ));
    }
}
  1. Finally, a view protected/views/upload/index.php:
<?php if($uploaded):?>
<p>File was uploaded. Check <?php echo $dir?>.</p>
<?php endif ?>
<?php echo CHtml::beginForm('','post',array
        ('enctype'=>'multipart/form-data'))?>
    <?php echo CHtml::error($model, 'file')?>
    <?php echo CHtml::activeFileField($model, 'file')?>
    <?php echo CHtml::submitButton('Upload')?>
<?php echo CHtml::endForm()?>
  1. That is it. Now run the upload controller and try uploading both ZIP archives and other files, as shown in the following screenshot:

How it works...

The model we use is pretty simple. We define only one field named $file and a validation rule that uses file validator (CFileValidator) which reads "only zip files are allowed".

Controller is a bit more complicated. We will review it line by line:

$dir = Yii::getPathOfAlias('application.uploads');
$uploaded = false;
$model=new Upload();
if(isset($_POST['Upload']))
{
    $model->attributes=$_POST['Upload'];

$dir is a directory that will hold the ZIP archives uploaded. We set it to protected/ uploads using an alias. $uploaded is a flag that determines if we need to display a success message. Then, we create a model instance and fill it with data from $_POST if form is submitted.

$file=CUploadedFile::getInstance($model,'file');
if($model->validate()){
    $file->saveAs($dir.'/'.$file->getName());
    $uploaded = true;

Then, we use CUploadedFile::getInstance that gives us access to use CUploadedFile instance. This class is a wrapper around the $_FILE array that PHP fills when the file is uploaded.

If we make sure that the file is a ZIP archive by calling the model's validate method, then we save the file using CUploadedFile::saveAs.

The rest is passing some values to the view:

<?php if($uploaded):?>
<p>File was uploaded. Check <?php echo $dir?>.</p>
<?php endif ?>

If there is a $uploaded flag set to true, then we display a message.

In order to upload a file, the HTML form must meet the following two important requirements:

  1. Method should be set to POST.
  2. The enctype attribute should be set to 'multipart/form-data'.

We can generate such HTML using the CHtml helper or CActiveForm with htmlOptions set. This time, CHtml was used:

<?php echo CHtml::beginForm('','post',array('enctype'=>'multipart/form-data'))?>

The rest is the standard form: We display an error and a field for model's file attribute and render a submit button.

There's more...

If you want to upload multiple files, then you should modify the code in the following way:

if(isset($_POST['Upload']))
{
    $model->attributes=$_POST['Upload'];
    $files=CUploadedFile::getInstance($model,'file');
    if($model->validate())
    {
        foreach($files as $file)
            $file->saveAs($dir.'/'.$file->getName());

In a view, you should echo file fields in the following way:

<?php echo CHtml::activeFileField($model, "[0]file")?>
<?php echo CHtml::activeFileField($model, "[1]file")?>
<?php echo CHtml::activeFileField($model, "[2]file")?>
File validation

The file validator we use in a model allows us not only to limit files to a certain type, but also to set other limits, such as file size or number of files in case of a multiple file upload. For example, the following rule will only allow uploading images with file size less than one megabyte:

array('file', 'file', 'types'=>'jpg, gif, png', 'maxSize' => 1048576),

Further reading

For further information, refer to the following URLs:

  • http://www.yiiframework.com/doc/api/CFileValidator
  • http://www.yiiframework.com/doc/api/CUploadedFile

See also

  • The recipe named Handling variable number of inputs in Chapter 3, AJAX and jQuery

Adding CAPTCHA

Nowadays, on the Internet, if you leave a form without a spam protection, you will get a ton of spam data entered in a short time. Yii includes a CAPTCHA component that makes adding such a protection a breeze. The only problem is that there is no systematic guide on how to use it.

In the following example, we will add a CAPTCHA protection to a simple form.

Getting ready

  1. Create a fresh application using yiic webapp
  2. Create a form model named protected/models/EmailForm.php as follows:
<?php
class EmailForm extends CFormModel
{
    public $email;
    function rules(){
        return array(
            array('email', 'email'),
        );
    }
}
  1. Create a controller named protected/controllers/EmailController.php as follows:
<?php
class EmailController extends Controller
{
    public function actionIndex()
    {
        $success = false;
        $model = new EmailForm();
        if(!empty($_POST['EmailForm']))
        {
            $model->setAttributes($_POST['EmailForm']);
            if($model->validate())
            {
                $success = true;
                // handle form here
            }
        }
        $this->render('index', array(
            'model' => $model,
            'success' => $success,
        ));
    }
}
  1. Create a view named protected/views/email/index.php as follows:
<?php if($success):?>
<p>Success!</p>
<?php endif?>
<?php echo CHtml::beginForm()?>
    <p>
        <?php echo CHtml::activeLabel($model, 'email')?>
        <?php echo CHtml::activeTextField($model, 'email')?>
        <?php echo CHtml::error($model, 'email')?>
    </p>
    <p>
        <?php echo CHtml::submitButton()?>
    </p>
<?php echo CHtml::endForm()?>
  1. Now, we have an e-mail submission form which validates the e-mail field. Let's add CAPTCHA.

How to do it...

  1. First, we need to customize the form model. We need to add $verifyCode which will hold the verification code entered and add a validation rule for it.
<?php
class EmailForm extends CFormModel
{
    public $verifyCode;
    public $email;
    
    function rules(){
        return array(
            array('email', 'email'),
            array('verifyCode', 'captcha', 'allowEmpty'=>
                !CCaptcha::checkRequirements()),
        );
    }
}
  1. Then, we need to add an external action to the controller. Add the following code to it:
public function actions()
{
    return array(
        'captcha'=>array(
            'class'=>'CCaptchaAction',
        ),
    );
}
  1. In a view, we need to show an additional field and the CAPTCHA image. The following code will do this for us:
<?php if(CCaptcha::checkRequirements()&& Yii::app()->user->isGuest):?>
    <p>
        <?php echo CHtml::activeLabelEx($model, 'verifyCode')?>
        <?php $this->widget('CCaptcha')?>
    </p>
    <p>
        <?php echo CHtml::activeTextField($model, 'verifyCode')?>
        <?php echo CHtml::error($model, 'verifyCode')?>
    </p>
<?php endif?>
  1. That is it. Now, you can run the email controller and check CAPTCHA in action, as shown in the following screenshot:

If there are no errors on the screen and no CAPTCHA field in the form, then most probably, you don't have the GD PHP extension installed and configured. GD is required for CAPTCHA because it generates images. We have added several CCaptcha::checkRequirements() checks, so the application will not use CAPTCHA if the image cannot be displayed, but will still work.

How it works...

In a view, we call the CCaptcha widget that renders the <img tag with a src attribute pointing to CCaptchaAction we added to the controller. In this action, an image with a random word is generated. The word generated is a code that the user should enter into the form. It is stored in a user session and an image is displayed to the user.

When the user enters the e-mail and verification code into the form, we assign these values to the form model and then validate it. For the verification of the code field, we use CCaptchaValidator. It gets the code from the user session and compares it to the code entered. If they don't match, then the model data is considered invalid.

There's more...

If you restrict access to controller actions by using the accessRules controller method, then don't forget to grant everyone access to it:

public function accessRules() {
    return array(
        // ...
        array('allow',
            'actions'=>array('captcha'),
            'users'=>array('*'),
        ),
        array('deny',
            'users'=>array('*'),
        ),
    );
}
Further reading:

For further information, refer to the following URLs:

  • http://www.yiiframework.com/doc/api/CCaptcha/
  • http://www.yiiframework.com/doc/api/CCaptchaAction/
  • http://www.yiiframework.com/doc/api/CCaptchaValidator/

See also

  • The recipe named Using external actions in Chapter 2, Router, Controller, and Views
  • The recipe named Customizing CAPTCHA in this chapter

Customizing CAPTCHA

A standard Yii CAPTCHA is good enough to protect you from spam, but there are situations where you may want to customize it, such as the following:

  • You face a spam-bot that can read image text and need to add more challenge
  • You want to make it more interesting or easier to enter the CAPTCHA text

In our example, we will modify Yii's CAPTCHA, so it will require the user to solve a really simple arithmetic puzzle instead of just repeating a text in an image.

Getting ready

As a starting point for this example, we will take the result of the Adding CAPTCHA recipe. Alternatively, you can take any form that uses CAPTCHA as we are not modifying the existing code a lot.

How to do it...

We need to customize CCaptchaAction which generates the code and renders its image representation. The code should be a random number and the representation should be an arithmetic expression which gives the same result.

  1. Create protected/components/MathCaptchaAction.php as follows:
<?php
class MathCaptchaAction extends CCaptchaAction
{
    protected function generateVerifyCode()
    {
        return mt_rand((int)$this->minLength, (int)$this->maxLength);
    }
    
    public function renderImage($code)
    {
        parent::renderImage($this->getText($code));
    }
    
    protected function getText($code)
    {
        $code = (int)$code;
        $rand = mt_rand(1, $code-1);
        $op = mt_rand(0, 1);
        if($op)
            return $code-$rand.»+».$rand;
        else
        return $code+$rand.»-».$rand;
    }
}
  1. Now, in our controller actions method, we need to replace CCaptchaAction with our own CAPTCHA action as follows:
public function actions()
{
    return array(
        'captcha'=>array(
            'class'=>'MathCaptchaAction',
            'minLength' => 1,
            'maxLength' => 10,
        ),
    );
}
  1. Now, run your form and try the new CAPTCHA. It will show arithmetic expressions with numbers from 1 to 10 and will require entering an answer, as shown in the following screenshot:

How it works...

We override two CCaptchaAction methods. In generateVerifyCode, we generate a random number instead of text. As we need to render an expression instead of just showing text, we override renderImage. The expression itself is generated in our custom getText method.

There's more

In order to learn more about CAPTCHA, you can use the following resources:

  • http://www.yiiframework.com/doc/api/CCaptcha/
  • http://www.yiiframework.com/doc/api/CCaptchaAction/
  • http://www.yiiframework.com/doc/api/CCaptchaValidator/

See also

  • The recipe named Using external actions in Chapter 3, Router, Controller, and Views
  • The recipe named Adding CAPTCHA in this chapter

Creating a custom input widget with CInputWidget

Yii has a very good set of form widgets, but as every framework out there, Yii cannot have them all. In this recipe, we will learn how to create your own input widget. For our example, we will create a range input widget.

Getting ready

Create a fresh application by using yiic webapp.

How to do it...

We will start with the widget itself.

  1. Create a widget class named protected/components/RangeInputField.php as follows:
<?php
class RangeInputField extends CInputWidget
{
    public $attributeFrom;
    public $attributeTo;
    
    public $nameFrom;
    public $nameTo;
    
    public $valueFrom;
    public $valueTo;
    
    function run()
    {
        if($this->hasModel())
        {
            echo CHtml::activeTextField
                ($this->model, $this->attributeFrom);
            echo ' &rarr; ';
            echo CHtml::activeTextField
                ($this->model, $this->attributeTo);
        }
        else {
            echo CHtml::textField($this->nameFrom, $this->valueFrom);
            echo ' &rarr; ';
            echo CHtml::textField($this->nameTo, $this->valueTo);
        }
    }
}
  1. Now we need to test how it works. We will need a form model named protected/models/RangeForm.php:
<?php
class RangeForm extends CFormModel
{
    public $from;
    public $to;
    
    function rules()
    {
        return array(
            array('from, to', 'numerical', 'integerOnly' => true),
            array('from', 'compare', 'compareAttribute' => 'to',
                'operator' => '<=', 'skipOnError' => true),
        );
    }
}
  1. Now create a controller named protected/controllers/RangeController. php as follows:
<?php
class RangeController extends Controller
{
    function actionIndex()
    {
        $success = false;
        $model = new RangeForm();
        if(!empty($_POST['RangeForm']))
        {
            $model->setAttributes($_POST['RangeForm']);
            if($model->validate())
                $success = true;
        }
        
        $this->render('index', array(
            'model' => $model,
            'success' => $success,
        ));
    }
}
  1. Create a view named protected/views/range/index.php as follows:
<?php if($success):?>
<p>Success!</p>
<?php endif?>
 
<?php echo CHtml::errorSummary($model)?>
<?php echo CHtml::beginForm()?>
    <?php $this->widget('RangeInputField', array(
        'model' => $model,
        'attributeFrom' => 'from',
        'attributeTo' => 'to',
    ))?>
    <?php echo CHtml::submitButton('Submit')?>
<?php echo CHtml::endForm()?>
  1. Now, run the range controller to see a widget in action, as shown in the following screenshot:

How it works...

A typical input widget can be used both with a model as an active field widget and without a model. Active field widget handles the value and validation automatically.

As there are two fields (from and to) in our widget, we define three pairs of public properties: attribute, name, and value. The attribute pair is used if there is a model passed to a widget; this means that the widget is used as an active input. The name and value pairs are used if you want to generate the input with custom names and values.

In our case, we simply override the run method to render two fields in a customized way. Actual field handling is delegated to CHtml::activeTextField and CHtml::textField.

In order to render a widget in a view, we use the CController::widget method as follows:

<?php $this->widget('RangeInputField', array(
    'model' => $model,
    'attributeFrom' => 'from',
    'attributeTo' => 'to',
))?>

All options set in an array are assigned to the corresponding public properties of a widget.

There's more...

In order to learn more about widgets, you can use the following resources:

  • http://www.yiiframework.com/doc/api/CInputWidget/
  • http://www.yiiframework.com/doc/api/CWidget/

See also

  • The recipe named Configuring components in Chapter 1, Under the Hood
  • The recipe named Configuring widget defaults in Chapter 1
评论 X

      友荐云推荐
      Copyright 2011-2014. YiiBook.com