Kohana v3

Kohana 3.1 Validation Adventures

Just today, I have setup Kohana unittest module for CLI and decided to dive into the validation class and look for hints for the migration. Because I was not that busy, I played the Kohana_Validation class with unit tests and it was fun! Please note that I only select the features and this post will be limited to them and here is the summary.

Class name change

Of course the most noticeable is the changing of name from Kohana_Validate to Kohana_Validation as the validation object. The actual validators are in Kohana_Valid as static methods. It is designed so that you can call Valid::email($some_email) anything without polluting the validation object – no validations in the validation array object.

// Sample validation object with no rules/labels
$validation = Validation::factory(array(
	'name' 		=> 'Lysender',
	'email' 	=> 'lysender@troy.org',
	'address' 	=> 'City of Troy',
	'zip' 		=> '1234'
));

Rules and callbacks are merged

Rules and callbacks are merged into just rules – technically they are all callbacks. Filters are gone! I don’t use them anyway. With rules, you can either supply a built-in kohana validation rule, a php function, a callback like static method call or object method call or lambda function. Using the above validation object, we can add rule which is almost the same in 3.0.

SYNTAX: $validation->rule($field, $rule, $params);


// Adding a rule looks the same syntax but let's dig deeper later on
$validation->rule('name', 'not_empty');

In the example above, name is the field name and not_empty is the rule which is a built-in rule under Valid class. Parameters are optional.

Rule parameters with keyword binding

Perhaps this is something new or maybe I didn’t notice this before. If you are familiar with kohana messages when used againts validation object in 3.0, there are keywords like :field, :value, param1, param2 and so on (in messages file). With the new validation class, they are also used in the parameters portion. This is because the validation class has the method bind whichs binds an object or a variable to a certain keyword.

Take a look at this example:

Valid::min_length() expects two parameters, value and length. To use the min_length rule, you may write:

$validation->rule('name', 'min_length', array(':value', 4));

Note: Rule parameters must be equal or at least compatible to the callback function. If the rule has 2 parameters, then your third parameter must also contain 2 parameters. Optional parameters takes effect of course.

As you’ve noticed, the third parameter is an array which will be passed on the rule callback. The :value is not hardcoded into the validation object, instead, they are just binded keys – which referes to the value of the field within the validation object. The :field keyword contains the name of the field as the name implies.

Another default key is :validation which is the validation object. You can also bind any variable or object as you wish, then use it in rule parameter. This allows us to pass an object (a model perhaps) to the rule/callback.

// Some custom callback function
function is_visitor_email_unique($visitor_model, $email)
{
	return (boolean) $visitor_model->get_by_email($email);
}

function is_visitor_name_unique($visitor_model, $name)
{
	return (boolean) $visitor_model->get_by_name($name);
}
// Then your validation rule will look like this
$visitor_model = new Model_Visitor;

$validation->bind(':visitor', $visitor_model);

$validation->rule('email', 'is_visitor_email_unique', array(':visitor', ':value'));
$validation->rule('name', 'is_visitor_name_unique', array(':visitor', ':value'));

Another example of object callback

The previous example uses a plain function. This time, we will not use the key binding and use the callback directly.

Your visitor model:

class Model_Visitor
{
	public function is_visitor_name_unique($name)
	{
		return (boolean) $this->get_by_name($name);
	}

	public function is_visitor_email_unique($email)
	{
		return (boolean) $this->get_by_email($email);
	}
	
	public function get_by_name($name)
	{
		// Some magic that will retrieve a record by looking up a name
		// ...
		
		return $record;
	}
	
	public function get_by_email($email)
	{
		// Some magic that will retrieve a record by looking up an email address
		// ...
		return $record;
	}
}

Your validation:

$visitor = new Model_Visitor;
$validation->rule('name', array($visitor, 'is_visitor_name_unique'));

Note: If no parameter is passed, the value is passed automatically as the only one parameter.

More samples

There are some examples with multiple rules per field.

$validation->rule('name', 'not_empty')
	->rule('name', 'min_length', array(':value', 4))
	->rule('name', 'max_length', array(':value', 16))
	->rule('email', 'not_empty')
	->rule('email', 'email')
	->rule('address', 'min_length', array(':value', 5))
	->rule('address', 'max_length', array(':value', 100))
	->rule('zip', 'min_length', array(':value', 3))
	->rule('zip', 'max_length', array(':value', 4));
	
$result = $validation->check();

Password and password confirm example

This is a typical password_confirm validation rule.

$validation->rule('password_confirm', 'matches', array(':validation', 'password_confirm', 'password'));

Where matches is the rule (built-in), :validation is the validation object binded, password_confirm is the field to validate and password is the field to compare. You will only need to supply the field names.

The test code

I have done all of this is a test case. Here is the full source code of the test case.

application/tests/classes/ValidateTest.php

<?php defined('SYSPATH') or die('No direct access allowed.');

class ValidateTest extends Kohana_UnitTest_TestCase
{
	public function test_object()
	{
		$v = Validation::factory(array());
		
		$this->assertType('Validation', $v);
	}
	
