Phil Sturgeon

Web developer, kayaker, outdoors madman and part-time alcoholic.


CodeIgniter Base Classes: Keeping it DRY

Posted CodeIgniter at Feb 08, 2010

Most applications in CodeIgniter will have various types of pages. The public frontend, a backend admin panel, perhaps some sort of moderator or staff panel, etc. Logic for these types of pages is normally copied between all of their different Controllers which means, for example,  if the way the admin area protection is handled is changed there will be lots of Controllers to change and test. This logic can instead be shared by some creative extending of the Controller class to create custom Base Controllers like Public_Controller, Admin_Controller, etc.

WTF are you talking about?

The idea is that most of your controllers share something in common with each other. For example: All admin controllers need to make sure a logged in user is present and that they are an administrator. A public controller may want to load a theme for your application and load default user data, navigation links or anything else frontend related.

Wicked! How?

The first step is to create these Base Controllers.

application/libraries/MY_Controller.php

MY_Controller is a basic core library extension. Whenever you create a class with the MY_ prefix the CodeIgniter Loader class will load this after loading the core library, allowing your code to replace/extend the core library. We won't be replacing anything, but we will be adding to it.

class MY_Controller extends Controller
{
function __construct()
{
parent::Controller();

$user_id = $this->session->userdata('user_id');
$this->data['user'] = $this->user_lib->get($user_id);
}
}

All we have done here is create a base class that all of our Controllers and "controller types" will inherit. Anything we put in here and assign to $this will be available to anything that extends this class.

Notice that we have used parent::Controller() in the MY_Controller::__construct(). That is because while our code can be PHP 4 or PHP 5, CodeIgniter's core classes use PHP 4 syntax to maintain backwards compatibility. So if we use parent::Controller() here and set a __construct(), we can use parent::__construct() in the rest of our Controllers and "Controller types".

application/libraries/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');
        }
    }
}

Public_Controller is pretty much the same, but you can see we have some frontend-only related code here. The first statement will check to see if the site is currently open using a theoretical settings library that your application might habe and shows an error if the site is closed. The next statement uses the user agent library to offer a mobile version of the site to anyone on a mobile device.

application/libraries/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.');
        }
    }
}

Admin_Controller is again fairly similar. It uses a generic sort of user level checking to see if the user is an admin and shows an error if not.

Connecting Base Controllers to Controllers

While there are a few ways to do this, the easiest is to use PHP 5's wonderful __autoload() magic function. By placing this at the bottom of your config.php you can make it load early enough to run before the Controller and it will be somewhere that wont get overridden on upgrade.

