Configuration Object Help

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

Just wanted to add, I think think this is a rather good way of handling the configuration, but I am open to criticism and if you have any better ideas I’m listening.

Best regards, George