Over the past couple of years I have been using the framework CodeIgniter to develop websites and web applications in PHP. When I start a new project I start from my own modified base CodeIgniter installation and follow a few rules. My code examples are CodeIgniter specific but the principles can readily be applied to other web frameworks and languages.
1. Create some generic CRUD models
Create, Update, Delete. In my definition I actually define delete as Deactivate. I setup the field active as a boolean, with a default of ’1′. When I ‘delete’ a item I in fact just update this to ’0′. My select models all contain the statement ‘WHERE active = 1′ so that ‘deleted’ items never appear to the end user. The advantage of this is that I can ‘restore’ items instantly, without having to dig into my automated SQL dumps.
The databases’ I have been working with recently have been small, and so keeping the extra database rows has not caused storage or performance issue. I have provided both delete and deactivate models below.
I used to use an auto incrementing field named ‘id’ for table primary keys, but have recently moved to meaningful id names such as restaurant_id. When you are looping through various objects or arrays in your views it is often helpful to create meaningful id’s to avoid conflicts, and confusions. I have therefore made it possible to pass a $id_fieldname and $id_value to my generic model functions.
<?php
class Crud_model extends Model {
function __construct()
{
parent::Model();
}
/*
| -------------------------------------------------------------------------
| Generic functions
| -------------------------------------------------------------------------
*/
function GerericUpdate($table, $id_fieldname, $id_value, $data)
{
$this->db->where($primary_key, $primary_value);
$this->db->update($table, $data);
if ($this->db->affected_rows() == '1')
{
return TRUE;
}
return FALSE;
}
function GerericInsert($table, $form_data)
{
$this->db->insert($table, $form_data);
if ($this->db->affected_rows() == '1')
{
return TRUE;
}
return FALSE;
}
function GerericDeactivate($table, $id_fieldname, $id_value)
{
$this->db->where($id_fieldname, $id_value);
$this->db->update($table, array('active' => '0'));
if ($this->db->affected_rows() == '1')
{
return TRUE;
}
return FALSE;
}
function GerericDelete($table, $id_fieldname, $id_value)
{
$this->db->delete($table, array($id_fieldname => $id_value));
if ($this->db->affected_rows() == '1')
{
return TRUE;
}
return FALSE;
}
}
?>
2. Export the database schema and put into under version control.
You should be able to ‘build’ a running app from the files in version control, and to do this you need to have the latest copy of the database schema. If a schema change has been made, the schema file should be updated and the commit marked clearly so that other developers know that they need to update their database. A good example of this is when I moved from the field name id to a meaningful word such as restaurant_id as described above. My commit for this change would look as follows:
revno: 38 committer: Oliver Rattue - orattue@example.com branch nick: trunk timestamp: Fri 2009-09-18 10:13:49 +0100 message: *** SCHEMA CHANGE *** now using meaningful ids i.e restaurant_id instead of id modified: system/application/models/crud_model.php schema/example.com-apps-schema.sql
3. Use a template system
Every web application should have some kind of template system. There are many fully blown template systems around, but I have settled for a little home brewed function, which allows me to load different headers and footers depending on the controller, and pass a few other variables such as a page title and a css body class. It can accept either a single view file or an array or view files and it makes the $data variable available to all views.
helpers/template.php
/**
* function BuildPage()
*
* a simple template system
* @access private
* @param $page - string
* @param $data - string
* @param $title - string
* @param $class - string
* @return string
*/
function BuildPage($path, $data = null, $title = null,$class = 'default')
{
if ($title == NULL)
{
$title = 'Restaurant Guide.';
}
$data['path'] = $path;
$data['class'] = $class;
$data['title'] = $title;
list($controller, $function, $page_view) = explode("/", substr($path, 0));
$data['controller'] = $controller;
$data['function'] = $function;
$data['page_view'] = $page_view;
$CI =& get_instance();
$CI->load->vars($data);
switch( $controller ){
case 'admin':
$CI->load->view('templates/admin/base');
break;
case 'welcome':
$CI->load->view('templates/welcome/base');
break;
case 'reviews':
case 'questionnaire':
$CI->load->view('templates/reviews/base');
break;
default:
$CI->load->view('templates/default/base');
break;
}
}
templates/reviews/base.php
<?php
$this->load->view('templates/reviews/header_public');
if (is_array($path))
{
foreach($path as $value)
{
$this->load->view("$path");
}
}
else
{
$this->load->view("$path");
}
$this->load->view('templates/reviews/footer_public');
?>
From my controller I then call this function like this:
BuildPage('reviews/search/search', $this->data, 'Your search results', 'search');
4. Setup a constants file
I always add a few generic extra constants to the default CodeIgniter constants file. On top of this I will usually add a few more application specific constants aswell. Note I use {_SERVER['HTTP_HOST']} for email a couple of various the email address domains, so that I can quickly tell which server it was sent from e.g. system@mywebapp.com.macbook – I know this came from my local dev environment.
// Debug mode - used to enable or disable features such as session stage check redirects, and the CodeIgniter profiler. Useful for development and testing
define('DEBUG_MODE', TRUE);
// Administrator notifications - for clients
define('CLIENT_EMAIL', 'ollierattue@example.com');
// Developer notifications
define('DEVELOPER_EMAIL', 'ollierattue@example.com');
// System from email address
define('SYSTEM_EMAIL',"system@${_SERVER['HTTP_HOST']}");
// Autoresponder settings for end users
define('AUTORESPONDERS_FROMEMAIL', "info@${_SERVER['HTTP_HOST']}");
define('AUTORESPONDERS_FROMNAME', 'My web app');
define('AUTORESPONDERS', 'ON'); // used for debugging and development to switch autoresponder emails ON or OFF
define('APP_NAME', "Webappzer");
define('APP_SLOGAN', "Changing the world as you know it!");
5. Auto email log errors to yourself
Logging errors is a great idea, but once your application goes live, how often do you actually check them? If you do any client work you will quickly have logs files scattered across numerous websites, possibly on numerous servers. The only way to realistically track logged errors is to get them emailed as and when they happen. I found a library extension at http://coding.cglounge.com/2009/05/email-log-messages-library-codeigniter/, and made a few changes to fix a couple of bugs and use my constant file settings (as explained above):
<?php
/**
* MY_Log Class
*
* This library extends the native Log library.
* It adds the function to have the log messages being emailed when they have been outputted to the log file.
*
* @package CodeIgniter
* @subpackage Libraries
* @category Logging
* @author Johan Steen
* @link http://coding.cglounge.com/
*/
class MY_Log extends CI_Log {
/**
* Constructor
*
* @access public
*/
function MY_Log()
{
parent::CI_Log();
}
/**
* Write Log File
*
* Calls the native write_log() method and then sends an email if a log message was generated.
*
* @access public
* @param string the error level
* @param string the error message
* @param bool whether the error is a native PHP error
* @return bool
*/
function write_log($level = 'error', $msg, $php_error = FALSE)
{
$msg = "http://${_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']} - $msg";
$result = parent::write_log($level, $msg, $php_error);
if ($result == TRUE && strtoupper($level) == 'ERROR') {
$message = "An error occurred: \n\n";
$message .= $level.' - '.date($this->_date_fmt). ' --> '.$msg."\n";
$to = DEVELOPER_EMAIL;
$subject = 'An error has occured on apps';
$headers = 'From: '.AUTORESPONDERS_FROMNAME.' system <'.SYSTEM_EMAIL.'>' . "\r\n";
$headers .= 'Content-type: text/plain; charset=utf-8\r\n';
mail($to, $subject, $message, $headers);
}
return $result;
}
}
?>
6. Setup a language file for your apps copy
Every web application has a personality and your success and your copy plays a large part in portraying it. I investigated the benefits of this in my post Improving your web apps personality with a language file.
I wrote a little wrapper function to enable me to use CodeIgniter’s default language file, and allow variable substitution.
langauge/english/flash_messages_lang.php
<?php /* Amnesia */ $lang['flash_error_reset_code'] = "There was a problem resetting your password. Please try again later."; $lang['flash_error_reset_code_incorrect_or_expired'] = "Your reset code is incorrect or has expired (over 15 days old)"; $lang['flash_error_password_match'] = "Please make sure your passwords match."; $lang['flash_error_incorrect_email'] = "Please make sure you entered your correct email address."; $lang['flash_error_reset_password_general_problem'] = "There was a problem resetting your password. Please try again later."; $lang['flash_success_password_reset'] = "Password reset successful. Please login using your new password."; /* Login */ $lang['flash_success_logged_in'] = "Hello %s"; $lang['flash_error_valid_email_password'] = "Please enter a valid email address and password to login."; /* End of file flash_messages_langg.php */ /* Location: ./system/application/language/english/flash_messages_lang.php */
Helper function
## Returns a language file using sprintf to substitute variable where necessary
function language($language_key = NULL, $variable = NULL)
{
$CI =& get_instance();
$CI->lang->load('flash_messages','English');
if (!empty($variable))
{
return sprintf($CI->lang->line($language_key),$variable);
}
return $CI->lang->line($language_key);
}
View:
echo language('flash_error_reset_code');
Passing a variable to be substituted:
echo language('flash_success_logged_in', $user->first_name);
7. Create defaults of constants and database files and ignore the standard ones from version control for server deployment
For security reasons I run different database settings on my dev, local and live boxes. I also run different constant settings so that I can switch into a DEBUG mode, or change the autoresponder emails during development. When testing locally I want to maintain these settings, but I don’t want to commit them to live. To ignore them using bzr I do:
cp system/applications/config/database.php system/applications/config/database.php-default cp system/applications/config/constants.php system/applications/config/constants.php-default bzr ignore system/applications/config/constants.php bzr ignore system/applications/config/database.php
When doing this you must get into the habbit of adding any new constants to the default file and adding a commit flag, in the same way as a schema change, to tell other developers in the team that they need to update their config files.
8. Create some standardised flashdata error, success divs
I use flashdata to pass error and success messages between pages. I always use an error or success div e.g. <div class=”error”>. So rather than including the div in the flashdata session value itself, I modified the session library to allow me to pass an additional $type parameter, which would output the correct div styling.
system/applications/libraries/MY_Session.php
<?
class MY_Session extends CI_Session {
// ------------------------------------------------------------------------
# Modified to print out <div> with success or error css class
/**
* Add or change flashdata, only available
* until the next request
*
* @access public
* @param mixed
* @param string
* @return void
*/
function set_flashdata($newdata = array(), $newval = '', $type = NULL)
{
if (is_string($newdata))
{
$newdata = array($newdata => $newval);
}
if (count($newdata) > 0)
{
foreach ($newdata as $key => $val)
{
$flashdata_key = $this->flashdata_key.':new:'.$key;
if ($type == 'success')
{
$val = '<div class="flash_success">'.$val.'</div>';
}
if ($type == 'error')
{
$val = '<div class="flash_error">'.$val.'</div>';
}
$this->set_userdata($flashdata_key, $val);
}
}
}
}
I then set my flashdata, which runs from a language files, as follows:
// Success
$this->session->set_flashdata( 'flash', language('flash_success_logged_in', $user->first_name), 'success');
// Error - with language file variable substitution to pass the user's first name
$this->session->set_flashdata( 'flash', language('flash_error_valid_email_password'), 'error');
Included in my view (usually in my header) I check for flashdata as follows:
<?php
// displays message set by previous page
if ( $this->session->flashdata('flash') != '') {
echo $this->session->flashdata('flash');
}
?>
My css styling:
/* =Flash data styling
--------------------------------------------------------*/
div.flash_success {
background:#E2F9E3 url('/images/green_tick.gif') no-repeat 0px 3px;
border: 1px solid #99CC99;
padding:2px 0px 2px 33px;
margin: 10px 0 18px 0;
font-size: 1.3em;
}
div.flash_error {
background: #FBE6F2 url('/images/error.png') no-repeat 5px 5px;
border: 1px solid #F83434;
padding:2px 10px 2px 33px;
margin: 10px 0 18px 0;
font-size: 1.3em;
}
Revised – thanks Brandon for pointing our these images were not downloadable
The finished product:


9. Use a logical file, variable, function and database naming convention.
Database:
- table names are normally plurals e.g. restaurants as oppose to restaurant.
- Meaningful id fieldnames i.e. restaurant_id instead of just id
View file:
- I like to label them controller/function/viewname.php to make it completely transparent where the file is e.g. restaurant/add/form.php
- I also like to label files as ajax where applicable
Model functions and variable names could easily be structured differently to below. What is important is to keep your naming convention consistent throught out your whole application.
Model functions:
- I have taken to using Camel Case e.g. SelectAllRestaurants(). There isn’t really much logic here but I think this is very readable.
Variables:
- I always try to use meaningful words, and stick to lowercase with underscores e.g. $restaurant_details
- I usually return database results as an object but occasionally return an array. When doing this I will append array to the variable name e.g. $data['films_array'] = $this->select_model->GetFilmsForDropdown();
Conclusion:
Everyone has their own habits and ‘best practices’ when setting up a new project. Sometimes these are actually integrated into the framework themselves, such as Ruby on Rails. I like CodeIgniter for the fact that it doesn’t impose any rules onto the developer. Over time we settle on what works for us. The list above saves me time, and ensures a consistency across my companies codebase making it easy for a freelancer, or new team member to come onboard and get going on one of our projects.

