History | Log In     View a printable version of the current page.  
Issue Details (XML | Word | Printable)

Key: ZF-998
Type: New Feature New Feature
Status: Resolved Resolved
Resolution: Fixed
Priority: Minor Minor
Assignee: Rob Allen
Reporter: Romain Lalaut
Votes: 3
Watchers: 2
Operations

If you were logged in you would be able to see more operations.
Google issue summary
Zend Framework

Possibility to merge two Zend_Config objects

Created: 01/Mar/07 04:19 PM   Updated: 23/Jan/08 02:40 PM
Component/s: Zend_Config
Affects Version/s: 0.9.0
Fix Version/s: 1.0.2

Time Tracking:
Not Specified

File Attachments: 1. Text File zf-998.patch (1 kb)


Tags:
Participants: Darby Felton, Matthew Ratzloff, Mike Simons, Rob Allen, Romain Lalaut, Uros and Wil Sinclair


 Description  « Hide
I propose the ability to merge two Zend_Config objects.
The purpose is to overload a default configuration with another.

For example, this is the default conf inside a package

$default_conf_array = array(
    'wonderful_feature_but_instable' => false,
    'some_files' => array(
        'foo'=>'dir/foo.xml',
        'bar'=>'dir/bar.xml',
    ),
);
$default_conf = new Zend_Config($default_conf_array);

Then this is my own conf outside the previous package

$user_conf_array = array(
    'wonderful_feature_but_instable'=>true,
    'some_files' => array(
       'bar' => 'myDir/bar.xml',
       'baz' => 'myDir/baz.xml,
    ),
);
$user_conf = new Zend_Config($user_conf_array);

Now i want to overload the package default conf
the result could be into a new object

$merged_conf = Zend_Config::merge($default_conf, $user_conf);

or updated with a method

$default_conf->merge($user_conf);

anyway the expected result should be

array(    
    'wonderful_feature_but_instable'=>true,
    'some_files' => array(
       'foo' => 'dir/foo.xml',
       'bar' => 'myDir/bar.xml',
       'baz' => 'myDir/baz.xml,
    ),
);

It will be very useful for my needs (hoping that i'm not alone
If you like the idea but don't have enough time, i can do the work.



 All   Comments   Work Log   Change History   FishEye   Crucible      Sort Order: Ascending order - Click to sort in descending order
Rob Allen - 02/Mar/07 01:03 AM
Doesn't something like:
$merged = new Zend_Config(array_merge($default_conf->asArray(), $user_conf->asArray()));

work?


Romain Lalaut - 02/Mar/07 07:36 AM
No because array_merge() is not recursive... and array_merge_recursive() has a bad behaviour with the keys (see comments http://php.net/manual/en/function.array-merge-recursive.php)

I used this static method for a personal project

public static function mergeArrays( &$offset1, $offset2 )
	{
		$type1 = is_array($offset1);
		$type2 = is_array($offset2);
		
		// if both offsets don't have same type : problem
		if( $type1 != $type2 )
				throw new MergeArraysException( $offset1, $offset2 );
		
		// both are not an array (string, int, ...) ? it means we must replace the current value
		if( !$type1 ) // && !$type2
			$offset1 = $offset2;
		
		// both are an array : recursivity 
		else
			foreach ( $offset2 as $key2=>$foo )
				self::mergeArrays($offset1[$key2], $offset2[$key2]);
	}

Uros - 05/Mar/07 07:51 AM
Hi,

I modified Config like this


public function __construct($array, $allowModifications = false)
{
$this->_allowModifications = (boolean) $allowModifications;
$this->_loadedSection = null;
$this->_index = 0;
$this->_data = array();
foreach ($array as $key => $value) {
if ($this->_isValidKeyName($key)) {
if (is_array($value)) { $this->_data[$key] = new Zend_Config($value, $this->_allowModifications); } else { $this->_data[$key] = $value; }
} elseif ( $value instanceof Zend_Config ) {
foreach ($value as $key => $v) {
if ($this->_isValidKeyName($key)) {
if (is_array($v)) { $this->_data[$key] = new Zend_Config($v, $this->_allowModifications); } else { $this->_data[$key] = $v; }
} else { throw new Zend_Config_Exception("Invalid key: '$key'"); }
}
} else { throw new Zend_Config_Exception("Invalid key: '$key'"); }
}
$this->_count = count($this->_data);

{/code}

Darby Felton - 07/Mar/07 03:42 PM
Why does inter-section inheritance not fulfill your need to inherit and override configuration data? I'm unsure from the problem description that you could not structure your configuration data into sections:
; ini format example

[development]
database.host = 'db.dev.example.com'
database.name = 'mydata'
; ...

[production : development]
database.host = 'db.example.com'
; ...

In the above example, you could load the development or the production section. If you load the production section, it inherits all the values from development, and, as you can see, database.host has been overridden to a different server. The database.name value, however, would remain equal to mydata.

Zend_Config was specifically designed to support config data inheritance; for what reasons can you not use the built-in support?


Rob Allen - 30/May/07 04:18 PM
I have resolved this issue as discussion has stalled as to the benefits a merge system would have over using inter-section inheritance.

Matthew Ratzloff - 29/Jun/07 02:34 PM
This functionality is useful when you want a settings file that can be checked in to your repository and one that is developer/environment-specific, but that you don't want checked in. Having two separate files is essential, but having multiple Zend_Config objects is a hassle.

For example, say I have the basic configuration file:

[production]
; settings...

[development : production]
; other settings...

In groups, an individual developer will need to supply his own settings that might override development settings. For example, they will need to change database.port to their database instance. You don't want this file checked in accidentally and somehow missed by QA--that would be bad. Thus, separate configurations that are merged together.

Here's one way to do it. I've implemented this functionality based on a function written by Keith Devens (i.e., _mergeArrays(), posted on his blog). I'm not sure about the licensing, but he has his own license. I have contacted him to get clarification.

In Zend_Config:

/**
     * Merge two configurations.
     *
     * @see    _mergeArrays()
     * @param  Zend_Config $otherConfig Configuration to merge with
     * @return Dna_Configuration
     */
    public function mergeWith(Zend_Config $otherConfig)
    {
        $array = $this->_mergeArrays($this->toArray(), $otherConfig->toArray());
        $this->__construct($array, $this->_allowModifications);

        return $this;
    }

    /**
     * Recursively merges two arrays and returns the result.
     *
     * Where there is duplication, values from the second array overwrite 
     * values from the first.  This differs from array_merge_recursive() and 
     * the + operator, which append those values into a new array.
     *
     * @param  array $a First array
     * @param  array $b Second array (supercedes first array)
     * @return array Merged array
     */
    protected function _mergeArrays($a, $b)
    {
        if (is_array($a) and is_array($b) and !array_key_exists(0, $a)) {
            while (list($key) = each($b)) {
                if (array_key_exists($key, $a)) {
                    $a[$key] = $this->_mergeArrays($a[$key], $b[$key]);
                } else {
                    $a[$key] = $b[$key];
                }
            }
        } else {
            $a = $b;
        }
        return $a;
    }

Darby Felton - 29/Jun/07 05:28 PM
Alternatively, you can solve the problem by having each developer with his own settings that inherit from the "development" section. If committing such information is undesirable, this is also easily solved by using svn:ignore. There is a configuration file that is in the repository and used by default, but if a copy of it exists at a known location, then the copy is used (and ignored by SVN). (Similar functionality is available in other SCM programs like CVS.)

This is the same methodology used in the test suite, where TestConfiguration.php.dist is the default configuration, and users can copy this file to TestConfiguration.php, making whatever changes they like, and SVN ignores the file so that it is not committed with other work.


Mike Simons - 13/Sep/07 03:37 PM
I don't like hacking classes; Here is a non-destructive OO approach
class Ixulai_Config_Compound extends Zend_Config
{
	public function __construct(Array $configs, $allowModifications = false)
	{
		$compoundConfig = array();

		foreach($configs as $config) {

			if(!($config instanceof Zend_Config)) {
				throw new Zend_Config_Exception('All elements of config array must be an instance of Zend_Config');
			}

			$compoundConfig = $this->_mergeRecursive($compoundConfig, $config->toArray());
		}

		parent::__construct($compoundConfig, $allowModifications);
	}

	protected function _mergeRecursive($array1, $array2)
	{
		if(is_array($array1) && is_array($array2)) {
			$keys = array_keys($array2);
			foreach($keys as $key) {
				if(isset($array1[$key])) {
					$array1[$key] = $this->_mergeRecursive($array1[$key], $array2[$key]);
				} else {
                    $array1[$key] = $array2[$key];
                }
			}
		} else {
            $array1 = $array2;
		}

        return $array1;
	}
}

I believe I may have borrowed the mergeRecursive function from the same place as the one above though mine looks a bit different. I've had it kicking around for a while. It doesn't matter; any recursive merge function would work, even array_merge_recursive if thats what you need.

You can compound configs as many times as you like...

$config1 = new Zend_Config_Xml('test.xml');
$config2 = new Zend_Config_Ini('test.ini');
$config3 = new Zend_Config(array('moo'));
//Create a merge of 1 & 2
$config4 = new Ixulai_Config_Compound($config1, $config2);
//Create a merge of 3 with the result from merging 1 & 2
$config5 = new Ixulai_Config_Compound($config3, $config4);

Mike Simons - 13/Sep/07 03:41 PM
Doh; Examples above for config4 and 5 should have arguments in an array.
Thats what you get for not testing examples.

Darby Felton - 14/Sep/07 07:52 AM
Pasting from original message by Nico Edtinger:
<?php
function _fix_merge($array) {
    foreach($array as $k => $v) {
        if (!is_array($v)) {
            continue;
        }
        if (is_int(key($v))) {
            $array[$k] = $v[1];
        } else {
            $array[$k] = _fix_merge($v);
        }
    }
    
    return $array;
}

$config1 = new Zend_Config_Ini(...);
$config2 = new Zend_Config_Ini(...);
$config = new Zend_Config(_fix_merge(array_merge_recursive($config1->toArray(), $config2->toArray())));
?>

Rob Allen - 15/Sep/07 03:25 PM
Suggested patch:
/**
 * Merge another Zend_Config with this one. The items
 * in $merge will override the same named items in
 * the current config.
 *
 * @param Zend_Config $merge
 * @return Zend_Config
 */
public function merge(Zend_Config $merge)
{
    foreach($merge as $key => $item) {
        if(array_key_exists($key, $this->_data)) {
            if(get_class($item) == 'Zend_Config'  && get_class($this->$key) == 'Zend_Config') {
                $this->$key = $this->$key->merge($item);
            } else {
                $this->$key = $item;
            }
        } else {
            $this->$key = $item;
        }
    }
    
    return $this;
}

Test:

public function testMerge()
{
    $stdArray = array(
        'test_feature' => false,
        'some_files' => array(
            'foo'=>'dir/foo.xml',
            'bar'=>'dir/bar.xml',
        ),
        2 => 123,
    );
    $stdConfig = new Zend_Config($stdArray, true);
    
    $devArray = array(
        'test_feature'=>true,
        'some_files' => array(
           'bar' => 'myDir/bar.xml',
           'baz' => 'myDir/baz.xml',
        ),
        2 => 456,
    );
    $devConfig = new Zend_Config($devArray);
    
    $stdConfig->merge($devConfig);
    
    $this->assertTrue($stdConfig->test_feature);
    $this->assertEquals('myDir/bar.xml', $stdConfig->some_files->bar);
    $this->assertEquals('myDir/baz.xml', $stdConfig->some_files->baz);
    $this->assertEquals('dir/foo.xml', $stdConfig->some_files->foo);
    $this->assertEquals(456, $stdConfig->{2});
}

Anyone have any objections to me committing this to the trunk?

Regards,

Rob...


Darby Felton - 17/Sep/07 08:38 AM
I like this patch, Rob. Why did you choose get_class() over instanceof? The former approach does not support extending Zend_Config as easily, since one will have to override the merge() method in such cases where the method will be used in an extending class. Otherwise, I think this would be a nice, self-contained feature we could squeeze into 1.0.2.

Rob Allen - 17/Sep/07 09:45 AM
I forgot all about instanceof ! I'll change it this evening and get it onto the trunk.

Rob...


Rob Allen - 17/Sep/07 01:38 PM
Updated merge function:
public function merge(Zend_Config $merge)
{
    foreach($merge as $key => $item) {
        if(array_key_exists($key, $this->_data)) {
            if($item instanceof Zend_Config && $this->$key instanceof Zend_Config) {
                $this->$key = $this->$key->merge($item);
            } else {
                $this->$key = $item;
            }
        } else {
            $this->$key = $item;
        }
    }
    
    return $this;
}

Commited to trunk in svn revision 6387.


Rob Allen - 17/Sep/07 02:05 PM
Merged to 1.0.2 in r6389.

Matthew Ratzloff - 21/Sep/07 12:40 PM
Thanks, Rob. Now I can get rid of my custom class.

Wil Sinclair - 23/Jan/08 02:40 PM
Fixing Fix Version to follow issue tracker conventions.