Manipulating Configuration Data with Zend_Config

September 28, 2010

Tutorials, Zend Framework

Out With The Old…

A few months ago, I read about Zend_Config, a Zend Framework component that offers a complete API to read and write configuration data in a variety of formats. This was interesting to me, because one of the most common tasks I encounter, in almost every Web application I work on, involves writing a module to save and retrieve user-defined configuration values. Thus far, I’d been using a hand-rolled library for this task; however, this library was now fairly dated and didn’t take advantage of many of the newer PHP 5.x features and so, I’d been looking for a more modern replacement.

Zend_Config seemed to meet my needs, so I played with it a little and then deployed it in a couple of projects. It did everything I needed it to, and was easy to integrate with both framework and non-framework PHP projects. It also has a couple of neat features, such as the ability to merge multiple configurations together. Keep reading and I’ll give you a quick crash course in how it works.

Busy Signal

Before diving into the code, a few notes and assumptions. I’ll assume throughout this article that you have a working Apache/PHP/MySQL development environment, and that you have downloaded and installed the latest version of the Zend Framework. I’ll also assume that you know the basics of working with classes and objects in PHP.

The Zend_Config component provides an API to read and write configuration files, in array, INI or XML format. Data can be structured in “flat” or “tree” style, and the component provides a consistent API to access data items, regardless of the format in which they are stored.

Let’s begin with a simple example that illustrates Zend_Config in action. Consider the following simple XML configuration file:

<?xml version='1.0'?>
<config>
  <dialer>
    <number>12345678</number>
    <retries>15</retries>
    <protocol>ppp</protocol>
  </dialer>
</config>

Here’s a PHP script that illustrates how to access configuration data from this file:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// read XML config file
$config = new Zend_Config_Xml('config.xml', 'dialer');

// access individual nodes 
printf("Number: %s \r\n", $config->number);
printf("Retries: %s \r\n", $config->retries);
printf("Protocol: %s \r\n", $config->protocol);
?>

Zend_Config offers adapters for two different configuration file formats, INI and XML, implemented as Zend_Config_Ini and Zend_Config_Xml respectively. This script uses the Zend_Config_Xml adapter to read the configuration file and convert it into an object representation. It begins by setting up the Zend auto-loader, which takes care of automatically loading Zend Framework components as needed. It then initializes an instance of Zend_Config_Xml, passing it the file name and (optionally) the parent node to start with. Children of the specified parent node can now be accessed using standard object->property notation.

Here’s what the output looks like:

Setting Rules

You can omit the node name, in which case Zend_Config will create an object tree beginning with the document root node. The following revision illustrates this, producing the same output as before:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// read XML config file
$config = new Zend_Config_Xml('config.xml');

// access individual nodes 
printf("Number: %s \r\n", $config->dialer->number);
printf("Retries: %s \r\n", $config-> dialer->retries);
printf("Protocol: %s \r\n", $config-> dialer->protocol);
?>

Node attributes are converted into child properties of the parent node, and node lists can be accessed using indexes. Consider the following configuration file, which illustrates these aspects:

<?xml version='1.0'?>
<config>
  <firewall context="default" access="deny" >
    <rule access="allow" network="10.0.0.0/8" />
    <rule access="allow" host="192.168.1.17" />
  </firewall>
</config>

Here’s a PHP script to access these values:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// read XML config file
$config = new Zend_Config_Xml('config.xml', 'firewall');

// returns 'default'
echo $config->context; 
// returns 'allow'
echo $config->rule->{0}->access; 
// returns '192.168.1.17'
echo $config->rule->{1}->host; 
?>

Format Frenzy

If your data is stored in INI format, it’s just as easy to access it. Here’s an example of an INI configuration file:

[dialer]
number = 12345678
retries = 15
protocol = ppp

To read configuration data from an INI file, use the Zend_Config_Ini adapter, as shown below:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// read XML config file
$config = new Zend_Config_Ini('config.ini', 'dialer');

// access individual elements 
printf("Number: %s \r\n", $config->number);
printf("Retries: %s \r\n", $config->retries);
printf("Protocol: %s \r\n", $config->protocol);
?>

On the previous page, I’d said that Zend_Config offers adapters for two file formats, INI or XML. This is not strictly true; apart from these formats, it’s also possible to store configuration data as a PHP array, and read this directly into a Zend_Config object. If your configuration files are unlikely to be modified by hand, storing data as a PHP array is usually a good idea, because it offers one key advantage: a PHP opcode cache like APC can read and cache this data, reducing file reads and improving performance.

Here’s an example of the same configuration data in a PHP array:

<?php
return $config = array(
  'dialer' => array(
    'number'    => 12345678,
    'retries'   => 15,
    'protocol'  => 'ppp'
  )
);
?>

