The following is the quoted content:
If you plan to make your php application available to other people or companies, you need to make sure that 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.
Use INI file for configuration
PHP has built-in support for configuration files. This is achieved through an initialization file (INI) mechanism such as a 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.
List 1. ini1.php
Function get_template_directory()
{
$v = get_cfg_var( “myapptempdir” );
return ( $v == null ) ? “tempdir” : $v;
}
echo( get_template_directory().” ” );
?>
When you run this code on the command line, you get the following results:
% php ini1.php
foo
%
Awesome. But why can't I 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 method 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.
List 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 an INI file, so if you want to use a different mechanism or store the INI file somewhere else, you don't have to go through the trouble of changing a bunch 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 nearly 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.
List 3. ini3.php
echo( ini_get(”include_path”).” ” );
ini_set("include_path",
ini_get("include_path").":./mylib" );
echo( ini_get(”include_path”).” ” );
?>
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
# Specify the location of the temporary directory
#
$TEMPLATE_DIRECTORY = “tempdir”;
?>
The code using this constant is as follows.
Listing 5. php.php
require_once ‘config.php’;
Function get_template_directory()
{
global $TEMPLATE_DIRECTORY;
return $TEMPLATE_DIRECTORY;
}
echo( get_template_directory().” ” );
?>
The code first contains the configuration file (config.php), and then you can use these constants 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 and serve 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 out of this file is a piece of cake, but adjusting the data in this 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.
Text file
The previous two examples are both 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 helper tool. To do this, I created my own Configuration class as shown below.
Listing 7. text1.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.” ” );
?>
This 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 the TemplateDirectory, which is not directly defined in the object. So, the magic __get method is called with $id set to 'TemplateDirectory', and the __get method 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 on the command line, you can see the following results:
http://www.knowsky.com/php.asp
% php text1.php
tempdir
%
Everything is 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? Create a new method and some new test code in this class to get this function, as shown below.
Listing 8. text2.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.” ” );
$c->TemplateDirectory = ‘foobar’;
echo( $c->TemplateDirectory.” ” );
?>
Now, there is a __set function, which is the "cousin" of the __get function. This function does not get the value of 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 the result when running this code on the command line:
% php text2.php
tempdir
foobar
%
Great! But how do you store it in 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
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]].” ”;
}
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.” ” );
$c->TemplateDirectory = ‘foobar’;
echo( $c->TemplateDirectory.” ” );
$c->save();
?>
The new save function manipulates config.txt cleverly. 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 new values.
xml configuration file
Although text files are easy to read and edit, they are not as popular as XML files. In addition, there are many editors available for XML that understand markup, special symbol escaping, and so on. 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
Listing 12 shows an updated version of the Configuration class that uses XML to load configuration settings.
Listing 12. xml1.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.” ” );
?>
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 that 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 to this model. After completing this, use the save method to save the XML to a file.
Use database
A final alternative 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 would add the user ID as an extra column.
To read and write data, I wrote the updated Configuration class shown in Figure 15.
Listing 15. db1.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.” ” );
$c->TemplateDirectory = ‘new foo’;
echo( $c->TemplateDirectory.” ” );
?>
这实际上是一个混合的文本/数据库解决方案。请仔细观察 parse 方法。该类首先读取文本文件来获取初始值,然后读取数据库,进而将键更新为最新的值。在设置一个值后,键就从数据库中移除掉,并添加一条具有更新过的值的新记录。
观察 Configuration 类如何通过本文的多个版本来发挥作用是一件有趣的事,该类能从文本文件、XML 及数据库中读取数据,并一直保持相同的接口。我鼓励您在开发中也使用具有相同稳定性的接口。对于对象的客户机来说,这项工作具体是如何运行的是不明确的。关键的是对象与客户机之间的契约。
什么是配置及怎样配置
在配置过多的配置选项与配置不足间找一个适当的中间点是一件困难的事。可以肯定的是,任何数据库配置(例如,数据库名称、数据库用户用及密码)都应该是可配置的。除此之外,我还有一些基本的推荐配置项。
在高级设置中,每一个特性都应该有一个独立的启用/禁用选项。根据其对应用程序的重要性来允许或禁用这些选项。例如,在一个 Web 论坛应用程序中,延时特性在缺省状态下是启用的。但电子邮件通知在缺省状态下却是禁用的,因为这似乎需要定制。
用户界面(UI)选项全应该设置到一个位置上。界面的结构(例如,菜单位置、额外的菜单项、链接到界面特定元素的 URL、使用的 logo,诸如此类)全应该设置到一个单一位置上。我强烈地建议不要将字体、颜色或样式条目指定为配置项。这些都应该通过层叠样式表(Cascading Style Sheets,CSS)来设置,且配置系统应该指定使用哪个 CSS 文件。CSS 是设置字体、样式、颜色等等的一种有效且灵活的方式。有许多出色的 CSS 工具,您的应用程序应该很好地利用 CSS,而不是试图自行设置标准。
在每一个特性中,我推荐设置 3 到 10 个配置选项。这些配置选项应该以一种意义明显的方式命名。如果配置选项能够通过 UI 设置,在文本文件、XML 文件及数据库中的选项名称应该直接同界面元素的标题相关。另外,这些选项全应该有明确的缺省值。
总的来说,下面这些选项应该是可配置的:电子邮件地址、CSS 所使用的东西、从文件中引用的系统资源的位置以及图形元素的文件名。
对于图形元素,您也许想要创建一个名为皮肤 的独立的配置文件类型,该类型中包含了对配置文件的设置,包括 CSS 文件的位置、图形的位置及这些类型的东西。然后,让用户在多种皮肤文件中进行挑选。这使得对应用程序外观和感觉的大规模更改变得简单。这也同样为用户提供了一个机会,使应用程序能够在不同的产品安装间更换皮肤。本文并不涵盖这些皮肤文件,但您在这里学到的基础知识将会使对皮肤文件的支持变得更加简单。