From fdef40c681a328b6e7a2ec6bdb6c2b262026aa12 Mon Sep 17 00:00:00 2001 From: Reinfi Date: Fri, 30 Dec 2022 22:14:36 +0100 Subject: [PATCH 01/10] add new filter to filter to an enum Signed-off-by: Reinfi --- src/ToEnum.php | 113 ++++++++++++++++++++++++ test/TestAsset/TestIntBackedEnum.php | 11 +++ test/TestAsset/TestStringBackedEnum.php | 11 +++ test/TestAsset/TestUnitEnum.php | 11 +++ test/ToEnumTest.php | 94 ++++++++++++++++++++ 5 files changed, 240 insertions(+) create mode 100644 src/ToEnum.php create mode 100644 test/TestAsset/TestIntBackedEnum.php create mode 100644 test/TestAsset/TestStringBackedEnum.php create mode 100644 test/TestAsset/TestUnitEnum.php create mode 100644 test/ToEnumTest.php diff --git a/src/ToEnum.php b/src/ToEnum.php new file mode 100644 index 00000000..b5495404 --- /dev/null +++ b/src/ToEnum.php @@ -0,0 +1,113 @@ +} + * @extends AbstractFilter + */ +class ToEnum extends AbstractFilter +{ + /** @var Options */ + protected $options = [ + 'enum' => null, + ]; + + /** + * @param class-string|Traversable|Options $enumOrOptions + */ + public function __construct($enumOrOptions) + { + if ($enumOrOptions instanceof Traversable) { + $enumOrOptions = ArrayUtils::iteratorToArray($enumOrOptions); + } + + if ( + is_array($enumOrOptions) && + isset($enumOrOptions['enum']) + ) { + $this->setOptions($enumOrOptions); + + return; + } + + if (is_string($enumOrOptions)) { + $this->setEnum($enumOrOptions); + } + } + + /** + * @param class-string $enum + */ + public function setEnum(string $enum): self + { + if (! is_subclass_of($enum, UnitEnum::class)) { + throw new Exception\InvalidArgumentException( + 'enum is not of type enum' + ); + } + + $this->options['enum'] = $enum; + return $this; + } + + /** + * @return class-string|null + */ + public function getEnum(): ?string + { + return $this->options['enum']; + } + + /** + * Defined by Laminas\Filter\FilterInterface + * + * Returns an enum representation of $value or null + * + * @param null|array|bool|float|int|string $value + * @return UnitEnum|BackedEnum|null + */ + public function filter($value) + { + $enum = $this->getEnum(); + + if ($enum === null) { + throw new RuntimeException( + 'enum class not set' + ); + } + + if (! is_string($value) && ! is_int($value)) { + return null; + } + + if (is_subclass_of($enum, 'BackedEnum')) { + return $enum::tryFrom($value); + } + + if (! is_string($value) || ! is_subclass_of($enum, 'UnitEnum')) { + return null; + } + + foreach ($enum::cases() as $enumCase) { + if ($enumCase->name === $value) { + return $enumCase; + } + } + + return null; + } +} 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 @@ + [TestUnitEnum::class, 'foo', TestUnitEnum::foo], + 'backed string enum' => [TestStringBackedEnum::class, 'foo', TestStringBackedEnum::Foo], + 'backed integer enum' => [TestIntBackedEnum::class, 2, TestIntBackedEnum::Bar], + ]; + } + + /** + * @dataProvider filterableValuesProvider + * @param string|int $value + */ + public function testCanFilterToEnum(string $enumClass, $value, UnitEnum $expectedFilteredValue): void + { + $filter = new ToEnum($enumClass); + + self::assertSame($expectedFilteredValue, $filter->filter($value)); + } + + /** + * @dataProvider filterableValuesProvider + * @param string|int $value + */ + public function testCanFilterToEnumWithOptions(string $enumClass, $value, UnitEnum $expectedFilteredValue): void + { + $filter = new ToEnum(['enum' => $enumClass]); + + self::assertSame($expectedFilteredValue, $filter->filter($value)); + } + + 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 mixed $value + */ + public function testFiltersToNull(string $enumClass, $value): void + { + $filter = new ToEnum($enumClass); + + self::assertNull($filter->filter($value)); + } + + public function testThrowsExceptionIfEnumNotSet(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('enum class not set'); + + $filter = new ToEnum([]); + + $filter->filter('foo'); + } + + public function testThrowsExceptionIfEnumNotOfEnumType(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('enum is not of type enum'); + + $filter = new ToEnum([]); + + $filter->setEnum('foo'); + } +} From 84f400f1f9ef574c3ed74fa9e022344d5b5f51a7 Mon Sep 17 00:00:00 2001 From: Reinfi Date: Sun, 8 Jan 2023 09:30:35 +0100 Subject: [PATCH 02/10] fix some issues with types Signed-off-by: Reinfi --- src/ToEnum.php | 43 +++++++++++++++---------------------------- test/ToEnumTest.php | 23 ++++++----------------- 2 files changed, 21 insertions(+), 45 deletions(-) diff --git a/src/ToEnum.php b/src/ToEnum.php index b5495404..faaf6c3f 100644 --- a/src/ToEnum.php +++ b/src/ToEnum.php @@ -4,7 +4,6 @@ namespace Laminas\Filter; -use BackedEnum; use Laminas\Filter\Exception\RuntimeException; use Laminas\Stdlib\ArrayUtils; use Traversable; @@ -16,15 +15,16 @@ use function is_subclass_of; /** - * @psalm-type Options array{enum: class-string} - * @extends AbstractFilter + * @psalm-type Options = array{ + * enum: class-string, + * } */ -class ToEnum extends AbstractFilter +final class ToEnum implements FilterInterface { - /** @var Options */ - protected $options = [ - 'enum' => null, - ]; + /** + * @var class-string|null + */ + private ?string $enumClass = null; /** * @param class-string|Traversable|Options $enumOrOptions @@ -39,7 +39,7 @@ public function __construct($enumOrOptions) is_array($enumOrOptions) && isset($enumOrOptions['enum']) ) { - $this->setOptions($enumOrOptions); + $this->setEnum($enumOrOptions['enum']); return; } @@ -52,37 +52,24 @@ public function __construct($enumOrOptions) /** * @param class-string $enum */ - public function setEnum(string $enum): self + protected function setEnum(string $enum): self { - if (! is_subclass_of($enum, UnitEnum::class)) { - throw new Exception\InvalidArgumentException( - 'enum is not of type enum' - ); - } + $this->enumClass = $enum; - $this->options['enum'] = $enum; return $this; } - /** - * @return class-string|null - */ - public function getEnum(): ?string - { - return $this->options['enum']; - } - /** * Defined by Laminas\Filter\FilterInterface * * Returns an enum representation of $value or null * - * @param null|array|bool|float|int|string $value - * @return UnitEnum|BackedEnum|null + * @param mixed $value + * @return UnitEnum|null */ - public function filter($value) + public function filter($value): ?UnitEnum { - $enum = $this->getEnum(); + $enum = $this->enumClass; if ($enum === null) { throw new RuntimeException( diff --git a/test/ToEnumTest.php b/test/ToEnumTest.php index a3cc456b..792d7a80 100644 --- a/test/ToEnumTest.php +++ b/test/ToEnumTest.php @@ -4,7 +4,6 @@ namespace LaminasTest\Filter; -use Laminas\Filter\Exception\InvalidArgumentException; use Laminas\Filter\Exception\RuntimeException; use Laminas\Filter\ToEnum; use LaminasTest\Filter\TestAsset\TestIntBackedEnum; @@ -29,9 +28,9 @@ public function filterableValuesProvider(): array /** * @dataProvider filterableValuesProvider - * @param string|int $value + * @param class-string $enumClass */ - public function testCanFilterToEnum(string $enumClass, $value, UnitEnum $expectedFilteredValue): void + public function testCanFilterToEnum(string $enumClass, string|int $value, UnitEnum $expectedFilteredValue): void { $filter = new ToEnum($enumClass); @@ -40,9 +39,9 @@ public function testCanFilterToEnum(string $enumClass, $value, UnitEnum $expecte /** * @dataProvider filterableValuesProvider - * @param string|int $value + * @param class-string $enumClass */ - public function testCanFilterToEnumWithOptions(string $enumClass, $value, UnitEnum $expectedFilteredValue): void + public function testCanFilterToEnumWithOptions(string $enumClass, string|int $value, UnitEnum $expectedFilteredValue): void { $filter = new ToEnum(['enum' => $enumClass]); @@ -63,9 +62,9 @@ public function unfilterableValuesProvider(): array /** * @dataProvider unfilterableValuesProvider - * @param mixed $value + * @param class-string $enumClass */ - public function testFiltersToNull(string $enumClass, $value): void + public function testFiltersToNull(string $enumClass, mixed $value): void { $filter = new ToEnum($enumClass); @@ -81,14 +80,4 @@ public function testThrowsExceptionIfEnumNotSet(): void $filter->filter('foo'); } - - public function testThrowsExceptionIfEnumNotOfEnumType(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('enum is not of type enum'); - - $filter = new ToEnum([]); - - $filter->setEnum('foo'); - } } From cd769fb8e83abd501fb7f4e3c9eab97cf4813a0e Mon Sep 17 00:00:00 2001 From: Reinfi Date: Thu, 12 Jan 2023 04:20:26 +0100 Subject: [PATCH 03/10] fix psalm errors and return value instead of null Signed-off-by: Reinfi --- src/ToEnum.php | 29 ++++++++++++++--------------- test/ToEnumTest.php | 8 +++++++- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/ToEnum.php b/src/ToEnum.php index faaf6c3f..35803a48 100644 --- a/src/ToEnum.php +++ b/src/ToEnum.php @@ -4,6 +4,7 @@ namespace Laminas\Filter; +use BackedEnum; use Laminas\Filter\Exception\RuntimeException; use Laminas\Stdlib\ArrayUtils; use Traversable; @@ -27,7 +28,7 @@ final class ToEnum implements FilterInterface private ?string $enumClass = null; /** - * @param class-string|Traversable|Options $enumOrOptions + * @param Traversable|class-string|Options $enumOrOptions */ public function __construct($enumOrOptions) { @@ -52,7 +53,7 @@ public function __construct($enumOrOptions) /** * @param class-string $enum */ - protected function setEnum(string $enum): self + private function setEnum(string $enum): self { $this->enumClass = $enum; @@ -62,12 +63,12 @@ protected function setEnum(string $enum): self /** * Defined by Laminas\Filter\FilterInterface * - * Returns an enum representation of $value or null + * Returns an enum representation of $value if matching. * * @param mixed $value - * @return UnitEnum|null + * @return UnitEnum|mixed */ - public function filter($value): ?UnitEnum + public function filter($value): mixed { $enum = $this->enumClass; @@ -78,23 +79,21 @@ public function filter($value): ?UnitEnum } if (! is_string($value) && ! is_int($value)) { - return null; + return $value; } - if (is_subclass_of($enum, 'BackedEnum')) { - return $enum::tryFrom($value); + if (is_subclass_of($enum, BackedEnum::class)) { + return $enum::tryFrom($value) ?: $value; } - if (! is_string($value) || ! is_subclass_of($enum, 'UnitEnum')) { - return null; + if (! is_subclass_of($enum, UnitEnum::class)) { + return $value; } - foreach ($enum::cases() as $enumCase) { - if ($enumCase->name === $value) { - return $enumCase; - } + if (in_array($value, array_column($enum::cases(), 'name'), true)) { + return constant($enum . '::' . $value); } - return null; + return $value; } } diff --git a/test/ToEnumTest.php b/test/ToEnumTest.php index 792d7a80..b616af91 100644 --- a/test/ToEnumTest.php +++ b/test/ToEnumTest.php @@ -11,12 +11,14 @@ use LaminasTest\Filter\TestAsset\TestUnitEnum; use PHPUnit\Framework\TestCase; use UnitEnum; +use BackedEnum; /** * @requires PHP 8.1 */ class ToEnumTest extends TestCase { + /** @return array, 1: string|int, 2: UnitEnum|BackedEnum}> */ public function filterableValuesProvider(): array { return [ @@ -48,6 +50,7 @@ public function testCanFilterToEnumWithOptions(string $enumClass, string|int $va self::assertSame($expectedFilteredValue, $filter->filter($value)); } + /** @return array, 1: mixed}> */ public function unfilterableValuesProvider(): array { return [ @@ -68,7 +71,7 @@ public function testFiltersToNull(string $enumClass, mixed $value): void { $filter = new ToEnum($enumClass); - self::assertNull($filter->filter($value)); + self::assertEquals($value, $filter->filter($value)); } public function testThrowsExceptionIfEnumNotSet(): void @@ -76,6 +79,9 @@ public function testThrowsExceptionIfEnumNotSet(): void $this->expectException(RuntimeException::class); $this->expectExceptionMessage('enum class not set'); + /** + * @psalm-suppress InvalidArgument + */ $filter = new ToEnum([]); $filter->filter('foo'); From 8a6fd866aab820c07a10ac1019f1344d0b724e63 Mon Sep 17 00:00:00 2001 From: Reinfi Date: Thu, 12 Jan 2023 04:23:40 +0100 Subject: [PATCH 04/10] remove setter method Signed-off-by: Reinfi --- src/ToEnum.php | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/ToEnum.php b/src/ToEnum.php index 35803a48..245ed3f0 100644 --- a/src/ToEnum.php +++ b/src/ToEnum.php @@ -33,6 +33,7 @@ final class ToEnum implements FilterInterface public function __construct($enumOrOptions) { if ($enumOrOptions instanceof Traversable) { + /** @var Options $enumOrOptions */ $enumOrOptions = ArrayUtils::iteratorToArray($enumOrOptions); } @@ -40,26 +41,16 @@ public function __construct($enumOrOptions) is_array($enumOrOptions) && isset($enumOrOptions['enum']) ) { - $this->setEnum($enumOrOptions['enum']); + $this->enumClass = $enumOrOptions['enum']; return; } if (is_string($enumOrOptions)) { - $this->setEnum($enumOrOptions); + $this->enumClass = $enumOrOptions; } } - /** - * @param class-string $enum - */ - private function setEnum(string $enum): self - { - $this->enumClass = $enum; - - return $this; - } - /** * Defined by Laminas\Filter\FilterInterface * From 7de3e2eb5dc1192483cd804a2c5103d526dea6dd Mon Sep 17 00:00:00 2001 From: Reinfi Date: Thu, 12 Jan 2023 04:24:15 +0100 Subject: [PATCH 05/10] remove useless return in constructor Signed-off-by: Reinfi --- src/ToEnum.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ToEnum.php b/src/ToEnum.php index 245ed3f0..f7c260d4 100644 --- a/src/ToEnum.php +++ b/src/ToEnum.php @@ -42,8 +42,6 @@ public function __construct($enumOrOptions) isset($enumOrOptions['enum']) ) { $this->enumClass = $enumOrOptions['enum']; - - return; } if (is_string($enumOrOptions)) { From 2fca6f18f66d4414b09c392ece0d60bb09839c29 Mon Sep 17 00:00:00 2001 From: Reinfi Date: Thu, 12 Jan 2023 04:26:17 +0100 Subject: [PATCH 06/10] adjust codingstyle Signed-off-by: Reinfi --- src/ToEnum.php | 8 ++++---- test/ToEnumTest.php | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/ToEnum.php b/src/ToEnum.php index f7c260d4..a6451851 100644 --- a/src/ToEnum.php +++ b/src/ToEnum.php @@ -10,6 +10,9 @@ use Traversable; use UnitEnum; +use function array_column; +use function constant; +use function in_array; use function is_array; use function is_int; use function is_string; @@ -22,9 +25,7 @@ */ final class ToEnum implements FilterInterface { - /** - * @var class-string|null - */ + /** @var class-string|null */ private ?string $enumClass = null; /** @@ -55,7 +56,6 @@ public function __construct($enumOrOptions) * Returns an enum representation of $value if matching. * * @param mixed $value - * @return UnitEnum|mixed */ public function filter($value): mixed { diff --git a/test/ToEnumTest.php b/test/ToEnumTest.php index b616af91..b64ba04b 100644 --- a/test/ToEnumTest.php +++ b/test/ToEnumTest.php @@ -4,6 +4,7 @@ namespace LaminasTest\Filter; +use BackedEnum; use Laminas\Filter\Exception\RuntimeException; use Laminas\Filter\ToEnum; use LaminasTest\Filter\TestAsset\TestIntBackedEnum; @@ -11,7 +12,6 @@ use LaminasTest\Filter\TestAsset\TestUnitEnum; use PHPUnit\Framework\TestCase; use UnitEnum; -use BackedEnum; /** * @requires PHP 8.1 @@ -43,8 +43,11 @@ public function testCanFilterToEnum(string $enumClass, string|int $value, UnitEn * @dataProvider filterableValuesProvider * @param class-string $enumClass */ - public function testCanFilterToEnumWithOptions(string $enumClass, string|int $value, UnitEnum $expectedFilteredValue): void - { + public function testCanFilterToEnumWithOptions( + string $enumClass, + string|int $value, + UnitEnum $expectedFilteredValue + ): void { $filter = new ToEnum(['enum' => $enumClass]); self::assertSame($expectedFilteredValue, $filter->filter($value)); From 7182172876f0f780b192439ab297a137d38f001f Mon Sep 17 00:00:00 2001 From: Reinfi Date: Thu, 12 Jan 2023 19:55:12 +0100 Subject: [PATCH 07/10] specify return type of filter function Signed-off-by: Reinfi --- src/ToEnum.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ToEnum.php b/src/ToEnum.php index a6451851..946885cf 100644 --- a/src/ToEnum.php +++ b/src/ToEnum.php @@ -56,6 +56,7 @@ public function __construct($enumOrOptions) * Returns an enum representation of $value if matching. * * @param mixed $value + * @return UnitEnum|mixed */ public function filter($value): mixed { From fc001a4181d255b9f2c1d8ffd5d20da1598a1e08 Mon Sep 17 00:00:00 2001 From: Reinfi Date: Thu, 12 Jan 2023 19:58:45 +0100 Subject: [PATCH 08/10] register filter in plugin manager Signed-off-by: Reinfi --- src/FilterPluginManager.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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, From 18ece908ff71fa4a81a46ed84e8a7f3ec1fb188b Mon Sep 17 00:00:00 2001 From: Reinfi Date: Thu, 12 Jan 2023 20:02:23 +0100 Subject: [PATCH 09/10] add default value for constructor Signed-off-by: Reinfi --- src/ToEnum.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ToEnum.php b/src/ToEnum.php index 946885cf..97769c42 100644 --- a/src/ToEnum.php +++ b/src/ToEnum.php @@ -31,7 +31,7 @@ final class ToEnum implements FilterInterface /** * @param Traversable|class-string|Options $enumOrOptions */ - public function __construct($enumOrOptions) + public function __construct($enumOrOptions = []) { if ($enumOrOptions instanceof Traversable) { /** @var Options $enumOrOptions */ From 7d7f97d219f53f7be4576a32147d3730bfc5f016 Mon Sep 17 00:00:00 2001 From: Reinfi Date: Sun, 15 Jan 2023 06:45:05 +0100 Subject: [PATCH 10/10] fix return type of function Signed-off-by: Reinfi --- src/ToEnum.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ToEnum.php b/src/ToEnum.php index 97769c42..2759c07c 100644 --- a/src/ToEnum.php +++ b/src/ToEnum.php @@ -56,7 +56,7 @@ public function __construct($enumOrOptions = []) * Returns an enum representation of $value if matching. * * @param mixed $value - * @return UnitEnum|mixed + * @psalm-return UnitEnum|mixed */ public function filter($value): mixed {