Skip to content

Commit

Permalink
Refactor MonthSelect
Browse files Browse the repository at this point in the history
Signed-off-by: ramchale <[email protected]>
  • Loading branch information
ramchale committed Dec 9, 2024
1 parent 08e0bb9 commit 5ead0a6
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 20 deletions.
18 changes: 18 additions & 0 deletions docs/book/v3/migration/v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
23 changes: 23 additions & 0 deletions docs/book/v3/standard-filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
86 changes: 75 additions & 11 deletions src/MonthSelect.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<TOptions, InputArray>
* @implements FilterInterface<string|null>
*/
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;
}
}
46 changes: 37 additions & 9 deletions test/MonthSelectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<array{0: array, 1: array, 2: string|null}> */
/** @return list<array{0: Options, 1: array, 2: string|null}> */
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<string, array{0: mixed}> */
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']],
];
}
}

0 comments on commit 5ead0a6

Please sign in to comment.