CakePHP’s advanced model fields validation

Posted by Jad on September 20, 2007

After checking different blogs and tutorials, the bakery, API and IRC channel, it was obvious that some kind of documentation for the validation methods available in the Model was necessary. I can’t say that I will fulfill this mission but I’ll at least share what I came up with for future reference.

Here is the ‘User’ model I will be using in my example:

class User extends AppModel
{
   var $name = 'User'; //optional

   var $validate = array(
                     'username' => array(
                                       array(
                                          'allowEmpty' => false,
                                          'required' => true,
                                          'rule' => 'alphaNumeric',
                                          'message' => 'Username should only contain alpha-numeric characters.',
                                          ),
                                       array(
                                          'rule' => array('between', 3, 10),
                                          'message' => 'User should be between 3 and 10 characters long.',
                                          ),
                                       array(
                                          'rule' => 'isUnique',
                                          'message' => 'Username is already in use.',
                                          ),
                                       ),
                     'passwd' => array(
                                    'alphaNumeric' => array(
                                                         'allowEmpty' => false,
                                                         'required' => true,
                                                         'rule' => 'alphaNumeric',
                                                         'message' => 'Username should only contain alpha-numeric characters.',
                                                         ),
                                    'validLength' => array(
                                                         'rule' => array('between', 3, 10),
                                                         'message' => 'User should be between 3 and 10 characters long.',
                                                         ),
                                    ),
                     'website' => array(
                                       array(
                                          'rule' => 'url',
                                          'on' => 'update',
                                          'message' => 'Invalid URL.',
                                          ),
                                       ),
                     'agree_tos' => array(
                                       array(
                                          'allowEmpty' => false,
                                          'required' => true,
                                          'on' => 'create',
                                          ),
                                       ),
                     );

);
}

That’s a lot of validation rules, I know - I just wanted to try covering the multiple ways of using the Model->validates() method.

Like you can see, $validate is an array where keys are field names and values are rulesets.

