ORM with Validation in Kohana 3
I really like Kohana 3 so far mainly because it feels like someone sliced and diced CodeIgniter into just the pieces I want and added a few major missing components. I have absolutely no complaints about how anything is structured, but the documentation is at a point where it is extremely difficult to get rolling with the framework if you are new to it. One thing that Kohana does very well is ORM and Model level validation, but the pages on how to do this are literally blank at this point.
This article assumes you have a basic understanding of MVC frameworks and how files are organized under Kohana.
I do not have a comprehensive understanding of how Kohana handles this under the hood, and there are likely better ways to write this, but this ended up working fine for me. We'll be setting up a form to create and edit a user. In this particular example we're referring to users as Judges. Our goals are:
- All validation defined within our model inside of a single array using anonymous functions.
- Error messages are not defined in our controller or view
- Minimal PHP in our view that is just outputting values and error messages and does not contain any logic.
- Keeping our controllers as skinny as possible and keeping record creation logic inside of our model.
- Each user has uniquely salted encryption for their password.
- A new user cannot be added if a user already exists with the same email address. A user cannot be edited to have the same address as another user either.
This would be a form that would be used for an administrator to register a new user, rather than let a user register themselves.
You can view all of these files here as well. Comments are inline in each file explain what is going on.
SQL Table Structure

