CodeIgniter Base Classes Revisited
A little over a year ago, Phil Sturgeon wrote a great piece on how to use multiple base classes to reduce code duplication and make the development process much easier. It takes advantage of PHP 5's autoloading capabilities to include a class file when that class is referenced, allowing CI libraries to extend classes that have not yet been loaded.
Phil explains how this can be used to create base controllers that have logic that other controllers share.
It works like this: (borrowing Phil's example)
First, create the base classes: MY_Controller, Public_Controller and Admin_Controller. Save them in application/core/.
application/core/MY_Controller.php
class MY_Controller extends CI_Controller
{
function __construct()
{
parent::__construct();
$user_id = $this->session->userdata('user_id');
$this->data['user'] = $this->user_lib->get($user_id);
}
}
application/core/Public_Controller.php
class Public_Controller extends MY_Controller
{
function __construct()
{
parent::__construct();
if($this->config->item('site_open') === FALSE)
{
show_error('Sorry the site is shut for now.');
}
// If the user is using a mobile, use a mobile theme
$this->load->library('user_agent');
if( $this->agent->is_mobile() )
{
/*
* Use my template library to set a theme for your staff
* http://philsturgeon.co.uk/code/codeigniter-template
*/
$this->template->set_theme('mobile');
}
}
}
application/core/Admin_Controller.php
class Admin_Controller extends MY_Controller
{
function __construct()
{
parent::__construct();
if($this->data['user']['group'] !== 'admin')
{
show_error('Shove off, this is for admins.');
}
}
}
Then use PHP 5's magic function __autoload() in application/config/config.php to include the class files.
/*
| -------------------------------------------------------------------
| Native Auto-load
| -------------------------------------------------------------------
|
| Nothing to do with cnfig/autoload.php, this allows PHP autoload to work
| for base controllers and some third-party libraries.
|
*/
function __autoload($class)
{
if(strpos($class, 'CI_') !== 0)
{
@include_once( APPPATH . 'core/'. $class . EXT );
}
}
This allows you to go from
class Blog extends CI_Controller
{
function __construct()
{
parent::__construct();
// Whatever
$this->data['stuff'] = $whatever;
}
}
to
class Blog extends Public_Controller
{
function __construct()
{
parent::__construct();
// Whatever
$this->data['stuff'] = $whatever;
}
}
What would you do different?
This is a clever solution but has a couple of issues and doesn't really fit with the way I like to keep my apps organized.
First, Phil's implementation of __autoload() is placed in config.php which works but the autoloader isn't a config item so it should be handled elsewhere.
There is also the possibility that another library could come along and redeclare __autoload() or register an autoload function with spl_autoload_register(). Either of these cases will disable the __autoload() function in the config file. This means that spl_autoload_register() is probably a better solution for autoloading our base classes.
I've spent some time on this, trying out a few different ideas and I have finally come to a solution that I am more or less happy with.
How does it work?
It uses a pre system hook to register an autoload function before any controllers are loaded.
First, create a new folder, base, in the application folder and move the two base controllers, Public_Controller and Admin_Controller to the new folder.
Now, make sure hooks are enabled in application/config/config.php
$config['enable_hooks'] = TRUE;
Now create the autoloader hook class in application/hooks/MY_Autoloader.php. The class will have two functions. One is the actual autoload function and the other is a register function that calls spl_autoload_register(). We will call this register function from the hook.
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/**
* MY_Autoloader Class
*
* @package CodeIgniter
* @subpackage Hooks
* @category Hooks
* @author Shane Pearson <shane@highermedia.com>
*/
class MY_Autoloader {
private $_include_paths = array();
/**
* Register the autoloader function.
*
* @access public
* @param array include paths
* @return void
*/
public function register(array $paths = array())
{
$this->_include_paths = $paths;
spl_autoload_register(array($this, 'autoloader'));
}
// --------------------------------------------------------------------
/**
* Autoload base classes.
*
* @access public
* @param string class to load
* @return void
*/
public function autoloader($class)
{
foreach($this->_include_paths as $path)
{
$filepath = $path . $class . EXT;
if(! class_exists($class, FALSE) AND is_file($filepath))
{
include_once($filepath);
break;
}
}
}
// --------------------------------------------------------------------
} // end class MY_Autoloader
/* End of file MY_Autoloader.php */
/* Location: ./application/hooks/MY_Autoloader.php */
Next we need to create a pre_system hook to register the autoloader. Open application/config/hooks.php add the following:
$hook['pre_system'] = array(
'class' => 'MY_Autoloader',
'function' => 'register',
'filename' => 'MY_Autoloader.php',
'filepath' => 'hooks',
'params' => array(APPPATH.'base/')
);
The hook params take an array of paths that the autoloader will check for files in when loading a class. You can adjust this to fit your needs.
And that's it. The autoloader does the rest.
Summary
Base classes make for a more OOP friendly environment in CodeIgniter allowing controllers to be grouped together by sharing logic through a common parent base controller. Using CodeIgniter's hook system we can register an autoload function that includes these base classes from a specified location. This can also be used with libraries, models and helpers but we'll get to that another day.
I've made this code available on BitBucket for those interested.