-
-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #274 from gsteel/v2/number-comparison
Introduce NumberComparison validator
- Loading branch information
Showing
8 changed files
with
330 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# Number Comparison Validator | ||
|
||
`Laminas\Validator\NumberComparison` allows you to validate if a given value is a numeric value that is either: | ||
|
||
- Between a min and max value | ||
- Greater than a min value | ||
- Less than a max value | ||
|
||
By default, comparisons are inclusive. | ||
|
||
CAUTION: **Only supports number validation** | ||
`Laminas\Validator\NumberComparison` supports only the validation of numbers. | ||
Strings or dates can not be validated with this validator. | ||
|
||
## Supported Options | ||
|
||
The following options are supported for `Laminas\Validator\NumberComparison`: | ||
|
||
| Option | Data Type | Default Value | Description | | ||
|----------------|-----------|---------------|--------------------------------------------------------------------------| | ||
| `max` | `numeric` | `null` | Sets the upper bound for the input. | | ||
| `min` | `numeric` | `null` | Sets the lower bound for the input. | | ||
| `inclusiveMin` | `bool` | `true` | Defines if the validation is inclusive of the lower bound, or exclusive. | | ||
| `inclusiveMax` | `bool` | `true` | Defines if the validation is inclusive of the upper bound, or exclusive. | | ||
|
||
## Basic Usage | ||
|
||
Per default, this validator checks if a value is between `min` and `max` where both upper and lower bounds are considered valid. | ||
|
||
```php | ||
$valid = new Laminas\Validator\NumberComparison(['min' => 0, 'max' => 10]); | ||
$value = 10; | ||
$result = $valid->isValid($value); | ||
// returns true | ||
``` | ||
|
||
In the above example, the result is `true` due to the reason that the default search is inclusive of the border values. | ||
This means in our case that any value from `0` to `10` is allowed; values like `-1` and `11` will return `false`. | ||
|
||
## Excluding Upper and Lower Bounds | ||
|
||
Sometimes it is useful to validate a value by excluding the bounds. See the following example: | ||
|
||
```php | ||
$valid = new Laminas\Validator\NumberComparison([ | ||
'min' => 0, | ||
'max' => 10, | ||
'inclusiveMin' => false, | ||
'inclusiveMax' => false, | ||
]); | ||
|
||
$valid->isValid(10); // false | ||
$valid->isValid(0); // false | ||
$valid->isValid(9); // true | ||
``` | ||
|
||
The example above is almost identical to our first example, but we now exclude the bounds as valid values; as such, the values `0` and `10` are no longer allowed and will return `false`. | ||
|
||
## Min and Max behaviour | ||
|
||
In order to validate a number that is simply greater than a lower bound, either omit the `max` option, or set it explicitly to `null`: | ||
|
||
```php | ||
$validator = new Laminas\Validator\NumberComparison(['min' => 10, 'max' => null]); | ||
$validator->isValid(12345); // true | ||
``` | ||
|
||
Conversely, to ensure a number is less than an upper bound, omit the `min` option or explicitly set it to `null`: | ||
|
||
```php | ||
$validator = new Laminas\Validator\NumberComparison(['max' => 5]); | ||
$validator->isValid(99); // false | ||
``` | ||
|
||
You *must* provide one of the `min` or the `max` _(or both)_ options or an exception will be thrown. | ||
It doesn't make sense to compare the input to nothing for this validator. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace Laminas\Validator; | ||
|
||
use Laminas\Validator\Exception\InvalidArgumentException; | ||
|
||
use function is_numeric; | ||
|
||
/** | ||
* @psalm-type OptionsArgument = array{ | ||
* min?: numeric|null, | ||
* max?: numeric|null, | ||
* inclusiveMin?: bool, | ||
* inclusiveMax?: bool, | ||
* ...<string, mixed> | ||
* } | ||
* @psalm-type Options = array{ | ||
* min: numeric|null, | ||
* max: numeric|null, | ||
* inclusiveMin: bool, | ||
* inclusiveMax: bool, | ||
* } | ||
*/ | ||
final class NumberComparison extends AbstractValidator | ||
{ | ||
public const ERROR_NOT_NUMERIC = 'notNumeric'; | ||
public const ERROR_NOT_GREATER_INCLUSIVE = 'notGreaterInclusive'; | ||
public const ERROR_NOT_GREATER = 'notGreater'; | ||
public const ERROR_NOT_LESS_INCLUSIVE = 'notLessInclusive'; | ||
public const ERROR_NOT_LESS = 'notLess'; | ||
|
||
/** @var array<string, string> */ | ||
protected array $messageTemplates = [ | ||
self::ERROR_NOT_NUMERIC => 'Expected a numeric value', | ||
self::ERROR_NOT_GREATER_INCLUSIVE => 'Values must be greater than or equal to %min%. Received "%value%"', | ||
self::ERROR_NOT_GREATER => 'Values must be greater than %min%. Received "%value%', | ||
self::ERROR_NOT_LESS_INCLUSIVE => 'Values must be less than or equal to %max%. Received "%value%"', | ||
self::ERROR_NOT_LESS => 'Values must be less than %max%. Received "%value%"', | ||
]; | ||
|
||
/** @var array<string, array<string, string>> */ | ||
protected array $messageVariables = [ | ||
'min' => ['options' => 'min'], | ||
'max' => ['options' => 'max'], | ||
]; | ||
|
||
/** @var Options */ | ||
protected array $options = [ | ||
'min' => null, | ||
'max' => null, | ||
'inclusiveMin' => true, | ||
'inclusiveMax' => true, | ||
]; | ||
|
||
/** @param OptionsArgument $options */ | ||
public function __construct(array $options = []) | ||
{ | ||
parent::__construct($options); | ||
|
||
$min = $options['min'] ?? null; | ||
$max = $options['max'] ?? null; | ||
|
||
if (! is_numeric($min) && ! is_numeric($max)) { | ||
throw new InvalidArgumentException( | ||
'A numeric option value for either min, max or both must be provided', | ||
); | ||
} | ||
|
||
if ($min !== null && $max !== null && $min > $max) { | ||
throw new InvalidArgumentException( | ||
'The minimum constraint cannot be greater than the maximum constraint', | ||
); | ||
} | ||
|
||
$this->options['min'] = $min; | ||
$this->options['max'] = $max; | ||
$this->options['inclusiveMin'] = $options['inclusiveMin'] ?? true; | ||
$this->options['inclusiveMax'] = $options['inclusiveMax'] ?? true; | ||
} | ||
|
||
public function isValid(mixed $value): bool | ||
{ | ||
if (! is_numeric($value)) { | ||
$this->error(self::ERROR_NOT_NUMERIC); | ||
|
||
return false; | ||
} | ||
|
||
$this->setValue($value); | ||
|
||
$min = $this->options['min']; | ||
$max = $this->options['max']; | ||
$inclusiveMin = $this->options['inclusiveMin']; | ||
$inclusiveMax = $this->options['inclusiveMax']; | ||
|
||
if ($min !== null && $inclusiveMin && $value < $min) { | ||
$this->error(self::ERROR_NOT_GREATER_INCLUSIVE); | ||
|
||
return false; | ||
} | ||
|
||
if ($min !== null && ! $inclusiveMin && $value <= $min) { | ||
$this->error(self::ERROR_NOT_GREATER); | ||
|
||
return false; | ||
} | ||
|
||
if ($max !== null && $inclusiveMax && $value > $max) { | ||
$this->error(self::ERROR_NOT_LESS_INCLUSIVE); | ||
|
||
return false; | ||
} | ||
|
||
if ($max !== null && ! $inclusiveMax && $value >= $max) { | ||
$this->error(self::ERROR_NOT_LESS); | ||
|
||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace LaminasTest\Validator; | ||
|
||
use Laminas\Validator\Exception\InvalidArgumentException; | ||
use Laminas\Validator\NumberComparison; | ||
use PHPUnit\Framework\Attributes\DataProvider; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
use const PHP_INT_MAX; | ||
|
||
class NumberComparisonTest extends TestCase | ||
{ | ||
public static function basicTestDataProvider(): array | ||
{ | ||
return [ | ||
[null, 10, true, 9, true, null], | ||
[null, 10, true, 10, true, null], | ||
[null, 10, true, 11, false, NumberComparison::ERROR_NOT_LESS_INCLUSIVE], | ||
[null, 10, false, 9, true, null], | ||
[null, 10, false, 10, false, NumberComparison::ERROR_NOT_LESS], | ||
[null, 10, false, 11, false, NumberComparison::ERROR_NOT_LESS], | ||
[10, null, true, 11, true, null], | ||
[10, null, true, 10, true, null], | ||
[10, null, true, 9, false, NumberComparison::ERROR_NOT_GREATER_INCLUSIVE], | ||
[10, null, false, 11, true, null], | ||
[10, null, false, 10, false, NumberComparison::ERROR_NOT_GREATER], | ||
[10, null, false, 9, false, NumberComparison::ERROR_NOT_GREATER], | ||
// Numerics should validate successfully | ||
[10, null, true, PHP_INT_MAX, true, null], | ||
[10, null, true, '99', true, null], | ||
[10, null, true, 99.9, true, null], | ||
[10, null, true, '99.9', true, null], | ||
// Non numerics should fail regardless of options | ||
[10, null, true, true, false, NumberComparison::ERROR_NOT_NUMERIC], | ||
[10, null, true, null, false, NumberComparison::ERROR_NOT_NUMERIC], | ||
[10, null, true, '', false, NumberComparison::ERROR_NOT_NUMERIC], | ||
[10, null, true, 'muppets', false, NumberComparison::ERROR_NOT_NUMERIC], | ||
[10, null, true, ['foo'], false, NumberComparison::ERROR_NOT_NUMERIC], | ||
[10, null, true, [1], false, NumberComparison::ERROR_NOT_NUMERIC], | ||
// Floats behave as expected both as options and input | ||
[10.0, 20.0, true, 10.1, true, null], | ||
[10.0, 20.0, true, 15, true, null], | ||
[10.0, 20.0, true, '15', true, null], | ||
[10.0, 20.0, true, 19.9999999, true, null], | ||
[10.0, 20.0, true, 4.2, false, NumberComparison::ERROR_NOT_GREATER_INCLUSIVE], | ||
[10.0, 20.0, true, 20.000001, false, NumberComparison::ERROR_NOT_LESS_INCLUSIVE], | ||
// Numeric strings behave as expected both as options and input | ||
['10', '20', true, 10.1, true, null], | ||
['10', '20', true, 15, true, null], | ||
['10', '20', true, '15', true, null], | ||
['10', '20', true, '19.9999999', true, null], | ||
['10', '20', true, '4.2', false, NumberComparison::ERROR_NOT_GREATER_INCLUSIVE], | ||
['10', '20', true, '20.000001', false, NumberComparison::ERROR_NOT_LESS_INCLUSIVE], | ||
]; | ||
} | ||
|
||
/** | ||
* @param numeric|null $min | ||
* @param numeric|null $max | ||
*/ | ||
#[DataProvider('basicTestDataProvider')] | ||
public function testBasic( | ||
int|float|string|null $min, | ||
int|float|string|null $max, | ||
bool $inclusive, | ||
mixed $input, | ||
bool $expect, | ||
string|null $errorKey, | ||
): void { | ||
$validator = new NumberComparison([ | ||
'min' => $min, | ||
'max' => $max, | ||
'inclusiveMin' => $inclusive, | ||
'inclusiveMax' => $inclusive, | ||
]); | ||
|
||
self::assertSame($expect, $validator->isValid($input)); | ||
|
||
if ($errorKey === null) { | ||
return; | ||
} | ||
|
||
self::assertArrayHasKey($errorKey, $validator->getMessages()); | ||
} | ||
|
||
public function testOmittingBothMinAndMaxIsExceptional(): void | ||
{ | ||
$this->expectException(InvalidArgumentException::class); | ||
$this->expectExceptionMessage('A numeric option value for either min, max or both must be provided'); | ||
|
||
new NumberComparison(); | ||
} | ||
|
||
public function testThatMinAndMaxMustBeSane(): void | ||
{ | ||
$this->expectException(InvalidArgumentException::class); | ||
$this->expectExceptionMessage('The minimum constraint cannot be greater than the maximum constraint'); | ||
|
||
new NumberComparison([ | ||
'min' => 10, | ||
'max' => 5, | ||
]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters