diff --git a/docs/09-list-of-rules-by-category.md b/docs/09-list-of-rules-by-category.md index 0e2a6aeab..38b5e3a93 100644 --- a/docs/09-list-of-rules-by-category.md +++ b/docs/09-list-of-rules-by-category.md @@ -265,6 +265,7 @@ - [Length](rules/Length.md) - [Max](rules/Max.md) - [Min](rules/Min.md) +- [Size](rules/Size.md) ## Types diff --git a/docs/rules/Size.md b/docs/rules/Size.md index f67f73e99..e5d30b38a 100644 --- a/docs/rules/Size.md +++ b/docs/rules/Size.md @@ -1,78 +1,71 @@ # Size -- `Size(string $minSize)` -- `Size(string $minSize, string $maxSize)` -- `Size(null, string $maxSize)` +- `Size(string $unit, Rule $rule)` Validates whether the input is a file that is of a certain size or not. ```php -v::size('1KB')->isValid($filename); // Must have at least 1KB size -v::size('1MB', '2MB')->isValid($filename); // Must have the size between 1MB and 2MB -v::size(null, '1GB')->isValid($filename); // Must not be greater than 1GB +v::size('KB', v::greaterThan(1))->isValid($filename); +v::size('MB', v::between(1, 2))->isValid($filename); +v::size('GB', v::lessThan(1))->isValid($filename); ``` -Sizes are not case-sensitive and the accepted values are: +Accepted data storage units are `B`, `KB`, `MB`, `GB`, `TB`, `PB`, `EB`, `ZB`, and `YB`. -- B -- KB -- MB -- GB -- TB -- PB -- EB -- ZB -- YB +This validator will accept: -This validator will consider `SplFileInfo` instances, like: +* `string` file paths +* `SplFileInfo` objects (see [SplFileInfo][]) +* `Psr\Http\Message\UploadedFileInterface` objects (see [PSR-7][]) +* `Psr\Http\Message\StreamInterface` objects (see [PSR-7][]) -```php -v::size('1.5mb')->isValid(new SplFileInfo($filename)); // Will return true or false -``` +## Templates -Message template for this validator includes `{{minSize}}` and `{{maxSize}}`. +### `Size::TEMPLATE_STANDARD` -## Templates +| Mode | Template | +|------------|------------------------------------| +| `default` | The size in {{unit|trans}} of | +| `inverted` | The size in {{unit|trans}} of | -### `Size::TEMPLATE_BOTH` +This template serve as message prefix: -| Mode | Template | -|------------|----------------------------------------------------------| -| `default` | {{name}} must be between {{minSize}} and {{maxSize}} | -| `inverted` | {{name}} must not be between {{minSize}} and {{maxSize}} | +```php +v::size('MB', v::equals(2))->assert('filename.txt') +// Message: The size in megabytes of "filename.txt" must be equal to 2 -### `Size::TEMPLATE_LOWER` +v::size('KB', v::not(v::equals(56)))->assert('filename.txt') +// Message: The size in kilobytes of "filename.txt" must not be equal to 56 +``` -| Mode | Template | -|------------|-----------------------------------------------| -| `default` | {{name}} must be greater than {{minSize}} | -| `inverted` | {{name}} must not be greater than {{minSize}} | +### `Size::TEMPLATE_WRONG_TYPE` -### `Size::TEMPLATE_GREATER` +Used when the input is not a valid file path, a `SplFileInfo` object, or a PSR-7 interface. -| Mode | Template | -|------------|---------------------------------------------| -| `default` | {{name}} must be lower than {{maxSize}} | -| `inverted` | {{name}} must not be lower than {{maxSize}} | +| Mode | Template | +|------------|------------------------------------------------------------------------------------| +| `default` | {{name}} must be a filename or an instance of SplFileInfo or a PSR-7 interface | +| `inverted` | {{name}} must not be a filename or an instance of SplFileInfo or a PSR-7 interface | ## Template placeholders | Placeholder | Description | |-------------|------------------------------------------------------------------| -| `maxSize` | | -| `minSize` | | | `name` | The validated input or the custom validator name (if specified). | +| `unit` | The name of the storage unit (bytes, kilobytes, etc.) | ## Categorization - File system +- Transformations ## Changelog -| Version | Description | -|--------:|-------------------| -| 2.1.0 | Add PSR-7 support | -| 1.0.0 | Created | +| Version | Description | +|--------:|-------------------------| +| 3.0.0 | Became a transformation | +| 2.1.0 | Add [PSR-7][] support | +| 1.0.0 | Created | *** See also: @@ -88,3 +81,6 @@ See also: - [SymbolicLink](SymbolicLink.md) - [Uploaded](Uploaded.md) - [Writable](Writable.md) + +[PSR-7]: https://www.php-fig.org/psr/psr-7/ +[SplFileInfo]: https://www.php.net/SplFileInfo diff --git a/library/Factory.php b/library/Factory.php index 67e8d3b81..d1ad97d69 100644 --- a/library/Factory.php +++ b/library/Factory.php @@ -21,6 +21,7 @@ use Respect\Validation\Transformers\DeprecatedKeyValue; use Respect\Validation\Transformers\DeprecatedLength; use Respect\Validation\Transformers\DeprecatedMinAndMax; +use Respect\Validation\Transformers\DeprecatedSize; use Respect\Validation\Transformers\DeprecatedType; use Respect\Validation\Transformers\Prefix; use Respect\Validation\Transformers\RuleSpec; @@ -44,7 +45,9 @@ public function __construct( new DeprecatedKeyValue( new DeprecatedMinAndMax( new DeprecatedAge( - new DeprecatedKeyNested(new DeprecatedLength(new DeprecatedType(new Aliases(new Prefix())))) + new DeprecatedKeyNested(new DeprecatedLength(new DeprecatedType(new DeprecatedSize( + new Aliases(new Prefix()) + )))) ) ) ) diff --git a/library/Mixins/ChainedKey.php b/library/Mixins/ChainedKey.php index 9badb0a6e..d1da4f4dd 100644 --- a/library/Mixins/ChainedKey.php +++ b/library/Mixins/ChainedKey.php @@ -293,11 +293,10 @@ public function keyRoman(int|string $key): ChainedValidator; public function keyScalarVal(int|string $key): ChainedValidator; - public function keySize( - int|string $key, - string|int|null $minSize = null, - string|int|null $maxSize = null, - ): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public function keySize(int|string $key, string $unit, Rule $rule): ChainedValidator; public function keySlug(int|string $key): ChainedValidator; diff --git a/library/Mixins/ChainedNot.php b/library/Mixins/ChainedNot.php index d5b500991..b3977e339 100644 --- a/library/Mixins/ChainedNot.php +++ b/library/Mixins/ChainedNot.php @@ -295,7 +295,10 @@ public function notRoman(): ChainedValidator; public function notScalarVal(): ChainedValidator; - public function notSize(string|int|null $minSize = null, string|int|null $maxSize = null): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public function notSize(string $unit, Rule $rule): ChainedValidator; public function notSlug(): ChainedValidator; diff --git a/library/Mixins/ChainedNullOr.php b/library/Mixins/ChainedNullOr.php index 3fec3045a..a54834d75 100644 --- a/library/Mixins/ChainedNullOr.php +++ b/library/Mixins/ChainedNullOr.php @@ -305,7 +305,10 @@ public function nullOrRoman(): ChainedValidator; public function nullOrScalarVal(): ChainedValidator; - public function nullOrSize(string|int|null $minSize = null, string|int|null $maxSize = null): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public function nullOrSize(string $unit, Rule $rule): ChainedValidator; public function nullOrSlug(): ChainedValidator; diff --git a/library/Mixins/ChainedProperty.php b/library/Mixins/ChainedProperty.php index 1e9d0db8c..dc63fdca2 100644 --- a/library/Mixins/ChainedProperty.php +++ b/library/Mixins/ChainedProperty.php @@ -314,11 +314,10 @@ public function propertyRoman(string $propertyName): ChainedValidator; public function propertyScalarVal(string $propertyName): ChainedValidator; - public function propertySize( - string $propertyName, - string|int|null $minSize = null, - string|int|null $maxSize = null, - ): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public function propertySize(string $propertyName, string $unit, Rule $rule): ChainedValidator; public function propertySlug(string $propertyName): ChainedValidator; diff --git a/library/Mixins/ChainedUndefOr.php b/library/Mixins/ChainedUndefOr.php index 9a66b6ed7..cb11cc387 100644 --- a/library/Mixins/ChainedUndefOr.php +++ b/library/Mixins/ChainedUndefOr.php @@ -303,7 +303,10 @@ public function undefOrRoman(): ChainedValidator; public function undefOrScalarVal(): ChainedValidator; - public function undefOrSize(string|int|null $minSize = null, string|int|null $maxSize = null): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public function undefOrSize(string $unit, Rule $rule): ChainedValidator; public function undefOrSlug(): ChainedValidator; diff --git a/library/Mixins/ChainedValidator.php b/library/Mixins/ChainedValidator.php index 7bd0a03aa..18a51b7a3 100644 --- a/library/Mixins/ChainedValidator.php +++ b/library/Mixins/ChainedValidator.php @@ -349,7 +349,10 @@ public function roman(): ChainedValidator; public function scalarVal(): ChainedValidator; - public function size(string|int|null $minSize = null, string|int|null $maxSize = null): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public function size(string $unit, Rule $rule): ChainedValidator; public function slug(): ChainedValidator; diff --git a/library/Mixins/StaticKey.php b/library/Mixins/StaticKey.php index 0e8aa9826..b07bf401b 100644 --- a/library/Mixins/StaticKey.php +++ b/library/Mixins/StaticKey.php @@ -301,11 +301,10 @@ public static function keyRoman(int|string $key): ChainedValidator; public static function keyScalarVal(int|string $key): ChainedValidator; - public static function keySize( - int|string $key, - string|int|null $minSize = null, - string|int|null $maxSize = null, - ): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public static function keySize(int|string $key, string $unit, Rule $rule): ChainedValidator; public static function keySlug(int|string $key): ChainedValidator; diff --git a/library/Mixins/StaticNot.php b/library/Mixins/StaticNot.php index 7ad8f9178..220f9f06e 100644 --- a/library/Mixins/StaticNot.php +++ b/library/Mixins/StaticNot.php @@ -295,7 +295,10 @@ public static function notRoman(): ChainedValidator; public static function notScalarVal(): ChainedValidator; - public static function notSize(string|int|null $minSize = null, string|int|null $maxSize = null): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public static function notSize(string $unit, Rule $rule): ChainedValidator; public static function notSlug(): ChainedValidator; diff --git a/library/Mixins/StaticNullOr.php b/library/Mixins/StaticNullOr.php index f2d9c7ccd..8a0b47b95 100644 --- a/library/Mixins/StaticNullOr.php +++ b/library/Mixins/StaticNullOr.php @@ -305,10 +305,10 @@ public static function nullOrRoman(): ChainedValidator; public static function nullOrScalarVal(): ChainedValidator; - public static function nullOrSize( - string|int|null $minSize = null, - string|int|null $maxSize = null, - ): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public static function nullOrSize(string $unit, Rule $rule): ChainedValidator; public static function nullOrSlug(): ChainedValidator; diff --git a/library/Mixins/StaticProperty.php b/library/Mixins/StaticProperty.php index 414c32042..a444a248e 100644 --- a/library/Mixins/StaticProperty.php +++ b/library/Mixins/StaticProperty.php @@ -358,11 +358,10 @@ public static function propertyRoman(string $propertyName): ChainedValidator; public static function propertyScalarVal(string $propertyName): ChainedValidator; - public static function propertySize( - string $propertyName, - string|int|null $minSize = null, - string|int|null $maxSize = null, - ): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public static function propertySize(string $propertyName, string $unit, Rule $rule): ChainedValidator; public static function propertySlug(string $propertyName): ChainedValidator; diff --git a/library/Mixins/StaticUndefOr.php b/library/Mixins/StaticUndefOr.php index 26065184d..7fd08b985 100644 --- a/library/Mixins/StaticUndefOr.php +++ b/library/Mixins/StaticUndefOr.php @@ -303,10 +303,10 @@ public static function undefOrRoman(): ChainedValidator; public static function undefOrScalarVal(): ChainedValidator; - public static function undefOrSize( - string|int|null $minSize = null, - string|int|null $maxSize = null, - ): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public static function undefOrSize(string $unit, Rule $rule): ChainedValidator; public static function undefOrSlug(): ChainedValidator; diff --git a/library/Mixins/StaticValidator.php b/library/Mixins/StaticValidator.php index 3f9ac7893..11a37f15c 100644 --- a/library/Mixins/StaticValidator.php +++ b/library/Mixins/StaticValidator.php @@ -329,7 +329,10 @@ public static function roman(): ChainedValidator; public static function scalarVal(): ChainedValidator; - public static function size(string|int|null $minSize = null, string|int|null $maxSize = null): ChainedValidator; + /** + * @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit + */ + public static function size(string $unit, Rule $rule): ChainedValidator; public static function slug(): ChainedValidator; diff --git a/library/Result.php b/library/Result.php index 4f5962216..d308dab3c 100644 --- a/library/Result.php +++ b/library/Result.php @@ -117,7 +117,7 @@ public function withInput(mixed $input): self return $this->clone( input: $input, children: array_map( - static fn (Result $child) => $child->input === $currentInput ? $input : $child->input, + static fn (Result $child) => $child->input === $currentInput ? $child->withInput($input) : $child, $this->children ), ); diff --git a/library/Rules/Size.php b/library/Rules/Size.php index 53d10479a..8ddca3058 100644 --- a/library/Rules/Size.php +++ b/library/Rules/Size.php @@ -15,126 +15,102 @@ use Respect\Validation\Exceptions\InvalidRuleConstructorException; use Respect\Validation\Message\Template; use Respect\Validation\Result; -use Respect\Validation\Rules\Core\Standard; +use Respect\Validation\Rule; +use Respect\Validation\Rules\Core\Wrapper; use SplFileInfo; +use function array_map; use function filesize; -use function floatval; -use function is_numeric; use function is_string; -use function preg_match; +use function ucfirst; #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] #[Template( - '{{name}} must be between {{minSize}} and {{maxSize}}', - '{{name}} must not be between {{minSize}} and {{maxSize}}', - self::TEMPLATE_BOTH, + 'The size in {{unit|trans}} of', + 'The size in {{unit|trans}} of', + Size::TEMPLATE_STANDARD )] #[Template( - '{{name}} must be greater than {{minSize}}', - '{{name}} must not be greater than {{minSize}}', - self::TEMPLATE_LOWER, + '{{name}} must be a filename or an instance of SplFileInfo or a PSR-7 interface', + '{{name}} must not be a filename or an instance of SplFileInfo or a PSR-7 interface', + self::TEMPLATE_WRONG_TYPE )] -#[Template( - '{{name}} must be lower than {{maxSize}}', - '{{name}} must not be lower than {{maxSize}}', - self::TEMPLATE_GREATER, -)] -final class Size extends Standard +final class Size extends Wrapper { - public const TEMPLATE_LOWER = '__lower__'; - public const TEMPLATE_GREATER = '__greater__'; - public const TEMPLATE_BOTH = '__both__'; - - private readonly ?float $minValue; - - private readonly ?float $maxValue; - + public const TEMPLATE_WRONG_TYPE = '__wrong_type__'; + + private const DATA_STORAGE_UNITS = [ + 'B' => ['name' => 'bytes', 'bytes' => 1], + 'KB' => ['name' => 'kilobytes', 'bytes' => 1024], + 'MB' => ['name' => 'megabytes', 'bytes' => 1024 ** 2], + 'GB' => ['name' => 'gigabytes', 'bytes' => 1024 ** 3], + 'TB' => ['name' => 'terabytes', 'bytes' => 1024 ** 4], + 'PB' => ['name' => 'petabytes', 'bytes' => 1024 ** 5], + 'EB' => ['name' => 'exabytes', 'bytes' => 1024 ** 6], + 'ZB' => ['name' => 'zettabytes', 'bytes' => 1024 ** 7], + 'YB' => ['name' => 'yottabytes', 'bytes' => 1024 ** 8], + ]; + + /** @param "B"|"KB"|"MB"|"GB"|"TB"|"PB"|"EB"|"ZB"|"YB" $unit */ public function __construct( - private readonly string|int|null $minSize = null, - private readonly string|int|null $maxSize = null + private readonly string $unit, + Rule $rule ) { - $this->minValue = $minSize ? $this->toBytes((string) $minSize) : null; - $this->maxValue = $maxSize ? $this->toBytes((string) $maxSize) : null; - } + if (!isset(self::DATA_STORAGE_UNITS[$unit])) { + throw new InvalidRuleConstructorException('"%s" is not a recognized data storage unit.', $unit); + } - public function evaluate(mixed $input): Result - { - return new Result( - $this->isValid($input), - $input, - $this, - ['minSize' => $this->minSize, 'maxSize' => $this->maxSize], - $this->getStandardTemplate() - ); + parent::__construct($rule); } - private function isValid(mixed $input): bool + public function evaluate(mixed $input): Result { - if ($input instanceof SplFileInfo) { - return $this->isValidSize((float) $input->getSize()); + $size = $this->getSize($input); + if ($size === null) { + return Result::failed($input, $this, [], self::TEMPLATE_WRONG_TYPE) + ->withId('size' . ucfirst($this->rule->evaluate($input)->id)); } - if ($input instanceof UploadedFileInterface) { - return $this->isValidSize((float) $input->getSize()); - } + $result = $this->rule->evaluate($this->getSize($input) / self::DATA_STORAGE_UNITS[$this->unit]['bytes']); - if ($input instanceof StreamInterface) { - return $this->isValidSize((float) $input->getSize()); - } - - if (is_string($input)) { - return $this->isValidSize((float) filesize($input)); - } - - return false; + return $this->enrichResult($input, $result); } - private function getStandardTemplate(): string + private function getSize(mixed $input): ?int { - if (!$this->minValue) { - return self::TEMPLATE_GREATER; + if (is_string($input)) { + return (int) filesize($input); } - if (!$this->maxValue) { - return self::TEMPLATE_LOWER; + if ($input instanceof SplFileInfo) { + return $input->getSize(); } - return self::TEMPLATE_BOTH; - } - - /** - * @todo Move it to a trait - */ - private function toBytes(string $size): float - { - $value = $size; - $units = ['b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']; - foreach ($units as $exponent => $unit) { - if (!preg_match('/^(\d+(.\d+)?)' . $unit . '$/i', $size, $matches)) { - continue; - } - $value = floatval($matches[1]) * 1024 ** $exponent; - break; + if ($input instanceof UploadedFileInterface) { + return $input->getSize(); } - if (!is_numeric($value)) { - throw new InvalidRuleConstructorException('"%s" is not a recognized file size.', $size); + if ($input instanceof StreamInterface) { + return $input->getSize(); } - return (float) $value; + return null; } - private function isValidSize(float $size): bool + private function enrichResult(mixed $input, Result $result): Result { - if ($this->minValue !== null && $this->maxValue !== null) { - return $size >= $this->minValue && $size <= $this->maxValue; + if (!$result->allowsSubsequent()) { + return $result + ->withInput($input) + ->withChildren( + ...array_map(fn(Result $child) => $this->enrichResult($input, $child), $result->children) + ); } - if ($this->minValue !== null) { - return $size >= $this->minValue; - } + $parameters = ['unit' => self::DATA_STORAGE_UNITS[$this->unit]['name']]; - return $size <= $this->maxValue; + return (new Result($result->isValid, $input, $this, $parameters, id: $result->id)) + ->withPrefixedId('size') + ->withSubsequent($result->withInput($input)); } } diff --git a/library/Transformers/DeprecatedSize.php b/library/Transformers/DeprecatedSize.php new file mode 100644 index 000000000..1cfdbef40 --- /dev/null +++ b/library/Transformers/DeprecatedSize.php @@ -0,0 +1,130 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Transformers; + +use Respect\Validation\Rule; +use Respect\Validation\Rules\Between; +use Respect\Validation\Rules\Equals; +use Respect\Validation\Rules\GreaterThanOrEqual; +use Respect\Validation\Rules\LessThanOrEqual; + +use function filter_var; +use function is_float; +use function is_int; +use function preg_replace; +use function Respect\Stringifier\stringify; +use function sprintf; +use function strtoupper; +use function trigger_error; + +use const E_USER_DEPRECATED; +use const FILTER_VALIDATE_FLOAT; +use const FILTER_VALIDATE_INT; + +final class DeprecatedSize implements Transformer +{ + public function __construct( + private readonly Transformer $next + ) { + } + + public function transform(RuleSpec $ruleSpec): RuleSpec + { + if ($ruleSpec->name !== 'size' || $ruleSpec->arguments === []) { + return $this->next->transform($ruleSpec); + } + + if (isset($ruleSpec->arguments[1]) && $ruleSpec->arguments[1] instanceof Rule) { + return $this->next->transform($ruleSpec); + } + + $minValue = $ruleSpec->arguments[0] ?? null; + $maxValue = $ruleSpec->arguments[1] ?? null; + + $message = 'Calling size() with scalar values has been deprecated, ' . + 'and will not be allowed in the next major version. '; + + if (!$maxValue) { + $unit = $this->getUnit($minValue); + $numberValue = $this->getValue($minValue); + trigger_error( + sprintf($message . 'Use size(\'%s\', greaterThanOrEqual(%s)) instead.', $unit, stringify($numberValue)), + E_USER_DEPRECATED + ); + + return new RuleSpec('size', [$unit, new GreaterThanOrEqual($numberValue)]); + } + + if (!$minValue) { + $unit = $this->getUnit($maxValue); + $numberValue = $this->getValue($maxValue); + trigger_error( + sprintf($message . 'Use size(\'%s\', lessThanOrEqual(%s)) instead.', $unit, stringify($numberValue)), + E_USER_DEPRECATED + ); + + return new RuleSpec('size', [$unit, new LessThanOrEqual($numberValue)]); + } + + if ($minValue === $maxValue) { + $unit = $this->getUnit($maxValue); + $numberValue = $this->getValue($maxValue); + trigger_error( + sprintf($message . 'Use size(\'%s\', equals(%s)) instead.', $unit, stringify($numberValue)), + E_USER_DEPRECATED + ); + + return new RuleSpec('size', [$unit, new Equals($numberValue)]); + } + + $unit = $this->getUnit($maxValue); + $minNumberValue = $this->getValue($minValue); + $maxNumberValue = $this->getValue($maxValue); + + trigger_error( + sprintf( + $message . 'Use size(\'%s\', between(%s, %s)) instead.', + $unit, + stringify($minNumberValue), + stringify($maxNumberValue), + ), + E_USER_DEPRECATED + ); + + return new RuleSpec('size', [$unit, new Between($minNumberValue, $maxNumberValue)]); + } + + public function getValue(mixed $maxValue): int|float|string + { + if (is_int($maxValue) || is_float($maxValue)) { + return $maxValue; + } + + $filtered = preg_replace('/[^0-9.]/', '', $maxValue); + if (filter_var($filtered, FILTER_VALIDATE_INT)) { + return (int) $filtered; + } + + if (filter_var($filtered, FILTER_VALIDATE_FLOAT)) { + return (float) $filtered; + } + + return $filtered; + } + + private function getUnit(mixed $maxValue): string + { + if (is_int($maxValue)) { + return 'B'; + } + + return strtoupper(preg_replace('/[0-9.]/', '', $maxValue)); + } +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 43e2a2696..331611805 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -7,10 +7,16 @@ parameters: # Why: SimpleXMLElement is weird and doesn't implement anything ArrayAccess-like message: '/Instanceof between mixed and SimpleXMLElement will always evaluate to false\./' path: library/Rules/ArrayVal.php - - message: '/Call to an undefined method .+::skip\(\)/' + - message: '/Call to an undefined method .+::(skip|throws)\(\)/' path: tests/feature - message: '/Call to an undefined method .+::expectException\(\)/' path: tests/Pest.php + - message: '/Undefined variable: \$this/' + path: tests/feature + - message: '/Variable \$this might not be defined./' + path: tests/feature + - message: '/Undefined variable: \$this/' + path: tests/Pest.php - message: '/Call to deprecated method optional\(\).+/' path: tests/feature/Transformers/AliasesTest.php - message: '/Call to an undefined static method Respect\\Validation\\Validator::(min|max)Age\(\)./' @@ -31,6 +37,8 @@ parameters: path: tests/feature/Transformers/DeprecatedMaxTest.php - message: '/Parameter #1 \$rule of static method Respect\\Validation\\Mixins\\StaticValidator::min\(\) expects Respect\\Validation\\Rule.+/' path: tests/feature/Transformers/DeprecatedMinTest.php + - message: '/Parameter #(1|2) \$(unit|rule) of static method Respect\\Validation\\Mixins\\StaticValidator::size\(\) expects .+/' + path: tests/feature/Transformers/DeprecatedSizeTest.php - message: '/Call to an undefined static method Respect\\Validation\\Validator::type\(\)./' path: tests/feature/Transformers/DeprecatedTypeTest.php - message: '/Method .+\\TestingStringifier::stringify\(\) never returns null so it can be removed from the return type./' diff --git a/tests/Pest.php b/tests/Pest.php index 22ac312b2..da5f77868 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -13,11 +13,11 @@ use function PHPUnit\Framework\assertStringMatchesFormat; /** @param array $messages */ -function expectAll(callable $callback, string $message, string $fullMessage, array $messages): Closure +function expectAll(Closure $callback, string $message, string $fullMessage, array $messages): Closure { return function () use ($callback, $message, $fullMessage, $messages): void { try { - $callback(); + $callback->call($this); test()->expectException(ValidationException::class); } catch (ValidationException $e) { expect($e->getMessage())->toBe($message) @@ -28,7 +28,7 @@ function expectAll(callable $callback, string $message, string $fullMessage, arr } /** @param array $messages */ -function expectAllToMatch(callable $callback, string $message, string $fullMessage, array $messages): Closure +function expectAllToMatch(Closure $callback, string $message, string $fullMessage, array $messages): Closure { return function () use ($callback, $message, $fullMessage, $messages): void { try { @@ -46,7 +46,7 @@ function expectAllToMatch(callable $callback, string $message, string $fullMessa }; } -function expectMessage(callable $callback, string $message): Closure +function expectMessage(Closure $callback, string $message): Closure { return function () use ($callback, $message): void { try { @@ -58,7 +58,7 @@ function expectMessage(callable $callback, string $message): Closure }; } -function expectFullMessage(callable $callback, string $fullMessage): Closure +function expectFullMessage(Closure $callback, string $fullMessage): Closure { return function () use ($callback, $fullMessage): void { try { @@ -71,7 +71,7 @@ function expectFullMessage(callable $callback, string $fullMessage): Closure } /** @param array $messages */ -function expectMessages(callable $callback, array $messages): Closure +function expectMessages(Closure $callback, array $messages): Closure { return function () use ($callback, $messages): void { try { @@ -83,6 +83,29 @@ function expectMessages(callable $callback, array $messages): Closure }; } +function expectDeprecation(Closure $callback, string $error): Closure +{ + return function () use ($callback, $error): void { + $lastError = null; + set_error_handler(static function (int $errno, string $errstr) use (&$lastError): bool { + if ($errno !== E_USER_DEPRECATED) { + return false; + } + $lastError = $errstr; + + return true; + }); + + try { + $callback->call($this); + } catch (Throwable $e) { + restore_error_handler(); + expect($lastError)->toBe($error); + throw $e; + } + }; +} + function expectMessageAndError(Closure $callback, string $message, string $error): Closure { return function () use ($callback, $message, $error): void { @@ -93,7 +116,7 @@ function expectMessageAndError(Closure $callback, string $message, string $error return true; }); try { - $callback(); + $callback->call($this); test()->expectException(ValidationException::class); } catch (ValidationException $e) { expect($e->getMessage())->toBe($message, 'Validation message does not match'); diff --git a/tests/feature/Rules/SizeTest.php b/tests/feature/Rules/SizeTest.php index 89aa1ca8b..c01a65070 100644 --- a/tests/feature/Rules/SizeTest.php +++ b/tests/feature/Rules/SizeTest.php @@ -7,62 +7,67 @@ declare(strict_types=1); -test('Scenario #1', expectMessage( - fn() => v::size('1kb', '2kb')->assert('tests/fixtures/valid-image.gif'), - '"tests/fixtures/valid-image.gif" must be between "1kb" and "2kb"', -)); - -test('Scenario #2', expectMessage( - fn() => v::size('700kb', null)->assert('tests/fixtures/valid-image.gif'), - '"tests/fixtures/valid-image.gif" must be greater than "700kb"', -)); - -test('Scenario #3', expectMessage( - fn() => v::size(null, '1kb')->assert('tests/fixtures/valid-image.gif'), - '"tests/fixtures/valid-image.gif" must be lower than "1kb"', -)); +use org\bovigo\vfs\content\LargeFileContent; +use org\bovigo\vfs\vfsStream; -test('Scenario #4', expectMessage( - fn() => v::not(v::size('500kb', '600kb'))->assert('tests/fixtures/valid-image.gif'), - '"tests/fixtures/valid-image.gif" must not be between "500kb" and "600kb"', -)); +beforeEach(function (): void { + $this->root = vfsStream::setup(); -test('Scenario #5', expectMessage( - fn() => v::not(v::size('500kb', null))->assert('tests/fixtures/valid-image.gif'), - '"tests/fixtures/valid-image.gif" must not be greater than "500kb"', -)); + $this->file2Kb = vfsStream::newFile('2kb.txt') + ->withContent(LargeFileContent::withKilobytes(2)) + ->at($this->root); -test('Scenario #6', expectMessage( - fn() => v::not(v::size(null, '600kb'))->assert('tests/fixtures/valid-image.gif'), - '"tests/fixtures/valid-image.gif" must not be lower than "600kb"', -)); + $this->file2Mb = vfsStream::newFile('3mb.txt') + ->withContent(LargeFileContent::withMegabytes(3)) + ->at($this->root); +}); -test('Scenario #7', expectFullMessage( - fn() => v::size('1kb', '2kb')->assert('tests/fixtures/valid-image.gif'), - '- "tests/fixtures/valid-image.gif" must be between "1kb" and "2kb"', +test('Default', expectAll( + fn() => v::size('KB', v::lessThan(2))->assert($this->file2Kb->url()), + 'The size in kilobytes of "vfs://root/2kb.txt" must be less than 2', + '- The size in kilobytes of "vfs://root/2kb.txt" must be less than 2', + ['sizeLessThan' => 'The size in kilobytes of "vfs://root/2kb.txt" must be less than 2'] )); -test('Scenario #8', expectFullMessage( - fn() => v::size('700kb', null)->assert('tests/fixtures/valid-image.gif'), - '- "tests/fixtures/valid-image.gif" must be greater than "700kb"', +test('Wrong type', expectAll( + fn() => v::size('KB', v::lessThan(2))->assert(new stdClass()), + '`stdClass {}` must be a filename or an instance of SplFileInfo or a PSR-7 interface', + '- `stdClass {}` must be a filename or an instance of SplFileInfo or a PSR-7 interface', + ['sizeLessThan' => '`stdClass {}` must be a filename or an instance of SplFileInfo or a PSR-7 interface'] )); -test('Scenario #9', expectFullMessage( - fn() => v::size(null, '1kb')->assert('tests/fixtures/valid-image.gif'), - '- "tests/fixtures/valid-image.gif" must be lower than "1kb"', +test('Inverted', expectAll( + fn() => v::size('MB', v::not(v::equals(3)))->assert($this->file2Mb->url()), + 'The size in megabytes of "vfs://root/3mb.txt" must not be equal to 3', + '- The size in megabytes of "vfs://root/3mb.txt" must not be equal to 3', + ['sizeNotEquals' => 'The size in megabytes of "vfs://root/3mb.txt" must not be equal to 3'] )); -test('Scenario #10', expectFullMessage( - fn() => v::not(v::size('500kb', '600kb'))->assert('tests/fixtures/valid-image.gif'), - '- "tests/fixtures/valid-image.gif" must not be between "500kb" and "600kb"', +test('Wrapped with name', expectAll( + fn() => v::size('KB', v::lessThan(2)->setName('Wrapped'))->assert($this->file2Kb->url()), + 'The size in kilobytes of Wrapped must be less than 2', + '- The size in kilobytes of Wrapped must be less than 2', + ['sizeLessThan' => 'The size in kilobytes of Wrapped must be less than 2'] )); -test('Scenario #11', expectFullMessage( - fn() => v::not(v::size('500kb', null))->assert('tests/fixtures/valid-image.gif'), - '- "tests/fixtures/valid-image.gif" must not be greater than "500kb"', +test('Wrapper with name', expectAll( + fn() => v::size('KB', v::lessThan(2))->setName('Wrapper')->assert($this->file2Kb->url()), + 'The size in kilobytes of Wrapper must be less than 2', + '- The size in kilobytes of Wrapper must be less than 2', + ['sizeLessThan' => 'The size in kilobytes of Wrapper must be less than 2'] )); -test('Scenario #12', expectFullMessage( - fn() => v::not(v::size(null, '600kb'))->assert('tests/fixtures/valid-image.gif'), - '- "tests/fixtures/valid-image.gif" must not be lower than "600kb"', +test('Chained wrapped rule', expectAll( + fn() => v::size('KB', v::between(5, 7)->odd())->assert($this->file2Kb->url()), + 'The size in kilobytes of "vfs://root/2kb.txt" must be between 5 and 7', + <<<'FULL_MESSAGE' + - All of the required rules must pass for "vfs://root/2kb.txt" + - The size in kilobytes of "vfs://root/2kb.txt" must be between 5 and 7 + - The size in kilobytes of "vfs://root/2kb.txt" must be an odd number + FULL_MESSAGE, + [ + '__root__' => 'All of the required rules must pass for "vfs://root/2kb.txt"', + 'sizeBetween' => 'The size in kilobytes of "vfs://root/2kb.txt" must be between 5 and 7', + 'sizeOdd' => 'The size in kilobytes of "vfs://root/2kb.txt" must be an odd number', + ] )); diff --git a/tests/feature/Transformers/DeprecatedSizeTest.php b/tests/feature/Transformers/DeprecatedSizeTest.php new file mode 100644 index 000000000..b1d5c0546 --- /dev/null +++ b/tests/feature/Transformers/DeprecatedSizeTest.php @@ -0,0 +1,80 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +use org\bovigo\vfs\content\LargeFileContent; +use org\bovigo\vfs\vfsStream; + +require_once 'vendor/autoload.php'; + +$baseError = 'Calling size() with scalar values has been deprecated, and will not be allowed in the next major version. '; +beforeEach(function (): void { + $root = vfsStream::setup(); + $this->filename = vfsStream::newFile('filename.blob') + ->withContent(LargeFileContent::withKilobytes(2)) + ->at($root) + ->url(); +}); + +test('Greater than, only integer', expectMessageAndError( + fn() => v::size(6042, null)->assert($this->filename), + 'The size in bytes of "vfs://root/filename.blob" must be greater than or equal to 6042', + $baseError . 'Use size(\'B\', greaterThanOrEqual(6042)) instead.' +)); + +test('Greater than, with storage unit', expectMessageAndError( + fn() => v::size('2.5MB', null)->assert($this->filename), + 'The size in megabytes of "vfs://root/filename.blob" must be greater than or equal to 2.5', + $baseError . 'Use size(\'MB\', greaterThanOrEqual(2.5)) instead.' +)); + +test('Less than, only integer', expectMessageAndError( + fn() => v::size(null, 526)->assert($this->filename), + 'The size in bytes of "vfs://root/filename.blob" must be less than or equal to 526', + $baseError . 'Use size(\'B\', lessThanOrEqual(526)) instead.' +)); + +test('Less than, with storage unit', expectMessageAndError( + fn() => v::size(null, '1KB')->assert($this->filename), + 'The size in kilobytes of "vfs://root/filename.blob" must be less than or equal to 1', + $baseError . 'Use size(\'KB\', lessThanOrEqual(1)) instead.' +)); + +test('Equal, only integer', expectMessageAndError( + fn() => v::size(1024, 1024)->assert($this->filename), + 'The size in bytes of "vfs://root/filename.blob" must be equal to 1024', + $baseError . 'Use size(\'B\', equals(1024)) instead.' +)); + +test('Equal, with storage unit', expectMessageAndError( + fn() => v::size('1PB', '1PB')->assert($this->filename), + 'The size in petabytes of "vfs://root/filename.blob" must be equal to 1', + $baseError . 'Use size(\'PB\', equals(1)) instead.' +)); + +test('Between, only integer', expectMessageAndError( + fn() => v::size(1, 1024)->assert($this->filename), + 'The size in bytes of "vfs://root/filename.blob" must be between 1 and 1024', + $baseError . 'Use size(\'B\', between(1, 1024)) instead.' +)); + +test('Between, with storage unit', expectMessageAndError( + fn() => v::size('1zb', '2.5zb')->assert($this->filename), + 'The size in zettabytes of "vfs://root/filename.blob" must be between 1 and 2.5', + $baseError . 'Use size(\'ZB\', between(1, 2.5)) instead.' +)); + +test('Wrong storage unit', expectDeprecation( + fn() => v::size('1jb', null), + $baseError . 'Use size(\'JB\', greaterThanOrEqual(1)) instead.', +))->throws('"JB" is not a recognized data storage unit'); + +test('Missing storage unit and size', expectDeprecation( + fn() => v::size('something', null), + $baseError . 'Use size(\'SOMETHING\', greaterThanOrEqual("")) instead.', +))->throws('"SOMETHING" is not a recognized data storage unit'); diff --git a/tests/unit/Rules/SizeTest.php b/tests/unit/Rules/SizeTest.php index e77c0b453..c2a86f740 100644 --- a/tests/unit/Rules/SizeTest.php +++ b/tests/unit/Rules/SizeTest.php @@ -11,79 +11,91 @@ use org\bovigo\vfs\content\LargeFileContent; use org\bovigo\vfs\vfsStream; +use org\bovigo\vfs\vfsStreamDirectory; +use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use Respect\Validation\Exceptions\InvalidRuleConstructorException; -use Respect\Validation\Test\RuleTestCase; +use Respect\Validation\Test\Rules\Stub; use Respect\Validation\Test\Stubs\StreamStub; use Respect\Validation\Test\Stubs\UploadedFileStub; +use Respect\Validation\Test\TestCase; use SplFileInfo; +use function uniqid; + #[Group('rule')] #[CoversClass(Size::class)] -final class SizeTest extends RuleTestCase +final class SizeTest extends TestCase { + private vfsStreamDirectory $root; + + #[Before] + public function setUpVfsStream(): void + { + $this->root = vfsStream::setup(); + } + #[Test] public function shouldThrowsAnExceptionWhenSizeIsNotValid(): void { $this->expectException(InvalidRuleConstructorException::class); - $this->expectExceptionMessage('"42jb" is not a recognized file size'); + $this->expectExceptionMessage('"whatever" is not a recognized data storage unit'); - new Size('42jb'); + // @phpstan-ignore-next-line + new Size('whatever', Stub::daze()); } - /** @return iterable */ - public static function providerForValidInput(): iterable + #[Test] + public function shouldGetTheSizeOfFilePassedAsString(): void { - $root = vfsStream::setup(); - $file2Kb = vfsStream::newFile('2kb.txt') + $file = vfsStream::newFile(uniqid()) ->withContent(LargeFileContent::withKilobytes(2)) - ->at($root); - $file2Mb = vfsStream::newFile('2mb.txt') - ->withContent(LargeFileContent::withMegabytes(2)) - ->at($root); - - return [ - 'file with at least 1kb' => [new Size('1kb', null), $file2Kb->url()], - 'file with at least 2k' => [new Size('2kb', null), $file2Kb->url()], - 'file with up to 2kb' => [new Size(null, '2kb'), $file2Kb->url()], - 'file with up to 3kb' => [new Size(null, '3kb'), $file2Kb->url()], - 'file between 1kb and 3kb' => [new Size('1kb', '3kb'), $file2Kb->url()], - 'file with at least 1mb' => [new Size('1mb', null), $file2Mb->url()], - 'file with at least 2mb' => [new Size('2mb', null), $file2Mb->url()], - 'file with up to 2mb' => [new Size(null, '2mb'), $file2Mb->url()], - 'file with up to 3mb' => [new Size(null, '3mb'), $file2Mb->url()], - 'file between 1mb and 3mb' => [new Size('1mb', '3mb'), $file2Mb->url()], - 'SplFileInfo instance' => [new Size('1mb', '3mb'), new SplFileInfo($file2Mb->url())], - 'PSR-7 stream' => [new Size('1kb', '2kb'), StreamStub::createWithSize(1024)], - 'PSR-7 UploadedFile' => [new Size('1kb', '2kb'), UploadedFileStub::createWithSize(1024)], - ]; + ->at($this->root); + + $wrapped = Stub::pass(1); + $rule = new Size('KB', $wrapped); + $rule->evaluate($file->url()); + + self::assertSame([2], $wrapped->inputs); } - /** @return iterable */ - public static function providerForInvalidInput(): iterable + #[Test] + public function shouldGetTheSizeOfFilePassedAsSplFileInfo(): void { - $root = vfsStream::setup(); - $file2Kb = vfsStream::newFile('2kb.txt') - ->withContent(LargeFileContent::withKilobytes(2)) - ->at($root); - $file2Mb = vfsStream::newFile('2mb.txt') - ->withContent(LargeFileContent::withMegabytes(2)) - ->at($root); - - return [ - 'file with at least 3kb' => [new Size('3kb', null), $file2Kb->url()], - 'file with up to 1kb' => [new Size(null, '1kb'), $file2Kb->url()], - 'file between 1kb and 1.5kb' => [new Size('1kb', '1.5kb'), $file2Kb->url()], - 'file with at least 2.5mb' => [new Size('2.5mb', null), $file2Mb->url()], - 'file with at least 3gb' => [new Size('3gb', null), $file2Mb->url()], - 'file with up to 1b' => [new Size(null, '1b'), $file2Mb->url()], - 'file between 1pb and 3pb' => [new Size('1pb', '3pb'), $file2Mb->url()], - 'SplFileInfo instancia' => [new Size('1pb', '3pb'), new SplFileInfo($file2Mb->url())], - 'parameter invalid' => [new Size('1pb', '3pb'), []], - 'PSR-7 stream' => [new Size('1MB', '1.1MB'), StreamStub::createWithSize(1024)], - 'PSR-7 UploadedFile' => [new Size('1MB', '1.1MB'), UploadedFileStub::createWithSize(1024)], - ]; + $file = vfsStream::newFile(uniqid()) + ->withContent(LargeFileContent::withGigabytes(1)) + ->at($this->root); + + $wrapped = Stub::pass(1); + $rule = new Size('GB', $wrapped); + $rule->evaluate(new SplFileInfo($file->url())); + + self::assertSame([1], $wrapped->inputs); + } + + #[Test] + public function shouldGetTheSizeOfFilePassedAsUploadedFileInterface(): void + { + $file = UploadedFileStub::createWithSize(1024); + + $wrapped = Stub::pass(1); + $rule = new Size('KB', $wrapped); + $rule->evaluate($file); + + self::assertSame([1], $wrapped->inputs); + } + + #[Test] + public function shouldGetTheSizeOfFilePassedAsStreamInterface(): void + { + $file = StreamStub::createWithSize(2 * 1024 ** 2); + + $wrapped = Stub::pass(1); + $rule = new Size('MB', $wrapped); + $rule->evaluate($file); + + self::assertSame([2], $wrapped->inputs); } }