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