Implement the enum_cases function

The implementation contains an optimized implementation of the function
for the common case of using a string literal as the argument. It will
validate the enum existence during compilation and compile the code to
use `MyEnum::cases()` directly.
This commit is contained in:
Christophe Coevoet
2024-08-07 16:18:40 +02:00
parent b3f953c132
commit ff4d01f940
12 changed files with 172 additions and 1 deletions
+2 -1
View File
@@ -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)
+21
View File
@@ -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
+1
View File
@@ -10,6 +10,7 @@ Functions
cycle
date
dump
enum_cases
html_classes
include
max
+22
View File
@@ -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<T> $enum
*
* @return list<T>
*
* @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.
*
@@ -0,0 +1,41 @@
<?php
namespace Twig\Node\Expression\FunctionNode;
use Twig\Compiler;
use Twig\Error\SyntaxError;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
class EnumCasesFunction extends FunctionExpression
{
public function compile(Compiler $compiler): void
{
$arguments = $this->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));
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Twig\Tests;
enum DummyBackedEnum: string
{
case FOO = 'foo';
case BAR = 'bar';
}
+9
View File
@@ -0,0 +1,9 @@
<?php
namespace Twig\Tests;
enum DummyUnitEnum
{
case BAR;
case BAZ;
}
@@ -0,0 +1,13 @@
--TEST--
"enum_cases" function with invalid dynamic enum class
--CONDITION--
\PHP_VERSION_ID >= 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.
@@ -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.
@@ -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.
@@ -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.
@@ -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