Codeigniter – Extending One Install To Handle Multiple Sites

Codeigniter – Extending One Install To Handle Multiple Sites

In a previous blog post I talked about building a custom CMS, that allowed us to manage data that was displaying in a mobile app.

I managed to get a basic version of the CMS up and running. We had been using it for a good 12 months to manage the data without too many issues or bugs.

Without going into war and peace as to what our mobile app was trying to achieve. The basic idea was to allow people to find pubs, bars, cafes, takeaways, hotels and transport information in certain areas of the UK.

After around 18 months of the app being out in the world. We got in contact with someone who had a business managing a lot of Pubs around the UK.

One of the ideas that we came up with was to create a ‘customer portal’ so they could login in and manage the data for the pubs that they owned. Allowing them to display offers and other changing information in our app.

This benefited us as we could start making a bit of money from the app. The company benefited as people travelling to the area would see their pubs as a recommended place to go to. As well as enticing people in with relevant offers.

The big challenge for me was to get this customer portal up and running.

One of the principles of professional programming and software development is not to repeat yourself in terms of the code you write. As I had already done the grunt work for a login mechnicasm, as well as being able to edit entries and update information. I didn’t want to have two code bases that did the same thing.

Don’t Repeat Yourself

I had already written a load of code and functions that allowed you to add, edit and delete entries into our app in our existing CMS.

So I wanted to be able to re-use these code files in the new customer portal. However the design of login screen, dashboard and edit screen had to be different.

I wanted our CMS and the customer portal to run on different domains but still have access to a lot of the same code.

It was a bit of a challenge.

Two different front-ends, using the same back-end code to update a database.

The reason I wanted to use the same code is that if I had to make a change to the way we saved an entry. I only had to change it in one place and the update would then filter down to both sites.

If I had two code bases, I most certainly would have forgot to change one of them at some point. Causing issues for us, or our customers.

When Two Become One

I had hung my hat on the Codeigniter framework. It’s great to get something up and running very quickly without having to write loads of code yourself.

Anwyay, I couldn’t find anything obvious on how to achieve what I wanted. I started digging around on a lot of the developer forums like stackoverflow. Hoping to find a solution.

Luckily, I managed to find something that helped. I can’t find the link now, whoever worked it out wants a medal.

It saved me so much time and effort.

So how did I implement it in our sites?

First thing I need to do was to create a base controller and base model class to be able to route users to the correct website.

In the core folder I created two new files called ADMIN_Controller.php and ADMIN_Model.php

The ADMIN_Controller.php looked like this

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class ADMIN_Controller extends CI_Controller {

    function __construct()
    {
        parent::__construct();

        $logged_in_value = 'logged_in';
        $redirect_url = 'admin/login';

        if (SITE == 'my')
        {
            $logged_in_value = 'my_logged_in';
            $redirect_url = 'my/mylogin';
        }

        if($this->session->userdata($logged_in_value) !== TRUE)
        {
            $this->session->set_userdata('redirect_url', current_url());
            redirect($redirect_url);
        }
    }
}

This controller would default to the admin, main CMS unless the site was for the my. subdomain then it would assume you wanted to login to our customer portal.

The main purpose of this controller was to determine if you where logged in or not and what the redirect url should be if you weren’t.

The ADMIN_Model.php looked like this

<?php

class ADMIN_Model extends CI_Model
{
    function __construct()
    {
        parent::__construct();

        //If the site is for my.xxx load new db config
        $site = SITE; 
    }
}

Pretty straight forward so far?

The next thing to do was to create a hook called HostNameRouter.php.

This file was to redirect you to the main admin CMS or the client portal, depending on which subdomain you visited.

<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
/*
	Manage multiple hostnames (domains, sub-domains) within a single instance of CodeIgniter.
	Example:
		If you had the following domain/sub-domain style for your site:
		your-domain.com
		api.your-domain.com
		shop.your-domain.com
		Create the following sub-directories (+ files) in your application/controllers directory:
		application/controllers/home.php
		application/controllers/api/product.php
		application/controllers/api/products.php
		application/controllers/shop/catalog.php
		And, in your application/config/hosts.php file:
		$config['home'] = 'your-domain.com';
		$config['api'] = 'api.your-domain.com';
		$config['shop'] = 'shop.your-domain.com';
		
		Now if you navigate to your site in a browser, here's what you should get:
		your-domain.com -> Your site's home page
		api.your-domain.com/product -> The product end-point of your API
		api.your-domain.com/products -> The products end-point of your API
		shop.your-domain.com/catalog -> The catalog page of your shop
*/


