Multiple Validation as Behavior in CakePHP 1.2
After some inspiration from a recently published Bakery article, I decided to convert my multiple validation function into a Behavior. Even better, I've thrown it into my plugin collection to make it super easy to drop into any project.
Using the new behavior is much like using the script as it was before. You can name the validation properties to include the action name and it'll automatically set that validation set as the default.
You can also define a specific validation set to be used with $this->ModelName->useValidationRules('ExampleSet')
which will look for a validation property called validationExampleSet
. If the property doesn't exist, you'll get an error, so be careful to match the name.
Things are a little different in that the custom validation rules aren't automatically reset after a validation is performed. This is more a limitation of CakePHP since behaviors have beforeValidate callbacks (which is used to alter the validation set) but don't have afterValidate callbacks (which would be used to alter them back). Instead, you have to run $this->ModelName->resetValidationRules()
to set the validate property back to the default.
To set a model to make use of the behavior, just add it to the actsAs property.
class ExampleModel extends AppModel {
var $actsAs = array('Snook.MultipleValidatable');
}
That's all you need to do! The following chunk of code is the behavior itself. It should be saved to the /app/plugins/snook/models/behaviors/
folder. You could save it in a plugin of a different name (you'd need to change the Snook prefix to the proper plugin name) or save it as a standard behavior (in which case, you'd need to remove the Snook prefix altogether).
<?php
class MultipleValidatableBehavior extends ModelBehavior {
var $__default = array();
var $__useRules = array();
function setup(&$model, $settings = array()) {
$this->__default[$model->alias] = $model->validate;
}
function beforeValidate(&$model) {
$actionSet = 'validate' . Inflector::camelize(Router::getParam('action'));
if (isset($this->__useRules[$model->alias])) {
$param = 'validate' . $this->__useRules[$model->alias];
$model->validate = $model->{$param};
} elseif (isset($model->{$actionSet})) {
$param = $actionSet;
$model->validate = $model->{$param};
}
}
function useValidationRules(&$model, $param) {
$this->__useRules[$model->alias] = $param;
}
function resetValidationRules(&$model) {
$model->validate = $this->__default[$model->alias];
}
}
?>
I hope you enjoy it.
Conversation
I didn't expect another CakePHP article so soon. but there you have it. :)
Hi Jonathan,
nice article.
But let me disagree about something:
$actionSet = 'validate' . Inflector::camelize(Router::getParam('action'));
I've read something like that in the first article (and maybe this comment belongs to it), but I don't like it because you are coupling the model to the controller.
It's a good practice to keep lower layers as general as possible and loose coupled, and make the upper layers more specific to the application (and you can let them couple to lower layers).
So, making the model to know about controller actions makes it difficult to change that later and to use the same model in other controllers/components (as Ad7Six MiniControllers).
Just my 2 cents.
Dardo, if you don't like the tighter coupling, you can use
$this->ModelName->useValidationRules('Example')
. The fact is, Cake couples the model to the controller automatically. All I'm doing is taking the automation to the validation stage.I understand the concern about having a model tied to an action and then trying to push it over to another controller. However, this was written more for edge cases. 99% of the time, you're only going to need one set of validation and it'll apply for inserts and updates and whatever else you need to do. Where the multiple validation comes in is mainly in unique situations and the user model is probably the most common situation. Things like forgotten passwords and profile edits are unique to the user object so there's less concern of portability issues.
I like this flexibility.
I just added these lines into the behavior in order to make more verbal.
if (isset($model->{$param})) {
$model->validate = $model->{$param};
} else {
e('MultipleValidatableBehavior: Please define ' . $param .' in your model ' . $model->alias . '. Be sure useValidationRules call in the controller "' . Router::getParam('action') . '" is correct.');
}
Jonathan, I'm using it in many models, if that weren't the case it would be overkill to make a behavior, I'll just put the methods in the user model as in your first post.
For example it's useful for me in form wizards.
Anyway I like your methods names (api?) and having every rule set in a separate variable improves readability. I'm going to change my behavior (not the one in the article) as I see your approach cleaner.
And for the action coupling thing, I don't see me using it so I'll leave that out.
Thanks.
Without trying it out (so I apologize in advance if this is nonsense) but, is it not possible, at least in php5, to eliminate the need for calling resetValidationRules()?
Could one not override the model Save() in appModel something like:
function Save(...){
- your before validate stuff.
parent::Save(...);
- your reset validate stuff.
}
It should be implemented as unbindModel() with two modes: persistent mode and a non-persistent mode.
I'm seemingly forever comparing Code Igniter and CakePHP. I use a custom framework at my day job, but for personal projects, I'm still researching. You showed something that I find the 2 frameworks doing differently. In CakePHP, the validation is taken care in the Model. In CodeIgniter, validation is done in the controller before being given to the Model. Which do you think is more proper, or could you justify the logic belonging in either class?
@keymaster: yes, it'd be possible but I don't love that approach.
@Sean: I think it makes more sense in the Model. With CI, they have a validation class, which I actually like. With CakePHP, I think the Model should use a Validation object to determine how data should be validated. This would allow the validation object to be subclassed, instead of defining custom function names. But maybe for CakePHP 2.0. :)
As I was thinking about it, I agree. Putting it in the model means you don't have to remake validation rules if you make another function to insert data into the database.
I really like CI's Validation class. I admit, I ripped it off and threw it into my framework at work. What would it take to make the same thing in Cake? It seems Cake is pretty mod-able. I'd be interested in throwing that together, as I get more and more sold on Cake.
wow! its been a while im not visiting your site and now you got a series of article for cake php which is the framework i am using aside from zend and code igniter. I take some time reading all your articles about cake. thanks for the nice article snook.
Thank you so much! This is *exactly* what I needed.
Sean, turns out upon further inspection that CakePHP does have its own Validation object. It just uses the validation rules defined in the Model to validate against the functions defined in the validation object. There could be some additional separation but with that said, I'm not sure much would be gained.
Just a note..
[quote]
You can also define a specific validation set to be used with $this->ModelName->useValidationRules('ExampleSet') which will look for a validation property called validationExampleSet. If the property doesn't exist, you'll get an error, so be careful to match the name.
[/quote]
I think that the Behavior will look for properties prefixed with 'validate' and not 'validation'. On the example the correct property to be set on the Model object is "$validateExampleSet".
Great work!
After trying to run the acl command line utility I got some errors, because the class Router is not loaded. I did some modifications to fix up the shell commands:
if(class_exists('Router'))
{
$actionSet = 'validate' . Inflector::camelize(Router::getParam('action'));
}
else
{
$actionSet = 'validate';
}
Continuing the discussion on coupling between the controller and model, here's my idea for loosely coupling them.
The default rules are placed in the model. Action specific rules are specified in the controller meaning the model doesn't need to know about controller actions, but it gets the appropriate rules to validate against.
Action specific validation rules go in the $validate array in the controller:
I needed to change the Behavior slightly. In the beforeValidate function the code for the if statement becomes:
because as you'll see below the Behavior now stores the actual rules.
I then put the following in the beforeFilter callback of my app_controller:
This checks for a set of validation rules for the action. If they are set they are passed to the Behavior which then passes them to the model when it validates.
This way the model doesn't know anything about the controller or it's actions and is given the appropriate set of validation rules as needed.