diff --git a/src/FilterPluginManager.php b/src/FilterPluginManager.php index 53bf403e..f8123f84 100644 --- a/src/FilterPluginManager.php +++ b/src/FilterPluginManager.php @@ -152,6 +152,9 @@ class FilterPluginManager extends AbstractPluginManager 'toint' => ToInt::class, 'toInt' => ToInt::class, 'ToInt' => ToInt::class, + 'toenum' => ToEnum::class, + 'toEnum' => ToEnum::class, + 'ToEnum' => ToEnum::class, 'tofloat' => ToFloat::class, 'toFloat' => ToFloat::class, 'ToFloat' => ToFloat::class, @@ -360,6 +363,7 @@ class FilterPluginManager extends AbstractPluginManager HtmlEntities::class => InvokableFactory::class, Inflector::class => InvokableFactory::class, ToInt::class => InvokableFactory::class, + ToEnum::class => InvokableFactory::class, ToFloat::class => InvokableFactory::class, MonthSelect::class => InvokableFactory::class, ToNull::class => InvokableFactory::class, @@ -373,8 +377,6 @@ class FilterPluginManager extends AbstractPluginManager StringTrim::class => InvokableFactory::class, StripNewlines::class => InvokableFactory::class, StripTags::class => InvokableFactory::class, - ToInt::class => InvokableFactory::class, - ToNull::class => InvokableFactory::class, UriNormalize::class => InvokableFactory::class, Whitelist::class => InvokableFactory::class, Word\CamelCaseToDash::class => InvokableFactory::class, diff --git a/src/ToEnum.php b/src/ToEnum.php new file mode 100644 index 00000000..2759c07c --- /dev/null +++ b/src/ToEnum.php @@ -0,0 +1,89 @@ +, + * } + */ +final class ToEnum implements FilterInterface +{ + /** @var class-string|null */ + private ?string $enumClass = null; + + /** + * @param Traversable|class-string|Options $enumOrOptions + */ + public function __construct($enumOrOptions = []) + { + if ($enumOrOptions instanceof Traversable) { + /** @var Options $enumOrOptions */ + $enumOrOptions = ArrayUtils::iteratorToArray($enumOrOptions); + } + + if ( + is_array($enumOrOptions) && + isset($enumOrOptions['enum']) + ) { + $this->enumClass = $enumOrOptions['enum']; + } + + if (is_string($enumOrOptions)) { + $this->enumClass = $enumOrOptions; + } + } + + /** + * Defined by Laminas\Filter\FilterInterface + * + * Returns an enum representation of $value if matching. + * + * @param mixed $value + * @psalm-return UnitEnum|mixed + */ + public function filter($value): mixed + { + $enum = $this->enumClass; + + if ($enum === null) { + throw new RuntimeException( + 'enum class not set' + ); + } + + if (! is_string($value) && ! is_int($value)) { + return $value; + } + + if (is_subclass_of($enum, BackedEnum::class)) { + return $enum::tryFrom($value) ?: $value; + } + + if (! is_subclass_of($enum, UnitEnum::class)) { + return $value; + } + + if (in_array($value, array_column($enum::cases(), 'name'), true)) { + return constant($enum . '::' . $value); + } + + return $value; + } +} diff --git a/test/TestAsset/TestIntBackedEnum.php b/test/TestAsset/TestIntBackedEnum.php new file mode 100644 index 00000000..dc9e706b --- /dev/null +++ b/test/TestAsset/TestIntBackedEnum.php @@ -0,0 +1,11 @@ +, 1: string|int, 2: UnitEnum|BackedEnum}> */ + public function filterableValuesProvider(): array + { + return [ + 'unit enum' => [TestUnitEnum::class, 'foo', TestUnitEnum::foo], + 'backed string enum' => [TestStringBackedEnum::class, 'foo', TestStringBackedEnum::Foo], + 'backed integer enum' => [TestIntBackedEnum::class, 2, TestIntBackedEnum::Bar], + ]; + } + + /** + * @dataProvider filterableValuesProvider + * @param class-string $enumClass + */ + public function testCanFilterToEnum(string $enumClass, string|int $value, UnitEnum $expectedFilteredValue): void + { + $filter = new ToEnum($enumClass); + + self::assertSame($expectedFilteredValue, $filter->filter($value)); + } + + /** + * @dataProvider filterableValuesProvider + * @param class-string $enumClass + */ + public function testCanFilterToEnumWithOptions( + string $enumClass, + string|int $value, + UnitEnum $expectedFilteredValue + ): void { + $filter = new ToEnum(['enum' => $enumClass]); + + self::assertSame($expectedFilteredValue, $filter->filter($value)); + } + + /** @return array, 1: mixed}> */ + public function unfilterableValuesProvider(): array + { + return [ + 'array' => [TestUnitEnum::class, []], + 'float' => [TestUnitEnum::class, 1.1], + 'bool' => [TestUnitEnum::class, false], + 'unit enum' => [TestUnitEnum::class, 'baz'], + 'backed string enum' => [TestStringBackedEnum::class, 'baz'], + 'backed integer enum' => [TestIntBackedEnum::class, 3], + ]; + } + + /** + * @dataProvider unfilterableValuesProvider + * @param class-string $enumClass + */ + public function testFiltersToNull(string $enumClass, mixed $value): void + { + $filter = new ToEnum($enumClass); + + self::assertEquals($value, $filter->filter($value)); + } + + public function testThrowsExceptionIfEnumNotSet(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('enum class not set'); + + /** + * @psalm-suppress InvalidArgument + */ + $filter = new ToEnum([]); + + $filter->filter('foo'); + } +}