Yet another data validation approach

Published on and tagged with cakephp  programming  validation

The data validation in the current version of CakePHP is limited, so different people presented alternatives (solution of tobius and Felix Geisendörfer, solution of Myles Eftos). Well, I was not fully happy with these solutions, and so I have written my own data validation thing ;-) Nonetheless, a big THANK YOU goes to the aforementioned people, as their work has influenced my solution. And I hope that my solution inspires someone to write an even better solution!

Ok, let us dive in the code. First the model:

class User extends AppModel
{
    var $validate = array('username' => array(
                         array(VALID_NOT_EMPTY, 
                                 'Username is required'),
			 array('isUsernameUnique', 
                                 'Not unique')));

    function isUsernameUnique()
    {
        return (!$this->hasAny(array('User.username' => 
                  $this->data[$this->name]['username'])));
    }
}

In our AppModel (in app/app_model.php) we have to override the function “invalidFields()”. Please note that I present two slightly different “invalidFields()” functions. You need the first version if you use CakePHP up to version 1.0.1.2708, otherwise you have to use the second “invalidFields()” function.

function invalidFields ($data = array()) 
{
    if (!isset($this->validate) || !empty($this->validationErrors))
    {
        if (!isset($this->validate))
        {
            return true;
        }
        else
        {
            return $this->validationErrors;
        }
    }
			 
    if ($data == null)
    {
        if (isset($this->data))
        {
            $data = $this->data;
        }
        else
        {
            $data = array();
        }
    }
		
    $errors = array();	
    $this->set($data);
		
    foreach ($data as $table => $field)
    {
        foreach ($this->validate as $field_name => $validators) 
        {
            foreach($validators as $validator) 
            {
                if (isset($validator[0]))
                {
                    if (method_exists(&$this, $validator[0]))
                    {
                        if (isset($data[$table][$field_name]) && 
                        !call_user_func(array(&$this, $validator[0])))
                        {
                            if (!isset($errors[$field_name]))
                            {
                                $errors[$field_name] = isset($validator[1]) ? 
                                                               $validator[1] : 1;
                            }
                        }
                    }
                    else
                    { 
                        if (isset($data[$table][$field_name]) && 
                !preg_match($validator[0], $data[$table][$field_name]))
                        {
                            if (!isset($errors[$field_name]))
                            {
                                $errors[$field_name] = isset($validator[1]) ? 
                                                               $validator[1] : 1;
                            }
                        }
                    }
                }
            }
        }
    }			
    $this->validationErrors = $errors;
    return $errors;
}

Use this version of “invalidFields()” if you are using a CakePHP with a version number higher than 1.0.1.2708.

function invalidFields ($data = array()) 
{
    if(!$this->beforeValidate())
    {
        return false;
    }

    if (!isset($this->validate) || !empty($this->validationErrors))
    {
        if (!isset($this->validate))
        {
            return true;
        }
        else
        {
            return $this->validationErrors;
        }
    }

    if (isset($this->data))
    {
        $data = array_merge($data, $this->data);
    }
		
    $errors = array();	
    $this->set($data);
		
    foreach ($data as $table => $field)
    {
        foreach ($this->validate as $field_name => $validators) 
        {
            foreach($validators as $validator) 
            {
                if (isset($validator[0]))
                {
                    if (method_exists(&$this, $validator[0]))
                    {
                        if (isset($data[$table][$field_name]) && 
                        !call_user_func(array(&$this, $validator[0])))
                        {
                            if (!isset($errors[$field_name]))
                            {
                                $errors[$field_name] = isset($validator[1]) ? 
                                                               $validator[1] : 1;
                            }
                        }
                    }
                    else
                    { 
                        if (isset($data[$table][$field_name]) && 
                !preg_match($validator[0], $data[$table][$field_name]))
                        {
                            if (!isset($errors[$field_name]))
                            {
                                $errors[$field_name] = isset($validator[1]) ? 
                                                               $validator[1] : 1;
                            }
                        }
                    }
                }
            }
        }
    }			
    $this->validationErrors = $errors;
    return $errors;
}

As “HtmlHelper::tagErrorMsg()” does not fit our needs, we have to write our own function, which we put in a custom helper:

class ErrorHelper extends Helper
{
    function showMessage($target)
    {
        list($model, $field) = explode('/', $target);

        if (isset($this->validationErrors[$model][$field]))
        {
            return sprintf('<div class="error_message">%s</div>', 
                              $this->validationErrors[$model][$field]);
        }
        else
        {
            return null;
        }
    }
}

