Wednesday, June 10, 2009

Integrating CodeIgniter with Legacy PHP applications (Part II)

This is Part II of a blog covering how I've integrated CI into a legacy PHP application. Have a read of Part I for an introduction.

Firstly, a bit of background about the legacy application. To protect the client, and myself, I'm going to call it Legacy.

In SVN, the code has the following general structure:

trunk/src/legacy. This directory is where all the source code lives. There are about 30 directories under it, and a bucket load more of programs, either in those directories, or at the root level. Each sub directory has tried to split out a logical part of the system, but it hasnt worked so well. There are two items of note. One is config.inc.php, which contains the global configuration information. Its mostly about database connection information, and a few other tidbits of information relevant to the application. The other is that when deployed, the application will be accessed as https://legacy.host.com/legacy.
trunk/config. This directory contains different versions of the global configuration file. There's one for production, and there's one for development.
trunk/db. This directory contains database information, like what the latest schema looks like.

Some other things worthy of note.

The PHP ini setting register_globals is forced on. The code base was already quite large when I got it, and it was going to be too risky to go around and find all those globals and change them to $_GET or $_POST accesses. One might argue its just as risky to leave them there, but given that the application is intranet based, I figured the risk was low.

The application also runs under PHP5. Probably one of the few things that works in our favour, with regard to support for OOP.

So how did I integrate CI into the mess?

First thing was to download CI and unzip it. For reference, I used CodeIgniter 1.7.1. I put the contents of the system directory under the trunk/src/legacy directory. Luckily, I didnt have any directory name clashes.

I then set up trunk/src/index.php to rename the $system_folder to "legacy", then edited trunk/src/legacy/application/config/config.php and database.php for the base_url and database connection information. I also configured sessions to be stored in the database, as per CI Session instructions, and added a trunk/src/.htaccess file so I could skip having index.php in the URL.

Having

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?/$1 [L]

in the .htaccess file would allow me to run the legacy application, and any new CI controllers side by side. Make sure you change the index_page in config.php to "".

Just to make sure everything was ok, I made sure I could log into the application and perform a few basic tasks. Yippee! All working.

The next step was to follow some basic guide lines from the article Calling CI models from outside script to create some stubs to allow integration with the application.

First file was a trunk/src/ci_index.php, to match index.php in CI, and config_constants.php from the article.


<?php/*
* Copy of index.php for use with legacy code
*/
/*
|---------------------------------------------------------------
| PHP ERROR REPORTING LEVEL
|---------------------------------------------------------------
|
| By default CI runs with error reporting set to ALL.  For security
| reasons you are encouraged to change this when your site goes live.
| For more info visit:  http://www.php.net/error_reporting
|
*/
error_reporting(E_ALL && ~E_NOTICE);

/*
|---------------------------------------------------------------
| SYSTEM FOLDER NAME
|---------------------------------------------------------------
|
| This variable must contain the name of your "system" folder.
| Include the path if the folder is not in the same  directory
| as this file.
|
| NO TRAILING SLASH!
|
*/
  $system_folder = "legacy";

/*
|---------------------------------------------------------------
| APPLICATION FOLDER NAME
|---------------------------------------------------------------
|
| If you want this front controller to use a different "application"
| folder then the default one you can set its name here. The folder
| can also be renamed or relocated anywhere on your server.
| For more info please see the user guide:
| http://codeigniter.com/user_guide/general/managing_apps.html
|
|
| NO TRAILING SLASH!
|
*/ 
$application_folder = "application";

/*
|===============================================================
| END OF USER CONFIGURABLE SETTINGS
|===============================================================
*/


/*
|---------------------------------------------------------------
| SET THE SERVER PATH
|---------------------------------------------------------------
|
| Let's attempt to determine the full-server path to the "system"
| folder in order to reduce the possibility of path problems.
| Note: We only attempt this if the user hasn't specified a
| full server path.
|
*/
if (strpos($system_folder, '/') === FALSE)
{
if (function_exists('realpath') AND @realpath(dirname(__FILE__)) !== FALSE)
{
$system_folder = realpath(dirname(__FILE__)).'/'.$system_folder;
}
}
else
{
// Swap directory separators to Unix style for consistency
$system_folder = str_replace("\\", "/", $system_folder);
}

/*
|---------------------------------------------------------------
| DEFINE APPLICATION CONSTANTS
|---------------------------------------------------------------
|
| EXT  - The file extension.  Typically ".php"
| FCPATH - The full server path to THIS file
| SELF  - The name of THIS file (typically "index.php")
| BASEPATH - The full server path to the "system" folder
| APPPATH - The full server path to the "application" folder
|
*/
define('EXT', '.'.pathinfo(__FILE__, PATHINFO_EXTENSION));
define('FCPATH', __FILE__);
define('SELF', pathinfo(__FILE__, PATHINFO_BASENAME));
define('BASEPATH', $system_folder.'/');

