Yii 1.1: Add model validator in Controller

3 followers

There are cases that model validators should be depended by controller/action and you couldn't manipulate on the model class or using scenarios in easy way

For example, when inserting a new record we want the controller/action to deside whether or not a validator (or more than one) should be applied.

So a robust way to do that is by insert a validator to the model on Controller/action

For example

class MyUser extends CActiveRecord {
 
 public function rules() {
    array('user,email,password', 'required', 'on' => 'insert'),
    array('user,email,password,city,postcode','length', 'min' => 4, 'max' => 15),
 }
 
}

In Controller/action

function actionCreateUser($type) {
$model = new MyUser();
...
if ($type=='company') {
    $model->validatorList->add(
        CValidator::createValidator('required', $model, 'city,postcode')
    );
}
.... now set massive data,validate and save
}

PS: You could check the type of user on beforeValidate Model method adding a validator conditionally but in fact is more easy and more MVC-way to do that in controller.

Why ? If we add this condition to the model we give to the Model more logic task. But MVC should have more seperated role for Model Viewer and Controller and not overloaded micro-conception MVC architecture

see this article to get another real issue what exactly I mean http://www.yiiframework.com/forum/index.php/topic/43999-activate-a-captcha-after-of-several-times/

Total 4 comments

#18411 report it
Kostas Apazidis (KonApaz) at 2014/10/25 05:01am
All about that

Hi le_top again!

In my case I had use too many complicated rules,scenarios (on and except) also user permissions, csv importer, dependency Model, Components etc that make it almost impossible to do that! (I had to spend much much time!... and headache too :))

So Ι used the wiki approach and safe me a lot of things!

I agree, validators should be handled by the model according to MVC. But In fact there are exceptions that make application too complicated (and I mean that!) misbehaved from the MVC pattern even if we use MVC architecture rules strictly.

So (In my opinion) I prefer to make an exception of MVC rules rather than complicated and may unstable rules that drives to the many exception MVC rules!

Keep in mind that all above is about rarely issues and not the rule!

All opinion are weclome :)

#18408 report it
le_top at 2014/10/24 05:20pm
Handling more complex cases

IMHO, only the model should know about how to do the validation. Putting the validators in the model will help ensure that any validation that is added is applied everywhere.

In fact, I do not like scenarios that much and I prefer putting the conditions inside the validators. One can use YiiConditionalValidator for that, or create a custom function.

As a real example, I work with devices in one application and these devices have serial numbers. When a device is added to the database (manual entry, CSV import, ...), the application checks the serial number's validity (checksum, and other rules). However there are different types of devices, all performing a similar function, but having different rules for the serial numbers. For several reasons, the device type has to be known, one of the reasons is the serial number, but other reasons are the capabilities of the devices.

To check the serial number of the device, there is a rule like this:

array('device_identifier','SerialNumberValidator','message'=>Yii::t('app','The serial number is invalid')),

SerialNumberValidator is a custom CValidator. Actually this SerialNumberValidator acts as a proxy for the specific validator. The proxy function is implemented by using a factory method that provides the appropriate validator for a given device type.

This "ProxyValidator" is not completely generic yet, but almost. The implementation below will look for an attribute called 'type' or 'type_id' amongst the attributes of the model. The value of that attribute is then used to select the appropriate validator from the '$map'. Hence, the validation is completely dependent on the 'type_id'.

The 'type_id' in your example would correspond to user/company/... and would have to be one of the attributes of 'MyUser'. Then, where ever you use 'MyUser' in your application, the validation is always done while taking into account the 'type_id'.

In my application where devices are used, this means that I can add a new Device type by changing only the Device Model class and the SerialValidator class and have validation operating in all use cases.

You are entitled to a different opinion, by in my humble opinion setting the validator in the controller is bad practice. The model should know about its validation, not the controller. If you can't have the model select the validator because it depends on other models to know how to validate, you probably need some kind of "SuperModel" (likely a Facade) that knows how the lower level models are interdependent and that would have its own validation rules that add to the rules of the lower level models. Or, a model could also depend on a database lookup to validate a given property if you can allow it to know about the other models.

The SerialValidator class (almost a generic ProxyValidator) goes like this (I changed some 'private' stuff):

