For the last few days I have been struggling, sort of, to find the best way to implement a configuration class or object that will allow me to continually append values to it and allow easy access to the members of it. Think of it as a sort of data store that houses multi-tiered data that is logically structured. For example, lets say I have a configuration array like this:
<?php $array = array( 'defaults' => array( 'page' => 'index', 'action' => 'default', ), 'routes' => array( 'controller', 'view', 'model', ), 'application' => 'Bilbo', 'nested' => array( 'levels' => array( 1 => 'floor', 'garage', 'canopy', ), 'names' => array( 1 => 'Hectar', 'Jonas', 'Phil', ), 'named' => array( 'floor' => 'Hectar', 'garage' => 'Jonas', 'canopy' => 'Phil', ), ), ); ?> |
And lets say that this array is parsed into a config object that is in essence an interface so that config params can be set and got from it. The config array itself is protected so it cannot be manipulated directly (because seriously, why should you be able to manipulate configuration settings directly?). What if I wanted to find the value for $array['defaults']['page']? Without being able to access the array directly you cannot easily do this without writing function that would somehow recurse the array until it finds the correctly nested key that matches you request and returns its value, if it is found.
That is one idea. And I tried that. A lot. And as I went through trying that I began to realize that if I have a 200 member config array (say it has been continually built up) and I am accessing that from 20 different sources per application (think a framework hitting itself and a framework-hooked client hitting it as well) the the config array would essentially be traversed for each request coming in to the object. As I thought about this I began to think that it might be easier to traverse the array once and set the array into a levelized series of keys and values that map to each level of the config array so that a simple request to something like config::fetch('defaults:page'); would return ‘index’ (based on our example array).
So I set out to do that and I came up with something that has been immeasurably useful to me already. Essentially it is a class that allows for the setting of config parameters into the class in a way that makes each level accessible by its current location in the tree, separated by a delimiter (in my case the colon ‘:’). I know the following code needs a little more love, but it is doing what I want it to at the moment (I will be making changes that allow setting a custom delimiter and the whatnot):
<?php /** * Padlock - The PHP Application Developer's Library of Objects and Code Kits * * @category Padlock * @package Padlock * @author Robert Gonzalez <robert@everah.com> * @license PLEASE SEE ACCOMPANYING LICENSE TEXT OR THE {@see Padlock::license()} method * @version $Id: Config.php 33 2008-08-20 06:30:39Z robert $ */ /** * Padlock Configuration object abstract * * The Padlock Configuration object handles parsing of initial framework instructions * and individual configuration items. This class is designed to be extended by * concrete classes that are specific to a particular type of config implementation. * * @author Robert Gonzalez <robert@everah.com> * @category Padlock * @package Padlock * @version @package_version@ */ abstract class Padlock_Config { /** * String type config param constant */ const CONFIG_TYPE_STRING = 'string'; /** * Array type config param constant */ const CONFIG_TYPE_ARRAY = 'array'; /** * Object type config param constant */ const CONFIG_TYPE_OBJECT = 'object'; /** * Flag that tells the configuration class whether to merge new values or not * * @access protected * @var boolean */ protected static $_merge = true; /** * The framework configuration array * * @access protected * @var array */ protected static $_config = array(); /** * The framework configuration array in the raw * * @access protected * @var array */ protected static $_configRaw = array(); /** * Object constructor * * This is final and private - basicaly this class is meant to never be * instantiated. * * @access private */ final private function __construct() { trigger_error('The Config object cannot be instantiated', E_USER_ERROR); } /** * Abstracted child method that requires definition within child classes * * @access public * @param mixed $config Source of the configuration collection */ abstract public static function setFrom($config); /** * Sets up and loads a configuration collection into the framework * * If the $config param is an array the array will be loaded into the config * class as a merge. It was also be traversed and exploded out into each * component of the array as a map to the end element value of the array * path. * * If the $config param is an object the object will be traversed as an * array and set into the config class as an array and a mapped array. * * If the $config param is a string it will be treated as though it were a * file name and handled accordingly. * * @access public * @param mixed $config Configuration to process */ public static function setup($config) { switch(gettype($config)) { case self::CONFIG_TYPE_ARRAY: case self::CONFIG_TYPE_OBJECT: Padlock_Config_Array::setFrom($config); break; case self::CONFIG_TYPE_STRING: Padlock_Config_File::setFrom($config); break; } // Set it now self::_setConfig(self::$_configRaw); } /** * Appends the config setup with more config params * * @access public * @param array $config More config params to set, in the form of an array */ public static function append($config) { self::_setRaw($config); self::_setConfig(self::$_configRaw); } /** * Sets a single config property and value * * This method will not set null values as null values have special meaning * throughout the framework. Essentially if the value is null then it means * there is no known config setting for the name/property. Nulls are returned * when names/props cannot be found. * * @param string $name Config property to set * @param mixed $value Value for this config property */ public static function set($name, $value, $merge = true) { /** * Before anything we do we must check a value. * * Null values are special to the config class in that a NULL will be * literally translated to nonexitence. So if the value being passed is * null any searching for it will return null anyway. */ if ($value === null) { return false; } /** * If we are merging OR if the config item is not currently set then we * handle that first and call it done. */ if ($merge || self::$_merge || !self::has($name)) { self::append(array($name => $value)); return true; } /** * The only this could mean is that the value is null or the name * already lives and is not being merged. */ return false; } /** * Checks existence of a config property * * This will return true for null values of a property. This might appear * counterintuitive until you consider that we are just checking if there * is a property set in this object. Yes, the property can be set as a false * or a null. It will be without value, but it will be set, which is what * we are asking about. * * @access public * @param string $name Config property to check * @return boolean True if set, false otherwise */ public static function has($name) { return is_string($name) && array_key_exists($name, self::$_config); } /** * Gets a property value * * @access public * @param string $name Property to get the value for * @return mixed */ public static function get($name) { // Return what is being asked for, or null return self::has($name) ? self::$_config[$name] : null; } /** * Fetches the entire configuration array from this class * * This is a useful convenience method meant to allow the fetching of the * config settings array and use outside of this class. It can also be used * to set these configs into other objects. * * @access public * @return array Entire configuration array */ public static function fetch() { return self::$_config; } /** * Tells the class to turn merging of values on or off. * * This can be useful when the need to override or not override comes up as * this method allows for changing of the config merge argument. By default * this class will merge all set values. * * @access public * @param boolean $on Flag setting to pass to the class */ public static function setMerge($on = true) { self::$_merge = (bool) $on; } /** * Sets a series of properties from an array of properties * * @access protected * @param array $source Array to set values from * @param string $key Name of the key to append to the config stack */ protected static function _setConfig($source, $key = null) { // This should only be pushed if there is an array that isn't empty if (is_array($source) && !empty($source)) { // Loop through the array and start setting stuff foreach ($source as $k => $v) { /** * This is where the setting magic takes place, putting the * hierarchy together for the entire config tree. */ $newkey = $key === null ? $k : "$key:$k"; // Set the new values into the config self::$_config[$newkey] = $v; // Run through this process again until we need not do this self::_setConfig($v, $newkey); } } } /** * Sets the raw config array data for use later, if it is needed * * @access public * @param array $config Config array to set into the raw array */ protected static function _setRaw($config) { if (is_array($config) || is_object($config)) { self::$_configRaw = array_merge(self::$_configRaw, (array) $config); } } } |
You might notice that the method Padlock_Config::setup() is where everything takes place. Because this is essentially a deciding method I have included the Padlock_Config_Array class so you can see what happens when you load an array into the Padock_Config class for setting.
<?php /** * Padlock - The PHP Application Developer's Library of Objects and Code Kits * * @category Padlock * @package Padlock_Config * @author Robert Gonzalez <robert@everah.com> * @license PLEASE SEE ACCOMPANYING LICENSE TEXT OR THE {@see Padlock::license()} method * @version $Id: Array.php 32 2008-08-20 00:59:12Z robert $ */ /** * Padlock Configuration Array handler class * * The Padlock Configuration Array handling class handles setting of config params * from an array. The nature of this handler builds the configuration array into * tiers that are made up of other segments of the initial data set so that the * hierarchy of config elements is easily fetched. * * @author Robert Gonzalez <robert@everah.com> * @category Padlock * @package Padlock_Config * @version @package_version@ */ class Padlock_Config_Array extends Padlock_Config { /** * Sets a series of properties from an array of properties * * @access public * @param array|object $config Array or object to set values from */ public static function setFrom($config) { self::_setRaw($config); } } |
Please keep in mind a few things about this: 1) I have an autoload method registered in the Padlock superclass that handles loading of support files, so you won’t see includes and requires anywhere in here, and 2) The file class has considerably more code than the array class.
To use this, all you do is create your array and pass that to the Padlock_Config::setup() method. After that you can use Padlock_Config::get('path:to:a:nested:config') to get to one, or use Padlock_Config::fetch() to get all of them in the config class’ core config array. For reference, that array would now look like:
array(21) {
["defaults"]=>
array(2) {
["page"]=>
string(5) "index"
["action"]=>
string(7) "default"
}
["defaults:page"]=>
string(5) "index"
["defaults:action"]=>
string(7) "default"
["routes"]=>
array(3) {
[0]=>
string(10) "controller"
[1]=>
string(4) "view"
[2]=>
string(5) "model"
}
["routes:0"]=>
string(10) "controller"
["routes:1"]=>
string(4) "view"
["routes:2"]=>
string(5) "model"
["application"]=>
string(5) "Bilbo"
["nested"]=>
array(3) {
["levels"]=>
array(3) {
[1]=>
string(5) "floor"
[2]=>
string(6) "garage"
[3]=>
string(6) "canopy"
}
["names"]=>
array(3) {
[1]=>
string(6) "Hectar"
[2]=>
string(5) "Jonas"
[3]=>
string(4) "Phil"
}
["named"]=>
array(3) {
["floor"]=>
string(6) "Hectar"
["garage"]=>
string(5) "Jonas"
["canopy"]=>
string(4) "Phil"
}
}
["nested:levels"]=>
array(3) {
[1]=>
string(5) "floor"
[2]=>
string(6) "garage"
[3]=>
string(6) "canopy"
}
["nested:levels:1"]=>
string(5) "floor"
["nested:levels:2"]=>
string(6) "garage"
["nested:levels:3"]=>
string(6) "canopy"
["nested:names"]=>
array(3) {
[1]=>
string(6) "Hectar"
[2]=>
string(5) "Jonas"
[3]=>
string(4) "Phil"
}
["nested:names:1"]=>
string(6) "Hectar"
["nested:names:2"]=>
string(5) "Jonas"
["nested:names:3"]=>
string(4) "Phil"
["nested:named"]=>
array(3) {
["floor"]=>
string(6) "Hectar"
["garage"]=>
string(5) "Jonas"
["canopy"]=>
string(4) "Phil"
}
["nested:named:floor"]=>
string(6) "Hectar"
["nested:named:garage"]=>
string(5) "Jonas"
["nested:named:canopy"]=>
string(4) "Phil"
}
I hope this is useful for someone in some capacity. I played with this for a little while before getting it right. But now it is something that I am using all over the place.
Good luck with your coding and happy PHPing (did that sound weird or what?)!