Merge branch 'master' into 3.1-merge

This commit is contained in:
李铭昕 2023-08-30 11:45:07 +08:00
commit d57affbb69
33 changed files with 1828 additions and 52 deletions

View File

@ -1,5 +1,19 @@
# v3.0.35 - TBD
## Fixed
- [#6097](https://github.com/hyperf/hyperf/pull/6097) Fixed error that using non-zipkin driver of tracer.
- [#6099](https://github.com/hyperf/hyperf/pull/6099) Fixed bug that `ConstantFrequency` cannot work when using `redis`.
## 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
- [#6098](https://github.com/hyperf/hyperf/pull/6098) Optimize kafka reporter for `hyperf/tracer`.
# v3.0.34 - 2023-08-25
## Added
@ -14,12 +28,14 @@
## Fixed
- [#6065](https://github.com/hyperf/hyperf/pull/6065) Fixed bug that `Context::override` and `Context::getOrSet` cannot work when using `$coroutineId`.
- [#6090](https://github.com/hyperf/hyperf/pull/6090) Fix the bug that cannot stop the command from terminating gracefully.
## Optimized
- [#6046](https://github.com/hyperf/hyperf/pull/6046) Using the tracer instance from coroutine context.
- [#6061](https://github.com/hyperf/hyperf/pull/6061) Enhance server config to support key-value mode.
- [#6077](https://github.com/hyperf/hyperf/pull/6077) Don't show deprecated notice when using `#[Hyperf\Constants\Annotation\Constants]`.
- [#6088](https://github.com/hyperf/hyperf/pull/6088) Optimize `Hyperf\Support\Composer`.
# v3.0.33 - 2023-08-18

View File

@ -16,8 +16,12 @@ use Throwable;
class FailToExecute extends Event
{
public function __construct(Crontab $crontab, public Throwable $throwable)
public function __construct(public Crontab $crontab, public Throwable $throwable)
{
parent::__construct($crontab);
}
public function getThrowable(): Throwable
{
return $this->throwable;
}
}

View File

@ -24,4 +24,9 @@ abstract class Event
public string $server = 'http'
) {
}
public function getThrowable(): ?Throwable
{
return $this->exception;
}
}

View File

@ -127,6 +127,10 @@ class MetricFactory implements MetricFactoryInterface
}
}
if (CoordinatorManager::until(Coord::WORKER_EXIT)->isClosing()) {
return;
}
$server = $this->factory->make($host, (int) $port);
Coroutine::create(static function () use ($server) {

View File

@ -37,7 +37,7 @@ class RedisPool extends Pool
$this->config = $config->get($key);
$options = Arr::get($this->config, 'pool', []);
$this->frequency = make(Frequency::class);
$this->frequency = make(Frequency::class, [$this]);
parent::__construct($container, $options);
}

View File

@ -37,13 +37,14 @@ class Composer
public static function getLockContent(): Collection
{
if (! self::$content) {
$path = self::discoverLockFile();
if (! $path) {
if (! $path = self::discoverLockFile()) {
throw new RuntimeException('composer.lock not found.');
}
self::$content = collect(json_decode(file_get_contents($path), true));
$packages = self::$content->offsetGet('packages') ?? [];
$packagesDev = self::$content->offsetGet('packages-dev') ?? [];
foreach (array_merge($packages, $packagesDev) as $package) {
$packageName = '';
foreach ($package ?? [] as $key => $value) {
@ -51,42 +52,40 @@ class Composer
$packageName = $value;
continue;
}
switch ($key) {
case 'extra':
$packageName && self::$extra[$packageName] = $value;
break;
case 'scripts':
$packageName && self::$scripts[$packageName] = $value;
break;
case 'version':
$packageName && self::$versions[$packageName] = $value;
break;
}
$packageName && match ($key) {
'extra' => self::$extra[$packageName] = $value,
'scripts' => self::$scripts[$packageName] = $value,
'version' => self::$versions[$packageName] = $value,
default => null,
};
}
}
}
return self::$content;
}
public static function getJsonContent(): Collection
{
if (! self::$json) {
$path = BASE_PATH . '/composer.json';
if (! is_readable($path)) {
throw new RuntimeException('composer.json is not readable.');
}
self::$json = collect(json_decode(file_get_contents($path), true));
if (self::$json) {
return self::$json;
}
return self::$json;
if (! is_readable($path = BASE_PATH . '/composer.json')) {
throw new RuntimeException('composer.json is not readable.');
}
return self::$json = collect(json_decode(file_get_contents($path), true));
}
public static function discoverLockFile(): string
{
$path = '';
if (is_readable(BASE_PATH . '/composer.lock')) {
$path = BASE_PATH . '/composer.lock';
if (is_readable($path = BASE_PATH . '/composer.lock')) {
return $path;
}
return $path;
return '';
}
public static function getMergedExtra(string $key = null)
@ -94,10 +93,13 @@ class Composer
if (! self::$extra) {
self::getLockContent();
}
if ($key === null) {
return self::$extra;
}
$extra = [];
foreach (self::$extra as $project => $config) {
foreach ($config ?? [] as $configKey => $item) {
if ($key === $configKey && $item) {
@ -111,30 +113,35 @@ class Composer
}
}
}
return $extra;
}
public static function getLoader(): ClassLoader
{
if (! self::$classLoader) {
self::$classLoader = self::findLoader();
}
return self::$classLoader;
return self::$classLoader ??= self::findLoader();
}
public static function setLoader(ClassLoader $classLoader): ClassLoader
{
self::$classLoader = $classLoader;
return $classLoader;
return self::$classLoader = $classLoader;
}
public static function getScripts(): array
{
if (! self::$scripts) {
self::getLockContent();
}
return self::$scripts;
}
public static function getVersions(): array
{
if (! self::$versions) {
self::getLockContent();
}
return self::$versions;
}
@ -158,6 +165,7 @@ class Composer
private static function findLoader(): ClassLoader
{
$loaders = spl_autoload_functions();
foreach ($loaders as $loader) {
if (is_array($loader) && $loader[0] instanceof ClassLoader) {
return $loader[0];

View File

@ -0,0 +1,58 @@
<?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 OpenTracing;
use Hyperf\Tracer\TracerContext;
final class GlobalTracer
{
/**
* @var Tracer
*/
private static $instance;
/**
* @var bool
*/
private static $isRegistered = false;
/**
* GlobalTracer::set sets the [singleton] Tracer returned by get().
* Those who use GlobalTracer (rather than directly manage a Tracer instance)
* should call GlobalTracer::set as early as possible in bootstrap, prior to
* start a new span. Prior to calling GlobalTracer::set, any Spans started
* via the `Tracer::startActiveSpan` (etc) globals are noops.
*/
public static function set(Tracer $tracer): void
{
TracerContext::setTracer($tracer);
self::$isRegistered = true;
}
/**
* GlobalTracer::get returns the global singleton `Tracer` implementation.
* Before `GlobalTracer::set` is called, the `GlobalTracer::get` is a noop
* implementation that drops all data handed to it.
*/
public static function get(): Tracer
{
return TracerContext::getTracer();
}
/**
* Returns true if a global tracer has been registered, otherwise returns false.
*/
public static function isRegistered(): bool
{
return self::$isRegistered;
}
}

View File

@ -11,34 +11,113 @@ declare(strict_types=1);
*/
namespace Hyperf\Tracer\Adapter\Reporter;
use Closure;
use Hyperf\Coordinator\Constants;
use Hyperf\Coordinator\CoordinatorManager;
use Hyperf\Engine\Channel;
use Hyperf\Engine\Coroutine;
use Hyperf\Tracer\Exception\ConnectionClosedException;
use longlang\phpkafka\Producer\Producer;
use longlang\phpkafka\Producer\ProducerConfig;
use Throwable;
class KafkaClientFactory
{
private ?Producer $producer = null;
protected ?Channel $chan = null;
protected ?Producer $producer = null;
protected array $options = [];
protected int $channelSize = 65535;
public function build(array $options): callable
{
$this->producer ??= $this->createProducer($options);
$this->options = $options;
if (isset($options['channel_size'])) {
$this->channelSize = (int) $options['channel_size'];
}
$this->loop();
return function (string $payload) use ($options): void {
$this->producer->send(
$options['topic'] ?? 'zipkin',
$payload,
uniqid('', true)
);
$topic = $options['topic'] ?? 'zipkin';
$key = $options['key'] ?? uniqid('', true);
$headers = $options['headers'] ?? [];
$partitionIndex = $options['partition_index'] ?? null;
$chan = $this->chan;
$chan->push(function () use ($topic, $key, $payload, $headers, $partitionIndex) {
try {
$this->producer->send($topic, $payload, $key, $headers, $partitionIndex);
} catch (Throwable $e) {
throw $e;
}
});
if ($chan->isClosing()) {
throw new ConnectionClosedException('Connection closed.');
}
};
}
private function createProducer(array $options): Producer
public function close(): void
{
$chan = $this->chan;
$producer = $this->producer;
$this->chan = null;
$this->producer = null;
$chan?->close();
$producer?->close();
}
protected function loop(): void
{
if ($this->chan != null) {
return;
}
$this->chan = new Channel($this->channelSize);
Coroutine::create(function () {
while (true) {
$this->producer = $this->makeProducer();
while (true) {
/** @var null|Closure $closure */
$closure = $this->chan?->pop();
if (! $closure) {
break 2;
}
try {
$closure->call($this);
} catch (Throwable) {
$this->producer->close();
break;
} finally {
$closure = null;
}
}
}
$this->close();
});
Coroutine::create(function () {
if (CoordinatorManager::until(Constants::WORKER_EXIT)->yield()) {
$this->close();
}
});
}
protected function makeProducer(): Producer
{
$options = array_replace([
'bootstrap_servers' => '127.0.0.1:9092',
'acks' => -1,
'connect_timeout' => 1,
'send_timeout' => 1,
], $options);
], $this->options);
$config = new ProducerConfig();
$config->setBootstrapServer($options['bootstrap_servers']);

View File

@ -0,0 +1,34 @@
<?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\Tracer\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Tracer\TracerContext;
use Zipkin\Propagation\TraceContext;
class CreateTraceContextAspect extends AbstractAspect
{
public array $classes = [
TraceContext::class . '::create',
TraceContext::class . '::create*',
];
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
$traceContext = $proceedingJoinPoint->process();
if ($traceContext instanceof TraceContext) {
TracerContext::setTraceId($traceContext->getTraceId());
}
return $traceContext;
}
}

View File

@ -12,11 +12,13 @@ declare(strict_types=1);
namespace Hyperf\Tracer;
use GuzzleHttp\Client;
use Hyperf\Tracer\Aspect\CreateTraceContextAspect;
use Hyperf\Tracer\Aspect\HttpClientAspect;
use Hyperf\Tracer\Aspect\RedisAspect;
use Hyperf\Tracer\Aspect\TraceAnnotationAspect;
use Hyperf\Tracer\Listener\DbQueryExecutedListener;
use Jaeger\ThriftUdpTransport;
use OpenTracing\GlobalTracer;
use OpenTracing\Tracer;
use Zipkin\Propagation\Map;
@ -37,12 +39,14 @@ class ConfigProvider
'annotations' => [
'scan' => [
'class_map' => [
GlobalTracer::class => __DIR__ . '/../class_map/GlobalTracer.php',
Map::class => __DIR__ . '/../class_map/Map.php',
ThriftUdpTransport::class => __DIR__ . '/../class_map/ThriftUdpTransport.php',
],
],
],
'aspects' => [
CreateTraceContextAspect::class,
HttpClientAspect::class,
RedisAspect::class,
TraceAnnotationAspect::class,

View File

@ -0,0 +1,18 @@
<?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\Tracer\Exception;
use RuntimeException;
class ConnectionClosedException extends RuntimeException
{
}

View File

@ -0,0 +1,18 @@
<?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\Tracer\Exception;
use RuntimeException;
class TimeoutException extends RuntimeException
{
}

View File

@ -53,11 +53,9 @@ class TraceMiddleware implements MiddlewareInterface
});
try {
$response = $handler->handle($request);
/** @var \ZipkinOpenTracing\SpanContext $spanContent */
$spanContent = $span->getContext();
/** @var \Zipkin\Propagation\TraceContext $traceContext */
$traceContext = $spanContent->getContext();
$response = $response->withHeader('Trace-Id', $traceContext->getTraceId());
if ($traceId = TracerContext::getTraceId()) {
$response = $response->withHeader('Trace-Id', $traceId);
}
$span->setTag($this->spanTagManager->get('response', 'status_code'), $response->getStatusCode());
} catch (Throwable $exception) {
$this->switchManager->isEnable('exception') && $this->appendExceptionToSpan($span, $exception);

View File

@ -45,9 +45,7 @@ trait SpanStarter
TracerContext::setRoot($root);
return $root;
}
$carrier = array_map(function ($header) {
return $header[0];
}, $request->getHeaders());
$carrier = array_map(fn ($header) => $header[0], $request->getHeaders());
if ($container->has(Rpc\Context::class) && $rpcContext = $container->get(Rpc\Context::class)) {
$rpcCarrier = $rpcContext->get('tracer.carrier');
if (! empty($rpcCarrier)) {

View File

@ -23,6 +23,8 @@ class TracerContext
public const ROOT = 'tracer.root';
public const TRACE_ID = 'tracer.trace_id';
public static function setTracer(Tracer $tracer): Tracer
{
return Context::set(self::TRACER, $tracer);
@ -42,4 +44,14 @@ class TracerContext
{
return Context::get(self::ROOT) ?: null;
}
public static function setTraceId(string $traceId): string
{
return Context::set(self::TRACE_ID, $traceId);
}
public static function getTraceId(): ?string
{
return Context::get(self::TRACE_ID) ?: null;
}
}

View File

@ -21,6 +21,7 @@
"php": ">=8.1",
"egulias/email-validator": "^3.0",
"hyperf/collection": "~3.1.0",
"hyperf/conditionable": "~3.1.0",
"hyperf/contract": "~3.1.0",
"hyperf/database": "~3.1.0",
"hyperf/di": "~3.1.0",

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

@ -22,9 +22,11 @@ use Hyperf\Stringable\Str;
use Hyperf\Stringable\StrCache;
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;
@ -889,6 +891,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());
}
}