Skip to content

Commit

Permalink
feature #46771 [Yaml] Add support for !php/enum *->value syntax (ni…
Browse files Browse the repository at this point in the history
…colas-grekas)

This PR was merged into the 6.2 branch.

Discussion
----------

[Yaml] Add support for `!php/enum *->value` syntax

| Q             | A
| ------------- | ---
| Branch?       | 6.2
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #44203 and the likes
| License       | MIT
| Doc PR        | -

This PR allows one to use the following Yaml together with enums:
```yaml
- !php/enum SomeEnum::Bar
- !php/enum SomeEnum::Bar->value
```

The first line gets the `SomeEnum::Bar` instance. It's the same as writing `!php/const SomeEnum::Bar` but with an additional check that this is really an enum.

The second line is the one that is really needed as it allows referencing the value backed by an enum, which is something that is not possible currently.

Commits
-------

f71e4ab [Yaml] Add support for `!php/enum *->value` syntax
  • Loading branch information
fabpot committed Jun 25, 2022
2 parents 2a2d5b3 + f71e4ab commit fe368ec
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src/Symfony/Component/Yaml/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========

6.2
---

* Add support for `!php/enum` and `!php/enum *->value`

6.1
---

Expand Down
38 changes: 36 additions & 2 deletions src/Symfony/Component/Yaml/Inline.php
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
8 changes: 8 additions & 0 deletions src/Symfony/Component/Yaml/Tests/Fixtures/FooBackedEnum.php
@@ -0,0 +1,8 @@
<?php

namespace Symfony\Component\Yaml\Tests\Fixtures;

enum FooBackedEnum: string
{
case BAR = 'bar';
}
54 changes: 54 additions & 0 deletions src/Symfony/Component/Yaml/Tests/InlineTest.php
Expand Up @@ -15,6 +15,7 @@
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Inline;
use Symfony\Component\Yaml\Tag\TaggedValue;
use Symfony\Component\Yaml\Tests\Fixtures\FooBackedEnum;
use Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum;
use Symfony\Component\Yaml\Yaml;

Expand Down Expand Up @@ -72,13 +73,45 @@ public function testParsePhpConstantThrowsExceptionWhenUndefined()
Inline::parse('!php/const WRONG_CONSTANT', Yaml::PARSE_CONSTANT);
}

public function testParsePhpEnumThrowsExceptionWhenUndefined()
{
$this->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
*/
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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 [
Expand Down

0 comments on commit fe368ec

Please sign in to comment.