PHP configuration patterns

The right way to build configurable PHP applications

This article demonstrates several ways to create configurable PHP applications. It also discusses the ideal configuration points in an application, and finding the balance point between an application that is too configurable and one that is too closed.

Jack D. Herrington (jherr@pobox.com), Senior Software Engineer, Leverage Software Inc.

Jack D. Herrington is a senior software engineer with more than 20 years of experience. He's the author of Code Generation in Action, Podcasting Hacks and PHP Hacks. He has also written more than 30 articles.



29 August 2006

If you plan to ship your PHP application for use by another person or company, you need to make it configurable. At the very least, you want to allow the user to set the database login and password in a way that is secure so that material isn't externally visible.

This article shows several techniques for storing configuration settings and editing them. In addition, it provides some guidance for what elements to make configurable and how to avoid the traps of building too much or too little configurability.

Configuration with INI files

PHP comes with support for configuration files built in. This is through the initialization file (INI) mechanism that you see with the php.ini file, which defines constants such as the timeout on database connections or how sessions are stored. You can, if you wish, allow for custom configurations for your applications to go in this php.ini file. To demonstrate, I add this line to the php.ini file:

myapptempdir=foo

Then I write a small PHP script to read the configuration item, shown in Listing 1.

Listing 1. ini1.php
<?php
function get_template_directory()
{
  $v = get_cfg_var( "myapptempdir" );
  return ( $v == null ) ? "tempdir" : $v;
}

echo( get_template_directory()."\n" );
?>

When I run this on the command line, I get this result:

% php ini1.php
foo
%

That's pretty good. But why can't I use the standard INI functions to get the value of the myapptempdir item? Well, I played around with that and found that under most circumstances, custom configuration items were not accessible using those methods. However, they were visible to the get_cfg_var function.

To make this method a little easier, I wrap the access to the variable in a second function that takes a configuration key name and a default value, as shown below.

Listing 2. ini2.php
function get_ini_value( $n, $dv )
{
  $c = get_cfg_var( $n );
  return ( $c == null ) ? $dv : $c;
}

function get_template_directory()
{
  return get_ini_value( "myapptempdir", "tempdir" );
}

This is a nice generalization of accessing the INI file so that if I decide to use a different mechanism or store the INI in some other position, I don't have to go around changing lots of functions.

I don't recommend using the INI files for configuration items for your applications for a couple of reasons. First, it's easy to read INI files, but it's almost impossible to write INI files securely. So it's only good for read-only items. Second, the php.ini file is shared between all of the applications on the server, so I don't think application-specific items should go in it.

What should you know about the INI files? The most important thing is how to reset the include path to add items, as shown below.

Listing 3. ini3.php
<?php
echo( ini_get("include_path")."\n" );
ini_set("include_path",
        ini_get("include_path").":./mylib" );
echo( ini_get("include_path")."\n" );
?>

In this example, I add my local mylib directory to the include path so I can require PHP files from that directory without adding the path to the require statement.

Configuration in PHP

A common alternative to storing configuration items in an INI file is to use a simple PHP script to hold the data. An example is shown below.

Listing 4. config.php
<?php
# Specify the location of the temporary directory
#
$TEMPLATE_DIRECTORY = "tempdir";
?>

The code that uses this constant is shown below.

Listing 5. php.php
<?php
require_once 'config.php';

function get_template_directory()
{
  global $TEMPLATE_DIRECTORY;
  return $TEMPLATE_DIRECTORY;
}

echo( get_template_directory()."\n" );
?>

The code first requires in the configuration file (config.php), then uses the constants directly.

There are many advantages to using this technique. First, if someone just browses to config.php, the page is simply blank. So you can put config.php in the same file as the root of the Web application. Second, it's editable in any editor, and with some editors, you even get syntax coloring and checking.

The disadvantage is that this is a read-only technique like the INI file. It's trivial to get the data out of this file, but it's hard -- even impossible in some circumstances -- to adjust the data in the PHP file.