	public function rule_builtin_provider()
	{
		return array(
			array(
				'name' 		=> 'Leonel',
				'email' 	=> 'my.email@gmail.com',
				'address' 	=> '',
				'zip' 		=> NULL,
				'pass' 		=> TRUE
			),
			array(
				'name' 		=> 'xxxxxxxxxx',
				'email' 	=> 'somebody@gmail.com',
				'address' 	=> 'some address',
				'zip' 		=> '1870',
				'pass' 		=> TRUE
			),
			array(
				'name' 		=> '',
				'email' 	=> 'somebody@gmail.com',
				'address' 	=> 'some address',
				'zip' 		=> '1870',
				'pass' 		=> FALSE
			),
			array(
				'name' 		=> 'xxxxxxxxxx',
				'email' 	=> '',
				'address' 	=> 'some address',
				'zip' 		=> '1870',
				'expected' 		=> FALSE
			),
			array(
				'name' 		=> 'x',
				'email' 	=> 'some.email@gmail.com',
				'address' 	=> 'some address',
				'zip' 		=> '1870',
				'expected' 		=> FALSE
			),
			array(
				'name' 		=> 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
				'email' 	=> 'some.email@gmail.com',
				'address' 	=> 'some address',
				'zip' 		=> '1870',
				'expected' 		=> FALSE
			),
			array(
				'name' 		=> 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
				'email' 	=> 'bad email address',
				'address' 	=> 'some address',
				'zip' 		=> '1870asaaa',
				'expected' 		=> FALSE
			)
		);
	}
	
	/** 
	 * @test
	 * @dataProvider rule_builtin_provider
	 * 
	 * @param string $name
	 * @param string $email
	 * @param string $address
	 * @param string $zip
	 * @param boolean $expected
	 */
	public function test_rules_builtin($name, $email, $address, $zip, $expected)
	{		
		$validation = Validation::factory(array(
			'name' 		=> $name,
			'email' 	=> $email,
			'address' 	=> $address,
			'zip' 		=> $zip
		));
			
		$validation->rule('name', 'not_empty')
			->rule('name', 'min_length', array(':value', 4))
			->rule('name', 'max_length', array(':value', 16))
			->rule('email', 'not_empty')
			->rule('email', 'email')
			->rule('address', 'min_length', array(':value', 5))
			->rule('address', 'max_length', array(':value', 100))
			->rule('zip', 'min_length', array(':value', 3))
			->rule('zip', 'max_length', array(':value', 4));
			
		$result = $validation->check();
		
		$this->assertEquals($result, $expected);
		
		if ($expected === FALSE)
		{
			$errors = $validation->errors('signup');
		}
	}
	
	public function callback_number_even($value)
	{		
		return ($value % 2) === 0;
	}
	
	public function rule_callback_provider()
	{
		return array(
			array(
				'number' => 2,
				'expected' => TRUE
			),
			array(
				'number' => 1,
				'expected' => FALSE
			),
			array(
				'number' => 0,
				'expected' => TRUE
			),
			array(
				'number' => 101,
				'expected' => FALSE
			)
		);
	}
	
	/** 
	 * @test
	 * @dataProvider rule_callback_provider
	 * 
	 */
	public function test_rules_callback($number, $expected)
	{
		$validation = Validation::factory(array(
			'number' => $number
		));
		
		$validation->rule('number', array($this, 'callback_number_even'));
		
		$result = $validation->check();
		
		$this->assertSame($result, $expected);
	}
	
	public function rule_matches_provider()
	{
		return array(
			array('123', '123', TRUE),
			array('123', '456', FALSE),
			array('', NULL, FALSE),
		);
	}
	
	/** 
	 * @dataProvider rule_matches_provider
	 * 
	 * @param mixed $input
	 * @param mixed $compare
	 * @param boolean $expected
	 */
	public function test_rules_matches($input, $compare, $expected)
	{
		$validation = Validation::factory(array(
			'password' => $input,
			'password_confirm' => $compare
		));
		
		$validation->rule('password', 'not_empty')
			->rule('password_confirm', 'matches', array(':validation', 'password_confirm', 'password'));
		
		$result = $validation->check();
		
		$this->assertSame($result, $expected);
	}
}

And here is the message file at: application/messages/signup.php

<?php

return array(
	'name' 		=> array(
		'not_empty' => 'Name is required',
		'min_length' => 'Name is too short, minimum is :param2',
		'max_length' => 'Name is too long, maximum is :param2'
	),
	'email' 	=> array(
		'not_empty' => 'Email is required',
		'email'		=> 'Email must be a valid email address'
	),
	'address' 	=> array(
		'min_length' => 'Address is too short, minimum is :param2',
		'max_length' => 'Address is too long, maximum is :param2'	
	),
	'zip' 		=> array(
		'min_length' => 'Zip is too short, minimum is :param2',
		'max_length' => 'Zip is too long, maximum is :param2'
	),
);

5 thoughts on “Kohana 3.1 Validation Adventures”

  1. Thanks for all your awesome Kohana posts, they’ve helped me a great deal. Question about how you are handling population of form data now that they have apparently removed setting default values if the form wasn’t submitted. I just switched from 3.0 to 3.1, and all my forms are broken, and was curious how others are dealing with it before I reinvent a probably not so good wheel.

    Thanks!

  2. @daveV – Thanks for visiting.

    Regarding your form question, I have migrated few of my apps and most of the problem are request/response related. Other than that, everything works fine thanks zombor created a 3.1 branch for Sprig modeling library.

    I was not aware about the default value thing and I think you should show some code perhaps – you can wrap them in code tag. Our situations are quite different but of course I can help.

  3. Great article.. I was searching for changes in Kohana 3.1. Your blog is an awesome resource for kohana developers… Thanks

  4. Hi, I’ve just found your article. It’s great. However, I don’t understand why Kohana’s got rid of callback and filters.
    I understand that they do the same thing in rules. I could create my own callback function which manipulates the data (e.g. calls a trim function on a description field), but there is no way in kohana 3.2 to get back the validated array of fields. this is annoying because it would be really useful.

    do you know any method to use any filtering in validation and get back the filtered data?

    Thx,
    Pal

  5. @Pal,

    If you use Kohana_ORM, there is a filter support which they maintained and is kinda awesome.

Leave a reply

Your email address will not be published. Required fields are marked *