matt.toigo

GET EXCITED AND MAKE THINGS

ORM with Validation in Kohana 3

October 17th, 2011 dev

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:

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

SQL 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')
)
)
);
}
}
?>
view raw Model hosted with ❤ by GitHub

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();
}
}
?>
view raw Controller hosted with ❤ by GitHub

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 raw Messages hosted with ❤ by GitHub

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>
view raw Form View hosted with ❤ by GitHub