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
100 changes: 100 additions & 0 deletions src/ToEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Laminas\Filter;

use Laminas\Filter\Exception\RuntimeException;
use Laminas\Stdlib\ArrayUtils;
use Traversable;
use UnitEnum;

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 class-string<UnitEnum>|Traversable|Options $enumOrOptions
*/
public function __construct($enumOrOptions)
Copy link
Member

Choose a reason for hiding this comment

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

I would love for this to only accept class-string<UnitEnum> :D

Copy link
Member

Choose a reason for hiding this comment

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

The constructor arg needs to be

Suggested change
public function __construct($enumOrOptions)
public function __construct($enumOrOptions = [])

or

Suggested change
public function __construct($enumOrOptions)
public function __construct($enumOrOptions = null)

(null would also need to be added to the @param in this case)

The FilterPluginManager will just new the filter without arguments unless a factory is created (which is pointless).

In order for the filter to be available in the plugin manager, FilterPluginManager must be updated with entries under factories and aliases

The problem is that at this point, PHPUnit will fail CI on PHP 8.0 (Along with Psalm)

I don't see an easy way around this at the moment unless dropping support for PHP 8.0 is an option.

Thoughts @Ocramius ?

Copy link
Member

Choose a reason for hiding this comment

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

The FilterPluginManager will just new the filter without arguments unless a factory is created…

Correct! 👍🏻

{
if ($enumOrOptions instanceof Traversable) {
$enumOrOptions = ArrayUtils::iteratorToArray($enumOrOptions);
}

if (
is_array($enumOrOptions) &&
isset($enumOrOptions['enum'])
) {
$this->setEnum($enumOrOptions['enum']);

return;
}

if (is_string($enumOrOptions)) {
$this->setEnum($enumOrOptions);
}
}

/**
* @param class-string<UnitEnum> $enum
*/
protected function setEnum(string $enum): self
gsteel marked this conversation as resolved.
Show resolved Hide resolved
{
$this->enumClass = $enum;

return $this;
}

/**
* Defined by Laminas\Filter\FilterInterface
*
* Returns an enum representation of $value or null
*
* @param mixed $value
* @return UnitEnum|null
gsteel marked this conversation as resolved.
Show resolved Hide resolved
*/
reinfi marked this conversation as resolved.
Show resolved Hide resolved
public function filter($value): ?UnitEnum
{
$enum = $this->enumClass;

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')) {
gsteel marked this conversation as resolved.
Show resolved Hide resolved
return $enum::tryFrom($value);
}

if (! is_string($value) || ! is_subclass_of($enum, 'UnitEnum')) {
gsteel marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

foreach ($enum::cases() as $enumCase) {
if ($enumCase->name === $value) {
return $enumCase;
}
}
reinfi marked this conversation as resolved.
Show resolved Hide resolved

return null;
}
}
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;
}
83 changes: 83 additions & 0 deletions test/ToEnumTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace LaminasTest\Filter;

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
{
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));
}

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::assertNull($filter->filter($value));
}

public function testThrowsExceptionIfEnumNotSet(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('enum class not set');

$filter = new ToEnum([]);

$filter->filter('foo');
}
}