$validate = array('fieldname'=>array('rule'=>'method','message'=>'error message to show');

By default, a ruleset is defined as follow:

$default = array(
               'rule' => 'blank',
               'message' => 'This field cannot be left blank', //or it's translation
               'allowEmpty' => null,
               'required' => null,
               'last' => false,
               'on' => null,
               );

rule: Required, string.
By order of priority, it can be a custom model method (defined in AppModel or TestModel), a Validation method or a custom regex.

message: Required, string.
Error message to associate with the invalidated field.

allowEmpty: Optional, boolean.
If set to true, field will invalidate if the following check is true: empty($data[$fieldName]) && $data[$fieldName] != '0'. If set to false, it will invalidate if isset($data[$fieldName]) && (empty($data[$fieldName]) && !is_numeric($data[$fieldName])) returns true.

required: Optional, boolean.
If set to true, field will invalidate if !isset($data[$fieldName]) returns true.

on: Optional, string.
This can be either null, create or update. It will define when to run the validation rule respectively: always, only when creating the record, only when updating the record.

last: Optional, unknown.
I couldn’t figure what this is for, most probably an unfinished feature.

When ‘allowEmpty’ or ‘required’ are set, they have priority on ‘rule’. If any of them invalidates the field, ‘rule’ will not be checked.

The names given to the different keys in the ruleset are just for better code readability and future unbinding of some rules (next paragraph). The two following snippets act exactly the same way:

   var $validate = array(
                     'passwd' => array(
                                    'alphaNumeric' => array(
                                                         'allowEmpty' => false,
                                                         'required' => true,
                                                         'rule' => 'alphaNumeric',
                                                         'message' => 'Username should only contain alpha-numeric characters.',
                                                         ),
                                    'validLength' => array(
                                                         'rule' => array('between', 3, 10),
                                                         'message' => 'User should be between 3 and 10 characters long.',
                                                         ),
                                    ),
                     );
   var $validate = array(
                     'passwd' => array(
                                    array(
                                       'allowEmpty' => false,
                                       'required' => true,
                                       'rule' => 'alphaNumeric',
                                       'message' => 'Username should only contain alpha-numeric characters.',
                                       ),
                                    array(
                                       'rule' => array('between', 3, 10),
                                       'message' => 'User should be between 3 and 10 characters long.',
                                       ),
                     );

I mentioned unbinding validation rules, but what is that? By default, Cake will run all the rules you have set in $User->validate whenever it runs $User->invalidFields() - like when saving your model with $Model->save(). What if, in certain cases (mostly admin actions), you want to skip some (or all) of the validation rules for some (or all) the fields?

There are no pre-built cake methods for doing so, even though it should be pretty obvious - maybe an enhancement in the future. For now (1.2.0.5427alpha), here is what you can add to your custom AppModel class (updated on 30th Sept. - thanks Kiger for the work you’ve done!) :

/**
	 * Unbinds validation rules.
	 *
	 * EXAMPLES:
	 * $this->User->unbindValidation('username');
	 * $this->User->unbindValidation(array('username', 'password'=>'required', 'email'=>array('valid', 'length'), 'newsletter')));
	 *
	 * @param mixed $arr
	 * @return
	 */
	function unbindValidation($arr)
	{
		if (!is_array($arr))
		{
			$arr = array_flip(array($arr));
		}

		foreach ($arr as $fieldName => $ruleSet)
		{
			// where arr=array('username'), so $ruleSet ends up containing the field name
			if (is_numeric($fieldName))
			{
				$fieldName = $ruleSet;
				if (!array_key_exists($fieldName, $this->validate))
				{
					trigger_error(sprintf(__('AppModel::unbindValidation() could not find the validation fieldname %s::$validate[%s]', true), $this->name, $fieldName), E_USER_WARNING);
					continue;
				}

				$arr[$fieldName] = array_flip(array_keys($this->validate[$fieldName]));

			}
			// where arr=array('password'=>'required') or arr=array(email'=>array('valid', 'length'))
			else
			{
				$ruleSet = (!is_array($ruleSet)) ? array_flip(array($ruleSet)) : array_flip($ruleSet);

				if (!array_key_exists($fieldName, $this->validate))
				{
					trigger_error(sprintf(__('AppModel::unbindValidation() could not find the validation fieldname %s::$validate[%s]', true), $this->name, $fieldName), E_USER_WARNING);
					continue;
				}

				foreach ($ruleSet as $rule => $data)
				{
					if (!array_key_exists($rule, $this->validate[$fieldName]))
					{
						trigger_error(sprintf(__('AppModel::unbindValidation() could not find the validation rule %s::$validate[%s][%s]', true), $this->name, $fieldName, $rule), E_USER_WARNING);
					}
				}

				$arr[$fieldName] = $ruleSet;
			}

			$this->validate[$fieldName] = array_diff_key($this->validate[$fieldName], $arr[$fieldName]);

			if (empty($this->validate[$fieldName]))
			{
				unset($this->validate[$fieldName]);
			}
		}
	}

Now you can skip some of the validation rules before saving as follow:

//skips all validation rules for username
$this->User->unbindValidation('username');

//skips the 'url' rule for website
$this->User->unbindValidation(array('website'=>'url'));

//skips the 'alphaNumeric' and 'validLength' set of rules for passwd
$this->User->unbindValidation(array('passwd'=>array('alphaNumeric','validLength')));

That’s if for now. If I learn anything new about Cake validations, I’ll make sure to post about it. In the meantime, if I have missed something, please let me know in the comments.

Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

  1. links for 2007-09-21 « Richard@Home Fri, 21 Sep 2007 08:17:48 EDT

    […] Loud Baking » Blog Archive » CakePHP’s advanced model fields validation (tags: cakephp validation) […]

  2. Kiger Fri, 28 Sep 2007 21:33:01 EDT

    The unbindValidation() is really cool and handy. I added a slight addition to it which will now allow you to do this:

    $this->User->unbindValidation(array(’username’, ‘password’));

    Get it here: http://bin.cakephp.org/view/1653520038

  3. Kiger Fri, 28 Sep 2007 21:56:50 EDT

    Multiple rule per field validation is currently broken in respect to error messages. See this: https://trac.cakephp.org/ticket/3297

    The problem is that once a rule for a field is invalidated, validation for that field should stop; but it doesn’t. The remainder of the rules for the field are processed. The two consequences of this are (1) you can’t show the user more than one error at a time, so cake is just wasting time processing the remaining rules; (2) if you have multiple error messages, the error messages are displayed in reverse order of failure.

    So here is a fix which you can throw in your app_model.php for the time being. It inserts a ‘break;’ in two places; that’s it!

    http://bin.cakephp.org/view/1192637127

    And I got the invalidFields() method from $Revision: 5677 of model.php

  4. Jad Fri, 28 Sep 2007 22:05:51 EDT

    @Kiger: thanks for the change you brought to the AppModel::unbindInvalidation() - can’t believe it slipped my mind. I have implemented it now in the function.

    I checked that ticket when you added it the other day, the part I am not comfortable with is:

    What should happen? In model::invalidfields(), once a field is invalidated, cake should stop processing rules for that field.

    Because, in some cases, for better usability, it’s good to show all validation errors to the user at once (in a list for example) so they don’t have to make several attempts before getting it right.

    I haven’t gotten the time yet to wrap my head around all the validations in the app I am working on, busy extending the Auth and ACL components for dealing with limits per action, but once I get back to validation I will definitely either apply the patch or look for a way that doesn’t violate my usability guidelines.

    Before I forget, it’s great to have you here!

    Regards,

    Jad

  5. Kiger Fri, 28 Sep 2007 22:57:01 EDT

    @jad

    Well, I would more than go for a param which if set to true/false would break on the first error in a field so that we could both have our cake and eat it too. For myself, I wouldn’t want 10 fields, each with 4 errors, so a total of up to 40 errors being displayed at once. That would be a nightmare to look at.

  6. Jad Fri, 28 Sep 2007 23:11:25 EDT

    @Kiger: what you are saying here is very true, but then instead of a bool, just a mixed param where you can either set a limit number of errors, true or false. This would make it work everyone I believe.

  7. Kiger Fri, 28 Sep 2007 23:56:41 EDT

    @jad
    That suggestion is even better. Why not add it to the ticket.

    Also, here is a second and I believe final update to the unbind method. This update includes all the prior updates along with error logging.

    http://bin.cakephp.org/view/1557992436

  8. Jad Sat, 29 Sep 2007 00:58:24 EDT

    Yes, I should add that to the ticket.

    For the error logging, I think I prefer the trigger_error() for something like this, you don’t want to be checking your log when baking your app for some mis-use of a method. It should be something like this instead:


    trigger_error(sprintf(__(AppModel::unbindValidation() could not find the validation fieldname %s::$validate[%s]’, true), $this->name, $fieldName), E_USER_WARNING);

    And man that’s a lot of repetition in a small function, when I get some time I’ll look into making it much shorter.

  9. Kiger Sat, 29 Sep 2007 01:57:36 EDT

    I incorporated the trigger_error(); I don’t know why I didn’t use that to begin with. Also I shortened it as much as I could; I don’t think I have the skills to reduce it anymore.

    http://bin.cakephp.org/view/1231026677

  10. Jad Sun, 30 Sep 2007 07:44:48 EDT

    I believe that’s the best it could get. I have updated the post - thanks Kiger!

  11. Kiger Sun, 30 Sep 2007 08:23:40 EDT

    @Jad
    It’s probably not a big deal, but the second set of comments in the code got cut off in the updated post. The post ends up just saying ” // where )”

    If you are care to fix it, the paste-bin has the comments in their entirety.

  12. Jad Sun, 30 Sep 2007 08:48:09 EDT

    Stupid code view add-on am using I guess.. It was there in the post on the admin side, go figure. Anyhow, I de-activated it on this post, so now all shows, just on black background.

  13. […] too long ago, I wrote a quite lengthy ‘how-to use validation in CakePHP‘ post. Over the past couple of days, I had to work with a form that uses 2 models and for […]

  14. Kiger Thu, 04 Oct 2007 05:41:38 EDT

    @Jad
    Something just hit me. Remember this https://trac.cakephp.org/ticket/3297 where I suggested cake stop testing rules for a field once a rule for that field has failed? You had suggested a mixed param as the middle ground, which sounds great. But I just realized something else. There is more that would need to be done to fix this because of two things.

    In the current code, cake doesn’t know which rule failed; it just knows that the field failed. So if you had 10 rules, and wanted the ones that filed displayed, cake cannot do that because it doesn’t know what rule failed. It only knows that the field failed. The code would need to be modified so that an identifier for each failed rule was stored, so you would know which rules failed. Also, the ability to store a custom error message with each identifier would need to be created.

  15. Jad Sat, 06 Oct 2007 08:13:11 EDT

    @Kiger: sorry haven’t had time for the blog the past few days. I can see what you are saying here, however, without diving into the code right now, I believe you can make it save the field validation as an array and modify the method used to echo that in the input by making it loop that, no?

  16. Andruu Mon, 08 Oct 2007 07:43:04 EDT

    Thanks a lot, this article was just what I needed.

  17. Jad Mon, 08 Oct 2007 15:08:47 EDT

    Andruu: Thanks for dropping by and glad it could help! :)

  18. Kiger Mon, 22 Oct 2007 19:46:19 EDT

    Well, gwoo made me feel very silly today. Cake can inherently unbind rules by doing this:

    unset($this->Model->validate[’field’][’rule’]);

    I feel like an idiot for not seeing this to begin with. Granted, you cannot unset various rules in an array like you can with our method, still, we should have seen this…

  19. Jad Mon, 22 Oct 2007 19:59:15 EDT

    @Kiger: hmm, you must have been tired when you worked on that because common, it’s used in the method itself! ;)

    Reason why I didn’t use it is exactly for the multiple validation rules. Getting used to cake, you start getting addicted to the fact that you never do the same thing twice and in that philosophy, the method is missing.

    I still believe this is a required enhancement and until then, I’ll keep using ours.

Comments