Added some validation rules such as ExcludeIf and ProhibitedIf. (#6094)

This commit is contained in:
宣言就是Siam 2023-08-29 21:09:06 +08:00 committed by GitHub
parent b3db5cd692
commit 54a98c678c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1524 additions and 2 deletions

View File

@ -8,6 +8,7 @@
## Added
- [#6096](https://github.com/hyperf/hyperf/pull/6096) Added `getThrowable` method to request events and crontab event.
- [#6094](https://github.com/hyperf/hyperf/pull/6094) Added some validation rules such as `ExcludeIf` `File` `ImageFile` and `ProhibitedIf`.
## Optimized

View File

@ -21,6 +21,7 @@
"php": ">=8.0",
"egulias/email-validator": "^3.0",
"hyperf/collection": "~3.0.0",
"hyperf/conditionable": "~3.0.0",
"hyperf/contract": "~3.0.0",
"hyperf/database": "~3.0.0",
"hyperf/di": "~3.0.0",
@ -29,6 +30,7 @@
"hyperf/macroable": "~3.0.0",
"hyperf/tappable": "~3.0.0",
"hyperf/translation": "~3.0.0",
"hyperf/stringable": "~3.0.0",
"hyperf/support": "~3.0.0",
"hyperf/utils": "~3.0.0",
"nesbot/carbon": "^2.21",

View File

@ -342,13 +342,25 @@ trait ValidatesAttributes
*/
public function validateDimensions(string $attribute, $value, array $parameters): bool
{
if (! $this->isValidFileInstance($value) || ! $sizeDetails = @getimagesize($value->getRealPath())) {
if ($this->isValidFileInstance($value) && in_array($value->getMimeType(), ['image/svg+xml', 'image/svg'])) {
return true;
}
if (! $this->isValidFileInstance($value)) {
return false;
}
$dimensions = method_exists($value, 'dimensions')
? $value->dimensions()
: @getimagesize($value->getRealPath());
if (! $dimensions) {
return false;
}
$this->requireParameterCount(1, $parameters, 'dimensions');
[$width, $height] = $sizeDetails;
[$width, $height] = $dimensions;
$parameters = $this->parseNamedParameters($parameters);

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Contract;
interface DataAwareRule
{
/**
* Set the data under validation.
*
* @return $this
*/
public function setData(array $data): static;
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Contract;
use Hyperf\Validation\Validator;
interface ValidatorAwareRule
{
/**
* Set the current validator.
*
* @return $this
*/
public function setValidator(Validator $validator): static;
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
use Closure;
use InvalidArgumentException;
use Stringable;
class ExcludeIf implements Stringable
{
public Closure|bool $condition;
/**
* Create a new exclude validation rule based on a condition.
* @param bool|Closure $condition the condition that validates the attribute
* @throws InvalidArgumentException
*/
public function __construct($condition)
{
if ($condition instanceof Closure || is_bool($condition)) {
$this->condition = $condition;
} else {
throw new InvalidArgumentException('The provided condition must be a callable or boolean.');
}
}
/**
* Convert the rule to a validation string.
*
* @return string
*/
public function __toString()
{
if (is_callable($this->condition)) {
return call_user_func($this->condition) ? 'exclude' : '';
}
return $this->condition ? 'exclude' : '';
}
}

View File

@ -0,0 +1,334 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
use Hyperf\Collection\Arr;
use Hyperf\Conditionable\Conditionable;
use Hyperf\Context\ApplicationContext;
use Hyperf\Macroable\Macroable;
use Hyperf\Stringable\Str;
use Hyperf\Validation\Contract\DataAwareRule;
use Hyperf\Validation\Contract\Rule;
use Hyperf\Validation\Contract\ValidatorAwareRule;
use Hyperf\Validation\Validator;
use Hyperf\Validation\ValidatorFactory;
use InvalidArgumentException;
class File implements Rule, DataAwareRule, ValidatorAwareRule
{
use Conditionable;
use Macroable;
/**
* The callback that will generate the "default" version of the file rule.
*
* @var null|array|callable|string
*/
public static $defaultCallback;
/**
* The MIME types that the given file should match. This array may also contain file extensions.
*/
protected array $allowedMimetypes = [];
/**
* The minimum size in kilobytes that the file can be.
*/
protected ?int $minimumFileSize = null;
/**
* The maximum size in kilobytes that the file can be.
*/
protected ?int $maximumFileSize = null;
/**
* An array of custom rules that will be merged into the validation rules.
*/
protected array $customRules = [];
/**
* The error message after validation, if any.
*/
protected array $messages = [];
/**
* The data under validation.
*/
protected array $data = [];
/**
* The validator performing the validation.
*/
protected ?Validator $validator = null;
/**
* Set the default callback to be used for determining the file default rules.
*
* If no arguments are passed, the default file rule configuration will be returned.
*
* @param null|callable|static $callback
* @return null|static
*/
public static function defaults(File|callable $callback = null)
{
if (is_null($callback)) {
return static::default();
}
if (! is_callable($callback) && ! $callback instanceof static) {
throw new InvalidArgumentException('The given callback should be callable or an instance of ' . static::class);
}
static::$defaultCallback = $callback;
}
/**
* Get the default configuration of the file rule.
*/
public static function default()
{
$file = is_callable(static::$defaultCallback)
? call_user_func(static::$defaultCallback)
: static::$defaultCallback;
return $file instanceof Rule ? $file : new self();
}
/**
* Limit the uploaded file to only image types.
*/
public static function image(): ImageFile
{
return new ImageFile();
}
/**
* Limit the uploaded file to the given MIME types or file extensions.
*
* @param array<int, string>|string $mimetypes
*/
public static function types(array|string $mimetypes): static
{
return \Hyperf\Tappable\tap(new static(), fn ($file) => $file->allowedMimetypes = (array) $mimetypes);
}
/**
* Indicate that the uploaded file should be exactly a certain size in kilobytes.
*
* @return $this
*/
public function size(int|string $size): static
{
$this->minimumFileSize = $this->toKilobytes($size);
$this->maximumFileSize = $this->minimumFileSize;
return $this;
}
/**
* Indicate that the uploaded file should be between a minimum and maximum size in kilobytes.
*
* @return $this
*/
public function between(int|string $minSize, int|string $maxSize): static
{
$this->minimumFileSize = $this->toKilobytes($minSize);
$this->maximumFileSize = $this->toKilobytes($maxSize);
return $this;
}
/**
* Indicate that the uploaded file should be no less than the given number of kilobytes.
*
* @return $this
*/
public function min(int|string $size): static
{
$this->minimumFileSize = (int) $this->toKilobytes($size);
return $this;
}
/**
* Indicate that the uploaded file should be no more than the given number of kilobytes.
*
* @return $this
*/
public function max(int|string $size): static
{
$this->maximumFileSize = (int) $this->toKilobytes($size);
return $this;
}
/**
* Specify additional validation rules that should be merged with the default rules during validation.
*
* @param mixed $rules
* @return $this
*/
public function rules($rules): static
{
$this->customRules = array_merge($this->customRules, Arr::wrap($rules));
return $this;
}
/**
* Determine if the validation rule passes.
*/
public function passes(string $attribute, mixed $value): bool
{
$this->messages = [];
$test = $this->buildValidationRules();
$validator = ApplicationContext::getContainer()->get(ValidatorFactory::class)->make(
$this->data,
[$attribute => $test],
$this->validator->customMessages,
$this->validator->customAttributes
);
if ($validator->fails()) {
return $this->fail($validator->messages()->all());
}
return true;
}
/**
* Get the validation error message.
*/
public function message(): array|string
{
return $this->messages;
}
/**
* Set the current validator.
*
* @return $this
*/
public function setValidator(Validator $validator): static
{
$this->validator = $validator;
return $this;
}
/**
* Set the current data under validation.
*
* @return $this
*/
public function setData(array $data): static
{
$this->data = $data;
return $this;
}
/**
* Convert a potentially human-friendly file size to kilobytes.
*
* @param int|string $size
* @return mixed
*/
protected function toKilobytes($size)
{
if (! is_string($size)) {
return $size;
}
$value = floatval($size);
return round(match (true) {
Str::endsWith($size, 'kb') => $value * 1,
Str::endsWith($size, 'mb') => $value * 1000,
Str::endsWith($size, 'gb') => $value * 1000000,
Str::endsWith($size, 'tb') => $value * 1000000000,
default => throw new InvalidArgumentException('Invalid file size suffix.'),
});
}
/**
* Build the array of underlying validation rules based on the current state.
*
* @return array
*/
protected function buildValidationRules()
{
$rules = ['file'];
$rules = array_merge($rules, $this->buildMimetypes());
$rules[] = match (true) {
is_null($this->minimumFileSize) && is_null($this->maximumFileSize) => null,
is_null($this->maximumFileSize) => "min:{$this->minimumFileSize}",
is_null($this->minimumFileSize) => "max:{$this->maximumFileSize}",
$this->minimumFileSize !== $this->maximumFileSize => "between:{$this->minimumFileSize},{$this->maximumFileSize}",
default => "size:{$this->minimumFileSize}",
};
return array_merge(array_filter($rules), $this->customRules);
}
/**
* Separate the given mimetypes from extensions and return an array of correct rules to validate against.
*
* @return array
*/
protected function buildMimetypes()
{
if (count($this->allowedMimetypes) === 0) {
return [];
}
$rules = [];
$mimetypes = array_filter(
$this->allowedMimetypes,
fn ($type) => str_contains($type, '/')
);
$mimes = array_diff($this->allowedMimetypes, $mimetypes);
if (count($mimetypes) > 0) {
$rules[] = 'mimetypes:' . implode(',', $mimetypes);
}
if (count($mimes) > 0) {
$rules[] = 'mimes:' . implode(',', $mimes);
}
return $rules;
}
/**
* Adds the given failures, and return false.
*
* @param array|string $messages
* @return bool
*/
protected function fail($messages)
{
$messages = collect(Arr::wrap($messages))->map(function ($message) {
return $this->validator->getTranslator()->get($message);
})->all();
$this->messages = array_merge($this->messages, $messages);
return false;
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
class ImageFile extends File
{
/**
* Create a new image file rule instance.
*/
public function __construct()
{
$this->rules('image');
}
/**
* The dimension constraints for the uploaded file.
*/
public function dimensions(Dimensions $dimensions): static
{
$this->rules($dimensions);
return $this;
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
use Closure;
use InvalidArgumentException;
use Stringable;
class ProhibitedIf implements Stringable
{
public Closure|bool $condition;
/**
* Create a new prohibited validation rule based on a condition.
*
* @param bool|Closure $condition the condition that validates the attribute
*
* @throws InvalidArgumentException
*/
public function __construct($condition)
{
if ($condition instanceof Closure || is_bool($condition)) {
$this->condition = $condition;
} else {
throw new InvalidArgumentException('The provided condition must be a callable or boolean.');
}
}
/**
* Convert the rule to a validation string.
*
* @return string
*/
public function __toString()
{
if (is_callable($this->condition)) {
return call_user_func($this->condition) ? 'prohibited' : '';
}
return $this->condition ? 'prohibited' : '';
}
}

View File

@ -21,9 +21,11 @@ use Hyperf\HttpMessage\Upload\UploadedFile;
use Hyperf\Stringable\Str;
use Hyperf\Support\Fluent;
use Hyperf\Support\MessageBag;
use Hyperf\Validation\Contract\DataAwareRule;
use Hyperf\Validation\Contract\ImplicitRule;
use Hyperf\Validation\Contract\PresenceVerifierInterface;
use Hyperf\Validation\Contract\Rule as RuleContract;
use Hyperf\Validation\Contract\ValidatorAwareRule;
use Psr\Container\ContainerInterface;
use RuntimeException;
use Stringable;
@ -888,6 +890,14 @@ class Validator implements ValidatorContract
*/
protected function validateUsingCustomRule(string $attribute, $value, RuleContract $rule)
{
if ($rule instanceof ValidatorAwareRule) {
$rule->setValidator($this);
}
if ($rule instanceof DataAwareRule) {
$rule->setData($this->data);
}
if (! $rule->passes($attribute, $value)) {
$this->failedRules[$attribute][$rule::class] = [];

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Exception;
use Hyperf\Validation\Rules\ExcludeIf;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use stdClass;
/**
* @internal
* @coversNothing
*/
class ValidationExcludeIfTest extends TestCase
{
public function testItReturnsStringVersionOfRuleWhenCast()
{
$rule = new ExcludeIf(function () {
return true;
});
$this->assertSame('exclude', (string) $rule);
$rule = new ExcludeIf(function () {
return false;
});
$this->assertSame('', (string) $rule);
$rule = new ExcludeIf(true);
$this->assertSame('exclude', (string) $rule);
$rule = new ExcludeIf(false);
$this->assertSame('', (string) $rule);
}
public function testItValidatesCallableAndBooleanAreAcceptableArguments()
{
$this->assertInstanceOf(ExcludeIf::class, new ExcludeIf(false));
$this->assertInstanceOf(ExcludeIf::class, new ExcludeIf(true));
$this->assertInstanceOf(ExcludeIf::class, new ExcludeIf(fn () => true));
foreach ([1, 1.1, 'phpinfo', new stdClass()] as $condition) {
try {
$this->assertInstanceOf(ExcludeIf::class, new ExcludeIf($condition));
$this->fail('The ExcludeIf constructor must not accept ' . gettype($condition));
} catch (InvalidArgumentException $exception) {
$this->assertEquals('The provided condition must be a callable or boolean.', $exception->getMessage());
}
}
}
public function testItThrowsExceptionIfRuleIsNotSerializable()
{
$this->expectException(Exception::class);
serialize(new ExcludeIf(function () {
return true;
}));
}
}

View File

@ -0,0 +1,361 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Collection\Arr;
use Hyperf\Context\ApplicationContext;
use Hyperf\Di\Container;
use Hyperf\Translation\ArrayLoader;
use Hyperf\Translation\Translator;
use Hyperf\Validation\Rules\File;
use Hyperf\Validation\Validator;
use Hyperf\Validation\ValidatorFactory;
use HyperfTest\Validation\File\FileFactory;
use Mockery;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ValidationFileRuleTest extends TestCase
{
protected function setUp(): void
{
$container = Mockery::mock(Container::class);
ApplicationContext::setContainer($container);
$container->shouldReceive('get')->with(ValidatorFactory::class)->andReturn(
new ValidatorFactory($this->getIlluminateArrayTranslator())
);
}
protected function tearDown(): void
{
}
public function testBasic()
{
$this->fails(
File::default(),
'foo',
['validation.file'],
);
$this->passes(
File::default(),
(new FileFactory())->create('foo.bar'),
);
$this->passes(File::default(), null);
}
public function testSingleMimetype()
{
$this->fails(
File::types('text/plain'),
(new FileFactory())->createWithContent('foo.png', file_get_contents(__DIR__ . '/fixtures/image.png')),
['validation.mimetypes']
);
$this->passes(
File::types('image/png'),
(new FileFactory())->createWithContent('foo.png', file_get_contents(__DIR__ . '/fixtures/image.png')),
);
}
public function testMultipleMimeTypes()
{
$this->fails(
File::types(['text/plain', 'image/jpeg']),
(new FileFactory())->createWithContent('foo.png', file_get_contents(__DIR__ . '/fixtures/image.png')),
['validation.mimetypes']
);
$this->passes(
File::types(['text/plain', 'image/png']),
(new FileFactory())->createWithContent('foo.png', file_get_contents(__DIR__ . '/fixtures/image.png')),
);
}
public function testSingleMime()
{
$this->fails(
File::types('txt'),
(new FileFactory())->createWithContent('foo.png', file_get_contents(__DIR__ . '/fixtures/image.png')),
['validation.mimes']
);
$this->passes(
File::types('png'),
(new FileFactory())->createWithContent('foo.png', file_get_contents(__DIR__ . '/fixtures/image.png')),
);
}
public function testMultipleMimes()
{
$this->fails(
File::types(['png', 'jpg', 'jpeg', 'svg']),
(new FileFactory())->createWithContent('foo.txt', 'Hello World!'),
['validation.mimes']
);
$this->passes(
File::types(['png', 'jpg', 'jpeg', 'svg']),
[
(new FileFactory())->createWithContent('foo.png', file_get_contents(__DIR__ . '/fixtures/image.png')),
(new FileFactory())->createWithContent('foo.svg', file_get_contents(__DIR__ . '/fixtures/image.svg')),
]
);
}
public function testMixOfMimetypesAndMimes()
{
$this->fails(
File::types(['png', 'image/png']),
(new FileFactory())->createWithContent('foo.txt', 'Hello World!'),
['validation.mimetypes', 'validation.mimes']
);
$this->passes(
File::types(['png', 'image/png']),
(new FileFactory())->createWithContent('foo.png', file_get_contents(__DIR__ . '/fixtures/image.png')),
);
}
public function testImage()
{
$this->fails(
File::image(),
(new FileFactory())->createWithContent('foo.txt', 'Hello World!'),
['validation.image']
);
$this->passes(
File::image(),
(new FileFactory())->image('foo.png'),
);
}
public function testSize()
{
$this->fails(
File::default()->size(1024),
[
(new FileFactory())->create('foo.txt', 1025),
(new FileFactory())->create('foo.txt', 1023),
],
['validation.size.file']
);
$this->passes(
File::default()->size(1024),
(new FileFactory())->create('foo.txt', 1024),
);
}
public function testBetween()
{
$this->fails(
File::default()->between(1024, 2048),
[
(new FileFactory())->create('foo.txt', 1023),
(new FileFactory())->create('foo.txt', 2049),
],
['validation.between.file']
);
$this->passes(
File::default()->between(1024, 2048),
[
(new FileFactory())->create('foo.txt', 1024),
(new FileFactory())->create('foo.txt', 2048),
(new FileFactory())->create('foo.txt', 1025),
(new FileFactory())->create('foo.txt', 2047),
]
);
}
public function testMin()
{
$this->fails(
File::default()->min(1024),
(new FileFactory())->create('foo.txt', 1023),
['validation.min.file']
);
$this->passes(
File::default()->min(1024),
[
(new FileFactory())->create('foo.txt', 1024),
(new FileFactory())->create('foo.txt', 1025),
(new FileFactory())->create('foo.txt', 2048),
]
);
}
public function testMinWithHumanReadableSize()
{
$this->fails(
File::default()->min('1024kb'),
(new FileFactory())->create('foo.txt', 1023),
['validation.min.file']
);
$this->passes(
File::default()->min('1024kb'),
[
(new FileFactory())->create('foo.txt', 1024),
(new FileFactory())->create('foo.txt', 1025),
(new FileFactory())->create('foo.txt', 2048),
]
);
}
public function testMax()
{
$this->fails(
File::default()->max(1024),
(new FileFactory())->create('foo.txt', 1025),
['validation.max.file']
);
$this->passes(
File::default()->max(1024),
[
(new FileFactory())->create('foo.txt', 1024),
(new FileFactory())->create('foo.txt', 1023),
(new FileFactory())->create('foo.txt', 512),
]
);
}
public function testMaxWithHumanReadableSize()
{
$this->fails(
File::default()->max('1024kb'),
(new FileFactory())->create('foo.txt', 1025),
['validation.max.file']
);
$this->passes(
File::default()->max('1024kb'),
[
(new FileFactory())->create('foo.txt', 1024),
(new FileFactory())->create('foo.txt', 1023),
(new FileFactory())->create('foo.txt', 512),
]
);
}
public function testMaxWithHumanReadableSizeAndMultipleValue()
{
$this->fails(
File::default()->max('1mb'),
(new FileFactory())->create('foo.txt', 1025),
['validation.max.file']
);
$this->passes(
File::default()->max('1mb'),
[
(new FileFactory())->create('foo.txt', 1000),
(new FileFactory())->create('foo.txt', 999),
(new FileFactory())->create('foo.txt', 512),
]
);
}
public function testMacro()
{
File::macro('toDocument', function () {
return static::default()->rules('mimes:txt,csv');
});
$this->fails(
File::toDocument(),
(new FileFactory())->create('foo.png'),
['validation.mimes']
);
$this->passes(
File::toDocument(),
[
(new FileFactory())->create('foo.txt'),
(new FileFactory())->create('foo.csv'),
]
);
}
public function testItCanSetDefaultUsing()
{
$this->assertInstanceOf(File::class, File::default());
File::defaults(function () {
return File::types('txt')->max(12 * 1024);
});
$this->fails(
File::default(),
(new FileFactory())->create('foo.png', 13 * 1024),
[
'validation.mimes',
'validation.max.file',
]
);
File::defaults(File::image()->between(1024, 2048));
$this->passes(
File::default(),
(new FileFactory())->create('foo.png', (int) (1.5 * 1024)),
);
}
public function getIlluminateArrayTranslator(): Translator
{
return new Translator(
new ArrayLoader(),
'en'
);
}
protected function fails($rule, $values, $messages)
{
$this->assertValidationRules($rule, $values, false, $messages);
}
protected function assertValidationRules($rule, $values, $result, $messages)
{
$values = Arr::wrap($values);
foreach ($values as $value) {
$v = new Validator(
$this->getIlluminateArrayTranslator(),
['my_file' => $value],
['my_file' => is_object($rule) ? clone $rule : $rule]
);
$this->assertSame($result, $v->passes());
$this->assertSame(
$result ? [] : ['my_file' => $messages],
$v->messages()->toArray()
);
}
}
protected function passes($rule, $values)
{
$this->assertValidationRules($rule, $values, true, []);
}
}

View File

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Collection\Arr;
use Hyperf\Context\ApplicationContext;
use Hyperf\Di\Container;
use Hyperf\Testing\HttpMessage\Upload\UploadedFile;
use Hyperf\Translation\ArrayLoader;
use Hyperf\Translation\Translator;
use Hyperf\Validation\Rule;
use Hyperf\Validation\Rules\ImageFile;
use Hyperf\Validation\Validator;
use Hyperf\Validation\ValidatorFactory;
use HyperfTest\Validation\File\FileFactory;
use Mockery;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ValidationImageFileRuleTest extends TestCase
{
protected function setUp(): void
{
$container = Mockery::mock(Container::class);
ApplicationContext::setContainer($container);
$container->shouldReceive('get')->with(ValidatorFactory::class)->andReturn(
new ValidatorFactory($this->getIlluminateArrayTranslator())
);
}
protected function tearDown(): void
{
}
public function testDimensions()
{
$this->fails(
( new ImageFile())->dimensions(Rule::dimensions()->width(100)->height(100)),
(new FileFactory())->image('foo.png', 101, 101),
['validation.dimensions'],
);
$this->passes(
( new ImageFile())->dimensions(Rule::dimensions()->width(100)->height(100)),
(new FileFactory())->image('foo.png', 100, 100),
);
}
public function testDimensionsWithCustomImageSizeMethod()
{
$this->fails(
(new ImageFile())->dimensions(Rule::dimensions()->width(100)->height(100)),
new UploadedFileWithCustomImageSizeMethod(stream_get_meta_data($tmpFile = tmpfile())['uri'], 0, 0, 'foo.png'),
['validation.dimensions'],
);
$this->passes(
(new ImageFile())->dimensions(Rule::dimensions()->width(200)->height(200)),
new UploadedFileWithCustomImageSizeMethod(stream_get_meta_data($tmpFile = tmpfile())['uri'], 0, 0, 'foo.png'),
);
}
public function getIlluminateArrayTranslator(): Translator
{
return new Translator(
new ArrayLoader(),
'en'
);
}
protected function fails($rule, $values, $messages): void
{
$this->assertValidationRules($rule, $values, false, $messages);
}
protected function assertValidationRules($rule, $values, $result, $messages): void
{
$values = Arr::wrap($values);
foreach ($values as $value) {
$v = new Validator(
$this->getIlluminateArrayTranslator(),
['my_file' => $value],
['my_file' => is_object($rule) ? clone $rule : $rule]
);
$this->assertSame($result, $v->passes());
$this->assertSame(
$result ? [] : ['my_file' => $messages],
$v->messages()->toArray()
);
}
}
protected function passes($rule, $values): void
{
$this->assertValidationRules($rule, $values, true, []);
}
}
class UploadedFileWithCustomImageSizeMethod extends UploadedFile
{
public function isValid(): bool
{
return true;
}
public function guessExtension(): string
{
return 'png';
}
public function dimensions(): array
{
return [200, 200];
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Exception;
use Hyperf\Validation\Rules\ProhibitedIf;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use stdClass;
/**
* @internal
* @coversNothing
*/
class ValidationProhibitedIfTest extends TestCase
{
public function testItReturnsStringVersionOfRuleWhenCast()
{
$rule = new ProhibitedIf(function () {
return true;
});
$this->assertSame('prohibited', (string) $rule);
$rule = new ProhibitedIf(function () {
return false;
});
$this->assertSame('', (string) $rule);
$rule = new ProhibitedIf(true);
$this->assertSame('prohibited', (string) $rule);
$rule = new ProhibitedIf(false);
$this->assertSame('', (string) $rule);
}
public function testItValidatesCallableAndBooleanAreAcceptableArguments()
{
$this->assertInstanceOf(ProhibitedIf::class, new ProhibitedIf(false));
$this->assertInstanceOf(ProhibitedIf::class, new ProhibitedIf(true));
$this->assertInstanceOf(ProhibitedIf::class, new ProhibitedIf(fn () => true));
foreach ([1, 1.1, 'phpinfo', new stdClass()] as $condition) {
try {
$this->assertInstanceOf(ProhibitedIf::class, new ProhibitedIf($condition));
$this->fail('The ProhibitedIf constructor must not accept ' . gettype($condition));
} catch (InvalidArgumentException $exception) {
$this->assertEquals('The provided condition must be a callable or boolean.', $exception->getMessage());
}
}
}
public function testItThrowsExceptionIfRuleIsNotSerializable()
{
$this->expectException(Exception::class);
serialize(new ProhibitedIf(function () {
return true;
}));
}
}

View File

@ -0,0 +1 @@
Hello World!

View File

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\File;
use Hyperf\Testing\HttpMessage\Upload\UploadedFile;
class File extends UploadedFile
{
/**
* The temporary file resource.
*
* @var resource
*/
public $tempFile;
/**
* The "size" to report.
*/
public int $sizeToReport = 0;
/**
* Create a new file instance.
*
* @param resource $tempFile
*/
public function __construct(
public string $name,
$tempFile,
private int $error = 0,
private ?string $mimeType = null
) {
$this->tempFile = $tempFile;
parent::__construct(
$this->tempFilePath(),
$this->sizeToReport,
$this->error,
$this->name,
$this->mimeType
);
}
/**
* Create a new fake file.
* @param null|mixed $clientFilename
* @param null|mixed $clientMediaType
*/
public static function create(string $name, int|string $kilobytes = 0, int $error = 0, $clientFilename = null, $clientMediaType = null): File
{
return (new FileFactory())->create($name, $kilobytes, $error, $clientFilename, $clientMediaType);
}
/**
* Create a new fake file with content.
*/
public static function createWithContent(string $name, string $content): File
{
return (new FileFactory())->createWithContent($name, $content);
}
/**
* Create a new fake image.
*/
public static function image(string $name, int $width = 10, int $height = 10): File
{
return (new FileFactory())->image($name, $width, $height);
}
/**
* Set the "size" of the file in kilobytes.
*
* @return $this
*/
public function size(int $kilobytes): static
{
$this->sizeToReport = $kilobytes * 1024;
return $this;
}
/**
* Get the size of the file.
*/
public function getSize(): int
{
return $this->sizeToReport ?: parent::getSize();
}
/**
* Set the "MIME type" for the file.
*
* @return $this
*/
public function mimeType(string $mimeType): static
{
$this->mimeType = $mimeType;
return $this;
}
/**
* Get the MIME type of the file.
*/
public function getMimeType(): string
{
return $this->mimeType ?: MimeType::from($this->name);
}
/**
* Get the path to the temporary file.
*/
protected function tempFilePath(): string
{
return stream_get_meta_data($this->tempFile)['uri'];
}
}

View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\File;
use LogicException;
use function Hyperf\Tappable\tap;
class FileFactory
{
/**
* Create a new fake file.
* @param null|mixed $clientFilename
* @param null|mixed $clientMediaType
*/
public function create(string $name, int|string $kilobytes = 0, int $error = 0, $clientFilename = null, $clientMediaType = null): File
{
if (is_string($kilobytes)) {
return $this->createWithContent($name, $kilobytes);
}
return tap(new File($name, tmpfile(), $error, $clientMediaType), function ($file) use ($kilobytes, $clientMediaType) {
$file->sizeToReport = $kilobytes * 1024;
$file->mimeTypeToReport = $clientMediaType;
});
}
/**
* Create a new fake file with content.
*/
public function createWithContent(string $name, string $content): File
{
$tmpFile = tmpfile();
fwrite($tmpFile, $content);
return tap(new File($name, $tmpFile), function ($file) use ($tmpFile) {
$file->sizeToReport = fstat($tmpFile)['size'];
});
}
/**
* Create a new fake image.
*
* @throws LogicException
*/
public function image(string $name, int $width = 10, int $height = 10): File
{
return new File($name, $this->generateImage(
$width,
$height,
pathinfo($name, PATHINFO_EXTENSION)
));
}
/**
* Generate a dummy image of the given width and height.
*
* @return resource
*
* @throws LogicException
*/
protected function generateImage(int $width, int $height, string $extension)
{
if (! function_exists('imagecreatetruecolor')) {
throw new LogicException('GD extension is not installed.');
}
return tap(tmpfile(), function ($temp) use ($width, $height, $extension) {
ob_start();
$extension = in_array($extension, ['jpeg', 'png', 'gif', 'webp', 'wbmp', 'bmp'])
? strtolower($extension)
: 'jpeg';
$image = imagecreatetruecolor($width, $height);
if (! function_exists($functionName = "image{$extension}")) {
ob_get_clean();
throw new LogicException("{$functionName} function is not defined and image cannot be generated.");
}
call_user_func($functionName, $image);
fwrite($temp, ob_get_clean());
});
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\File;
use Hyperf\Support\MimeTypeExtensionGuesser;
class MimeType
{
/**
* The mime types instance.
*/
private static ?MimeTypeExtensionGuesser $mime = null;
/**
* Get the mime types instance.
*/
public static function getMimeTypes(): MimeTypeExtensionGuesser
{
if (self::$mime === null) {
self::$mime = new MimeTypeExtensionGuesser();
}
return self::$mime;
}
/**
* Get the MIME type for a file based on the file's extension.
*/
public static function from(string $filename): string
{
$extension = pathinfo($filename, PATHINFO_EXTENSION);
return self::get($extension);
}
/**
* Get the MIME type for a given extension or return all mimes.
*/
public static function get(string $extension): string
{
return self::getMimeTypes()->guessMimeType($extension) ?? 'application/octet-stream';
}
/**
* Search for the extension of a given MIME type.
*/
public static function search(string $mimeType): ?string
{
return self::getMimeTypes()->guessExtension($mimeType);
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing0
* @coversNothing
*/
class FileTest extends TestCase
{
public function testFile()
{
$file = \HyperfTest\Validation\File\File::create('foo.txt', 1024);
$this->assertSame('text/plain', $file->getMimeType());
$this->assertSame(1024 * 1024, $file->getSize());
$this->assertSame(0, $file->getError());
$file = \HyperfTest\Validation\File\File::createWithContent('foo.txt', 'bar');
$this->assertSame('text/plain', $file->getMimeType());
$this->assertSame(3, $file->getSize());
$this->assertSame(0, $file->getError());
}
public function testImage()
{
$file = \HyperfTest\Validation\File\File::image('foo.png', 1024, 1024);
$this->assertSame('image/png', $file->getMimeType());
// 读取图片尺寸
$imageSize = getimagesize($file->getPathname());
$this->assertSame([1024, 1024], [$imageSize[0], $imageSize[1]]);
$this->assertSame(0, $file->getError());
$this->assertSame('png', $file->getExtension());
$file = \HyperfTest\Validation\File\File::image('foo.jpg', 1024, 1024);
$this->assertSame('image/jpeg', $file->getMimeType());
// 读取图片尺寸
$imageSize = getimagesize($file->getPathname());
$this->assertSame([1024, 1024], [$imageSize[0], $imageSize[1]]);
$this->assertSame(0, $file->getError());
$this->assertSame('jpg', $file->getExtension());
$file = \HyperfTest\Validation\File\File::image('foo.gif', 1024, 1024);
$this->assertSame('image/gif', $file->getMimeType());
// 读取图片尺寸
$imageSize = getimagesize($file->getPathname());
$this->assertSame([1024, 1024], [$imageSize[0], $imageSize[1]]);
$this->assertSame(0, $file->getError());
$this->assertSame('gif', $file->getExtension());
}
}