From 30713fe095e556468df679ffd5259e1931f714f0 Mon Sep 17 00:00:00 2001 From: Simon Praetorius Date: Sun, 7 Dec 2025 18:17:04 +0100 Subject: [PATCH] [FEATURE] Automatic enum conversion for arguments It is already possible to use PHP enums in Fluid templates: Both ViewHelpers and components can define an enum as argument type by using the full name of the enum. With the `` ViewHelper, it is possible to access individual enum cases. And enum cases are just special PHP objects, which is why their properties (such as "name" or "value") can be accessed with Fluid's variable syntax. This patch aims to make the usage of enums for ViewHelper and components easier: If an argument type is a known enum, Fluid automatically tries to convert the supplied value to the matching enum case. This works both for basic and backed enums: For backed enums, Fluid first tries to find the enum case by its backed value and falls back to the enum name. For basic enums, only the name is considered. If the supplied value cannot be converted, the value remains unchanged, which leads to the normal validation exception for a non-matching argument type. Given the following enum and component: ```php namespace Vendor\Package; enum VariantEnum: int { case BASIC = 123456; } ``` ```xml Selected variant: {variant.value} ``` Before this change, the `` ViewHelper needed to be used if the value isn't already an enum: ```xml ``` With this change, the name of the enum case is sufficient: ```xml ``` And because the enum is int-backed, this also works: ```xml ``` --- .../ViewHelper/StrictArgumentProcessor.php | 49 ++++++- .../Core/Component/ComponentRenderingTest.php | 4 + .../EnumTypeArgument/EnumTypeArgument.html | 2 + .../StrictArgumentProcessorTest.php | 133 ++++++++++++++++++ 4 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 tests/Functional/Fixtures/Components/EnumTypeArgument/EnumTypeArgument.html diff --git a/src/Core/ViewHelper/StrictArgumentProcessor.php b/src/Core/ViewHelper/StrictArgumentProcessor.php index be8208e15..d9dd84ce9 100644 --- a/src/Core/ViewHelper/StrictArgumentProcessor.php +++ b/src/Core/ViewHelper/StrictArgumentProcessor.php @@ -10,8 +10,11 @@ namespace TYPO3Fluid\Fluid\Core\ViewHelper; use ArrayAccess; +use BackedEnum; +use ReflectionEnum; use Stringable; use Traversable; +use UnitEnum; /** * The StrictArgumentProcessor offers an alternative, stricter implementation @@ -32,15 +35,19 @@ public function process(mixed $value, ArgumentDefinition $definition): mixed if (!$definition->isRequired() && $value === $definition->getDefaultValue()) { return $value; } + // Scalar values can be type-casted automatically // Boolean expressions are evaluated at the parser level, so we just make sure // that the input has the correct type - return match ($definition->getType()) { - 'string' => is_scalar($value) ? (string)$value : $value, - 'int', 'integer' => is_scalar($value) ? (int)$value : $value, - 'float', 'double' => is_scalar($value) ? (float)$value : $value, - 'bool', 'boolean' => is_scalar($value) ? (bool)$value : $value, - default => $value, - }; + if (is_scalar($value)) { + return match ($definition->getType()) { + 'string' => (string)$value, + 'int', 'integer' => (int)$value, + 'float', 'double' => (float)$value, + 'bool', 'boolean' => (bool)$value, + default => enum_exists($definition->getType()) ? $this->convertValueToEnum($definition->getType(), $value) : $value, + }; + } + return $value; } public function isValid(mixed $value, ArgumentDefinition $definition): bool @@ -64,6 +71,34 @@ public function isValid(mixed $value, ArgumentDefinition $definition): bool return false; } + /** + * Attempt to convert a scalar value to a valid enum case if expected type is an enum + * + * @param class-string $type + */ + private function convertValueToEnum(string $type, mixed $value): mixed + { + // For backed enums, the scalar equivalent is preferred, but the case name can + // be used as well + if (is_a($type, BackedEnum::class, true)) { + // Make sure that tryFrom() can be called without type mismatches + $backingType = (string)(new ReflectionEnum($type))->getBackingType(); + if ( + ($backingType === 'string' && is_string($value)) + || ($backingType === 'int' && is_int($value)) + ) { + $enum = $type::tryFrom($value); + if ($enum !== null) { + return $enum; + } + } + } + // Check if enum case name exists + return (is_string($value) && defined("$type::$value")) + ? constant("$type::$value") + : $value; + } + /** * Check whether the defined type matches the value type */ diff --git a/tests/Functional/Core/Component/ComponentRenderingTest.php b/tests/Functional/Core/Component/ComponentRenderingTest.php index 64b232e4d..2e0e5c220 100644 --- a/tests/Functional/Core/Component/ComponentRenderingTest.php +++ b/tests/Functional/Core/Component/ComponentRenderingTest.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use TYPO3Fluid\Fluid\Tests\Functional\AbstractFunctionalTestCase; +use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\IntBackedEnumExample; use TYPO3Fluid\Fluid\View\TemplateView; final class ComponentRenderingTest extends AbstractFunctionalTestCase @@ -62,6 +63,9 @@ public static function basicComponentCollectionDataProvider(): iterable 'additional arguments can be provided if delegate allows' => ['', '{"foo":"bar","myAdditionalVariable":"my additional value","viewHelperName":"additionalArgumentsJson"}' . "\n"], 'union type, array provided' => ['', "\nfoo\n"], 'union type, string provided' => ['', "\nbar\n"], + 'enum type, enum object provided' => ['', "\nBAR => 123\n"], + 'enum type, enum name provided' => ['', "\nBAR => 123\n"], + 'enum type, enum value provided' => ['', "\nBAR => 123\n"], ]; } diff --git a/tests/Functional/Fixtures/Components/EnumTypeArgument/EnumTypeArgument.html b/tests/Functional/Fixtures/Components/EnumTypeArgument/EnumTypeArgument.html new file mode 100644 index 000000000..a6bbd7b3a --- /dev/null +++ b/tests/Functional/Fixtures/Components/EnumTypeArgument/EnumTypeArgument.html @@ -0,0 +1,2 @@ + +{value.name} => {value.value} diff --git a/tests/Unit/Core/ViewHelper/StrictArgumentProcessorTest.php b/tests/Unit/Core/ViewHelper/StrictArgumentProcessorTest.php index 8055c4481..c75205d97 100644 --- a/tests/Unit/Core/ViewHelper/StrictArgumentProcessorTest.php +++ b/tests/Unit/Core/ViewHelper/StrictArgumentProcessorTest.php @@ -18,6 +18,8 @@ use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition; use TYPO3Fluid\Fluid\Core\ViewHelper\StrictArgumentProcessor; use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\ArrayAccessExample; +use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\EnumExample; +use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\IntBackedEnumExample; use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\StringBackedEnumExample; use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\UserWithToString; @@ -513,7 +515,53 @@ public function count(): int 'expectedProcessedValue' => $stdClass, 'expectedProcessedValidity' => false, ]; + + // // Enums + // + yield [ + 'type' => EnumExample::class, + 'value' => EnumExample::FOO, + 'expectedValidity' => true, + 'expectedProcessedValue' => EnumExample::FOO, + 'expectedProcessedValidity' => true, + ]; + yield [ + 'type' => EnumExample::class, + 'value' => 'FOO', + 'expectedValidity' => false, + 'expectedProcessedValue' => EnumExample::FOO, + 'expectedProcessedValidity' => true, + ]; + yield [ + 'type' => EnumExample::class, + 'value' => 'INVALIDCASE', + 'expectedValidity' => false, + 'expectedProcessedValue' => 'INVALIDCASE', + 'expectedProcessedValidity' => false, + ]; + yield [ + 'type' => EnumExample::class, + 'value' => '', + 'expectedValidity' => false, + 'expectedProcessedValue' => '', + 'expectedProcessedValidity' => false, + ]; + yield [ + 'type' => EnumExample::class, + 'value' => $stdClass, + 'expectedValidity' => false, + 'expectedProcessedValue' => $stdClass, + 'expectedProcessedValidity' => false, + ]; + yield [ + 'type' => EnumExample::class, + 'value' => [], + 'expectedValidity' => false, + 'expectedProcessedValue' => [], + 'expectedProcessedValidity' => false, + ]; + // string-backed enums yield [ 'type' => StringBackedEnumExample::class, 'value' => StringBackedEnumExample::BAR, @@ -521,6 +569,34 @@ public function count(): int 'expectedProcessedValue' => StringBackedEnumExample::BAR, 'expectedProcessedValidity' => true, ]; + yield [ + 'type' => StringBackedEnumExample::class, + 'value' => 'BAR', + 'expectedValidity' => false, + 'expectedProcessedValue' => StringBackedEnumExample::BAR, + 'expectedProcessedValidity' => true, + ]; + yield [ + 'type' => StringBackedEnumExample::class, + 'value' => 'INVALIDCASE', + 'expectedValidity' => false, + 'expectedProcessedValue' => 'INVALIDCASE', + 'expectedProcessedValidity' => false, + ]; + yield [ + 'type' => StringBackedEnumExample::class, + 'value' => 'bar value', + 'expectedValidity' => false, + 'expectedProcessedValue' => StringBackedEnumExample::BAR, + 'expectedProcessedValidity' => true, + ]; + yield [ + 'type' => StringBackedEnumExample::class, + 'value' => '', + 'expectedValidity' => false, + 'expectedProcessedValue' => '', + 'expectedProcessedValidity' => false, + ]; yield [ 'type' => StringBackedEnumExample::class, 'value' => $stdClass, @@ -528,6 +604,63 @@ public function count(): int 'expectedProcessedValue' => $stdClass, 'expectedProcessedValidity' => false, ]; + yield [ + 'type' => StringBackedEnumExample::class, + 'value' => [], + 'expectedValidity' => false, + 'expectedProcessedValue' => [], + 'expectedProcessedValidity' => false, + ]; + // int-backed enums + yield [ + 'type' => IntBackedEnumExample::class, + 'value' => IntBackedEnumExample::BAR, + 'expectedValidity' => true, + 'expectedProcessedValue' => IntBackedEnumExample::BAR, + 'expectedProcessedValidity' => true, + ]; + yield [ + 'type' => IntBackedEnumExample::class, + 'value' => 'BAR', + 'expectedValidity' => false, + 'expectedProcessedValue' => IntBackedEnumExample::BAR, + 'expectedProcessedValidity' => true, + ]; + yield [ + 'type' => IntBackedEnumExample::class, + 'value' => 'INVALIDCASE', + 'expectedValidity' => false, + 'expectedProcessedValue' => 'INVALIDCASE', + 'expectedProcessedValidity' => false, + ]; + yield [ + 'type' => IntBackedEnumExample::class, + 'value' => 123, + 'expectedValidity' => false, + 'expectedProcessedValue' => IntBackedEnumExample::BAR, + 'expectedProcessedValidity' => true, + ]; + yield [ + 'type' => IntBackedEnumExample::class, + 'value' => 0, + 'expectedValidity' => false, + 'expectedProcessedValue' => 0, + 'expectedProcessedValidity' => false, + ]; + yield [ + 'type' => IntBackedEnumExample::class, + 'value' => $stdClass, + 'expectedValidity' => false, + 'expectedProcessedValue' => $stdClass, + 'expectedProcessedValidity' => false, + ]; + yield [ + 'type' => IntBackedEnumExample::class, + 'value' => [], + 'expectedValidity' => false, + 'expectedProcessedValue' => [], + 'expectedProcessedValidity' => false, + ]; // // Iterable