Validation combined with i18n in CakePHP
Not 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 which i18n is combined with validation - using __() for error messages.
As I had imagined, including the magic i18n function in the Model::validate definition in my models didn’t work. However, there was the Model::beforeValidate() method that was brought to my attention by biesbjerg on IRC. Simple, no?. Creating a new method in my models User::loadValidation() and calling that with AppModel::beforeValidate() so it applies on all models.
Here is an example of what I had at that point:
function beforeValidate()
{
$this->loadValidation();
return true;
}
and in my model:
function loadValidation()
{
$this->validate = array();
}
Everything was working fine until I had the curiosity of checking the HTML it was spitting out. Trying to make myself a list of the Cake CSS classes and figure out how to best implement the Blueprint CSS framework which I plan on using for this app.
I was surprised to see that the magic ‘required’ class for the INPUTs’ wrapping DIVs had disappeared. A quick look in the FormHelper helped me identify the problem for which I intend on submitting a patch for after testing it for a while. Let me explain.
When creating forms using $form->create() in your view, the helper populates its $fieldset array with different information related to your current model’s fields (which you normally passed when calling the create() method). One of the arrays created is ‘validates’, a list of all fields that have validation rules assigned. That’s done by the part of code shown below from /cake/libs/view/helpers/form.php
$fields = $object->loadInfo();
$fieldNames = $fields->extract('{n}.name');
$fieldTypes = $fields->extract('{n}.type');
$fieldLengths = $fields->extract('{n}.length');
if (!count($fieldNames) || !count($fieldTypes)) {
trigger_error(__('(FormHelper::create) Unable to use model field data. If you are using a model without a database table, try implementing loadInfo()', true), E_USER_WARNING);
}
if (!count($fieldNames) || !count($fieldLengths) || (count($fieldNames) != count($fieldTypes))) {
trigger_error(__('(FormHelper::create) Unable to use model field data. If you are using a model without a database table, try implementing loadInfo()', true), E_USER_WARNING);
}
$data = array(
'fields' => array_combine($fieldNames, $fieldTypes),
'sizes' => array_combine($fieldNames, $fieldLengths),
'key' => $object->primaryKey,
'validates' => (ife(empty($object->validate), array(), array_keys($object->validate)))
);
$habtm = array();
if (!empty($object->hasAndBelongsToMany)) {
$habtm = array_combine(array_keys($object->hasAndBelongsToMany), array_keys($object->hasAndBelongsToMany));
}
$data['fields'] = am($habtm, $data['fields']);
$this->fieldset = $data;
Ok, so what’s the problem exactly? Well, like you can see, it’s trying to read the rules directly from $object->validate, which when using i18n can’t be defined. Also, while dissecting the FormHelper::create() and FormHelper::input() methods, I also noticed that, when using 2 or more models in a form, the magic ‘required’ class will not appear on the extra models’ fields.
After looking into different possible solutions, it was pretty obvious to me that the changes had to be made to the helper itself. After all, I ain’t the only person who will need to combine i18n and model validation.
One of the things I like the most about Cake is the freedom of overloading any existing class in the core. So here is the DIFF with my patched ‘form.php’ that I saved in /app/views/helpers/:
Index: form.php
===================================================================
--- form.php (revision 86)
+++ form.php (working copy)
@@ -133,6 +133,7 @@
}
$data['fields'] = am($habtm, $data['fields']);
$this->fieldset = $data;
+ $this->fieldsetValidates($model);
}
if (isset($this->data[$model]) && isset($this->data[$model][$data['key']]) && !empty($this->data[$model][$data['key']])) {
@@ -1388,5 +1389,38 @@
$this->__options[$name] = $data;
return $this->__options[$name];
}
+/**
+ * Populates required fields on the fly for
+ * FormHelper::fieldset[validates]
+ *
+ * @param mixed model name(s)
+ * @return void
+ * @access public
+ */
+ function fieldsetValidates($models = null){
+ if (is_string($models)){
+ $models = array($models);
+ }
+ if (!is_array($models)){
+ $this->fieldset['validates'] = array();
+ return;
+ }
+
+ if (is_array($models)){
+ foreach ($models as $model){
+ if (ClassRegistry::isKeySet($model)) {
+ $object =& ClassRegistry::getObject($model);
+ } else {
+ $object =& ClassRegistry::set($model);
+ }
+ if (empty($object->validate) && method_exists($object, 'loadValidation')){
+ $object->loadValidation();
+ if (!empty($object->validate)){
+ $this->fieldset['validates'] = am($this->fieldset['validates'], array_keys($object->validate));
+ }
+ }
+ }
+ }
+ }
}
?>
The solution is straight forward:
- FormHelper::create() method finishes it’s job trying to retrieve the Model::validate value
- Calls new FormHelper::fieldsetValidates() method with the model name as param.
- The function tries checking if the model is already set in the ClassRegistry (otherwise, it will set it) before running it’s loadValidation() method.
Only now, did our FormHelper learn of our validation rules.
Now, what about when you have multiple models in a single form? Using i18n or not, FormHelper will not include those external fields in the $fieldset[’validates’] array and therefore, the wrapping DIV will be incorrect.
Using the same changes made above, you can now force it to load extra models’ fields info like this:
e($form->create('User');
$form->fieldsetValidates(array('Profile', 'Group'));
e($form->input('User.username'));
e($form->input('Profile.name'));
e($form->input('Group.name'));
e($form->end('Submit');
Now this will output the magic DIV wrapper’s ‘required’ class like it does by default.
So there you go, here’s how I managed to combine both i18n and validation in CakePHP. As always, I expect to see better (and worse) implementations - but so far, I couldn’t find any information related to the subject. If you have previously worked with both and know something I don’t, please share.
UPDATE: After seeing biesbjerg again on IRC, I asked him about the ‘required’ class bug, seems like he had submitted it a month or so ago. He also had a patch for it that makes it load all the models included in the form. I removed my changes, applied his patch and then called the different Model::loadValidation() required - everything worked! Both methods require to modify the FormHelper but his is DRYer and makes you write less code - now let’s hope it makes it into the core pretty soon.
Lesson of the day: learn how to better search CakePHP’s Trac for related issues.
Trackbacks
Use this link to trackback from your own site.


Best solution is i18n behavior created by poLK
@SkieDr: thanks for dropping by. You have any link to it?