Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new filter to filter to an enum #94

Open
wants to merge 10 commits into
base: 2.31.x
Choose a base branch
from
6 changes: 4 additions & 2 deletions src/FilterPluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
89 changes: 89 additions & 0 deletions src/ToEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace Laminas\Filter;

use BackedEnum;
use Laminas\Filter\Exception\RuntimeException;
use Laminas\Stdlib\ArrayUtils;
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;
use function is_subclass_of;

/**
* @psalm-type Options = array{
* enum: class-string<UnitEnum>,
* }
*/
final class ToEnum implements FilterInterface
{
/** @var class-string<UnitEnum>|null */
private ?string $enumClass = null;

/**
* @param Traversable|class-string<UnitEnum>|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
*/
reinfi marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
11 changes: 11 additions & 0 deletions test/TestAsset/TestIntBackedEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Filter\TestAsset;

enum TestIntBackedEnum: int
{
case Foo = 1;
case Bar = 2;
}
11 changes: 11 additions & 0 deletions test/TestAsset/TestStringBackedEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Filter\TestAsset;

enum TestStringBackedEnum: string
{
case Foo = 'foo';
case Bar = 'bar';
}
11 changes: 11 additions & 0 deletions test/TestAsset/TestUnitEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Filter\TestAsset;

enum TestUnitEnum
{
case foo;
case bar;
}
92 changes: 92 additions & 0 deletions test/ToEnumTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Filter;

use BackedEnum;
use Laminas\Filter\Exception\RuntimeException;
use Laminas\Filter\ToEnum;
use LaminasTest\Filter\TestAsset\TestIntBackedEnum;
use LaminasTest\Filter\TestAsset\TestStringBackedEnum;
use LaminasTest\Filter\TestAsset\TestUnitEnum;
use PHPUnit\Framework\TestCase;
use UnitEnum;

/**
* @requires PHP 8.1
*/
class ToEnumTest extends TestCase
{
/** @return array<string, array{0: class-string<UnitEnum>, 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<UnitEnum> $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<UnitEnum> $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<string, array{0: class-string<UnitEnum>, 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<UnitEnum> $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');
}
}