From e58af04377b4b7b5de06719e2865a230e1995001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Wed, 23 Apr 2014 19:52:37 +0200 Subject: [PATCH 1/4] Allow C-style delimiters --- README.markdown | 186 ++++++++++++++++++++--------------- src/IniParser.php | 40 ++++++-- tests/Test/IniParserTest.php | 12 +++ tests/fixtures/fixture11.ini | 2 + 4 files changed, 154 insertions(+), 86 deletions(-) create mode 100644 tests/fixtures/fixture11.ini diff --git a/README.markdown b/README.markdown index 1f48b4b..ecbb4bd 100644 --- a/README.markdown +++ b/README.markdown @@ -6,6 +6,24 @@ IniParser is a simple parser for complex INI files, providing a number of extra **IMPORTANT:** IniParser should be considered beta-quality, and there may still be bugs. Feel free to open an issue or submit a pull request, and I'll take a look at it! +## Installing by [Composer](https://getcomposer.org) + +Set your `composer.json` file to have : + +```json +{ + "require": { + "austinhyde/iniparser": "dev-master" + } +} +``` + +Then install the dependencies : + +```shell +composer install +``` + ## An Example Standard INI files look like this: @@ -18,13 +36,15 @@ Standard INI files look like this: And when parsed with PHP's built-in `parse_ini_string()` or `parse_ini_file()`, looks like - array( - 'key' => 'value', - 'another_key' => 'another value', - 'section_name' => array( - 'a_sub_key' => 'yet another value' - ) +```php +array( + 'key' => 'value', + 'another_key' => 'another value', + 'section_name' => array( + 'a_sub_key' => 'yet another value' ) +) +``` This is great when you just want a simple configuration file, but here is a super-charged INI file that you might find in the wild: @@ -55,39 +75,41 @@ And when parsed with IniParser: You get the following structure: - array( - 'environment' => 'testing', - 'testing' => array( - 'debug' => '1', - 'database' => array( - 'connection' => 'mysql:host=127.0.0.1', - 'name' => 'test', - 'username' => '', - 'password' => '' - ), - 'secrets' => array('1','2','3') +```php +array( + 'environment' => 'testing', + 'testing' => array( + 'debug' => '1', + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'test', + 'username' => '', + 'password' => '' ), - 'staging' => array( - 'debug' => '1', - 'database' => array( - 'connection' => 'mysql:host=127.0.0.1', - 'name' => 'stage', - 'username' => 'staging', - 'password' => '12345' - ), - 'secrets' => array('1','2','3') + 'secrets' => array('1','2','3') + ), + 'staging' => array( + 'debug' => '1', + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'stage', + 'username' => 'staging', + 'password' => '12345' ), - 'production' => array( - 'debug' => '', - 'database' => array( - 'connection' => 'mysql:host=127.0.0.1', - 'name' => 'production', - 'username' => 'root', - 'password' => '12345' - ), - 'secrets' => array('1','2','3') - ) + 'secrets' => array('1','2','3') + ), + 'production' => array( + 'debug' => '', + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'production', + 'username' => 'root', + 'password' => '12345' + ), + 'secrets' => array('1','2','3') ) +) +``` ## Supported Features @@ -122,26 +144,24 @@ Besides arrays, you can create dictionaries and more complex structures using JS This turns into an array like: - array ( - 'boss' => - array ( - 'name' => 'John', - 'age' => 42, - ), - 'staff' => - array ( - 0 => - array ( - 'name' => 'Mark', - 'age' => 35, - ), - 1 => - array ( - 'name' => 'Bill', - 'age' => 44, - ), - ), - ); +```php +array( + 'boss' => array( + 'name' => 'John', + 'age' => 42 + ), + 'staff' => array( + array ( + 'name' => 'Mark', + 'age' => 35, + ), + array ( + 'name' => 'Bill', + 'age' => 44, + ), + ), +) +``` **NOTE:** Remember to wrap the JSON strings in single quotes for a correct analysis. The JSON names must be enclosed in double quotes and trailing commas are not allowed. @@ -155,15 +175,17 @@ IniParser allows you to treat properties as associative arrays: This turns into an array like: - array ( - 'person' => array ( - 'age' => 42, - 'name' => array ( - 'first' => 'John', - 'last' => 'Doe' - ) +```php +array ( + 'person' => array ( + 'age' => 42, + 'name' => array ( + 'first' => 'John', + 'last' => 'Doe' ) ) +) +``` ### Section Inheritance @@ -183,16 +205,18 @@ During the inheritance process, if a key ends in a `+`, the merge behavior chang would be parsed into the following: - array ( - 'parent' => array ( - 'arr' => array('a','b','c'), - 'val' => 'foo' - ), - 'child' => array ( - 'arr' => array('a','b','c','x','y','z'), - 'val' => 'foobar' - ) +```php +array( + 'parent' => array( + 'arr' => array('a','b','c'), + 'val' => 'foo' + ), + 'child' => array( + 'arr' => array('a','b','c','x','y','z'), + 'val' => 'foobar' ) +) +``` *If you can think of a more useful operation than concatenation for non-array types, please open an issue* @@ -204,16 +228,20 @@ Finally, it is possible to inherit from the special `^` section, representing th Parses to: - array ( - 'foo' => 'bar', - 'sect' => array ( - 'foo' => 'bar' - ) +```php +array ( + 'foo' => 'bar', + 'sect' => array ( + 'foo' => 'bar' ) +) +``` ### ArrayObject As an added bonus, IniParser also allows you to access the values OO-style: - echo $config->production->database->connection; // output: mysql:host=127.0.0.1 - echo $config->staging->debug; // output: 1 \ No newline at end of file +```php +echo $config->production->database->connection; // output: mysql:host=127.0.0.1 +echo $config->staging->debug; // output: 1 +``` diff --git a/src/IniParser.php b/src/IniParser.php index e8092f0..02d60a2 100644 --- a/src/IniParser.php +++ b/src/IniParser.php @@ -40,6 +40,12 @@ class IniParser { */ public $include_original_sections = false; + /** + * Parse C-like delimiters in strings (\r\n\t) + * @var boolean + */ + public $parse_delimiters = true; + /** * Disable array literal parsing */ @@ -60,7 +66,7 @@ class IniParser { * Array literals parse mode * @var int */ - public $array_literals_behaviour = self::PARSE_JSON; + public $array_literals_behavior = self::PARSE_SIMPLE; /** * @param string $file @@ -174,7 +180,7 @@ private function parseKeys(array $arr) { $output = $this->getArrayValue(); $append_regex = '/\s*\+\s*$/'; foreach ($arr as $k => $v) { - if (is_array($v)) { + if (is_array($v) && FALSE === strpos($k, '.')) { // this element represents a section; recursively parse the value $output[$k] = $this->parseKeys($v); } else { @@ -205,7 +211,10 @@ private function parseKeys(array $arr) { } // parse value - $value = $this->parseValue($v); + $value = $v; + if (!is_array($v)) { + $value = $this->parseValue($v); + } if ($append && $current !== null) { if (is_array($value)) { @@ -224,6 +233,20 @@ private function parseKeys(array $arr) { return $output; } + + /** + * Callback for replace delimiters regex + * @param array $matches + * @return string + */ + protected function replaceDelimiter($matches) { + switch ($matches[0]) { + case '\\n':return "\n"; + case '\\t':return "\t"; + case '\\r':return "\r"; + default:return $matches[0]; + } + } /** * Parses and formats the value in a key-value pair @@ -233,7 +256,11 @@ private function parseKeys(array $arr) { * @return mixed */ protected function parseValue($value) { - switch ($this->array_literals_behaviour) { + if ($this->parse_delimiters && !is_numeric($value)) {//parse_ini_string treats all values as strings, even numeric ones + $value = preg_replace_callback('/(?array_literals_behavior) { case self::PARSE_JSON: if (in_array(substr($value, 0, 1), array('[', '{')) && in_array(substr($value, -1), array(']', '}'))) { if (defined('JSON_BIGINT_AS_STRING')) { @@ -246,9 +273,8 @@ protected function parseValue($value) { return $output; } } - //try regex parser for simple estructures not JSON-compatible (ex: colors = [blue, green, red]) - - + // fallthrough + // try regex parser for simple estructures not JSON-compatible (ex: colors = [blue, green, red]) case self::PARSE_SIMPLE: // if the value looks like [a,b,c,...], interpret as array if (preg_match('/^\[\s*.*?(?:\s*,\s*.*?)*\s*\]$/', trim($value))) { diff --git a/tests/Test/IniParserTest.php b/tests/Test/IniParserTest.php index 9f46b74..15c97a1 100644 --- a/tests/Test/IniParserTest.php +++ b/tests/Test/IniParserTest.php @@ -87,6 +87,18 @@ public function testArrayObject() $this->assertEquals('world', $configObj->prod->hello); } + /** + * Test delimiter parsing + * + * @return void + */ + public function testDelimiters() + { + $configObj = $this->getConfig('fixture11.ini'); + + $this->assertEquals("Lorem \n ipsum \r dolor \t sit \\n amet, \\r consectetur \\t adipiscing elit.", $configObj->helloworld->lorem); + } + /** * Make sure stacked configuration settings are always 'ArrayObject'. * diff --git a/tests/fixtures/fixture11.ini b/tests/fixtures/fixture11.ini new file mode 100644 index 0000000..d5e00a1 --- /dev/null +++ b/tests/fixtures/fixture11.ini @@ -0,0 +1,2 @@ +[helloworld] +lorem = "Lorem \n ipsum \r dolor \t sit \\n amet, \\r consectetur \\t adipiscing elit." \ No newline at end of file From 9298410aee718a9cca8f63247fcbaed44adde65f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Wed, 23 Apr 2014 19:56:21 +0200 Subject: [PATCH 2/4] Allow C-style delimiters --- src/IniParser.php | 4 +- tests/Test/IniParserTest.php | 161 +++++++++++++++++++++++++---------- tests/fixtures/fixture11.ini | 5 +- tests/fixtures/fixture12.ini | 2 + 4 files changed, 122 insertions(+), 50 deletions(-) create mode 100644 tests/fixtures/fixture12.ini diff --git a/src/IniParser.php b/src/IniParser.php index 02d60a2..05d708f 100644 --- a/src/IniParser.php +++ b/src/IniParser.php @@ -233,7 +233,7 @@ private function parseKeys(array $arr) { return $output; } - + /** * Callback for replace delimiters regex * @param array $matches @@ -259,7 +259,7 @@ protected function parseValue($value) { if ($this->parse_delimiters && !is_numeric($value)) {//parse_ini_string treats all values as strings, even numeric ones $value = preg_replace_callback('/(?array_literals_behavior) { case self::PARSE_JSON: if (in_array(substr($value, 0, 1), array('[', '{')) && in_array(substr($value, -1), array(']', '}'))) { diff --git a/tests/Test/IniParserTest.php b/tests/Test/IniParserTest.php index 15c97a1..b0a51db 100644 --- a/tests/Test/IniParserTest.php +++ b/tests/Test/IniParserTest.php @@ -40,17 +40,14 @@ public function testParser() $configObj = $this->getConfig('fixture01.ini'); $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); - $this->assertArrayHasKey('production', $config); - - $productionConfig = $config['production']; - - $this->assertArrayHasKey('hello', $productionConfig); - $this->assertArrayHasKey('super', $productionConfig); - - $super = $productionConfig['super']; + $expected = array( + 'production' => array( + 'hello' => 'world', + 'super' => array('funny' => 'config') + ) + ); - $this->assertArrayHasKey('funny', $super); - $this->assertEquals('config', $super['funny']); + $this->assertSame($expected, $config); } /** @@ -63,10 +60,16 @@ public function testInheritance() $configObj = $this->getConfig('fixture02.ini'); $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); - $this->assertArrayHasKey('prod', $config); - $this->assertArrayHasKey('dev', $config); + $expected = array( + 'prod' => array( + 'hello' => 'world' + ), + 'dev' => array( + 'hello' => 'world' + ) + ); - $this->assertSame($config['prod'], $config['dev']); + $this->assertSame($expected, $config); } /** @@ -94,7 +97,7 @@ public function testArrayObject() */ public function testDelimiters() { - $configObj = $this->getConfig('fixture11.ini'); + $configObj = $this->getConfig('fixture12.ini'); $this->assertEquals("Lorem \n ipsum \r dolor \t sit \\n amet, \\r consectetur \\t adipiscing elit.", $configObj->helloworld->lorem); } @@ -183,27 +186,41 @@ public function testComplex() $configObj = $this->getConfig('fixture03.ini'); $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); - $this->assertArrayHasKey('environment', $config); - $this->assertEquals('testing', $config['environment']); - - $this->assertArrayHasKey('testing', $config); - $this->assertArrayHasKey('staging', $config); - $this->assertArrayHasKey('production', $config); - - $confTesting = $config['testing']; - $confStaging = $config['staging']; - $confProd = $config['production']; - - $this->assertEquals('', $confTesting['database']['username']); - $this->assertEquals('staging', $confStaging['database']['username']); - $this->assertEquals('root', $confProd['database']['username']); - - $this->assertEmpty($confTesting['database']['password']); - $this->assertEquals($confStaging['database']['password'], $confProd['database']['password']); + $expected = array( + 'environment' => 'testing', + 'testing' => array( + 'debug' => true, + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'test', + 'username' => '', + 'password' => '' + ), + 'secrets' => array(1, 2, 3) + ), + 'staging' => array( + 'debug' => true, + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'stage', + 'username' => 'staging', + 'password' => 12345 + ), + 'secrets' => array(1, 2, 3) + ), + 'production' => array( + 'debug' => false, + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'production', + 'username' => 'root', + 'password' => 12345 + ), + 'secrets' => array(1, 2, 3) + ) + ); - $this->assertEquals('1', $confTesting['debug']); - $this->assertEquals('1', $confStaging['debug']); - $this->assertEquals('', $confProd['debug']); + $this->assertEquals($expected, $config); } /** @@ -262,8 +279,7 @@ public function testUseArrayObject() { * * @return void */ - public function testArrayWithZeroAsKey() - { + public function testArrayWithZeroAsKey() { $configObj = $this->getConfig('fixture09.ini'); $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); @@ -279,25 +295,22 @@ public function testArrayWithZeroAsKey() * @return void */ public function testJson() { - $configObj = $this->getConfig('fixture10.ini'); + $configObj = $this->getConfig('fixture10.ini', + array('array_literals_behavior' => IniParser::PARSE_JSON)); $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); $this->assertObjectHasAttribute('people', $configObj); $array = array( - 'boss' => - array( + 'boss' => array( 'name' => 'John', 'age' => 42, ), - 'staff' => - array( - 0 => + 'staff' => array( array( 'name' => 'Mark', 'age' => 35, ), - 1 => array( 'name' => 'Bill', 'age' => 44, @@ -305,7 +318,60 @@ public function testJson() { ), ); - $this->assertEquals($config['people'], $array); + $this->assertEquals($array, $config['people']); + } + + public function testNoPropertyNesting() { + $configObj = $this->getConfig('fixture03.ini', + array('property_nesting' => false)); + $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); + + $expected = array( + 'environment' => 'testing', + 'testing' => array( + 'debug' => true, + 'database.connection' => 'mysql:host=127.0.0.1', + 'database.name' => 'test', + 'database.username' => '', + 'database.password' => '', + 'secrets' => array(1, 2, 3) + ), + 'staging' => array( + 'debug' => true, + 'database.connection' => 'mysql:host=127.0.0.1', + 'database.name' => 'stage', + 'database.username' => 'staging', + 'database.password' => 12345, + 'secrets' => array(1, 2, 3) + ), + 'production' => array( + 'debug' => false, + 'database.connection' => 'mysql:host=127.0.0.1', + 'database.name' => 'production', + 'database.username' => 'root', + 'database.password' => 12345, + 'secrets' => array(1, 2, 3) + ) + ); + + $this->assertEquals($expected, $config); + } + + /** + * Tests that appending to a potentially non-existent array works as expected + * when also using property nesting. + * + * @return void + */ + public function testNestedArrayAppend() + { + $configObj = $this->getConfig('fixture11.ini'); + $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); + $expected = array(1, 2, 'c'); + + $this->assertArrayHasKey('nesting', $config); + $this->assertArrayHasKey('a', $config['nesting']); + $this->assertEquals($expected, $config['nesting']['a']); } /** @@ -315,9 +381,12 @@ public function testJson() { * * @return array */ - protected function getConfig($file) + protected function getConfig($file, $options = array()) { $parser = new IniParser(BASE_DIR . '/tests/fixtures/' . $file); + foreach ($options as $key => $value) { + $parser->$key = $value; + } $config = $parser->parse(); return $config; } @@ -339,4 +408,4 @@ protected function phpUnitDoesntUnderstandArrayObject(ArrayObject $config) } return $arr; } -} \ No newline at end of file +} diff --git a/tests/fixtures/fixture11.ini b/tests/fixtures/fixture11.ini index d5e00a1..36422e0 100644 --- a/tests/fixtures/fixture11.ini +++ b/tests/fixtures/fixture11.ini @@ -1,2 +1,3 @@ -[helloworld] -lorem = "Lorem \n ipsum \r dolor \t sit \\n amet, \\r consectetur \\t adipiscing elit." \ No newline at end of file +nesting.a[] = 1 +nesting.a[] = 2 +nesting.a[] = "c" diff --git a/tests/fixtures/fixture12.ini b/tests/fixtures/fixture12.ini new file mode 100644 index 0000000..d5e00a1 --- /dev/null +++ b/tests/fixtures/fixture12.ini @@ -0,0 +1,2 @@ +[helloworld] +lorem = "Lorem \n ipsum \r dolor \t sit \\n amet, \\r consectetur \\t adipiscing elit." \ No newline at end of file From 4ac57eb013094c0a06d0784593bf4d64a1b36d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Wed, 23 Apr 2014 20:12:38 +0200 Subject: [PATCH 3/4] Synced repo --- src/IniParser.php | 24 ------------------------ tests/Test/IniParserTest.php | 12 ------------ tests/fixtures/fixture12.ini | 2 -- 3 files changed, 38 deletions(-) delete mode 100644 tests/fixtures/fixture12.ini diff --git a/src/IniParser.php b/src/IniParser.php index 05d708f..893a941 100644 --- a/src/IniParser.php +++ b/src/IniParser.php @@ -40,12 +40,6 @@ class IniParser { */ public $include_original_sections = false; - /** - * Parse C-like delimiters in strings (\r\n\t) - * @var boolean - */ - public $parse_delimiters = true; - /** * Disable array literal parsing */ @@ -234,20 +228,6 @@ private function parseKeys(array $arr) { return $output; } - /** - * Callback for replace delimiters regex - * @param array $matches - * @return string - */ - protected function replaceDelimiter($matches) { - switch ($matches[0]) { - case '\\n':return "\n"; - case '\\t':return "\t"; - case '\\r':return "\r"; - default:return $matches[0]; - } - } - /** * Parses and formats the value in a key-value pair * @@ -256,10 +236,6 @@ protected function replaceDelimiter($matches) { * @return mixed */ protected function parseValue($value) { - if ($this->parse_delimiters && !is_numeric($value)) {//parse_ini_string treats all values as strings, even numeric ones - $value = preg_replace_callback('/(?array_literals_behavior) { case self::PARSE_JSON: if (in_array(substr($value, 0, 1), array('[', '{')) && in_array(substr($value, -1), array(']', '}'))) { diff --git a/tests/Test/IniParserTest.php b/tests/Test/IniParserTest.php index b0a51db..ef6645f 100644 --- a/tests/Test/IniParserTest.php +++ b/tests/Test/IniParserTest.php @@ -90,18 +90,6 @@ public function testArrayObject() $this->assertEquals('world', $configObj->prod->hello); } - /** - * Test delimiter parsing - * - * @return void - */ - public function testDelimiters() - { - $configObj = $this->getConfig('fixture12.ini'); - - $this->assertEquals("Lorem \n ipsum \r dolor \t sit \\n amet, \\r consectetur \\t adipiscing elit.", $configObj->helloworld->lorem); - } - /** * Make sure stacked configuration settings are always 'ArrayObject'. * diff --git a/tests/fixtures/fixture12.ini b/tests/fixtures/fixture12.ini deleted file mode 100644 index d5e00a1..0000000 --- a/tests/fixtures/fixture12.ini +++ /dev/null @@ -1,2 +0,0 @@ -[helloworld] -lorem = "Lorem \n ipsum \r dolor \t sit \\n amet, \\r consectetur \\t adipiscing elit." \ No newline at end of file From 0c2818cfea876db974a7219694133c36de8cfb4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Mar=C3=ADn?= Date: Wed, 23 Apr 2014 20:16:11 +0200 Subject: [PATCH 4/4] Added C-style delimiters --- README.markdown | 192 ++++++++++++++++++++++++----------- src/IniParser.php | 40 ++++++-- tests/Test/IniParserTest.php | 192 ++++++++++++++++++++++++++++------- tests/fixtures/fixture10.ini | 16 +++ tests/fixtures/fixture11.ini | 3 + tests/fixtures/fixture12.ini | 2 + 6 files changed, 341 insertions(+), 104 deletions(-) create mode 100644 tests/fixtures/fixture10.ini create mode 100644 tests/fixtures/fixture11.ini create mode 100644 tests/fixtures/fixture12.ini diff --git a/README.markdown b/README.markdown index 032496b..ecbb4bd 100644 --- a/README.markdown +++ b/README.markdown @@ -6,6 +6,24 @@ IniParser is a simple parser for complex INI files, providing a number of extra **IMPORTANT:** IniParser should be considered beta-quality, and there may still be bugs. Feel free to open an issue or submit a pull request, and I'll take a look at it! +## Installing by [Composer](https://getcomposer.org) + +Set your `composer.json` file to have : + +```json +{ + "require": { + "austinhyde/iniparser": "dev-master" + } +} +``` + +Then install the dependencies : + +```shell +composer install +``` + ## An Example Standard INI files look like this: @@ -18,13 +36,15 @@ Standard INI files look like this: And when parsed with PHP's built-in `parse_ini_string()` or `parse_ini_file()`, looks like - array( - 'key' => 'value', - 'another_key' => 'another value', - 'section_name' => array( - 'a_sub_key' => 'yet another value' - ) +```php +array( + 'key' => 'value', + 'another_key' => 'another value', + 'section_name' => array( + 'a_sub_key' => 'yet another value' ) +) +``` This is great when you just want a simple configuration file, but here is a super-charged INI file that you might find in the wild: @@ -55,39 +75,41 @@ And when parsed with IniParser: You get the following structure: - array( - 'environment' => 'testing', - 'testing' => array( - 'debug' => '1', - 'database' => array( - 'connection' => 'mysql:host=127.0.0.1', - 'name' => 'test', - 'username' => '', - 'password' => '' - ), - 'secrets' => array('1','2','3') +```php +array( + 'environment' => 'testing', + 'testing' => array( + 'debug' => '1', + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'test', + 'username' => '', + 'password' => '' ), - 'staging' => array( - 'debug' => '1', - 'database' => array( - 'connection' => 'mysql:host=127.0.0.1', - 'name' => 'stage', - 'username' => 'staging', - 'password' => '12345' - ), - 'secrets' => array('1','2','3') + 'secrets' => array('1','2','3') + ), + 'staging' => array( + 'debug' => '1', + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'stage', + 'username' => 'staging', + 'password' => '12345' ), - 'production' => array( - 'debug' => '', - 'database' => array( - 'connection' => 'mysql:host=127.0.0.1', - 'name' => 'production', - 'username' => 'root', - 'password' => '12345' - ), - 'secrets' => array('1','2','3') - ) + 'secrets' => array('1','2','3') + ), + 'production' => array( + 'debug' => '', + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'production', + 'username' => 'root', + 'password' => '12345' + ), + 'secrets' => array('1','2','3') ) +) +``` ## Supported Features @@ -99,6 +121,50 @@ You can directly create arrays using the syntax `[a, b, c]` on the right hand si **NOTE:** At the moment, quoted strings inside array literals have undefined behavior. +### Dictionaries and complex structures + +Besides arrays, you can create dictionaries and more complex structures using JSON syntax. For example, you can use: + + people = '{ + "boss": { + "name": "John", + "age": 42 + }, + "staff": [ + { + "name": "Mark", + "age": 35 + }, + { + "name": "Bill", + "age": 44 + } + ] + }' + +This turns into an array like: + +```php +array( + 'boss' => array( + 'name' => 'John', + 'age' => 42 + ), + 'staff' => array( + array ( + 'name' => 'Mark', + 'age' => 35, + ), + array ( + 'name' => 'Bill', + 'age' => 44, + ), + ), +) +``` + +**NOTE:** Remember to wrap the JSON strings in single quotes for a correct analysis. The JSON names must be enclosed in double quotes and trailing commas are not allowed. + ### Property Nesting IniParser allows you to treat properties as associative arrays: @@ -109,15 +175,17 @@ IniParser allows you to treat properties as associative arrays: This turns into an array like: - array ( - 'person' => array ( - 'age' => 42, - 'name' => array ( - 'first' => 'John', - 'last' => 'Doe' - ) +```php +array ( + 'person' => array ( + 'age' => 42, + 'name' => array ( + 'first' => 'John', + 'last' => 'Doe' ) ) +) +``` ### Section Inheritance @@ -137,16 +205,18 @@ During the inheritance process, if a key ends in a `+`, the merge behavior chang would be parsed into the following: - array ( - 'parent' => array ( - 'arr' => array('a','b','c'), - 'val' => 'foo' - ), - 'child' => array ( - 'arr' => array('a','b','c','x','y','z'), - 'val' => 'foobar' - ) +```php +array( + 'parent' => array( + 'arr' => array('a','b','c'), + 'val' => 'foo' + ), + 'child' => array( + 'arr' => array('a','b','c','x','y','z'), + 'val' => 'foobar' ) +) +``` *If you can think of a more useful operation than concatenation for non-array types, please open an issue* @@ -158,16 +228,20 @@ Finally, it is possible to inherit from the special `^` section, representing th Parses to: - array ( - 'foo' => 'bar', - 'sect' => array ( - 'foo' => 'bar' - ) +```php +array ( + 'foo' => 'bar', + 'sect' => array ( + 'foo' => 'bar' ) +) +``` ### ArrayObject As an added bonus, IniParser also allows you to access the values OO-style: - echo $config->production->database->connection; // output: mysql:host=127.0.0.1 - echo $config->staging->debug; // output: 1 \ No newline at end of file +```php +echo $config->production->database->connection; // output: mysql:host=127.0.0.1 +echo $config->staging->debug; // output: 1 +``` diff --git a/src/IniParser.php b/src/IniParser.php index e8092f0..05d708f 100644 --- a/src/IniParser.php +++ b/src/IniParser.php @@ -40,6 +40,12 @@ class IniParser { */ public $include_original_sections = false; + /** + * Parse C-like delimiters in strings (\r\n\t) + * @var boolean + */ + public $parse_delimiters = true; + /** * Disable array literal parsing */ @@ -60,7 +66,7 @@ class IniParser { * Array literals parse mode * @var int */ - public $array_literals_behaviour = self::PARSE_JSON; + public $array_literals_behavior = self::PARSE_SIMPLE; /** * @param string $file @@ -174,7 +180,7 @@ private function parseKeys(array $arr) { $output = $this->getArrayValue(); $append_regex = '/\s*\+\s*$/'; foreach ($arr as $k => $v) { - if (is_array($v)) { + if (is_array($v) && FALSE === strpos($k, '.')) { // this element represents a section; recursively parse the value $output[$k] = $this->parseKeys($v); } else { @@ -205,7 +211,10 @@ private function parseKeys(array $arr) { } // parse value - $value = $this->parseValue($v); + $value = $v; + if (!is_array($v)) { + $value = $this->parseValue($v); + } if ($append && $current !== null) { if (is_array($value)) { @@ -225,6 +234,20 @@ private function parseKeys(array $arr) { return $output; } + /** + * Callback for replace delimiters regex + * @param array $matches + * @return string + */ + protected function replaceDelimiter($matches) { + switch ($matches[0]) { + case '\\n':return "\n"; + case '\\t':return "\t"; + case '\\r':return "\r"; + default:return $matches[0]; + } + } + /** * Parses and formats the value in a key-value pair * @@ -233,7 +256,11 @@ private function parseKeys(array $arr) { * @return mixed */ protected function parseValue($value) { - switch ($this->array_literals_behaviour) { + if ($this->parse_delimiters && !is_numeric($value)) {//parse_ini_string treats all values as strings, even numeric ones + $value = preg_replace_callback('/(?array_literals_behavior) { case self::PARSE_JSON: if (in_array(substr($value, 0, 1), array('[', '{')) && in_array(substr($value, -1), array(']', '}'))) { if (defined('JSON_BIGINT_AS_STRING')) { @@ -246,9 +273,8 @@ protected function parseValue($value) { return $output; } } - //try regex parser for simple estructures not JSON-compatible (ex: colors = [blue, green, red]) - - + // fallthrough + // try regex parser for simple estructures not JSON-compatible (ex: colors = [blue, green, red]) case self::PARSE_SIMPLE: // if the value looks like [a,b,c,...], interpret as array if (preg_match('/^\[\s*.*?(?:\s*,\s*.*?)*\s*\]$/', trim($value))) { diff --git a/tests/Test/IniParserTest.php b/tests/Test/IniParserTest.php index cee96e9..6544bbd 100644 --- a/tests/Test/IniParserTest.php +++ b/tests/Test/IniParserTest.php @@ -40,17 +40,14 @@ public function testParser() $configObj = $this->getConfig('fixture01.ini'); $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); - $this->assertArrayHasKey('production', $config); - - $productionConfig = $config['production']; - - $this->assertArrayHasKey('hello', $productionConfig); - $this->assertArrayHasKey('super', $productionConfig); - - $super = $productionConfig['super']; - - $this->assertArrayHasKey('funny', $super); - $this->assertEquals('config', $super['funny']); + $expected = array( + 'production' => array( + 'hello' => 'world', + 'super' => array('funny' => 'config') + ) + ); + + $this->assertSame($expected, $config); } /** @@ -63,10 +60,16 @@ public function testInheritance() $configObj = $this->getConfig('fixture02.ini'); $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); - $this->assertArrayHasKey('prod', $config); - $this->assertArrayHasKey('dev', $config); + $expected = array( + 'prod' => array( + 'hello' => 'world' + ), + 'dev' => array( + 'hello' => 'world' + ) + ); - $this->assertSame($config['prod'], $config['dev']); + $this->assertSame($expected, $config); } /** @@ -100,6 +103,18 @@ public function testArrayObjectComplex() $this->assertEquals('mysql:host=127.0.0.1', $configObj->production->database->connection); } + /** + * Test delimiter parsing + * + * @return void + */ + public function testDelimiters() + { + $configObj = $this->getConfig('fixture12.ini'); + + $this->assertEquals("Lorem \n ipsum \r dolor \t sit \\n amet, \\r consectetur \\t adipiscing elit.", $configObj->helloworld->lorem); + } + /** * Test that array literals are parsed correctly * @@ -171,27 +186,41 @@ public function testComplex() $configObj = $this->getConfig('fixture03.ini'); $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); - $this->assertArrayHasKey('environment', $config); - $this->assertEquals('testing', $config['environment']); - - $this->assertArrayHasKey('testing', $config); - $this->assertArrayHasKey('staging', $config); - $this->assertArrayHasKey('production', $config); - - $confTesting = $config['testing']; - $confStaging = $config['staging']; - $confProd = $config['production']; - - $this->assertEquals('', $confTesting['database']['username']); - $this->assertEquals('staging', $confStaging['database']['username']); - $this->assertEquals('root', $confProd['database']['username']); - - $this->assertEmpty($confTesting['database']['password']); - $this->assertEquals($confStaging['database']['password'], $confProd['database']['password']); - - $this->assertEquals('1', $confTesting['debug']); - $this->assertEquals('1', $confStaging['debug']); - $this->assertEquals('', $confProd['debug']); + $expected = array( + 'environment' => 'testing', + 'testing' => array( + 'debug' => true, + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'test', + 'username' => '', + 'password' => '' + ), + 'secrets' => array(1, 2, 3) + ), + 'staging' => array( + 'debug' => true, + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'stage', + 'username' => 'staging', + 'password' => 12345 + ), + 'secrets' => array(1, 2, 3) + ), + 'production' => array( + 'debug' => false, + 'database' => array( + 'connection' => 'mysql:host=127.0.0.1', + 'name' => 'production', + 'username' => 'root', + 'password' => 12345 + ), + 'secrets' => array(1, 2, 3) + ) + ); + + $this->assertEquals($expected, $config); } /** @@ -250,8 +279,7 @@ public function testUseArrayObject() { * * @return void */ - public function testArrayWithZeroAsKey() - { + public function testArrayWithZeroAsKey() { $configObj = $this->getConfig('fixture09.ini'); $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); @@ -261,6 +289,91 @@ public function testArrayWithZeroAsKey() $this->assertEquals((array)$configObj->helloworld->hello, array(1 => 'world', 0 => 'hello')); } + /** + * Tests that json is parsed as expected + * + * @return void + */ + public function testJson() { + $configObj = $this->getConfig('fixture10.ini', + array('array_literals_behavior' => IniParser::PARSE_JSON)); + $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); + + $this->assertObjectHasAttribute('people', $configObj); + + $array = array( + 'boss' => array( + 'name' => 'John', + 'age' => 42, + ), + 'staff' => array( + array( + 'name' => 'Mark', + 'age' => 35, + ), + array( + 'name' => 'Bill', + 'age' => 44, + ), + ), + ); + + $this->assertEquals($array, $config['people']); + } + + public function testNoPropertyNesting() { + $configObj = $this->getConfig('fixture03.ini', + array('property_nesting' => false)); + $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); + + $expected = array( + 'environment' => 'testing', + 'testing' => array( + 'debug' => true, + 'database.connection' => 'mysql:host=127.0.0.1', + 'database.name' => 'test', + 'database.username' => '', + 'database.password' => '', + 'secrets' => array(1, 2, 3) + ), + 'staging' => array( + 'debug' => true, + 'database.connection' => 'mysql:host=127.0.0.1', + 'database.name' => 'stage', + 'database.username' => 'staging', + 'database.password' => 12345, + 'secrets' => array(1, 2, 3) + ), + 'production' => array( + 'debug' => false, + 'database.connection' => 'mysql:host=127.0.0.1', + 'database.name' => 'production', + 'database.username' => 'root', + 'database.password' => 12345, + 'secrets' => array(1, 2, 3) + ) + ); + + $this->assertEquals($expected, $config); + } + + /** + * Tests that appending to a potentially non-existent array works as expected + * when also using property nesting. + * + * @return void + */ + public function testNestedArrayAppend() + { + $configObj = $this->getConfig('fixture11.ini'); + $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); + $expected = array(1, 2, 'c'); + + $this->assertArrayHasKey('nesting', $config); + $this->assertArrayHasKey('a', $config['nesting']); + $this->assertEquals($expected, $config['nesting']['a']); + } + /** * Create a config array (from the given fixture). * @@ -268,9 +381,12 @@ public function testArrayWithZeroAsKey() * * @return array */ - protected function getConfig($file) + protected function getConfig($file, $options = array()) { $parser = new IniParser(BASE_DIR . '/tests/fixtures/' . $file); + foreach ($options as $key => $value) { + $parser->$key = $value; + } $config = $parser->parse(); return $config; } diff --git a/tests/fixtures/fixture10.ini b/tests/fixtures/fixture10.ini new file mode 100644 index 0000000..7786630 --- /dev/null +++ b/tests/fixtures/fixture10.ini @@ -0,0 +1,16 @@ +people = '{ + "boss": { + "name": "John", + "age": 42 + }, + "staff": [ + { + "name": "Mark", + "age": 35 + }, + { + "name": "Bill", + "age": 44 + } + ] + }' \ No newline at end of file diff --git a/tests/fixtures/fixture11.ini b/tests/fixtures/fixture11.ini new file mode 100644 index 0000000..36422e0 --- /dev/null +++ b/tests/fixtures/fixture11.ini @@ -0,0 +1,3 @@ +nesting.a[] = 1 +nesting.a[] = 2 +nesting.a[] = "c" diff --git a/tests/fixtures/fixture12.ini b/tests/fixtures/fixture12.ini new file mode 100644 index 0000000..d5e00a1 --- /dev/null +++ b/tests/fixtures/fixture12.ini @@ -0,0 +1,2 @@ +[helloworld] +lorem = "Lorem \n ipsum \r dolor \t sit \\n amet, \\r consectetur \\t adipiscing elit." \ No newline at end of file