The alternatives that follow show how to write configuration systems that are both read and write in nature.

Text files

Both of the previous examples were good for read-only configuration items, but how about for reading and writing configuration parameters? To start, take the text configuration file in Listing 6.

Listing 6. config.txt
# My application's configuration file
Title=My App
TemplateDirectory=tempdir

This is the same file format as an INI file, but I write my own accessories for it. To do that, I create my own Configuration class, as shown below.

Listing 7. text1.php
<?php
class Configuration
{
  private $configFile = 'config.txt';

  private $items = array();

  function __construct() { $this->parse(); }

  function __get($id) { return $this->items[ $id ]; }

  function parse()
  {
    $fh = fopen( $this->configFile, 'r' );
    while( $l = fgets( $fh ) )
    {
      if ( preg_match( '/^#/', $l ) == false )
      {
        preg_match( '/^(.*?)=(.*?)$/', $l, $found );
        $this->items[ $found[1] ] = $found[2];
      }
    }
    fclose( $fh );
  }
}

$c = new Configuration();

echo( $c->TemplateDirectory."\n" );
?>

The first thing the code does is to create a Configuration object. That constructor then reads the config.txt file and sets the local $items variable with the parsed contents of the file.

The script then looks for the TemplateDirectory, which isn't defined on the object natively. So, the magic __get method is called with the $id set to 'TemplateDirectory', and that __get method returns the value from the $items array for that key.

This __get method is specific to PHP V5, so this script must be run with PHP V5. In fact, PHP V5 is required for all the scripts in this article.

When I run this script on the command line, I see the following result:

% php text1.php 
tempdir
%

As expected, the object read in the config.txt file, then spit out the correct value for the TemplateDirectory item.

But what about setting a configuration value? That comes with a new method on the class and some new test code, as shown below.

Listing 8. text2.php
<?php
class Configuration
{
  ...

  function __get($id) { return $this->items[ $id ]; }

 function __set($id,$v) { $this->items[ $id ] = $v; }
  function parse() { ... }
}
$c = new Configuration();
echo( $c->TemplateDirectory."\n" );
$c->TemplateDirectory = 'foobar';
echo( $c->TemplateDirectory."\n" );
?>

Now I have a __set function, which is the cousin of the __get statement. Instead of getting the value for a member variable, it's called when a member value should be set. The test code at the bottom then sets the value and prints out the new value.

Here's what happens when I run this code on the command line:

% php text2.php 
tempdir
foobar
%

Perfect! But how do I get this stored in the file so the change is permanent? For that, I need to write the file as well as read it. The new function for writing the file is shown below.

Listing 9. text3.php
<?php
class Configuration
{
  ...

  function save()
  {
    $nf = '';
    $fh = fopen( $this->configFile, 'r' );
    while( $l = fgets( $fh ) )
    {
      if ( preg_match( '/^#/', $l ) == false )
      {
        preg_match( '/^(.*?)=(.*?)$/', $l, $found );
        $nf .= $found[1]."=".$this->items[$found[1]]."\n";
      }
      else
      {
        $nf .= $l;
      }
    }
    fclose( $fh );
    copy( $this->configFile, $this->configFile.'.bak' );
    $fh = fopen( $this->configFile, 'w' );
    fwrite( $fh, $nf );
    fclose( $fh );
  }
}

$c = new Configuration();
echo( $c->TemplateDirectory."\n" );
$c->TemplateDirectory = 'foobar';
echo( $c->TemplateDirectory."\n" );
$c->save();
?>

This new save function plays a tricky game with the config.txt file. Instead of just rewriting the file with the updated items, which would remove any comments, I read the file and rewrite the contents on the fly from the $items array. That way, the comments are preserved in the file.

When I run the script on the command line and print the contents of the text configuration file, I see the output shown below.

Listing 10. Save function output
%  php text3.php 
tempdir
foobar
% cat config.txt
# My application's configuration file
Title=My App
TemplateDirectory=foobar
%

