Open bertoost opened 2 years ago
Well, for the cases where you already have the enum instance in a variable (coming from an object getter for instance), you would have to use my_enum.value
anyway in Twig. I'm not sure a special enum_value(...)
function replacing constant(...).value
is worth it in the core.
@bertoost Is a function like enum_value('App\\...\\Enum::Key')
what you had in mind?
Definitely true @stof ... but when you don't have a getter to assign it to the template, then constant(..).value
is kinda weird since enums doesn't feel like constants...
@ThomasLandauer could be, or just enum() since the value of the key is probably the only thing you want to use/compare against. Therefore there should be an option to retrieve the enum itself too. For example when you want to build a dropdown of checklist/radio-list with the enum values... eq. for item in enum(..)
OK, that's 2 different things:
enum()
for the existing constant()
?cases()
method: https://www.php.net/manual/language.enumerations.listing.php But AFAIK there's no Twig function to get the entire enum; constant('App\\...\\Enum')
is not working. So maybe an enum()
function for that?@ThomasLandauer there is nothing like getter the entire enum. Calling MyEnum::cases()
gives you a list of MyEnum instances. But constant('App\\...\\MyEnum')
means something totally different (in PHP as well).
Wouldn't it suffice to add an enum(...)
method working comparable to constant(...)
, but would (a) validate the class in question is in fact an enum, and (b) could accept a second argument instructing it to either return the enum itself (default - not all enums have to be backed, and you don't always need to know what the backed value is), or a value, or its cases?
Even if it ended up being just a decorator of what powers constant(...)
, with the additional type check, I'd say it's a good start?
At first I tried to add a twig function for native enums like this:
public function enum(string $className): object
{
if (!is_subclass_of($className, Enum::class)) {
throw new \InvalidArgumentException(sprintf('"%s" is not an enum.', $className));
}
return new class ($className) {
public function __construct(private string $className)
{
}
public function __call(string $caseName, array $arguments): mixed
{
Assert::count($arguments, 0);
return ($this->className)::$caseName();
}
};
}
Which allows to use it in templates:
{% set PostStatus = enum('Acme\\Post\\PostStatus') %}
{% if post.status == PostStatus.Posted %}
{# ... #}
{% endif %}
{% for status in PostStatus.cases() %}
{# ... #}
{% endfor %}
In the end I decided to use isser methods on my entities and not exposing enums to templates.
Sharing this in case someone will find it useful. :) This code will need some changes to work with built-in enums.
Hi, my solution:
<?php
declare(strict_types=1);
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class EnumExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('enum', [$this, 'enum']),
];
}
public function enum(string $fullClassName): object
{
$parts = explode('::', $fullClassName);
$className = $parts[0];
$constant = $parts[1] ?? null;
if (!enum_exists($className)) {
throw new \InvalidArgumentException(sprintf('"%s" is not an enum.', $className));
}
if ($constant) {
return constant($fullClassName);
}
return new class($fullClassName) {
public function __construct(private string $fullClassName)
{
}
public function __call(string $caseName, array $arguments): mixed
{
return call_user_func_array([$this->fullClassName, $caseName], $arguments);
}
};
}
}
Templates:
{% dump(enum('App\\Entity\\Status').cases()) %}
{% dump(enum('App\\Entity\\Status').customStaticMethod()) %}
{% dump(enum('App\\Entity\\Status::NEW')) %}
{% dump(enum('App\\Entity\\Status::NEW').name()) %}
{% dump(enum('App\\Entity\\Status::NEW').customMethod()) %}
I was a bit skeptical at first but both ideas from @luxemate and @codeg-pl look interesting to me.
{{ constant('App\...\Enum::Key').value }}
About this use case, the .value
suffix is boilerplate that could be removed if https://github.com/php/php-src/pull/8825 is accepted.
Slightly different implementation from @codeg-pl's version that allows for something closer to PHP's syntax.
<?php declare(strict_types=1);
namespace App\Twig;
use BadMethodCallException;
use InvalidArgumentException;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class EnumExtension extends AbstractExtension
{
/**
* @return TwigFunction[]
*/
public function getFunctions(): array
{
return [
new TwigFunction('enum', [$this, 'createProxy']),
];
}
public function createProxy(string $enumFQN): object
{
return new class($enumFQN) {
public function __construct(private readonly string $enum)
{
if (!enum_exists($this->enum)) {
throw new InvalidArgumentException("$this->enum is not an Enum type and cannot be used in this function");
}
}
public function __call(string $name, array $arguments)
{
$enumFQN = sprintf('%s::%s', $this->enum, $name);
if (defined($enumFQN)) {
return constant($enumFQN);
}
if (method_exists($this->enum, $name)) {
return $this->enum::$name(...$arguments);
}
throw new BadMethodCallException("Neither \"{$enumFQN}\" nor \"{$enumFQN}::{$name}()\" exist in this runtime.");
}
};
}
}
{% set OrderStatus = enum('\\App\\Helpers\\OrderStatus') %}
{% set waitingStatus = [ OrderStatus.Placed, OrderStatus.BeingPrepared ] %}
{% if order.status in waitingStatus %}
Be patient
{% elseif order.status == OrderStatus.Completed %}
Order complete!
{% endif %}
...
<select>
{% for type in OrderStatus.cases() %}
<option value="{{ type.value }}">
{{ type.stringLiteral() }} {# getStringLiteral is a custom method in my enum #}
</option>
{% endfor %}
</select>
Updates
Excellent. Thanks
The @allejo solution should be in the core IMHO.
Such function would give access to any static method available in PHP (at least the suggested implementation). This cannot go in core as is (integrating that with the sandbox system would be a nightmare).
Ohai, never thought other people would find my snippet helpful. @stof is 100% right, my snippet does give access to any static method (I've added an enum_exists
check to mitigate this), which is definitely dangerous; I never noticed that 😓 Would adding a check to the constructor to ensure that $enum
is an Enum (i.e. enum_exists
) be a decent safety check? Anything else I'm not thinking of?
I'm not too familiar with Twig's sandboxing other than it being a whitelist-only system. The enum_exists
check does not resolve the sandbox issue though. Are functions like If not, then could constant
allowed inside of a sandbox? If so, how do those work?enum
just be excluded from sandboxed Twig environments?
Edit: It just hit me, constant()
in Twig/PHP doesn't execute any code, it just retrieves values so the safety concern of this enum()
function calling arbitrary methods in Enum classes makes sandboxing difficult.
Thanks @allejo ! This works like charm. Should be added to the core.
enum MyEnum : string
{
case READY = 'в очереди';
case PROCESSING = 'обрабатывается';
case REJECTED = 'забраковано';
public static function getAsAssociatedArray () : array
{
$to_return = [];
foreach (self::cases() as $status) {
$to_return[$status->name] = $status;
$to_return[strtolower($status->name)] = $status;
}
return $to_return;
}
Controller
(new \App\Twig)->render('template.twig', ["my_enums" => MyEnum::getAsAssociatedArray()]);
TWIG
{# @var my_enums MyEnum #}
{{ dump(my_enums.ready) }}
{{ dump(my_enums.ready.name) }}
{{ dump(my_enums.READY.value) }}
@stof Do your objections still hold with the updates made to https://github.com/twigphp/Twig/issues/3681#issuecomment-1162728959?
If I am not missing anything, it takes a solution like this to be able to pass beim cases e. g. into methods from Twig?
Neither \"{$enumFQN}\" or \"{$enumFQN}::{$name}()\"
Being pendantic here, but that should be
Neither \"{$enumFQN}\" nor \"{$enumFQN}::{$name}()\"
but otherwise I hope the patch makes it in, in some shape or other
@EarthDweller A simpler way is:
(new \App\Twig)->render('template.twig', ["my_enums" => array_column(Module::cases(), null, 'name')]);
Then you don't need the getAsAssociatedArray function.
@michelbrons And both ways will work? {{ dump(my_enums.ready.name) }} {{ dump(my_enums.READY.value) }}
TWIG code more accurate and readable when all in lower_snake_case, BUT sometimes more visually usefull UPPERCASE 😎
Only uppercase works..
It would be nice if a developer can pass enums to templates and the designer can use autocompletion using my_enums.R...{Modal}
@michelbrons
It is possible, you can fork TWIG and add that check, then pull request to main repo.
TWIG already cheking methods:
some.method
getMethod, isMethod, hasMethod, method
Why pass ENUM through controller? Use global solution: https://github.com/twigphp/Twig/issues/3681#issuecomment-1159029881
I use it in production :)
Why pass ENUM through controller? Use global solution: #3681 (comment)
I use it in production :)
Depends from how many templates use enum, if only one, controller good way, if uses in more than one template, Twig\Extension – good way. 💪😎
I made a small modification to the sollution of @codeg-pl https://github.com/twigphp/Twig/issues/3681#issuecomment-1159029881
I added the code below to the enum
function, also removed the string
type from the function parameter
public function enum($fullClassName): object
{
if (is_object($fullClassName)) {
$fullClassName = get_class($fullClassName).'::'.$fullClassName->name;
}
// Original code continues
In Twig I can now use Enum values from the database like:
<h1>{{ enum(entity.enumPropertie).name() }}</h1>
In the end I decided to use isser methods on my entities and not exposing enums to templates.
Definitely the best advice, thanks
@allejo It could be possible to limit the issue with a check if it is one of the cases.
Instead of return constant($enumFQN);
, something like:
$constant = constant(...);
if (in_array($constant, $enum::cases())) {
return $constant;
}
@allejo excellent stuff. Adopted yours right away. <3 Open Source
Hi,
Since PHP 8.1 we can use Enums. Which in my opinion are a great asset to PHP. But unfortunate there is not an elegant way of retrieving values inside Twig templates.
I currently use this in my Symfony project;
Which will return the value of the key in de the Enum.
Maybe it's good to add some specific functions for this in the Twig core?
Regards, Bert