How to use Kohana 3 validation (with forms) 3


Displaying validation errors in context on the form

Here is how we want to show the forms:
To accomplish this, I have created a new Appform helper which uses the Form class, but wraps it’s input with application-specific markup for errors.

The basic idea is that you can pass default values, form values and error messages to Appform prior to outputting the fields. It will then create the contextual markup for each of the fields. I have maintained compatibility with the Form API, with additional properties for added functionality.
As a side note, I think this is exactly how the division of labor between framework and application developer should be: the framework should not impose a particular markup on the developer, but to keep application-specific code shorter, the developer should be able to create an extended version of the Form helper for the basic form layout. There are too many different preferred styles of markup, and the framework should not try to guess how you like your forms but rather provide the backend. Kohana gets this right.
Here is how a sample invocation would look like (note that the Appform class is not static, since each form has it’s own contextual data):
$form = new Appform();
if(isset($defaults)) {
  $form->defaults = $defaults;
}
if(isset($errors)) {
  $form->errors = $errors;
}
if(isset($values)) {
  $form->values = $values;
}
echo $form->open('user/login');
echo '<ul>';
echo '<li>'.$form->label('username', __('Username')).'</li>';
echo $form->input('username', NULL, array('info' => __('You can also log in using your email address instead of your username.')));
echo '<li>'.$form->label('password', __('Password')).'</li>';
echo $form->input('password');
echo '</ul>';
echo $form->submit(NULL, __('Login'));
echo $form->close();

You can download my form validation helper from Bitbucket. It’s only a couple of hundred lines so it is easy to improve – a useful start for building more complex functionality for your app and also to reduce the lines of code needed for each form.

ORM and form validation

The Kohana ORM provides support for field validation. However, it is very much oriented towards each model having one set of validations. With ORM, the function calls are:
if ( !empty($_POST) && is_numeric($id) ) {
   $model = ORM::factory('user', $id); // create
   $model->values($_POST); // load values to model
   // check() initializes $model->_validate with a Validation object containing the
   // rules, filters and callbacks from Model_User (e.g. $_rules, $_callbacks..)
   if ($model->check()) {
      $model->save(); // save the model
   } else {
      // Load errors. The first param is the path to the
      // message file (e.g. /messages/register.php)
      $content->errors = $user->validate()->errors('register');
   }
}
You may want to do something different, either using a different set of rules or a different set of features! The classic example is user profile editing, where you do not want to force the user to re-enter their password – so you need to exclude the password rules from the Validation check.
There are many different suggestions regarding how you should handle this case. For example, in the unofficial docs they define two functions which each initialize a different validation object. I did this in my KO3 Auth sample implementation, andbiakaveron (author/maintainer of KO3 Auth) suggested it is a “WTF” – since ORM already has the properties and methods for initializing a validation object from the given rules.
I would suggest using the following pattern: rely on the ORM $_rules property when possible, and when you need different types of validation (e.g. create, update, change subset of fields), define a checkCreate/checkUpdate/checkXYZ function on your model which loads the alternative set of $_rules and then calls the ORM check() to actually perform the validation.
Here is an example (model function, see also sample code):
public function check_edit() {
   $values = $this->as_array();
   // since removing validation rules is tricky (this is needed to ignore
   // the password),we will just create our own alternate _validate
   // object and store it in the model.
   $this->_validate = Validate::factory($values)
      ->label('username', $this->_labels['username'])
      ->label('email', $this->_labels['email'])
      ->rules('username', $this->_rules['username'])
      ->rules('email', $this->_rules['email'])
      ->filter('username', 'trim')
      ->filter('email', 'trim')
      ->filter('password', 'trim')
      ->filter('password_confirm', 'trim');
   // if the password is set, then validate it
   // Note: the password field is always set if the model was loaded
   // from DB (since there is a DB value for it)
   // So we will check for the password_confirm field instead.
   if(
      isset($values['password_confirm']) &&
      (trim($values['password_confirm']) != '')
   ) {
   $this->_validate
      ->label('password', $this->_labels['password'])
      ->label('password_confirm', $this->_labels['password_confirm'])
      ->rules('password', $this->_rules['password'])
      ->rules('password_confirm', $this->_rules['password_confirm']);
   }
   // Since new versions of Kohana automatically exclude the current user from
   // the uniqueness checks, we no longer need to define our own callbacks.
   foreach ($this->_callbacks as $field => $callbacks) {
      foreach ($callbacks as $callback) {
         if (is_string($callback) AND method_exists($this,$callback)) {
            // Callback method exists in current ORM model
            $this->_validate->callback($field, array($this,$callback));
         } else {
            // Try global function
            $this->_validate->callback($field, $callback);
         }
      }
   }
   return $this->_validate->check();
}

This way you can still keep using the same, or almost identical code e.g. $model->values(), $model->validation->errors() and $model->checkXYZ(), while benefiting from the builtin code.
For example (in the Controller, see also sample code):
if ( !empty($_POST) && is_numeric($id) ) {
   $model = ORM::factory('user', $id); // create
   $model->values($_POST); // load values to model
   // check() initializes $model->_validate with a Validation object containing the
   // rules, filters and callbacks from Model_User (e.g. $_rules, $_callbacks..)
   if ($model->check_edit()) {
      $model->save(); // save the model
   } else {
      // Load errors. The first param is the path to the
      // message file (e.g. /messages/register.php)
      $content->errors = $user->validate()->errors('register');
   }
}

A note about ORM: $_ignored_columns
This field is used to specify fields which will not be saved to the database, but which may be written and accessed in while the model class exists. If you use validation through ORM, then you should specify any additional fields which are part of the model validation here (e.g. fields which will be combined during the actual save, but are transmitted separately).

0 comments:

Post a Comment