Article
Instant XML with PHP and PEAR::XML_Serializer
Managing Configuration Information
Now that you've had a dry view of what PEAR::XML_Serializer offers, and have seen some basic examples, it's time to do something useful with it.
Most PHP applications require some form of configuration to enable them to "understand" the environment in which they're being used, such as the domain name of the Web server, the administrator's email address, database connection settings and so on. There are a number of common approaches to handling this in PHP, from simply having a PHP script with a list of variables that need editing, to using the parse_ini_file() function (note that PHP can parse an ini file faster than it can include and parse the equivalent PHP script).
XML makes another choice, being relatively friendly to edit manually, relatively easy to parse and generate and allowing more complex data structures than an ini file. On the downside, retrieving configuration data from an XML file is liable to be slow, compared to alternatives (although some tricks with PHP code generation can help you get round this, but that's another story).
Performance issues aside, here's one approach using PEAR::XML_Serializer and a class that allows you to retrieve and modify configuration settings.
First, I define two classes: one in which to store configuration data, and a second to manage access to it:
<?php
/**
* The name of file used to store config data
*/
define ('CONFIG_FILE', 'config.xml');
/**
* Stores configuration data
*/
class Config {
/**
* Array of configuration options
* @var array
* @access private
*/
var $options = array();
/**
* Returns the value of a configuration option, if found
* @param string name of option
* @return mixed value if found or void if not
* @access public
*/
function get($name) {
if (isset($this->options[$name])) {
return $this->options[$name];
}
}
/**
* Sets a configuration option
* @param string name of option
* @param mixed value of option
* @return void
* @access public
*/
function set($name, $value) {
$this->options[$name] = $value;
}
}
The Config class acts as a simple store for values, allowing access via the get() and set() methods.
/**
* Provides a gateway to the Config class, managing its serialization
*/
class ConfigManager {
/**
* Returns a singleton instance of Config
* @return Config
* @access public
* @static
*/
function &instance() {
static $Config = NULL;
if (!$Config) {
$Config = ConfigManager::load();
}
return $Config;
}
/**
* Loads the Config instance from it's XML representation
* @return Config
* @access private
* @static
*/
function load() {
error_reporting(E_ALL ^ E_NOTICE);
require_once 'XML/Unserializer.php';
$Unserializer = &new XML_Unserializer();
if (file_exists(CONFIG_FILE)) {
$status = $Unserializer->unserialize(CONFIG_FILE, TRUE);
if (PEAR::isError($status)) {
trigger_error ($status->getMessage(), E_USER_WARNING);
}
$Config = $Unserializer->getUnserializedData();
} else {
$Config = new Config();
}
return $Config;
}
/**
* Stores the Config instance, serializing it to an XML file
* @return boolean TRUE on succes
* @access public
* @static
*/
function store() {
error_reporting(E_ALL ^ E_NOTICE);
require_once 'XML/Serializer.php';
$Config = &ConfigManager::instance();
$serializer_options = array (
'addDecl' => TRUE,
'encoding' => 'ISO-8859-1',
'indent' => ' ',
'typeHints' => TRUE,
);
$Serializer = &new XML_Serializer($serializer_options);
$status = $Serializer->serialize($Config);
$success = FALSE;
if (PEAR::isError($status)) {
trigger_error($status->getMessage(), E_USER_WARNING);
}
$data = $Serializer->getSerializedData();
if (!$fp = fopen(CONFIG_FILE, 'wb')) {
trigger_error('Cannot open ' . CONFIG_FILE);
} else {
if (!fwrite($fp, $data, strlen($data))){
trigger_error(
'Cannot write to ' . CONFIG_FILE, E_USER_WARNING
);
} else {
$success = TRUE;
}
fclose($fp);
}
return $success;
}
}
?>
Filename: configmanager.php
The ConfigManager class is a bit more complex. I'll explain the key points here, but if you have any specific questions, feel free to drop them into the discussion at the end of this article.
The static instance() method uses the PHP4 trick for creating Singleton instances on an object. Whether you're aware of the Singleton design pattern or not, what the instance() method allows me to do is fetch the same instance of Config from anywhere in my code, simply by calling ConfigManager::instance(), making sure that any changes that happen the Config object are available from wherever it's used.
The load() and store() methods handle serializing and unserializing the Config object to XML. The load() method is intended only to be called by the instance() method while the store() method should be called at the end of my application's execution, if any external changes were made to the Config object. Note that I'm using Lazy Includes inside these methods to keep the amount of parsing the PHP engine needs to do to a minimum. There may be instances where load() is called but not store(), when the code using it only needs to retrieve configuration values, not modify them. In these instances, including the XML_Serializer class on every request wastes overhead.
Now, using PEAR::HTML_QuickForm, I can build a form for editing the configuration file. This time, I'm going to skip explaining HTML_QuickForm (you can find further examples in the package and tutorials in The PHP Anthology). Make sure you have it installed by typing:
$ pear install HTML_Quickform
The HTML_Quickform Version used here was 3.2.2.
The form code:
<?php
require_once 'configmanager.php';
require_once 'HTML/QuickForm.php';
// Fetch the singleton instance of Config
$Config = &ConfigManager::instance();
// Build a form with PEAR::HTML_QuickForm
$Form = new HTML_QuickForm('labels_example', 'post');
$Form->addElement('text', 'domain', 'Domain');
$Form->addRule('domain', 'Please enter a domain name', 'required',
NULL, 'client');
$Form->addRule('domain', 'Please enter a valid domain name',
'regex', '/^(www\.)?.+\.(com|net|org)$/', 'client');
$Form->addElement('text', 'email', 'Email');
$Form->addRule('email', 'Please enter an email address', 'required',
NULL, 'client');
$Form->addRule('email', 'Please enter a valid email address',
'email', NULL, 'client');
$Form->addElement('text', 'docroot', 'Document Root');
$Form->addRule('docroot', 'Please enter the document root',
'required', NULL, 'client');
$Form->addRule('docroot', 'Please enter a valid document root',
'callback', 'is_dir');
$Form->addElement('text', 'tmp', 'Tmp Dir');
$Form->addRule('tmp', 'Please enter the tmp dir', 'required',
NULL, 'client');
$Form->addRule('tmp', 'Please enter a valid tmp dir', 'callback',
'is_dir');
$Form->addElement('text', 'db_host', 'DB Host');
$Form->addRule('db_host', 'Please enter a value for DB Host', 'required',
NULL, 'client');
$Form->addRule('db_host', 'Please enter a valid value for DB Host',
'regex', '/^[a-zA-Z0-9\.]+$/', 'client');
$Form->addElement('text', 'db_user', 'DB User');
$Form->addRule('db_user', 'Please enter a value for DB User', 'required',
NULL, 'client');
$Form->addRule('db_user', 'Please enter a valid value for DB User',
'regex', '/^[a-zA-Z0-9]+$/', 'client');
$Form->addElement('text', 'db_pass', 'DB Password');
$Form->addRule('db_pass', 'Please enter a value for DB Password', 'required',
NULL, 'client');
$Form->addRule('db_pass', 'Please enter a valid value for DB Password',
'regex', '/^[a-zA-Z0-9]+$/', 'client');
$Form->addElement('text', 'db_name', 'DB Name');
$Form->addRule('db_name', 'Please enter a value for DB Name', 'required',
NULL, 'client');
$Form->addRule('db_name', 'Please enter a valid value for DB Name', 'regex',
'/^[a-zA-Z0-9]+$/', 'client');
$Form->addElement('submit', null, 'Update');
// Initialize $db array as needed
$db = $Config->get('db');
if (!is_array($db)) $db = array();
if (!isset($db['db_host'])) $db['db_host'] = '';
if (!isset($db['db_user'])) $db['db_user'] = '';
if (!isset($db['db_pass'])) $db['db_pass'] = '';
if (!isset($db['db_name'])) $db['db_name'] = '';
// Set initial form values from Config
$Form->setDefaults(array(
'domain' => $Config->get('domain'),
'email' => $Config->get('email'),
'docroot' => $Config->get('docroot'),
'tmp' => $Config->get('tmp'),
'db_host' => $db['db_host'],
'db_user' => $db['db_user'],
'db_pass' => $db['db_pass'],
'db_name' => $db['db_name'],
));
// If the form is valid update the configuration file
if ($Form->validate()) {
$result = $Form->getSubmitValues();
$Config->set('domain',$result['domain']);
$Config->set('email',$result['email']);
$Config->set('docroot',$result['docroot']);
$Config->set('tmp',$result['tmp']);
$db['db_host'] = $result['db_host'];
$db['db_user'] = $result['db_user'];
$db['db_pass'] = $result['db_pass'];
$db['db_name'] = $result['db_name'];
$Config->set('db', $db);
if (ConfigManager::store()) {
echo "Config updated successfully";
} else {
echo "Error updating configuration";
}
} else {
echo '<h1>Edit ' . CONFIG_FILE . '</h1>';
$Form->display();
}
?>
Filename: configedit.php
At the start, I fetch the instance of Config from ConfigManager and use it to populate the default form values. I need to initialise the $db array in case this is the first time the config.xml file has been edited (i.e. it doesn't yet exist) and Config contains empty values.
Once the form is submitted as successfully validated, I place the values back in the Config instance, then call ConfigManager::store() to update the config.xml document with the latest values.
Here's how the form looks in a browser:

The config.xml file stored looks like this:
<?xml version="1.0" encoding="ISO-8859-1"?>
<config _class="config" _type="object">
<options _type="array">
<domain _type="string">www.sitepoint.com</domain>
<email _type="string">info@sitepoint.com</email>
<docroot _type="string">/www</docroot>
<tmp _type="string">/tmp</tmp>
<db _type="array">
<db_host _type="string">db.sitepoint.com</db_host>
<db_user _type="string">phpclient</db_user>
<db_pass _type="string">secret</db_pass>
<db_name _type="string">sitepointdb</db_name>
</db>
</options>
</config>
Filename: config.xml
The use of typehints makes it a little unfriendly to the human eye, but it's still possible to edit this file manually, should it be necessary.
Now, using another script, I can access the values in config.xml:
<?php
require_once 'configmanager.php';
// Fetch the singleton instance of Config
$Config = &ConfigManager::instance();
?>
<h1><?php echo CONFIG_FILE; ?></h1>
<table>
<tr>
<td>Domain:</td><td><?php echo $Config->get('domain'); ?></td>
</tr>
<tr>
<td>Email:</td><td><?php echo $Config->get('email'); ?></td>
</tr>
<tr>
<td>Docroot:</td><td><?php echo $Config->get('docroot'); ?></td>
</tr>
<tr>
<td>Tmp Dir:</td><td><?php echo $Config->get('tmp'); ?></td>
</tr>
<?php $db = $Config->get('db'); ?>
<tr>
<td>DB Host:</td><td><?php echo $db['db_host']; ?></td>
</tr>
<tr>
<td>DB User:</td><td><?php echo $db['db_user']; ?></td>
</tr>
<tr>
<td>DB Pass:</td><td><?php echo $db['db_pass']; ?></td>
</tr>
<tr>
<td>DB Name:</td><td><?php echo $db['db_name']; ?></td>
</tr>
</table>
Filename: configview.php
Here, I'm simply displaying them in a table, so you can see how it works. Because I can call ConfigManager::instance() from anywhere in my code, and receive an up-to-date reference to the Config object, it's easy to retrieve the values stored in it when I need to configure the behaviour of my application. Also, because I'm working with a Singleton instance of Config, the overhead of unserializing the underlying xml document only needs to be incurred once, the first time I fetch an instance of Config.
Using PEAR::XML_Serializer in this example helps me avoid getting involved with the nitty gritty of XML, allowing me to focus my efforts on code that builds on it and has direct value to my application.