kohana 3.2 tutorial
Below you will find an article / tutorial on Kohana 3.2 – An elegant HMVC PHP5 framework that provides a rich set of components for building web applications.
I am sharing a site template that use authentication & internationalization. You can view the website (not much to see) and download it from github. Any pull request will be more than welcome!
Download via Github:
https://github.com/patricksebastien/kohana-3.2-example
Topics
Install
Using GIT:
http://kohanaframework.org/3.2/guide/kohana/tutorials/git
or
Download:
http://kohanaframework.org/download
Structure of folders:
www/yoursite/site/index.php & .htaccess -> and your assets (css, images, js)
www/yoursite/application -> the very core of your site
www/yoursite/module & system -> core of kohana
or:
www/kohana/version/system & module -> multiple site using kohana
or:
everything in www/yoursite/site (application, module, system, index.php, .htaccess, …)
Test:
127.0.0.1/
If it’s greenish, then remove install.php
index.php
Reflect this structure in www/yoursite/site/index.php:
1 2 3 | $application = '../application'; $modules = '../modules'; $system = '../system'; |
bootstrap.php
Modify application/bootstrap.php
1 |
Set PRODUCTION vs DEVELOPMENT
1 2 3 4 5 | if (isset($_SERVER['KOHANA_ENV'])) { Kohana::$environment = constant('Kohana::'.strtoupper($_SERVER['KOHANA_ENV'])); } else { Kohana::$environment = ($_SERVER['REMOTE_ADDR'] == '127.0.0.1' ? Kohana::DEVELOPMENT : Kohana::PRODUCTION); } |
Initialize Kohana
1 2 3 4 5 | 'base_url' => '/', // or for example: /yoursite/site/mykoapp 'index_file' => FALSE, // SEO (avoid index.php/mycontroller/action) 'profile' => (Kohana::$environment !== Kohana::PRODUCTION), //see how good you are 'caching' => (Kohana::$environment === Kohana::PRODUCTION), 'errors' => TRUE, //for custom 404, 500 FALSE for internal error handling |
Enable modules (for example):
1 2 3 | 'auth' => MODPATH.'auth', // Basic authentication 'database' => MODPATH.'database', // Database access 'orm' => MODPATH.'orm', // Object Relationship Mapping |
Many modules are available:
https://github.com/kolanos/kohana-universe
http://kohana-modules.com/
Set the routes (default controller will be login.php in this example)
http://kohanaframework.org/3.2/guide/kohana/routing
The order of your routes are important!
1 2 3 4 5 | Route::set('default', '(<controller>(/<action>(/<id>)))') ->defaults(array( 'controller' => 'login', // application/classes/controller/login.php 'action' => 'index', )); |
.htaccess
Add this line at the very top of .htaccess (protect from sniffing directory)
1 | Options All -Indexes -Multiviews |
and modify (/ or for example: /yoursite/site/mykoapp)
1 2 | # Installation directory RewriteBase / |
Template
http://kerkness.ca/kowiki/doku.php?id=template-site:create_the_template
Create the template controller (classes/controller/template/website.php) extending Controller_Template
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <?php defined('SYSPATH') or die('No direct script access.'); class Controller_Template_Website extends Controller_Template { public $template = 'template/website'; /** * The before() method is called before your controller action. * In our template controller we override this method so that we can * set up default values. These variables are then available to our * controllers if they need to be modified. */ public function before() { parent::before(); if ($this->auto_render) { // Initialize empty values $this->template->title = ''; $this->template->content = ''; $this->template->styles = array(); $this->template->scripts = array(); } } /** * The after() method is called after your controller action. * In our template controller we override this method so that we can * make any last minute modifications to the template before anything * is rendered. */ public function after() { if ($this->auto_render) { $styles = array( 'assets/css/website.css' => 'screen, projection', ); $scripts = array( 'http://code.jquery.com/jquery.min.js', ); $this->template->styles = array_merge( $this->template->styles, $styles ); $this->template->scripts = array_merge( $this->template->scripts, $scripts ); } parent::after(); } } |
Create the controller (classes/controller/login.php) extending Controller_Template_Website (look at the second action for an example on how to use another template per action):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Controller_Login extends Controller_Template_Website { public function action_index() { $this->template->title = 'Log in'; $this->template->content = View::factory('login'); // application/views/login.php } // this action is using another template but using the same Controller_Template_Website public function action_showinfooverlay() { $this->template = 'template/overlay'; parent::before(); $this->template->title = 'Log in'; $this->template->content = View::factory('login'); // application/views/login.php } } |
Create the html template (view/template/website.php)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<?php echo substr(I18n::$lang, 0, 2); ?>" lang="<?php echo substr(I18n::$lang, 0, 2); ?>"> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <meta name="language" content="<?php echo I18n::$lang ?>" /> <title><?php echo $title ?></title> <?php foreach ($styles as $file => $type) echo HTML::style($file, array('media' => $type)), PHP_EOL ?> <?php foreach ($scripts as $file) echo HTML::script($file), PHP_EOL ?> </head> <body> <div id="wrapper"> <?php echo $content ?> </div> </body> </html> |
Finally create your view content application/views/login.php
1 2 3 4 5 6 7 8 9 | <?php echo Form::open(); ?> <dl> <dt><?php echo Form::label('username', 'User') ?></dt> <dd><?php echo Form::input('username') ?></dd> <dt><?php echo Form::label('password', 'Pwd') ?></dt> <dd><?php echo Form::password('password') ?></dd> </dl> <p><?php echo Form::submit(NULL, 'Log in'); ?></p> <?php echo Form::close(); ?> |
At this point you can point your browser to see the login page:
http://localhost/ -> depending on base_url and .htaccess (could be in a sub-folder)
http://localhost/login -> not defined login as the default controller in bootstrap.php
Someone on #kohana (irc / freenode) made a suggestion of using view classes instead of template controller. Here’s two solutions: https://github.com/zombor/kostache & https://github.com/beautiful/view
Configure
Database
Copy modules/database/config/database.php to application/config/database.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 'default' => array ( 'type' => 'mysql', 'connection' => array( 'hostname' => 'localhost', 'database' => 'yourdb', 'username' => 'user', 'password' => 'pwd', 'persistent' => FALSE, ), 'table_prefix' => '', 'charset' => 'utf8', 'caching' => FALSE, 'profiling' => FALSE, // if you use profiling turn this on (to see querys) ), |
Cookie
in application/bootstrap.php
must be after: spl_autoload_register(array(‘Kohana’, ‘auto_load’));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * Cookie */ // Set the magic salt to add to a cookie Cookie::$salt = 'fjsdijeihrewhbfsugfuyegwufewgwb'; // Set the number of seconds before a cookie expires Cookie::$expiration = DATE::WEEK; // by default until the browser close // Restrict the path that the cookie is available to //Cookie::$path = '/'; // Restrict the domain that the cookie is available to //Cookie::$domain = 'www.mydomain.com'; // Only transmit cookies over secure connections //Cookie::$secure = TRUE; // Only transmit cookies over HTTP, disabling Javascript access //Cookie::$httponly = TRUE; |
Session (stored in database)
http://kohanaframework.org/3.2/guide/kohana/sessions
in application/bootstrap.php add the default session handler:
1 | Session::$default = 'database'; |
Copy system/config/encrypt.php to application/config/encrypt.php
1 2 3 4 5 6 7 8 9 |
Create a table if you want to use database session
1 2 3 4 5 6 7 | CREATE TABLE `sessions` ( `session_id` VARCHAR(24) NOT NULL, `last_active` INT UNSIGNED NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY (`session_id`), INDEX (`last_active`) ) ENGINE = MYISAM; |
Copy system/config/session.php to application/config/session.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | return array( 'database' => array( 'name' => 'session', 'encrypted' => TRUE, // need a key in config/encrypt.php 'lifetime' => DATE::HOUR, // 0 = expire when the browser close 'group' => 'default', 'table' => 'sessions', 'columns' => array( 'session_id' => 'session_id', 'last_active' => 'last_active', 'contents' => 'contents' ), 'gc' => 500, ), ); |
Use it in your controller:
1 2 | Session::instance()->set('key', 'value'); Session::instance()->get('key'); |
yoursite
Create a file in application/config/yoursite.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Then you can call a config like this:
1 2 | $playlists = Kohana::$config->load('yoursite.playlists'); Kohana::$config->load('yoursite.myconfig1 '); |
Message
(might be better to use I18N directly)
Create a file in application/message/yoursite.php for you project
1 2 3 4 5 |
Then you can use it like this:
1 | Kohana::message('yoursite', 'permission'); |
Translation
http://blog.mixu.net/2010/11/11/kohana-3-i18n-tutorial/
1 | <?php echo __('Dear :firstname, your username is: :user', array(':firstname' => 'gfdgfdg', ':user' => 'gfdgfd')); ?> |
Validation
http://kohanaframework.org/3.2/guide/kohana/security/validation
Copy system/messages/validation to application/message/validation.php if you want to change the error message
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Validate a form ($_POST) if (isset($_POST) && Valid::not_empty($_POST)) { // Validate the login form $post = Validation::factory($_POST) ->rule('username', 'not_empty') ->rule('username', 'regex', array(':value', '/^[a-z_.]++$/iD')) ->rule('password', 'not_empty') ->rule('password', 'min_length', array(':value', 3)); // If the form is valid and the username and password matches if ($post->check()) { echo 'Validated'; } |
Using a callback for custom validation & error message
1 2 3 4 5 6 7 8 9 |
1 2 3 4 5 6 7 8 9 | // CALLBACK // validation rule: password != username public function pwdneusr($validation, $password, $username) { if ($validation[$password] === $validation[$username]) { $validation->error($password, 'pwdneusr'); } } |
http://kohanaframework.org/3.2/guide/kohana/tutorials/error-pages
Create the views
views/error/404.php / 500.php etc…
Extend the exception handler of Kohana
classes/kohana/exception.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | <?php defined('SYSPATH') or die('No direct script access.'); class Kohana_Exception extends Kohana_Kohana_Exception { public static function handler(Exception $e) { if (Kohana::DEVELOPMENT === Kohana::$environment) { parent::handler($e); } else { try { Kohana::$log->add(Log::ERROR, parent::text($e)); $attributes = array ( 'controller' => 'error', 'action' => 500, 'message' => rawurlencode($e->getMessage()) ); if ($e instanceof HTTP_Exception) { $attributes['action'] = $e->getCode(); } // Error sub-request. echo Request::factory(Route::get('error')->uri($attributes)) ->execute() ->send_headers() ->body(); } catch (Exception $e) { // Clean the output buffer if one exists ob_get_level() and ob_clean(); // Display the exception text echo parent::text($e); // Exit with an error status exit(1); } } } } |
Create the controller:
classes/controller/error.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <?php defined('SYSPATH') or die('No direct script access.'); class Controller_Error extends Controller_Template_Nobrand { public function before() { parent::before(); // Internal request only! if (Request::$initial !== Request::$current) { if ($message = rawurldecode($this->request->param('message'))) { $this->template->message = $message; } } else { $this->request->action(404); } $this->response->status((int) $this->request->action()); } public function action_404() { $this->template->title = '404 Not Found'; $this->template->content = View::factory('error/404' ); } public function action_500() { $this->template->title = 'Internal Server Error'; $this->template->content = View::factory('error/500' ); } public function action_503() { $this->template->title = 'Maintenance Mode'; $this->template->content = View::factory('error/503' ); } } |
Edit application/bootstrap.php to add the route:
1 2 3 4 |
Authentication
Copy modules/auth/config/auth.php to application/config/auth.php
1 2 3 4 5 6 7 |
Schema for mysql / postgresql located:
modules/orm/auth-schema-mysql.sql
Change the rules if you don’t want to required an email (you will also need to remove the index in mysql: uniq_email – BTREE)
Copy modules/orm/classes/model/auth/user.php to application/classes/model/auth/user.php and change the public function rules() to your needs
It’s a good idea to add a new role for your normal user, that way you can list them easily:
1 |
Create application/messages/models/user.php
1 2 3 4 5 |
If you want to use the remember feature:
1 2 | $remember = isset($post['remember']); Auth::instance()->login($post['username'], $post['password'], $remember) |
then you need to be sure to have a cookie salt in application/bootstrap.php
1 2 3 4 | /** * Cookie salt for remember user info */ Cookie::$salt = 'fdsh-tretgd-re-gfds-gt-erg-fdg-'; |
Some useful stuff:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Add an administrator (in a temporary controller) $model = ORM::factory('user'); $model->values(array( 'username' => 'admin', 'password' => 'admin', 'password_confirm' => 'admin', 'email' => 'your@email.com', )); $model->save(); // remember to add the login role AND the admin role // add a role; add() executes the query immediately $model->add('roles', ORM::factory('role')->where('name', '=', 'login')->find()); $model->add('roles', ORM::factory('role')->where('name', '=', 'admin')->find()); |
1 2 3 4 5 6 7 8 9 | // If this user doesn't have the admin role, and is not trying to login, redirect to login public function before() { parent::before(); if ( ! Auth::instance()->logged_in('admin') AND Request::current()->uri() !== 'manage') { $this->request->redirect('/manage'); } } |
1 2 3 4 | // Administrator already logged in, redirect to dashboard if (Auth::instance()->logged_in('admin')) { $this->request->redirect('manage/dashboard'); } |
1 2 | // Log the user Auth::instance()->login($post['username'], $post['password'], FALSE) |
1 2 | // Check if the user have the admin permission if(!Auth::instance()->logged_in('admin')) { |
1 2 | // Log user out Auth::instance()->logout(); |
1 2 3 4 | // check if email or username (automagic) is already taken if(ORM::factory('user')->unique_key_exists($_POST['username'])) { echo "FOUND"; } |
ORM
http://kohanaframework.org/3.2/guide/orm/
http://karlsheen.com/kohana/kohana-3-orm-tutorial-and-samples/
http://kohanaframework.blogspot.com/2010/12/kohana-3-orm-simple-example.html
http://www.geekgumbo.com/2011/05/24/kohana-3-orm-a-working-example/
http://kohanaframework.org/3.2/guide/api/ORM
ORM is included with the Kohana 3.x install but needs to be enabled before you can use it. In your application/bootstrap.php file modify the call to Kohana::modules and include the ORM modules:
1 | 'orm' => MODPATH.'orm', |
The table name must be in plural;
The table must have an id with auto increment (required);
You must create a Model that extends ORM class (this one not in plural)
1)
Create a table with a “s” as the end:
tracking -> trackings
category -> categories
2)
Create a model (application/classes/model) without the “s”
tracking.php
category.php
1 2 3 4 5 |
1 2 3 4 5 |
3)
Establish your relation (one-to-one, one-to-many etc…)
http://kohanaframework.org/3.2/guide/orm/relationships
4)
Use your model / ORM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | try { $tracking = ORM::factory('tracking'); $tracking->user_id = Auth::instance()->get_user()->id; $tracking->session_id = Session::instance()->id(); $tracking->title = ' fdsfdsf '; if($tracking->save()) { echo "save"; } else { echo "for some reason, there's an error"; } } catch (ORM_Validation_Exception $e) { echo "error"; var_dump($e->errors()); } |
Some useful stuff:
1 2 3 | $user = ORM::factory('user'); echo $user->count_all(); echo $user->last_query(); |
1 2 3 4 | $playlists = ORM::factory('playlist')->where('week_id', '=', 1)->find_all(); foreach ($playlists as $playlist) { echo $playlist->url; } |
Last ID from ->save();
1 2 | $myormmodel->save(); echo $myormmodel->id(); |
Dynamic ORM query builder
1 2 3 4 5 6 7 8 | public function search_keywords(array $keywords) { foreach($keywords as $keyword) { $this->or_where('title', 'like', '%'.$keyword.'%'); } return $this->find_all(); } |
ORM Validation
http://kohanaframework.org/3.2/guide/orm/examples/validation
http://kohanaframework.org/3.2/guide/kohana/security/validation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?php defined('SYSPATH') or die('No direct access allowed.'); class Model_Member extends ORM { public function rules() { return array( 'username' => array( array('not_empty'), array('min_length', array(':value', 4)), array('max_length', array(':value', 32)), array(array($this, 'username_available')), ), 'password' => array( array('not_empty'), ), ); } public function filters() { return array( 'password' => array( array(array($this, 'hash_password')), ), ); } public function username_available($username) { // There are simpler ways to do this, but I will use ORM for the sake of the example return ORM::factory('member', array('username' => $username))->loaded(); } public function hash_password($password) { // Do something to hash the password } } |
Database:
http://kohanaframework.org/3.2/guide/database/
http://kohanaframework.org/3.2/guide/api/Database
There’s 2 ways to query a database: prepared (normal SQL) and query builder (dynamic)
Prepared:
1 2 3 4 5 6 | $query = DB::query(Database::SELECT, 'SELECT * FROM users WHERE username = :user AND status = :status'); $query->parameters(array( ':user' => 'john', ':status' => 'active', )); |
Query builder:
1 2 3 4 | $query = DB::select()->from('users')->where('username', '=', 'john'); $query = DB::select('username')->distinct(TRUE)->from('posts'); $query = DB::select()->from(`posts`)->limit(10)->offset(30); … |
Results:
1 2 3 4 5 6 | $results = DB::select()->from('users')->where('verified', '=', 0)->execute(); foreach($results as $user) { // Send reminder email to $user['email'] echo $user['email']." needs to verify his/her account\n"; } |
1 2 3 4 5 6 | $results = DB::select()->from('users')->where('verified', '=', 0)->as_object()->execute(); foreach($results as $user) { // Send reminder email to $user->email echo $user->email." needs to verify his/her account\n"; } |
Only get 1 result:
1 | $total_users = DB::select(array('COUNT("username")', 'total_users'))->from('users')->execute()->get('total_users', 0); |
1 2 3 | // Get the total number of records in the "users" table $db = Database::instance(); $count = $db->count_records('testi'); |
3rd party libraries
The convention is to place 3rd party files in application/vendor. For instance, if you had an installation of Doctrine, you would place it in application/vendor/doctrine.
1 2 | require Kohana::find_file('vendor', 'Swift-4.0.5/lib/swift_required'); $transport = Swift_SmtpTransport::newInstance(...); // This is autoloaded for me by Swiftmailer |
PHPExcel – create PDF, CSV, Excel:
application/vendor/phpexcel/PHPExcel.php & PHPExcel
then in your controller:
1 2 | require Kohana::find_file('vendor', 'phpexcel/PHPExcel'); $objPHPExcel = new PHPExcel(); |
Email – you can use this module:
https://github.com/Luwe/Kohana-Email
or directly use swiftmailer:
http://swiftmailer.org/
1 2 3 4 5 6 7 8 9 10 11 12 | require Kohana::find_file('vendor', 'swift/swift_required'); //Create the Transport $transport = Swift_SmtpTransport::newInstance('localhost', 25); //Create the Mailer using your created Transport $mailer = Swift_Mailer::newInstance($transport); //Create a message $message = Swift_Message::newInstance('Email') ->setFrom(array('from@email.net' => 'From')) ->setTo(array('to@email.net')) ->setBody('An email'); //Send the message $result = $mailer->send($message); |
Helper
http://kohanaframework.org/3.2/guide/api/Arr
If you want to add some custom helper (generally used statically) or library (instantiated / object), for example: application/classes/participants.php
1 2 3 4 5 |
in your controller:
1 | $weektodisplay = Participant::currentweek(); |
Tips
To get the params in a controller (depending on your routes in application/bootstrap.php)
1 | $this->request->param('id') |
Debug:
1 | echo Debug::vars(); |
To get the current controller:
1 |
To point at the right directory use:
1 | <?php echo URL::base(); ?> |
To get a custom column from users (auth):
1 | echo Auth::instance()->get_user()->week; |
Date:
1 2 |
Way to use the model and the post for edition in form:
1 2 3 4 5 |
To redirect use:
1 | $this->request->redirect('manage/dashboard'); |
To make a link use:
1 |
Function inside controller:
inside action_x():
1 |
outside action_x():
1 2 3 | static function _replaceplaylistendtime(&$value, $key, $p) { ... } |
Jquery
Use the latest minified version on google server:
http://code.jquery.com/jquery.min.js
User interface:
http://ninjaui.com/
http://jqueryui.com/
http://flowplayer.org/tools/index.html
Ajax:
In your “ajax” controller / action
1 2 3 4 5 | if ($this->request->is_ajax()) { $id = json_decode($_POST['refresh']); $this->auto_render = FALSE; echo json_encode(array('result' => $id)); } |
In your jquery:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | $.ajax({ type: "POST", url: "<?php echo URL::base(); ?>home/refresh", data: "refresh=1", async: true, dataType: "json", success: function(resultArray, textStatus, XMLHttpRequest) { var result = parseInt(resultArray['result']); alert(result); }, error: function(request, textStatus, errorThrown) { alert('error refreshing the session'); } }); |
Bookmarks
http://kohanaframework.org/3.2/guide/kohana/tutorials/
http://kohanaframework.org/3.2/guide/api or http://kohana.nerdblog.pl/api/
Deploying:
See this for more information about deploying Kohana application
http://nerdblog.pl/2011/09/05/deploying-kohana-3-2-application-in-production/

hi Omry,
have a look at my site template that use authentication & internationalization:
https://github.com/patricksebastien/kohana-3.2-example
http://www.workinprogress.ca/ko32example/site/
regards
hi,
i would like to see i18n implemenatation, for content, forms and messages.
regards,
Omry
Hi Sinan,
GIT doesn’t add empty directories. Fixed (by adding a .gitignore).
Please add “cache” and “logs” folders to your application folder.
In unix system it causes errors if they do not exist.
Thanks for the project.
Hi Robert,
You’re right, I forgot to add the “reset_token” field in “users”. You have to implement the mailing functionality yourself (you can see an example in this post under 3rd party libraries). After that, the reset password will work. About the logging, the “Remember” checkbox should add a cookie BUT something is missing in “config/ko32example.php”: session_lifetime => 0. Will fix it soon.
The best thing would be to make a pull request on github if you want to contribute your code.
EDIT: fixed on github
Thanks for the excellent article/code example. It really helps me a lot to implement my own authentication stuff.
I have a few questions, and I hope you are willing to answer them.
In function action_password() in class Controller_Account you have got the following line:
$user = ORM::factory(‘user’)->where(‘email’, ‘=’, $_GET['email'])->where(‘reset_token’, ‘=’, $_GET['token'])->find();
If I am correct, there is no field “reset_token” in table users.
So action_reset does not work, or does it?
I noticed you commented out the mailing stuff and replaced it with “//TODO email”.
That is no problem, because I think I will be able to add the mailing functioality myself.
A few questions though:
- Did you implement it afterwards? If so, is the code available?
- Do you use a field “reset_token”, or do you use table “user_tokens” to store the tokens? And if you haven’t implemented this part (yet), what would you advise to do?
It would be best fantastic if you could complete action_reset and action_password.
I am going to try it myself, but I am fairly new to Kohana, so it won’t be easy. If I succeed, I will send you a copy.
Thanks again for the article/code example.
Cheers,
Robert
PS: It looks like the user stays logged in. It does not make a difference you choose ‘remember’ or not. A small bug?
Thanks for this article! I’m sure many will find this very useful. Also thanks for showing me http://kohana.nerdblog.pl/api/ :)