class HostNameRouter
{
	protected $hosts;
	/*
		Run as a pre-system hook.
	*/
	public function pre_system()
	{
		$this->prepare_hosts();
		$this->prevent_direct_controller_group_access();
		$this->route_host_to_controller_group();
	}
	/*
		Run as a pre-controller hook.
	*/
	public function pre_controller()
	{
		$this->prepare_hosts();
		$this->restore_uri();
	}
	/*
		Routes a host name to a specific controller group.
	*/
	protected function route_host_to_controller_group()
	{
		/*
			Have to use a super global here, because CI's Hooks class re-instantiates
			this class for every call to it from the config/hooks.php file.
		*/
		$_SERVER['ORIGINAL_REQUEST_URI'] = $_SERVER['REQUEST_URI'];
		/*
			Only route the request if there is a host name route
			for the current host.
		*/
		if ($this->has_controller_group($_SERVER['HTTP_HOST']))
		{
			$group = $this->get_controller_group($_SERVER['HTTP_HOST']);
			$_SERVER['REQUEST_URI'] = '/' . $group . $_SERVER['REQUEST_URI'];
		}
	}
	/*
		Restores the URI-related variables to their originals.
	*/
	protected function restore_uri()
	{
		/*
			Have to do it this way because the $CI object is not available yet.
		*/
		$this->uri =& load_class('URI', 'core');
		$_SERVER['REQUEST_URI'] = $_SERVER['ORIGINAL_REQUEST_URI'];
		$this->uri->uri_string = ltrim($_SERVER['REQUEST_URI'], '/');
		// Remove the query string, if there is one.
		if (strpos($this->uri->uri_string, '?') !== false)
			list ($this->uri->uri_string, ) = explode('?', $this->uri->uri_string);
		$this->uri->segments = array();
		if ($this->uri->uri_string !== '')
			foreach (explode('/', $this->uri->uri_string) as $i => $segment)
				$this->uri->segments[$i + 1] = $segment;
	}
	/*
		Returns TRUE/FALSE depending upon if the given host has a controller group.
	*/
	protected function has_controller_group($host)
	{
		$group = $this->get_controller_group($host);
		
		$controller_subdir = APPPATH . 'controllers/' . $group;
		return 	$group !== null &&
				file_exists($controller_subdir) &&
				is_dir($controller_subdir);
	}
	/*
		Returns the host's controller group.
	*/
	protected function get_controller_group($host)
	{
		$host_to_group = array_flip($this->hosts);
		return isset($host_to_group[$host]) ? $host_to_group[$host] : null;
	}
	/*
		Prevents direct URI access of controller groups.
	*/
	protected function prevent_direct_controller_group_access()
	{
		if (!($group = $this->uri_segment(1)))
			return;
		if ($this->group_has_host($group))
		{
			$protocol = $this->request_protocol();
			$host = $this->get_host_by_group($group);
			$uri = substr($_SERVER['REQUEST_URI'], strlen('/' . $group));
			header('Location: ' . $protocol . '://' . $host . $uri, true, 301);
			exit;
		}
	}
	/*
		Returns TRUE/FALSE depending upon if the given group has a host.
	*/
	protected function group_has_host($group)
	{
		return $this->get_host_by_group($group) !== null;
	}
	/*
		Returns group's host.
	*/
	protected function get_host_by_group($group)
	{
		return isset($this->hosts[$group]) ? $this->hosts[$group] : null;
	}
	/*
		Returns the URI segment specified by $n
	*/
	protected function uri_segment($n)
	{
		$uri = ltrim($_SERVER['REQUEST_URI'], '/');
		$segments = explode('/', $uri);
		return isset($segments[$n - 1]) ? $segments[$n - 1] : null;
	}
	/*
		Returns 'https' if that was the protocol used by the current request.
		Returns 'http' otherwise.
	*/
	protected function request_protocol()
	{
		return isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) === 'on' ? 'https' : 'http';
	}
	/*
		Prepares the hosts array.
	*/
	protected function prepare_hosts()
	{
		$this->config =& load_class('Config', 'core');
		$this->config->load('hosts', true);
		$this->hosts = $this->config->item('hosts');
	}
}
/* End of file HostNameRouter.php */
/* Location: ./application/hooks/HostNameRouter.php */

Next I went and configured the hooks.php file to know about the new router file.

You tell the hooks file about the new router file, to let the system know what to do before the webapp starts up and before a controller does it’s job to.

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');
/*
| -------------------------------------------------------------------------
| Hooks
| -------------------------------------------------------------------------
| This file lets you define "hooks" to extend CI without hacking the core
| files.  Please see the user guide for info:
|
|	http://codeigniter.com/user_guide/general/hooks.html
|
*/
$hook['pre_system'][] = array(
	'class'    => 'HostNameRouter',
	'function' => 'pre_system',
	'filename' => 'HostNameRouter.php',
	'filepath' => 'hooks',
	'params'   => array()
);
$hook['pre_controller'][] = array(
	'class'    => 'HostNameRouter',
	'function' => 'pre_controller',
	'filename' => 'HostNameRouter.php',
	'filepath' => 'hooks',
	'params'   => array()
);

