Hey,
I’m currently writing a framework to test myself and put all of my skills to use.
I’ve designed the following configuration object (it’s very messy, still under heavy dev)
class Configuration
{
protected
$_registered = array(),
$_loaded = null,
$_compiled = array();
public
$insert_before = false,
$insert_position = null,
$insert_if_exists = false,
$ignore = false;
public function __construct()
{
}
public function register($id, $path)
{
if (file_exists($path)) {
$this->_registered[$id] = $path;
}
}
public function get($config)
{
//lazy load config
$this->_load($config);
//compile config
$this->_compile($config);
//return config
return $this->_compiled[$config];
}
public function __get($config)
{
return $this->get($config);
}
protected function _load($filename)
{
//has this already been loaded?
if (isset($this->_loaded[$filename])) {
return;
}
//load
foreach ($this->_registered as $id => $path) {
if (file_exists($path.'/'.$filename.'.php')) {
$config = $this->_import($path.'/'.$filename.'.php');
if(!$this->ignore) {
$this->_loaded[$filename][$id] =
array('before' => $this->insert_before,
'existing' => $this->insert_position,
'if_exists' => $this->insert_if_exists,
'file_path' => $path,
'data' => $config);
}
$this->_reset();
}
}
}
protected function _reset()
{
$this->insert_before = false;
$this->insert_position = null;
$this->insert_if_exists = false;
$this->ignore = false;
}
protected function _compile($filename)
{
//has this already been compiled
if (isset($this->_compiled[$filename])) {
return;
}
//does this config file exist
if (!isset($this->_loaded[$filename])) {
return;
}
//set pre-order parts
$ordered = array();
$unordered = array();
$before_all = array();
$after_all = array();
//pre-order config files
foreach($this->_loaded[$filename] as $id => $data) {
if ($data['existing'] == '*') { // before or after
if($data['before']) { // before
$before_all[$id] = $data;
} else { // after
$after_all[$id] = $data;
}
} else if($data['existing'] == null) { // unordered
$unordered[$id] = $data;
} else { // ordered
if(isset($this->_loaded[$filename][$data['existing']])) {
$ordered[$id] = $data;
} else if (!$data['if_exists']) {
//TOOD should be start or end depending on before value
$unordered[$id] = $data;
}
}
}
$pre_compiled = array_merge($before_all, $unordered, $after_all);
//inject specifically ordered config files
$assembler =
function($ordered, &$pre_compiled) use(&$assembler) {
$unorderable = array();
//loop the ordered configs and injects them into $pre_compiled
foreach ($ordered as $id => $data) {
//can we find a reference point for this config
if (false === ($key = array_search($data['existing'], array_keys($pre_compiled)))) {
$unorderable[$id] = $data;
continue;
}
//before or after the reference point
if (!$data['before']) {
++$key;
}
//inject relative to reference point
$pre_compiled = array_merge(array_slice($pre_compiled, 0, $key), array($id => $data), array_slice($pre_compiled, $key));
}
//try again and hope the reference point now exists
//TODO if the reference point never exists, when do we break this loop??
if (!empty($unorderable)) {
$assembler($unorderable, $pre_compiled);
}
};
//run the assembler if there's anything to assemble
if (!empty($ordered) && !empty($pre_compiled)) {
$assembler($ordered, $pre_compiled);
}
//if pre_compiled contains some data
if (!empty($pre_compiled)) {
$compilable = array();
//compile it
foreach ($pre_compiled as $data) {
$compilable[] = $data['data'];
}
$this->_compiled[$filename] = $this->_merge($compilable);
echo '<h2>'.$filename.'</h2><pre>'; print_r($this->_compiled[$filename]); echo '</pre>';
} else {
$this->_compiled[$filename] = array();
}
}
protected function _merge($compilable)
{
return call_user_func_array('array_merge', $compilable);
}
protected function _import($file)
{
return include_once $file;
}
public function insertBefore($id)
{
$this->insert_before = true;
$this->insert_position = $id;
}
public function insertAfter($id)
{
$this->insert_before = false;
$this->insert_position = $id;
}
public function insertBeforeIfExists($id)
{
$this->insert_before = true;
$this->insert_position = $id;
$this->insert_if_exists = true;
}
public function insertAfterIfExists($id)
{
$this->insert_before = false;
$this->insert_position = $id;
$this->insert_if_exists = true;
}
public function ignoreMePlease()
{
$this->ignore = true;
}
}
It works by first registering configuration locations, like so
$config = new Configuration();
$config->register('System', /home/username/System/Configuration');
$config->register('Application', '/home/username/Application/Configuration');
$config->register('Example_Module', '/home/username/Application/Modules/Example/Configuration');
Getting a configuration value is done like this
$domain = $config->Website['domain_name'];
Website is the name of the configuration file e.g. /home/username/Application/Configuration/Website.php. The configuration file can exist at all registered locations.
The configuration file could look like this
namespace Application\\Configuration\\Website;
$this->insertAfter('System');
return array('domain_name' => 'www.example.com');
Assuming the same configuration file exists in /home/username/System/Configuration the returned arrays will be merged with the above taking precedence.
Hopefully that explains how it works well enough.
I now have a new requirement. Currently config files can only return arrays to be compiled, I also need to allow config files to update an object in the correct order.
Example
namespace Application\\Configuration\\Website;
$this->insertAfter('System');
$some_object->some_value = 'example_value';
The problem is that the way I load my configuration files would mean that $some_object is updated before ordering can take place. I think I could achieve the ordering if I returned an anonymous function instead.
Example
namespace Application\\Configuration\\Website;
$this->insertAfter('System');
return function(&$some_object) {
$some_object->some_value = 'example_value';
}
That way I could reorder the functions, then run them in the correct order. This could be done in the _merge() method of my configuration object.
protected function _merge($compilable, $type)
{
if ('array' == $type) {
return call_user_func_array('array_merge', $compilable);
} else if ('function' == $type) {
//run each function in $compilable in order
//passing reference to $some_object
}
}
The question is, how can I set a return type (array or function) for a configuration file?
What if the file at different levels returns a different type?
How can I tell the configuration object which object to pass through the anonymous functions?
Any help would be greatly appreciated. It’s probably really simple but I’ve ran out of brain power today.
Best regards, George