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

Refactor MonthSelect removing inheritance and improving tests #200

Merged
merged 1 commit into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
gsteel marked this conversation as resolved.
Show resolved Hide resolved
{
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
gsteel marked this conversation as resolved.
Show resolved Hide resolved
{
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
gsteel marked this conversation as resolved.
Show resolved Hide resolved
{
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']],
];
}
}
Loading