Extended core validation rules

I18n Translation

Some translate the rules in the view – but it usually creates redundancy. In some projects this might be intentional. I like to keep the error messages centralized, though.

For that, you can just override the core translation rule – add this to app_model.php:

/**
 * Overrides the Core invalidate function from the Model class
 * with the addition to use internationalization (I18n and L10n)
 * @param string $field Name of the table column
 * @param mixed $value The message or value which should be returned
 * @param bool $translate If translation should be done here
 */
public function invalidate($field, $value = null, $translate = true) {
    if (!is_array($this->validationErrors)) {
        $this->validationErrors = array();
    }
    if (empty($value)) {
        $value = true;
    } else {
        $value = (array )$value;
    }

    //TODO: make more generic?
    if (is_array($value)) {
        $value[0] = $translate?__($value[0], true) : $value[0];

        if (count($value) > 3) { # string %s %s string, trans1, trans2
            $value = sprintf($value[0], $value[1], $value[2], $value[3]);
        } elseif (count($value) > 2) { # string %s %s string, trans1, trans2
            $value = sprintf($value[0], $value[1], $value[2]);
        } elseif (count($value) > 1) { # string %s string, trans1
            $value = sprintf($value[0], $value[1]);
        } else {
            $value = $value[0];
        }
    }
    $this->validationErrors[$field] = $value;
}

Usage (some examples):

var $validate = array(
    'username' => array(
        'notEmpty' => array(
            'rule' => array('notEmpty'),
            'message' => 'valErrEmpty', // some short form to translate
            //normal sentences would of course work, too
        ),
    ),
    'pwd' => array(
        'between' => array(
            'rule' => array('between', 6, 30),
            'message' => array('valErrBetweenCharacters %s %s', 6, 30), // short form string with variables
            // maybe resulting in something like "Between 6 and 30 chars" as defined in locale.po
        )
    ),
    'code' => array(
        'maxLength' => array(
            'rule' => array('maxLength', 5),
            'message' => array('The code cannot be longer than %s chars', 5), // normal text with a variable
        )
    ),
    ...
);

I like the short forms like "valErrBetweenCharacters %s %s" – they don’t have a certain "grammar", so if you want to change the translation result "Between %s and %s chars" to "Please insert between %s and %s chars" this can be done with one change in locale.po instead of changing all xxx places you used this rule.

Working with dynamic error messages

If you are in need for a way to work with dynamically concatenated message strings you need to use the __construct method:

public function __construct($id = false, $table = null, $ds = null) {
    parent::__construct($id, $table, $ds);
        
    $this->validate['code']['maxLength']['message'] = array('valErrCodeLength %s', Configure::read('Code.maxLength'));
}

This will either override the existing placeholder message or add it to the rule array if it didn’t contain a message param yet.
You can also add complete rules dynamically this way:

$this->validate['code']['extraRule'] = array(...);

// or replace ALL rules for a field:
$this->validate['code'] = array('ruleOne'=>array(...), 'ruleTwo'=>array(...), ...);

Just make sure your rules all contain the last => true setting then.

Custom validation rules

Here is one example how to use a custom validation rule with one param (custom rules can be placed in the model for local validation or the app_model for global validation):

public $validate = array(
    'pwd_repeat' => array(
        'validateIdentical' => array(
            'rule' => array('validateIdentical', 'pwd'), // we want to compare this to "pwd"
            'message' => 'valErrPwdNotMatch',
        ),	
    )
);

// in app_model.php:
/**
 * checks if the content of 2 fields are equal
 * Does not check on empty fields! Return TRUE even if both are empty (secure against empty in another rule)!
* //TODO: make it more generic with Model.field syntax
 */
public function validateIdentical($field, $compareWith) {
    return ($this->data[$this->alias][$compareWith] === array_shift($field));
}

Multiple validation rules

The Cookbook states:
"By default CakePHP tries to validate a field using all the validation rules declared for it and returns the error message for the last failing rule"
I don’t really like this default behavior – what sense does it make to validate all if only the last error can be returned anyway. But as long as "last" is not true by default, we have to manually set it:

var $validate = array(
    'login' => array(
        'loginRule-1' => array(
            'rule' => 'alphaNumeric',  
            'message' => '...',
            'last' => true
         ),
        'loginRule-2' => array(
            'rule' => array('minLength', 8),  
            'message' => '...'
        )  
    )
);

The rules are validated top down. So if the first one fails, it will now return the error right away. Usually the following validation rules rely on a positive result of the predecessor. At least, thats how you should arrange your rules.

One example:
[email] (in this order – each with a specific error message)

  • notEmpty
  • email
  • undisposable (custom – per vendor)
  • notBlocked (custom – per webservice)

As you can see, it would not make sense to check all of them every time. It really slows down the validation – especially if the email address is not even valid. So we first want to check the simple stuff and then move on to the advanced (and sometimes more time consuming) rules. All rules get "last"=>true to achieve that.

Other quite handy custom translation rules

They can be put into app_model.php:

/**
 * checks a record, if it is unique - depending on other fields in this table (transfered as array)
 * example in model: 'rule' => array ('uniqueRecord',array('belongs_to_table_id','some_id','user_id')),
 * if all keys (of the array transferred) match a record, return false, otherwise true
 * @param ARRAY other fields
 * TODO: add possibity of deep nested validation (User -> Comment -> CommentCategory: UNIQUE comment_id, Comment.user_id)
 */
public function validateUnique($arguments, $fields = array(), $options = null) {
    $id = (!empty($this->data[$this->alias]['id'])?$this->data[$this->alias]['id'] : 0);

    foreach ($arguments as $key => $value) {
        $fieldName = $key;
        $fieldValue = $value; // equals: $this->data[$this->alias][$fieldName]
    }

    if (empty($fieldName) || empty($fieldValue)) { // return true, if nothing is transfered (check on that first)
        return true;
    }

    $conditions = array($this->alias.'.'.$fieldName => $fieldValue, // Model.field => $this->data['Model']['field']
        $this->alias.'.id !=' => $id, );

    foreach ((array )$fields as $dependingField) {
        if (!empty($this->data[$this->alias][$dependingField])) { // add ONLY if some content is transfered (check on that first!)
            $conditions[$this->alias.'.'.$dependingField] = $this->data[$this->alias][$dependingField];

        } elseif (!empty($this->data['Validation'][$dependingField])) { // add ONLY if some content is transfered (check on that first!
            $conditions[$this->alias.'.'.$dependingField] = $this->data['Validation'][$dependingField];

        } elseif (!empty($id)) {
            # manual query! (only possible on edit)
            $res = $this->find('first', array('fields' => array($this->alias.'.'.$dependingField), 'conditions' => array($this->alias.'.id' => $this->data[$this->alias]['id'])));
            if (!empty($res)) {
                $conditions[$this->alias.'.'.$dependingField] = $res[$this->alias][$dependingField];
            }
        }
    }

    $this->recursive = -1;
    if (count($conditions) > 2) {
        $this->recursive = 0;
    }
    $res = $this->find('first', array('fields' => array($this->alias.'.id'), 'conditions' => $conditions));
    if (!empty($res)) {
        return false;
    }

    return true;
}

// validateUrl and validateUndisposable are to come

More

See the Cookbook for details on that matter (deprecated 1.2).

2 Comments

  1. Well, I keep them simple and then this will later translate into the actual localized one for me.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.