/*
| -------------------------------------------------------------------
| 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 . 'libraries/'. $class . EXT );
}
}

Now the Base Controllers are being made and loaded, you need to inheriting them in your Controllers. So instead of the usual...

class Blog extends Controller
{
    function __construct()
    {
        parent::Controller();
        // Whatever
$data['stuff'] = $whatever;
    }
}

use...

class Blog extends Public_Controller
{
    function __construct()
    {
        parent::__construct();
        // Whatever
$this->data['stuff'] = $whatever;
    }
}

And there you have it! In your Controller you'll have all your data set in MY_Controller or other Base Controllers available in $this->data, so pass that to your views and it will be available. You can also use $this->load->vars('foo', $bar) in your Base Controllers to set values that are only available in your views.

Summary

Base Controllers are a nice simple way to give you global data, logic and shared code which can be specific to a certain part of your site. They can do all sorts of crazy stuff which I will leave for you to think about.

Please post your most inventive uses i the comments section.

Now I have upgraded to PyroCMS v0.9.8-beta2 they should actually be working.

Comments

User comments
  • Gravatar Mike

    May 27, 2010

    If you don't want the error messages within the log files (and want to keep the static flexibility), use:

    function __autoload($class)
    {
    if (strpos($class, 'CI_') !== 0)
    {
    if (is_file($location = APPPATH.'libraries/'.$class.EXT))
    {
    include_once $location;
    }
    // core folder is used by CodeIgniter 2
    else if (is_file($location = APPPATH.'core/'.$class.EXT))
    {
    include_once $location;
    }
    }
    }

    If you want to restrict to specific classes, use:

    function __autoload($class)
    {
    if (strpos($class, 'CI_') !== 0)
    {
    if (in_array($class, array('MY_Controller', 'MY_Lang'))) // etc.
    {
    if (is_file($location = APPPATH.'libraries/'.$class.EXT))
    {
    include_once $location;
    }
    else if (is_file($location = APPPATH.'core/'.$class.EXT))
    {
    include_once $location;
    }
    }
    }
    }

  • Gravatar Mike

    May 27, 2010

    It's a good idea to restrict the loading like Buso explained. If you don't restrict it and watch your log files you see that it tries to load MY_ functions that can not be found. This is because they don't exist, are placed within other files (bundle all ..._Controller classes within MY_Controller.php for ex.) or are not at the right place (there is a lybraries and core folder). At the same time if you look at the benchmarks, not restricting it will slow down the application al lot. 0.6 or with restrictions 0.03.

  • Gravatar Kyle

    Apr 06, 2010

    So, I'm using a MY_Controller like you did above, and I'm trying to redirect('signin'); if no one is signed in, but it won't work. Does a MY_Controller constructor not allow redirect(), because they work in normal controller constructors?

  • Gravatar Phil Sturgeon

    Mar 29, 2010

    @Buso: I did not see what Zoran was getting at. There is no issue with allowing an autoloader to potentially load any library as it gives you flexibility to use static classes if you like.

    There would be no point in restricting it to ONLY load controller base classes, as that is two conditions that don't need to be run. In trying to make it more efficient you are actually slowing things down and restricting your possibilities.

  • Gravatar Cahva

    Mar 29, 2010

    Just had to drop in and thank you for a very good post! I've changed using this new autoload from now on. I dont even use MY_Controller anymore. Instead I do a Base_Controller which describes the base controller better than MY_Controller :)

  • Gravatar Buso

    Mar 27, 2010

    Thanks for the tips phil.

    Im gonna have to agree with Zoram though:

    Isn't it better to change the if condition to something like..

    $class=='Public_Controller' || $class=='Admin_Controller'

    ?

  • Gravatar Twisted1919

    Mar 24, 2010

    What if you do something like :
    libraries/MY_Controller.php

    class MY_Controller extends Controller{

    protected $data ;

    public function __construct(){
    parent::Controller();
    $this->data['blah'] = 'Something available on Front and Backend'
    }

    }
    //Same file
    class Frontend extends MY_Controller{

    public function __construct(){
    parent::__construct();
    //load here something needed only on frontend etc .
    }

    }
    //Same file
    class Backend extends MY_Controller{

    public function __construct(){
    parent::__construct();
    //do something here for backend only.
    }

    }


    What's wrong if you do like this ?

  • Gravatar Phil Sturgeon

    Mar 18, 2010

    @Obolus: What do you mean? you can load models and libraries just as you do now. The autoload obviously will allow you to load libraries directly if they support it, but that really is not the point of the article.

    CodeIgniter does not require this functionality to run, it is a Don't Repeat Yourself step that can be translated to almost any framework. Kohana does not support this out of the box, but you could implement it very similarly.

    @Carlos: Jamie Rumbelow has created a MY_Model which I have been contributing too which I use in all my projects.

    http://github.com/jamierumbelow/codeigniter-base-model

  • Gravatar Phil Sturgeon

    Mar 18, 2010

    @Dimas: Neither method is better or worse, they are two different things. For any code that you want in EVERY controller, use MY_Controller. For code you want in SOME controllers, use the named method.

  • Gravatar Carlos

    Mar 18, 2010

    I would like share this MY_Model class with you
    http://csotelo.blogspot.com/2009/10/model-library-for-codeigniter-php-if.html

  • Gravatar Jim Wardlaw

    Mar 17, 2010

    Great work Phil.
    Solved my issue with distributing a shared admin system across multiple sites like a dream!

  • Gravatar Obolus

    Mar 17, 2010

    This is swell until you need to load libraries or models. Thanks for the write-up in any case.

    If an project requires separation like this, it would be better to go with Kohana. Until the next CI release anyway.

  • Gravatar David

    Mar 17, 2010

    Hi,

    when loading a model, i had the same error as Ionut Marinescu :

    Message: include(~/application/libraries/Model.php) [function.include]: failed to open stream: No such file or directory

    and

    Message: include() [function.include]: Failed opening 'C:wampwwwoutils/Yhoi8_yt%kjZd23/application/libraries/Model.php' for inclusion (include_path='.;C:\php5\pear')

  • Gravatar Dimas

    Mar 10, 2010

    Is there any benefit of doing one or the other?

    1) having MY_controller.php and putting it in the libraries folder or ...
    2) just creating a Base_controller.php in your controllers folder and then having any new controller extend it?

    Or is it just a matter of preference? It seems that you do both above ...

  • Gravatar Ionut Marinescu

    Mar 09, 2010

    i tried to use your method but i get a error in the function __autoload it tries to load application/libraries/Model.php
    i have some autoloaded models in autoload.php not sure if that is the problem

  • Gravatar Nguy?n ?ình Trung

    Mar 04, 2010

    I'm just getting used to those OOP approach. Although it can help me reduce the number of line to write, it just keep bloating up by the time. Keeping dynamic super variables for the child Controller to override is such a complex task.
    I found it very difficult to maintain the code this way, because I have to keep eyes on parent classes as well as child class. Or maybe my classes just too poor designed.

  • Gravatar Codemagician

    Feb 19, 2010

    May this code live for ever. Please keep it on the most secure server on the planet, clone to keep alive! :D

    Thanks heaps. I wonder why this is so hard to find at CI head quaters. Don't people build reusable code in PHP land? ;)

    Cheers :)

  • Gravatar Zoran

    Feb 15, 2010

    This is good idea which i am using also, in your __autoload method, you can check for Controller's children classes( Admin_Controller and Public_Controller) and load only them, cause it won't be good idea to load other classes that you only need in only few application controllers.
    This is the best way, cause you are extending the system the way it is built.
    Really good post. Thank you

  • Gravatar Sam Bruner

    Feb 12, 2010

    Great article,
    few weeks ago I read the posts of this guy: http://blog.umnet.co.il/ about developing cms with codeigniter, and He also talked about that.
    So for newbies all this info is very useful.

  • Gravatar Bradfields

    Feb 11, 2010

    I like it. I've definitely gone about this kind of thing the long-winded way in the past and included things like permission checks in the top of every controller.

    I'll be testing out this method in my next CI project so props from me.

  • Gravatar Phil Sturgeon

    Feb 09, 2010

    @Dave Bowman: I considered doing it this way, but it's an extra check we don't need. If a file does not exist then it will throw a "Class does not exist" fatal error anyway, so by just trying to include then saying "meh" it doesn't make any real difference.

    I don't like to make a habit of using @ but I don't think it should be considered bad practice. The error will still show in the error logs and if you truly don't care about whether something works or fails it is perfect.

  • Gravatar Dave Bowman

    Feb 09, 2010

    Simple, but effective technique, Thanks, Phil. There's this one thing though, I'm not sure about: it is supposed to be an ill practice - suppressing possible errors with @. Wouldn't it be more "righteous" to check if file_exists and if it does, then include it in normal way, thus presenting the possibility for it to eloquently fail?

Post a comment