The original config.txt file has now been updated with the new value.

XML configuration files

While text files are easy to understand and edit, they aren't as trendy as XML files. Plus, XML files have the advantage of having lots of editors for them that understand tags, special-character escaping, and more. So what does an XML version of the configuration file look like? Listing 11 shows the configuration file as XML.

Listing 11. config.xml
<?xml version="1.0"?>
<config>
  <Title>My App</Title>
  <TemplateDirectory>tempdir</TemplateDirectory>
</config>

Listing 12 shows an updated version of the Configuration class that uses XML to load the configuration settings.

Listing 12. xml1.php
<?php
class Configuration
{
  private $configFile = 'config.xml';

  private $items = array();

  function __construct() { $this->parse(); }

  function __get($id) { return $this->items[ $id ]; }

  function parse()
  {
    $doc = new DOMDocument();
    $doc->load( $this->configFile );

    $cn = $doc->getElementsByTagName( "config" );

    $nodes = $cn->item(0)->getElementsByTagName( "*" );
    foreach( $nodes as $node )
      $this->items[ $node->nodeName ] = $node->nodeValue;
  }
}

$c = new Configuration();
echo( $c->TemplateDirectory."\n" );
?>

It looks like there's another benefit to XML: The code is a lot cleaner and easier than the text version. To save the XML, I need another version of the save function that saves as XML, instead of text.

Listing 13. xml2.php
...
  function save()
  {
    $doc = new DOMDocument();
    $doc->formatOutput = true;

    $r = $doc->createElement( "config" );
    $doc->appendChild( $r );

    foreach( $this->items as $k => $v )
    {
      $kn = $doc->createElement( $k );
      $kn->appendChild( $doc->createTextNode( $v ) );
      $r->appendChild( $kn );
    }

    copy( $this->configFile, $this->configFile.'.bak' );

    $doc->save( $this->configFile );
  }
...

This code creates a new XML Document Object Model (DOM), then adds all of the data in the $items array to it. After that is completed, the XML is saved to a file using the save method.

Going to the database

The final alternative is to use a database to hold the values of configuration elements. That starts with a simple schema to store the configuration data. A simple schema is shown below.

Listing 14. schema.sql
DROP TABLE IF EXISTS settings;
CREATE TABLE settings (
  id MEDIUMINT NOT NULL AUTO_INCREMENT,
  name TEXT,
  value TEXT,
  PRIMARY KEY ( id )
);

This requires some adjustment based on the needs of your application. For example, if you want configuration elements to be stored on a per-user basis, you need to add a user ID as an extra column.

To read and write the data, I write the updated Configuration class shown in Listing 15.

Listing 15. db1.php
<?php
require_once( 'DB.php' );

$dsn = 'mysql://root:password@localhost/config';

$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }

class Configuration
{
  private $configFile = 'config.xml';

  private $items = array();

  function __construct() { $this->parse(); }

  function __get($id) { return $this->items[ $id ]; }

  function __set($id,$v)
  {
    global $db;

    $this->items[ $id ] = $v;

    $sth1 = $db->prepare( 'DELETE FROM settings WHERE name=?' );
    $db->execute( $sth1, $id );
    if (PEAR::isError($db)) { die($db->getMessage()); }

    $sth2 = $db->prepare(
      'INSERT INTO settings ( id, name, value ) VALUES ( 0, ?, ? )' );
    $db->execute( $sth2, array( $id, $v ) );
    if (PEAR::isError($db)) { die($db->getMessage()); }
  }

  function parse()
  {
    global $db;

    $doc = new DOMDocument();
    $doc->load( $this->configFile );

    $cn = $doc->getElementsByTagName( "config" );

    $nodes = $cn->item(0)->getElementsByTagName( "*" );
    foreach( $nodes as $node )
    $this->items[ $node->nodeName ] = $node->nodeValue;

    $res = $db->query( 'SELECT name,value FROM settings' );
    if (PEAR::isError($db)) { die($db->getMessage()); }
    while( $res->fetchInto( $row ) ) {
      $this->items[ $row[0] ] = $row[1];
    }
  }
}