In your view you can simply use (don’t forget to add the error helper to the helper array in your controller):

echo $error->showMessage('User/username');

That’s it :)

Update (2006-02-12): I created from this post a short tutorial.

Update (2006-04-04): Fixed a small bug in the function invalidFields. The two lines

$this->validationErrors = $errors;
return $errors;

should be outside of the foreach loop. Thanks to Chris for the hint.

Update (2006-05-06): Fixed a small bug which occured with version 1.0 of CakePHP. I changed the default value for the parameter of the invalidFields() function and replaced the first line of that function:

if (!isset($this->validate) || is_array($this->validationErrors))

with

if (!isset($this->validate) || !empty($this->validationErrors))

Thanks to Zachary Naiman!

Update (2006-05-09): Added a code block to invalidFields() which is necessary if you are using a CakePHP revision higher than 2708.

Update (2006-05-11): Created a second version of the function “invalidFields()” which should be used when using CakePHP with a version number higher than 1.0.1.2708.

44 comments baked

  • RosSoft

    Even simplier,
    array(‘username’ => array(
    array(VALID_NOT_EMPTY, ‘Username is required’),
    array(‘isUsernameUnique’,’Not unique’)))

    Then use $validator[1] for message

  • JMG

    Nice idea.

    But there’s a bug : user may use ‘/regexp/’, but he may use ‘#regexp#’ ‘|regexp|’, and so on. So your regexp detection might fail. Why not use method_exists() instead ? If the method exists then this is a callback, if not then it must be a regexp.

  • ThinkingPHP » Validation Time - again

    […] dhofstet just presentet a new validation approach that allows you to use functions as well as regex style validation. But even better, it will let you define your own error messages for everything, laying the foundation for a successfull and easy-to-use pattern for validation. You should definitly have a look at it! […]

  • RosSoft
  • cakebaker

    @RosSoft, JMG: Thanks for your feedback. I have applied it. It is now also possible to omit the message so that you can use HtmlHelper::tagErrorMsg().

    A thank you goes to olle, too. He suggested a better solution for the isUniqueUsername function.

  • RosSoft

    that paste has another solution, creating validation-classes
    http://cakephp.org/pastes/show/ee040ca7f5fd3f709db5c17f028a3a56

  • Troy Schmidt

    Okay. So what… I looked at the custom validation tutorial and it seemed MUCH more involved that what is here. I haven’t yet implemented errormessage because it confuses me so much right now.

    I really like the simplicity of this approach CakeBaker!

  • mememe

    some ideas to take it to the next level:
    degradable ajax form validation :)

  • cake baker » Data validation tutorial

    […] I found the time to write a short tutorial about the data validation approach I described in an earlier post. […]

  • lemp

    Is there a way to use a generic validator function which could be used by many models?

  • sentino » Blog Archive » new approach cakephp validation

    […] while there a lot ways on cakephp data validation cakebaker posted another approach of advance validation in cake. […]

  • cakebaker

    @lemp: maybe it works if you put your validation function to your app model.

  • Tony Summerville

    Is there a way to just create one error message call and place it above the form, instead of creating an error message call for each field?

    For example:

    Instead of “echo $error->showMessage(‘Model/field);”, I want to have something like “echo $error->showMessage();” or “echo $error->showMessage(‘Model’);” and just have it show all the error messages (if there are any).

  • cakebaker

    @Tony Summerville: It shouldn’t be difficult to do that. You only have to change the ErrorHelper. The errors, if any, are available in $this->validationErrors.

  • Tony Summerville

    Yep – I figured it out. I created another function that will show all the messages in an unordered list:

    function showMessages($model)
    {
    if (isset($this->validationErrors))
    {
    $output = ” . “\n”;
    foreach ($this->validationErrors[$model] as $message)
    {
    $output .= “\t” . ” . $message . ” . “\n”;
    }
    $output .= ” . “\n”;
    return $output;
    }
    else
    {
    return null;
    }
    }

  • Tony Summerville

    Damn – some of the HTML tags were stripped out … oh well!

  • cake baker » Test your models

    […] (Notice: The definition of the validation rules is slightly different from the standard way as I use the validation approach I described in an earlier post) […]

  • wluigi

    So my stuff:
    – User::$validate can be a string or an array for multiple validation rules
    – HtmlHelper::tagErrorMsg take a string or an array for multiple error message
    – AppModel::invalidFields give you the occured error and can call a model function for validation.

    /*app_model.php*/
    validate) || is_array($this->validationErrors))
    {
    if (!isset($this->validate))
    {
    return true;
    }
    else
    {
    return $this->validationErrors;
    }
    }

    if ($data == null)
    {
    if (isset($this->data))
    {
    $data = $this->data;
    }
    else
    {
    $data = array();
    }
    }

    $errors = array();
    foreach ($data as $table => $field)
    {
    foreach ($this->validate as $field_name => $validators)
    {
    if(isset($data[$table][$field_name]))
    {
    if(!is_array($validators))
    {
    $validator=$validators;
    if (!method_exists($this, $validator))
    {
    if (!preg_match($validator, $data[$table][$field_name]))
    {
    $errors[$field_name] = 1;
    }
    }
    elseif(!call_user_func(array(&$this, $validator)))
    {
    $errors[$field_name] = 1;
    }
    }
    else
    {
    $i=1;
    foreach($validators as $validator)
    {
    if (!method_exists($this, $validator))
    {
    if (!preg_match($validator, $data[$table][$field_name]))
    {
    $errors[$field_name] = $i;
    }
    }
    elseif(!call_user_func(array(&$this, $validator)))
    {
    $errors[$field_name] = $i;
    }
    $i++;
    }
    }
    }
    }
    $this->validationErrors = $errors;

    return $errors;
    }
    }
    }
    ?>

    /*user.php*/
    array(VALID_NOT_EMPTY,’isLoginUnique’),
    ‘password’=>VALID_NOT_EMPTY,
    ’email’=>array(VALID_EMAIL,VALID_NOT_EMPTY));

    function isLoginUnique()
    {
    return (!$this->hasAny(array(‘User.login’ => $this->data[$this->name][‘login’])));
    }
    }
    ?>

    /*html.php*/
    /**
    * Returns a formatted error message for given FORM field, NULL if no errors.
    *
    * @param string $field A field name, like “Modelname/fieldname”
    * @param string $text Error message
    * @return string If there are errors this method returns an error message, else NULL.
    */
    function tagErrorMsg ($field, $text)
    {
    $error = 1;
    $this->setFormTag($field);
    $myerror=$this->tagIsInvalid($this->model, $this->field);

    if ($myerror >= $error)
    {
    return sprintf(‘%s’, is_array($text)? (empty($text[$myerror-1])? ‘Error in field’: $text[$myerror-1]): $text);
    }
    else
    {
    return null;
    }
    }

    /*user/add.php*/
    tagErrorMsg(‘User/login’, array(‘Login required1.’,’Login not unique’)) ?>

  • chris

    cakebaker, I’ve tried this method, and I’m having incredible problems getting it to work. I’ve added all the code suggested here and in the wiki, and now any form I submit gets saved without being validated first.

    I’m not asking for you to hold my hand about this, I’m just curious if there are issues in the latest cake release, or if there’s some common gotcha I should be on the lookout for.

    thanks

  • kain

    I think there is a problem if we apply this method to a Model/field like User/name.. data cannot be validated.

    ‘name’ => array(array(VALID_NOT_EMPTY))

    I’m using tagerrormsg way

    works with
    var $validate = array(‘name’ => VALID_NOT_EMPTY);
    as normal but we cannot use this string with this method.

  • Zachary Naiman

    My colleagues and I are new to CAKE, and after a couple weeks of evaluating it versus other products (mainly symphony), we decided to use CAKE for all future php development in our office. Shortly after we tried to implement this validation method as CAKE’s current validation functions don’t allow for creating multiple validation rules. As with chris, we followed the tutorial, but invalid data was still being saved to the database and no messages were reported.

    What we’ve figured out so far is that the very first “if” statement in the AppModel “invalidFields” function was being interpreted as true because $this->validationErrors is an (enpty) array. So our quick way around this, which seems to make things work OK, is to either change the condition on that first if statement (put an exclamation point before is_array($this->validationErrors)), or (what seems like a better idea) to put this code before the if statment: if (empty($this->validationErrors)){unset($this->validationErrors);$this->validationErrors=””;}

    Since we’re really new to CAKE, we’re not sure what the implications of this strategy will be, but we’d love some feedback.

    Also, thanks to cakebaker and the whole CAKE community.

  • cakebaker

    @Zachary Naiman: Thanks for your comment. I had the same problem, but “solved” it in a different way. I thought it was a bug in Model->save() and fixed that “bug” ;-) I applied a bugfix and will modify this post and the wiki entry in a moment.

    Thanks, and happy baking :)

  • Brandon Pearce

    I think this is the greatest thing! Makes form validation so much easier. The only thing I see lacking in Cake now in regards to validation is the Form Helper. Wouldn’t it be nice if the Form Helper could tell by looking at the model whether a field is required, and what the error message should be, rather than me having to pass in those values? It would be great to get a form looking like this:
    http://kalsey.com/simplified/form_errors/index.html

    So, instead of this:
    generateInputDiv(‘User/first_name’, ‘First Name’, true, ‘Please enter your first name’)?>

    Just take all the validation from the model, so all you have to type is:
    generateInputDiv(‘User/first_name’)?>

    And it will generate the appropriate error messages, as well as the label (or prompt – just humanize first_name). Wouldn’t that save a lot of typing – and there’s no reason to type the validation messages in the model AND the view.
    (Also, why is there no generatePasswordDiv() function in the form helper?)

    Is this already done somewhere and currently possible with Cake or would someone have to write another helper for this? Any thoughts?

  • cakebaker

    @Brandon Pearce: Interesting idea. I am not aware of such a helper, so you have to write it yourself. I think it is not always possible to get the label text from the model (e.g. if the language of the user interface is not english), so this parameter should be at least optional.

    I think the reason why there is no generatePasswordDiv() function is that it is not possible to automatically create such fields with scaffolding. But you can open a ticket with such a feature request.

  • cake baker » Upgrading to version 1.0 III

    […] Update (2006-05-06): It transpired that the bug was in the advanced validation approach I use, and not in Model->save(). […]

  • olegs

    Hello,
    Why do you need to merge old and new data ?

    if (isset($this->data))
    {
    $data = array_merge($data, $this->data);
    }
    (I’m referring here to the function for Cake>1.0.1.2708. In cake original invalidFields() function there is no such behaviour).

  • cakebaker

    @olegs: I have added this functionality to fix the following bug: https://trac.cakephp.org/ticket/781

  • binoy

    Hi,
    I am using Cake 1.0.1.2708 and I am using this advanced validation technique. It is working fine.

    But I want to send some parameters to the custom function. What I need to change in the code. Any help ?

  • Xadio

    I modified Cakebaker’s Advanced Validation so that I could reduce the number of validating methods and increase reuse through the allowance of parameters. (ie one unique method that checks unique of username and email dynamically) You can find it on the CakePHP Wiki. http://wiki.cakephp.org/tutorials:advanced_validation:advance_validation_with_parameters#advanced_validation_with_parameters

  • cakebaker » Test your models

    […] (Notice: The definition of the validation rules is slightly different from the standard way as I use the validation approach I described in an earlier post) […]

  • Yuri Morini

    if for any reason you need to invalidate a value depending from a controller logic using the same helper you can override also Model::invalidate() in your app_model.php

    function invalidate($field, $message = null) {
    if (!is_array($this->validationErrors)) { $this->validationErrors = }
    $this->validationErrors[$field] = isset($message) ? $message : 1;
    }

    changing the input parameters and the last line of the original function.

  • cakebaker

    @Yuri: Thanks for your addition.

  • Yuri Morini

    Maybe is the wrong approach but if you want to validate a field based on the validation of a previous parameter (i think the Controller is the best choice for logic and this seems logic) you need to check Model::validationErrors before Model::invalidFields() ends.

    An example (data arrives from a registration form and ask for enter password two times):

    var $validate = array(
    ‘password’ =>
    array(
    array(‘validateYourPassword’,’Password not valid’), array(‘validateYourPasswordAgain’,’Password not valid again’)
    ),
    ‘checkpassword’ =>
    array(
    array(‘isPasswordValid’,’Ouch! Check the password error!’),
    array(‘equalToPassword’, ‘Insert the same password’)
    );

    function isPasswordValid() {
    // search $this->validationErrors[“password”]
    // if you find it, password is not valid
    // and also this rule
    }

    The problem here is in the invalidFields function above where Model::validationErrors is empty until the end of function. You can solve this issue changing every call to “$errors” with “$this->validationErrors”. This fills directly our property.

    Any drawback? The only problem i see is to use a sort of logic validating data, and the logic is for the Controller. But also checking a duplicate username (like in your example can be considered logic i think).

    This is my first approach with MVC and cake and i think are great. This site is a great resource. Thanks.

  • cakebaker

    @Yuri: The validation should be done in the model, imho. An often used approach is to put the logic you described in the beforeValidate() function of your model.

    HTH

    PS: I am glad it is a useful resource for you :)

  • Steve

    Great approach.

    I have a form where data from an associated model must also be validated. Let’s say hypothetically I had a model called Post and another model called Comment. For the sake of this example I have one form that allows me to create a post and comment at the same time.

    So input fields would have something like Post/name and Comment/name … etc.

    The Post and Comment models has it’s respective validations. When I print out the $errors variable in the App_Model class it will show errors for both if they don’t validate. However, the errors helper doesn’t print any errors for the associated model.

  • cakebaker

    @Steve: Hm, what’s in $this->validationErrors?

  • Tim

    Check here:

    The Validation-Errors issue.

    It basically returns only the number of validation errors.

  • chess64

    Checking for uniqueness doesn’t seem to work if you are updating existing records…

  • cakebaker

    @chess64: How do you check it?

  • All About Validation in CakePHP 1.2 « Another Cake Baker

    […] and flexibility. This is evidenced by the number of alternatives that people have written such as Daniel Hofstetter, Evan Sagge and Adeel Khan’s ruby-esque […]

  • Dia

    Hi,

    First of all, thanks, Daniel, for this blog, which was and still is very usefull to me

    I use the validation approach you described in this post with Cake 1.1

    I know you posted it a long time ago now but I have a little complement to suggest

    I needed in many models to valid fields with “isMyFieldUnique” functions

    with your solution you have to put one method per field in every model

    here is my solution to use a “isUnique” method in AppModel and how to pass it an array of arguments

    in AppModel, function isUnique :

    function isUnique($params) {
    $fieldName = $params[0];
    if ($this->{$this->primaryKey} == null) // add
    return (!$this->hasAny(array($this->name.’.’.$fieldName => $this->data[$this->name][$fieldName])));
    else // edit
    return (!$this->hasAny(array($this->name.’.’.$fieldName => $this->data[$this->name][$fieldName], $this->name.’.’.$this->primaryKey => ‘!=’.$this->data[$this->name][$this->primaryKey])));
    }

    in AppModel, lines to modify in your invalidFields function :

    […]
    if (isset($validator[0])) {
    if (is_array($validator[0])) {
    $function_name = $validator[0][0];
    $params = $validator[0][1];

    if (method_exists($this, $function_name)) {
    if (isset($data[$table][$field_name])
    and !call_user_func(array($this, $function_name), $params)) {
    if (!isset($errors[$field_name])) {
    $errors[$field_name] = isset ($validator[1]) ? $validator[1] : 1;
    }
    }
    }
    } else {
    if (isset ($data[$table][$field_name]) && !preg_match($validator[0], $data[$table][$field_name])) {
    if (!isset ($errors[$field_name])) {
    $errors[$field_name] = isset ($validator[1]) ? $validator[1] : 1;
    }
    }
    }
    }
    […]

    using it in your model :

    var $validate = array (
    ‘my_field’ => array (
    array (VALID_NOT_EMPTY, ‘Please enter something.’),
    array (array(‘isUnique’, array(‘my_field’)), ‘This name is already used.’)
    )
    );

    $validator[0] has to be an array if you wanna call a callback function
    first element : function name
    second element : parameters array (can be empty or be of the size you need, dun care)

    hope this comment could be usefull for even one person ^^

  • cakebaker

    @Dia: Thanks for your addition! And yes, you are right, the example I showed is not that generic ;-)

  • austintx

    cakebaker, thanks so much for this. very easy to use the model validation piece to this.

    i can’t figure out how to use this if i am trying to “validate” form data in my controller though. it was mentioned above but it doesn’t make sense to this newbie.

    e.g. i have 2 checkboxes where one has to be checked. if both are checked or not checked i want to use showmessage to draw attention to it. is that possible? how can i do that in the controller?

    thanks again for your work on this validation solution!

  • cakebaker

    @austintx: Hm, usually the validation is done in the model, so I would try to validate your checkboxes in the model, too.

    I think it should be possible to validate it in the controller, but at the moment I don’t see how (it is quite some time since I used this validation approach the last time).

    HTH

Bake a comment




(for code please use <code>...</code> [no escaping necessary])

© daniel hofstetter. Licensed under a Creative Commons License