Model
/application/classes/model/judge.php
<?php | |
class Model_Judge extends ORM { | |
//Define all validations our model must pass before being saved | |
//Notice how the errors defined here correspond to the errors defined in our Messages file | |
public function rules() { | |
return array( | |
'first' => array(array('not_empty')), //Standard, build into Kohana validation library | |
'last' => array(array('not_empty')), | |
'email' => array( | |
array('not_empty'), | |
//Make sure we have a unique email when registering or updating a judge | |
//Uses anonymous function rather than define them somewhere else | |
//The array(':model', ':validation'), corresponds to our ($judge, Validation $valid) arguments that are passed to our function | |
//:model will give us access to the the model with values that is being validated | |
//:validation will give us access to our Validation object which will let us invalidate fields | |
array( | |
function($judge, Validation $valid){ | |
//Find a judge with the same email address | |
$existing_judge = ORM::factory('judge')->where('email', '=', $judge->email)->find(); | |
if($existing_judge->id) { | |
//Don't invalidate if we are just editing a judge | |
if($judge->id!=$existing_judge->id) { | |
//Invalidate our email field | |
$valid->error('email', 'unique_email'); | |
} | |
} | |
}, array(':model', ':validation') | |
) | |
), | |
//Only require a password on reg and not updating | |
'password' => array( | |
array( | |
function($judge, Validation $valid) { | |
if($judge->id==null && $judge->password=='') { | |
$valid->error('password', 'not_empty'); | |
} | |
}, array(':model', ':validation') | |
) | |
) | |
); | |
} | |
//Filter are data transformations that are run when values are set in our model (before our validations above) | |
//We're using them here to encrypt our password and generate an encryption salt string if we need it | |
//Notice how array(':value', ':model') corresponds to our anonymous function arguments ($password, $judge) | |
//:value will be the value of our field (in this case password) | |
//:model will be our judge that we need access to in order to set our salt if this is a new record | |
//Whatever value our anonymous function returns will be the new value for password in our model | |
public function filters() { | |
return array( | |
'password' => array( | |
array( | |
function($password, $judge) { | |
if($password=='') { | |
return ''; | |
} | |
else { | |
if($judge->id==null) { | |
$judge->salt = sha1($judge->email).rand(0,1000000); | |
} | |
return sha1($judge->salt.$password); | |
} | |
}, array(':value', ':model') | |
) | |
) | |
); | |
} | |
} | |
?> |
Controller
/application/classes/controller/admin/judges.php
<?php | |
class Controller_Admin_Judges extends Controller_Admin_Base { | |
public function action_add() { | |
//Setting various view variables | |
$this->view->section_title = 'Add a New Judge'; | |
$this->view->body = View::factory('admin/judges/form'); | |
$this->view->body->button_text = 'Add Judge'; | |
//Process our form POST | |
if($_SERVER['REQUEST_METHOD']=='POST') { | |
//Pass our submitted values back to our view so we can display the values that were just submitted | |
$this->view->body->judge = $this->request->post(); | |
//Create a new model and populate with our submitted form values | |
$new_judge = new Model_Judge(); | |
$new_judge->values($this->request->post()); | |
try { | |
//Save, redirect, flash messages are handled in our view and can be ignored in this example | |
$new_judge->save(); | |
Session::instance()->set('flash', 'Judge Added'); | |
$this->request->redirect('judges'); | |
} | |
//If we had any validation errors, see our model for how these are defined | |
catch (ORM_Validation_Exception $e) { | |
//Make our errors available in our view | |
$this->view->body->errors = $e->errors(''); | |
} | |
} | |
} | |
public function action_edit() { | |
//Set anything related to setting up our view | |
$this->view->section_title = 'Edit Judge'; | |
$this->view->body = View::factory('admin/judges/form'); | |
$this->view->body->button_text = 'Save Changes'; | |
//New judge based on our ID that was passed in our URL, requires some routing | |
$judge = new Model_Judge($this->request->param('id')); | |
//Handle our form submission | |
if($_SERVER['REQUEST_METHOD']=='POST') { | |
//Pull submitted values and set them in our model one at a time | |
$judge->first = $this->request->post('first'); | |
$judge->last = $this->request->post('last'); | |
$judge->email = $this->request->post('email'); | |
//Only touch password if the user entered something | |
if($this->request->post('password')!='') { | |
$judge->password = $this->request->post('password'); | |
} | |
try { | |
//Save, and redirect to our current url (Necessary for our flash to show since it's in the session) | |
$judge->save(); | |
Session::instance()->set('flash', 'Changes Saved'); | |
$this->request->redirect('judges/edit/'.$judge->id); | |
} | |
//Handle any validation errors and make them available in our view | |
catch (ORM_Validation_Exception $e) { | |
$this->view->body->errors = $e->errors(''); | |
} | |
} | |
//Set our password to '' when initially viewing the form | |
$judge->password = ''; | |
//Pass our judge to our view as an array, must be passed as an array for the same for to handle both additions and edits | |
$this->view->body->judge = $judge->as_array(); | |
} | |
} | |
?> |
Messages
/application/messages/judge.php
<?php | |
//These corespond to the fields that we are invalidating in our model and the error message that will be displayed on our form | |
return array( | |
'first' => array( | |
'not_empty' => 'First name is required.' | |
), | |
'last' => array( | |
'not_empty' => 'Last name is required.' | |
), | |
'email' => array( | |
'not_empty' => 'Email is required.' , | |
'unique_email' => 'A judge already exists with that email address.' | |
), | |
'password' => array( | |
'not_empty' => 'Password is required.' | |
) | |
) | |
?> |
View
/application/views/admin/judges/add.php
<form method="POST"> | |
<div> | |
<label for="first">First Name:</label> | |
<input type="text" name="first" id="first" value="<?php echo($judge['first']); ?>"/> | |
<div class="error_text"><?php echo($errors['first']); ?></div> | |
</div> | |
<div> | |
<label for="last">Last Name:</label> | |
<input type="test" name="last" id="last" value="<?php echo($judge['last']); ?>" /> | |
<div class="error_text"><?php echo($errors['last']); ?></div> | |
</div> | |
<div> | |
<label for="email">Email:</label> | |
<input type="text" name="email" id="email" value="<?php echo($judge['email']); ?>" /> | |
<div class="error_text"><?php echo($errors['email']); ?></div> | |
</div> | |
<div> | |
<label for="password">Password:</label> | |
<input type="password" name="password" id="password" value="<?php echo($judge['password']); ?>" /> | |
<div class="error_text"><?php echo($errors['password']); ?></div> | |
</div> | |
<div> | |
<input type="submit" value="<?php echo($button_text); ?>"> | |
</div> | |
</form> |