Skip to content

Commit 1fc1379

Browse files
[Yaml] Add support for dumping null as an empty value by using the Yaml::DUMP_NULL_AS_EMPTY flag
1 parent 9b0ca99 commit 1fc1379

File tree

6 files changed

+110
-11
lines changed

6 files changed

+110
-11
lines changed

src/Symfony/Component/Yaml/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Deprecate parsing duplicate mapping keys whose value is `null`
8+
* Add support for dumping `null` as an empty value by using the `Yaml::DUMP_NULL_AS_EMPTY` flag
89

910
7.1
1011
---

src/Symfony/Component/Yaml/Dumper.php

+17-8
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ public function __construct(private int $indentation = 4)
4141
* @param int-mask-of<Yaml::DUMP_*> $flags A bit field of Yaml::DUMP_* constants to customize the dumped YAML string
4242
*/
4343
public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags = 0): string
44+
{
45+
if ($flags & Yaml::DUMP_NULL_AS_EMPTY && $flags & Yaml::DUMP_NULL_AS_TILDE) {
46+
throw new \InvalidArgumentException('The Yaml::DUMP_NULL_AS_EMPTY and Yaml::DUMP_NULL_AS_TILDE flags cannot be used together.');
47+
}
48+
49+
return $this->doDump($input, $inline, $indent, $flags);
50+
}
51+
52+
private function doDump(mixed $input, int $inline = 0, int $indent = 0, int $flags = 0, int $nestingLevel = 0): string
4453
{
4554
$output = '';
4655
$prefix = $indent ? str_repeat(' ', $indent) : '';
@@ -51,9 +60,9 @@ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags
5160
}
5261

5362
if ($inline <= 0 || (!\is_array($input) && !$input instanceof TaggedValue && $dumpObjectAsInlineMap) || !$input) {
54-
$output .= $prefix.Inline::dump($input, $flags);
63+
$output .= $prefix.Inline::dump($input, $flags, 0 === $nestingLevel);
5564
} elseif ($input instanceof TaggedValue) {
56-
$output .= $this->dumpTaggedValue($input, $inline, $indent, $flags, $prefix);
65+
$output .= $this->dumpTaggedValue($input, $inline, $indent, $flags, $prefix, $nestingLevel);
5766
} else {
5867
$dumpAsMap = Inline::isHash($input);
5968

@@ -105,10 +114,10 @@ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags
105114
}
106115

107116
if ($inline - 1 <= 0 || null === $value->getValue() || \is_scalar($value->getValue())) {
108-
$output .= ' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n";
117+
$output .= ' '.$this->doDump($value->getValue(), $inline - 1, 0, $flags, $nestingLevel + 1)."\n";
109118
} else {
110119
$output .= "\n";
111-
$output .= $this->dump($value->getValue(), $inline - 1, $dumpAsMap ? $indent + $this->indentation : $indent + 2, $flags);
120+
$output .= $this->doDump($value->getValue(), $inline - 1, $dumpAsMap ? $indent + $this->indentation : $indent + 2, $flags, $nestingLevel + 1);
112121
}
113122

114123
continue;
@@ -126,15 +135,15 @@ public function dump(mixed $input, int $inline = 0, int $indent = 0, int $flags
126135
$prefix,
127136
$dumpAsMap ? Inline::dump($key, $flags).':' : '-',
128137
$willBeInlined ? ' ' : "\n",
129-
$this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags)
138+
$this->doDump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $flags, $nestingLevel + 1)
130139
).($willBeInlined ? "\n" : '');
131140
}
132141
}
133142

134143
return $output;
135144
}
136145

137-
private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, int $flags, string $prefix): string
146+
private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, int $flags, string $prefix, int $nestingLevel): string
138147
{
139148
$output = \sprintf('%s!%s', $prefix ? $prefix.' ' : '', $value->getTag());
140149

@@ -150,10 +159,10 @@ private function dumpTaggedValue(TaggedValue $value, int $inline, int $indent, i
150159
}
151160

152161
if ($inline - 1 <= 0 || null === $value->getValue() || \is_scalar($value->getValue())) {
153-
return $output.' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n";
162+
return $output.' '.$this->doDump($value->getValue(), $inline - 1, 0, $flags, $nestingLevel + 1)."\n";
154163
}
155164

156-
return $output."\n".$this->dump($value->getValue(), $inline - 1, $indent, $flags);
165+
return $output."\n".$this->doDump($value->getValue(), $inline - 1, $indent, $flags, $nestingLevel + 1);
157166
}
158167

159168
private function getBlockIndentationIndicator(string $value): string

src/Symfony/Component/Yaml/Inline.php

+7-3
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public static function parse(string $value, int $flags = 0, array &$references =
100100
*
101101
* @throws DumpException When trying to dump PHP resource
102102
*/
103-
public static function dump(mixed $value, int $flags = 0): string
103+
public static function dump(mixed $value, int $flags = 0, bool $rootLevel = false): string
104104
{
105105
switch (true) {
106106
case \is_resource($value):
@@ -138,7 +138,7 @@ public static function dump(mixed $value, int $flags = 0): string
138138
case \is_array($value):
139139
return self::dumpArray($value, $flags);
140140
case null === $value:
141-
return self::dumpNull($flags);
141+
return self::dumpNull($flags, $rootLevel);
142142
case true === $value:
143143
return 'true';
144144
case false === $value:
@@ -253,12 +253,16 @@ private static function dumpHashArray(array|\ArrayObject|\stdClass $value, int $
253253
return \sprintf('{ %s }', implode(', ', $output));
254254
}
255255

256-
private static function dumpNull(int $flags): string
256+
private static function dumpNull(int $flags, bool $rootLevel = false): string
257257
{
258258
if (Yaml::DUMP_NULL_AS_TILDE & $flags) {
259259
return '~';
260260
}
261261

262+
if (Yaml::DUMP_NULL_AS_EMPTY & $flags && !$rootLevel) {
263+
return '';
264+
}
265+
262266
return 'null';
263267
}
264268

src/Symfony/Component/Yaml/Tests/DumperTest.php

+57
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,63 @@ public function testObjectSupportDisabledWithExceptions()
216216
$this->dumper->dump(['foo' => new A(), 'bar' => 1], 0, 0, Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE);
217217
}
218218

219+
public function testDumpWithMultipleNullFlagsFormatsThrows()
220+
{
221+
$this->expectException(\InvalidArgumentException::class);
222+
$this->expectExceptionMessage('The Yaml::DUMP_NULL_AS_EMPTY and Yaml::DUMP_NULL_AS_TILDE flags cannot be used together.');
223+
224+
$this->dumper->dump(['foo' => 'bar'], 0, 0, Yaml::DUMP_NULL_AS_EMPTY | Yaml::DUMP_NULL_AS_TILDE);
225+
}
226+
227+
public function testDumpNullAsEmptyInExpandedMapping()
228+
{
229+
$expected = "qux:\n foo: bar\n baz: \n";
230+
231+
$this->assertSame($expected, $this->dumper->dump(['qux' => ['foo' => 'bar', 'baz' => null]], 2, flags: Yaml::DUMP_NULL_AS_EMPTY));
232+
}
233+
234+
public function testDumpNullAsEmptyWithObject()
235+
{
236+
$class = new \stdClass();
237+
$class->foo = 'bar';
238+
$class->baz = null;
239+
240+
$this->assertSame("foo: bar\nbaz: \n", $this->dumper->dump($class, 2, flags: Yaml::DUMP_NULL_AS_EMPTY | Yaml::DUMP_OBJECT_AS_MAP));
241+
}
242+
243+
public function testDumpNullAsEmptyDumpsWhenInInlineMapping()
244+
{
245+
$expected = "foo: \nqux: { foo: bar, baz: }\n";
246+
247+
$this->assertSame($expected, $this->dumper->dump(['foo' => null, 'qux' => ['foo' => 'bar', 'baz' => null]], 1, flags: Yaml::DUMP_NULL_AS_EMPTY));
248+
}
249+
250+
public function testDumpNullAsEmptyDumpsNestedMaps()
251+
{
252+
$expected = "foo: \nqux:\n foo: bar\n baz: \n";
253+
254+
$this->assertSame($expected, $this->dumper->dump(['foo' => null, 'qux' => ['foo' => 'bar', 'baz' => null]], 10, flags: Yaml::DUMP_NULL_AS_EMPTY));
255+
}
256+
257+
public function testDumpNullAsEmptyInExpandedSequence()
258+
{
259+
$expected = "qux:\n - foo\n - \n - bar\n";
260+
261+
$this->assertSame($expected, $this->dumper->dump(['qux' => ['foo', null, 'bar']], 2, flags: Yaml::DUMP_NULL_AS_EMPTY));
262+
}
263+
264+
public function testDumpNullAsEmptyWhenInInlineSequence()
265+
{
266+
$expected = "foo: \nqux: [foo, , bar]\n";
267+
268+
$this->assertSame($expected, $this->dumper->dump(['foo' => null, 'qux' => ['foo', null, 'bar']], 1, flags: Yaml::DUMP_NULL_AS_EMPTY));
269+
}
270+
271+
public function testDumpNullAsEmptyAtRoot()
272+
{
273+
$this->assertSame('null', $this->dumper->dump(null, 2, flags: Yaml::DUMP_NULL_AS_EMPTY));
274+
}
275+
219276
/**
220277
* @dataProvider getEscapeSequences
221278
*/

src/Symfony/Component/Yaml/Tests/ParserTest.php

+27
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,33 @@ public function testTopLevelNull()
5252
$this->assertSameData($expected, $data);
5353
}
5454

55+
public function testEmptyValueInExpandedMappingIsSupported()
56+
{
57+
$yml = <<<'YAML'
58+
foo:
59+
bar:
60+
baz: qux
61+
YAML;
62+
63+
$data = $this->parser->parse($yml);
64+
$expected = ['foo' => ['bar' => null, 'baz' => 'qux']];
65+
$this->assertSameData($expected, $data);
66+
}
67+
68+
public function testEmptyValueInExpandedSequenceIsSupported()
69+
{
70+
$yml = <<<'YAML'
71+
foo:
72+
- bar
73+
-
74+
- baz
75+
YAML;
76+
77+
$data = $this->parser->parse($yml);
78+
$expected = ['foo' => ['bar', null, 'baz']];
79+
$this->assertSameData($expected, $data);
80+
}
81+
5582
public function testTaggedValueTopLevelNumber()
5683
{
5784
$yml = '!number 5';

src/Symfony/Component/Yaml/Yaml.php

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class Yaml
3535
public const DUMP_EMPTY_ARRAY_AS_SEQUENCE = 1024;
3636
public const DUMP_NULL_AS_TILDE = 2048;
3737
public const DUMP_NUMERIC_KEY_AS_STRING = 4096;
38+
public const DUMP_NULL_AS_EMPTY = 8192;
3839

3940
/**
4041
* Parses a YAML file into a PHP value.

0 commit comments

Comments
 (0)