diff --git a/docs/book/v3/migration/v2-to-v3.md b/docs/book/v3/migration/v2-to-v3.md index b346ac55..bb6939f4 100644 --- a/docs/book/v3/migration/v2-to-v3.md +++ b/docs/book/v3/migration/v2-to-v3.md @@ -116,6 +116,24 @@ The following methods have been removed: The constructor now only accepts an associative array of [documented options](../standard-filters.md#denylist). +#### `MonthSelect` + +The following methods have been removed: + +- `setOptions` +- `getOptions` +- `setNullOnAllEmpty` +- `isNullOnAllEmpty` +- `setNullOnEmpty` +- `isNullOnEmpty` + +The constructor now only accepts an associative array of [documented options](../standard-filters.md#monthselect). + +RuntimeException are no longer thrown when the filter receives an array with the incorrect number of elements. + +All invalid values passed to the filter, including out of range months and years, will now return the original value. +Validators should be used to ensure the input has been filtered as expected, and to enforce any additional constraints. + #### `PregReplace` The following methods have been removed: diff --git a/docs/book/v3/standard-filters.md b/docs/book/v3/standard-filters.md index f620d8b8..47a40c4d 100644 --- a/docs/book/v3/standard-filters.md +++ b/docs/book/v3/standard-filters.md @@ -704,6 +704,29 @@ print $filter->filter('-4.4'); This will return `-4.4` (as a float). +## MonthSelect + +`Laminas\Filter\MonthSelect` allows you to filter a month and year value into a hyphen dash string. + +### Supported Options + +The following options are supported for `Laminas\Filter\MonthSelect`: + +- `null_on_empty` => This defaults to `false`. +If set to `true`, the filter will return `null` if either month or year is empty. +- `null_on_all_empty` => This defaults to `false`. +If set to `true`, the filter will return `null` if both month and year are empty. + +### Basic Usage + +```php +$filter = new Laminas\Filter\MonthSelect(); + +print $filter->filter(['month' => '2', 'year' => '2012']); +```` + +This will return '2012-02'. + ## ToInt `Laminas\Filter\ToInt` allows you to transform a scalar value into an integer. diff --git a/src/MonthSelect.php b/src/MonthSelect.php index d90ca512..7bd4292d 100644 --- a/src/MonthSelect.php +++ b/src/MonthSelect.php @@ -4,24 +4,88 @@ namespace Laminas\Filter; +use function filter_var; +use function is_array; +use function is_numeric; +use function sprintf; + +use const FILTER_VALIDATE_INT; + /** * @psalm-type Options = array{ * null_on_empty?: bool, * null_on_all_empty?: bool, - * ... * } - * @psalm-type InputArray = array{ - * year: numeric, - * month: numeric, - * } - * @template TOptions of Options - * @template-extends AbstractDateDropdown + * @implements FilterInterface */ -final class MonthSelect extends AbstractDateDropdown +final class MonthSelect implements FilterInterface { + private readonly bool $returnNullIfAnyFieldEmpty; + private readonly bool $returnNullIfAllFieldsEmpty; + + /** @param Options $options */ + public function __construct(array $options = []) + { + $this->returnNullIfAnyFieldEmpty = $options['null_on_empty'] ?? false; + $this->returnNullIfAllFieldsEmpty = $options['null_on_all_empty'] ?? false; + } + + public function __invoke(mixed $value): mixed + { + return $this->filter($value); + } + /** - * Year-Month + * Returns the result of filtering $value + * + * @template T + * @param T $value + * @return string|null|T */ - protected string $format = '%2$s-%1$s'; - protected int $expectedInputs = 2; + public function filter(mixed $value): mixed + { + if (! is_array($value)) { + return $value; + } + + $month = $value['month'] ?? null; + /** @var mixed $month */ + $month = $month === '' ? null : $month; + + $year = $value['year'] ?? null; + /** @var mixed $year */ + $year = $year === '' ? null : $year; + + if ($this->returnNullIfAnyFieldEmpty && ($month === null || $year === null)) { + return null; + } + + if ($this->returnNullIfAllFieldsEmpty && $month === null && $year === null) { + return null; + } + + if (! $this->isParsableAsDateValue($month, 1, 12) || ! $this->isParsableAsDateValue($year, 0, 9999)) { + /** @psalm-var T */ + return $value; + } + + return sprintf('%d-%02d', $year, $month); + } + + /** @psalm-assert-if-true int $value */ + private function isParsableAsDateValue(mixed $value, int $lowestValue, int $highestValue): bool + { + 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 bad755bb..03d2692d 100644 --- a/test/MonthSelectTest.php +++ b/test/MonthSelectTest.php @@ -4,37 +4,65 @@ namespace LaminasTest\Filter; -use Laminas\Filter\Exception\RuntimeException; use Laminas\Filter\MonthSelect as MonthSelectFilter; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +/** @psalm-import-type Options from MonthSelectFilter */ class MonthSelectTest extends TestCase { + /** @param Options $options */ #[DataProvider('provideFilter')] public function testFilter(array $options, array $input, ?string $expected): void { - $sut = new MonthSelectFilter(); - $sut->setOptions($options); + $sut = new MonthSelectFilter($options); + self::assertSame($expected, $sut->filter($input)); } - /** @return list */ + /** @return list */ public static function provideFilter(): array { return [ + [[], ['year' => '2014', 'month' => '2'], '2014-02'], [[], ['year' => '2014', 'month' => '10'], '2014-10'], - [['nullOnEmpty' => true], ['year' => null, 'month' => '10'], null], + [[], ['year' => 2014, 'month' => 10], '2014-10'], [['null_on_empty' => true], ['year' => null, 'month' => '10'], null], - [['nullOnAllEmpty' => true], ['year' => null, 'month' => null], 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], + [['null_on_all_empty' => true], ['year' => '', 'month' => ''], null], ]; } - public function testInvalidInput(): void + #[DataProvider('provideInvalidFilterValues')] + public function testInvalidInput(mixed $value): void { - $this->expectException(RuntimeException::class); $sut = new MonthSelectFilter(); - $sut->filter(['year' => '2120']); + + self::assertSame($value, $sut->filter($value)); + } + + /** @return array */ + public static function provideInvalidFilterValues(): array + { + return [ + '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']], + ]; } }