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..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 9f46b74..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']; + $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); } /** @@ -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']); + $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); } /** @@ -250,8 +279,7 @@ public function testUseArrayObject() { * * @return void */ - public function testArrayWithZeroAsKey() - { + public function testArrayWithZeroAsKey() { $configObj = $this->getConfig('fixture09.ini'); $config = $this->phpUnitDoesntUnderstandArrayObject($configObj); @@ -267,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, @@ -293,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']); } /** @@ -303,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; } @@ -327,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 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