$c = new Configuration();
echo( $c->TemplateDirectory."\n" );
$c->TemplateDirectory = 'new foo';
echo( $c->TemplateDirectory."\n" );
?>

This is actually a hybrid text/database solution. If you look carefully at the parse method, you see that the class first reads the text file for the initial values, then reads the database to update the keys to their latest values. Then when a value is set, the key is removed from the database and a new record is added with the updated value.

It's interesting to note how the Configuration class has gone through so many versions in this article -- reading from a text file, from XML, then from the database -- all the while retaining the same interface. I encourage you to seek the same stability in your interfaces as you develop them. It shouldn't be clear to the client of an object exactly how the work is getting done. What matters is the contract between an object and its client.

What and how to configure

Finding a happy medium between having too many configuration options and having too few can be difficult. Certainly, any database configurations (for example, the database name, database user name, and password) should be configurable. But beyond that, I have some basic recommendations.

At the high level, each feature should have a separate enable/disable option. Those options should be enabled or disabled based on whether the feature is central to the application. For example, in a Web forums application, the moderation feature should be enabled by default. But the e-mail notification should be off by default because it likely requires customization.

User interface (UI) options should all be set in one location. The structure of the interface (for example, the location of the menu, additional menu items, the URL linked to certain elements of the interface, the logos to use, etc.) should all be set in a single location. That said, I strongly recommend against specifying fonts, colors, or style items as configuration items. These should all be set using Cascading Style Sheets (CSS), and the configuration system should specify which CSS file to use. The CSS is an efficient and flexible way of setting fonts, styles, colors, and more. There are many excellent CSS tools out there, and your application should make sensible use of CSS, rather than trying to set its own standards.

Within each feature, I recommend having somewhere between three and 10 configuration options. These should be named in a way that's immediately meaningful. The names of the options in the text file, XML file, or database should relate directly to the title of the interface element if the configuration options can be set through the UI. In addition, the options should all have sensible default values.

In general, these items should be configurable: an e-mail address, what CSS to use, the location of any system resources referenced from files, and the file names of graphic elements.

For graphic elements, you may wish to create a separate type of configuration file called a skin, which includes sets of configuration files, including the location of CSS files, the location of the graphics, and these types of things. Then let the user select between multiple skin files. This makes it easy to make mass changes to the look and feel of the application. It also provides an opportunity for users of your application to exchange skins between product installations. These skin files aren't covered by the article, but the fundamentals you learn here should make supporting skin files a lot easier.

Conclusion

Configurability is an important part of any PHP application and should be a central part of your design from the start. I hope this article has provided you with some options for how to implement your configuration architecture and given some guidance as to what configuration options are allowed.

Resources

Learn

  • More information about XML is available at W3C.org.
  • Visit PHP.net for more information about PHP.
  • A repository of useful modules, including one specific to configuration files, is available at PEAR.php.net.
  • Another text format for configuration files is YAML. It's popular in the Ruby crowd.
  • MySQL is a prominent companion to the PHP programming language.
  • The tutorial series "Learning PHP" takes you from the most basic PHP script to working with databases and streaming from the file system.
  • Visit the developerWorks XML zone to find more resources for XML developers.
  • Visit IBM developerWorks' PHP project resources to learn more about PHP.
  • Stay current with developerWorks technical events and webcasts.
  • Check out upcoming conferences, trade shows, webcasts, and other Events around the world that are of interest to IBM open source developers.
  • Visit the developerWorks Open source zone for extensive how-to information, tools, and project updates to help you develop with open source technologies and use them with IBM's products.
  • To listen to interesting interviews and discussions for software developers, be sure to check out developerWorks podcasts.

Get products and technologies

  • Innovate your next open source development project with IBM trial software, available for download or on DVD.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Open source on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source
ArticleID=154170
ArticleTitle=PHP configuration patterns
publish-date=08292006