Permalink
Browse files

[Yaml] Added support for object maps

Previously, the parser treated maps ( {} ) the same as sets ( [] ).
Both were returned as PHP associative arrays. Since
these are distinct entities, this can cause considerably problems for
the users, especially when YAML is being serialized into another
format such as JSON.

This commit allows the user to enable object-map support via a third
parameter on the Parse method.  It defaults to `false`, which means
that this commit does not break backwards compatibility.

If the user enables object-map support, maps are represented
by stdClass() objects.  Sets remain as arrays.
  • Loading branch information...
1 parent 4c12b7b commit e2d5468b4a2035401bd2d3122100d9dc1a70b804 @polyfractal polyfractal committed with nicolas-grekas Jan 23, 2014
Showing with 152 additions and 23 deletions.
  1. +33 −23 src/Symfony/Component/Yaml/Inline.php
  2. +119 −0 src/Symfony/Component/Yaml/Tests/InlineTest.php
@@ -29,15 +29,16 @@ class Inline
/**
* Converts a YAML string to a PHP array.
*
- * @param string $value A YAML string
- * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
- * @param bool $objectSupport true if object support is enabled, false otherwise
+ * @param string $value A YAML string
+ * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
+ * @param bool $objectSupport true if object support is enabled, false otherwise
+ * @param bool $objectForMap true if maps should return a stdClass instead of array()
*
* @return array A PHP array representing the YAML string
*
- * @throws ParseException
+ * @throws Exception\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:
@@ -181,9 +182,9 @@ private static function dumpArray($value, $exceptionOnInvalidType, $objectSuppor
/**
* Parses a scalar to a YAML string.
*
- * @param scalar $scalar
- * @param string $delimiters
- * @param array $stringDelimiters
+ * @param scalar $scalar
+ * @param string $delimiters
+ * @param array $stringDelimiters
* @param int &$i
* @param bool $evaluate
*
@@ -232,7 +233,7 @@ public static function parseScalar($scalar, $delimiters = null, $stringDelimiter
* Parses a quoted scalar to YAML.
*
* @param string $scalar
- * @param int &$i
+ * @param int &$i
*
* @return string A YAML string
*
@@ -261,14 +262,15 @@ private static function parseQuotedScalar($scalar, &$i)
/**
* Parses a sequence to a YAML string.
*
- * @param string $sequence
+ * @param string $sequence
* @param int &$i
+ * @param bool $objectForMap true if maps should return a stdClass instead of array()
*
* @return string A YAML string
*
- * @throws ParseException When malformed inline YAML string is parsed
+ * @throws Exception\ParseException
*/
- private static function parseSequence($sequence, &$i = 0)
+ private static function parseSequence($sequence, &$i = 0, $objectForMap = false)
{
$output = array();
$len = strlen($sequence);
@@ -279,11 +281,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;
@@ -297,7 +299,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
}
@@ -317,14 +320,15 @@ private static function parseSequence($sequence, &$i = 0)
/**
* Parses a mapping to a YAML string.
*
- * @param string $mapping
+ * @param string $mapping
* @param int &$i
+ * @param bool $objectForMap true if maps should return a stdClass instead of array()
*
* @return string A YAML string
*
- * @throws ParseException When malformed inline YAML string is parsed
+ * @throws Exception\ParseException
*/
- private static function parseMapping($mapping, &$i = 0)
+ private static function parseMapping($mapping, &$i = 0, $objectForMap = false)
{
$output = array();
$len = strlen($mapping);
@@ -338,6 +342,10 @@ private static function parseMapping($mapping, &$i = 0)
++$i;
continue 2;
case '}':
+ if (true === $objectForMap) {
+ return (object) $output;
+ }
+
return $output;
}
@@ -346,11 +354,13 @@ private static function parseMapping($mapping, &$i = 0)
// value
$done = false;
+
+
while ($i < $len) {
switch ($mapping[$i]) {
case '[':
// nested sequence
- $value = self::parseSequence($mapping, $i);
+ $value = self::parseSequence($mapping, $i, $objectForMap);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
@@ -361,7 +371,7 @@ private static function parseMapping($mapping, &$i = 0)
break;
case '{':
// nested mapping
- $value = self::parseMapping($mapping, $i);
+ $value = self::parseMapping($mapping, $i, $objectForMap);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
@@ -399,7 +409,7 @@ private static function parseMapping($mapping, &$i = 0)
/**
* Evaluates scalars and replaces magic values.
*
- * @param string $scalar
+ * @param string $scalar
*
* @return string A YAML string
*/
@@ -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(
@@ -228,4 +324,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();
+ }
+ }
+
+ }
}

1 comment on commit e2d5468

@jbinfo
jbinfo commented on e2d5468 Jun 15, 2014

+1

Please sign in to comment.