Here's the quote:
If you plan to make your PHP application available to other people or companies, you need to make sure the application is configurable. At a minimum, allow users to set database logins and passwords in a secure manner so that the material within them is not made public.
This article demonstrates several techniques for storing configuration settings and editing these settings. In addition, the article also provides guidance on which elements need to be made configurable and how to avoid falling into the dilemma of over- or under-configuration.
Configuration using INI files
PHP has built-in support for configuration files. This is achieved through an initialization file (INI) mechanism such as the php.ini file, where constants such as database connection timeouts or how sessions are stored are defined. If you wish, you can customize the configuration for your application in this php.ini file. To illustrate, I added the following lines of code to the php.ini file.
myapptempdir=foo
Then, I wrote a small PHP script to read this configuration item, as 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 running this code on the command line, I get the following result:
% php ini1.php
foo
%
marvelous. But why can't we use the standard INI function to get the value of the myapptempdir configuration item? I did some research and found that in most cases, custom configuration items cannot be obtained using these methods. However, it is accessible using the get_cfg_var function.
To make this approach simpler, encapsulate access to the variable in a second function that takes the configuration key name and a default value as parameters, 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 good overview of how to access the INI file, so if you want to use a different mechanism or store the INI file somewhere else, you don't need to go to the trouble of changing a lot of functions.
I don't recommend using INI files for application configuration, for two reasons. First, while this makes it easier to read the INI file, it makes it almost impossible to write the INI file safely. So this is only suitable for read-only configuration items. Second, the php.ini file is shared across all applications on the server, so I don't think application-specific configuration items should be written in that file.
What do you need to know about INI files? The most important thing is how to reset the include path to add configuration 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 added 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 entries in an INI file is to use a simple PHP script to persist the data. Below is an example.
Listing 4. config.php
<?php
# Specify the location of the temporary directory
#
$TEMPLATE_DIRECTORY = "tempdir";
?>
The code using 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 includes the configuration file (config.php), and then the constants can be used directly.
There are many advantages to using this technology. First, if someone just browses the config.php file, the page is blank. So you can put config.php in the same file as the root of your web application. Second, it can be edited in any editor, and some editors even have syntax coloring and syntax checking functions.
The disadvantage of this technology is that it is a read-only technology like INI files. Extracting the data from this file is a piece of cake, but adjusting the data in the PHP file is difficult, and in some cases even impossible.
The following alternative shows how to write a configuration system that is both readable and writable in nature.
The previous two examples oftext files
are fine for read-only configuration entries, but what about configuration parameters that are both read and write? First, take a look at 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 wrote my own helpers. To do this, I created 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 code first creates a Configuration object. The constructor next reads config.txt and sets the local variable $items with the parsed file contents.
The script then looks for TemplateDirectory, which is not directly defined in the object. Therefore, the magic __get method is called with $id set to 'TemplateDirectory', which returns the value in the $items array for that key.
This __get method is specific to the PHP V5 environment, so this script must be run under PHP V5. In fact, all the scripts in this article need to be run under PHP V5.
When running this script from the command line, you can see the following results:
http://www.knowsky.com/php.asp
% php text1.php
tempdir
%
As expected, the object reads the config.txt file and gets the correct value for the TemplateDirectory configuration item.
But what should you do to set a configuration value? By creating a new method and some new test code in this class, you can get this functionality, 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, there is a __set function, which is a "cousin" of the __get function. This function does not get the value for a member variable. This function is called when a member variable is to be set. The test code at the bottom sets the value and prints out the new value.
Here is what happens when you run this code from the command line:
% php text2.php
tempdir
foobar
%
Very good! But how can I save it to a file so that the change is fixed? To do this, you need to write the file and read it. New function for writing files as 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();
?>
The new save function manipulates config.txt neatly. Rather than just rewriting the file with the updated configuration items (which would remove the comments), I read the file and flexibly rewrote the contents of the $items array. This way, the comments in the file are preserved.
Run the script on the command line and output the contents of the text configuration file. You can see the following output.
Listing 10. Saving 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 is now updated with the new values.
XML configuration files
Although text files are easy to read and edit, they are not as popular as XML files. Additionally, there are numerous editors available for XML that understand markup, special symbol escaping, and more. So what would the XML version of the configuration file look like? Listing 11 shows the configuration file in XML format.
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 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 seems that XML has another benefit: the code is simpler and easier than the text version. To save this XML, another version of the save function is needed, which saves the result in XML format instead of text format.
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) and then saves all the data in the $items array into this model. After completing this, use the save method to save the XML to a file.
A final alternative
to using a database
is to use a database to store the values of configuration elements.The first step is to use a simple schema to store configuration data. Below is a simple pattern.
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 adjustments based on application needs. For example, if you want the configuration element to be stored per user, you need to add the user ID as an additional column.
To read and write data, I wrote the updated Configuration class shown in Figure 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. Please take a closer look at the parse method. This class first reads the text file to get the initial value, then reads the database to update the key to the latest value. After setting a value, the key is removed from the database and a new record is added with the updated value.
It's interesting to see how the Configuration class functions through multiple versions of this article. It can read data from text files, XML, and databases, all while maintaining the same interface. I encourage you to use interfaces with the same stability in your development as well. Exactly how this works is unclear to the client of the object. The key is the contract between the object and the client.
What is configuration and how to configure it
Finding the right middle ground between too many configuration options and not enough configuration can be difficult. To be sure, any database configuration (for example, database name, database user and password) should be configurable. In addition, I have some basic recommended configuration items.
In the advanced settings, each feature should have a separate enable/disable option. Allow or disable these options based on their importance to the application. For example, in a Web forum application, the delay feature is enabled by default. However, email notifications are disabled by default, as this appears to require customization.
User interface (UI) options should all be set to one location. The structure of the interface (e.g., menu locations, additional menu items, URLs linking to specific elements of the interface, logos used, etc.) should all be set to a single location. I strongly recommend against specifying font, color, or style entries as configuration items. These should be set via Cascading Style Sheets (CSS), and the configuration system should specify which CSS file to use. CSS is an efficient and flexible way to set fonts, styles, colors, and more. There are many great CSS tools out there, and your application should make good use of CSS rather than trying to set the standard yourself.
Within each feature, I recommend setting 3 to 10 configuration options. These configuration options should be named in a meaningful way. If configuration options can be set through the UI, option names in text files, XML files, and databases should be directly related to the title of the interface element. In addition, these options should all have clear default values.
In general, the following options should be configurable: email addresses, what CSS to use, the location of system resources referenced from files, and the filenames of graphic elements.
For graphic elements, you might want to create a separate profile type called skin, which contains settings for the profile, including the placement of CSS files, the placement of graphics, and those types of things. Then, let the user choose from multiple skin files. This makes large-scale changes to the look and feel of your application simple. This also provides users with the opportunity to skin the application between different product installations. This article does not cover these skin files, but the basics you learn here will make supporting skin files much simpler.
Conclusion
Configurability is a vital part of any PHP application and should be a central part of the design from the start. I hope this article provides some help in implementing your configuration architecture and provides some guidance on what configuration options you should allow.