And here’s the PHP script you’d use to read it. The key difference here is that the configuration file containing the PHP array is loaded into the script with require_once(), and the array (not the file name) is then directly passed to Zend_Config.

<?php
// include PHP configuration data
require_once 'config.php';

// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// read XML config file
$config = new Zend_Config($config);

// access individual elements 
printf("Number: %s \r\n", $config->dialer->number);
printf("Retries: %s \r\n", $config->dialer->retries);
printf("Protocol: %s \r\n", $config->dialer->protocol);
?>

Zend_Config doesn’t yet support YAML, but in case you have configuration data in this format, consider these two approaches, which make use of the syck extension and the spyc library respectively to bring YAML support to Zend_Config.

A Tidy Nest

Zend_Config is also able to read nested configuration data, whether in INI, XML or array format. To illustrate, consider the following XML file:

<?xml version='1.0'?>
<configs>
  <config scope="php">
    <filters>
      <filter name="Tidy">
        <parameters name="tidy_options">          
          <parameter> 
            <name>output-xhtml</name>
            <value>true</value>
          </parameter> 
          <parameter> 
            <name>numeric-entities</name>
            <value>true</value>
          </parameter> 
          <parameter> 
            <name>encoding</name>
            <value>utf8</value>
          </parameter> 
        </parameters>
      </filter> 
    </filters>
  </config>
</configs>

Here’s an example of accessing a node value:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// read XML config file
$config = new Zend_Config_Xml('config.xml', 'config');

// returns 'true'
echo $config->filters->filter->parameters->parameter->{0}->value;
?>

It’s useful to remember that Zend_Config implements the Iterator interface, which means that you can also loop over a set of configuration values, like this:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// read XML config file
$config = new Zend_Config_Xml('config.xml', 'config');

// iterate over parameters
foreach ($config->filters->filter->parameters->parameter as $p) {
  printf("%s = %s \r\n", $p->name, $p->value);
}
?>

Good Parenting

Zend_Config also supports a form of inheritance, wherein sections of a configuration file can “inherit” configuration elements from other sections. When using XML configuration files, this inheritance is indicated with the special keyword “extends”. Here’s an example of an XML configuration file that uses this feature:

<?xml version='1.0'?>
<configs>
  <default>
    <log>
      <filename>general.log</filename>
      <data>time,message,code</data>
      <maxsize>2M</maxsize>    
    </log>
  </default>
  <error extends="default">
    <log>
      <filename>error.log</filename>
    </log>
  </error>
</configs>

As a result of this, keys that are not explicitly set in the child block will inherit values from the parent block. This is clearly seen in the following script:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// define Zend_Config objects
$config = new Zend_Config_Xml('config.xml');

// returns 'error.log'
echo $config->error->log->filename;

// returns '2M'
echo $config->error->log->maxsize;
?>

When using INI files, inheritance can be indicated through the colon symbol, as shown below:

[default]
log.filename = "general.log"
log.data = "time,message,code"
log.maxsize = "2M"

[error : default]
log.filename = "error.log"

Zend_Config also makes it possible to merge configuration data from more than one file, via its merge() method. Later items with the same name will override earlier ones. Here’s an example:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// define Zend_Config objects
$zca = new Zend_Config(array('a'=>10, 'b'=>2), true);
$zcb = new Zend_Config(array('c'=>4,  'b'=>16), true);

// merge objects
$zca->merge($zcb);

// check merged data
echo $zca->a; // 10
echo $zca->b; // 16
echo $zca->c; // 4
?>

The Write Stuff

In addition to reading configuration files, Zend_Config also offers the Zend_Config_Writer, which provides an API to write configuration files. Zend_Config_Writer comes with adapters for INI, XML and PHP array formats, making it possible to save configuration data in the format best suited for your application’s needs.

Here’s an example, which writes an INI file:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// define configuration array
$data = array(
  'name' => 'Zerg Ultralisk',
  'endurance' => '7',
  'strength' => '13',
  'intelligence' => '8',
  'damage' => '4'    
);

