All the web development frameworks I have worked with have a full fledged layout system out of the box except CodeIgniter. A good number of CI tutorials encourage including common header and footer files to build the final page but I don’t like this approach for the following reasons:
- It’s more difficult to swap layouts compared to using a single complete layout file.
- You need to have a knowledge (admittedly small) of the server-side language.
- A single complete layout file can be viewed in a WYSIWYG editor.
The layout system in the other frameworks I have worked with all use the two step view pattern and I have adapted CI to work the same way.
What is a two step view?
The idea behind using common header and footer files is to get a consistent look for your site or application and to make maintenance as painless as possible by providing a single place to make changes that affect the look and feel of the entire site or app.
Quoting Martin Fowler, the Two Step View pattern deals with this problem by splitting the transformation into two stages. The first transforms the model data into a logical presentation without any specific formatting; the second converts that logical presentation with the actual formatting needed. This way you can make a global change by altering the second stage, or you can support multiple output looks and feels with one second stage each.
How it works
Rather than extend the CodeIgniter controller class directly, the controller classes extend a custom base controller. This controller then extends the CI controller classes. The CI convention is to call this base controller class MY_Controller (default) and to place the class file in the libraries folder.
The base controller loads some default data and also has a render function which actually implements the two step view. I used the default Rails, Cake and Zend conventions of creating a folder named after the controller class and creating template files named after the actions. The render function does the first stage of the two step view pattern by checking if a file named after the action exists in folder named after a class. If it does, it loads it into a variable and then the second stage of the two step view pattern is implemented where this variable in addition to any other view data is injected into the layout file. So far, it’s worked great for me.
Some code, please
- The layout file (application/views/layouts/main.tpl.php)
<!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"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title><?php echo $title; ?></title> <?php echo $css; ?> </head> <body> <div id="page"> <div id="header"> <h1><?php echo $title; ?></h1> <?php $this->load->view('partials/menu.tpl.php'); ?> </div> <div id="content"> <h2><?php echo $heading; ?></h2> <?php echo $content; ?> </div> <div id="footer"> <p>© 2008 AVNet Labs.</p> </div> </div> </body> </html>
- The Base Controller (application/libraries/MY_Controller.php)
<?php if (!defined('BASEPATH')) exit('No direct script access allowed'); class MY_Controller extends Controller { protected $data = array(); protected $controller_name; protected $action_name; public function __construct() { parent::__construct(); $this->load_defaults(); } protected function load_defaults() { $this->data['heading'] = 'Page Heading'; $this->data['content'] = ''; $this->data['css'] = ''; $this->data['title'] = 'Page Title'; $this->controller_name = $this->router->fetch_directory() . $this->router->fetch_class(); $this->action_name = $this->router->fetch_method(); } protected function render($template='main') { $view_path = $this->controller_name . '/' . $this->action_name . '.tpl.php'; if (file_exists(APPPATH . 'views/' . $view_path)) { $this->data['content'] .= $this->load->view($view_path, $this->data, true); } $this->load->view("layouts/$template.tpl.php", $this->data); } protected function add_css($filename) { $this->data['css'] .= $this->load->view("partials/css.tpl.php", array('filename' => $filename), true); } } ?>
- The Welcome Controller (application/controllers/welcome.php)
<?php class Welcome extends MY_Controller { function __construct() { parent::__construct(); $this->add_css('main'); } function index() { $this->data['heading'] = 'Home Page'; $this->render(); } function edit() { $this->data['heading'] = 'Edit Page'; $this->render(); } }
- The Menu Partial Template (application/views/partials/menu.tpl.php)
<ul> <li><a href="<?php echo site_url(); ?>">Home</a></li> <li><a href="<?php echo site_url('welcome/edit'); ?>">Edit</a></li> </ul>
- The CSS Partial Template (application/views/partials/css.tpl.php)
<link href="<?php echo base_url() . 'css/' . $filename . '.css'; ?>" rel="stylesheet" type="text/css" />
- The Homepage Template (application/views/welcome/index.tpl.php)
<p>Home page stuff goes here</p>
- The Edit Page Template (application/views/welcome/edit.tpl.php)
<p>Edit page stuff goes here</p>
Update
ReLexEd added a comment pointing out that my original base controller code wouldn’t work with controllers in a subdirectory.
I have updated the base class to use the router object.
I was initially using segments to work out the view paths and although it’s been working for me, using the router is more robust.
For the controllers in subdirectories, the views also need to be in subdirectories eg. an index method in a controller at ‘application/controllers/admin/dashboard.php’ will have a corresponding view file at ‘application/views/admin/dashboard/index.tpl.php’
NB. To use the base_url() method in the css partial template, the url helper must be autoloaded.
Related posts:
Hi Ekerete, I got this problem with CI framework about where some logical checking should be performed, which also results from the limitation of CI template system. Please let me explain (a bit long, sorry):
I retrieve a list of items from database, each item has a category. I want to display all of them on the list (mix them all, sorted by create date). However, each of category has its own layout, for example, category A has image on the left, category B has image on the right, category C has an additional field to be shown, and so on.
So, where should the checking “if (category == ‘A’)…else if (category == ‘B’)…else…” be included taking best MVC practice, easy maintenance and logic/presentation separation factor into account?
1. Inside the model immidiately after retrieving data and before passing it to the controller?
2. Inside the controller just before sending the list to view (which means I have to render all the category-tempalte at this point and pass all of them as variables)?
3. Or inside the view? This means “raw” data will be sent to the view and the conditional statement will be written here. But this seems to “violate” the MVC model, and especially the CI template system does not allow us to use if statements inside views.
Your article seems to address just this problem, however sorry for my php knowledge, I still cant get how to do this using your 2 step view process. Do you mind to explain a bit more about this? And if you can do this in my context, it will be very appreciated.
Many thanks Ekerete.
Khoa
@Khoa,
You will need to write a custom helper to do this cleanly.
http://codeigniter.com/user_guide/general/helpers.html.
This is not strictly MVC but an implementation of the View Helper Pattern.
In your case, you would pass the array (or each item from the foreach loop – depending on your code) to the helper function and load different partial templates (or use conditionals in one partial template) depending on what category is being displayed.
Hope that helps.
Hi Ekerete,
This looks to be a great solution. Thanks for putting this together. A couple of things. First you may be autoloading the url helper but someone new to CI may be confused when the above code throws a fatal error at the first instance of base_url() or site_url() without loading the helper.
Also, what are you using the $param var for in MY_controller? Passing parameters via uri segments seems to work fine as is.
Thanks again Ekerete.
I see MY_Controller is not found in web browser, so how can I make it known to my Page Controllers? I am on PHP 4x
Hi matths,
Changing the __construct method to a PHP4 style constructor and changing the ‘protected’ access modifiers to ‘var’ should be enough to get it working.
But really, why aren’t you on PHP5?
Pingback: eloid's me2DAY
Thanks for the tutorial. It really saved me a lot of hassle and time! I can’t believe how easy it is to handle my views now. I’m new to codeigniter (or any framework for that matter) so was struggling with finding the “best” way to setup my layouts, and this works perfect for what I need. Thanks again.
@Matt,
Glad it helped.
First up, a big thanks to Ekerete!
This has put me on the right track, and I’m using this one very thankfully….
There is however one little thing I’m going to have to fix over the next few days; this solution will not play very well if you have placed controllers in subfolders, and wish to keep the same structure for the views.
Has to do with the following line:
$this->action = $this->uri->segment(2, ‘index’);
If anyone has already ‘fixed’ this little one, please share
I will try to fix this in my weekend, and if it passes mustard, I will post the results here.
@ReLexEd
I have just tried this with a controller in a subdirectory and I’m afraid you’re right.
I ended up having to use the Router object (which is available as one of the controller properties) to retrieve the path. In retrospect, I should have used this in the first place as it’s more reliable (but then again, I don’t have any nested controllers).
I have updated the post to reflect this.
Hi, nice article.
IMHO I think the “css.tpl.php” is unnecessary with .tpl.php suffix, since CI load it by default if you don’t specify it.
Nice blog, thanks.
It’s also worth noting that views can be nested within views, which means you can achieve a two step view very simply:
In controller:
$data['content'] = ‘my_partial_view’;
$this->load->view(‘layouts/main’, $data);
In ‘general’ view:
load->view($content); ?>
My last post got chopped:
In controller:
$data['content'] = ‘my_partial_view’;
$this->load->view(‘layouts/main’, $data);
In ‘general’ view:
$this->load->view($content);
hi , friends a Big thanks for this article ,
i create function Same As “add_css()” => “loadJs()”
///******************in MY_Controller.php***********
protected function loadJs($filename) {
$this->data['js'] .= $this->load->view(“partials/js.tpl.php”, array(‘filename’ => $filename), true);
}
//***************************************************
//******************in partials/js.tpl.php***********
<script type="text/javascript" language="javascript" src="”>
//**************************************************
//****************uses*****************************
$this->data['js'] = ‘jquery’;
$this->data['js'] = ‘tiny_mce’;
//**************************************************
//Enjoy Coding with CI
//harman_ds Says Big Thanks to all
//******************in partials/js.tpl.php***********
<script type="text/javascript" language="javascript" src="”>
src=”” Hey Comment System is Bad of this blog
i am not able to write lines in src=”" So please do it your self i tried twice ok i hope admin will correct this ,, have a nice day
Hi,
The solution you presented made me smile. I have been using this two step method in my custom developed CMS solutions. I will be pitching CI for one of my project and if approved I will be developing a CMS based on CI. For all uninformed, if you work on windows and modify core CI library, make sure you name your files “MY_” not simply “My”, windows treat “My” and “MY” same.
I really loved the CI vs other framework comparison you have done.
Hi All, I am Developing a web site. my below code work perfectly. but i have a problem i want “$statsid”. this is printed from other query. i want to pass this as variable or id to controller to get the data from table where equls to this “$statsistatsid
it is just like face book like Tom post a comments and there friends comments on tom comments
status_id; ?>
result() as $rs):?>
<img src="config->item(‘base_url’).$rows->profile_picture_path;?>” />
user_name;?> status_comment;?>