diff --git a/CHANGELOG b/CHANGELOG index ff4cb5bc5..31bbadcd9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ -# 3.11.1 (2024-XX-XX) +# 3.12.0 (2024-XX-XX) * Fix integration tests when a test has more than on data/expect section and deprecations + * Add the `enum_cases` function # 3.11.0 (2024-08-08) diff --git a/doc/functions/enum_cases.rst b/doc/functions/enum_cases.rst new file mode 100644 index 000000000..2e6f88348 --- /dev/null +++ b/doc/functions/enum_cases.rst @@ -0,0 +1,21 @@ +``enum_cases`` +============== + +.. versionadded:: 3.12 + + The ``enum_cases`` function was added in Twig 3.12. + +``enum_cases`` returns the list of cases for a given enum: + +.. code-block:: twig + + {% for case in enum_cases('App\\MyEnum') %} + {{ case.value }} + {% endfor %} + +When using a string literal for the ``enum`` argument, it will be validated during compile time to be a valid enum name. + +Arguments +--------- + +* ``enum``: The FQCN of the enum diff --git a/doc/functions/index.rst b/doc/functions/index.rst index eebcd61c7..0809fd0be 100644 --- a/doc/functions/index.rst +++ b/doc/functions/index.rst @@ -10,6 +10,7 @@ Functions cycle date dump + enum_cases html_classes include max diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 5ac80884a..260735abb 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -45,6 +45,7 @@ use Twig\Node\Expression\Binary\SpaceshipBinary; use Twig\Node\Expression\Binary\StartsWithBinary; use Twig\Node\Expression\Binary\SubBinary; use Twig\Node\Expression\Filter\DefaultFilter; +use Twig\Node\Expression\FunctionNode\EnumCasesFunction; use Twig\Node\Expression\NullCoalesceExpression; use Twig\Node\Expression\Test\ConstantTest; use Twig\Node\Expression\Test\DefinedTest; @@ -246,6 +247,7 @@ final class CoreExtension extends AbstractExtension new TwigFunction('date', [$this, 'convertDate']), new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), + new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]), ]; } @@ -1449,6 +1451,26 @@ final class CoreExtension extends AbstractExtension } } + /** + * Returns the list of cases of the enum. + * + * @template T of \UnitEnum + * + * @param class-string $enum + * + * @return list + * + * @internal + */ + public static function enumCases(string $enum): array + { + if (!enum_exists($enum)) { + throw new RuntimeError(\sprintf('Enum "%s" does not exist.', $enum)); + } + + return $enum::cases(); + } + /** * Provides the ability to get constants from instances as well as class/global constants. * diff --git a/src/Node/Expression/FunctionNode/EnumCasesFunction.php b/src/Node/Expression/FunctionNode/EnumCasesFunction.php new file mode 100644 index 000000000..171f611de --- /dev/null +++ b/src/Node/Expression/FunctionNode/EnumCasesFunction.php @@ -0,0 +1,41 @@ +getNode('arguments'); + if ($arguments->hasNode('enum')) { + $firstArgument = $arguments->getNode('enum'); + } elseif ($arguments->hasNode('0')) { + $firstArgument = $arguments->getNode('0'); + } else { + $firstArgument = null; + } + + if (!$firstArgument instanceof ConstantExpression || \count($arguments) !== 1) { + parent::compile($compiler); + + return; + } + + $value = $firstArgument->getAttribute('value'); + + if (!\is_string($value)) { + throw new SyntaxError('The first argument of the "enum_cases" function must be a string.', $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!enum_exists($value)) { + throw new SyntaxError(\sprintf('The first argument of the "enum_cases" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + $compiler->raw(\sprintf('%s::cases()', $value)); + } +} diff --git a/tests/DummyBackedEnum.php b/tests/DummyBackedEnum.php new file mode 100644 index 000000000..c9b07d208 --- /dev/null +++ b/tests/DummyBackedEnum.php @@ -0,0 +1,9 @@ += 80100 +--TEMPLATE-- +{% set from_variable = 'Twig\\Tests\\NonExistentEnum' %} +{% for c in enum_cases(from_variable) %} + {{~ c.name }} +{% endfor %} +--DATA-- +return [] +--EXCEPTION-- +Twig\Error\RuntimeError: Enum "Twig\Tests\NonExistentEnum" does not exist in "index.twig" at line 3. diff --git a/tests/Fixtures/functions/enum_cases/invalid_enum.test b/tests/Fixtures/functions/enum_cases/invalid_enum.test new file mode 100644 index 000000000..3084c37e3 --- /dev/null +++ b/tests/Fixtures/functions/enum_cases/invalid_enum.test @@ -0,0 +1,10 @@ +--TEST-- +"enum_cases" function with invalid enum class +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum_cases('Twig\\Tests\\NonExistentEnum') %} + {{~ c.name }} +{% endfor %} +--EXCEPTION-- +Twig\Error\SyntaxError: The first argument of the "enum_cases" function must be the name of an enum, "Twig\Tests\NonExistentEnum" given in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/enum_cases/invalid_enum_escaping.test b/tests/Fixtures/functions/enum_cases/invalid_enum_escaping.test new file mode 100644 index 000000000..1d5828fbc --- /dev/null +++ b/tests/Fixtures/functions/enum_cases/invalid_enum_escaping.test @@ -0,0 +1,10 @@ +--TEST-- +"enum_cases" function with missing \ escaping +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum_cases('Twig\Tests\DummyBackedEnum') %} + {{~ c.name }} +{% endfor %} +--EXCEPTION-- +Twig\Error\SyntaxError: The first argument of the "enum_cases" function must be the name of an enum, "TwigTestsDummyBackedEnum" given in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/enum_cases/invalid_literal_type.test b/tests/Fixtures/functions/enum_cases/invalid_literal_type.test new file mode 100644 index 000000000..6e7945d00 --- /dev/null +++ b/tests/Fixtures/functions/enum_cases/invalid_literal_type.test @@ -0,0 +1,10 @@ +--TEST-- +"enum_cases" function with invalid literal type +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum_cases(13) %} + {{~ c.name }} +{% endfor %} +--EXCEPTION-- +Twig\Error\SyntaxError: The first argument of the "enum_cases" function must be a string in "index.twig" at line 2. diff --git a/tests/Fixtures/functions/enum_cases/valid.test b/tests/Fixtures/functions/enum_cases/valid.test new file mode 100644 index 000000000..011d64786 --- /dev/null +++ b/tests/Fixtures/functions/enum_cases/valid.test @@ -0,0 +1,24 @@ +--TEST-- +"enum_cases" function +--CONDITION-- +\PHP_VERSION_ID >= 80100 +--TEMPLATE-- +{% for c in enum_cases('Twig\\Tests\\DummyBackedEnum') %} + {{~ c.name }}: {{ c.value }} +{% endfor %} +{% for c in enum_cases('Twig\\Tests\\DummyUnitEnum') %} + {{~ c.name }} +{% endfor %} +{% set from_variable='Twig\\Tests\\DummyUnitEnum' %} +{% for c in enum_cases(from_variable) %} + {{~ c.name }} +{% endfor %} +--DATA-- +return [] +--EXPECT-- +FOO: foo +BAR: bar +BAR +BAZ +BAR +BAZ