diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index 45efced3c89ac..502f3983f80d9 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -32,12 +32,13 @@ class Inline * @param string $value A YAML string * @param Boolean $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise * @param Boolean $objectSupport true if object support is enabled, false otherwise + * @param Boolean $objectForMap true if maps should return a stdClass instead of array() * + * @throws Exception\ParseException * @return array A PHP array representing the YAML string * - * @throws ParseException */ - public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false) + public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false) { self::$exceptionOnInvalidType = $exceptionOnInvalidType; self::$objectSupport = $objectSupport; @@ -56,11 +57,11 @@ public static function parse($value, $exceptionOnInvalidType = false, $objectSup $i = 0; switch ($value[0]) { case '[': - $result = self::parseSequence($value, $i); + $result = self::parseSequence($value, $i, $objectForMap); ++$i; break; case '{': - $result = self::parseMapping($value, $i); + $result = self::parseMapping($value, $i, $objectForMap); ++$i; break; default: @@ -259,14 +260,14 @@ private static function parseQuotedScalar($scalar, &$i) /** * Parses a sequence to a YAML string. * - * @param string $sequence + * @param string $sequence * @param integer &$i + * @param Boolean $objectForMap true if maps should return a stdClass instead of array() * + * @throws Exception\ParseException * @return string A YAML string - * - * @throws ParseException When malformed inline YAML string is parsed */ - private static function parseSequence($sequence, &$i = 0) + private static function parseSequence($sequence, &$i = 0, $objectForMap = false) { $output = array(); $len = strlen($sequence); @@ -277,11 +278,11 @@ private static function parseSequence($sequence, &$i = 0) switch ($sequence[$i]) { case '[': // nested sequence - $output[] = self::parseSequence($sequence, $i); + $output[] = self::parseSequence($sequence, $i, $objectForMap); break; case '{': // nested mapping - $output[] = self::parseMapping($sequence, $i); + $output[] = self::parseMapping($sequence, $i, $objectForMap); break; case ']': return $output; @@ -295,7 +296,8 @@ private static function parseSequence($sequence, &$i = 0) if (!$isQuoted && false !== strpos($value, ': ')) { // embedded mapping? try { - $value = self::parseMapping('{'.$value.'}'); + $j = 0; + $value = self::parseMapping('{'.$value.'}', $j, $objectForMap); } catch (\InvalidArgumentException $e) { // no, it's not } @@ -315,14 +317,15 @@ private static function parseSequence($sequence, &$i = 0) /** * Parses a mapping to a YAML string. * - * @param string $mapping + * @param string $mapping * @param integer &$i + * @param Boolean $objectForMap true if maps should return a stdClass instead of array() * + * @throws Exception\ParseException * @return string A YAML string * - * @throws ParseException When malformed inline YAML string is parsed */ - private static function parseMapping($mapping, &$i = 0) + private static function parseMapping($mapping, &$i = 0, $objectForMap = false) { $output = array(); $len = strlen($mapping); @@ -336,6 +339,10 @@ private static function parseMapping($mapping, &$i = 0) ++$i; continue 2; case '}': + if (true === $objectForMap) { + return (object) $output; + } + return $output; } @@ -344,16 +351,18 @@ private static function parseMapping($mapping, &$i = 0) // value $done = false; + + while ($i < $len) { switch ($mapping[$i]) { case '[': // nested sequence - $output[$key] = self::parseSequence($mapping, $i); + $output[$key] = self::parseSequence($mapping, $i, $objectForMap); $done = true; break; case '{': // nested mapping - $output[$key] = self::parseMapping($mapping, $i); + $output[$key] = self::parseMapping($mapping, $i, $objectForMap); $done = true; break; case ':': diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index af5018f2026f9..5fe0db1c55e7b 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -22,6 +22,23 @@ public function testParse() } } + public function testParseWithMapObjects() + { + foreach ($this->getTestsForMapObjectParse() as $yaml => $value) { + $actual = Inline::parse($yaml, false, false, true); + if (true === is_object($value)) { + $this->assertInstanceOf(get_class($value), $actual); + $this->assertEquals(get_object_vars($value), get_object_vars($actual)); + } elseif (true === is_array($value)) { + $this->assertEquals($value, $actual); + $this->assertMixedArraysSame($value, $actual); + } else { + $this->assertSame($value, $actual); + } + } + + } + public function testDump() { $testsForDump = $this->getTestsForDump(); @@ -182,6 +199,85 @@ protected function getTestsForParse() ); } + protected function getTestsForMapObjectParse() + { + return array( + '' => '', + 'null' => null, + 'false' => false, + 'true' => true, + '12' => 12, + '-12' => -12, + '"quoted string"' => 'quoted string', + "'quoted string'" => 'quoted string', + '12.30e+02' => 12.30e+02, + '0x4D2' => 0x4D2, + '02333' => 02333, + '.Inf' => -log(0), + '-.Inf' => log(0), + "'686e444'" => '686e444', + '686e444' => 646e444, + '123456789123456789123456789123456789' => '123456789123456789123456789123456789', + '"foo\r\nbar"' => "foo\r\nbar", + "'foo#bar'" => 'foo#bar', + "'foo # bar'" => 'foo # bar', + "'#cfcfcf'" => '#cfcfcf', + '::form_base.html.twig' => '::form_base.html.twig', + + '2007-10-30' => mktime(0, 0, 0, 10, 30, 2007), + '2007-10-30T02:59:43Z' => gmmktime(2, 59, 43, 10, 30, 2007), + '2007-10-30 02:59:43 Z' => gmmktime(2, 59, 43, 10, 30, 2007), + '1960-10-30 02:59:43 Z' => gmmktime(2, 59, 43, 10, 30, 1960), + '1730-10-30T02:59:43Z' => gmmktime(2, 59, 43, 10, 30, 1730), + + '"a \\"string\\" with \'quoted strings inside\'"' => 'a "string" with \'quoted strings inside\'', + "'a \"string\" with ''quoted strings inside'''" => 'a "string" with \'quoted strings inside\'', + + // sequences + // urls are no key value mapping. see #3609. Valid yaml "key: value" mappings require a space after the colon + '[foo, http://urls.are/no/mappings, false, null, 12]' => array('foo', 'http://urls.are/no/mappings', false, null, 12), + '[ foo , bar , false , null , 12 ]' => array('foo', 'bar', false, null, 12), + '[\'foo,bar\', \'foo bar\']' => array('foo,bar', 'foo bar'), + + // mappings + '{foo:bar,bar:foo,false:false,null:null,integer:12}' => (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12), + '{ foo : bar, bar : foo, false : false, null : null, integer : 12 }' => (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12), + '{foo: \'bar\', bar: \'foo: bar\'}' => (object) array('foo' => 'bar', 'bar' => 'foo: bar'), + '{\'foo\': \'bar\', "bar": \'foo: bar\'}' => (object) array('foo' => 'bar', 'bar' => 'foo: bar'), + '{\'foo\'\'\': \'bar\', "bar\"": \'foo: bar\'}' => (object) array('foo\'' => 'bar', "bar\"" => 'foo: bar'), + '{\'foo: \': \'bar\', "bar: ": \'foo: bar\'}' => (object) array('foo: ' => 'bar', "bar: " => 'foo: bar'), + + // nested sequences and mappings + '[foo, [bar, foo]]' => array('foo', array('bar', 'foo')), + '[foo, {bar: foo}]' => array('foo', (object) array('bar' => 'foo')), + '{ foo: {bar: foo} }' => (object) array('foo' => (object) array('bar' => 'foo')), + '{ foo: [bar, foo] }' => (object) array('foo' => array('bar', 'foo')), + + '[ foo, [ bar, foo ] ]' => array('foo', array('bar', 'foo')), + + '[{ foo: {bar: foo} }]' => array((object) array('foo' => (object) array('bar' => 'foo'))), + + '[foo, [bar, [foo, [bar, foo]], foo]]' => array('foo', array('bar', array('foo', array('bar', 'foo')), 'foo')), + + '[foo, {bar: foo, foo: [foo, {bar: foo}]}, [foo, {bar: foo}]]' => array('foo', (object) array('bar' => 'foo', 'foo' => array('foo', (object) array('bar' => 'foo'))), array('foo', (object) array('bar' => 'foo'))), + + '[foo, bar: { foo: bar }]' => array('foo', '1' => (object) array('bar' => (object) array('foo' => 'bar'))), + '[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']' => array('foo', '@foo.baz', (object) array('%foo%' => 'foo is %foo%', 'bar' => '%foo%',), true, '@service_container',), + + '{}' => new \stdClass(), + '{ foo : bar, bar : {} }' => (object) array('foo' => 'bar', 'bar' => new \stdClass()), + '{ foo : [], bar : {} }' => (object) array('foo' => array(), 'bar' => new \stdClass()), + '{foo: \'bar\', bar: {} }' => (object) array('foo' => 'bar', 'bar' => new \stdClass()), + '{\'foo\': \'bar\', "bar": {}}' => (object) array('foo' => 'bar', 'bar' => new \stdClass()), + '{\'foo\': \'bar\', "bar": \'{}\'}' => (object) array('foo' => 'bar', 'bar' => '{}'), + + '[foo, [{}, {}]]' => array('foo', array(new \stdClass(), new \stdClass())), + '[foo, [[], {}]]' => array('foo', array(array(), new \stdClass())), + '[foo, [[{}, {}], {}]]' => array('foo', array(array(new \stdClass(), new \stdClass()), new \stdClass())), + '[foo, {bar: {}}]' => array('foo', '1' => (object) array('bar' => new \stdClass())), + ); + } + protected function getTestsForDump() { return array( @@ -229,4 +325,27 @@ protected function getTestsForDump() '[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']' => array('foo', '@foo.baz', array('%foo%' => 'foo is %foo%', 'bar' => '%foo%',), true, '@service_container',), ); } + + protected function assertMixedArraysSame($a, $b) + { + + foreach ($a as $key => $value) { + if (array_key_exists($key, $b)) { + if (is_array($value)) { + $this->assertMixedArraysSame($value, $b[$key]); + } else { + if (true === is_object($value)) { + $this->assertEquals($value, $b[$key]); + $this->assertInstanceOf(get_class($value), $b[$key]); + $this->assertEquals(get_object_vars($value), get_object_vars($b[$key])); + } else { + $this->assertSame($value, $b[$key]); + } + } + } else { + $this->assertFail(); + } + } + + } }