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
88 changes: 88 additions & 0 deletions src/ToEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?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)
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) {
/** @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
*/
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');
}
}