Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 42 additions & 7 deletions src/Core/ViewHelper/StrictArgumentProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<UnitEnum> $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
*/
Expand Down
4 changes: 4 additions & 0 deletions tests/Functional/Core/Component/ComponentRenderingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -62,6 +63,9 @@ public static function basicComponentCollectionDataProvider(): iterable
'additional arguments can be provided if delegate allows' => ['<my:additionalArgumentsJson foo="bar" />', '{"foo":"bar","myAdditionalVariable":"my additional value","viewHelperName":"additionalArgumentsJson"}' . "\n"],
'union type, array provided' => ['<my:unionTypeArgument item="{property: \'foo\'}" />', "\nfoo\n"],
'union type, string provided' => ['<my:unionTypeArgument item="bar" />', "\nbar\n"],
'enum type, enum object provided' => ['<my:enumTypeArgument value="{f:constant(name: \'' . IntBackedEnumExample::class . '::BAR\')}" />', "\nBAR => 123\n"],
'enum type, enum name provided' => ['<my:enumTypeArgument value="BAR" />', "\nBAR => 123\n"],
'enum type, enum value provided' => ['<my:enumTypeArgument value="123" />', "\nBAR => 123\n"],
];
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<f:argument name="value" type="TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\IntBackedEnumExample" />
{value.name} => {value.value}
133 changes: 133 additions & 0 deletions tests/Unit/Core/ViewHelper/StrictArgumentProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -513,21 +515,152 @@ 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,
'expectedValidity' => true,
'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,
'expectedValidity' => false,
'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,
];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also test for string with int value inside.

Suggested change
];
];
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
Expand Down