From b56a4b1386a1d1ae9776309d35b6e3e62e342cc8 Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 13 Jun 2024 22:59:45 +0100 Subject: [PATCH] Refactor `Boolean` filter - Removes inheritance - Changes constructor signature to only accept a well-defined array of options - Removes option getters and setters - Marks class as immutable - Fixes all psalm issues in src and tests - Expands tests Signed-off-by: George Steel --- psalm-baseline.xml | 73 ---- src/Boolean.php | 211 ++++-------- test/BooleanTest.php | 363 +++++++++++++------- test/StaticAnalysis/BooleanFilterChecks.php | 12 +- 4 files changed, 326 insertions(+), 333 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index dd8b22b3..ad5daa78 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -32,48 +32,6 @@ - - - - - - - - - - ]]> - - - options['translations'][$message]]]> - - - - - - - - - - options['casting']]]> - options['type']]]> - - - - - - options['casting']]]> - options['type']]]> - - - - - - - - - - - options['callback'], $params)]]> @@ -838,37 +796,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Boolean.php b/src/Boolean.php index 3ba81160..7fa66639 100644 --- a/src/Boolean.php +++ b/src/Boolean.php @@ -4,29 +4,34 @@ namespace Laminas\Filter; -use Laminas\Stdlib\ArrayUtils; -use Traversable; - +use function array_merge; use function array_search; -use function get_debug_type; +use function assert; use function gettype; +use function in_array; use function is_array; use function is_bool; -use function is_float; use function is_int; use function is_string; use function sprintf; use function strtolower; /** - * @psalm-type Options = array{ - * type?: int-mask-of, + * @psalm-immutable + * phpcs:disable Generic.Files.LineLength + * @psalm-type TypeOption = int-mask-of|list|list>|value-of + * @psalm-type OptionsArgument = array{ + * type?: TypeOption, * casting?: bool, - * translations?: array, + * translations?: array, + * } + * @psalm-type Options = array{ + * type: int-mask-of, + * casting: bool, + * translations: array, * } - * @extends AbstractFilter */ -final class Boolean extends AbstractFilter +final class Boolean implements FilterInterface { public const TYPE_BOOLEAN = 1; public const TYPE_INTEGER = 2; @@ -55,161 +60,82 @@ final class Boolean extends AbstractFilter ]; /** @var Options */ - protected $options = [ - 'type' => self::TYPE_PHP, - 'casting' => true, - 'translations' => [], - ]; + private readonly array $options; /** - * phpcs:ignore Generic.Files.LineLength.TooLong - * @param self::TYPE_*|value-of|list|int-mask-of|Options|iterable|null $typeOrOptions - * @param bool $casting - * @param array $translations + * @param OptionsArgument $options */ - public function __construct($typeOrOptions = null, $casting = true, $translations = []) + public function __construct(array $options = []) { - if ($typeOrOptions instanceof Traversable) { - $typeOrOptions = ArrayUtils::iteratorToArray($typeOrOptions); - } - - if ( - is_array($typeOrOptions) && ( - isset($typeOrOptions['type']) - || isset($typeOrOptions['casting']) - || isset($typeOrOptions['translations']) - ) - ) { - $this->setOptions($typeOrOptions); - - return; - } - - if (is_array($typeOrOptions) || is_int($typeOrOptions) || is_string($typeOrOptions)) { - $this->setType($typeOrOptions); - } - - $this->setCasting($casting); - $this->setTranslations($translations); + $defaults = [ + 'type' => self::TYPE_PHP, + 'casting' => true, + 'translations' => [], + ]; + + $options = array_merge($defaults, $options); + $options['type'] = $this->resolveType($options['type']); + $this->options = $options; } /** - * Set boolean types + * Resolve int-mask type from various options * - * @param self::TYPE_*|int-mask-of|value-of|list|null $type + * @param int-mask-of|list|list>|value-of $type + * @return int-mask-of * @throws Exception\InvalidArgumentException - * @return self */ - public function setType($type = null) + private function resolveType(array|int|string $type): int { + if (is_int($type) && ($type & self::TYPE_ALL) !== 0) { + return $type; + } + + if (is_string($type) && in_array($type, self::CONSTANTS, true)) { + $type = array_search($type, self::CONSTANTS, true); + assert(is_int($type)); + + return $type; + } + if (is_array($type)) { $detected = 0; foreach ($type as $value) { if (is_int($value)) { + assert(($value & self::TYPE_ALL) !== 0); $detected |= $value; - } elseif (($found = array_search($value, self::CONSTANTS, true)) !== false) { + } else { + $found = array_search($value, self::CONSTANTS, true); + assert(is_int($found)); + $detected |= $found; } } - $type = $detected; - } elseif (is_string($type) && ($found = array_search($type, self::CONSTANTS, true)) !== false) { - $type = $found; - } - - if (! is_int($type) || ($type < 0) || ($type > self::TYPE_ALL)) { - throw new Exception\InvalidArgumentException(sprintf( - 'Unknown type value "%s" (%s)', - $type, - gettype($type) - )); - } - - $this->options['type'] = $type; - return $this; - } - - /** - * Returns defined boolean types - * - * @return int-mask-of - */ - public function getType() - { - return $this->options['type']; - } - - /** - * Set the working mode - * - * @param bool $flag When true this filter works like cast - * When false it recognises only true and false - * and all other values are returned as is - * @return self - */ - public function setCasting($flag = true) - { - $this->options['casting'] = (bool) $flag; - return $this; - } - - /** - * Returns the casting option - * - * @return bool - */ - public function getCasting() - { - return $this->options['casting']; - } - - /** - * @param array|Traversable $translations - * @throws Exception\InvalidArgumentException - * @return self - */ - public function setTranslations($translations) - { - if (! is_array($translations) && ! $translations instanceof Traversable) { - throw new Exception\InvalidArgumentException(sprintf( - '"%s" expects an array or Traversable; received "%s"', - __METHOD__, - get_debug_type($translations) - )); - } - - foreach ($translations as $message => $flag) { - $this->options['translations'][$message] = (bool) $flag; + /** @psalm-var int-mask-of */ + return $detected; } - return $this; + throw new Exception\InvalidArgumentException(sprintf( + 'Unknown type value "%s" (%s)', + $type, + gettype($type), + )); } /** - * @return array - */ - public function getTranslations() - { - return $this->options['translations'] ?? []; - } - - /** - * Defined by Laminas\Filter\FilterInterface - * * Returns a boolean representation of $value - * - * @param null|array|bool|float|int|string $value */ public function filter(mixed $value): mixed { - $type = $this->getType(); - $casting = $this->getCasting(); + $type = $this->options['type']; + $casting = $this->options['casting']; // LOCALIZED if ($type & self::TYPE_LOCALIZED) { if (is_string($value)) { if (isset($this->options['translations'][$value])) { - return (bool) $this->options['translations'][$value]; + return $this->options['translations'][$value]; } } } @@ -220,7 +146,7 @@ public function filter(mixed $value): mixed return false; } - if (! $casting && is_string($value) && strtolower($value) === 'true') { + if (is_string($value) && strtolower($value) === 'true') { return true; } } @@ -234,47 +160,47 @@ public function filter(mixed $value): mixed // EMPTY_ARRAY (array()) if ($type & self::TYPE_EMPTY_ARRAY) { - if (is_array($value) && $value === []) { + if ($value === []) { return false; } } // ZERO_STRING ('0') if ($type & self::TYPE_ZERO_STRING) { - if (is_string($value) && $value === '0') { + if ($value === '0') { return false; } - if (! $casting && is_string($value) && $value === '1') { + if (! $casting && $value === '1') { return true; } } // STRING ('') if ($type & self::TYPE_STRING) { - if (is_string($value) && $value === '') { + if ($value === '') { return false; } } // FLOAT (0.0) if ($type & self::TYPE_FLOAT) { - if (is_float($value) && $value === 0.0) { + if ($value === 0.0) { return false; } - if (! $casting && is_float($value) && $value === 1.0) { + if (! $casting && $value === 1.0) { return true; } } // INTEGER (0) if ($type & self::TYPE_INTEGER) { - if (is_int($value) && $value === 0) { + if ($value === 0) { return false; } - if (! $casting && is_int($value) && $value === 1) { + if (! $casting && $value === 1) { return true; } } @@ -292,4 +218,9 @@ public function filter(mixed $value): mixed return $value; } + + public function __invoke(mixed $value): mixed + { + return $this->filter($value); + } } diff --git a/test/BooleanTest.php b/test/BooleanTest.php index ed8f31be..03a640f9 100644 --- a/test/BooleanTest.php +++ b/test/BooleanTest.php @@ -4,7 +4,7 @@ namespace LaminasTest\Filter; -use Laminas\Filter\Boolean as BooleanFilter; +use Laminas\Filter\Boolean; use Laminas\Filter\Exception; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -13,78 +13,247 @@ use function sprintf; use function var_export; +/** + * @psalm-import-type TypeOption from Boolean + * @psalm-import-type OptionsArgument from Boolean + */ class BooleanTest extends TestCase { - public function testConstructorOptions(): void + /** + * @return list + * @psalm-suppress PossiblyUnusedMethod + */ + public static function integerProvider(): array { - $filter = new BooleanFilter([ - 'type' => BooleanFilter::TYPE_INTEGER, - 'casting' => false, - ]); + return [ + [['type' => Boolean::TYPE_INTEGER, 'casting' => false], 1, true], + [['type' => Boolean::TYPE_INTEGER, 'casting' => false], 0, false], + [['type' => Boolean::TYPE_INTEGER, 'casting' => true], 1, true], + [['type' => Boolean::TYPE_INTEGER, 'casting' => true], 0, false], + [['type' => Boolean::TYPE_INTEGER, 'casting' => true], 99, true], + [['type' => Boolean::TYPE_INTEGER, 'casting' => false], 99, 99], + [['type' => Boolean::TYPE_INTEGER, 'casting' => false], null, null], + [['type' => [Boolean::TYPE_INTEGER], 'casting' => false], 1, true], + [['type' => 'integer', 'casting' => false], 1, true], + [['type' => ['integer'], 'casting' => false], 1, true], + ]; + } - self::assertSame(BooleanFilter::TYPE_INTEGER, $filter->getType()); - self::assertFalse($filter->getCasting()); + /** + * @return list + * @psalm-suppress PossiblyUnusedMethod + */ + public static function floatProvider(): array + { + return [ + [['type' => Boolean::TYPE_FLOAT, 'casting' => false], 1.0, true], + [['type' => Boolean::TYPE_FLOAT, 'casting' => false], 0.0, false], + [['type' => Boolean::TYPE_FLOAT, 'casting' => true], 1.0, true], + [['type' => Boolean::TYPE_FLOAT, 'casting' => true], 0.0, false], + [['type' => Boolean::TYPE_FLOAT, 'casting' => true], 99.9, true], + [['type' => Boolean::TYPE_FLOAT, 'casting' => false], 99.9, 99.9], + [['type' => Boolean::TYPE_FLOAT, 'casting' => false], null, null], + [['type' => [Boolean::TYPE_FLOAT], 'casting' => false], 1.0, true], + [['type' => 'float', 'casting' => false], 1.0, true], + [['type' => ['float'], 'casting' => false], 1.0, true], + ]; } - public function testConstructorParams(): void + /** + * @return list + * @psalm-suppress PossiblyUnusedMethod + */ + public static function booleanProvider(): array { - $filter = new BooleanFilter(BooleanFilter::TYPE_INTEGER, false); + return [ + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => false], true, true], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => false], false, false], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => false], 'foo', 'foo'], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => false], 'true', 'true'], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => false], 1, 1], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => false], 0, 0], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => false], [], []], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => true], true, true], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => true], false, false], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => true], 'foo', true], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => true], 'true', true], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => true], 1, true], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => true], 0, true], + [['type' => Boolean::TYPE_BOOLEAN, 'casting' => true], [], true], + [['type' => [Boolean::TYPE_BOOLEAN], 'casting' => false], true, true], + [['type' => ['boolean'], 'casting' => false], true, true], + [['type' => 'boolean', 'casting' => false], true, true], + ]; + } - self::assertSame(BooleanFilter::TYPE_INTEGER, $filter->getType()); - self::assertFalse($filter->getCasting()); + /** + * @return list + * @psalm-suppress PossiblyUnusedMethod + */ + public static function stringProvider(): array + { + return [ + [['type' => Boolean::TYPE_STRING, 'casting' => false], 'foo', 'foo'], + [['type' => Boolean::TYPE_STRING, 'casting' => false], '', false], + [['type' => Boolean::TYPE_STRING, 'casting' => true], 'foo', true], + [['type' => Boolean::TYPE_STRING, 'casting' => true], '', false], + [['type' => Boolean::TYPE_STRING, 'casting' => true], ' ', true], + [['type' => Boolean::TYPE_STRING, 'casting' => true], "\t", true], + [['type' => Boolean::TYPE_STRING, 'casting' => true], "\n", true], + [['type' => [Boolean::TYPE_STRING], 'casting' => false], 'foo', 'foo'], + [['type' => ['string'], 'casting' => false], 'foo', 'foo'], + [['type' => 'string', 'casting' => false], 'foo', 'foo'], + ]; } - #[DataProvider('defaultTestProvider')] - public function testDefault(mixed $value, bool $expected): void + /** + * @return list + * @psalm-suppress PossiblyUnusedMethod + */ + public static function falseStringProvider(): array { - $filter = new BooleanFilter(); - self::assertSame($expected, $filter->filter($value)); + return [ + [['type' => Boolean::TYPE_FALSE_STRING, 'casting' => false], 'true', true], + [['type' => Boolean::TYPE_FALSE_STRING, 'casting' => false], 'false', false], + [['type' => Boolean::TYPE_FALSE_STRING, 'casting' => true], 'true', true], + [['type' => Boolean::TYPE_FALSE_STRING, 'casting' => true], 'false', false], + [['type' => [Boolean::TYPE_FALSE_STRING], 'casting' => false], 'false', false], + [['type' => ['false'], 'casting' => false], 'false', false], + [['type' => 'false', 'casting' => false], 'false', false], + ]; + } + + /** + * @return list + * @psalm-suppress PossiblyUnusedMethod + */ + public static function nullProvider(): array + { + return [ + [['type' => Boolean::TYPE_NULL, 'casting' => false], null, false], + [['type' => Boolean::TYPE_NULL, 'casting' => true], null, false], + [['type' => Boolean::TYPE_NULL, 'casting' => true], 'false', true], + [['type' => [Boolean::TYPE_NULL], 'casting' => false], 'false', 'false'], + [['type' => ['null'], 'casting' => false], null, false], + [['type' => 'null', 'casting' => false], null, false], + ]; + } + + /** + * @return list + * @psalm-suppress PossiblyUnusedMethod + */ + public static function zeroStringProvider(): array + { + return [ + [['type' => Boolean::TYPE_ZERO_STRING, 'casting' => false], '0', false], + [['type' => Boolean::TYPE_ZERO_STRING, 'casting' => false], '1', true], + [['type' => Boolean::TYPE_ZERO_STRING, 'casting' => true], '0', false], + [['type' => Boolean::TYPE_ZERO_STRING, 'casting' => true], '1', true], + [['type' => [Boolean::TYPE_ZERO_STRING], 'casting' => false], '0', false], + [['type' => ['zero'], 'casting' => false], '0', false], + [['type' => 'zero', 'casting' => false], '0', false], + ]; + } + + /** + * @return list + * @psalm-suppress PossiblyUnusedMethod + */ + public static function emptyArrayProvider(): array + { + return [ + [['type' => Boolean::TYPE_EMPTY_ARRAY, 'casting' => false], [], false], + [['type' => Boolean::TYPE_EMPTY_ARRAY, 'casting' => false], ['foo'], ['foo']], + [['type' => Boolean::TYPE_EMPTY_ARRAY, 'casting' => true], [], false], + [['type' => Boolean::TYPE_EMPTY_ARRAY, 'casting' => true], ['foo'], true], + [['type' => [Boolean::TYPE_EMPTY_ARRAY], 'casting' => false], [], false], + [['type' => ['array'], 'casting' => false], [], false], + [['type' => 'array', 'casting' => false], [], false], + ]; + } + + /** @param OptionsArgument $options */ + #[DataProvider('integerProvider')] + #[DataProvider('floatProvider')] + #[DataProvider('booleanProvider')] + #[DataProvider('stringProvider')] + #[DataProvider('falseStringProvider')] + #[DataProvider('nullProvider')] + #[DataProvider('zeroStringProvider')] + #[DataProvider('emptyArrayProvider')] + public function testIndividualTypes(array $options, mixed $input, mixed $expect): void + { + $filter = new Boolean($options); + + /** @psalm-var mixed $result */ + $result = $filter->filter($input); + + $message = sprintf( + 'Expected (%s) %s to be filtered to (%s) %s', + gettype($input), + var_export($input, true), + gettype($expect), + var_export($expect, true), + ); + + self::assertSame($expect, $result, $message); + self::assertSame($expect, $filter->__invoke($input)); } - #[DataProvider('noCastingTestProvider')] - public function testNoCasting(mixed $value, mixed $expected): void + #[DataProvider('defaultTestProvider')] + public function testDefault(mixed $value, bool $expected): void { - $filter = new BooleanFilter('all', false); + $filter = new Boolean(); self::assertSame($expected, $filter->filter($value)); } /** - * @param array{0: mixed, 1: mixed} $testData + * @param int-mask-of $type + * @param list $testData */ #[DataProvider('typeTestProvider')] public function testTypes(int $type, array $testData): void { - $filter = new BooleanFilter($type); + $filter = new Boolean(['type' => $type]); foreach ($testData as $data) { + /** + * @var mixed $value + * @var mixed $expected + */ [$value, $expected] = $data; $message = sprintf( '%s (%s) is not filtered as %s; type = %s', var_export($value, true), gettype($value), var_export($expected, true), - $type + $type, ); self::assertSame($expected, $filter->filter($value), $message); } } /** - * @param array $typeData - * @param array $testData + * @param list $typeData + * @param list $testData */ #[DataProvider('combinedTypeTestProvider')] - public function testCombinedTypes($typeData, $testData): void + public function testCombinedTypes(array $typeData, array $testData): void { foreach ($typeData as $type) { - $filter = new BooleanFilter(['type' => $type]); + $filter = new Boolean(['type' => $type]); foreach ($testData as $data) { + /** + * @psalm-var mixed $value + * @psalm-var mixed $expected + */ [$value, $expected] = $data; $message = sprintf( '%s (%s) is not filtered as %s; type = %s', var_export($value, true), gettype($value), var_export($expected, true), - var_export($type, true) + var_export($type, true), ); self::assertSame($expected, $filter->filter($value), $message); } @@ -93,8 +262,8 @@ public function testCombinedTypes($typeData, $testData): void public function testLocalized(): void { - $filter = new BooleanFilter([ - 'type' => BooleanFilter::TYPE_LOCALIZED, + $filter = new Boolean([ + 'type' => Boolean::TYPE_LOCALIZED, 'translations' => [ 'yes' => true, 'y' => true, @@ -111,35 +280,19 @@ public function testLocalized(): void self::assertFalse($filter->filter('nay')); } - public function testSettingFalseType(): void + public function testInvalidType(): void { - $filter = new BooleanFilter(); $this->expectException(Exception\InvalidArgumentException::class); $this->expectExceptionMessage('Unknown type value'); - /** @psalm-suppress InvalidArgument */ - $filter->setType(true); - } - public function testGettingDefaultType(): void - { - $filter = new BooleanFilter(); - self::assertSame(127, $filter->getType()); + /** @psalm-suppress InvalidArgument */ + new Boolean(['type' => 'foo']); } /** - * Ensures that if a type is specified more than once, we get the expected type, not something else. - * https://github.com/zendframework/zend-filter/issues/48 - * - * @param mixed $type Type to double initialize + * @return list + * @psalm-suppress PossiblyUnusedMethod */ - #[DataProvider('duplicateProvider')] - public function testDuplicateTypesWorkProperly(int|string $type, int $expected): void - { - $filter = new BooleanFilter([$type, $type]); - self::assertSame($expected, $filter->getType()); - } - - /** @return list */ public static function defaultTestProvider(): array { return [ @@ -163,37 +316,15 @@ public static function defaultTestProvider(): array ]; } - /** @return list */ - public static function noCastingTestProvider(): array - { - return [ - [false, false], - [true, true], - [0, false], - [1, true], - [2, 2], - [0.0, false], - [1.0, true], - [0.5, 0.5], - ['', false], - ['abc', 'abc'], - ['0', false], - ['1', true], - ['2', '2'], - [[], false], - [[0], [0]], - [null, false], - ['false', false], - ['true', true], - ]; - } - - /** @return list */ + /** + * @return list, 1: list}> + * @psalm-suppress PossiblyUnusedMethod + */ public static function typeTestProvider(): array { return [ [ - BooleanFilter::TYPE_BOOLEAN, + Boolean::TYPE_BOOLEAN, [ [false, false], [true, true], @@ -215,7 +346,7 @@ public static function typeTestProvider(): array ], ], [ - BooleanFilter::TYPE_INTEGER, + Boolean::TYPE_INTEGER, [ [false, true], [true, true], @@ -237,7 +368,7 @@ public static function typeTestProvider(): array ], ], [ - BooleanFilter::TYPE_FLOAT, + Boolean::TYPE_FLOAT, [ [false, true], [true, true], @@ -259,7 +390,7 @@ public static function typeTestProvider(): array ], ], [ - BooleanFilter::TYPE_STRING, + Boolean::TYPE_STRING, [ [false, true], [true, true], @@ -281,7 +412,7 @@ public static function typeTestProvider(): array ], ], [ - BooleanFilter::TYPE_ZERO_STRING, + Boolean::TYPE_ZERO_STRING, [ [false, true], [true, true], @@ -303,7 +434,7 @@ public static function typeTestProvider(): array ], ], [ - BooleanFilter::TYPE_EMPTY_ARRAY, + Boolean::TYPE_EMPTY_ARRAY, [ [false, true], [true, true], @@ -325,7 +456,7 @@ public static function typeTestProvider(): array ], ], [ - BooleanFilter::TYPE_NULL, + Boolean::TYPE_NULL, [ [false, true], [true, true], @@ -347,7 +478,7 @@ public static function typeTestProvider(): array ], ], [ - BooleanFilter::TYPE_PHP, + Boolean::TYPE_PHP, [ [false, false], [true, true], @@ -369,7 +500,7 @@ public static function typeTestProvider(): array ], ], [ - BooleanFilter::TYPE_FALSE_STRING, + Boolean::TYPE_FALSE_STRING, [ [false, true], [true, true], @@ -393,7 +524,7 @@ public static function typeTestProvider(): array // default behaviour with no translations provided // all values filtered as true [ - BooleanFilter::TYPE_LOCALIZED, + Boolean::TYPE_LOCALIZED, [ [false, true], [true, true], @@ -415,7 +546,7 @@ public static function typeTestProvider(): array ], ], [ - BooleanFilter::TYPE_ALL, + Boolean::TYPE_ALL, [ [false, false], [true, true], @@ -439,23 +570,27 @@ public static function typeTestProvider(): array ]; } + /** + * @return list, 1: list}> + * @psalm-suppress PossiblyUnusedMethod + */ public static function combinedTypeTestProvider(): array { return [ [ [ [ - BooleanFilter::TYPE_ZERO_STRING, - BooleanFilter::TYPE_STRING, - BooleanFilter::TYPE_BOOLEAN, + Boolean::TYPE_ZERO_STRING, + Boolean::TYPE_STRING, + Boolean::TYPE_BOOLEAN, ], [ 'zero', 'string', 'boolean', ], - BooleanFilter::TYPE_ZERO_STRING | BooleanFilter::TYPE_STRING | BooleanFilter::TYPE_BOOLEAN, - BooleanFilter::TYPE_ZERO_STRING + BooleanFilter::TYPE_STRING + BooleanFilter::TYPE_BOOLEAN, + Boolean::TYPE_ZERO_STRING | Boolean::TYPE_STRING | Boolean::TYPE_BOOLEAN, + Boolean::TYPE_ZERO_STRING + Boolean::TYPE_STRING + Boolean::TYPE_BOOLEAN, ], [ [false, false], @@ -480,32 +615,30 @@ public static function combinedTypeTestProvider(): array ]; } - /** @return list */ + /** + * @return list + * @psalm-suppress PossiblyUnusedMethod + */ public static function duplicateProvider(): array { return [ - [BooleanFilter::TYPE_BOOLEAN, BooleanFilter::TYPE_BOOLEAN], - [BooleanFilter::TYPE_INTEGER, BooleanFilter::TYPE_INTEGER], - [BooleanFilter::TYPE_FLOAT, BooleanFilter::TYPE_FLOAT], - [BooleanFilter::TYPE_STRING, BooleanFilter::TYPE_STRING], - [BooleanFilter::TYPE_ZERO_STRING, BooleanFilter::TYPE_ZERO_STRING], - [BooleanFilter::TYPE_EMPTY_ARRAY, BooleanFilter::TYPE_EMPTY_ARRAY], - [BooleanFilter::TYPE_NULL, BooleanFilter::TYPE_NULL], - [BooleanFilter::TYPE_PHP, BooleanFilter::TYPE_PHP], - [BooleanFilter::TYPE_FALSE_STRING, BooleanFilter::TYPE_FALSE_STRING], - [BooleanFilter::TYPE_LOCALIZED, BooleanFilter::TYPE_LOCALIZED], - [BooleanFilter::TYPE_ALL, BooleanFilter::TYPE_ALL], - ['boolean', BooleanFilter::TYPE_BOOLEAN], - ['integer', BooleanFilter::TYPE_INTEGER], - ['float', BooleanFilter::TYPE_FLOAT], - ['string', BooleanFilter::TYPE_STRING], - ['zero', BooleanFilter::TYPE_ZERO_STRING], - ['array', BooleanFilter::TYPE_EMPTY_ARRAY], - ['null', BooleanFilter::TYPE_NULL], - ['php', BooleanFilter::TYPE_PHP], - ['false', BooleanFilter::TYPE_FALSE_STRING], - ['localized', BooleanFilter::TYPE_LOCALIZED], - ['all', BooleanFilter::TYPE_ALL], + [['type' => [Boolean::TYPE_BOOLEAN, Boolean::TYPE_BOOLEAN], 'casting' => false]], + [['type' => ['boolean', Boolean::TYPE_BOOLEAN], 'casting' => false]], + [['type' => ['boolean', 'boolean'], 'casting' => false]], ]; } + + /** + * Ensures that if a type is specified more than once, we get the expected type, not something else. + * https://github.com/zendframework/zend-filter/issues/48 + * + * @param OptionsArgument $options + */ + #[DataProvider('duplicateProvider')] + public function testDuplicateTypesWorkProperly(array $options): void + { + $filter = new Boolean($options); + self::assertFalse($filter->filter(false)); + self::assertTrue($filter->filter(true)); + } } diff --git a/test/StaticAnalysis/BooleanFilterChecks.php b/test/StaticAnalysis/BooleanFilterChecks.php index 92f6779a..fea10efd 100644 --- a/test/StaticAnalysis/BooleanFilterChecks.php +++ b/test/StaticAnalysis/BooleanFilterChecks.php @@ -11,25 +11,27 @@ final class BooleanFilterChecks { public function constructorAcceptsSingleTypeConstant(): Filter\Boolean { - return new Filter\Boolean(Filter\Boolean::TYPE_FLOAT); + return new Filter\Boolean(['type' => Filter\Boolean::TYPE_FLOAT]); } public function constructorAcceptsListOfConstants(): Filter\Boolean { return new Filter\Boolean([ - Filter\Boolean::TYPE_EMPTY_ARRAY, - Filter\Boolean::TYPE_FALSE_STRING, + 'type' => [ + Filter\Boolean::TYPE_EMPTY_ARRAY, + Filter\Boolean::TYPE_FALSE_STRING, + ], ]); } public function constructorAcceptsIntMaskOfConstants(): Filter\Boolean { - return new Filter\Boolean(Filter\Boolean::TYPE_ALL ^ Filter\Boolean::TYPE_FLOAT); + return new Filter\Boolean(['type' => Filter\Boolean::TYPE_ALL ^ Filter\Boolean::TYPE_FLOAT]); } public function constructorAcceptsNamedType(): Filter\Boolean { - return new Filter\Boolean('localized'); + return new Filter\Boolean(['type' => 'localized']); } public function constructorAcceptsOptionsArray(): Filter\Boolean