Pragmatism in the real world

Akrabat_Config

Update! This version has bugs! Check out Akrabat_Config (2nd Attempt) for a better version.

Work has been manic, but I’ve finally found some time to do a bit more work on my Zend Framework test site. I’ve now got a concept for a simple CRUD type application that I’d like to write. It’s hardly imaginative and I’m sure that there are loads of other ones out there already, but it’ll do for testing stuff :)

My initial tests have used config variables directly in index.php, but I’ve decided that I’m going to need more flexibility than that, so I’ve written Akrabat_Config as a very simple inifile reader.

The idea is based on an email to the fw-general list from Andi Gutmans. I can’t find it in the archive, so I’ll quote the relevant bit:

Date: Fri, 10 Mar 2006 20:15:00 -0800
To: Zend Framework General
From: Andi Gutmans
Subject: Re: [fw-general] Code from existing framework

What might be interesting is something cascaded (i.e. sections can inherit configuration properties from others).
I personally would go with an INI like file which is easier to grasp and simple. I find that these kind of files are significantly easier for people to handle than XML.

Maybe something like the following where namespace would be optional:
[all]
namespace.property = example
db.connection = foo
hostname = www.zend.com

[andi_development]
include=all
hostname=andi_box
db.connection=localhost

[staging]
include=all
hostname=dev.zend.com

“include” would control which rule sets to inherit and can have more than one rule by listing them delimited by commas.

Then we can do something like:

$obj=new Zend_Config(“myapp.ini”, “andi_development”);
print $obj->hostname;
print $obj->db->connection;

I think this is a pretty simple and an easy to use approach. It gives you two levels, so it’s pretty flat but I think it’s definitely worth the simplification and fits nicely into the 20/80% rule.

Well, that’s not too difficult, so here is Akrabat_Config:

< ?php class Akrabat_Config { private $_config; /** * Load the section $section from the ini file called $filename. * If any keys with $section are called "include", then the section * pointed to by the "include" is then included first. Thus, the keys * in $section will override any keys of the same name in the sections * that have been "include"ed. * * example ini file: * [all] * db.connection = database * hostname = live * * [staging] * include = all * hostname = staging * * after callgin $config = new Akrabat_Config($file, 'staging'); then * $config->hostname = staging
* $config->db['connection'] = database
*
*
* @param string $filename
* @param string $section
*/
function __construct($filename, $section)
{
$iniArray = parse_ini_file($filename, true);

$config = array();
if(isset($iniArray[$section]))
{
foreach($iniArray[$section] as $key => $value)
{
if($key == 'include')
{
if(isset($iniArray[$value]))
{
$config = array_merge($config, $this->processSection($iniArray[$value]));
}
unset($iniArray[$section][$key]);

}
}
$config = array_merge($config, $this->processSection($iniArray[$section]));
}
else
{
throw new Exception("No section '$section' in $filename'");
}
$this->_config = $config;
}

/**
* Helper function to handle single level namespace in the key
*
* @param array $section
* @return array
*/
protected function processSection($section)
{
$config = array();
foreach($section as $key=>$value)
{
$pieces = explode('.', $key);
if(count($pieces) > 1)
{
$config[$pieces[0]][$pieces[1]] = $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);
}

}
?>

And here is the unit tests (which assume that your include path is set up correctly!) :

< ?php require_once 'PHPUnit2/Framework/TestCase.php'; include "Zend.php"; class ConfigTest extends PHPUnit2_Framework_TestCase { protected $iniFilename; function setUp() { $this->iniFilename = dirname(__FILE__).'/data/config.ini';
}

function tearDown()
{
}

function testLoadAll()
{
Zend::loadClass('Akrabat_Config');
$config = new Akrabat_Config($this->iniFilename, 'all');
$this->assertEquals('all', $config->hostname);
$this->assertEquals('all', $config->test['me']);
}

function testInclude()
{
Zend::loadClass('Akrabat_Config');
$config = new Akrabat_Config($this->iniFilename, 'staging');
$this->assertEquals('staging', $config->hostname);
$this->assertEquals('staging', $config->test['me']);
}
}
?>

The tests require a file called data/config.ini, which looks like this:

[all]
hostname = all
test.me = all

[staging]
include = all
hostname = staging
test.me = staging

And I think it meets the specification that Andy suggested. All I need to do now is work out how to pick which section to load based on where the site is being hosted. I suspect Apache environment variables set in the .htaccess or checking server_name in index.php…