Developing software in the Real World

Akrabat_Config (Take Three!)

Update: I’ve made the filename parameter in the constructor optional as per Nico’s comment on the mailing list. I’ve also updated the code so that it meets the coding style

There’s been some more discussion about config on fw-general. Thanks all!

I commented that I think the ability to load multiple ini files would be useful and suggested using an array of filename.

Nico suggested:

You could just put the code from the constructor in its own method load(). Then it would be possible to write:

$config = new Akrabat_Config();
$config->load(‘mainconfig.ini’);
$config->load(IS_DEV ? ‘dev.ini’ : ‘staging.ini’);

That’s much nicer, so that’s they way I’ve gone. I’ve also kept the “first dot” separation and the “include=” constructs so this new version passes all the original tests and allows for a lot of flexibility. Even nicer, each bit is optional so you can pick and choose what you use.

This is the class:


hostname = staging
* $config->db['connection'] = database
*
*
* @param string $filename
* @param string $section
*/
function __construct($filename=null, $section=null)
{
$this->_config = array();
if (!is_null($filename)) {
$this->load($filename, $section);
}
}

/**
* Load an inifile, overwriting any duplicate keys.
* If $section is null, then the entire file is loaded.
*
* @param string $filename
* @param string $section
*/
public function load($filename, $section=null)
{
$iniArray = parse_ini_file($filename, true);
if ($section) {
if( isset($iniArray[$section])) {
$this->_config = array_merge($this->_config, $this->processIncludes($iniArray, $section));
} else {
throw new Exception("No section '$section' in $filename'");
}
}
else {
foreach($iniArray as $section=>$value) {
if (is_array($value)) {
if(!isset($this->_config[$section])) {
$this->_config[$section] = array();
}
$this->_config[$section] = array_merge($this->_config[$section], $this->processIncludes($iniArray, $section));
} else {
$this->_config[$section] = $value;
}
}
}
}

/**
* Helper function to process the "include=" inheritance and then
* process the "dot" single level sub-array syntax in each key.
*
* @param array $iniArray
* @param string $section
* @return array
*/
protected function processIncludes($iniArray, $section)
{
$config = array();
foreach ($iniArray[$section] as $key => $value) {
if ($key == 'include') {
if( isset($iniArray[$value])) {
$config = array_merge($config, $this->processLevelsInSection($iniArray[$value]));
}
unset($iniArray[$section][$key]);
}
}
$config = array_merge($config, $this->processLevelsInSection($iniArray[$section]));
return $config;
}

/**
* Helper function to handle single level namespace in the key
*
* @param array $section
* @return array
*/
protected function processLevelsInSection($section)
{
$config = array();
foreach ($section as $key=>$value) {
if (strpos($key, '.')) {
$pieces = explode('.', $key, 2);
if (!empty($pieces[1])) {
$config[$pieces[0]][$pieces[1]] = $value;
} else {
$config[$key] = $value;
}
} else {
$config[$key] = $value;
}
}
return $config;
}

/**
* @param string $name
* @param mixed $default
* @return mixed
*/
function get($name, $default=false)
{
$result = $default;
if (isset($this->_config[$name])) {
$result = $this->_config[$name];
}
return $result;
}

/**
* magic function so that $config->value will work.
*
* @param string $name
* @return mixed
*/
function __get($name)
{
return $this->get($name);
}

/**
* This is a read only class...
*
* @param string $name
* @param mixed $value
*/
function __set($name, $value)
{
throw new Exception('Akrabat_Config is read only!');
}

}
?>

Obviously the tests have been updated too:

_iniFileConfig = dirname(__FILE__).'/data/config.ini';
$this->_iniFileOne = dirname(__FILE__).'/data/one.ini';
$this->_iniFileTwo = dirname(__FILE__).'/data/two.ini';
}

function tearDown()
{
}

function testLoadSectionAll()
{
$config = new Akrabat_Config($this->_iniFileConfig, 'all');
$this->assertEquals('all', $config->hostname);
$this->assertEquals('all', $config->test['me']);
}

function testSectionInclude()
{
$config = new Akrabat_Config($this->_iniFileConfig, 'staging');
$this->assertEquals('staging', $config->hostname);
$this->assertEquals('staging', $config->test['me']);
}

function testSectionMultiLevels()
{
$config = new Akrabat_Config($this->_iniFileConfig, 'multi');

$this->assertEquals('four', $config->one['two.three']);
$this->assertEquals('five', $config->one['two.three.four']);
$this->assertEquals('dotdotthree',$config->one['two..three']);
}

function testSectionLeadingDot()
{
$config = new Akrabat_Config($this->_iniFileConfig, 'dot');
$this->assertEquals('dot', $config->get("."));
$this->assertEquals('doubledot', $config->get(".."));
$this->assertEquals('t-dot', $config->get("t."));
$this->assertEquals('dot-t', $config->get(".t"));
}

function testLoadEntireConfigFile()
{
$config = new Akrabat_Config($this->_iniFileConfig);

$this->assertEquals('all', $config->all['hostname']);
$this->assertEquals('all', $config->all['test']['me']);

// ensure that the include=all in "staging" works
$this->assertEquals('avalue', $config->staging['akey']);

// test multi-level
$multi = $config->get('multi');
$this->assertEquals('four', $multi['one']['two.three']);

// test top level
$this->assertEquals('1', $config->toplevel);
}

function testReadOnly()
{
$config = new Akrabat_Config($this->_iniFileConfig);
try {
$config->test = '32';
}
catch (Exception $expected) {
return;
}

$this->fail('An expected Exception has not been raised.');
}

function testSecondFileOverwritesFirst()
{
$config = new Akrabat_Config();
$config->load($this->_iniFileOne);
$this->assertEquals('one', $config->akey);

$config->load($this->_iniFileTwo);
$this->assertEquals('two', $config->akey);

$this->assertEquals('two', $config->db['hostname']);
$this->assertEquals('thisDb', $config->db['database']);
}
}
?>

There are now three config files used, mainly because it was easier to see what’s going on!
config.ini:


one.ini:


two.ini:

This provides a lot of flexibility:
e.g. given an ini file containing this:


You can do do this:

$cfg = new Akrabat_Config('settings.ini');
$db = Zend_Db::factory($cfg->db['adapter'], $cfg->db['config']);

which allows for a nicely organised ini file and easy translation of the settings to the class that needs them.

Matt provided a solution that turns multiple level dots into objects, but I haven’t played with it enough yet. I suspect that using multiple dots should be discouraged, but if it doesn’t cost too much, then why not just provide it? We’ll see…

Nico also came up with some extension ideas which also work:
i.e.

I guess for big projects I wouldn’t let anyone define config names and use a singleton like (the UID is ‘our way’ to let each developer have his own config):


instance) $this->instance = new Big_Project_Config();
return $this->instance;
}
}
?>

That should all be possible with Rob’s config class (also credit to Andi of course), so I like this implementation. It’s so easy and straight forward, but can do everything you want by extending it.

2 thoughts on “Akrabat_Config (Take Three!)

Thoughts? Leave a reply

Your email address will not be published. Required fields are marked *