-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[YAML] Added support for object-maps #10114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
f32705e
3e8adee
244b50b
ed43239
53e6992
e5d2f80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
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
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,18 +26,20 @@ class Inline | |
private static $exceptionOnInvalidType = false; | ||
private static $objectSupport = false; | ||
|
||
|
||
/** | ||
* Converts a YAML string to a PHP array. | ||
* | ||
* @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() | ||
* | ||
10000 * @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 +58,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: | ||
|
@@ -256,17 +258,18 @@ private static function parseQuotedScalar($scalar, &$i) | |
return $output; | ||
} | ||
|
||
|
||
/** | ||
* 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 +280,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 +298,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 | ||
} | ||
|
@@ -312,19 +316,26 @@ private static function parseSequence($sequence, &$i = 0) | |
throw new ParseException(sprintf('Malformed inline YAML string %s', $sequence)); | ||
} | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why this extra lines? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing! I didn't think about it because Fabbot didn't yell at me :) |
||
/** | ||
* 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(); | ||
if ($objectForMap === true) { | ||
$output = new \stdClass(); | ||
} else { | ||
$output = array(); | ||
} | ||
|
||
$len = strlen($mapping); | ||
$i += 1; | ||
|
||
|
@@ -344,23 +355,30 @@ private static function parseMapping($mapping, &$i = 0) | |
|
||
// value | ||
$done = false; | ||
|
||
if ($objectForMap === true) { | ||
$editPosition = &$output->$key; | ||
} else { | ||
$editPosition = &$output[$key]; | ||
} | ||
|
||
while ($i < $len) { | ||
switch ($mapping[$i]) { | ||
case '[': | ||
// nested sequence | ||
$output[$key] = self::parseSequence($mapping, $i); | ||
$editPosition = self::parseSequence($mapping, $i, $objectForMap); | ||
$done = true; | ||
break; | ||
case '{': | ||
// nested mapping | ||
$output[$key] = self::parseMapping($mapping, $i); | ||
$editPosition = self::parseMapping($mapping, $i, $objectForMap); | ||
$done = true; | ||
break; | ||
case ':': | ||
case ' ': | ||
break; | ||
default: | ||
$output[$key] = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i); | ||
$editPosition = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i); | ||
$done = true; | ||
--$i; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,23 @@ public function testParse() | |
} | ||
} | ||
|
||
public function testParseWithMapObjects() | ||
{ | ||
foreach ($this->getTestsForMapObjectParse() as $yaml => $value) { | ||
$actual = Inline::parse($yaml, false, false, true); | ||
if (is_object($value) === true) { | ||
$this->assertInstanceOf(get_class($value), $actual); | ||
$this->assertEquals(get_object_vars($value), get_object_vars($actual)); | ||
} elseif (is_array($value) === true) { | ||
$this->assertEquals($value, $actual); | ||
$this->assertMixedArraysSame($value, $actual); | ||
} else { | ||
$this->assertSame($value, $actual); | ||
} | ||
} | ||
|
||
} | ||
|
||
public function testDump() | ||
{ | ||
$testsForDump = $this->getTestsForDump(); | ||
|
@@ -182,6 +199,88 @@ 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' => FC90 39;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 +328,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 (is_object($value) === true) { | ||
$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(); | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Argh, yep. Lemme look through the whole PR and see if I've done this anywhere else :) |
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The j variable is a bit useless here, why don't you use 0 as argument?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, it is required. :(
parseMapping()
requires the second parameter to be passed by reference:PHP disallows passing constants by reference because the method may mutate the value (and indeed it does). So you must create a temporary variable and pass that instead. PHP doesn't mind if you omit the parameter because of the default on the function, but if you specify arguments that come after the referenced parameter, you must supply one.
I could do the variable assignment inline, but it's effectively the same thing:
Open to alternatives, but I did it this way because alternative approaches (removing the reference) would require fairly drastic restructuring of how the parsing works for what is ultimately just a little semantic blemish :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, missed that. Sad it has to be done this way :(