Merge branch 'master' into 1.1-dbcommand

This commit is contained in:
黄朝晖 2019-10-03 13:24:04 +08:00 committed by GitHub
commit 0ba19493fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 12930 additions and 23 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ vendor/
.env
.DS_Store
*.lock
node_modules/*

View File

@ -6,6 +6,7 @@
- [#402](https://github.com/hyperf-cloud/hyperf/pull/402) Added Annotation `@AsyncQueueMessage`.
- [#418](https://github.com/hyperf-cloud/hyperf/pull/418) Allows send WebSocket message to any `fd` in current server, even the worker process does not hold the `fd`
- [#420](https://github.com/hyperf-cloud/hyperf/pull/420) Added listener for model.
- [#429](https://github.com/hyperf-cloud/hyperf/pull/429) Added validation component, a component similar to [illuminate/validation](https://github.com/illuminate/validation).
- [#441](https://github.com/hyperf-cloud/hyperf/pull/441) Automatically close the spare redis client when it is used in low frequency.
- [#478](https://github.com/hyperf-cloud/hyperf/pull/441) Adopt opentracing interfaces and support [Jaeger](https://www.jaegertracing.io/).
- [#500](https://github.com/hyperf-cloud/hyperf/pull/499) Added fluent method calls of `Hyperf\HttpServer\Contract\ResponseInterface`.
@ -15,6 +16,7 @@
- [#597](https://github.com/hyperf-cloud/hyperf/pull/597) Added concurrent for async-queue.
- [#599](https://github.com/hyperf-cloud/hyperf/pull/599) Allows set the retry seconds according to attempt times of async queue consumer.
- [#619](https://github.com/hyperf-cloud/hyperf/pull/619) Added HandlrStackFactory of guzzle.
- [#629](https://github.com/hyperf-cloud/hyperf/pull/629) Allows to modify the `clientIp`, `pullTimeout`, `intervalTimeout` of Apollo client via config file.
## Changed
@ -42,8 +44,6 @@ return ApplicationContext::setContainer($container);
- [#523](https://github.com/hyperf-cloud/hyperf/pull/523) The command `db:model` will generate the singular class name of an plural table as default.
- [#602](https://github.com/hyperf-cloud/hyperf/pull/602) Removed timeout property of `Hyperf\Utils\Coroutine\Concurrent`.
- [#614](https://github.com/hyperf-cloud/hyperf/pull/614) [#617](https://github.com/hyperf-cloud/hyperf/pull/617) Changed the structure of config provider, also moved `config/dependencies.php` to `config/autoload/dependencies.php`, also you could place `dependencies` into config/config.php.
- [#630](https://github.com/hyperf-cloud/hyperf/pull/630) Changed the way to instantiate `Hyperf\HttpServer\CoreMiddleware`, use `make()` instead of `new`
- [#631](https://github.com/hyperf-cloud/hyperf/pull/631) Changed the way to instantiate AMQP Consumer, use `make()` instead of `new`
Changed the structure of config provider:
Before:
@ -67,6 +67,9 @@ Now:
],
```
- [#630](https://github.com/hyperf-cloud/hyperf/pull/630) Changed the way to instantiate `Hyperf\HttpServer\CoreMiddleware`, use `make()` instead of `new`.
- [#631](https://github.com/hyperf-cloud/hyperf/pull/631) Changed the way to instantiate AMQP Consumer, use `make()` instead of `new`.
- [#637](https://github.com/hyperf-cloud/hyperf/pull/637) Changed the argument 1 of `Hyperf\Contract\OnMessageInterface` and `Hyperf\Contract\OnOpenInterface`, use `Swoole\WebSocket\Server` instead of `Swoole\Server`.
- [#638](https://github.com/hyperf-cloud/hyperf/pull/638) Renamed command `db:model` to `gen:model` and added rewrite connection name visitor.
## Deleted

View File

@ -1,6 +1,6 @@
#!/usr/bin/env php
<?php
\Swoole\Coroutine::create(function () {
\Swoole\Coroutine\Run(function () {
if (version_compare('7.1.0', PHP_VERSION, '>')) {
fwrite(
STDERR,
@ -51,9 +51,8 @@
require PHPUNIT_COMPOSER_INSTALL;
$code = PHPUnit\TextUI\Command::main(false);
if ($code > 0) {
exit($code);
}
swoole_event_exit();
});

View File

@ -11,6 +11,7 @@
"require": {
"php": ">=7.2",
"ext-json": "*",
"egulias/email-validator": "^2.1",
"bandwidth-throttle/token-bucket": "^2.0",
"doctrine/annotations": "^1.6",
"doctrine/inflector": "^1.3",
@ -37,7 +38,8 @@
"squizlabs/php_codesniffer": "^3.4",
"symfony/console": "^4.2",
"symfony/finder": "^4.1",
"vlucas/phpdotenv": "^3.1"
"vlucas/phpdotenv": "^3.1",
"egulias/email-validator": "^2.1"
},
"require-dev": {
"doctrine/common": "@stable",
@ -97,6 +99,7 @@
"hyperf/tracer": "self.version",
"hyperf/translation": "self.version",
"hyperf/utils": "self.version",
"hyperf/validation": "self.version",
"hyperf/view": "self.version",
"hyperf/websocket-client": "self.version",
"hyperf/websocket-server": "self.version"
@ -165,6 +168,7 @@
"Hyperf\\Tracer\\": "src/tracer/src/",
"Hyperf\\Translation\\": "src/translation/src/",
"Hyperf\\Utils\\": "src/utils/src/",
"Hyperf\\Validation\\": "src/validation/src/",
"Hyperf\\View\\": "src/view/src/",
"Hyperf\\WebSocketClient\\": "src/websocket-client/src/",
"Hyperf\\WebSocketServer\\": "src/websocket-server/src/"
@ -215,6 +219,7 @@
"HyperfTest\\Testing\\": "src/testing/tests/",
"HyperfTest\\Translation\\": "src/translation/tests/",
"HyperfTest\\Utils\\": "src/utils/tests/",
"HyperfTest\\Validation\\": "src/validation/tests/",
"HyperfTest\\WebSocketClient\\": "src/websocket-client/tests/"
}
},
@ -271,6 +276,7 @@
"Hyperf\\Tracer\\ConfigProvider",
"Hyperf\\Translation\\ConfigProvider",
"Hyperf\\Utils\\ConfigProvider",
"Hyperf\\Validation\\ConfigProvider",
"Hyperf\\View\\ConfigProvider",
"Hyperf\\WebSocketClient\\ConfigProvider",
"Hyperf\\WebSocketServer\\ConfigProvider"

View File

@ -71,7 +71,7 @@ This component is the official default configuration component that is implement
### Set configuration value
Configurations in the `config/config.php` and `config/server.php` and `autoload` folders can be scanned and injected into the corresponding object of `Hyperf\Contract\ConfigInterface` when the server starts. This process is done by `Hyperf\Config\ConfigFactory` when the Config object is instantiated.
Configurations in the `config/config.php` and `config/autoload/server.php` and `autoload` folders can be scanned and injected into the corresponding object of `Hyperf\Contract\ConfigInterface` when the server starts. This process is done by `Hyperf\Config\ConfigFactory` when the Config object is instantiated.
### Get configuration value

View File

@ -4,7 +4,7 @@
Hyperf is based on [Swoole](http://github.com/swoole/swoole-src). To understand the life cycle of Hyperf, then understand the life cycle of [Swoole](http://github.com/swoole/swoole-src) is also crucial.
Hyperf's command management is supported by [symfony/console](https://github.com/symfony/console) by default * (if you wish to replace this component you can also change the entry file of skeleton to the component that you wish to use) *, after executing `php bin/hyperf.php start`, it will be taken over by the `Hyperf\Server\Command\StartServer` command class and started one by one according to the `Server` defined in the configuration file `config/server.php`.
Hyperf's command management is supported by [symfony/console](https://github.com/symfony/console) by default * (if you wish to replace this component you can also change the entry file of skeleton to the component that you wish to use) *, after executing `php bin/hyperf.php start`, it will be taken over by the `Hyperf\Server\Command\StartServer` command class and started one by one according to the `Server` defined in the configuration file `config/autoload/server.php`.
Regarding the initialization of the dependency injection container, we are not implemented by any component, because once it is implemented by some component, the coupling will be very obvious, so by default, the configuration file `config/container.php` is loaded by the entry file to initialize the container.

View File

@ -18,7 +18,7 @@ The global middleware can ONLY be configured through the configuration file. The
```php
<?php
return [
// `http` corresponds to the value corresponding to the name attribute of each server in config/server.php. This configuration is only applied to the server you configured.
// `http` corresponds to the value corresponding to the name attribute of each server in config/autoload/server.php. This configuration is only applied to the server you configured.
'http' => [
// Configure your global middleware in an array, in order according to the order of the array
YourMiddleware::class

View File

@ -68,7 +68,7 @@ return [
### 设置配置
只需在 `config/config.php``config/server.php` 与 `autoload` 文件夹内的配置,都能在服务启动时被扫描并注入到 `Hyperf\Contract\ConfigInterface` 对应的对象中,这个流程是由 `Hyperf\Config\ConfigFactory` 在 Config 对象实例化时完成的。
只需在 `config/config.php``config/autoload/server.php` 与 `autoload` 文件夹内的配置,都能在服务启动时被扫描并注入到 `Hyperf\Contract\ConfigInterface` 对应的对象中,这个流程是由 `Hyperf\Config\ConfigFactory` 在 Config 对象实例化时完成的。
### 获取配置

View File

@ -189,6 +189,8 @@ $result = parallel([
### Concurrent 协程运行控制
> Concurrent 仅可在 1.0.16 版本或更高版本使用
`Hyperf\Utils\Coroutine\Concurrent` 基于 `Swoole\Coroutine\Channel` 实现,用来控制一个代码块内同时运行的最大协程数量的特性。
以下样例,当同时执行 `10` 个子协程时,会在循环中阻塞,但只会阻塞当前协程,直到释放出一个位置后,循环继续执行下一个子协程。

View File

@ -3,7 +3,7 @@
## 框架生命周期
Hyperf 是运行于 [Swoole](http://github.com/swoole/swoole-src) 之上的,想要理解透彻 Hyperf 的生命周期,那么理解 [Swoole](http://github.com/swoole/swoole-src) 的生命周期也至关重要。
Hyperf 的命令管理默认由 [symfony/console](https://github.com/symfony/console) 提供支持*(如果您希望更换该组件您也可以通过改变 skeleton 的入口文件更换成您希望使用的组件)*,在执行 `php bin/hyperf.php start` 后,将由 `Hyperf\Server\Command\StartServer` 命令类接管,并根据配置文件 `config/server.php` 内定义的 `Server` 逐个启动。
Hyperf 的命令管理默认由 [symfony/console](https://github.com/symfony/console) 提供支持*(如果您希望更换该组件您也可以通过改变 skeleton 的入口文件更换成您希望使用的组件)*,在执行 `php bin/hyperf.php start` 后,将由 `Hyperf\Server\Command\StartServer` 命令类接管,并根据配置文件 `config/autoload/server.php` 内定义的 `Server` 逐个启动。
关于依赖注入容器的初始化工作,我们并没有由组件来实现,因为一旦交由组件来实现,这个耦合就会非常的明显,所以在默认的情况下,是由入口文件来加载 `config/container.php` 来实现的。
## 请求与协程生命周期

View File

@ -18,7 +18,7 @@
```php
<?php
return [
// http 对应 config/server.php 内每个 server 的 name 属性对应的值,该配置仅应用在该 Server 中
// http 对应 config/autoload/server.php 内每个 server 的 name 属性对应的值,该配置仅应用在该 Server 中
'http' => [
// 数组内配置您的全局中间件,顺序根据该数组的顺序
YourMiddleware::class

View File

@ -44,7 +44,7 @@ return [
return [
// 默认语言
'locale' => 'zh_CN',
'locale' => 'zh-CN',
// 回退语言,当默认语言的语言文本没有提供时,就会使用回退语言的对应语言文本
'fallback_locale' => 'en',
// 语言文件存放的文件夹

848
doc/zh/validation.md Normal file
View File

@ -0,0 +1,848 @@
# 验证器
## 前言
> [hyperf/validation](https://github.com/hyperf-cloud/validation) 衍生于 [illuminate/validation](https://github.com/illuminate/validation),我们对它进行了一些改造,但保持了验证规则的相同。在这里感谢一下 Laravel 开发组,实现了如此强大好用的验证器组件。
## 安装
### 引入组件包
```bash
composer require hyperf/validation
```
### 添加中间件
您需要为使用到验证器组件的 Server 加上一个全局中间件 `Hyperf\Validation\Middleware\ValidationMiddleware` 配置,如下为 `http` Server 加上对应的全局中间件。如没有正确设置全局中间件,可能会导致 `表单请求(FormRequest)` 的使用方式无效。
```php
<?php
return [
// http 对应 config/autoload/server.php 内每个 server 的 name 属性对应的值,该配置仅应用在该 Server 中
'http' => [
// 数组内配置您的全局中间件,顺序根据该数组的顺序
\Hyperf\Validation\Middleware\ValidationMiddleware::class
// 这里隐藏了其它中间件
],
];
```
### 添加异常处理器
异常处理器主要对 `Hyperf\Validation\ValidationException` 异常进行处理,我们提供了一个 `Hyperf\Validation\ValidationExceptionHandler` 来进行处理,您需要手动将这个异常处理器配置到您的项目的 `config/autoload/exception.php` 文件内,当然,您也可以自定义您的异常处理器。
```php
<?php
use Hyperf\Validation\ValidationExceptionHandler;
return [
'handler' => [
// 这里对应您当前的 Server 名称
'http' => [
ValidationExceptionHandler::class,
],
],
];
```
### 发布验证器语言文件
```bash
php bin/hyperf.php vendor:publish hyperf/validation
```
执行上面的命令会将验证器的语言文件 `validation.php` 发布到对应的语言文件目录,`en` 指英文语言文件,`zh-CN` 指中文简体的语言文件,您可以按照实际需要对 `validation.php` 文件内容进行修改和自定义。
```
/storage
/languages
/en
validation.php
/zh-CN
validation.php
```
> 由于存在多语言的功能,故该组件依赖 [hyperf/translation](https://github.com/hyperf-cloud/translation) 组件。
## 使用
### 表单请求验证
对于复杂的验证场景,您可以创建一个 `表单请求(FormRequest)`,表单请求是包含验证逻辑的一个自定义请求类,您可以通过执行下面的命令创建一个名为 `FooRequest` 的表单验证类:
```bash
php bin/hyperf.php gen:request FooRequest
```
表单验证类会生成于 `app\Request` 目录下,如果该目录不存在,运行命令时会自动创建目录。
接下来我们添加一些验证规则到该类的 `rules` 方法:
```php
/**
* 获取应用到请求的验证规则
*/
public function rules(): array
{
return [
'foo' => 'required|max:255',
'bar' => 'required',
];
}
```
那么,验证规则要如何生效呢?您所要做的就是在控制器方法中通过类型提示声明该请求类为参数。这样在控制器方法被调用之前会验证传入的表单请求,这意味着你不需要在控制器中写任何验证逻辑并很好的解耦了这两部分的代码:
```php
<?php
namespace App\Controller;
use App\Request\FooRequest;
class IndexController
{
public function index(FooRequest $request)
{
// 传入的请求通过验证...
// 获取通过验证的数据...
$validated = $request->validated();
}
}
```
如果验证失败,验证器会抛一个 `Hyperf\Validation\ValidationException` 异常,您可以在通过添加一个自定义的异常处理类来处理该异常,与此同时,我们也提供了一个`Hyperf\Validation\ValidationExceptionHandler` 异常处理器来处理该异常,您也可以直接配置我们提供的异常处理器来处理。但默认提供的异常处理器不一定能够满足您的需求,您可以根据情况通过自定义异常处理器自定义处理验证失败后的行为。
#### 自定义错误消息
您可以通过重写 `messages` 方法来自定义表单请求使用的错误消息,该方法应该返回属性/规则对数组及其对应错误消息:
```php
/**
* 获取已定义验证规则的错误消息
*/
public function messages(): array
{
return [
'foo.required' => 'foo is required',
'bar.required' => 'bar is required',
];
}
```
#### 自定义验证属性
如果您希望将验证消息中的 `:attribute` 部分替换为自定义的属性名,则可以通过重写 `attributes` 方法来指定自定义的名称。该方法会返回属性名及对应自定义名称键值对数组:
```php
/**
* 获取验证错误的自定义属性
*/
public function attributes(): array
{
return [
'foo' => 'foo of request',
];
}
```
### 手动创建验证器
如果您不想使用 `表单请求(FormRequest)` 的自动验证功能,可以通过注入 `ValidationFactoryInterface` 接口类来获得验证器工厂类,然后通过 `make` 方法手动创建一个验证器实例:
```php
<?php
namespace App\Controller;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
class IndexController
{
/**
* @Inject()
* @var ValidationFactoryInterface
*/
protected $validationFactory;
public function foo(RequestInterface $request)
{
$validator = $this->validationFactory->make(
$request->all(),
[
'foo.required' => 'foo is required',
'bar.required' => 'bar is required',
]
);
if ($validator->fails()){
// Handle exception
$errorMessage = $validator->errors()->first();
}
// Do something
}
}
```
传给 `make` 方法的第一个参数是需要验证的数据,第二个参数则是该数据的验证规则。
#### 自定义错误消息
如果有需要,你也可以使用自定义错误信息代替默认值进行验证。有几种方法可以指定自定义信息。首先,你可以将自定义信息作为第三个参数传递给 `make` 方法:
```php
<?php
$messages = [
'required' => 'The :attribute field is required.',
];
$validator = $this->validationFactory->make($request->all(), $rules, $messages);
```
在这个例子中, `:attribute` 占位符会被验证字段的实际名称替换。除此之外,你还可以在验证消息中使用其它占位符。例如:
```php
$messages = [
'same' => 'The :attribute and :other must match.',
'size' => 'The :attribute must be exactly :size.',
'between' => 'The :attribute value :input is not between :min - :max.',
'in' => 'The :attribute must be one of the following types: :values',
];
```
#### 为给定属性指定自定义信息
有时候你可能只想为特定的字段自定义错误信息。只需在属性名称后使用「点」来指定验证的规则即可:
```php
$messages = [
'email.required' => 'We need to know your e-mail address!',
];
```
#### 在 PHP 文件中指定自定义信息
在大多数情况下,您可能会在文件中指定自定义信息,而不是直接将它们传递给 `Validator` 。为此,需要把你的信息放置于 `storage/languages/xx/validation.php` 语言文件内的 `custom` 数组中。
#### 在 PHP 文件中指定自定义属性
如果你希望将验证信息的 `:attribute` 部分替换为自定义属性名称,你可以在 `storage/languages/xx/validation.php` 语言文件的 `attributes` 数组中指定自定义名称:
```php
'attributes' => [
'email' => 'email address',
],
```
### 验证后钩子
验证器还允许你添加在验证成功之后允许的回调函数,以便你进行下一步的验证,甚至在消息集合中添加更多的错误消息。使用它只需在验证实例上使用 `after` 方法:
```php
<?php
namespace App\Controller;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
class IndexController
{
/**
* @Inject()
* @var ValidationFactoryInterface
*/
protected $validationFactory;
public function foo(RequestInterface $request)
{
$validator = $this->validationFactory->make(
$request->all(),
[
'foo.required' => 'foo is required',
'bar.required' => 'bar is required',
]
);
$validator->after(function ($validator) {
if ($this->somethingElseIsInvalid()) {
$validator->errors()->add('field', 'Something is wrong with this field!');
}
});
if ($validator->fails()) {
//
}
}
}
```
## 处理错误消息
通过 `Validator` 实例调用 `errors` 方法,会返回 `Hyperf\Utils\MessageBag` 实例,它拥有各种方便的方法处理错误信息。
### 查看特定字段的第一个错误信息
要查看特定字段的第一个错误消息,可以使用 `first` 方法:
```php
$errors = $validator->errors();
echo $errors->first('foo');
```
### 查看特定字段的所有错误消息
如果你需要获取指定字段的所有错误信息的数组,则可以使用 `get` 方法:
```php
foreach ($errors->get('foo') as $message) {
//
}
```
如果要验证表单的数组字段,你可以使用 `*` 来获取每个数组元素的所有错误消息:
```php
foreach ($errors->get('foo.*') as $message) {
//
}
```
### 查看所有字段的所有错误消息
如果你想要得到所有字段的所有错误消息,可以使用 `all` 方法:
```php
foreach ($errors->all() as $message) {
//
}
```
### 判断特定字段是否含有错误消息
`has` 方法可以被用来判断指定字段是否存在错误信息:
```php
if ($errors->has('foo')) {
//
}
```
## 验证规则
下面是有效规则及其函数列表:
##### accepted
验证字段的值必须是 `yes`、`on`、`1` 或 `true`,这在「同意服务协议」时很有用。
##### active_url
验证字段必须是基于 `PHP` 函数 `dns_get_record` 的,有 `A``AAAA` 记录的值。
##### after:date
验证字段必须是给定日期之后的一个值,日期将会通过 PHP 函数 strtotime 传递:
'start_date' => 'required|date|after:tomorrow'
你可以指定另外一个与日期进行比较的字段,而不是传递一个日期字符串给 strtotime 执行:
'finish_date' => 'required|date|after:start_date'
##### after_or_equal:date
验证字段必须是大于等于给定日期的值,更多信息,请参考 after:date 规则。
##### alpha
验证字段必须是字母。
##### alpha_dash
验证字段可以包含字母和数字,以及破折号和下划线。
##### alpha_num
验证字段必须是字母或数字。
##### array
验证字段必须是 PHP 数组。
##### bail
第一个验证规则验证失败则停止运行其它验证规则。
##### before:date
和 after:date 相对,验证字段必须是指定日期之前的一个数值,日期将会传递给 PHP strtotime 函数。
##### before_or_equal:date
验证字段必须小于等于给定日期。日期将会传递给 PHP 的 strtotime 函数。
##### between:min,max
验证字段大小在给定的最小值和最大值之间,字符串、数字、数组和文件都可以像使用 size 规则一样使用该规则:
'name' => 'required|between:1,20'
boolean
验证字段必须可以被转化为布尔值,接收 true, false, 1, 0, "1" 和 "0" 等输入。
##### confirmed
验证字段必须有一个匹配字段 foo_confirmation例如如果验证字段是 password必须输入一个与之匹配的 password_confirmation 字段。
##### date
验证字段必须是一个基于 PHP strtotime 函数的有效日期
##### date_equals:date
验证字段必须等于给定日期,日期会被传递到 PHP strtotime 函数。
##### date_format:format
验证字段必须匹配指定格式,可以使用 PHP 函数date 或 date_format 验证该字段。
##### different:field
验证字段必须是一个和指定字段不同的值。
##### digits:value
验证字段必须是数字且长度为 value 指定的值。
##### digits_between:min,max
验证字段数值长度必须介于最小值和最大值之间。
##### dimensions
验证的图片尺寸必须满足该规定参数指定的约束条件:
```php
'avatar' => 'dimensions:min_width=100,min_height=200'
```
有效的约束条件包括:`min_width`, `max_width`, `min_height`, `max_height`, `width`, `height`, `ratio`
`ratio` 约束宽度/高度的比率,这可以通过表达式 `3/2` 或浮点数 `1.5` 来表示:
```php
'avatar' => 'dimensions:ratio=3/2'
```
由于该规则要求多个参数,可以使用 `Rule::dimensions` 方法来构造该规则:
```
use Hyperf\Validation\Rule;
public function rules(): array
{
return [
'avatar' => [
'required',
Rule::dimensions()->maxWidth(1000)->maxHeight(500)->ratio(3 / 2),
],
];
}
```
##### distinct
处理数组时,验证字段不能包含重复值:
```php
'foo.*.id' => 'distinct'
```
##### email
验证字段必须是格式正确的电子邮件地址。
##### exists:table,column
验证字段必须存在于指定数据表。
基本使用:
```
'state' => 'exists:states'
```
如果 `column` 选项没有指定,将会使用字段名。
指定自定义列名:
```php
'state' => 'exists:states,abbreviation'
```
有时,你可能需要为 `exists` 查询指定要使用的数据库连接,这可以在表名前通过`.`前置数据库连接来实现:
```php
'email' => 'exists:connection.staff,email'
```
如果你想要自定义验证规则执行的查询,可以使用 `Rule` 类来定义规则。在这个例子中,我们还以数组形式指定了验证规则,而不是使用 `|` 字符来限定它们:
```php
use Hyperf\Validation\Rule;
$validator = $this->validationFactory->make($data, [
'email' => [
'required',
Rule::exists('staff')->where(function ($query) {
$query->where('account_id', 1);
}),
],
]);
```
##### file
验证字段必须是上传成功的文件。
##### filled
验证字段如果存在则不能为空。
##### gt:field
验证字段必须大于给定 `field` 字段,这两个字段类型必须一致,适用于字符串、数字、数组和文件,和 `size` 规则类似
##### gte:field
验证字段必须大于等于给定 `field` 字段,这两个字段类型必须一致,适用于字符串、数字、数组和文件,和 `size` 规则类似
##### image
验证文件必须是图片(`jpeg`、`png`、`bmp`、`gif` 或者 `svg`
##### in:foo,bar…
验证字段值必须在给定的列表中,由于该规则经常需要我们对数组进行 `implode`,我们可以使用 `Rule::in` 来构造这个规则:
```php
use Hyperf\Validation\Rule;
$validator = $this->validationFactory->make($data, [
'zones' => [
'required',
Rule::in(['first-zone', 'second-zone']),
],
]);
```
##### in_array:anotherfield
验证字段必须在另一个字段值中存在。
##### integer
验证字段必须是整型。
##### ip
验证字段必须是IP地址。
##### ipv4
验证字段必须是IPv4地址。
##### ipv6
验证字段必须是IPv6地址。
##### json
验证字段必须是有效的JSON字符串
##### lt:field
验证字段必须小于给定 `field` 字段,这两个字段类型必须一致,适用于字符串、数字、数组和文件,和 `size` 规则类似
##### lte:field
验证字段必须小于等于给定 `field` 字段,这两个字段类型必须一致,适用于字符串、数字、数组和文件,和 `size` 规则类似
##### max:value
验证字段必须小于等于最大值,和字符串、数值、数组、文件字段的 `size` 规则使用方式一样。
##### mimetypestext/plain…
验证文件必须匹配给定的 `MIME` 文件类型之一:
```php
'video' => 'mimetypes:video/avi,video/mpeg,video/quicktime'
```
为了判断上传文件的 `MIME` 类型,组件将会读取文件内容来猜测 `MIME` 类型,这可能会和客户端 `MIME` 类型不同。
##### mimes:foo,bar,…
验证文件的 `MIME` 类型必须是该规则列出的扩展类型中的一个
`MIME` 规则的基本使用:
```php
'photo' => 'mimes:jpeg,bmp,png'
```
尽管你只是指定了扩展名,该规则实际上验证的是通过读取文件内容获取到的文件 `MIME` 类型。
完整的 `MIME` 类型列表及其相应的扩展可以在这里找到:[mime types](http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types)
##### min:value
`max:value` 相对,验证字段必须大于等于最小值,对字符串、数值、数组、文件字段而言,和 `size` 规则使用方式一致。
##### not_in:foo,bar,…
验证字段值不能在给定列表中,和 `in` 规则类似,我们可以使用 `Rule::notIn` 方法来构建规则:
```php
use Hyperf\Validation\Rule;
$validator = $this->validationFactory->make($data, [
'toppings' => [
'required',
Rule::notIn(['sprinkles', 'cherries']),
],
]);
```
##### not_regex:pattern
验证字段不能匹配给定正则表达式
注:使用 `regex/not_regex` 模式时,规则必须放在数组中,而不能使用管道分隔符,尤其是正则表达式中包含管道符号时。
##### nullable
验证字段可以是 `null`,这在验证一些可以为 `null` 的原始数据如整型或字符串时很有用。
##### numeric
验证字段必须是数值
##### present
验证字段必须出现在输入数据中但可以为空。
##### regex:pattern
验证字段必须匹配给定正则表达式。
该规则底层使用的是 `PHP``preg_match` 函数。因此,指定的模式需要遵循 `preg_match` 函数所要求的格式并且包含有效的分隔符。例如:
```php
'email' => 'regex:/^.+@.+$/i'
```
注:使用 `regex/not_regex` 模式时,规则必须放在数组中,而不能使用管道分隔符,尤其是正则表达式中包含管道符号时。
##### required
验证字段值不能为空,以下情况字段值都为空:
值为`null`
值是空字符串
值是空数组或者空的 `Countable` 对象
值是上传文件但路径为空
##### required_if:anotherfield,value,…
验证字段在 `anotherfield` 等于指定值 `value` 时必须存在且不能为空。
如果你想要为 `required_if` 规则构造更复杂的条件,可以使用 `Rule::requiredIf` 方法,该方法接收一个布尔值或闭包。当传递一个闭包时,会返回 `true``false` 以表明验证字段是否是必须的:
```php
use Hyperf\Validation\Rule;
$validator = $this->validationFactory->make($request->all(), [
'role_id' => Rule::requiredIf($request->user()->is_admin),
]);
$validator = $this->validationFactory->make($request->all(), [
'role_id' => Rule::requiredIf(function () use ($request) {
return $request->user()->is_admin;
}),
]);
```
##### required_unless:anotherfield,value,…
除非 `anotherfield` 字段等于 `value`,否则验证字段不能空。
##### required_with:foo,bar,…
验证字段只有在任一其它指定字段存在的情况才是必须的。
##### required_with_all:foo,bar,…
验证字段只有在所有指定字段存在的情况下才是必须的。
##### required_without:foo,bar,…
验证字段只有当任一指定字段不存在的情况下才是必须的。
##### required_without_all:foo,bar,…
验证字段只有当所有指定字段不存在的情况下才是必须的。
##### same:field
给定字段和验证字段必须匹配。
##### size:value
验证字段必须有和给定值 `value` 相匹配的尺寸/大小,对字符串而言,`value` 是相应的字符数目;对数值而言,`value` 是给定整型值;对数组而言,`value` 是数组长度;对文件而言,`value` 是相应的文件千字节数KB
##### starts_with:foo,bar,...
验证字段必须以某个给定值开头。
##### string
验证字段必须是字符串,如果允许字段为空,需要分配 `nullable` 规则到该字段。
##### timezone
验证字符必须是基于 `PHP` 函数 `timezone_identifiers_list` 的有效时区标识
##### unique:table,column,except,idColumn
验证字段在给定数据表上必须是唯一的,如果不指定 `column` 选项,字段名将作为默认 `column`
1. 指定自定义列名:
```php
'email' => 'unique:users,email_address'
```
2. 自定义数据库连接:
有时候,你可能需要自定义验证器生成的数据库连接,正如上面所看到的,设置 `unique:users` 作为验证规则将会使用默认数据库连接来查询数据库。要覆盖默认连接,在数据表名后使用“.”指定连接:
```php
'email' => 'unique:connection.users,email_address'
```
3. 强制一个忽略给定 `ID` 的唯一规则:
有时候,你可能希望在唯一检查时忽略给定 `ID`,例如,考虑一个包含用户名、邮箱地址和位置的”更新属性“界面,你将要验证邮箱地址是唯一的,然而,如果用户只改变用户名字段而并没有改变邮箱字段,你不想要因为用户已经拥有该邮箱地址而抛出验证错误,你只想要在用户提供的邮箱已经被别人使用的情况下才抛出验证错误。
要告诉验证器忽略用户 `ID`,可以使用 `Rule` 类来定义这个规则,我们还要以数组方式指定验证规则,而不是使用 `|` 来界定规则:
```php
use Hyperf\Validation\Rule;
$validator = $this->validationFactory->make($data, [
'email' => [
'required',
Rule::unique('users')->ignore($user->id),
],
]);
```
除了传递模型实例主键值到 `ignore` 方法之外,你还可以传递整个模型实例。组件会自动从模型实例中解析出主键值:
```php
Rule::unique('users')->ignore($user)
```
如果你的数据表使用主键字段不是 `id`,可以在调用 `ignore` 方法的时候指定字段名称:
```php
'email' => Rule::unique('users')->ignore($user->id, 'user_id')
```
默认情况下,`unique` 规则会检查与要验证的属性名匹配的列的唯一性。不过,你可以指定不同的列名作为 `unique` 方法的第二个参数:
```php
Rule::unique('users', 'email_address')->ignore($user->id),
```
4. 添加额外的 `where` 子句:
使用 `where` 方法自定义查询的时候还可以指定额外查询约束,例如,下面我们来添加一个验证 `account_id` 为 1 的约束:
```php
'email' => Rule::unique('users')->where(function ($query) {
$query->where('account_id', 1);
})
```
##### url
验证字段必须是有效的 URL。
##### uuid
该验证字段必须是有效的 RFC 4122版本 1、3、4 或 5全局唯一标识符UUID
##### sometimes
添加条件规则
存在时验证
在某些场景下,你可能想要只有某个字段存在的情况下进行验证检查,要快速实现这个,添加 `sometimes` 规则到规则列表:
```php
$validator = $this->validationFactory->make($data, [
'email' => 'sometimes|required|email',
]);
```
在上例中,`email` 字段只有存在于 `$data` 数组时才会被验证。
注:如果你尝试验证一个总是存在但可能为空的字段时,参考可选字段注意事项。
复杂条件验证
有时候你可能想要基于更复杂的条件逻辑添加验证规则。例如,你可能想要只有在另一个字段值大于 100 时才要求一个给定字段是必须的,或者,你可能需要只有当另一个字段存在时两个字段才都有给定值。添加这个验证规则并不是一件头疼的事。首先,创建一个永远不会改变的静态规则到 Validator 实例:
$validator = $this->validationFactory->make($data, [
'email' => 'required|email',
'games' => 'required|numeric',
]);
让我们假定我们的 Web 应用服务于游戏收藏者。如果一个游戏收藏者注册了我们的应用并拥有超过 100 个游戏,我们想要他们解释为什么他们会有这么多游戏,例如,也许他们在运营一个游戏二手店,又或者他们只是喜欢收藏。要添加这种条件,我们可以使用 Validator 实例上的 sometimes 方法:
$v->sometimes('reason', 'required|max:500', function($input) {
return $input->games >= 100;
});
传递给 sometimes 方法的第一个参数是我们需要有条件验证的名称字段,第二个参数是我们想要添加的规则,如果作为第三个参数的闭包返回 true规则被添加。该方法让构建复杂条件验证变得简单你甚至可以一次为多个字段添加条件验证
$v->sometimes(['reason', 'cost'], 'required', function($input) {
return $input->games >= 100;
});
注:传递给闭包的 $input 参数是 Hyperf\Support\Fluent 的一个实例,可用于访问输入和文件。
### 验证数组输入
验证表单数组输入字段不再是件痛苦的事情,例如,如果进入的 HTTP 请求包含 `photos[profile]` 字段,可以这么验证:
$validator = $this->validationFactory->make($request->all(), [
'photos.profile' => 'required|image',
]);
我们还可以验证数组的每个元素,例如,要验证给定数组输入中每个 email 是否是唯一的,可以这么做(这种针对提交的数组字段是二维数组,如 `person[][email]``person[test][email]`
```php
$validator = $this->validationFactory->make($request->all(), [
'person.*.email' => 'email|unique:users',
'person.*.first_name' => 'required_with:person.*.last_name',
]);
```
类似地,在语言文件中你也可以使用 `*` 字符指定验证消息,从而可以使用单个验证消息定义基于数组字段的验证规则:
```php
'custom' => [
'person.*.email' => [
'unique' => '每个人的邮箱地址必须是唯一的',
]
],
```

View File

@ -43,6 +43,7 @@
<directory suffix="Test.php">./src/tracer/tests</directory>
<directory suffix="Test.php">./src/translation/tests</directory>
<directory suffix="Test.php">./src/utils/tests</directory>
<directory suffix="Test.php">./src/validation/tests</directory>
<directory suffix="Test.php">./src/websocket-client/tests</directory>
</testsuite>
</testsuites>

View File

@ -21,10 +21,14 @@ class ClientFactory
public function __invoke(ContainerInterface $container)
{
$config = $container->get(ConfigInterface::class);
/** @var \Hyperf\ConfigApollo\Option $option */
$option = make(Option::class);
$option->setServer($config->get('apollo.server', 'http://127.0.0.1:8080'))
->setAppid($config->get('apollo.appid', ''))
->setCluster($config->get('apollo.cluster', ''));
->setCluster($config->get('apollo.cluster', ''))
->setClientIp($config->get('apollo.client_ip', current(swoole_get_local_ip())))
->setPullTimeout($config->get('apollo.pull_timeout', 10))
->setIntervalTimeout($config->get('apollo.interval_timeout', 60));
$namespaces = $config->get('apollo.namespaces', []);
$callbacks = [];
foreach ($namespaces as $namespace => $callable) {

View File

@ -12,8 +12,8 @@ declare(strict_types=1);
namespace Hyperf\Contract;
use Swoole\Server;
use Swoole\Websocket\Frame;
use Swoole\WebSocket\Server;
interface OnMessageInterface
{

View File

@ -13,7 +13,7 @@ declare(strict_types=1);
namespace Hyperf\Contract;
use Swoole\Http\Request;
use Swoole\Server;
use Swoole\WebSocket\Server;
interface OnOpenInterface
{

View File

@ -0,0 +1,53 @@
<?php
namespace Hyperf\Contract;
use Hyperf\Utils\Contracts\MessageProvider;
use Hyperf\Utils\Contracts\MessageBag;
interface ValidatorInterface extends MessageProvider
{
/**
* Run the validator's rules against its data.
*/
public function validate(): array;
/**
* Get the attributes and values that were validated.
*/
public function validated(): array;
/**
* Determine if the data fails the validation rules.
*/
public function fails(): bool;
/**
* Get the failed validation rules.
*/
public function failed(): array;
/**
* Add conditions to a given field based on a Closure.
*
* @param array|string $attribute
* @param array|string $rules
* @return $this
*/
public function sometimes($attribute, $rules, callable $callback);
/**
* Add an after validation callback.
*
* @param callable|string $callback
* @return $this
*/
public function after($callback);
/**
* Get all of the validation error messages.
*/
public function errors(): MessageBag;
}

View File

@ -38,8 +38,11 @@ return [
'middleware' => [
'namespace' => 'App\\Middleware',
],
'Process' => [
'process' => [
'namespace' => 'App\\Process',
],
'request' => [
'namespace' => 'App\\Request',
],
],
];

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Devtool\Generator;
use Hyperf\Command\Annotation\Command;
/**
* @Command
*/
class RequestCommand extends GeneratorCommand
{
public function __construct()
{
parent::__construct('gen:request');
$this->setDescription('Create a new form request class');
}
protected function getStub(): string
{
return $this->getConfig()['stub'] ?? __DIR__ . '/stubs/validation-request.stub';
}
protected function getDefaultNamespace(): string
{
return $this->getConfig()['namespace'] ?? 'App\\Request';
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace %NAMESPACE%;
use Hyperf\Validation\Request\FormRequest;
class %CLASS% extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return false;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
//
];
}
}

View File

@ -102,7 +102,7 @@ class InitProxyCommand extends Command
try {
$this->container->get($item);
} catch (\Throwable $ex) {
// Entry cannot be resoleved.
// Entry cannot be resolved.
}
}
@ -111,7 +111,7 @@ class InitProxyCommand extends Command
try {
$this->container->get($key);
} catch (\Throwable $ex) {
// Entry cannot be resoleved.
// Entry cannot be resolved.
}
}
}

View File

@ -61,6 +61,11 @@ class UploadedFile extends \SplFileInfo implements UploadedFileInterface
*/
private $size;
/**
* @var string
*/
private $mimeType;
/**
* @param string $tmpFile
* @param null|int $size
@ -98,6 +103,14 @@ class UploadedFile extends \SplFileInfo implements UploadedFileInterface
return end($segments) ?? null;
}
public function getMimeType(): string
{
if (is_string($this->mimeType)) {
return $this->mimeType;
}
return $this->mimeType = mime_content_type($this->tmpFile);
}
/**
* Returns whether the file was uploaded successfully.
*

View File

@ -40,6 +40,9 @@
"cs-fix": "php-cs-fixer fix $1"
},
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
},
"hyperf": {
"config": "Hyperf\\Translation\\ConfigProvider"
}

View File

@ -274,7 +274,6 @@ class Translator implements TranslatorInterface
// Once we have the parsed array of this key's elements, such as its groups
// and namespace, we will cache each array inside a simple list that has
// the key and the parsed array for quick look-ups for later requests.
return $this->parsed[$key] = $parsed;
}
@ -394,6 +393,7 @@ class Translator implements TranslatorInterface
if (is_string($line)) {
return $this->makeReplacements($line, $replace);
}
if (is_array($line) && count($line) > 0) {
foreach ($line as $key => $value) {
$line[$key] = $this->makeReplacements($value, $replace);
@ -405,8 +405,11 @@ class Translator implements TranslatorInterface
/**
* Make the place-holder replacements on a line.
*
* @param array|string $line
* @return array|string
*/
protected function makeReplacements(string $line, array $replace): string
protected function makeReplacements($line, array $replace)
{
if (empty($replace)) {
return $line;

View File

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Utils\Contracts;
use Hyperf\Validation\Contracts\Support\MessageProvider;
interface MessageBag
{
/**
* Get the keys present in the message bag.
*/
public function keys(): array;
/**
* Add a message to the bag.
*/
public function add(string $key, string $message): MessageBag;
/**
* Merge a new array of messages into the bag.
*
* @param array|MessageProvider $messages
* @return $this
*/
public function merge($messages);
/**
* Determine if messages exist for a given key.
*
* @param array|string $key
*/
public function has($key): bool;
/**
* Get the first message from the bag for a given key.
*
* @param null|string $key
* @param null|string $format
*/
public function first(?string $key = null, ?string $format = null): string;
/**
* Get all of the messages from the bag for a given key.
*/
public function get(string $key, ?string $format = null): array;
/**
* Get all of the messages for every key in the bag.
*/
public function all(?string $format = null): array;
/**
* Get the raw messages in the container.
*/
public function getMessages(): array;
/**
* Get the default message format.
*/
public function getFormat(): string;
/**
* Set the default message format.
*
* @return $this
*/
public function setFormat(string $format = ':message');
/**
* Determine if the message bag has any messages.
*/
public function isEmpty(): bool;
/**
* Determine if the message bag has any messages.
*/
public function isNotEmpty(): bool;
/**
* Get the number of messages in the container.
*/
public function count(): int;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Utils\Contracts;
interface MessageProvider
{
/**
* Get the messages for the instance.
*/
public function getMessageBag(): MessageBag;
}

View File

@ -0,0 +1,352 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Utils;
use Countable;
use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Contracts\Jsonable;
use Hyperf\Utils\Contracts\MessageBag as MessageBagContract;
use Hyperf\Utils\Contracts\MessageProvider;
use JsonSerializable;
class MessageBag implements Arrayable, Countable, Jsonable, JsonSerializable, MessageBagContract, MessageProvider
{
/**
* All of the registered messages.
*
* @var array
*/
protected $messages = [];
/**
* Default format for message output.
*
* @var string
*/
protected $format = ':message';
/**
* Create a new message bag instance.
*/
public function __construct(array $messages = [])
{
foreach ($messages as $key => $value) {
$value = $value instanceof Arrayable ? $value->toArray() : (array) $value;
$this->messages[$key] = array_unique($value);
}
}
/**
* Convert the message bag to its string representation.
*/
public function __toString(): string
{
return $this->toJson();
}
/**
* Get the keys present in the message bag.
*/
public function keys(): array
{
return array_keys($this->messages);
}
/**
* Add a message to the message bag.
*/
public function add(string $key, string $message): MessageBagContract
{
if ($this->isUnique($key, $message)) {
$this->messages[$key][] = $message;
}
return $this;
}
/**
* Merge a new array of messages into the message bag.
*
* @param array|MessageProvider $messages
* @return $this
*/
public function merge($messages)
{
if ($messages instanceof MessageProvider) {
$messages = $messages->getMessageBag()->getMessages();
}
$this->messages = array_merge_recursive($this->messages, $messages);
return $this;
}
/**
* Determine if messages exist for all of the given keys.
*
* @param array|string $key
*/
public function has($key): bool
{
if ($this->isEmpty()) {
return false;
}
if (is_null($key)) {
return $this->any();
}
$keys = is_array($key) ? $key : func_get_args();
foreach ($keys as $key) {
if ($this->first($key) === '') {
return false;
}
}
return true;
}
/**
* Determine if messages exist for any of the given keys.
*
* @param array|string $keys
*/
public function hasAny($keys = []): bool
{
if ($this->isEmpty()) {
return false;
}
$keys = is_array($keys) ? $keys : func_get_args();
foreach ($keys as $key) {
if ($this->has($key)) {
return true;
}
}
return false;
}
/**
* Get the first message from the message bag for a given key.
*
* @param string $key
* @param string $format
*/
public function first($key = null, $format = null): string
{
$messages = is_null($key) ? $this->all($format) : $this->get($key, $format);
$firstMessage = Arr::first($messages, null, '');
return is_array($firstMessage) ? Arr::first($firstMessage) : $firstMessage;
}
/**
* Get all of the messages from the message bag for a given key.
*/
public function get(string $key, ?string $format = null): array
{
// If the message exists in the message bag, we will transform it and return
// the message. Otherwise, we will check if the key is implicit & collect
// all the messages that match the given key and output it as an array.
if (array_key_exists($key, $this->messages)) {
return $this->transform(
$this->messages[$key],
$this->checkFormat($format),
$key
);
}
if (Str::contains($key, '*')) {
return $this->getMessagesForWildcardKey($key, $format);
}
return [];
}
/**
* Get all of the messages for every key in the message bag.
*/
public function all(?string $format = null): array
{
$format = $this->checkFormat($format);
$all = [];
foreach ($this->messages as $key => $messages) {
$all = array_merge($all, $this->transform($messages, $format, $key));
}
return $all;
}
/**
* Get all of the unique messages for every key in the message bag.
*/
public function unique(?string $format = null): array
{
return array_unique($this->all($format));
}
/**
* Get the raw messages in the message bag.
*/
public function messages(): array
{
return $this->messages;
}
/**
* Get the raw messages in the message bag.
*/
public function getMessages(): array
{
return $this->messages();
}
/**
* Get the messages for the instance.
*/
public function getMessageBag(): MessageBagContract
{
return $this;
}
/**
* Get the default message format.
*/
public function getFormat(): string
{
return $this->format;
}
/**
* Set the default message format.
*/
public function setFormat(string $format = ':message'): self
{
$this->format = $format;
return $this;
}
/**
* Determine if the message bag has any messages.
*/
public function isEmpty(): bool
{
return ! $this->any();
}
/**
* Determine if the message bag has any messages.
*/
public function isNotEmpty(): bool
{
return $this->any();
}
/**
* Determine if the message bag has any messages.
*/
public function any(): bool
{
return $this->count() > 0;
}
/**
* Get the number of messages in the message bag.
*/
public function count(): int
{
return count($this->messages, COUNT_RECURSIVE) - count($this->messages);
}
/**
* Get the instance as an array.
*/
public function toArray(): array
{
return $this->getMessages();
}
/**
* Convert the object into something JSON serializable.
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* Convert the object to its JSON representation.
*/
public function toJson(int $options = 0): string
{
return json_encode($this->jsonSerialize(), $options);
}
/**
* Determine if a key and message combination already exists.
*/
protected function isUnique(string $key, string $message): bool
{
$messages = (array) $this->messages;
return ! isset($messages[$key]) || ! in_array($message, $messages[$key]);
}
/**
* Get the messages for a wildcard key.
*/
protected function getMessagesForWildcardKey(string $key, ?string $format): array
{
return collect($this->messages)
->filter(function ($messages, $messageKey) use ($key) {
return Str::is($key, $messageKey);
})
->map(function ($messages, $messageKey) use ($format) {
return $this->transform(
$messages,
$this->checkFormat($format),
$messageKey
);
})->all();
}
/**
* Format an array of messages.
*/
protected function transform(array $messages, string $format, string $messageKey): array
{
return collect($messages)
->map(function ($message) use ($format, $messageKey) {
// We will simply spin through the given messages and transform each one
// replacing the :message place holder with the real message allowing
// the messages to be easily formatted to each developer's desires.
return str_replace([':message', ':key'], [$message, $messageKey], $format);
})->all();
}
/**
* Get the appropriate format based on the given format.
*/
protected function checkFormat(?string $format): string
{
return $format ?: $this->format;
}
}

View File

@ -77,7 +77,6 @@ class ConcurrentTest extends TestCase
while (! $con->isEmpty()) {
Coroutine::sleep(0.1);
}
$this->assertSame(15, $count);
}

1
src/validation/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
/tests export-ignore

View File

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright (c) Hyperf
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

108
src/validation/README.md Normal file
View File

@ -0,0 +1,108 @@
# Hyperf Validation
## About
[hyperf/validation](https://github.com/hyperf-cloud/validation) 组件衍生于 `Laravel Validation` 组件的,我们对它进行了一些改造,大部分功能保持了相同。在这里感谢一下 Laravel 开发组,实现了如此强大好用的 Validation 组件。
## Installation
```
composer require hyperf/validation
```
## Config
### Publish config file
```
php bin/hyperf.php vendor:publish hyperf/validation
```
### Configuration path
```
your/config/path/autoload/translation.php
```
### Configuration
```php
<?php
return [
'locale' => 'en',
'fallback_locale' => '',
'lang' => BASE_PATH . '/resources/lang',
];
```
### Exception handler
```php
<?php
return [
'handler' => [
'http' => [
\Hyperf\Validation\ValidationExceptionHandler::class,
],
],
];
```
### Validation middleware
```php
<?php
return [
'http' => [
\Hyperf\Validation\Middleware\ValidationMiddleware::class,
],
];
```
## Usage
### Generate form request
Command:
```
php bin/hyperf.php gen:request FooRequest
```
Usage:
```php
class IndexController
{
public function foo(FooRequest $request)
{
$request->input('foo');
}
public function bar(RequestInterface $request){
$factory = $this->container->get(\Hyperf\Validation\Contracts\Validation\Factory::class);
$factory->extend('foo', function ($attribute, $value, $parameters, $validator) {
return $value == 'foo';
});
$factory->replacer('foo', function ($message, $attribute, $rule, $parameters) {
return str_replace(':foo', $attribute, $message);
});
$validator = $factory->make(
$request->all(),
[
'name' => 'required|foo',
],
[
'name.foo' => ':foo is not foo',
]
);
if (!$validator->passes()) {
$validator->errors();
}
}
}
```

61
src/validation/composer.json Executable file
View File

@ -0,0 +1,61 @@
{
"name": "hyperf/validation",
"type": "library",
"license": "MIT",
"keywords": [
"validation",
"hyperf"
],
"description": "hyperf validation",
"autoload": {
"psr-4": {
"Hyperf\\Validation\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\Validation\\": "tests"
}
},
"require": {
"php": ">=7.2",
"ext-swoole": ">=4.3",
"egulias/email-validator": "^2.1",
"hyperf/command": "~1.1.0",
"hyperf/contract": "~1.1.0",
"hyperf/database": "~1.1.0",
"hyperf/devtool": "~1.1.0",
"hyperf/di": "~1.1.0",
"hyperf/framework": "~1.1.0",
"hyperf/http-server": "~1.1.0",
"hyperf/utils": "~1.1.0",
"hyperf/translation": "~1.1.0",
"nesbot/carbon": "^2.21",
"psr/container": "^1.0",
"psr/http-message": "^1.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
"hyperf/db-connection": "^1.0",
"hyperf/testing": "1.0.*",
"mockery/mockery": "^1.2",
"phpstan/phpstan": "^0.10.5",
"swoft/swoole-ide-helper": "^4.4.0"
},
"config": {
"sort-packages": true
},
"scripts": {
"test": "co-phpunit -c phpunit.xml --colors=always",
"analyze": "phpstan analyse --memory-limit 300M -l 0 ./src",
"cs-fix": "php-cs-fixer fix $1"
},
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
},
"hyperf": {
"config": "Hyperf\\Validation\\ConfigProvider"
}
}
}

View File

@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'The :attribute must be accepted.',
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'after_or_equal' => 'The :attribute must be a date after or equal to :date.',
'alpha' => 'The :attribute may only contain letters.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'before' => 'The :attribute must be a date before :date.',
'before_or_equal' => 'The :attribute must be a date before or equal to :date.',
'between' => [
'numeric' => 'The :attribute must be between :min and :max.',
'file' => 'The :attribute must be between :min and :max kilobytes.',
'string' => 'The :attribute must be between :min and :max characters.',
'array' => 'The :attribute must have between :min and :max items.',
],
'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.',
'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'dimensions' => 'The :attribute has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.',
'email' => 'The :attribute must be a valid email address.',
'exists' => 'The selected :attribute is invalid.',
'file' => 'The :attribute must be a file.',
'filled' => 'The :attribute field is required.',
'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.',
'in_array' => 'The :attribute field does not exist in :other.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
'json' => 'The :attribute must be a valid JSON string.',
'max' => [
'numeric' => 'The :attribute may not be greater than :max.',
'file' => 'The :attribute may not be greater than :max kilobytes.',
'string' => 'The :attribute may not be greater than :max characters.',
'array' => 'The :attribute may not have more than :max items.',
],
'mimes' => 'The :attribute must be a file of type: :values.',
'mimetypes' => 'The :attribute must be a file of type: :values.',
'min' => [
'numeric' => 'The :attribute must be at least :min.',
'file' => 'The :attribute must be at least :min kilobytes.',
'string' => 'The :attribute must be at least :min characters.',
'array' => 'The :attribute must have at least :min items.',
],
'not_in' => 'The selected :attribute is invalid.',
'numeric' => 'The :attribute must be a number.',
'present' => 'The :attribute field must be present.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values is present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.',
'size' => [
'numeric' => 'The :attribute must be :size.',
'file' => 'The :attribute must be :size kilobytes.',
'string' => 'The :attribute must be :size characters.',
'array' => 'The :attribute must contain :size items.',
],
'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid zone.',
'unique' => 'The :attribute has already been taken.',
'uploaded' => 'The :attribute failed to upload.',
'url' => 'The :attribute format is invalid.',
'max_if' => [
'numeric' => 'The :attribute may not be greater than :max when :other is :value.',
'file' => 'The :attribute may not be greater than :max kilobytes when :other is :value.',
'string' => 'The :attribute may not be greater than :max characters when :other is :value.',
'array' => 'The :attribute may not have more than :max items when :other is :value.',
],
'min_if' => [
'numeric' => 'The :attribute must be at least :min when :other is :value.',
'file' => 'The :attribute must be at least :min kilobytes when :other is :value.',
'string' => 'The :attribute must be at least :min characters when :other is :value.',
'array' => 'The :attribute must have at least :min items when :other is :value.',
],
'between_if' => [
'numeric' => 'The :attribute must be between :min and :max when :other is :value.',
'file' => 'The :attribute must be between :min and :max kilobytes when :other is :value.',
'string' => 'The :attribute must be between :min and :max characters when :other is :value.',
'array' => 'The :attribute must have between :min and :max items when :other is :value.',
],
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
'phone_number' => 'The :attribute must be a valid phone number',
'telephone_number' => 'The :attribute must be a valid telephone number',
'chinese_word' => 'The :attribute must contain valid characters(chinese/english character, number, underscore)',
'sequential_array' => 'The :attribute must be sequential array',
];

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => ':attribute 必须接受',
'active_url' => ':attribute 必须是一个合法的 URL',
'after' => ':attribute 必须是 :date 之后的一个日期',
'after_or_equal' => ':attribute 必须是 :date 之后或相同的一个日期',
'alpha' => ':attribute 只能包含字母',
'alpha_dash' => ':attribute 只能包含字母、数字、中划线或下划线',
'alpha_num' => ':attribute 只能包含字母和数字',
'array' => ':attribute 必须是一个数组',
'before' => ':attribute 必须是 :date 之前的一个日期',
'before_or_equal' => ':attribute 必须是 :date 之前或相同的一个日期',
'between' => [
'numeric' => ':attribute 必须在 :min 到 :max 之间',
'file' => ':attribute 必须在 :min 到 :max KB 之间',
'string' => ':attribute 必须在 :min 到 :max 个字符之间',
'array' => ':attribute 必须在 :min 到 :max 项之间',
],
'boolean' => ':attribute 字符必须是 true 或 false, 1 或 0',
'confirmed' => ':attribute 二次确认不匹配',
'date' => ':attribute 必须是一个合法的日期',
'date_format' => ':attribute 与给定的格式 :format 不符合',
'different' => ':attribute 必须不同于 :other',
'digits' => ':attribute 必须是 :digits 位',
'digits_between' => ':attribute 必须在 :min 和 :max 位之间',
'dimensions' => ':attribute 具有无效的图片尺寸',
'distinct' => ':attribute 字段具有重复值',
'email' => ':attribute 必须是一个合法的电子邮件地址',
'exists' => '选定的 :attribute 是无效的',
'file' => ':attribute 必须是一个文件',
'filled' => ':attribute 的字段是必填的',
'image' => ':attribute 必须是 jpeg, png, bmp 或者 gif 格式的图片',
'in' => '选定的 :attribute 是无效的',
'in_array' => ':attribute 字段不存在于 :other',
'integer' => ':attribute 必须是个整数',
'ip' => ':attribute 必须是一个合法的 IP 地址',
'json' => ':attribute 必须是一个合法的 JSON 字符串',
'max' => [
'numeric' => ':attribute 的最大长度为 :max 位',
'file' => ':attribute 的最大为 :max',
'string' => ':attribute 的最大长度为 :max 字符',
'array' => ':attribute 的最大个数为 :max 个',
],
'mimes' => ':attribute 的文件类型必须是 :values',
'min' => [
'numeric' => ':attribute 的最小长度为 :min 位',
'file' => ':attribute 大小至少为 :min KB',
'string' => ':attribute 的最小长度为 :min 字符',
'array' => ':attribute 至少有 :min 项',
],
'not_in' => '选定的 :attribute 是无效的',
'numeric' => ':attribute 必须是数字',
'present' => ':attribute 字段必须存在',
'regex' => ':attribute 格式是无效的',
'required' => ':attribute 字段是必须的',
'required_if' => ':attribute 字段是必须的当 :other 是 :value',
'required_unless' => ':attribute 字段是必须的,除非 :other 是在 :values 中',
'required_with' => ':attribute 字段是必须的当 :values 是存在的',
'required_with_all' => ':attribute 字段是必须的当 :values 是存在的',
'required_without' => ':attribute 字段是必须的当 :values 是不存在的',
'required_without_all' => ':attribute 字段是必须的当 没有一个 :values 是存在的',
'same' => ':attribute 和 :other 必须匹配',
'size' => [
'numeric' => ':attribute 必须是 :size 位',
'file' => ':attribute 必须是 :size KB',
'string' => ':attribute 必须是 :size 个字符',
'array' => ':attribute 必须包括 :size 项',
],
'string' => ':attribute 必须是一个字符串',
'timezone' => ':attribute 必须是个有效的时区',
'unique' => ':attribute 已存在',
'uploaded' => ':attribute 上传失败',
'url' => ':attribute 无效的格式',
'max_if' => [
'numeric' => '当 :other 为 :value 时 :attribute 不能大于 :max',
'file' => '当 :other 为 :value 时 :attribute 不能大于 :max kb',
'string' => '当 :other 为 :value 时 :attribute 不能大于 :max 个字符',
'array' => '当 :other 为 :value 时 :attribute 最多只有 :max 个单元',
],
'min_if' => [
'numeric' => '当 :other 为 :value 时 :attribute 必须大于等于 :min',
'file' => '当 :other 为 :value 时 :attribute 大小不能小于 :min kb',
'string' => '当 :other 为 :value 时 :attribute 至少为 :min 个字符',
'array' => '当 :other 为 :value 时 :attribute 至少有 :min 个单元',
],
'between_if' => [
'numeric' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max 之间',
'file' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max kb 之间',
'string' => '当 :other 为 :value 时 :attribute 必须介于 :min - :max 个字符之间',
'array' => '当 :other 为 :value 时 :attribute 必须只有 :min - :max 个单元',
],
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
'phone_number' => ':attribute 必须为一个有效的电话号码',
'telephone_number' => ':attribute 必须为一个有效的手机号码',
'chinese_word' => ':attribute 必须包含以下有效字符 (中文/英文,数字, 下划线)',
'sequential_array' => ':attribute 必须是一个有序数组',
];

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Hyperf\Validation\Contract\Rule;
use Hyperf\Validation\Contract\Rule as RuleContract;
class ClosureValidationRule implements RuleContract
{
/**
* The callback that validates the attribute.
*
* @var \Closure
*/
public $callback;
/**
* Indicates if the validation callback failed.
*
* @var bool
*/
public $failed = false;
/**
* The validation error message.
*
* @var null|string
*/
public $message;
/**
* Create a new Closure based validation rule.
*
* @param \Closure $callback
*/
public function __construct($callback)
{
$this->callback = $callback;
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes(string $attribute, $value): bool
{
$this->failed = false;
$this->callback->__invoke($attribute, $value, function ($message) {
$this->failed = true;
$this->message = $message;
});
return ! $this->failed;
}
/**
* Get the validation error message.
*/
public function message(): string
{
return $this->message;
}
}

View File

@ -0,0 +1,345 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Concerns;
use Closure;
use Hyperf\HttpMessage\Upload\UploadedFile;
use Hyperf\Utils\Arr;
use Hyperf\Utils\Str;
use Hyperf\Validation\Validator;
trait FormatsMessages
{
use ReplacesAttributes;
/**
* Replace all error message place-holders with actual values.
*/
public function makeReplacements(string $message, string $attribute, string $rule, array $parameters): string
{
$message = $this->replaceAttributePlaceholder(
$message,
$this->getDisplayableAttribute($attribute)
);
$message = $this->replaceInputPlaceholder($message, $attribute);
if (isset($this->replacers[Str::snake($rule)])) {
return $this->callReplacer($message, $attribute, Str::snake($rule), $parameters, $this);
}
if (method_exists($this, $replacer = "replace{$rule}")) {
return $this->{$replacer}($message, $attribute, $rule, $parameters);
}
return $message;
}
/**
* Get the displayable name of the attribute.
*/
public function getDisplayableAttribute(string $attribute): string
{
$primaryAttribute = $this->getPrimaryAttribute($attribute);
$expectedAttributes = $attribute != $primaryAttribute
? [$attribute, $primaryAttribute] : [$attribute];
foreach ($expectedAttributes as $name) {
// The developer may dynamically specify the array of custom attributes on this
// validator instance. If the attribute exists in this array it is used over
// the other ways of pulling the attribute name for this given attributes.
if (isset($this->customAttributes[$name])) {
return $this->customAttributes[$name];
}
// We allow for a developer to specify language lines for any attribute in this
// application, which allows flexibility for displaying a unique displayable
// version of the attribute name instead of the name used in an HTTP POST.
if ($line = $this->getAttributeFromTranslations($name)) {
return $line;
}
}
// When no language line has been specified for the attribute and it is also
// an implicit attribute we will display the raw attribute's name and not
// modify it with any of these replacements before we display the name.
if (isset($this->implicitAttributes[$primaryAttribute])) {
return $attribute;
}
return str_replace('_', ' ', Str::snake($attribute));
}
/**
* Get the displayable name of the value.
*
* @param string $attribute
* @param mixed $value
* @return string
*/
public function getDisplayableValue(string $attribute, $value)
{
if (isset($this->customValues[$attribute][$value])) {
return $this->customValues[$attribute][$value];
}
$key = "validation.values.{$attribute}.{$value}";
if (($line = $this->translator->trans($key)) !== $key) {
return $line;
}
return $value;
}
/**
* Get the validation message for an attribute and rule.
*/
protected function getMessage(string $attribute, string $rule): string
{
$inlineMessage = $this->getInlineMessage($attribute, $rule);
// First we will retrieve the custom message for the validation rule if one
// exists. If a custom validation message is being used we'll return the
// custom message, otherwise we'll keep searching for a valid message.
if (! is_null($inlineMessage)) {
return $inlineMessage;
}
$lowerRule = Str::snake($rule);
$customMessage = $this->getCustomMessageFromTranslator(
$customKey = "validation.custom.{$attribute}.{$lowerRule}"
);
// First we check for a custom defined validation message for the attribute
// and rule. This allows the developer to specify specific messages for
// only some attributes and rules that need to get specially formed.
if ($customMessage !== $customKey) {
return $customMessage;
}
// If the rule being validated is a "size" rule, we will need to gather the
// specific error message for the type of attribute being validated such
// as a number, file or string which all have different message types.
if (in_array($rule, $this->sizeRules)) {
return $this->getSizeMessage($attribute, $rule);
}
// Finally, if no developer specified messages have been set, and no other
// special messages apply for this rule, we will just pull the default
// messages out of the translator service for this validation rule.
$key = "validation.{$lowerRule}";
if ($key != ($value = $this->translator->trans($key))) {
return $value;
}
return $this->getFromLocalArray(
$attribute,
$lowerRule,
$this->fallbackMessages
) ?: $key;
}
/**
* Get the proper inline error message for standard and size rules.
*
* @return null|string
*/
protected function getInlineMessage(string $attribute, string $rule)
{
$inlineEntry = $this->getFromLocalArray($attribute, Str::snake($rule));
return is_array($inlineEntry) && in_array($rule, $this->sizeRules)
? $inlineEntry[$this->getAttributeType($attribute)]
: $inlineEntry;
}
/**
* Get the inline message for a rule if it exists.
*
* @param null|array $source
* @return null|string
*/
protected function getFromLocalArray(string $attribute, string $lowerRule, $source = null)
{
$source = $source ?: $this->customMessages;
$keys = ["{$attribute}.{$lowerRule}", $lowerRule];
// First we will check for a custom message for an attribute specific rule
// message for the fields, then we will check for a general custom line
// that is not attribute specific. If we find either we'll return it.
foreach ($keys as $key) {
foreach (array_keys($source) as $sourceKey) {
if (Str::is($sourceKey, $key)) {
return $source[$sourceKey];
}
}
}
}
/**
* Get the custom error message from translator.
*/
protected function getCustomMessageFromTranslator(string $key): string
{
if (($message = $this->translator->trans($key)) !== $key) {
return $message;
}
// If an exact match was not found for the key, we will collapse all of these
// messages and loop through them and try to find a wildcard match for the
// given key. Otherwise, we will simply return the key's value back out.
$shortKey = preg_replace(
'/^validation\.custom\./',
'',
$key
);
return $this->getWildcardCustomMessages(Arr::dot(
(array) $this->translator->trans('validation.custom')
), $shortKey, $key);
}
/**
* Check the given messages for a wildcard key.
*/
protected function getWildcardCustomMessages(array $messages, string $search, string $default): string
{
foreach ($messages as $key => $message) {
if ($search === $key || (Str::contains((string) $key, ['*']) && Str::is($key, $search))) {
return $message;
}
}
return $default;
}
/**
* Get the proper error message for an attribute and size rule.
*/
protected function getSizeMessage(string $attribute, string $rule): string
{
$lowerRule = Str::snake($rule);
// There are three different types of size validations. The attribute may be
// either a number, file, or string so we will check a few things to know
// which type of value it is and return the correct line for that type.
$type = $this->getAttributeType($attribute);
$key = "validation.{$lowerRule}.{$type}";
return $this->translator->trans($key);
}
/**
* Get the data type of the given attribute.
*/
protected function getAttributeType(string $attribute): string
{
// We assume that the attributes present in the file array are files so that
// means that if the attribute does not have a numeric rule and the files
// list doesn't have it we'll just consider it a string by elimination.
if ($this->hasRule($attribute, $this->numericRules)) {
return 'numeric';
}
if ($this->hasRule($attribute, ['Array'])) {
return 'array';
}
if ($this->getValue($attribute) instanceof UploadedFile) {
return 'file';
}
return 'string';
}
/**
* Get the given attribute from the attribute translations.
*/
protected function getAttributeFromTranslations(string $name): string
{
return (string) Arr::get($this->translator->trans('validation.attributes'), $name);
}
/**
* Replace the :attribute placeholder in the given message.
*/
protected function replaceAttributePlaceholder(string $message, string $value): string
{
return str_replace(
[':attribute', ':ATTRIBUTE', ':Attribute'],
[$value, Str::upper($value), Str::ucfirst($value)],
$message
);
}
/**
* Replace the :input placeholder in the given message.
*/
protected function replaceInputPlaceholder(string $message, string $attribute): string
{
$actualValue = $this->getValue($attribute);
if (is_scalar($actualValue) || is_null($actualValue)) {
$message = str_replace(':input', $actualValue, $message);
}
return $message;
}
/**
* Transform an array of attributes to their displayable form.
*/
protected function getAttributeList(array $values): array
{
$attributes = [];
// For each attribute in the list we will simply get its displayable form as
// this is convenient when replacing lists of parameters like some of the
// replacement functions do when formatting out the validation message.
foreach ($values as $key => $value) {
$attributes[$key] = $this->getDisplayableAttribute($value);
}
return $attributes;
}
/**
* Call a custom validator message replacer.
*/
protected function callReplacer(string $message, string $attribute, string $rule, array $parameters, Validator $validator): ?string
{
$callback = $this->replacers[$rule];
if ($callback instanceof Closure) {
return call_user_func_array($callback, func_get_args());
}
if (is_string($callback)) {
return $this->callClassBasedReplacer($callback, $message, $attribute, $rule, $parameters, $validator);
}
}
/**
* Call a class based validator message replacer.
*
* @param \Hyperf\Validation\Validator $validator
*/
protected function callClassBasedReplacer(string $callback, string $message, string $attribute, string $rule, array $parameters, $validator): string
{
[$class, $method] = Str::parseCallback($callback, 'replace');
return call_user_func_array([make($class), $method], array_slice(func_get_args(), 1));
}
}

View File

@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Concerns;
use Hyperf\Utils\Arr;
trait ReplacesAttributes
{
/**
* Replace all place-holders for the between rule.
*/
protected function replaceBetween(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace([':min', ':max'], $parameters, $message);
}
/**
* Replace all place-holders for the date_format rule.
*/
protected function replaceDateFormat(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace(':format', $parameters[0], $message);
}
/**
* Replace all place-holders for the different rule.
*/
protected function replaceDifferent(string $message, string $attribute, string $rule, array $parameters): string
{
return $this->replaceSame($message, $attribute, $rule, $parameters);
}
/**
* Replace all place-holders for the digits rule.
*/
protected function replaceDigits(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace(':digits', $parameters[0], $message);
}
/**
* Replace all place-holders for the digits (between) rule.
*/
protected function replaceDigitsBetween(string $message, string $attribute, string $rule, array $parameters): string
{
return $this->replaceBetween($message, $attribute, $rule, $parameters);
}
/**
* Replace all place-holders for the min rule.
*/
protected function replaceMin(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace(':min', $parameters[0], $message);
}
/**
* Replace all place-holders for the max rule.
*/
protected function replaceMax(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace(':max', $parameters[0], $message);
}
/**
* Replace all place-holders for the in rule.
*/
protected function replaceIn(string $message, string $attribute, string $rule, array $parameters): string
{
foreach ($parameters as &$parameter) {
$parameter = $this->getDisplayableValue($attribute, $parameter);
}
return str_replace(':values', implode(', ', $parameters), $message);
}
/**
* Replace all place-holders for the not_in rule.
*/
protected function replaceNotIn(string $message, string $attribute, string $rule, array $parameters): string
{
return $this->replaceIn($message, $attribute, $rule, $parameters);
}
/**
* Replace all place-holders for the in_array rule.
*/
protected function replaceInArray(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace(':other', $this->getDisplayableAttribute($parameters[0]), $message);
}
/**
* Replace all place-holders for the mimetypes rule.
*/
protected function replaceMimetypes(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace(':values', implode(', ', $parameters), $message);
}
/**
* Replace all place-holders for the mimes rule.
*/
protected function replaceMimes(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace(':values', implode(', ', $parameters), $message);
}
/**
* Replace all place-holders for the required_with rule.
*/
protected function replaceRequiredWith(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace(':values', implode(' / ', $this->getAttributeList($parameters)), $message);
}
/**
* Replace all place-holders for the required_with_all rule.
*/
protected function replaceRequiredWithAll(string $message, string $attribute, string $rule, array $parameters): string
{
return $this->replaceRequiredWith($message, $attribute, $rule, $parameters);
}
/**
* Replace all place-holders for the required_without rule.
*/
protected function replaceRequiredWithout(string $message, string $attribute, string $rule, array $parameters): string
{
return $this->replaceRequiredWith($message, $attribute, $rule, $parameters);
}
/**
* Replace all place-holders for the required_without_all rule.
*/
protected function replaceRequiredWithoutAll(string $message, string $attribute, string $rule, array $parameters): string
{
return $this->replaceRequiredWith($message, $attribute, $rule, $parameters);
}
/**
* Replace all place-holders for the size rule.
*/
protected function replaceSize(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace(':size', $parameters[0], $message);
}
/**
* Replace all place-holders for the gt rule.
*/
protected function replaceGt(string $message, string $attribute, string $rule, array $parameters): string
{
if (is_null($value = $this->getValue($parameters[0]))) {
return str_replace(':value', $parameters[0], $message);
}
return str_replace(':value', $this->getSize($attribute, $value), $message);
}
/**
* Replace all place-holders for the lt rule.
*/
protected function replaceLt(string $message, string $attribute, string $rule, array $parameters): string
{
if (is_null($value = $this->getValue($parameters[0]))) {
return str_replace(':value', $parameters[0], $message);
}
return str_replace(':value', $this->getSize($attribute, $value), $message);
}
/**
* Replace all place-holders for the gte rule.
*/
protected function replaceGte(string $message, string $attribute, string $rule, array $parameters): string
{
if (is_null($value = $this->getValue($parameters[0]))) {
return str_replace(':value', $parameters[0], $message);
}
return str_replace(':value', $this->getSize($attribute, $value), $message);
}
/**
* Replace all place-holders for the lte rule.
*/
protected function replaceLte(string $message, string $attribute, string $rule, array $parameters): string
{
if (is_null($value = $this->getValue($parameters[0]))) {
return str_replace(':value', $parameters[0], $message);
}
return str_replace(':value', $this->getSize($attribute, $value), $message);
}
/**
* Replace all place-holders for the required_if rule.
*/
protected function replaceRequiredIf(string $message, string $attribute, string $rule, array $parameters): string
{
$parameters[1] = $this->getDisplayableValue($parameters[0], Arr::get($this->data, $parameters[0]));
$parameters[0] = $this->getDisplayableAttribute($parameters[0]);
return str_replace([':other', ':value'], $parameters, $message);
}
/**
* Replace all place-holders for the required_unless rule.
*/
protected function replaceRequiredUnless(string $message, string $attribute, string $rule, array $parameters): string
{
$other = $this->getDisplayableAttribute($parameters[0]);
$values = [];
foreach (array_slice($parameters, 1) as $value) {
$values[] = $this->getDisplayableValue($parameters[0], $value);
}
return str_replace([':other', ':values'], [$other, implode(', ', $values)], $message);
}
/**
* Replace all place-holders for the same rule.
*/
protected function replaceSame(string $message, string $attribute, string $rule, array $parameters): string
{
return str_replace(':other', $this->getDisplayableAttribute($parameters[0]), $message);
}
/**
* Replace all place-holders for the before rule.
*/
protected function replaceBefore(string $message, string $attribute, string $rule, array $parameters): string
{
if (! strtotime($parameters[0])) {
return str_replace(':date', $this->getDisplayableAttribute($parameters[0]), $message);
}
return str_replace(':date', $this->getDisplayableValue($attribute, $parameters[0]), $message);
}
/**
* Replace all place-holders for the before_or_equal rule.
*/
protected function replaceBeforeOrEqual(string $message, string $attribute, string $rule, array $parameters): string
{
return $this->replaceBefore($message, $attribute, $rule, $parameters);
}
/**
* Replace all place-holders for the after rule.
*/
protected function replaceAfter(string $message, string $attribute, string $rule, array $parameters): string
{
return $this->replaceBefore($message, $attribute, $rule, $parameters);
}
/**
* Replace all place-holders for the after_or_equal rule.
*/
protected function replaceAfterOrEqual(string $message, string $attribute, string $rule, array $parameters): string
{
return $this->replaceBefore($message, $attribute, $rule, $parameters);
}
/**
* Replace all place-holders for the date_equals rule.
*/
protected function replaceDateEquals(string $message, string $attribute, string $rule, array $parameters): string
{
return $this->replaceBefore($message, $attribute, $rule, $parameters);
}
/**
* Replace all place-holders for the dimensions rule.
*/
protected function replaceDimensions(string $message, string $attribute, string $rule, array $parameters): string
{
$parameters = $this->parseNamedParameters($parameters);
if (is_array($parameters)) {
foreach ($parameters as $key => $value) {
$message = str_replace(':' . $key, $value, $message);
}
}
return $message;
}
/**
* Replace all place-holders for the ends_with rule.
*/
protected function replaceEndsWith(string $message, string $attribute, string $rule, array $parameters): string
{
foreach ($parameters as &$parameter) {
$parameter = $this->getDisplayableValue($attribute, $parameter);
}
return str_replace(':values', implode(', ', $parameters), $message);
}
/**
* Replace all place-holders for the starts_with rule.
*/
protected function replaceStartsWith(string $message, string $attribute, string $rule, array $parameters): string
{
foreach ($parameters as &$parameter) {
$parameter = $this->getDisplayableValue($attribute, $parameter);
}
return str_replace(':values', implode(', ', $parameters), $message);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Hyperf\Validation\Contract\PresenceVerifierInterface;
use Hyperf\Validation\Contract\ValidatorFactoryInterface as FactoryInterface;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => [
PresenceVerifierInterface::class => DatabasePresenceVerifierFactory::class,
FactoryInterface::class => ValidatorFactoryFactory::class,
],
'scan' => [
'paths' => [
__DIR__,
],
],
'publish' => [
[
'id' => 'zh_CN',
'description' => 'The message bag for validation.',
'source' => __DIR__ . '/../publish/zh_CN/validation.php',
'destination' => BASE_PATH . '/storage/languages/zh_CN/validation.php',
],
[
'id' => 'en',
'description' => 'The message bag for validation.',
'source' => __DIR__ . '/../publish/en/validation.php',
'destination' => BASE_PATH . '/storage/languages/en/validation.php',
],
],
];
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Contract;
interface ImplicitRule extends Rule
{
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Contract;
interface PresenceVerifierInterface
{
/**
* Count the number of objects in a collection having the given value.
*
* @param null|int $excludeId
* @param null|string $idColumn
*/
public function getCount(string $collection, string $column, string $value, $excludeId = null, $idColumn = null, array $extra = []): int;
/**
* Count the number of objects in a collection with the given values.
*/
public function getMultiCount(string $collection, string $column, array $values, array $extra = []): int;
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Contract;
interface Rule
{
/**
* Determine if the validation rule passes.
*
* @param mixed $value
*/
public function passes(string $attribute, $value): bool;
/**
* Get the validation error message.
*
* @return array|string
*/
public function message();
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Contract;
interface ValidatesWhenResolved
{
/**
* Validate the given class instance.
*/
public function validateResolved();
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Contract;
use Hyperf\Contract\ValidatorInterface;
interface ValidatorFactoryInterface
{
/**
* Create a new Validator instance.
*/
public function make(array $data, array $rules, array $messages = [], array $customAttributes = []): ValidatorInterface;
/**
* Register a custom validator extension.
*
* @param \Closure|string $extension
*/
public function extend(string $rule, $extension, ?string $message = null);
/**
* Register a custom implicit validator extension.
*
* @param \Closure|string $extension
*/
public function extendImplicit(string $rule, $extension, ?string $message = null);
/**
* Register a custom implicit validator message replacer.
*
* @param \Closure|string $replacer
*/
public function replacer(string $rule, $replacer);
}

View File

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Closure;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\Utils\Str;
use Hyperf\Validation\Contract\PresenceVerifierInterface;
class DatabasePresenceVerifier implements PresenceVerifierInterface
{
/**
* The database connection instance.
*
* @var \Hyperf\Database\ConnectionResolverInterface
*/
protected $db;
/**
* The database connection to use.
*
* @var string
*/
protected $connection;
/**
* Create a new database presence verifier.
*
* @param \Hyperf\Database\ConnectionResolverInterface $db
*/
public function __construct(ConnectionResolverInterface $db)
{
$this->db = $db;
}
/**
* Count the number of objects in a collection having the given value.
*
* @param string $value
* @param null|int $excludeId
* @param null|string $idColumn
*/
public function getCount(string $collection, string $column, $value, $excludeId = null, $idColumn = null, array $extra = []): int
{
$query = $this->table($collection)->where($column, '=', $value);
if (! is_null($excludeId) && $excludeId !== 'NULL') {
$query->where($idColumn ?: 'id', '<>', $excludeId);
}
return $this->addConditions($query, $extra)->count();
}
/**
* Count the number of objects in a collection with the given values.
*/
public function getMultiCount(string $collection, string $column, array $values, array $extra = []): int
{
$query = $this->table($collection)->whereIn($column, $values);
return $this->addConditions($query, $extra)->distinct()->count($column);
}
/**
* Get a query builder for the given table.
*
* @return \Hyperf\Database\Query\Builder
*/
public function table(string $table)
{
return $this->db->connection($this->connection)->table($table)->useWritePdo();
}
/**
* Set the connection to be used.
*/
public function setConnection(?string $connection)
{
$this->connection = $connection;
}
/**
* Add the given conditions to the query.
*
* @param \Hyperf\Database\Query\Builder $query
* @return \Hyperf\Database\Query\Builder
*/
protected function addConditions($query, array $conditions)
{
foreach ($conditions as $key => $value) {
if ($value instanceof Closure) {
$query->where(function ($query) use ($value) {
$value($query);
});
} else {
$this->addWhere($query, $key, $value);
}
}
return $query;
}
/**
* Add a "where" clause to the given query.
*
* @param \Hyperf\Database\Query\Builder $query
* @param string $extraValue
*/
protected function addWhere($query, string $key, $extraValue)
{
if ($extraValue === 'NULL') {
$query->whereNull($key);
} elseif ($extraValue === 'NOT_NULL') {
$query->whereNotNull($key);
} elseif (Str::startsWith((string) $extraValue, '!')) {
$query->where($key, '!=', mb_substr($extraValue, 1));
} else {
$query->where($key, $extraValue);
}
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Hyperf\Database\ConnectionResolverInterface;
use Psr\Container\ContainerInterface;
class DatabasePresenceVerifierFactory
{
public function __invoke(ContainerInterface $container)
{
$db = $container->get(ConnectionResolverInterface::class);
return make(DatabasePresenceVerifier::class, compact('db'));
}
}

View File

@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Middleware;
use FastRoute\Dispatcher;
use Hyperf\Di\ReflectionManager;
use Hyperf\HttpServer\Router\Dispatched;
use Hyperf\Server\Exception\ServerException;
use Hyperf\Utils\Context;
use Hyperf\Validation\Contract\ValidatesWhenResolved;
use Hyperf\Validation\UnauthorizedException;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ValidationMiddleware implements MiddlewareInterface
{
/**
* @var \Psr\Container\ContainerInterface
*/
private $container;
/**
* @var array
*/
private $implements = [];
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Process an incoming server request and return a response, optionally delegating
* response creation to a handler.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
/** @var Dispatched $dispatched */
$dispatched = $request->getAttribute(Dispatched::class);
if (! $dispatched instanceof Dispatched) {
throw new ServerException(sprintf('The dispatched object is not a %s object.', Dispatched::class));
}
if ($dispatched->status !== Dispatcher::FOUND) {
return $handler->handle($request);
}
$reflectionMethod = ReflectionManager::reflectMethod(...$dispatched->handler->callback);
$parmeters = $reflectionMethod->getParameters();
try {
foreach ($parmeters as $parameter) {
$classname = $parameter->getType()->getName();
$implements = $this->getClassImplements($classname);
if (in_array(ValidatesWhenResolved::class, $implements, true)) {
/** @var \Hyperf\Validation\Contract\ValidatesWhenResolved $parameterInstance */
$parameterInstance = $this->container->get($classname);
$parameterInstance->validateResolved();
}
}
} catch (UnauthorizedException $exception) {
$response = Context::override(ResponseInterface::class, function (ResponseInterface $response) {
return $response->withStatus(403);
});
return $response;
}
return $handler->handle($request);
}
public function getClassImplements(string $class): array
{
if (! isset($this->implements[$class])) {
$this->implements[$class] = class_implements($class);
}
return $this->implements[$class];
}
}

View File

@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Request;
use Hyperf\Contract\ValidatorInterface;
use Hyperf\HttpServer\Request;
use Hyperf\Utils\Context;
use Hyperf\Validation\Contract\ValidatesWhenResolved;
use Hyperf\Validation\Contract\ValidatorFactoryInterface as ValidationFactory;
use Hyperf\Validation\UnauthorizedException;
use Hyperf\Validation\ValidatesWhenResolvedTrait;
use Hyperf\Validation\ValidationException;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
class FormRequest extends Request implements ValidatesWhenResolved
{
use ValidatesWhenResolvedTrait;
/**
* The container instance.
*
* @var ContainerInterface
*/
protected $container;
/**
* The key to be used for the view error bag.
*
* @var string
*/
protected $errorBag = 'default';
/**
* The input keys that should not be flashed on redirect.
*
* @var array
*/
protected $dontFlash = ['password', 'password_confirmation'];
public function __construct(ContainerInterface $container)
{
$this->setContainer($container);
}
/**
* Get the proper failed validation response for the request.
*/
public function response(): ResponseInterface
{
/** @var ResponseInterface $response */
$response = Context::get(ResponseInterface::class);
return $response->withStatus(422);
}
/**
* Get the validated data from the request.
*/
public function validated(): array
{
return $this->getValidatorInstance()->validated();
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [];
}
/**
* Get custom attributes for validator errors.
*/
public function attributes(): array
{
return [];
}
/**
* Set the container implementation.
*
* @return $this
*/
public function setContainer(ContainerInterface $container)
{
$this->container = $container;
return $this;
}
/**
* Get the validator instance for the request.
*/
protected function getValidatorInstance(): ValidatorInterface
{
$factory = $this->container->get(ValidationFactory::class);
if (method_exists($this, 'validator')) {
$validator = call_user_func_array([$this, 'validator'], compact('factory'));
} else {
$validator = $this->createDefaultValidator($factory);
}
if (method_exists($this, 'withValidator')) {
$this->withValidator($validator);
}
return $validator;
}
/**
* Create the default validator instance.
*/
protected function createDefaultValidator(ValidationFactory $factory): ValidatorInterface
{
return $factory->make(
$this->validationData(),
call_user_func_array([$this, 'rules'], []),
$this->messages(),
$this->attributes()
);
}
/**
* Get data to be validated from the request.
*/
protected function validationData(): array
{
return $this->all();
}
/**
* Handle a failed validation attempt.
*
* @throws ValidationException
*/
protected function failedValidation(ValidatorInterface $validator)
{
throw new ValidationException($validator, $this->response());
}
/**
* Format the errors from the given Validator instance.
*/
protected function formatErrors(ValidatorInterface $validator): array
{
return $validator->getMessageBag()->toArray();
}
/**
* Determine if the request passes the authorization check.
*/
protected function passesAuthorization(): bool
{
if (method_exists($this, 'authorize')) {
return call_user_func_array([$this, 'authorize'], []);
}
return false;
}
/**
* Handle a failed authorization attempt.
*/
protected function failedAuthorization()
{
throw new UnauthorizedException('This action is unauthorized.');
}
}

83
src/validation/src/Rule.php Executable file
View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Traits\Macroable;
class Rule
{
use Macroable;
/**
* Get a dimensions constraint builder instance.
*/
public static function dimensions(array $constraints = []): Rules\Dimensions
{
return new Rules\Dimensions($constraints);
}
/**
* Get a exists constraint builder instance.
*/
public static function exists(string $table, string $column = 'NULL'): Rules\Exists
{
return new Rules\Exists($table, $column);
}
/**
* Get an in constraint builder instance.
*
* @param array|Arrayable|string $values
*/
public static function in($values): Rules\In
{
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
return new Rules\In(is_array($values) ? $values : func_get_args());
}
/**
* Get a not_in constraint builder instance.
*
* @param array|Arrayable|string $values
*/
public static function notIn($values): Rules\NotIn
{
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
return new Rules\NotIn(is_array($values) ? $values : func_get_args());
}
/**
* Get a required_if constraint builder instance.
*
* @param bool|callable $callback
*/
public static function requiredIf($callback): Rules\RequiredIf
{
return new Rules\RequiredIf($callback);
}
/**
* Get a unique constraint builder instance.
*/
public static function unique(string $table, string $column = 'NULL'): Rules\Unique
{
return new Rules\Unique($table, $column);
}
}

View File

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
use Closure;
trait DatabaseRule
{
/**
* The table to run the query against.
*
* @var string
*/
protected $table;
/**
* The column to check on.
*
* @var string
*/
protected $column;
/**
* The extra where clauses for the query.
*
* @var array
*/
protected $wheres = [];
/**
* The array of custom query callbacks.
*
* @var array
*/
protected $using = [];
/**
* Create a new rule instance.
*/
public function __construct(string $table, string $column = 'NULL')
{
$this->table = $table;
$this->column = $column;
}
/**
* Set a "where" constraint on the query.
*
* @param \Closure|string $column
* @param null|array|string $value
* @return $this
*/
public function where($column, $value = null)
{
if (is_array($value)) {
return $this->whereIn($column, $value);
}
if ($column instanceof Closure) {
return $this->using($column);
}
$this->wheres[] = compact('column', 'value');
return $this;
}
/**
* Set a "where not" constraint on the query.
*
* @param array|string $value
* @return $this
*/
public function whereNot(string $column, $value)
{
if (is_array($value)) {
return $this->whereNotIn($column, $value);
}
return $this->where($column, '!' . $value);
}
/**
* Set a "where null" constraint on the query.
*
* @return $this
*/
public function whereNull(string $column)
{
return $this->where($column, 'NULL');
}
/**
* Set a "where not null" constraint on the query.
*
* @return $this
*/
public function whereNotNull(string $column)
{
return $this->where($column, 'NOT_NULL');
}
/**
* Set a "where in" constraint on the query.
*
* @return $this
*/
public function whereIn(string $column, array $values)
{
return $this->where(function ($query) use ($column, $values) {
$query->whereIn($column, $values);
});
}
/**
* Set a "where not in" constraint on the query.
*
* @return $this
*/
public function whereNotIn(string $column, array $values)
{
return $this->where(function ($query) use ($column, $values) {
$query->whereNotIn($column, $values);
});
}
/**
* Register a custom query callback.
*
* @return $this
*/
public function using(Closure $callback)
{
$this->using[] = $callback;
return $this;
}
/**
* Get the custom query callbacks for the rule.
*/
public function queryCallbacks(): array
{
return $this->using;
}
/**
* Format the where clauses.
*/
protected function formatWheres(): string
{
return collect($this->wheres)->map(function ($where) {
return $where['column'] . ',' . '"' . str_replace('"', '""', $where['value']) . '"';
})->implode(',');
}
}

View File

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
class Dimensions
{
/**
* The constraints for the dimensions rule.
*
* @var array
*/
protected $constraints = [];
/**
* Create a new dimensions rule instance.
*/
public function __construct(array $constraints = [])
{
$this->constraints = $constraints;
}
/**
* Convert the rule to a validation string.
*/
public function __toString(): string
{
$result = '';
foreach ($this->constraints as $key => $value) {
$result .= "{$key}={$value},";
}
return 'dimensions:' . substr($result, 0, -1);
}
/**
* Set the "width" constraint.
*
* @return $this
*/
public function width(int $value)
{
$this->constraints['width'] = $value;
return $this;
}
/**
* Set the "height" constraint.
*
* @return $this
*/
public function height(int $value)
{
$this->constraints['height'] = $value;
return $this;
}
/**
* Set the "min width" constraint.
*
* @return $this
*/
public function minWidth(int $value)
{
$this->constraints['min_width'] = $value;
return $this;
}
/**
* Set the "min height" constraint.
*
* @return $this
*/
public function minHeight(int $value)
{
$this->constraints['min_height'] = $value;
return $this;
}
/**
* Set the "max width" constraint.
*
* @return $this
*/
public function maxWidth(int $value)
{
$this->constraints['max_width'] = $value;
return $this;
}
/**
* Set the "max height" constraint.
*
* @return $this
*/
public function maxHeight(int $value)
{
$this->constraints['max_height'] = $value;
return $this;
}
/**
* Set the "ratio" constraint.
*
* @return $this
*/
public function ratio(float $value)
{
$this->constraints['ratio'] = $value;
return $this;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
class Exists
{
use DatabaseRule;
/**
* Convert the rule to a validation string.
*/
public function __toString(): string
{
return rtrim(sprintf(
'exists:%s,%s,%s',
$this->table,
$this->column,
$this->formatWheres()
), ',');
}
}

50
src/validation/src/Rules/In.php Executable file
View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
class In
{
/**
* The name of the rule.
*/
protected $rule = 'in';
/**
* The accepted values.
*
* @var array
*/
protected $values;
/**
* Create a new in rule instance.
*/
public function __construct(array $values)
{
$this->values = $values;
}
/**
* Convert the rule to a validation string.
*
* @see \Hyperf\Validation\ValidationRuleParser::parseParameters
*/
public function __toString(): string
{
$values = array_map(function ($value) {
return '"' . str_replace('"', '""', $value) . '"';
}, $this->values);
return $this->rule . ':' . implode(',', $values);
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
class NotIn
{
/**
* The name of the rule.
*/
protected $rule = 'not_in';
/**
* The accepted values.
*
* @var array
*/
protected $values;
/**
* Create a new "not in" rule instance.
*/
public function __construct(array $values)
{
$this->values = $values;
}
/**
* Convert the rule to a validation string.
*/
public function __toString(): string
{
$values = array_map(function ($value) {
return '"' . str_replace('"', '""', $value) . '"';
}, $this->values);
return $this->rule . ':' . implode(',', $values);
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
class RequiredIf
{
/**
* The condition that validates the attribute.
*
* @var bool|callable
*/
public $condition;
/**
* Create a new required validation rule based on a condition.
*
* @param bool|callable $condition
*/
public function __construct($condition)
{
$this->condition = $condition;
}
/**
* Convert the rule to a validation string.
*/
public function __toString(): string
{
if (is_callable($this->condition)) {
return call_user_func($this->condition) ? 'required' : '';
}
return $this->condition ? 'required' : '';
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation\Rules;
use Hyperf\Database\Model\Model;
class Unique
{
use DatabaseRule;
/**
* The ID that should be ignored.
*
* @var mixed
*/
protected $ignore;
/**
* The name of the ID column.
*
* @var string
*/
protected $idColumn = 'id';
/**
* Convert the rule to a validation string.
*/
public function __toString(): string
{
return rtrim(sprintf(
'unique:%s,%s,%s,%s,%s',
$this->table,
$this->column,
$this->ignore ? '"' . addslashes((string) $this->ignore) . '"' : 'NULL',
$this->idColumn,
$this->formatWheres()
), ',');
}
/**
* Ignore the given ID during the unique check.
*
* @param mixed $id
* @return $this
*/
public function ignore($id, ?string $idColumn = null)
{
if ($id instanceof Model) {
return $this->ignoreModel($id, $idColumn);
}
$this->ignore = $id;
$this->idColumn = $idColumn ?? 'id';
return $this;
}
/**
* Ignore the given model during the unique check.
*
* @param \Hyperf\Database\Model\Model $model
* @return $this
*/
public function ignoreModel($model, ?string $idColumn = null)
{
$this->idColumn = $idColumn ?? $model->getKeyName();
$this->ignore = $model->{$this->idColumn};
return $this;
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use RuntimeException;
class UnauthorizedException extends RuntimeException
{
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Hyperf\Contract\ValidatorInterface;
/**
* Provides default implementation of ValidatesWhenResolved contract.
*/
trait ValidatesWhenResolvedTrait
{
/**
* Validate the class instance.
*/
public function validateResolved()
{
$this->prepareForValidation();
if (! $this->passesAuthorization()) {
$this->failedAuthorization();
}
$instance = $this->getValidatorInstance();
if ($instance->fails()) {
$this->failedValidation($instance);
}
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation()
{
// no default action
}
/**
* Get the validator instance for the request.
*/
protected function getValidatorInstance(): ValidatorInterface
{
return $this->validator();
}
/**
* Handle a failed validation attempt.
*
* @throws \Hyperf\Validation\ValidationException
*/
protected function failedValidation(ValidatorInterface $validator)
{
throw new ValidationException($validator);
}
/**
* Determine if the request passes the authorization check.
*/
protected function passesAuthorization(): bool
{
if (method_exists($this, 'authorize')) {
return $this->authorize();
}
return true;
}
/**
* Handle a failed authorization attempt.
*
* @throws \Hyperf\Validation\UnauthorizedException
*/
protected function failedAuthorization()
{
throw new UnauthorizedException();
}
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Hyperf\Utils\Arr;
use Hyperf\Utils\Str;
class ValidationData
{
/**
* Initialize and gather data for given attribute.
*/
public static function initializeAndGatherData(string $attribute, array $masterData): array
{
$data = Arr::dot(static::initializeAttributeOnData($attribute, $masterData));
return array_merge($data, static::extractValuesForWildcards(
$masterData,
$data,
$attribute
));
}
/**
* Extract data based on the given dot-notated path.
*
* Used to extract a sub-section of the data for faster iteration.
*
* @param string $attribute
*/
public static function extractDataFromPath($attribute, array $masterData): array
{
$results = [];
$value = Arr::get($masterData, $attribute, '__missing__');
if ($value !== '__missing__') {
Arr::set($results, $attribute, $value);
}
return $results;
}
/**
* Get the explicit part of the attribute name.
*
* E.g. 'foo.bar.*.baz' -> 'foo.bar'
*
* Allows us to not spin through all of the flattened data for some operations.
*
* @return string
*/
public static function getLeadingExplicitAttributePath(string $attribute)
{
return rtrim(explode('*', $attribute)[0], '.') ?: null;
}
/**
* Gather a copy of the attribute data filled with any missing attributes.
*
* @return array
*/
protected static function initializeAttributeOnData(string $attribute, array $masterData)
{
$explicitPath = static::getLeadingExplicitAttributePath($attribute);
$data = static::extractDataFromPath($explicitPath, $masterData);
if (! Str::contains($attribute, '*') || Str::endsWith($attribute, '*')) {
return $data;
}
return data_set($data, $attribute, null, true);
}
/**
* Get all of the exact attribute values for a given wildcard attribute.
*/
protected static function extractValuesForWildcards(array $masterData, array $data, string $attribute): array
{
$keys = [];
$pattern = str_replace('\*', '[^\.]+', preg_quote($attribute));
foreach ($data as $key => $value) {
if ((bool) preg_match('/^' . $pattern . '/', $key, $matches)) {
$keys[] = $matches[0];
}
}
$keys = array_unique($keys);
$data = [];
foreach ($keys as $key) {
$data[$key] = Arr::get($masterData, $key);
}
return $data;
}
}

View File

@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Hyperf\Contract\ValidatorInterface;
use Hyperf\Server\Exception\ServerException;
use Hyperf\Utils\Arr;
use Psr\Http\Message\ResponseInterface;
class ValidationException extends ServerException
{
/**
* The validator instance.
*
* @var ValidatorInterface
*/
public $validator;
/**
* The recommended response to send to the client.
*
* @var ResponseInterface
*/
public $response;
/**
* The status code to use for the response.
*
* @var int
*/
public $status = 422;
/**
* The name of the error bag.
*
* @var string
*/
public $errorBag;
/**
* The path the client should be redirected to.
*
* @var string
*/
public $redirectTo;
/**
* Create a new exception instance.
*
* @param ValidatorInterface $validator
* @param null|ResponseInterface $response
*/
public function __construct(ValidatorInterface $validator, $response = null, string $errorBag = 'default')
{
parent::__construct('The given data was invalid.');
$this->response = $response;
$this->errorBag = $errorBag;
$this->validator = $validator;
}
/**
* Create a new validation exception from a plain array of messages.
*
* @return static
*/
public static function withMessages(array $messages)
{
return new static(tap(ValidatorFactory::make([], []), function ($validator) use ($messages) {
foreach ($messages as $key => $value) {
foreach (Arr::wrap($value) as $message) {
$validator->errors()->add($key, $message);
}
}
}));
}
/**
* Get all of the validation error messages.
*/
public function errors(): array
{
return $this->validator->errors()->messages();
}
/**
* Set the HTTP status code to be used for the response.
*
* @return $this
*/
public function status(int $status)
{
$this->status = $status;
return $this;
}
/**
* Set the error bag on the exception.
*
* @return $this
*/
public function errorBag(string $errorBag)
{
$this->errorBag = $errorBag;
return $this;
}
/**
* Set the URL to redirect to on a validation error.
*
* @return $this
*/
public function redirectTo(string $url)
{
$this->redirectTo = $url;
return $this;
}
/**
* Get the underlying response instance.
*
* @return ResponseInterface
*/
public function getResponse()
{
return $this->response;
}
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use Throwable;
class ValidationExceptionHandler extends ExceptionHandler
{
public function handle(Throwable $throwable, ResponseInterface $response)
{
$this->stopPropagation();
/** @var \Hyperf\Validation\ValidationException $throwable */
$body = $throwable->validator->errors()->first();
return $response->withStatus($throwable->status)->withBody(new SwooleStream($body));
}
public function isValid(Throwable $throwable): bool
{
return $throwable instanceof ValidationException;
}
}

View File

@ -0,0 +1,261 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Closure;
use Hyperf\Utils\Arr;
use Hyperf\Utils\Str;
use Hyperf\Validation\Contract\Rule as RuleContract;
use Hyperf\Validation\Rules\Exists;
use Hyperf\Validation\Rules\Unique;
class ValidationRuleParser
{
/**
* The data being validated.
*
* @var array
*/
public $data;
/**
* The implicit attributes.
*
* @var array
*/
public $implicitAttributes = [];
/**
* Create a new validation rule parser.
*/
public function __construct(array $data)
{
$this->data = $data;
}
/**
* Parse the human-friendly rules into a full rules array for the validator.
*
* @return \stdClass
*/
public function explode(array $rules)
{
$this->implicitAttributes = [];
$rules = $this->explodeRules($rules);
return (object) [
'rules' => $rules,
'implicitAttributes' => $this->implicitAttributes,
];
}
/**
* Merge additional rules into a given attribute(s).
*
* @param array|string $attribute
* @param array|string $rules
*/
public function mergeRules(array $results, $attribute, $rules = []): array
{
if (is_array($attribute)) {
foreach ($attribute as $innerAttribute => $innerRules) {
$results = $this->mergeRulesForAttribute($results, $innerAttribute, $innerRules);
}
return $results;
}
return $this->mergeRulesForAttribute(
$results,
$attribute,
$rules
);
}
/**
* Extract the rule name and parameters from a rule.
*
* @param array|string $rules
*/
public static function parse($rules): array
{
if ($rules instanceof RuleContract) {
return [$rules, []];
}
if (is_array($rules)) {
$rules = static::parseArrayRule($rules);
} else {
$rules = static::parseStringRule((string) $rules);
}
$rules[0] = static::normalizeRule($rules[0]);
return $rules;
}
/**
* Explode the rules into an array of explicit rules.
*/
protected function explodeRules(array $rules): array
{
foreach ($rules as $key => $rule) {
if (Str::contains((string) $key, '*')) {
$rules = $this->explodeWildcardRules($rules, $key, [$rule]);
unset($rules[$key]);
} else {
$rules[$key] = $this->explodeExplicitRule($rule);
}
}
return $rules;
}
/**
* Explode the explicit rule into an array if necessary.
*
* @param array|object|string $rule
*/
protected function explodeExplicitRule($rule): array
{
if (is_string($rule)) {
return explode('|', $rule);
}
if (is_object($rule)) {
return [$this->prepareRule($rule)];
}
return array_map([$this, 'prepareRule'], $rule);
}
/**
* Prepare the given rule for the Validator.
*
* @param mixed $rule
* @return mixed
*/
protected function prepareRule($rule)
{
if ($rule instanceof Closure) {
$rule = new ClosureValidationRule($rule);
}
if (! is_object($rule) ||
$rule instanceof RuleContract ||
($rule instanceof Exists && $rule->queryCallbacks()) ||
($rule instanceof Unique && $rule->queryCallbacks())) {
return $rule;
}
return (string) $rule;
}
/**
* Define a set of rules that apply to each element in an array attribute.
*
* @param array|string $rules
*/
protected function explodeWildcardRules(array $results, string $attribute, $rules): array
{
$pattern = str_replace('\*', '[^\.]*', preg_quote($attribute));
$data = ValidationData::initializeAndGatherData($attribute, $this->data);
foreach ($data as $key => $value) {
if (Str::startsWith($key, $attribute) || (bool) preg_match('/^' . $pattern . '\z/', $key)) {
foreach ((array) $rules as $rule) {
$this->implicitAttributes[$attribute][] = $key;
$results = $this->mergeRules($results, $key, $rule);
}
}
}
return $results;
}
/**
* Merge additional rules into a given attribute.
*
* @param array|string $rules
*/
protected function mergeRulesForAttribute(array $results, string $attribute, $rules): array
{
$merge = head($this->explodeRules([$rules]));
$results[$attribute] = array_merge(
isset($results[$attribute]) ? $this->explodeExplicitRule($results[$attribute]) : [],
$merge
);
return $results;
}
/**
* Parse an array based rule.
*/
protected static function parseArrayRule(array $rules): array
{
return [Str::studly(trim((string) Arr::get($rules, 0))), array_slice($rules, 1)];
}
/**
* Parse a string based rule.
*/
protected static function parseStringRule(string $rules): array
{
$parameters = [];
// The format for specifying validation rules and parameters follows an
// easy {rule}:{parameters} formatting convention. For instance the
// rule "Max:3" states that the value may only be three letters.
if (strpos($rules, ':') !== false) {
[$rules, $parameter] = explode(':', $rules, 2);
$parameters = static::parseParameters($rules, $parameter);
}
return [Str::studly(trim($rules)), $parameters];
}
/**
* Parse a parameter list.
*/
protected static function parseParameters(string $rule, string $parameter): array
{
$rule = strtolower($rule);
if (in_array($rule, ['regex', 'not_regex', 'notregex'], true)) {
return [$parameter];
}
return str_getcsv($parameter);
}
/**
* Normalizes a rule so that we can accept short types.
*/
protected static function normalizeRule(string $rule): string
{
switch ($rule) {
case 'Int':
return 'Integer';
case 'Bool':
return 'Boolean';
default:
return $rule;
}
}
}

1062
src/validation/src/Validator.php Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,252 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Closure;
use Hyperf\Contract\TranslatorInterface;
use Hyperf\Contract\ValidatorInterface;
use Hyperf\Utils\Str;
use Hyperf\Validation\Contract\PresenceVerifierInterface;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
use Psr\Container\ContainerInterface;
class ValidatorFactory implements ValidatorFactoryInterface
{
/**
* The Translator implementation.
*
* @var TranslatorInterface
*/
protected $translator;
/**
* The Presence Verifier implementation.
*
* @var \Hyperf\Validation\Contract\PresenceVerifierInterface
*/
protected $verifier;
/**
* The IoC container instance.
*
* @var ContainerInterface
*/
protected $container;
/**
* All of the custom validator extensions.
*
* @var array
*/
protected $extensions = [];
/**
* All of the custom implicit validator extensions.
*
* @var array
*/
protected $implicitExtensions = [];
/**
* All of the custom dependent validator extensions.
*
* @var array
*/
protected $dependentExtensions = [];
/**
* All of the custom validator message replacers.
*
* @var array
*/
protected $replacers = [];
/**
* All of the fallback messages for custom rules.
*
* @var array
*/
protected $fallbackMessages = [];
/**
* The Validator resolver instance.
*
* @var \Closure
*/
protected $resolver;
/**
* Create a new Validator factory instance.
*/
public function __construct(TranslatorInterface $translator, ContainerInterface $container = null)
{
$this->container = $container;
$this->translator = $translator;
}
/**
* Create a new Validator instance.
*/
public function make(array $data, array $rules, array $messages = [], array $customAttributes = []): ValidatorInterface
{
$validator = $this->resolve(
$data,
$rules,
$messages,
$customAttributes
);
// The presence verifier is responsible for checking the unique and exists data
// for the validator. It is behind an interface so that multiple versions of
// it may be written besides database. We'll inject it into the validator.
if (! is_null($this->verifier)) {
$validator->setPresenceVerifier($this->verifier);
}
// Next we'll set the IoC container instance of the validator, which is used to
// resolve out class based validator extensions. If it is not set then these
// types of extensions will not be possible on these validation instances.
if (! is_null($this->container)) {
$validator->setContainer($this->container);
}
$this->addExtensions($validator);
return $validator;
}
/**
* Validate the given data against the provided rules.
*
* @throws \Hyperf\Validation\ValidationException
*/
public function validate(array $data, array $rules, array $messages = [], array $customAttributes = []): array
{
return $this->make($data, $rules, $messages, $customAttributes)->validate();
}
/**
* Register a custom validator extension.
*
* @param \Closure|string $extension
*/
public function extend(string $rule, $extension, ?string $message = null)
{
$this->extensions[$rule] = $extension;
if ($message) {
$this->fallbackMessages[Str::snake($rule)] = $message;
}
}
/**
* Register a custom implicit validator extension.
*
* @param \Closure|string $extension
*/
public function extendImplicit(string $rule, $extension, ?string $message = null)
{
$this->implicitExtensions[$rule] = $extension;
if ($message) {
$this->fallbackMessages[Str::snake($rule)] = $message;
}
}
/**
* Register a custom dependent validator extension.
*
* @param \Closure|string $extension
*/
public function extendDependent(string $rule, $extension, ?string $message = null)
{
$this->dependentExtensions[$rule] = $extension;
if ($message) {
$this->fallbackMessages[Str::snake($rule)] = $message;
}
}
/**
* Register a custom validator message replacer.
*
* @param \Closure|string $replacer
*/
public function replacer(string $rule, $replacer)
{
$this->replacers[$rule] = $replacer;
}
/**
* Set the Validator instance resolver.
*/
public function resolver(Closure $resolver)
{
$this->resolver = $resolver;
}
/**
* Get the Translator implementation.
*/
public function getTranslator(): TranslatorInterface
{
return $this->translator;
}
/**
* Get the Presence Verifier implementation.
*/
public function getPresenceVerifier(): PresenceVerifierInterface
{
return $this->verifier;
}
/**
* Set the Presence Verifier implementation.
*/
public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier)
{
$this->verifier = $presenceVerifier;
}
/**
* Resolve a new Validator instance.
*/
protected function resolve(array $data, array $rules, array $messages, array $customAttributes): ValidatorInterface
{
if (is_null($this->resolver)) {
return new Validator($this->translator, $data, $rules, $messages, $customAttributes);
}
return call_user_func($this->resolver, $this->translator, $data, $rules, $messages, $customAttributes);
}
/**
* Add the extensions to a validator instance.
*/
protected function addExtensions(Validator $validator): void
{
$validator->addExtensions($this->extensions);
// Next, we will add the implicit extensions, which are similar to the required
// and accepted rule in that they are run even if the attributes is not in a
// array of data that is given to a validator instances via instantiation.
$validator->addImplicitExtensions($this->implicitExtensions);
$validator->addDependentExtensions($this->dependentExtensions);
$validator->addReplacers($this->replacers);
$validator->setFallbackMessages($this->fallbackMessages);
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Validation;
use Hyperf\Contract\TranslatorInterface;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\Validation\Contract\PresenceVerifierInterface;
use Psr\Container\ContainerInterface;
class ValidatorFactoryFactory
{
public function __invoke(ContainerInterface $container)
{
$translator = $container->get(TranslatorInterface::class);
/** @var \Hyperf\Validation\ValidatorFactory $validatorFactory */
$validatorFactory = make(ValidatorFactory::class, compact('translator', 'container'));
if ($container->has(ConnectionResolverInterface::class) && $container->has(PresenceVerifierInterface::class)) {
$presenceVerifier = $container->get(PresenceVerifierInterface::class);
$validatorFactory->setPresenceVerifier($presenceVerifier);
}
return $validatorFactory;
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use PHPUnit\Framework\TestCase;
/**
* Class AbstractTestCase.
*/
abstract class AbstractTestCase extends TestCase
{
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Validation\Validator;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ValidationAddFailureTest extends TestCase
{
/**
* Making Validator using ValidationValidatorTest.
*
* @return Validator
*/
public function makeValidator()
{
$mainTest = new ValidationValidatorTest();
$trans = $mainTest->getIlluminateArrayTranslator();
return new Validator($trans, ['foo' => ['bar' => ['baz' => '']]], ['foo.bar.baz' => 'sometimes|required']);
}
public function testAddFailureExists()
{
$validator = $this->makeValidator();
$method_name = 'addFailure';
$this->assertTrue(method_exists($validator, $method_name));
$this->assertTrue(is_callable([$validator, $method_name]));
}
public function testAddFailureIsFunctional()
{
$attribute = 'Eugene';
$validator = $this->makeValidator();
$validator->addFailure($attribute, 'not_in');
$messages = json_decode($validator->messages()->toJson());
$this->assertSame($messages->{'foo.bar.baz'}[0], 'validation.required', 'initial data in messages is lost');
$this->assertSame($messages->{$attribute}[0], 'validation.not_in', 'new data in messages was not added');
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Closure;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\Validation\DatabasePresenceVerifier;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use stdClass;
/**
* @internal
* @coversNothing
*/
class ValidationDatabasePresenceVerifierTest extends TestCase
{
protected function tearDown(): void
{
m::close();
}
public function testBasicCount()
{
$verifier = new DatabasePresenceVerifier($db = m::mock(ConnectionResolverInterface::class));
$verifier->setConnection('connection');
$db->shouldReceive('connection')->once()->with('connection')->andReturn($conn = m::mock(stdClass::class));
$conn->shouldReceive('table')->once()->with('table')->andReturn($builder = m::mock(stdClass::class));
$builder->shouldReceive('useWritePdo')->once()->andReturn($builder);
$builder->shouldReceive('where')->with('column', '=', 'value')->andReturn($builder);
$extra = ['foo' => 'NULL', 'bar' => 'NOT_NULL', 'baz' => 'taylor', 'faz' => true, 'not' => '!admin'];
$builder->shouldReceive('whereNull')->with('foo');
$builder->shouldReceive('whereNotNull')->with('bar');
$builder->shouldReceive('where')->with('baz', 'taylor');
$builder->shouldReceive('where')->with('faz', true);
$builder->shouldReceive('where')->with('not', '!=', 'admin');
$builder->shouldReceive('count')->once()->andReturn(100);
$this->assertEquals(100, $verifier->getCount('table', 'column', 'value', null, null, $extra));
}
public function testBasicCountWithClosures()
{
$verifier = new DatabasePresenceVerifier($db = m::mock(ConnectionResolverInterface::class));
$verifier->setConnection('connection');
$db->shouldReceive('connection')->once()->with('connection')->andReturn($conn = m::mock(stdClass::class));
$conn->shouldReceive('table')->once()->with('table')->andReturn($builder = m::mock(stdClass::class));
$builder->shouldReceive('useWritePdo')->once()->andReturn($builder);
$builder->shouldReceive('where')->with('column', '=', 'value')->andReturn($builder);
$closure = function ($query) {
$query->where('closure', 1);
};
$extra = ['foo' => 'NULL', 'bar' => 'NOT_NULL', 'baz' => 'taylor', 'faz' => true, 'not' => '!admin', 0 => $closure];
$builder->shouldReceive('whereNull')->with('foo');
$builder->shouldReceive('whereNotNull')->with('bar');
$builder->shouldReceive('where')->with('baz', 'taylor');
$builder->shouldReceive('where')->with('faz', true);
$builder->shouldReceive('where')->with('not', '!=', 'admin');
$builder->shouldReceive('where')->with(m::type(Closure::class))->andReturnUsing(function () use ($builder, $closure) {
$closure($builder);
});
$builder->shouldReceive('where')->with('closure', 1);
$builder->shouldReceive('count')->once()->andReturn(100);
$this->assertEquals(100, $verifier->getCount('table', 'column', 'value', null, null, $extra));
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Validation\Rule;
use Hyperf\Validation\Rules\Dimensions;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ValidationDimensionsRuleTest extends TestCase
{
public function testItCorrectlyFormatsAStringVersionOfTheRule()
{
$rule = new Dimensions(['min_width' => 100, 'min_height' => 100]);
$this->assertEquals('dimensions:min_width=100,min_height=100', (string) $rule);
$rule = Rule::dimensions()->width(200)->height(100);
$this->assertEquals('dimensions:width=200,height=100', (string) $rule);
$rule = Rule::dimensions()->maxWidth(1000)->maxHeight(500)->ratio(3 / 2);
$this->assertEquals('dimensions:max_width=1000,max_height=500,ratio=1.5', (string) $rule);
$rule = new Dimensions(['ratio' => '2/3']);
$this->assertEquals('dimensions:ratio=2/3', (string) $rule);
$rule = Rule::dimensions()->minWidth(300)->minHeight(400);
$this->assertEquals('dimensions:min_width=300,min_height=400', (string) $rule);
}
}

View File

@ -0,0 +1,223 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Database\Connection;
use Hyperf\Database\ConnectionResolver;
use Hyperf\Database\ConnectionResolverInterface;
use Hyperf\Database\Connectors\ConnectionFactory;
use Hyperf\Database\Connectors\MySqlConnector;
use Hyperf\Database\Model\Register;
use Hyperf\Database\Schema\Builder;
use Hyperf\DbConnection\ConnectionResolver as DBConnectionResolver;
use Hyperf\DbConnection\Model\Model;
use Hyperf\Server\Entry\EventDispatcher;
use Hyperf\Translation\ArrayLoader;
use Hyperf\Translation\Translator;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Validation\DatabasePresenceVerifier;
use Hyperf\Validation\Rules\Exists;
use Hyperf\Validation\Validator;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
/**
* @internal
* @coversNothing
*/
class ValidationExistsRuleTest extends TestCase
{
/**
* Setup the database schema.
*/
protected function setUp(): void
{
$container = m::mock(ContainerInterface::class);
$container->shouldReceive('has')->andReturn(true);
$container->shouldReceive('get')->with('db.connector.mysql')->andReturn(new MySqlConnector());
$connector = new ConnectionFactory($container);
$dbConfig = [
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'hyperf',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
];
$connection = $connector->make($dbConfig);
$resolver = new ConnectionResolver(['default' => $connection]);
$container->shouldReceive('get')->with(DBConnectionResolver::class)->andReturn($resolver);
ApplicationContext::setContainer($container);
Register::setConnectionResolver($resolver);
$container->shouldReceive('get')->with(EventDispatcherInterface::class)->andReturn(new EventDispatcher());
$this->createSchema();
}
/**
* Tear down the database schema.
*/
protected function tearDown(): void
{
$this->schema('default')->drop('users');
m::close();
}
public function testItCorrectlyFormatsAStringVersionOfTheRule()
{
$rule = new Exists('table');
$rule->where('foo', 'bar');
$this->assertEquals('exists:table,NULL,foo,"bar"', (string) $rule);
$rule = new Exists('table', 'column');
$rule->where('foo', 'bar');
$this->assertEquals('exists:table,column,foo,"bar"', (string) $rule);
}
public function testItChoosesValidRecordsUsingWhereInRule()
{
$rule = new Exists('users', 'id');
$rule->whereIn('type', ['foo', 'bar']);
DatabaseTestUser::create(['id' => '1', 'type' => 'foo']);
DatabaseTestUser::create(['id' => '2', 'type' => 'bar']);
DatabaseTestUser::create(['id' => '3', 'type' => 'baz']);
DatabaseTestUser::create(['id' => '4', 'type' => 'other']);
$trans = $this->getIlluminateArrayTranslator();
$v = new Validator($trans, [], ['id' => $rule]);
$v->setPresenceVerifier(new DatabasePresenceVerifier(Register::getConnectionResolver()));
$v->setData(['id' => 1]);
$this->assertTrue($v->passes());
$v->setData(['id' => 2]);
$this->assertTrue($v->passes());
$v->setData(['id' => 3]);
$this->assertFalse($v->passes());
$v->setData(['id' => 4]);
$this->assertFalse($v->passes());
}
public function testItChoosesValidRecordsUsingWhereNotInRule()
{
$rule = new Exists('users', 'id');
$rule->whereNotIn('type', ['foo', 'bar']);
DatabaseTestUser::create(['id' => '1', 'type' => 'foo']);
DatabaseTestUser::create(['id' => '2', 'type' => 'bar']);
DatabaseTestUser::create(['id' => '3', 'type' => 'baz']);
DatabaseTestUser::create(['id' => '4', 'type' => 'other']);
$trans = $this->getIlluminateArrayTranslator();
$v = new Validator($trans, [], ['id' => $rule]);
$v->setPresenceVerifier(new DatabasePresenceVerifier(Register::getConnectionResolver()));
$v->setData(['id' => 1]);
$this->assertFalse($v->passes());
$v->setData(['id' => 2]);
$this->assertFalse($v->passes());
$v->setData(['id' => 3]);
$this->assertTrue($v->passes());
$v->setData(['id' => 4]);
$this->assertTrue($v->passes());
}
public function testItChoosesValidRecordsUsingWhereNotInAndWhereNotInRulesTogether()
{
$rule = new Exists('users', 'id');
$rule->whereIn('type', ['foo', 'bar', 'baz'])->whereNotIn('type', ['foo', 'bar']);
DatabaseTestUser::create(['id' => '1', 'type' => 'foo']);
DatabaseTestUser::create(['id' => '2', 'type' => 'bar']);
DatabaseTestUser::create(['id' => '3', 'type' => 'baz']);
DatabaseTestUser::create(['id' => '4', 'type' => 'other']);
$trans = $this->getIlluminateArrayTranslator();
$v = new Validator($trans, [], ['id' => $rule]);
$v->setPresenceVerifier(new DatabasePresenceVerifier(Register::getConnectionResolver()));
$v->setData(['id' => 1]);
$this->assertFalse($v->passes());
$v->setData(['id' => 2]);
$this->assertFalse($v->passes());
$v->setData(['id' => 3]);
$this->assertTrue($v->passes());
$v->setData(['id' => 4]);
$this->assertFalse($v->passes());
}
public function getIlluminateArrayTranslator()
{
return new Translator(
new ArrayLoader(),
'en'
);
}
protected function createSchema()
{
$this->schema('default')->create('users', function ($table) {
$table->unsignedInteger('id');
$table->string('type');
});
}
/**
* Get a schema builder instance.
*
* @param mixed $connection
* @return Builder
*/
protected function schema($connection = 'default')
{
return $this->connection($connection)->getSchemaBuilder();
}
/**
* Get a database connection instance.
*
* @param mixed $connection
* @return Connection
*/
protected function connection($connection = 'default')
{
return $this->getConnectionResolver()->connection($connection);
}
/**
* Get connection resolver.
*
* @return ConnectionResolverInterface
*/
protected function getConnectionResolver()
{
return Register::getConnectionResolver();
}
}
/**
* Database Models.
*/
class DatabaseTestUser extends Model
{
public $timestamps = false;
protected $table = 'users';
protected $guarded = [];
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Contract\TranslatorInterface;
use Hyperf\Validation\Contract\PresenceVerifierInterface;
use Hyperf\Validation\Validator;
use Hyperf\Validation\ValidatorFactory;
use Mockery as m;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ValidationFactoryTest extends TestCase
{
protected function tearDown(): void
{
m::close();
}
public function testMakeMethodCreatesValidValidator()
{
$translator = m::mock(TranslatorInterface::class);
$factory = new ValidatorFactory($translator);
$validator = $factory->make(['foo' => 'bar'], ['baz' => 'boom']);
$this->assertEquals($translator, $validator->getTranslator());
$this->assertEquals(['foo' => 'bar'], $validator->getData());
$this->assertEquals(['baz' => ['boom']], $validator->getRules());
$presence = m::mock(PresenceVerifierInterface::class);
$noop1 = function () {
};
$noop2 = function () {
};
$noop3 = function () {
};
$factory->extend('foo', $noop1);
$factory->extendImplicit('implicit', $noop2);
$factory->extendDependent('dependent', $noop3);
$factory->replacer('replacer', $noop3);
$factory->setPresenceVerifier($presence);
$validator = $factory->make([], []);
$this->assertEquals(['foo' => $noop1, 'implicit' => $noop2, 'dependent' => $noop3], $validator->extensions);
$this->assertEquals(['replacer' => $noop3], $validator->replacers);
$this->assertEquals($presence, $validator->getPresenceVerifier());
$presence = m::mock(PresenceVerifierInterface::class);
$factory->extend('foo', $noop1, 'foo!');
$factory->extendImplicit('implicit', $noop2, 'implicit!');
$factory->extendImplicit('dependent', $noop3, 'dependent!');
$factory->setPresenceVerifier($presence);
$validator = $factory->make([], []);
$this->assertEquals(['foo' => $noop1, 'implicit' => $noop2, 'dependent' => $noop3], $validator->extensions);
$this->assertEquals(['foo' => 'foo!', 'implicit' => 'implicit!', 'dependent' => 'dependent!'], $validator->fallbackMessages);
$this->assertEquals($presence, $validator->getPresenceVerifier());
}
public function testValidateCallsValidateOnTheValidator()
{
$validator = m::mock(Validator::class);
$translator = m::mock(TranslatorInterface::class);
$factory = m::mock(ValidatorFactory::class . '[make]', [$translator]);
$factory->shouldReceive('make')->once()
->with(['foo' => 'bar', 'baz' => 'boom'], ['foo' => 'required'], [], [])
->andReturn($validator);
$validator->shouldReceive('validate')->once()->andReturn(['foo' => 'bar']);
$validated = $factory->validate(
['foo' => 'bar', 'baz' => 'boom'],
['foo' => 'required']
);
$this->assertEquals($validated, ['foo' => 'bar']);
}
public function testCustomResolverIsCalled()
{
unset($_SERVER['__validator.factory']);
$translator = m::mock(TranslatorInterface::class);
$factory = new ValidatorFactory($translator);
$factory->resolver(function ($translator, $data, $rules) {
$_SERVER['__validator.factory'] = true;
return new Validator($translator, $data, $rules);
});
$validator = $factory->make(['foo' => 'bar'], ['baz' => 'boom']);
$this->assertTrue($_SERVER['__validator.factory']);
$this->assertEquals($translator, $validator->getTranslator());
$this->assertEquals(['foo' => 'bar'], $validator->getData());
$this->assertEquals(['baz' => ['boom']], $validator->getRules());
unset($_SERVER['__validator.factory']);
}
public function testValidateMethodCanBeCalledPublicly()
{
$translator = m::mock(TranslatorInterface::class);
$factory = new ValidatorFactory($translator);
$factory->extend('foo', function ($attribute, $value, $parameters, $validator) {
return $validator->validateArray($attribute, $value);
});
$validator = $factory->make(['bar' => ['baz']], ['bar' => 'foo']);
$this->assertTrue($validator->passes());
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Validation\Rule;
use Hyperf\Validation\Rules\In;
use HyperfTest\Validation\Cases\fixtures\Values;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ValidationInRuleTest extends TestCase
{
public function testItCorrectlyFormatsAStringVersionOfTheRule()
{
$rule = new In(['Hyperf', 'Framework', 'PHP']);
$this->assertEquals('in:"Hyperf","Framework","PHP"', (string) $rule);
$rule = new In(['Life, the Universe and Everything', 'this is a "quote"']);
$this->assertEquals('in:"Life, the Universe and Everything","this is a ""quote"""', (string) $rule);
$rule = new In(["a,b\nc,d"]);
$this->assertEquals("in:\"a,b\nc,d\"", (string) $rule);
$rule = Rule::in([1, 2, 3, 4]);
$this->assertEquals('in:"1","2","3","4"', (string) $rule);
$rule = Rule::in(collect([1, 2, 3, 4]));
$this->assertEquals('in:"1","2","3","4"', (string) $rule);
$rule = Rule::in(new Values());
$this->assertEquals('in:"1","2","3","4"', (string) $rule);
$rule = Rule::in('1', '2', '3', '4');
$this->assertEquals('in:"1","2","3","4"', (string) $rule);
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Validation\Rule;
use Hyperf\Validation\Rules\NotIn;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ValidationNotInRuleTest extends TestCase
{
public function testItCorrectlyFormatsAStringVersionOfTheRule()
{
$rule = new NotIn(['Hyperf', 'Framework', 'PHP']);
$this->assertEquals('not_in:"Hyperf","Framework","PHP"', (string) $rule);
$rule = Rule::notIn([1, 2, 3, 4]);
$this->assertEquals('not_in:"1","2","3","4"', (string) $rule);
$rule = Rule::notIn(collect([1, 2, 3, 4]));
$this->assertEquals('not_in:"1","2","3","4"', (string) $rule);
$rule = Rule::notIn('1', '2', '3', '4');
$this->assertEquals('not_in:"1","2","3","4"', (string) $rule);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Validation\Rules\RequiredIf;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ValidationRequiredIfTest extends TestCase
{
public function testItClousureReturnsFormatsAStringVersionOfTheRule()
{
$rule = new RequiredIf(function () {
return true;
});
$this->assertEquals('required', (string) $rule);
$rule = new RequiredIf(function () {
return false;
});
$this->assertEquals('', (string) $rule);
$rule = new RequiredIf(true);
$this->assertEquals('required', (string) $rule);
$rule = new RequiredIf(false);
$this->assertEquals('', (string) $rule);
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Validation\Rule;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ValidationRuleTest extends TestCase
{
public function testMacroable()
{
// phone macro : validate a phone number
Rule::macro('phone', function () {
return 'regex:/^([0-9\s\-\+\(\)]*)$/';
});
$c = Rule::phone();
$this->assertSame('regex:/^([0-9\s\-\+\(\)]*)$/', $c);
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases;
use Hyperf\Database\Model\Model;
use Hyperf\Validation\Rules\Unique;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ValidationUniqueRuleTest extends TestCase
{
public function testItCorrectlyFormatsAStringVersionOfTheRule()
{
$rule = new Unique('table');
$rule->where('foo', 'bar');
$this->assertEquals('unique:table,NULL,NULL,id,foo,"bar"', (string) $rule);
$rule = new Unique('table', 'column');
$rule->ignore('Taylor, Otwell', 'id_column');
$rule->where('foo', 'bar');
$this->assertEquals('unique:table,column,"Taylor, Otwell",id_column,foo,"bar"', (string) $rule);
$rule = new Unique('table', 'column');
$rule->ignore('Taylor, Otwell"\'..-"', 'id_column');
$rule->where('foo', 'bar');
$this->assertEquals('unique:table,column,"Taylor, Otwell\"\\\'..-\"",id_column,foo,"bar"', (string) $rule);
$this->assertEquals('Taylor, Otwell"\'..-"', stripslashes(str_getcsv('table,column,"Taylor, Otwell\"\\\'..-\"",id_column,foo,"bar"')[2]));
$this->assertEquals('id_column', stripslashes(str_getcsv('table,column,"Taylor, Otwell\"\\\'..-\"",id_column,foo,"bar"')[3]));
$rule = new Unique('table', 'column');
$rule->ignore(null, 'id_column');
$rule->where('foo', 'bar');
$this->assertEquals('unique:table,column,NULL,id_column,foo,"bar"', (string) $rule);
$model = new DatabaseModelStub(['id_column' => 1]);
$rule = new Unique('table', 'column');
$rule->ignore($model);
$rule->where('foo', 'bar');
$this->assertEquals('unique:table,column,"1",id_column,foo,"bar"', (string) $rule);
$rule = new Unique('table', 'column');
$rule->ignore($model, 'id_column');
$rule->where('foo', 'bar');
$this->assertEquals('unique:table,column,"1",id_column,foo,"bar"', (string) $rule);
$rule = new Unique('table');
$rule->where('foo', '"bar"');
$this->assertEquals('unique:table,NULL,NULL,id,foo,"""bar"""', (string) $rule);
}
}
class DatabaseModelStub extends Model
{
protected $primaryKey = 'id_column';
protected $guarded = [];
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Validation\Cases\fixtures;
use Hyperf\Utils\Contracts\Arrayable;
class Values implements Arrayable
{
public function toArray(): array
{
return [1, 2, 3, 4];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

View File

@ -0,0 +1,2 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="300" viewBox="0 0 400 600">
</svg>

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
*/
require_once dirname(dirname(__FILE__)) . '/vendor/autoload.php';

View File

@ -0,0 +1,8 @@
[opcache]
opcache.enable_cli=1
[redis]
extension = "redis.so"
[swoole]
extension = "swoole.so"

View File

@ -0,0 +1,10 @@
#!/usr/bin/env bash
wget https://github.com/swoole/swoole-src/archive/v${SW_VERSION}.tar.gz -O swoole.tar.gz
mkdir -p swoole
tar -xf swoole.tar.gz -C swoole --strip-components=1
rm swoole.tar.gz
cd swoole
phpize
./configure --enable-openssl --enable-mysqlnd
make -j$(nproc)
make install

View File

@ -161,7 +161,7 @@ class Server implements MiddlewareInitializerInterface, OnHandShakeInterface, On
}
}
public function onMessage(SwooleServer $server, Frame $frame): void
public function onMessage(WebSocketServer $server, Frame $frame): void
{
$fdObj = FdCollector::get($frame->fd);
if (! $fdObj) {