if (is_dir($application_folder))
{
define('APPPATH', $application_folder.'/');
}
else
{
if ($application_folder == '')
{
$application_folder = 'application';
}

define('APPPATH', BASEPATH.$application_folder.'/');
}


/* End of file index.php */
/* Location: ./index.php */
?>


The second file was a trunk/src/ci_open.php to match a part of codeigniter/CodeIgniter.php in CI, and ci_model_remote_open.php from the article.

<?php
/*
* Copy of system/codeigniter/CodeIgniter.php
* Beginning processing and load the base controller
*/

require_once('ci_index.php');

/*
* ------------------------------------------------------
*  Load the global functions
* ------------------------------------------------------
*/
require(BASEPATH.'codeigniter/Common'.EXT);

/*
* ------------------------------------------------------
*  Load the compatibility override functions
* ------------------------------------------------------
*/
require(BASEPATH.'codeigniter/Compat'.EXT);

/*
* ------------------------------------------------------
*  Load the framework constants
* ------------------------------------------------------
*/
require(APPPATH.'config/constants'.EXT);

/*
* ------------------------------------------------------
*  Define a custom error handler so we can log PHP errors
* ------------------------------------------------------
*/
set_error_handler('_exception_handler');
set_magic_quotes_runtime(0); // Kill magic quotes


/* $CFG =& load_class('Config'); */

if (floor(phpversion()) < 5)
{
load_class('Loader', FALSE);
require(BASEPATH.'codeigniter/Base4'.EXT);
}
else
{
require(BASEPATH.'codeigniter/Base5'.EXT);
}

// Load the base controller class
$CI = load_class('Controller');
?>


The third file was a trunk/src/ci_close.php to match the last part of codeigniter/CodeIgniter.php in CI, and ci_model_remote_close.php from the article.

<?php
/*
* Copy of system/codeigniter/CodeIgniter.php
* End processing
*
*/
/*
* ------------------------------------------------------
*  Close the DB connection if one exists
* ------------------------------------------------------
*/
if (class_exists('CI_DB') AND isset($CI->db))
{
$CI->db->close();
}

?>



My first task was to convert sessions to CI Sessions. The first program to change would be the login screen. Currently functionality was to clear any sessions that existed when visiting this screen. I replaced my ugly session clearing code in trunk/src/legacy/login/login.php with
require_once('../../ci_open.php');
$CI->load->library('session');
$CI->session->sess_destroy();
require_once('../../ci_close.php');

The next step was to change the page that handled the login post to use CI sessions once the user had been authenticated.

As I'd mentioned previously, the legacy application made use of register_globals, which is nasty. The login page used them as well, so I made a small change to load this from the post, but left the rest as close to the same as I could. I ended up with this...

<?php
ini_set('register_globals', 'on');
include_once('../config.inc.php');
require_once('../../ci_open.php');
$CI->load->library('session');

$userxname = $CI->input->post('userxname');
$passxword = $CI->input->post('passxword');
/* do user authentication with username and password
* if authentication is successful:
*/
$userdata = array('logged' => 'yes', 'User' => $row['userid'], ... );
$CI->session->set_userdata($userdata);
/* near the bottom */
require_once('../../ci_close.php');
?>


Now, luckily, we do all our session checking in one place (trunk/src/legacy/login/logincheck.php). And that modification went something like:

<?php
require_once('../ci_open.php');
$CI->load->library('session');
$found = $CI->session->userdata('logged');
if ($found != "yes") { /* redirect to login page */}
?>


Now, for this one, I've left the ci_close.php out, since it closed the database connection and prevented the legacy application from any more database accesses.

That all nearly worked.

After a successful login, the page would redirect to /legacy/legacy_main_screen.php?l=y. legacy_main_screen.php would use the quest string to work out what page to run, and where to position the menu structure. However, CI was getting in the way, and tried to remove the query string. So I had to enable the query string in config.php.

The next problem was that the Router would get initialised during the initialisation of the Controller, and would try to intepret the URL to work out what controller to forward the user to. I didnt want to forward control to a controller, I just wanted the controller to start and let processing continue.

So I subclassed the Controller with my own one that specifically did not initialise the Router.

To do this, I first made a note of the subclass_prefix in config.php. The value is "MY_".

Next, I made a copy of trunk/src/legacy/libraries/Controller.php and put it into trunk/src/legacy/application/libraries/my_controller.php, and changed a few things. The first was the class name and constructor name (MY_Controller). I made it extend the Controller, rather than CI_Base. The contents of the constructor remained the same, calling parent::CI_Base() and $this->_ci_initialize() rather than parent::Controller. And most important, I took a copy of _ci_initialize() and commented out the 'router' entry from the $classes array.

Awesome! I could now login into the application and see the first screen of the application. I thought I was home free. Not quite.

Stay tuned for Part III.

No comments: