diff --git a/lib/Cake/Configure/IniReader.php b/lib/Cake/Configure/IniReader.php index f47713294..debd685c6 100644 --- a/lib/Cake/Configure/IniReader.php +++ b/lib/Cake/Configure/IniReader.php @@ -16,13 +16,15 @@ * @since CakePHP(tm) v 2.0 * @license MIT License (http://www.opensource.org/licenses/mit-license.php) */ +App::uses('Hash', 'Utility'); /** - * Ini file configuration parser. Since IniReader uses parse_ini_file underneath, - * you should be aware that this class shares the same behavior, especially with - * regards to boolean and null values. + * Ini file configuration engine. * - * In addition to the native parse_ini_file features, IniReader also allows you + * Since IniReader uses parse_ini_file underneath, you should be aware that this + * class shares the same behavior, especially with regards to boolean and null values. + * + * In addition to the native `parse_ini_file` features, IniReader also allows you * to create nested array structures through usage of `.` delimited names. This allows * you to create nested arrays structures in an ini config file. For example: * @@ -126,6 +128,7 @@ class IniReader implements ConfigReaderInterface { if ($value === '') { $value = false; } + unset($values[$key]); if (strpos($key, '.') !== false) { $values = Hash::insert($values, $key, $value); } else { @@ -135,4 +138,47 @@ class IniReader implements ConfigReaderInterface { return $values; } +/** + * Dumps the state of Configure data into an ini formatted string. + * + * @param string $filename The filename on $this->_path to save into. + * @param array $data The data to convert to ini file. + * @return int Bytes saved. + */ + public function dump($filename, $data) { + $result = array(); + foreach ($data as $key => $value) { + if ($key[0] != '[') { + $result[] = "[$key]"; + } + if (is_array($value)) { + $keyValues = Hash::flatten($value, '.'); + foreach ($keyValues as $k => $v) { + $result[] = "$k = " . $this->_value($v); + } + } + } + $contents = join("\n", $result); + return file_put_contents($this->_path . $filename, $contents); + } + +/** + * Converts a value into the ini equivalent + * + * @param mixed $value to export. + * @return string String value for ini file. + */ + protected function _value($val) { + if ($val === null) { + return 'null'; + } + if ($val === true) { + return 'true'; + } + if ($val === false) { + return 'false'; + } + return (string)$val; + } + } diff --git a/lib/Cake/Configure/PhpReader.php b/lib/Cake/Configure/PhpReader.php index ed3d2e4b7..f5d961961 100644 --- a/lib/Cake/Configure/PhpReader.php +++ b/lib/Cake/Configure/PhpReader.php @@ -87,4 +87,17 @@ class PhpReader implements ConfigReaderInterface { return $config; } +/** + * Converts the provided $data into a string of PHP code that can + * be used saved into a file and loaded later. + * + * @param string $filename The filename to create on $this->_path. + * @param array $data Data to dump. + * @return int Bytes saved. + */ + public function dump($filename, $data) { + $contents = '_path . $filename, $contents); + } + } diff --git a/lib/Cake/Core/Configure.php b/lib/Cake/Core/Configure.php index 45e60fd2d..b90fcfc6d 100644 --- a/lib/Cake/Core/Configure.php +++ b/lib/Cake/Core/Configure.php @@ -278,6 +278,45 @@ class Configure { return self::write($values); } +/** + * Dump data currently in Configure into $filename. The serialization format + * is decided by the config reader attached as $config. For example, if the + * 'default' adapter is a PhpReader, the generated file will be a PHP + * configuration file loadable by the PhpReader. + * + * ## Usage + * + * Given that the 'default' reader is an instance of PhpReader. + * Save all data in Configure to the file `my_config.php`: + * + * `Configure::dump('my_config.php', 'default');` + * + * Save only the error handling configuration: + * + * `Configure::dump('error.php', 'default', array('Error', 'Exception');` + * + * @param string $key The identifier to create in the config adapter. + * This could be a filename or a cache key depending on the adapter being used. + * @param string $config The name of the configured adapter to dump data with. + * @param array $keys The name of the top-level keys you want to dump. + * This allows you save only some data stored in Configure. + * @return boolean success + * @throws ConfigureException if the adapter does not implement a `dump` method. + */ + public static function dump($key, $config = 'default', $keys = array()) { + if (empty(self::$_readers[$config])) { + throw new ConfigureException(__d('cake', 'There is no "%s" adapter.', $config)); + } + if (!method_exists(self::$_readers[$config], 'dump')) { + throw new ConfigureException(__d('cake', 'The "%s" adapter, does not have a dump() method.', $config)); + } + $values = self::$_values; + if (!empty($keys) && is_array($keys)) { + $values = array_intersect_key($values, array_flip($keys)); + } + return (bool)self::$_readers[$config]->dump($key, $values); + } + /** * Used to determine the current version of CakePHP. * diff --git a/lib/Cake/Test/Case/Configure/IniReaderTest.php b/lib/Cake/Test/Case/Configure/IniReaderTest.php index 6ce077fa2..e4b463450 100644 --- a/lib/Cake/Test/Case/Configure/IniReaderTest.php +++ b/lib/Cake/Test/Case/Configure/IniReaderTest.php @@ -21,11 +21,24 @@ App::uses('IniReader', 'Configure'); class IniReaderTest extends CakeTestCase { /** - * The test file that will be read. + * Test data to serialize and unserialize. * - * @var string + * @var array */ - public $file; + public $testData = array( + 'One' => array( + 'two' => 'value', + 'three' => array( + 'four' => 'value four' + ), + 'is_null' => null, + 'bool_false' => false, + 'bool_true' => true, + ), + 'Asset' => array( + 'timestamp' => 'force' + ), + ); /** * setup @@ -92,6 +105,8 @@ class IniReaderTest extends CakeTestCase { $this->assertTrue(isset($config['database']['db']['username'])); $this->assertEquals('mark', $config['database']['db']['username']); $this->assertEquals(3, $config['nesting']['one']['two']['three']); + $this->assertFalse(isset($config['database.db.username'])); + $this->assertFalse(isset($config['database']['db.username'])); } /** @@ -125,4 +140,49 @@ class IniReaderTest extends CakeTestCase { $config = $reader->read('nested'); $this->assertTrue($config['bools']['test_on']); } + +/** + * test dump method. + * + * @return void + */ + public function testDump() { + $reader = new IniReader(TMP); + $result = $reader->dump('test.ini', $this->testData); + $this->assertTrue($result > 0); + + $expected = <<assertTextEquals($expected, $result); + } + +/** + * Test that dump() makes files read() can read. + * + * @return void + */ + public function testDumpRead() { + $reader = new IniReader(TMP); + $reader->dump('test.ini', $this->testData); + $result = $reader->read('test.ini'); + unlink(TMP . 'test.ini'); + + $expected = $this->testData; + $expected['One']['is_null'] = false; + + $this->assertEquals($expected, $result); + } + } diff --git a/lib/Cake/Test/Case/Configure/PhpReaderTest.php b/lib/Cake/Test/Case/Configure/PhpReaderTest.php index 8531103a3..08982cb4e 100644 --- a/lib/Cake/Test/Case/Configure/PhpReaderTest.php +++ b/lib/Cake/Test/Case/Configure/PhpReaderTest.php @@ -20,6 +20,26 @@ App::uses('PhpReader', 'Configure'); class PhpReaderTest extends CakeTestCase { +/** + * Test data to serialize and unserialize. + * + * @var array + */ + public $testData = array( + 'One' => array( + 'two' => 'value', + 'three' => array( + 'four' => 'value four' + ), + 'is_null' => null, + 'bool_false' => false, + 'bool_true' => true, + ), + 'Asset' => array( + 'timestamp' => 'force' + ), + ); + /** * setup * @@ -96,4 +116,55 @@ class PhpReaderTest extends CakeTestCase { $this->assertTrue(isset($result['plugin_load'])); CakePlugin::unload(); } + +/** + * Test dumping data to PHP format. + * + * @return void + */ + public function testDump() { + $reader = new PhpReader(TMP); + $result = $reader->dump('test.php', $this->testData); + $this->assertTrue($result > 0); + $expected = << + array ( + 'two' => 'value', + 'three' => + array ( + 'four' => 'value four', + ), + 'is_null' => NULL, + 'bool_false' => false, + 'bool_true' => true, + ), + 'Asset' => + array ( + 'timestamp' => 'force', + ), +); +PHP; + $file = TMP . 'test.php'; + $contents = file_get_contents($file); + + unlink($file); + $this->assertTextEquals($expected, $contents); + } + +/** + * Test that dump() makes files read() can read. + * + * @return void + */ + public function testDumpRead() { + $reader = new PhpReader(TMP); + $reader->dump('test.php', $this->testData); + $result = $reader->read('test.php'); + unlink(TMP . 'test.php'); + + $this->assertEquals($this->testData, $result); + } + } diff --git a/lib/Cake/Test/Case/Core/ConfigureTest.php b/lib/Cake/Test/Case/Core/ConfigureTest.php index 705e83b95..7351f83f1 100644 --- a/lib/Cake/Test/Case/Core/ConfigureTest.php +++ b/lib/Cake/Test/Case/Core/ConfigureTest.php @@ -362,4 +362,47 @@ class ConfigureTest extends CakeTestCase { $this->assertNull(Configure::read('debug')); $this->assertNull(Configure::read('test')); } + +/** + * @expectedException ConfigureException + */ + public function testDumpNoAdapter() { + Configure::dump(TMP . 'test.php', 'does_not_exist'); + } + +/** + * test dump integrated with the PhpReader. + * + * @return void + */ + public function testDump() { + Configure::config('test_reader', new PhpReader(TMP)); + + $result = Configure::dump('config_test.php', 'test_reader'); + $this->assertTrue($result > 0); + $result = file_get_contents(TMP . 'config_test.php'); + $this->assertContains('assertContains('$config = ', $result); + @unlink(TMP . 'config_test.php'); + } + +/** + * Test dumping only some of the data. + * + * @return + */ + public function testDumpPartial() { + Configure::config('test_reader', new PhpReader(TMP)); + + $result = Configure::dump('config_test.php', 'test_reader', array('Error')); + $this->assertTrue($result > 0); + $result = file_get_contents(TMP . 'config_test.php'); + $this->assertContains('assertContains('$config = ', $result); + $this->assertContains('Error', $result); + $this->assertNotContains('debug', $result); + + @unlink(TMP . 'config_test.php'); + } + }