Yet another data validation approach

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.

About these ads

24 Comments

  1. RosSoft
    Posted February 6, 2006 at 10:56 pm | Permalink

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

    Then use $validator[1] for message

  2. Posted February 6, 2006 at 11:24 pm | Permalink

    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.

  3. RosSoft
    Posted February 7, 2006 at 5:39 pm | Permalink

    see http://cakephp.org/pastes/show/ee040ca7f5fd3f709db5c17f028a3a56

  4. Posted February 7, 2006 at 5:45 pm | Permalink

    @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.

  5. RosSoft
    Posted February 7, 2006 at 6:00 pm | Permalink

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

  6. Posted February 8, 2006 at 10:29 pm | Permalink

    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!

  7. mememe
    Posted February 9, 2006 at 8:32 pm | Permalink

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

  8. lemp
    Posted February 14, 2006 at 9:44 pm | Permalink

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

  9. Posted February 15, 2006 at 9:57 am | Permalink

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

  10. Posted February 22, 2006 at 3:28 am | Permalink

    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).

  11. Posted February 22, 2006 at 9:13 am | Permalink

    @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.

  12. Posted February 22, 2006 at 4:52 pm | Permalink

    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;
    }
    }

  13. Posted February 22, 2006 at 4:54 pm | Permalink

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

  14. wluigi
    Posted March 2, 2006 at 3:41 pm | Permalink

    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’)) ?>

  15. Posted April 4, 2006 at 9:20 am | Permalink

    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

  16. Posted April 20, 2006 at 4:43 am | Permalink

    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.

  17. Zachary Naiman
    Posted May 5, 2006 at 5:43 pm | Permalink

    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.

  18. Posted May 6, 2006 at 9:49 am | Permalink

    @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 :)

  19. Posted May 6, 2006 at 11:48 pm | Permalink

    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?

  20. Posted May 7, 2006 at 11:11 am | Permalink

    @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.

  21. olegs
    Posted June 24, 2006 at 10:45 pm | Permalink

    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).

  22. Posted June 28, 2006 at 8:57 am | Permalink

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

  23. binoy
    Posted July 4, 2006 at 4:59 pm | Permalink

    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 ?

  24. Xadio
    Posted July 23, 2006 at 10:24 am | Permalink

    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


5 Trackbacks/Pingbacks

  1. [...] 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! [...]

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

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

  4. [...] (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) [...]

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

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: