From f71e4ab614f584ee6420c70337eaead13d15a350 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 24 Jun 2022 14:29:51 +0200 Subject: [PATCH] [Yaml] Add support for `!php/enum *->value` syntax --- src/Symfony/Component/Yaml/CHANGELOG.md | 5 ++ src/Symfony/Component/Yaml/Inline.php | 38 ++++++++++++- .../Yaml/Tests/Fixtures/FooBackedEnum.php | 8 +++ .../Component/Yaml/Tests/InlineTest.php | 54 +++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/FooBackedEnum.php diff --git a/src/Symfony/Component/Yaml/CHANGELOG.md b/src/Symfony/Component/Yaml/CHANGELOG.md index 3fb33a84a94a..aeb416958928 100644 --- a/src/Symfony/Component/Yaml/CHANGELOG.md +++ b/src/Symfony/Component/Yaml/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +6.2 +--- + + * Add support for `!php/enum` and `!php/enum *->value` + 6.1 --- diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index 1a862b773958..7c2e19cd78b6 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -432,7 +432,7 @@ private static function parseMapping(string $mapping, int $flags, int &$i = 0, a throw new ParseException('Missing mapping key.', self::$parsedLineNumber + 1, $mapping); } - if ('!php/const' === $key) { + if ('!php/const' === $key || '!php/enum' === $key) { $key .= ' '.self::parseScalar($mapping, $flags, [':'], $i, false); $key = self::evaluateScalar($key, $flags); } @@ -623,6 +623,40 @@ private static function evaluateScalar(string $scalar, int $flags, array &$refer throw new ParseException(sprintf('The string "%s" could not be parsed as a constant. Did you forget to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); } + return null; + case str_starts_with($scalar, '!php/enum'): + if (self::$constantSupport) { + if (!isset($scalar[11])) { + throw new ParseException('Missing value for tag "!php/enum".', self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); + } + + $i = 0; + $enum = self::parseScalar(substr($scalar, 10), 0, null, $i, false); + if ($useValue = str_ends_with($enum, '->value')) { + $enum = substr($enum, 0, -7); + } + if (!\defined($enum)) { + throw new ParseException(sprintf('The enum "%s" is not defined.', $enum), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); + } + + $value = \constant($enum); + + if (!$value instanceof \UnitEnum) { + throw new ParseException(sprintf('The string "%s" is not the name of a valid enum.', $enum), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); + } + if (!$useValue) { + return $value; + } + if (!$value instanceof \BackedEnum) { + throw new ParseException(sprintf('The enum "%s" defines no value next to its name.', $enum), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); + } + + return $value->value; + } + if (self::$exceptionOnInvalidType) { + throw new ParseException(sprintf('The string "%s" could not be parsed as an enum. Did you forget to pass the "Yaml::PARSE_CONSTANT" flag to the parser?', $scalar), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename); + } + return null; case str_starts_with($scalar, '!!float '): return (float) substr($scalar, 8); @@ -703,7 +737,7 @@ private static function parseTag(string $value, int &$i, int $flags): ?string } // Is followed by a scalar and is a built-in tag - if ('' !== $tag && (!isset($value[$nextOffset]) || !\in_array($value[$nextOffset], ['[', '{'], true)) && ('!' === $tag[0] || 'str' === $tag || 'php/const' === $tag || 'php/object' === $tag)) { + if ('' !== $tag && (!isset($value[$nextOffset]) || !\in_array($value[$nextOffset], ['[', '{'], true)) && ('!' === $tag[0] || \in_array($tag, ['str', 'php/const', 'php/enum', 'php/object'], true))) { // Manage in {@link self::evaluateScalar()} return null; } diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/FooBackedEnum.php b/src/Symfony/Component/Yaml/Tests/Fixtures/FooBackedEnum.php new file mode 100644 index 000000000000..91acf5fe188b --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/FooBackedEnum.php @@ -0,0 +1,8 @@ +expectException(ParseException::class); + $this->expectExceptionMessage('The enum "SomeEnum::Foo" is not defined'); + Inline::parse('!php/enum SomeEnum::Foo', Yaml::PARSE_CONSTANT); + } + + public function testParsePhpEnumThrowsExceptionWhenNotAnEnum() + { + $this->expectException(ParseException::class); + $this->expectExceptionMessage('The string "PHP_INT_MAX" is not the name of a valid enum'); + Inline::parse('!php/enum PHP_INT_MAX', Yaml::PARSE_CONSTANT); + } + + public function testParsePhpEnumThrowsExceptionWhenNotBacked() + { + $this->expectException(ParseException::class); + $this->expectExceptionMessage('The enum "Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum::BAR" defines no value next to its name'); + Inline::parse('!php/enum Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum::BAR->value', Yaml::PARSE_CONSTANT); + } + public function testParsePhpConstantThrowsExceptionOnInvalidType() { + $this->assertNull(Inline::parse('!php/const PHP_INT_MAX')); + $this->expectException(ParseException::class); $this->expectExceptionMessageMatches('#The string "!php/const PHP_INT_MAX" could not be parsed as a constant.*#'); Inline::parse('!php/const PHP_INT_MAX', Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE); } + public function testParsePhpEnumThrowsExceptionOnInvalidType() + { + $this->assertNull(Inline::parse('!php/enum SomeEnum::Foo')); + + $this->expectException(ParseException::class); + $this->expectExceptionMessageMatches('#The string "!php/enum SomeEnum::Foo" could not be parsed as an enum.*#'); + Inline::parse('!php/enum SomeEnum::Foo', Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE); + } + /** * @dataProvider getTestsForDump */ @@ -589,6 +622,16 @@ public function testDumpUnitEnum() $this->assertSame("!php/const Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum::BAR", Inline::dump(FooUnitEnum::BAR)); } + public function testParseUnitEnum() + { + $this->assertSame(FooUnitEnum::BAR, Inline::parse("!php/enum Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum::BAR", Yaml::PARSE_CONSTANT)); + } + + public function testParseBackedEnumValue() + { + $this->assertSame(FooBackedEnum::BAR->value, Inline::parse("!php/enum Symfony\Component\Yaml\Tests\Fixtures\FooBackedEnum::BAR->value", Yaml::PARSE_CONSTANT)); + } + public function getDateTimeDumpTests() { $tests = []; @@ -818,6 +861,17 @@ public function testPhpConstTagWithEmptyValue(string $value) Inline::parse($value, Yaml::PARSE_CONSTANT); } + /** + * @dataProvider phpConstTagWithEmptyValueProvider + */ + public function testPhpEnumTagWithEmptyValue(string $value) + { + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Missing value for tag "!php/enum" at line 1 (near "!php/enum").'); + + Inline::parse(str_replace('!php/const', '!php/enum', $value), Yaml::PARSE_CONSTANT); + } + public function phpConstTagWithEmptyValueProvider() { return [