diff --git a/src/MonthSelect.php b/src/MonthSelect.php index 555e491e..19bc1ec5 100644 --- a/src/MonthSelect.php +++ b/src/MonthSelect.php @@ -4,11 +4,12 @@ namespace Laminas\Filter; -use function array_reduce; -use function count; +use function filter_var; use function is_array; -use function ksort; -use function vsprintf; +use function is_numeric; +use function sprintf; + +use const FILTER_VALIDATE_INT; /** * @psalm-type Options = array{ @@ -16,19 +17,18 @@ * null_on_all_empty?: bool, * } * @psalm-type InputArray = array{ - * year: numeric, - * month: numeric, + * year: int, + * month: int, * } * @template TInput of array - * @implements FilterInterface + * @implements FilterInterface */ final class MonthSelect implements FilterInterface { private readonly bool $returnNullIfAnyFieldEmpty; private readonly bool $returnNullIfAllFieldsEmpty; - private const EXPECTED_INPUTS = 2; - /** Options $options */ + /** @param Options $options */ public function __construct(array $options = []) { $this->returnNullIfAnyFieldEmpty = $options['null_on_empty'] ?? false; @@ -40,44 +40,48 @@ public function __invoke(mixed $value): mixed return $this->filter($value); } - /** - * @psalm-return ($value is InputArray ? string : mixed|null) - */ + /** @psalm-suppress InvalidReturnType */ public function filter(mixed $value): mixed { if (! is_array($value)) { return $value; } - if (count($value) !== self::EXPECTED_INPUTS) { - return $value; - } + /** @var mixed $month */ + $month = $value['month'] ?? null; + /** @var mixed $year */ + $year = $value['year'] ?? null; - // Convert the date to a specific format - if ( - $this->returnNullIfAnyFieldEmpty - && array_reduce($value, self::reduce(...), false) - ) { + if ($this->returnNullIfAnyFieldEmpty && ($month === null || $year === null)) { return null; } - if ( - $this->returnNullIfAllFieldsEmpty - && array_reduce($value, self::reduce(...), true) - ) { + if ($this->returnNullIfAllFieldsEmpty && $month === null && $year === null) { return null; } - ksort($value); - - - /** @psalm-var array $value Forcing the type here because it has already been asserted */ + if (! $this->isParsableAsDateValue($month, 1, 12) || ! $this->isParsableAsDateValue($year, 0, 9999)) { + /** @psalm-suppress InvalidReturnStatement */ + return $value; + } - return vsprintf('%2$s-%1$s', $value); + /** @psalm-suppress MixedArgument */ + return sprintf('%s-%s', $year, $month); } - private static function reduce(bool $soFar, string|null $value): bool + private function isParsableAsDateValue(mixed $value, int $lowestValue, int $highestValue): bool { - return $soFar || ($value === null || $value === ''); + if ( + ! is_numeric($value) + || filter_var( + $value, + FILTER_VALIDATE_INT, + ['options' => ['min_range' => $lowestValue, 'max_range' => $highestValue]] + ) === false + ) { + return false; + } + + return true; } } diff --git a/test/MonthSelectTest.php b/test/MonthSelectTest.php index 82251975..ff2889d2 100644 --- a/test/MonthSelectTest.php +++ b/test/MonthSelectTest.php @@ -13,6 +13,7 @@ class MonthSelectTest extends TestCase #[DataProvider('provideFilter')] public function testFilter(array $options, array $input, ?string $expected): void { + /** @psalm-suppress MixedArgumentTypeCoercion */ $sut = new MonthSelectFilter($options); self::assertSame($expected, $sut->filter($input)); } @@ -22,11 +23,12 @@ public static function provideFilter(): array { return [ [[], ['year' => '2014', 'month' => '10'], '2014-10'], - [[], ['year' => '2014', 'month' => '48'], '2014-48'], - [[], ['year' => 'not a year', 'month' => '10'], 'not a year-10'], - [[], ['year' => '2023', 'month' => 'not a month'], '2023-not a month'], + [[], ['year' => 2014, 'month' => 10], '2014-10'], [['null_on_empty' => true], ['year' => null, 'month' => '10'], null], + [['null_on_empty' => true], ['month' => null], null], + [['null_on_empty' => true], ['year' => null], null], [['null_on_all_empty' => true], ['year' => null, 'month' => null], null], + [['null_on_all_empty' => true], [], null], ]; } @@ -41,13 +43,21 @@ public function testInvalidInput(mixed $value): void public static function provideInvalidFilterValues(): array { return [ - [[]], - [['month' => '10']], - [['year' => '2023']], - [true], - ['string'], - [10], - [10.5], + 'empty array' => [[]], + 'missing year' => [['month' => '10']], + 'missing month' => [['year' => '2023']], + 'passed bool' => [true], + 'passed string' => ['string'], + 'passed int' => [10], + 'passed float' => [10.5], + 'invalid keys' => [['should be year' => '2014', 'should be month' => '10']], + 'year is invalid type' => [['year' => true, 'month' => '09']], + 'year out of bounds' => [['year' => '-1', 'month' => '09']], + 'month is too high' => [['year' => '2014', 'month' => '13']], + 'month is low' => [['year' => '2014', 'month' => '0']], + 'month is invalid type' => [['year' => '2014', 'month' => true]], + 'invalid year' => [['year' => 'not a year', 'month' => '10']], + 'invalid month' => [['year' => '2023', 'month' => 'not a month']], ]; } }