/* End of file hooks.php */
/* Location: ./application/config/hooks.php */

The last bit of this configuration section it to edit the config.php file itself.

You need to make sure the following lines of code / values are set. This tell the Codeigniter framework to use the hooks you set up in the previous step. And the default prefix for the base classes you created. In this case ADMIN_

$config['enable_hooks'] = TRUE;

$config['subclass_prefix'] = 'ADMIN_';

Next you either need to create or edit the hosts.php file.

This tells the Codeigniter framework about all the sites you are hoping to run in this one solution.

The content of my hosts file was:

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');
$config = array();
$config[''] = 'admin.xxx';
$config['my'] = 'my.xx';
/*
	Define the SITE constant.
*/

foreach ($config as $site => $host)
if ($_SERVER['HTTP_HOST'] === $host)
{
    define('SITE', $site);
    break;
}

As you can see, the default host was set to the admin area and I had a specific key for the customer portal.

If the host name of the server matched a host name set in the array it would define a global SITE constant that will be used by the login_redirect controller later.

Last, and not least. Tell the routes.php what to do on first load and if it cannot find a page.


$route['default_controller'] = "login_redirect";
$route['404_override'] = 'admin_404';

I will show you how to implement the login_redirect section later in the post.

I’m a Model, You Know What I Mean

The models folder was my next port of call. This is where the main code was that I didn’t want to duplicate.

This is how I structured the models folder.

Nothing really crazy in there. The only thing worth mentioning is I created two folders. One called ‘admin’ for our main CMS. One called ‘my’ which was for the customer portal subdomain.

Each model related to a different database table, used to authenticate a user.

The rest of the models I didn’t need to touch that much. I had to add a few new functions to load the relevant data when a customer logged into the portal. Other than that, there was very little to do in the models section.

Take Back Control(ler)

Now, this is where more of the grunt work was done. As each site needed to work and look differently

Here is how I structured the controllers folder.

The admin CMS site and customer portal having separate folders that contained all the controllers that particular site needed.

There wasn’t any shared code or re-use of certain controllers. This section was kept separate on purpose to keep a separation of concerns just in case.

I touched the login_redirect.php in the configuration section. This is how the file looked

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Login_redirect extends CI_Controller {

    function __construct()
    {
        parent::__construct();
    }

    public function index()
    {
        $site = SITE; 
        if($site == 'my')
        {           
            redirect("my/mylogin");
        }                     
        redirect("admin/login");
    }
}

If you look back to the hosts.php file we constructed. This file determines which site we are on. Depending on which site we are on, a different login screen is displayed.

One thing to note about these controllers is you have to make sure you inherit from the main controller. In this case the ADMIN_controller.php

Here is an example of the home.php controller for the customer portal.

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Home extends ADMIN_Controller {

    function __construct()
    {
        parent::__construct();
        $this->load->model('entry_model');
        $this->load->model('client_contact_model');
    }

    public function index()
    {
        $places = $this->entry_model->get_total_for_client($this->session->userdata('client_id'));
        $contacts = $this->client_contact_model->get_total_for_client($this->session->userdata('client_id'));
        $data['contacts'] = $contacts;
        $data['title'] = 'Home';
        $data['main_content'] = 'my/home/index';
        $data['places'] = $places;
        $data['contacts'] = $contacts;
        $this->load->view('my/includes/template', $data);
    }

}

You will notice that the ‘Home extends ADMIN_Controller’ line. If this isn’t set you may hit a lot of issues with redirects and authenticating users.

View To A Kill

As with the controllers. When creating views to render html to a user. Each site had it’s own folder, ‘admin’ and ‘my’

Again I did this for separation of concerns. The admin CMS was pretty basic and the goal of the customer portal was to be more easy on the eye for our clients.

Each section has it’s own ‘includes’ folder which rendered the header, footer and main content in the form of a template file.

This is what the template.php file looked like for the customer portal.

<?php $this->load->view('my/includes/header'); ?>
<?php $this->load->view($main_content); ?>
<?php $this->load->view('my/includes/footer'); ?>

To render a page using the correct template. All I needed to do in each controller was to call the following lines of code

data['title'] = 'Home';
$data['main_content'] = 'my/home/index';
$this->load->view('my/includes/template', $data);

This would then display the correct html to the logged in user.

Thumbs Up or Thumbs Down

There you have it. I was able to quickly re-use a lot of code I had already written to get a new customer portal up and running.

The only thing I need to focus on was getting the correct information and displaying it to the user in a prettier way.

If you would like to know more about the projects that I have used Codeigniter on. Or have any questions please get in touch. Cheers

Leave a Comment