<?php
/**
 *proxy class that automatically adapts to the validator to type of the device
 *
 */
class SerialNumberValidator extends CValidator {
    /**
     * Define validators for the serial numbers.
     * @var unknown
     */
    public static $map =array(
            0=>'FirstValidator',
            1=>'SecondValidator',
            ...
    );
 
    public $defaultValidator = 'CStringValidator';
 
    protected function validateAttribute($object,$attributes)
    {
        $this->getValidator($object)->validateAttribute($object,$attributes);
    }
 
    /**
     * Returns the JavaScript needed for performing client-side validation.
     * @param CModel $object the data object being validated
     * @param string $attribute the name of the attribute to be validated.
     * @return string the client-side validation script.
     * @see CActiveForm::enableClientValidation
     */
    public function clientValidateAttribute($object,$attribute)
    {
        /*
        $js="";
        $typevar="type_id"; // For example.
        foreach(self::$map as $key=>$validator) {
            $jsSerial=$this->getValidator($object,$key);
            if("$jsSerial" !== "") {
                $js.=<<<EOJS
                if($typevar==$key) {
                    $jsSerial
                }
EOJS;
            }
        }
        return $js;
        */
    }
 
    /** @var CValidator */
    private $_validator=array();
 
    /**
     * @param CModel $object
     * @param string $type_attribute
     * @return CValidator
     */
    private function getValidator($object,$type_attribute=null) {
        if(!isset($this->_validator[$type_attribute])) {
            if($type_attribute===null) {
                foreach( array('type_id','type') as $attr) {
                    if(isset($object->$attr)) {
                        $type_attribute = $attr;
                    }
                }
            }
            if($type_attribute===null) {
                throw new InvalidArgumentException("No assigned type attribute found for ".get_class($object));
            }
            if (array_key_exists($object->$type_attribute, self::$map)){
                $validatorClass = self::$map[$object->$type_attribute];
            } else {
                $validatorClass=$this->defaultValidator;
            }
 
            $this->_validator[$type_attribute]=new $validatorClass;
            $attributes = array_merge(
                    // Attributes known in CValidator
                    array('on','attributes','message','skipOnError','except','safe','enableClientValidation',),
                    // Extra attributes
                    $this->attributeNames());
            $reflection = new ReflectionClass($validatorClass);
            $availableProperties = array_map(function($d){return $d->getName();},$reflection->getProperties());
            foreach($attributes as $attr) {
                if(isset($this->$attr) && (in_array($attr,$availableProperties) || $this->_validator[$type_attribute]->canSetProperty($attr))) {
                    $this->_validator[$type_attribute]->$attr = $this->$attr;
                }
            }
        }
        return $this->_validator[$type_attribute];
    }
 
 
    private $_data = array();
 
    public function __get($key) {
        return (array_key_exists($key,$this->_data) ? $this->_data[$key] : null);
    }
 
    public function __set($key, $value) {
        $this->_data[$key] = $value;
    }
 
    public function __isset($name) {
        return array_key_exists($name,$this->_data);
    }
 
    public function attributeNames() {
        return array_keys($this->_data);
    }
}
#18400 report it
Kostas Apazidis (KonApaz) at 2014/10/24 05:26am
RE: Use a scenario

I agree with you in more cases.

But there are cases that the 'control' of Validators should be in Controller rather than in Model

In fact, for web application (especially in large scale) with many combination of 'scenarios' and compinated Validators (for example company,company_brand,company_organization etc) is more difficult to handle with scenarios in the Model.

So both of two ways is acceptable depedend of the issue.

#18396 report it
le_top at 2014/10/23 08:49pm
Use a scenario

A better way is to use scenarios.

class MyUser extends CActiveRecord {
 
 public function rules() {
    array('user,email,password', 'required', 'on' => 'insert'),
    array('user,email,password,city,postcode','length', 'min' => 4, 'max' => 15),
    array('user,email,password,city,postcode','required','on'=>'insertcompany'),
 }
 
}
function actionCreateUser($type) {
$scenario=($type==='company')?'insertcompany':'insert';
$model = new MyUser($scenario);
 
.... now set massive data,validate and save
}

Leave a comment

Please to leave your comment.

Write new article