// write INI config file
try {
  $config = new Zend_Config_Writer_Ini();
  $config->write('my.ini', new Zend_Config($data));
  echo 'Configuration successfully written to file.';
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

Every Zend_Config_Writer implementation exposes a write() method, which takes care of writing the configuration file in the correct format. This write() method requires two arguments: the name of the configuration file, and a Zend_Config instance containing the configuration data to be written. The previous example uses the Zend_Config_Writer_Ini implementation, which produces an INI file. The Zend_Config instance required by the write() method is created from a PHP associative array, which holds the configuration data as key-value pairs.

Prefer XML? Simply switch the previous script to use the Zend_Config_Writer_Xml adapter:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// define configuration array
$data = array(
  'name' => 'Zerg Ultralisk',
  'endurance' => '7',
  'strength' => '13',
  'intelligence' => '8',
  'damage' => '4'    
);

// write XML config file
try {
  $config = new Zend_Config_Writer_Xml();
  $config->write('my.xml', new Zend_Config($data));
  echo 'Configuration successfully written to file.';
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

If you’d like the output to be stored as a native PHP array, use the Zend_Config_Writer_Array adapter:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// define configuration array
$data = array(
  'name' => 'Zerg Ultralisk',
  'endurance' => '7',
  'strength' => '13',
  'intelligence' => '8',
  'damage' => '4'    
);

// write array config file
try {
  $config = new Zend_Config_Writer_Array();
  $config->write('my.php', new Zend_Config($data));
  echo 'Configuration successfully written to file.';
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

Here’s what each of the scripts above would produce:

Building XML Trees

You can also create nested trees of configuration data with Zend_Config_Writer, as illustrated in the next example:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// define configuration array
$data = array(
  'global' => array(
    'smtp' => array(
      'parameters'  => array(
        'host'      => 'smtp.example.com',
        'port'      => '587',
        'username'  => 'jeky11',
        'password'  => 'hyd3',
        'security'  => 'tls',
      )
    )
  )
);    

// write XML config file
try {
  $config = new Zend_Config_Writer_Xml();
  $config->write('my.xml', new Zend_Config($data));
  echo 'Configuration successfully written to file.';
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

Here’s what the output looks like:

The previous examples all defined an array, used the array to initialize a Zend_Config object, and then wrote the Zend_Config object to a file. A simpler approach is to dynamically build the Zend_Config object by adding parameters to it using object->property notation. Here’s an example, which is equivalent to the previous one:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// define configuration object
$config = new Zend_Config(array(), true);
$config->global = array();
$config->global->smtp = array();
$config->global->smtp->parameters = array();
$config->global->smtp->parameters->host = 'smtp.example.com';
$config->global->smtp->parameters->port = '587';
$config->global->smtp->parameters->username = 'jeky11';
$config->global->smtp->parameters->password = 'hyd3';
$config->global->smtp->parameters->security = 'tls';

// write XML config file
try {
  $writer = new Zend_Config_Writer_Xml();
  $writer->write('my.xml', $config);
  echo 'Configuration successfully written to file.';
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

When nesting INI configuration data, you can define the separator used for the different nesting levels, via the setNestSeparator() method. The default separator is a period.. Here’s a revision of the previous example, which demonstrates:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// define configuration array
$data = array(
  'global' => array(
    'smtp' => array(
      'parameters'  => array(
        'host'      => 'smtp.example.com',
        'port'      => '587',
        'username'  => 'jeky11',
        'password'  => 'hyd3',
        'security'  => 'tls',
      )
    )
  ),
  'local' => array(
    'version' => '1.1'
  )  
);

// write INI config file
try {
  $config = new Zend_Config_Writer_Ini();
  $config->setNestSeparator('::');
  $config->write('my.ini', new Zend_Config($data));
  echo 'Configuration successfully written to file.';
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

You can also decide whether configuration values should be divided into sections or not via the setRenderWithoutSections() method, which accepts a Boolean argument. Here’s how you’d use it:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// define configuration array
$data = array(
  'global' => array(
    'smtp' => array(
      'parameters'  => array(
        'host'      => 'smtp.example.com',
        'port'      => '587',
        'username'  => 'jeky11',
        'password'  => 'hyd3',
        'security'  => 'tls',
      )
    )
  ),
  'local' => array(
    'version' => '1.1'
  )  
);

// write INI config file
try {
  $config = new Zend_Config_Writer_Ini();
  $config->setNestSeparator('::');
  $config->setRenderWithoutSections(true);  
  $config->write('my.ini', new Zend_Config($data));
  echo 'Configuration successfully written to file.';
} catch (Exception $e) {
  die('ERROR: ' . $e->getMessage());
}
?>

The following image illustrates the difference in output:

Task Manager

Now that you know how Zend_Config works, let’s look at using it in a practical example. Assume for a moment that you’re writing a Web-based task scheduling application, and you’d like some aspects of the application to be user-definable. Your plan is to give your users a browser-based configuration tool to define these values, and then store their selections in a configuration file. The data in this file could then be used by your controllers and models as needed.

Zend_Config has a dual role to play here. On GET requests, it must check if an existing configuration file exists and, if yes, it must pre-populate the form with the current configuration. On POST requests, it must validate the POST-ed form input and write it back to the configuration file.

Since users will interact with the configuration file only through a browser and never edit it directly, it’s quite safe to store the configuration data as a PHP array; this also simplifies integration with the rest of the application and offers some caching possibilities.

Here’s the complete script:

<?php
// include auto-loader class
require_once 'Zend/Loader/Autoloader.php';

// register auto-loader
$loader = Zend_Loader_Autoloader::getInstance();

// set name of config file
$configFile = 'config.php';

// if file exists, read into Zend_Config object
if (file_exists($configFile)) {  
  $config = new Zend_Config(include $configFile, null); 
}
?>
<html>
  <head></head>
  <body>
    <h2>Edit Configuration</h2>
    <?php 
    if (!isset($_POST['submit'])) { 
    ?>    
    <form method="post" action="<?php echo htmlentities($_SERVER['PHP_SELF']); ?>">
    
    <p>
    Task reminders: 
    <input type="text" name="rd" size="4" value="<?php echo $config->tasks->reminder_days; ?>"/> 
    days before due date
    </p>
    
    <p>
    Week starts on: 
    <select name="wsd">
    <option value="0" <?php echo ($config->calendar->week_start == 0) ? 'selected' : ''; ?> />Sunday
    <option value="1" <?php echo ($config->calendar->week_start == 1) ? 'selected' : ''; ?> />Monday
    </select>
    </p>
    
    <p>
    Time tracking for tasks: 
    <input type="radio" name="tt" value="1" <?php echo ($config->tasks->enable_time == 1) ? 'checked' : ''; ?> />On
    <input type="radio" name="tt" value="0" <?php echo ($config->tasks->enable_time == 0) ? 'checked' : ''; ?> />Off
    </p>
    
    <p>
    Dependency tracking for tasks: 
    <input type="radio" name="dt" value="1" <?php echo ($config->global->enable_deps == 1) ? 'checked' : ''; ?> />On
    <input type="radio" name="dt" value="0" <?php echo ($config->global->enable_deps == 0) ? 'checked' : ''; ?> />Off
    </p>
    
    <p>
    Completed tasks:
    <input type="radio" name="ct" value="0" <?php echo ($config->tasks->on_complete == 0) ? 'checked' : ''; ?> />Delete 
    <input type="radio" name="ct" value="1" <?php echo ($config->tasks->on_complete == 1) ? 'checked' : ''; ?> />Archive
    </p>
    
    <input type="submit" name="submit" value="Save" />
    </form>
    
    <?php
    } else {
      try {
        // validate input
        $rd = strip_tags(trim($_POST['rd']));
        if (!ctype_digit($rd)) {
          throw new Exception('Invalid input for reminder days');  
        } 
  
        $wsd = strip_tags(trim($_POST['wsd']));
        if (!in_array($wsd, array(0,1))) {
          throw new Exception('Invalid input for week start day');  
        } 
              
        $tt = strip_tags(trim($_POST['tt']));
        if (!in_array($tt, array(0,1))) {
          throw new Exception('Invalid input for time tracking configuration');  
        } 
        
        $dt = strip_tags(trim($_POST['dt']));
        if (!in_array($dt, array(0,1))) {
          throw new Exception('Invalid input for dependency tracking configuration');  
        } 
        
        $ct = strip_tags(trim($_POST['ct']));
        if (!in_array($ct, array(0,1))) {
          throw new Exception('Invalid input for completed tasks configuration');  
        }
  
        // update configuration
        $config = new Zend_Config(array(), true);
        $config->global = array();
        $config->global->enable_deps = $dt;
        $config->calendar = array();
        $config->calendar->week_start = $wsd;
        $config->tasks = array();
        $config->tasks->reminder_days = $rd;
        $config->tasks->enable_time = $tt;
        $config->tasks->on_complete = $ct;
        
        // write PHP config file
        $writer = new Zend_Config_Writer_Array();      
        $writer->write($configFile, $config);
        echo 'Configuration successfully written to file.';
      } catch (Exception $e) {
        die('ERROR: ' . $e->getMessage());
      }
    }
    ?>
  </body>
</html>

Nothing special here: the script checks if a configuration file exists and if yes, loads it into a Zend_Config object. Within the form, conditional tests are used to check the Zend_Config object data and automatically check/select/populate the various form fields with the correct values. Once the user submits the form, the Zend_Config object is recreated with the new configuration and written back to the file using the Zend_Config_Writer_Array writer.

Here’s what the form looks like:

As these examples will have illustrated, Zend_Config only does one thing – managing configuration data – but it does that thing very well. It not only has the ability to read and write data in INI, XML and native PHP formats, but it also offers built-in support for nested configuration trees and inheritance. In short, if you need manage your application’s configuration data, Zend_Config probably has everything you need (and a bit more besides). Try it out the next time you sit down to write a PHP application, and see for yourself!

Copyright Melonfire, 2010. All rights reserved.

2 Responses to “Manipulating Configuration Data with Zend_Config”

  1. martin1982 Says:

    The image below the Zend_Config_Writer_Xml shows different data than the array contains, I assume this image is created with a different config array?

    The rest of the article is very clear, and especially the Writer part is something I want to start working with!