diff --git a/src/Symfony/Component/Yaml/Dumper.php b/src/Symfony/Component/Yaml/Dumper.php index 52db38c3b032b..67102ed62c03b 100644 --- a/src/Symfony/Component/Yaml/Dumper.php +++ b/src/Symfony/Component/Yaml/Dumper.php @@ -29,6 +29,13 @@ class Dumper */ protected $indentation; + /** + * String of spaces with length equal to $this->indentation. + * + * @var string + */ + private $indentStr; + public function __construct(int $indentation = 4) { if ($indentation < 1) { @@ -36,6 +43,7 @@ public function __construct(int $indentation = 4) } $this->indentation = $indentation; + $this->indentStr = str_repeat(' ', $indentation); } /** @@ -50,93 +58,106 @@ public function __construct(int $indentation = 4) */ public function dump($input, int $inline = 0, int $indent = 0, int $flags = 0): string { - $output = ''; $prefix = $indent ? str_repeat(' ', $indent) : ''; - $dumpObjectAsInlineMap = true; - if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($input instanceof \ArrayObject || $input instanceof \stdClass)) { - $dumpObjectAsInlineMap = empty((array) $input); + $tag = null; + if ($input instanceof TaggedValue) { + $tag = $input->getTag(); + $input = $input->getValue(); + if ($input instanceof TaggedValue) { + throw new \InvalidArgumentException('Nested tags are not supported.'); + } } - if ($inline <= 0 || (!\is_array($input) && !$input instanceof TaggedValue && $dumpObjectAsInlineMap) || empty($input)) { - $output .= $prefix.Inline::dump($input, $flags); - } else { - $dumpAsMap = Inline::isHash($input); - - foreach ($input as $key => $value) { - if ('' !== $output && "\n" !== $output[-1]) { - $output .= "\n"; - } - - if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value) && false !== strpos($value, "\n") && false === strpos($value, "\r")) { - // If the first line starts with a space character, the spec requires a blockIndicationIndicator - // http://www.yaml.org/spec/1.2/spec.html#id2793979 - $blockIndentationIndicator = (' ' === substr($value, 0, 1)) ? (string) $this->indentation : ''; - - if (isset($value[-2]) && "\n" === $value[-2] && "\n" === $value[-1]) { - $blockChompingIndicator = '+'; - } elseif ("\n" === $value[-1]) { - $blockChompingIndicator = ''; - } else { - $blockChompingIndicator = '-'; - } + if ($this->shouldDumpAsInline($inline, $input, $flags)) { + return $prefix + .(null !== $tag ? '!'.$tag.' ' : '') + .Inline::dump($input, $flags) + ; + } - $output .= sprintf('%s%s%s |%s%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', '', $blockIndentationIndicator, $blockChompingIndicator); + $dumpAsMap = Inline::isHash($input); - foreach (explode("\n", $value) as $row) { - if ('' === $row) { - $output .= "\n"; - } else { - $output .= sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); - } - } - - continue; - } + $output = ''; + foreach ($input as $key => $value) { + if ('' !== $output && "\n" !== $output[-1]) { + $output .= "\n"; + } + $output .= $prefix; + $output .= $dumpAsMap ? Inline::dump($key, $flags).':' : '-'; + $tagged = false; + if ($value instanceof TaggedValue) { + $output .= ' !'.$value->getTag(); + $value = $value->getValue(); if ($value instanceof TaggedValue) { - $output .= sprintf('%s%s !%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', $value->getTag()); + throw new \InvalidArgumentException('Nested tags are not supported.'); + } + $tagged = true; + } - if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && false !== strpos($value->getValue(), "\n") && false === strpos($value->getValue(), "\r\n")) { - // If the first line starts with a space character, the spec requires a blockIndicationIndicator - // http://www.yaml.org/spec/1.2/spec.html#id2793979 - $blockIndentationIndicator = (' ' === substr($value->getValue(), 0, 1)) ? (string) $this->indentation : ''; - $output .= sprintf(' |%s', $blockIndentationIndicator); + if (Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value) && false !== strpos($value, "\n") && false === strpos($value, "\r")) { + $output .= ' |'; + // If the first line starts with a space character, the spec requires a blockIndicationIndicator + // http://www.yaml.org/spec/1.2/spec.html#id2793979 + if (' ' === substr($value, 0, 1)) { + $output .= $this->indentation; + } - foreach (explode("\n", $value->getValue()) as $row) { - $output .= sprintf("\n%s%s%s", $prefix, str_repeat(' ', $this->indentation), $row); - } + if (isset($value[-2]) && "\n" === $value[-2] && "\n" === $value[-1]) { + $output .= '+'; + } elseif ("\n" !== $value[-1]) { + $output .= '-'; + } - continue; + foreach (explode("\n", $value) as $row) { + $output .= "\n"; + if ('' !== $row) { + $output .= $prefix.$this->indentStr.$row; } + } - if ($inline - 1 <= 0 || null === $value->getValue() || \is_scalar($value->getValue())) { - $output .= ' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n"; - } else { - $output .= "\n"; - $output .= $this->dump($value->getValue(), $inline - 1, $dumpAsMap ? $indent + $this->indentation : $indent + 2, $flags); - } + continue; + } - continue; - } + $willBeInlined = $this->shouldDumpAsInline($inline - 1, $value, $flags); + + $output .= ($willBeInlined ? ' ' : "\n") + .$this->dump( + $value, + $inline - 1, + $willBeInlined + ? 0 + : $indent + ($tagged && !$dumpAsMap ? 2 : $this->indentation), + $flags) + .($willBeInlined ? "\n" : '') + ; + } + + if (null !== $tag) { + $output = '!'.$tag."\n".$output; + } - $dumpObjectAsInlineMap = true; + return $output; + } - if (Yaml::DUMP_OBJECT_AS_MAP & $flags && ($value instanceof \ArrayObject || $value instanceof \stdClass)) { - $dumpObjectAsInlineMap = empty((array) $value); - } + private function shouldDumpAsInline(int $inline, $value, int $flags): bool + { + if ($inline <= 0 || empty($value)) { + return true; + } - $willBeInlined = $inline - 1 <= 0 || !\is_array($value) && $dumpObjectAsInlineMap || empty($value); + if (\is_array($value)) { + return false; + } - $output .= sprintf('%s%s%s%s', - $prefix, - $dumpAsMap ? Inline::dump($key, $flags).':' : '-', - $willBeInlined ? ' ' : "\n", - $this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags) - ).($willBeInlined ? "\n" : ''); - } + if (Yaml::DUMP_OBJECT_AS_MAP & $flags + && ($value instanceof \ArrayObject || $value instanceof \stdClass) + && !empty((array) $value) + ) { + return false; } - return $output; + return true; } } diff --git a/src/Symfony/Component/Yaml/Tests/DumperTest.php b/src/Symfony/Component/Yaml/Tests/DumperTest.php index 59a47a8130f31..164b01c7e74b9 100644 --- a/src/Symfony/Component/Yaml/Tests/DumperTest.php +++ b/src/Symfony/Component/Yaml/Tests/DumperTest.php @@ -443,20 +443,20 @@ public function testDumpingTaggedValueTopLevelAssocInline() public function testDumpingTaggedValueTopLevelAssoc() { $data = new TaggedValue('user', ['name' => 'jane']); + $expected = <<<'YAML' +!user +name: jane - // @todo Fix the dumper, the output should not be ''. - $expected = ''; - $yaml = $this->dumper->dump($data, 2); - $this->assertSame($expected, $yaml); +YAML; + $this->assertSame($expected, $this->dumper->dump($data, 2)); } public function testDumpingTaggedValueTopLevelMultiLine() { $data = new TaggedValue('text', "a\nb\n"); - - // @todo Fix the dumper, the output should not be ''. - $expected = ''; + $expected = '!text "a\\nb\\n"'; $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + $this->assertEquals($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); } public function testDumpingTaggedValueSpecialCharsInTag() @@ -588,7 +588,7 @@ public function testDumpingMultiLineStringAsScalarBlockTaggedValue() $data = [ 'foo' => new TaggedValue('bar', "foo\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz"), ]; - $expected = "foo: !bar |\n". + $expected = "foo: !bar |-\n". " foo\n". " line with trailing spaces:\n". " \n". @@ -596,7 +596,7 @@ public function testDumpingMultiLineStringAsScalarBlockTaggedValue() " integer like line:\n". " 123456789\n". " empty line:\n". - " \n". + "\n". ' baz'; $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); @@ -608,12 +608,12 @@ public function testDumpingTaggedMultiLineInList() $data = [ new TaggedValue('bar', "a\nb"), ]; - $expected = "- !bar |\n a\n b"; + $expected = "- !bar |-\n a\n b"; $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); // @todo Fix the parser, eliminate these exceptions. $this->expectException(ParseException::class); - $this->expectExceptionMessage('Unable to parse at line 3 (near "!bar |").'); + $this->expectExceptionMessage('Unable to parse at line 3 (near "!bar |-").'); $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS); } @@ -623,14 +623,11 @@ public function testDumpingTaggedMultiLineTrailingNewlinesInMap() $data = [ 'foo' => new TaggedValue('bar', "a\nb\n\n\n"), ]; - $expected = "foo: !bar |\n a\n b\n \n \n "; + $expected = "foo: !bar |+\n a\n b\n\n\n"; $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); - // @todo Fix the parser, the result should be identical to $data. $this->assertSameData( - [ - 'foo' => new TaggedValue('bar', "a\nb\n"), - ], + $data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); } @@ -639,12 +636,12 @@ public function testDumpingTaggedMultiLineTrailingNewlinesInList() $data = [ new TaggedValue('bar', "a\nb\n\n\n"), ]; - $expected = "- !bar |\n a\n b\n \n \n "; + $expected = "- !bar |+\n a\n b\n\n\n"; $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); // @todo Fix the parser, eliminate these exceptions. $this->expectException(ParseException::class); - $this->expectExceptionMessage('Unable to parse at line 6 (near "!bar |").'); + $this->expectExceptionMessage('Unable to parse at line 6 (near "!bar |+").'); $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS); } @@ -696,7 +693,7 @@ public function testDumpMultiLineStringAsScalarBlock() nested_inlined_multi_line_string: { inlined_multi_line: "foo\nbar\r\nempty line:\n\nbaz" } YAML -); + ); $this->assertSame($expected, $yml); $this->assertSame($data, $this->parser->parse($yml)); }