Pragmatism 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:


< ?php class Akrabat_Config { private $_config; /** * Load the section $section from the ini file called $filename. If * $section is null, then the entire file is loaded. * * 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. * * If any key includes a ".", then this will act as a separator to create * a sub-array. * * 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=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:

< ?php require_once 'PHPUnit2/Framework/TestCase.php'; include "Zend.php"; class ConfigTest extends PHPUnit2_Framework_TestCase { protected $_iniFileConfig; protected $_iniFileOne; protected $_iniFileTwo; function setUp() { Zend::loadClass('Akrabat_Config'); $this->_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:

toplevel = 1

[all]
hostname = all
akey = avalue
test.me = all

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

[multi]
one.two.three = four
one.two.three.four = five
one.two..three = dotdotthree

[dot]
. = dot
.. = doubledot
t. = t-dot
.t = dot-t

one.ini:

akey = one

[db]
hostname = one
database = thisDb

two.ini:

akey = two

[db]
hostname = two

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

[db]
adapter = pdoMysql
config.host = localhost
config.username = akrabat
config.password = 123456
config.dbname = atasks

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):


< ?php final class Big_Project_Config extends Akrabat_Config { private $instance; private function __construct() { parent::__construct(); parent::load('config_main.ini'); parent::load('config_' . fileowner(__FILE__) . '.ini'); } public function load() { throw new Exception('no way'); } public function getInstance() { if(!$this->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!)

Comments are closed.