This commit is contained in:
Reasno 2019-11-15 05:14:12 +08:00
commit 55f0b110ed
191 changed files with 6715 additions and 480 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
.idea/
.git/
runtime/
codeCoverage/
vendor/
.phpintel/
.env

View File

@ -38,6 +38,6 @@ before_script:
- composer config -g process-timeout 900 && composer update
script:
- composer analyse src/di src/json-rpc src/tracer src/metric src/redis src/nats
- composer analyse src/di src/json-rpc src/tracer src/metric src/redis src/nats src/db
- composer test -- --exclude-group NonCoroutine
- vendor/bin/phpunit --group NonCoroutine

View File

@ -1,8 +1,40 @@
# v1.1.6 - TBD
# v1.1.7 - TBD
# v1.1.6 - 2019-11-14
## Added
- [#827](https://github.com/hyperf/hyperf/pull/827) Added a simple db component.
- [#905](https://github.com/hyperf/hyperf/pull/905) Added twig template engine for view.
- [#911](https://github.com/hyperf/hyperf/pull/911) Added support for crontab task run on one server.
- [#913](https://github.com/hyperf/hyperf/pull/913) Added `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`.
- [#931](https://github.com/hyperf/hyperf/pull/931) Added `strict_mode` for config-apollo.
- [#933](https://github.com/hyperf/hyperf/pull/933) Added plates template engine for view.
- [#937](https://github.com/hyperf/hyperf/pull/937) Added consume events for nats.
- [#941](https://github.com/hyperf/hyperf/pull/941) Added an zookeeper adapter for Hyperf config component.
## Fixed
- [#897](https://github.com/hyperf/hyperf/pull/897) Fixed `pool` for `Hyperf\Nats\Annotation\Consumer` does not works.
- [#897](https://github.com/hyperf/hyperf/pull/897) Fixed connection pool of `Hyperf\Nats\Annotation\Consumer` does not works as expected.
- [#901](https://github.com/hyperf/hyperf/pull/901) Fixed Annotation `Factory` does not works for GraphQL.
- [#903](https://github.com/hyperf/hyperf/pull/903) Fixed execute `init-proxy` command can not stop when `hyperf/rpc-client` component exists.
- [#904](https://github.com/hyperf/hyperf/pull/904) Fixed the hooked I/O request does not works in the listener that listening `Hyperf\Framework\Event\BeforeMainServerStart` event.
- [#906](https://github.com/hyperf/hyperf/pull/906) Fixed `port` property of URI of `Hyperf\HttpMessage\Server\Request`.
- [#907](https://github.com/hyperf/hyperf/pull/907) Fixed the expire time is double of the config for `requestSync` in nats.
- [#909](https://github.com/hyperf/hyperf/pull/909) Fixed a issue that causes staled parallel execution.
- [#925](https://github.com/hyperf/hyperf/pull/925) Fixed the dead cycle caused by socket closed.
- [#932](https://github.com/hyperf/hyperf/pull/932) Fixed `Translator::setLocale` does not works in coroutine evnironment.
- [#940](https://github.com/hyperf/hyperf/pull/940) Fixed WebSocketClient::push TypeError, expects integer, but boolean given.
## Optimized
- [#907](https://github.com/hyperf/hyperf/pull/907) Optimized nats consumer process restart frequently.
- [#928](https://github.com/hyperf/hyperf/pull/928) Optimized `Hyperf\ModelCache\Cacheable::query` to delete the model cache when batch update
- [#936](https://github.com/hyperf/hyperf/pull/936) Optimized `increment` to atomic operation for model-cache.
## Changed
- [#934](https://github.com/hyperf/hyperf/pull/934) WaitGroup inherit \Swoole\Coroutine\WaitGroup.
# v1.1.5 - 2019-11-07

91
bin/md-format Executable file
View File

@ -0,0 +1,91 @@
#!/usr/bin/env php
<?php
use Symfony\Component\Finder\Finder;
foreach ([__DIR__ . '/../../autoload.php', __DIR__ . '/../vendor/autoload.php', __DIR__ . '/vendor/autoload.php'] as $file) {
if (file_exists($file)) {
require $file;
break;
}
}
$files = Finder::create()
->in(__DIR__ . '/../doc/zh')
->name('*.md')
->files();
foreach ($files as $file) {
file_put_contents($file, replace(file_get_contents($file)));
}
echo count($files).' markdown files formatted!'.PHP_EOL;
function replace($text)
{
$cjk = '' .
'\x{2e80}-\x{2eff}' .
'\x{2f00}-\x{2fdf}' .
'\x{3040}-\x{309f}' .
'\x{30a0}-\x{30ff}' .
'\x{3100}-\x{312f}' .
'\x{3200}-\x{32ff}' .
'\x{3400}-\x{4dbf}' .
'\x{4e00}-\x{9fff}' .
'\x{f900}-\x{faff}';
$patterns = [
'cjk_quote' => [
'([' . $cjk . '])(["\'])',
'$1 $2',
],
'quote_cjk' => [
'(["\'])([' . $cjk . '])',
'$1 $2',
],
'fix_quote' => [
'(["\']+)(\s*)(.+?)(\s*)(["\']+)',
'$1$3$5',
],
'cjk_operator_ans' => [
'([' . $cjk . '])([A-Za-zΑ-Ωα-ω0-9])([\+\-\*\/=&\\|<>])',
'$1 $2 $3',
],
'bracket_cjk' => [
'([' . $cjk . '])([`]+\w(.*?)\w[`]+)([' . $cjk . ',。])',
'$1 $2 $4',
],
'ans_operator_cjk' => [
'([\+\-\*\/=&\\|<>])([A-Za-zΑ-Ωα-ω0-9])([' . $cjk . '])',
'$1 $2 $3',
],
'cjk_ans' => [
'([' . $cjk . '])([A-Za-zΑ-Ωα-ω0-9@&%\=\$\^\\-\+\\\><])',
'$1 $2',
],
'ans_cjk' => [
'([A-Za-zΑ-Ωα-ω0-9~!%&=;\,\.\?\$\^\\-\+\\\<>])([' . $cjk . '])',
'$1 $2',
],
];
$code = [];
$i = 0;
$text = preg_replace_callback('/```(\n|.)*?\n```/m', function ($match) use (&$code, &$i) {
$code[++$i] = $match[0];
return "__REPLACEMARK__{$i}__";
}, $text);
foreach ($patterns as $key => $value) {
$text = preg_replace('/' . $value[0] . '/iu', $value[1], $text);
}
$text = preg_replace_callback('/__REPLACEMARK__(\d+)__/s', function ($match) use ($code) {
return $code[$match[1]];
}, $text);
return $text;
}

View File

@ -1,7 +1,7 @@
{
"name": "hyperf/hyperf",
"description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices or middlewares.",
"license": "Apache-2.0",
"license": "MIT",
"keywords": [
"php",
"swoole",
@ -46,18 +46,21 @@
},
"require-dev": {
"doctrine/common": "@stable",
"domnikl/statsd": "^3.0.1",
"friendsofphp/php-cs-fixer": "^2.14",
"influxdb/influxdb-php": "^1.15.0",
"jonahgeorge/jaeger-client-php": "^0.4.4",
"league/plates": "^3.3",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"php-di/php-di": "^6.0",
"phpstan/phpstan": "^0.11.15",
"phpunit/phpunit": "^7.0.0",
"smarty/smarty": "^3.1",
"swoft/swoole-ide-helper": "dev-master",
"symfony/property-access": "^4.3",
"symfony/serializer": "^4.3",
"influxdb/influxdb-php": "^1.15.0",
"domnikl/statsd": "^3.0.1"
"twig/twig": "^2.12"
},
"replace": {
"hyperf/amqp": "self.version",
@ -99,6 +102,7 @@
"hyperf/redis": "self.version",
"hyperf/server": "self.version",
"hyperf/service-governance": "self.version",
"hyperf/session": "self.version",
"hyperf/swagger": "self.version",
"hyperf/swoole-enterprise": "self.version",
"hyperf/task": "self.version",
@ -128,11 +132,13 @@
"Hyperf\\ConfigAliyunAcm\\": "src/config-aliyun-acm/src/",
"Hyperf\\ConfigApollo\\": "src/config-apollo/src/",
"Hyperf\\ConfigEtcd\\": "src/config-etcd/src/",
"Hyperf\\ConfigZookeeper\\": "src/config-zookeeper/src/",
"Hyperf\\Config\\": "src/config/src/",
"Hyperf\\Constants\\": "src/constants/src/",
"Hyperf\\Consul\\": "src/consul/src/",
"Hyperf\\Contract\\": "src/contract/src/",
"Hyperf\\Crontab\\": "src/crontab/src/",
"Hyperf\\DB\\": "src/db/src/",
"Hyperf\\Database\\": "src/database/src/",
"Hyperf\\DbConnection\\": "src/db-connection/src/",
"Hyperf\\Devtool\\": "src/devtool/src/",
@ -169,6 +175,7 @@
"Hyperf\\Rpc\\": "src/rpc/src/",
"Hyperf\\Server\\": "src/server/src/",
"Hyperf\\ServiceGovernance\\": "src/service-governance/src/",
"Hyperf\\Session\\": "src/session/src/",
"Hyperf\\Snowflake\\": "src/snowflake/src/",
"Hyperf\\Socket\\": "src/socket/src/",
"Hyperf\\Swagger\\": "src/swagger/src/",
@ -196,10 +203,12 @@
"HyperfTest\\ConfigAliyunAcm\\": "src/config-aliyun-acm/tests/",
"HyperfTest\\ConfigApollo\\": "src/config-apollo/tests/",
"HyperfTest\\ConfigEtcd\\": "src/config-etcd/tests/",
"HyperfTest\\ConfigZookeeper\\": "src/config-zookeeper/tests/",
"HyperfTest\\Config\\": "src/config/tests/",
"HyperfTest\\Constants\\": "src/constants/tests/",
"HyperfTest\\Consul\\": "src/consul/tests/",
"HyperfTest\\Crontab\\": "src/crontab/tests/",
"HyperfTest\\DB\\": "src/db/tests/",
"HyperfTest\\Database\\": "src/database/tests/",
"HyperfTest\\DbConnection\\": "src/db-connection/tests/",
"HyperfTest\\Di\\": "src/di/tests/",
@ -228,6 +237,7 @@
"HyperfTest\\Rpc\\": "src/rpc/tests/",
"HyperfTest\\Server\\": "src/server/tests/",
"HyperfTest\\ServiceGovernance\\": "src/service-governance/tests/",
"HyperfTest\\Session\\": "src/session/tests/",
"HyperfTest\\Snowflake\\": "src/snowflake/tests/",
"HyperfTest\\Socket\\": "src/socket/tests/",
"HyperfTest\\Task\\": "src/task/tests/",
@ -235,6 +245,7 @@
"HyperfTest\\Translation\\": "src/translation/tests/",
"HyperfTest\\Utils\\": "src/utils/tests/",
"HyperfTest\\Validation\\": "src/validation/tests/",
"HyperfTest\\View\\": "src/view/tests/",
"HyperfTest\\WebSocketClient\\": "src/websocket-client/tests/"
}
},
@ -252,10 +263,12 @@
"Hyperf\\ConfigAliyunAcm\\ConfigProvider",
"Hyperf\\ConfigApollo\\ConfigProvider",
"Hyperf\\ConfigEtcd\\ConfigProvider",
"Hyperf\\ConfigZookeeper\\ConfigProvider",
"Hyperf\\Config\\ConfigProvider",
"Hyperf\\Constants\\ConfigProvider",
"Hyperf\\Consul\\ConfigProvider",
"Hyperf\\Crontab\\ConfigProvider",
"Hyperf\\DB\\ConfigProvider",
"Hyperf\\DbConnection\\ConfigProvider",
"Hyperf\\Devtool\\ConfigProvider",
"Hyperf\\Di\\ConfigProvider",
@ -286,6 +299,7 @@
"Hyperf\\RpcServer\\ConfigProvider",
"Hyperf\\Server\\ConfigProvider",
"Hyperf\\ServiceGovernance\\ConfigProvider",
"Hyperf\\Session\\ConfigProvider",
"Hyperf\\Snowflake\\ConfigProvider",
"Hyperf\\Socket\\ConfigProvider",
"Hyperf\\Swagger\\ConfigProvider",

View File

@ -184,7 +184,7 @@ Config Provider 内数据结构的变化:
- [#630](https://github.com/hyperf/hyperf/pull/630) 变更了 `Hyperf\HttpServer\CoreMiddleware` 类的实例化方式,使用 `make()` 来替代了 `new`
- [#631](https://github.com/hyperf/hyperf/pull/631) 变更了 AMQP Consumer 的实例化方式,使用 `make()` 来替代了 `new`
- [#637](https://github.com/hyperf/hyperf/pull/637) 调整了Hyperf\Contract\OnMessageInterface` 和 `Hyperf\Contract\OnOpenInterface` 的第一个参数的类型约束, 使用 `Swoole\WebSocket\Server` 替代 `Swoole\Server`
- [#637](https://github.com/hyperf/hyperf/pull/637) 调整了 `Hyperf\Contract\OnMessageInterface``Hyperf\Contract\OnOpenInterface` 的第一个参数的类型约束, 使用 `Swoole\WebSocket\Server` 替代 `Swoole\Server`
- [#638](https://github.com/hyperf/hyperf/pull/638) 重命名了 `db:model` 命令为 `gen:model` 命令,同时增加了一个 Visitor 来优化创建的 `$connection` 成员属性,如果要创建的模型类的 `$connection` 属性的值与继承的父类一致,那么创建的模型类将不会包含此属性;
## 移除

View File

@ -155,6 +155,8 @@ $wg->wait();
```php
<?php
use Hyperf\Utils\Exception\ParallelExecutionException;
$parallel = new \Hyperf\Utils\Parallel();
$parallel->add(function () {
\Hyperf\Utils\Coroutine::sleep(1);
@ -164,8 +166,13 @@ $parallel->add(function () {
\Hyperf\Utils\Coroutine::sleep(1);
return \Hyperf\Utils\Coroutine::id();
});
// $result 结果为 [1, 2]
$result = $parallel->wait();
try{
// $results 结果为 [1, 2]
$results = $parallel->wait();
} catch(ParallelExecutionException $e){
//$e->getResults() 获取协程中的返回值。
//$e->getThrowables() 获取协程中出现的异常。
}
```
通过上面的代码我们可以看到仅花了 1 秒就得到了两个不同的协程的 ID在调用 `add(callable $callable)` 的时候 `Parallel` 类会为之自动创建一个协程,并加入到 `WaitGroup` 的调度去。

View File

@ -99,7 +99,11 @@ class FooTask
#### singleton
多实例部署项目时,如果设置为 `true`,则只会触发一次。
解决任务的并发执行问题任务永远只会同时运行1个。但是这个没法保障任务在集群时重复执行的问题。
#### onOneServer
多实例部署项目时,则只有一个实例会被触发。
#### mutexPool
@ -132,19 +136,19 @@ return [
策略类:`Hyperf\Crontab\Strategy\WorkerStrategy`
默认情况下使用此策略,即为 `CrontabDispatcherProcess` 进程解析定时任务,并通过进程间通讯轮传递执行任务到各个 `Worker` 进程中,由各个 `Worker` 进程以协程来实际运行执行任务。
默认情况下使用此策略,即为 `CrontabDispatcherProcess` 进程解析定时任务,并通过进程间通讯轮传递执行任务到各个 `Worker` 进程中,由各个 `Worker` 进程以协程来实际运行执行任务。
##### TaskWorker 进程执行策略
策略类:`Hyperf\Crontab\Strategy\TaskWorkerStrategy`
此策略为 `CrontabDispatcherProcess` 进程解析定时任务,并通过进程间通讯轮传递执行任务到各个 `TaskWorker` 进程中,由各个 `TaskWorker` 进程以协程来实际运行执行任务,使用此策略需注意 `TaskWorker` 进程是否配置了支持协程。
此策略为 `CrontabDispatcherProcess` 进程解析定时任务,并通过进程间通讯轮传递执行任务到各个 `TaskWorker` 进程中,由各个 `TaskWorker` 进程以协程来实际运行执行任务,使用此策略需注意 `TaskWorker` 进程是否配置了支持协程。
##### 多进程执行策略
策略类:`Hyperf\Crontab\Strategy\ProcessStrategy`
此策略为 `CrontabDispatcherProcess` 进程解析定时任务,并通过进程间通讯轮传递执行任务到各个 `Worker` 进程和 `TaskWorker` 进程中,由各个进程以协程来实际运行执行任务,使用此策略需注意 `TaskWorker` 进程是否配置了支持协程。
此策略为 `CrontabDispatcherProcess` 进程解析定时任务,并通过进程间通讯轮传递执行任务到各个 `Worker` 进程和 `TaskWorker` 进程中,由各个进程以协程来实际运行执行任务,使用此策略需注意 `TaskWorker` 进程是否配置了支持协程。
##### 协程执行策略

69
doc/zh/db/db.md Normal file
View File

@ -0,0 +1,69 @@
# 极简的 DB 组件
[hyperf/database](https://github.com/hyperf/database) 功能十分强大,但也不可否认效率上确实些许不足。这里提供一个极简的 `DB` 组件,支持 `PDO``Swoole Mysql`
> 压测对比 database 1800qpsdb 6800qps。
## 组件配置
默认配置 `autoload/db.php` 如下,数据库支持多库配置,默认为 `default`
| 配置项 | 类型 | 默认值 | 备注 |
|:--------------------:|:------:|:------------------:|:--------------------------------:|
| driver | string | 无 | 数据库引擎 支持 `pdo``mysql` |
| host | string | `localhost` | 数据库地址 |
| port | int | 3306 | 数据库地址 |
| database | string | 无 | 数据库默认 DB |
| username | string | 无 | 数据库用户名 |
| password | string | null | 数据库密码 |
| charset | string | utf8 | 数据库编码 |
| collation | string | utf8_unicode_ci | 数据库编码 |
| fetch_mode | int | `PDO::FETCH_ASSOC` | PDO 查询结果集类型 |
| pool.min_connections | int | 1 | 连接池内最少连接数 |
| pool.max_connections | int | 10 | 连接池内最大连接数 |
| pool.connect_timeout | float | 10.0 | 连接等待超时时间 |
| pool.wait_timeout | float | 3.0 | 超时时间 |
| pool.heartbeat | int | -1 | 心跳 |
| pool.max_idle_time | float | 60.0 | 最大闲置时间 |
| options | array | | PDO 配置 |
## 组件支持的方法
具体接口可以查看 `Hyperf\DB\ConnectionInterface`
| 方法名 | 返回值类型 | 备注 |
|:----------------:|:----------:|:--------------------------------------:|
| beginTransaction | void | 开启事务 支持事务嵌套 |
| commit | void | 提交事务 支持事务嵌套 |
| rollBack | void | 回滚事务 支持事务嵌套 |
| insert | int | 插入数据,返回主键 ID非自增主键返回 0 |
| execute | int | 执行 SQL返回受影响的行数 |
| query | array | 查询 SQL |
| fetch | array | object|查询 SQL返回首行数据 |
## 使用
### 使用 DB 实例
```php
<?php
use Hyperf\Utils\ApplicationContext;
use Hyperf\DB\DB;
$db = ApplicationContext::getContainer()->get(DB::class);
$res = $db->query('SELECT * FROM `user` WHERE gender = ?;',[1]);
```
### 使用静态方法
```php
<?php
use Hyperf\DB\DB;
$res = DB::query('SELECT * FROM `user` WHERE gender = ?;',[1]);
```

View File

@ -126,3 +126,13 @@ $models = User::findManyFromCache($ids);
另外一点就是,缓存更新机制,框架内实现了对应的 `Hyperf\ModelCache\Listener\DeleteCacheListener` 监听器,每当数据修改,会主动删除缓存。
如果用户不想由框架来删除缓存,可以主动覆写 `deleteCache` 方法,然后由自己实现对应监听即可。
### 批量修改或删除
`Hyperf\ModelCache\Cacheable` 会自动接管 `Model::query` 方法,只需要用户通过以下方式修改数据,就可以自动清理缓存。
```php
<?php
// 删除用户数据 并自动删除缓存
User::query(true)->where('gender', '>', 1)->delete();
```

View File

@ -300,6 +300,53 @@ return [
> 当然在该场景中可以通过 `@Value` 注解来更便捷的注入配置而无需构建工厂类,此仅为举例
### 懒加载
Hyperf的长生命周期依赖注入在项目启动时完成。这意味着长生命周期的类需要注意
* 构造函数时还不是协程环境,如果注入了可能会触发协程切换的类,就会导致框架启动失败。
* 构造函数中要避免循坏依赖(比较典型的例子为 `Listener``EventDispatcherInterface`),不然也会启动失败。
目前解决方案是:只在实例中注入 `ContainerInterface` ,而其他的组件在非构造函数执行时通过 `container` 获取。PSR-11中指出:
> 「用户不应该将容器作为参数传入对象然后在对象中通过容器获得对象的依赖。这样是把容器当作服务定位器来使用,而服务定位器是一种反模式」
也就是说这样的做法虽然有效,但是从设计模式角度来说并不推荐。
另一个方案是使用PHP中常用的惰性代理模式注入一个代理对象在使用时再实例化目标对象。Hyperf DI组件设计了基于类型提示TypeHint的懒加载注入功能。
添加 `config/autoload/lazy_loader.php` 文件并绑定懒加载关系:
```php
<?php
return [
\App\Service\LazyUserService::class => \App\Service\UserServiceInterface::class
];
```
这样在类型提示 `LazyUserService` 的时候容器就会创建一个懒加载代理注入到构造函数或属性中了。
当该代理对象执行下列操作时,被代理对象才会被真正实例化。
```php
// 方法调用
$proxy->someMethod();
// 读取属性
echo $proxy->someProperty;
// 写入属性
$proxy->someProperty = 'foo';
// 检查属性是否存在
isset($proxy->someProperty);
// 删除属性
unset($proxy->someProperty);
```
## 注意事项
### 容器仅管理长生命周期的对象

View File

@ -1,6 +1,6 @@
# 异常处理器
`Hyperf` 里,业务代码都运行在 `Worker进程` 上,也就意味着一旦任意一个请求的业务存在没有捕获处理的异常的话,都会导致对应的 `Worker进程` 被中断退出,虽然被中断的 `Worker进程` 仍会被重新拉起,但对服务而言也是不能接受的,且捕获异常并输出合理的报错内容给客户端也是更加友好的。
`Hyperf` 里,业务代码都运行在 `Worker 进程` 上,也就意味着一旦任意一个请求的业务存在没有捕获处理的异常的话,都会导致对应的 `Worker 进程` 被中断退出,这对服务而言也是不能接受的,捕获异常并输出合理的报错内容给客户端也是更加友好的。
我们可以通过对各个 `server` 定义不同的 `异常处理器(ExceptionHandler)`,一旦业务流程存在没有捕获的异常,都会被传递到已注册的 `异常处理器(ExceptionHandler)` 去处理。
## 自定义一个异常处理
@ -105,3 +105,43 @@ class IndexController extends Controller
```
在上面这个例子,我们先假设 `FooException` 是存在的一个异常,以及假设已经完成了该处理器的配置,那么当业务抛出一个没有被捕获处理的异常时,就会根据配置的顺序依次传递,整一个处理流程可以理解为一个管道,若前一个异常处理器调用 `$this->stopPropagation()` 则不再往后传递,若最后一个配置的异常处理器仍不对该异常进行捕获处理,那么就会交由 Hyperf 的默认异常处理器处理了。
## Error 监听器
框架提供了 `error_reporting()` 错误级别的监听器 `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`
### 配置
`config/autoload/listeners.php` 中添加监听器
```php
<?php
return [
\Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler::class
];
```
则当出现类似以下的代码时会抛出 `\ErrorException` 异常
```php
<?php
try {
$a = [];
var_dump($a[1]);
} catch (\Throwable $throwable) {
var_dump(get_class($throwable), $throwable->getMessage());
}
// string(14) "ErrorException"
// string(19) "Undefined offset: 1"
```
如果不配置监听器则如下,且不会抛出异常。
```
PHP Notice: Undefined offset: 1 in IndexController.php on line 24
Notice: Undefined offset: 1 in IndexController.php on line 24
NULL
```

View File

@ -33,23 +33,25 @@ php bin/hyperf.php vendor:publish hyperf/metric
#### 选项
* `default`:配置文件内的 `default` 对应的值则为使用的驱动名称。驱动的具体配置在 `metric` 项下定义,使用与 `key` 相同的驱动。
`default`:配置文件内的 `default` 对应的值则为使用的驱动名称。驱动的具体配置在 `metric` 项下定义,使用与 `key` 相同的驱动。
```php
'default' => env('TELEMETRY_DRIVER', 'prometheus'),
```
* `use_standalone_process`: 是否使用 `独立监控进程`。推荐开启。关闭后将在 `Worker 进程` 中处理指标收集与上报。
```php
'use_standalone_process' => env('TELEMETRY_USE_STANDALONE_PROCESS', true),
```
* `enable_default_metric`: 是否统计默认指标。默认指标包括内存占用、系统 CPU 负载以及 Swoole Server 指标和 Swoole Coroutine 指标。
```php
'enable_default_metric' => env('TELEMETRY_ENABLE_DEFAULT_TELEMETRY', true),
```
* `default_metric_interval`: 默认指标推送周期,单位为秒,下同。
`default_metric_interval`: 默认指标推送周期,单位为秒,下同。
```php
'default_metric_interval' => env('DEFAULT_METRIC_INTERVAL', 5),
```
@ -158,7 +160,7 @@ InfluxDB 使用默认的 HTTP 模式,需要配置地址 `host`UDP端口 `po
三种类型分别为:
* 计数器(Counter): 用于描述单向递增的某种指标。如 HTTP 请求计数。
计数器(Counter): 用于描述单向递增的某种指标。如 HTTP 请求计数。
```php
interface CounterInterface
@ -169,7 +171,7 @@ interface CounterInterface
}
```
* 测量器(Gauge):用于描述某种随时间发生增减变化的指标。如连接池内的可用连接数。
测量器(Gauge):用于描述某种随时间发生增减变化的指标。如连接池内的可用连接数。
```php
interface GaugeInterface
@ -308,3 +310,49 @@ class OnMetricFactoryReady implements ListenerInterface
您可以使用 `@Counter(name="stat_name_here")``@Histogram(name="stat_name_here")` 来统计切面的调用次数和运行时间。
关于注解的使用请参阅[注解章节](https://doc.hyperf.io/#/zh/annotation)。
### 自定义 Histogram Bucket
> 本节只适用于 Prometheus 驱动
当您在使用 Prometheus 的 Histogram 时,有时会有自定义 Bucket 的需求。您可以在服务启动前,依赖注入 Registry 并自行注册 Histogram ,设置所需 Bucket 。稍后使用时 `MetricFactory` 就会调用您注册好同名 Histogram 。示例如下:
```php
<?php
namespace App\Listener;
use Hyperf\Config\Annotation\Value;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BeforeMainServerStart;
use Prometheus\CollectorRegistry;
class OnMainServerStart implements ListenerInterface
{
protected $registry;
public function __construct(CollectorRegistry $registry)
{
$this->registry = $registry;
}
public function listen(): array
{
return [
BeforeMainServerStart::class,
];
}
public function process(object $event)
{
$this->registry->registerHistogram(
config("metric.metric.prometheus.namespace"),
'test',
'help_message',
['labelName'],
[0.1, 1, 2, 3.5]
);
}
}
```
之后您使用 `$metricFactory->makeHistogram('test')` 时返回的就是您提前注册好的 Histogram 了。

View File

@ -49,6 +49,7 @@
* [模型事件](zh/db/event.md)
* [模型缓存](zh/db/model-cache.md)
* [数据库迁移](zh/db/migration.md)
* [极简的 DB 组件](zh/db/db.md)
* 微服务

View File

@ -204,17 +204,17 @@ composer test -- --filter=testUserDaoFirst
## 测试替身
Gerard Meszaros 在 Meszaros2007 中介绍了测试替身的概念:
`Gerard Meszaros``Meszaros2007` 中介绍了测试替身的概念:
有时候对被测系统(SUT)进行测试是很困难的,因为它依赖于其他无法在测试环境中使用的组件。这有可能是因为这些组件不可用,它们不会返回测试所需要的结果,或者执行它们会有不良副作用。在其他情况下,我们的测试策略要求对被测系统的内部行为有更多控制或更多可见性。
有时候对 `被测系统(SUT)` 进行测试是很困难的,因为它依赖于其他无法在测试环境中使用的组件。这有可能是因为这些组件不可用,它们不会返回测试所需要的结果,或者执行它们会有不良副作用。在其他情况下,我们的测试策略要求对被测系统的内部行为有更多控制或更多可见性。
如果在编写测试时无法使用(或选择不使用)实际的依赖组件(DOC),可以用测试替身来代替。测试替身不需要和真正的依赖组件有完全一样的的行为方式;他只需要提供和真正的组件同样的 API 即可,这样被测系统就会以为它是真正的组件!
下面展示分别通过构造函数注入依赖、通过inject注释注入依赖的测试替身
下面展示分别通过构造函数注入依赖、通过 `@Inject` 注释注入依赖的测试替身
### 构造函数注入依赖的测试替身
```
```php
<?php
namespace App\Logic;
@ -240,10 +240,9 @@ class DemoLogic
return $result;
}
}
```
```
```php
<?php
namespace App\Api;
@ -257,10 +256,9 @@ class DemoApi
];
}
}
```
```
```php
<?php
namespace HyperfTest\Cases;
@ -303,12 +301,11 @@ class DemoLogicTest extends HttpTestCase
return $container;
}
}
```
### 通过inject注释注入依赖的测试替身
### 通过 Inject 注释注入依赖的测试替身
```
```php
<?php
namespace App\Logic;
@ -333,7 +330,7 @@ class DemoLogic
}
```
```
```php
<?php
namespace App\Api;
@ -347,10 +344,9 @@ class DemoApi
];
}
}
```
```
```php
<?php
namespace HyperfTest\Cases;
@ -398,5 +394,4 @@ class DemoLogicTest extends HttpTestCase
return $container;
}
}
```

View File

@ -36,6 +36,8 @@ return [
## 配置语言环境
### 配置默认语言环境
关于国际化组件的相关配置都是在 `config/autoload/translation.php` 配置文件里设定的,你可以按照实际需要修改它。
```php
@ -52,6 +54,30 @@ return [
];
```
### 配置临时语言环境
```php
<?php
use Hyperf\Di\Annotation\Inject;
use Hyperf\Contract\TranslatorInterface;
class FooController
{
/**
* @Inject
* @var TranslatorInterface
*/
private $translator;
public function index()
{
// 只在当前请求或协程生命周期有效
$this->translator->setLocale('zh_CN');
}
}
```
# 翻译字符串
## 通过 TranslatorInterface 翻译

View File

@ -1,6 +1,6 @@
# 视图
视图组件由 [hyperf/view](https://github.com/hyperf/view) 实现并提供使用,满足您对视图渲染的需求,组件默认支持 `Blade` `Smarty`种模板引擎。
视图组件由 [hyperf/view](https://github.com/hyperf/view) 实现并提供使用,满足您对视图渲染的需求,组件默认支持 `Blade` `Smarty``Twig``Plates`种模板引擎。
## 安装
@ -64,7 +64,7 @@ return [
## 视图渲染引擎
官方目前支持 `Blade` `Smarty`种模板,默认安装 [hyperf/view](https://github.com/hyperf/view) 时不会自动安装任何模板引擎,需要您根据自身需求,自行安装对应的模板引擎,使用前必须安装任一模板引擎。
官方目前支持 `Blade` `Smarty``Twig``Plates`种模板,默认安装 [hyperf/view](https://github.com/hyperf/view) 时不会自动安装任何模板引擎,需要您根据自身需求,自行安装对应的模板引擎,使用前必须安装任一模板引擎。
### 安装 Blade 引擎
@ -78,6 +78,18 @@ composer require duncan3dc/blade
composer require smarty/smarty
```
### 安装 Twig 引擎
```bash
composer require twig/twig
```
### 安装 Plates 引擎
```bash
composer require league/plates
```
### 接入其他模板
假设我们想要接入一个虚拟的模板引擎名为 `TemplateEngine`,那么我们需要在任意地方创建对应的 `TemplateEngine` 类,并实现 `Hyperf\View\Engine\EngineInterface` 接口。

View File

@ -14,9 +14,11 @@
<directory suffix="Test.php">./src/async-queue/tests</directory>
<directory suffix="Test.php">./src/cache/tests</directory>
<directory suffix="Test.php">./src/config/tests</directory>
<directory suffix="Test.php">./src/config-zookeeper/tests</directory>
<directory suffix="Test.php">./src/constants/tests</directory>
<directory suffix="Test.php">./src/consul/tests</directory>
<directory suffix="Test.php">./src/database/tests</directory>
<directory suffix="Test.php">./src/db/tests</directory>
<directory suffix="Test.php">./src/db-connection/tests</directory>
<directory suffix="Test.php">./src/di/tests</directory>
<directory suffix="Test.php">./src/dispatcher/tests</directory>
@ -40,6 +42,7 @@
<directory suffix="Test.php">./src/rpc/tests</directory>
<directory suffix="Test.php">./src/server/tests</directory>
<directory suffix="Test.php">./src/service-governance/tests</directory>
<directory suffix="Test.php">./src/session/tests</directory>
<directory suffix="Test.php">./src/snowflake/tests</directory>
<directory suffix="Test.php">./src/socket/tests</directory>
<directory suffix="Test.php">./src/task/tests</directory>
@ -53,7 +56,35 @@
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src/database</directory>
<directory suffix=".php">./src/amqp/src</directory>
<directory suffix=".php">./src/async-queue/src</directory>
<directory suffix=".php">./src/cache/src</directory>
<directory suffix=".php">./src/config/src</directory>
<directory suffix=".php">./src/constants/src</directory>
<directory suffix=".php">./src/consul/src</directory>
<directory suffix=".php">./src/database/src</directory>
<directory suffix=".php">./src/db-connection/src</directory>
<directory suffix=".php">./src/di/src</directory>
<directory suffix=".php">./src/dispatcher/src</directory>
<directory suffix=".php">./src/elasticsearch/src</directory>
<directory suffix=".php">./src/event/src</directory>
<directory suffix=".php">./src/grpc-client/src</directory>
<directory suffix=".php">./src/guzzle/src</directory>
<directory suffix=".php">./src/http-message/src</directory>
<directory suffix=".php">./src/http-server/src</directory>
<directory suffix=".php">./src/json-rpc/src</directory>
<directory suffix=".php">./src/logger/src</directory>
<directory suffix=".php">./src/model-cache/src</directory>
<directory suffix=".php">./src/paginator/src</directory>
<directory suffix=".php">./src/redis/src</directory>
<directory suffix=".php">./src/rpc/src</directory>
<directory suffix=".php">./src/server/src</directory>
<directory suffix=".php">./src/service-governance/src</directory>
<directory suffix=".php">./src/session/src</directory>
<directory suffix=".php">./src/snowflake/src</directory>
<directory suffix=".php">./src/task/src</directory>
<directory suffix=".php">./src/utils/src</directory>
<directory suffix=".php">./src/websocket-client/src</directory>
</whitelist>
</filter>
</phpunit>

View File

@ -19,4 +19,5 @@ return [
'application',
],
'interval' => 5,
'strict_mode' => false,
];

View File

@ -83,10 +83,39 @@ class OnPipeMessageListener implements ListenerInterface
return;
}
foreach ($data->configurations ?? [] as $key => $value) {
$this->config->set($key, $value);
$this->config->set($key, $this->formatValue($value));
$this->logger->debug(sprintf('Config [%s] is updated', $key));
}
ReleaseKey::set($cacheKey, $data->releaseKey);
}
}
private function formatValue($value)
{
if (! $this->config->get('apollo.strict_mode', false)) {
return $value;
}
switch (strtolower($value)) {
case 'true':
case '(true)':
return true;
case 'false':
case '(false)':
return false;
case 'empty':
case '(empty)':
return '';
case 'null':
case '(null)':
return;
}
if (is_numeric($value)) {
$value = (strpos($value, '.') === false) ? (int) $value : (float) $value;
}
return $value;
}
}

1
src/config-zookeeper/.gitattributes vendored Normal file
View File

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

View File

@ -0,0 +1,21 @@
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.

View File

@ -0,0 +1,61 @@
{
"name": "hyperf/config-zookeeper",
"description": "An zookeeper adapter for Hyperf config component.",
"license": "MIT",
"keywords": [
"php",
"swoole",
"hyperf",
"config",
"configuration",
"zookeeper"
],
"support": {
},
"require": {
"php": ">=7.2",
"psr/container": "^1.0",
"hyperf/contract": "~1.1.0"
},
"require-dev": {
"hyperf/config": "~1.1.0",
"hyperf/event": "~1.1.0",
"hyperf/framework": "~1.1.0",
"hyperf/process": "~1.1.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
"friendsofphp/php-cs-fixer": "^2.9"
},
"suggest": {
"hyperf/process": "Use hyperf process to run ConfigFetcherProcess.",
"ext-swoole-zookeeper": "coroutine client for zookeeper"
},
"autoload": {
"psr-4": {
"Hyperf\\ConfigZookeeper\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\ConfigZookeeper\\": "tests/"
}
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
},
"hyperf": {
"config": "Hyperf\\ConfigZookeeper\\ConfigProvider"
}
},
"bin": [
],
"scripts": {
"cs-fix": "php-cs-fixer fix $1",
"test": "phpunit --colors=always"
}
}

View File

@ -0,0 +1,18 @@
<?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/hyperf/blob/master/LICENSE
*/
return [
'enable' => false,
'interval' => 5,
'server' => env('ZOOKEEPER_SERVER', '127.0.0.1:2181'),
'path' => env('ZOOKEEPER_CONFIG_PATH', '/conf'),
];

View File

@ -0,0 +1,38 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ConfigZookeeper;
use Hyperf\Contract\ConfigInterface;
use Psr\Container\ContainerInterface;
use Swoole\Zookeeper;
class Client implements ClientInterface
{
/**
* @var ConfigInterface
*/
private $config;
public function __construct(ContainerInterface $container)
{
$this->config = $container->get(ConfigInterface::class);
}
public function pull(): array
{
$zk = new Zookeeper($this->config->get('zookeeper.server'), 2.5);
$path = $this->config->get('zookeeper.path', '/conf');
$config = $zk->get($path);
return json_decode($config, true);
}
}

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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ConfigZookeeper;
interface ClientInterface
{
/**
* Pull the config values from configuration center, and then update the Config values.
*/
public function pull(): array;
}

View File

@ -0,0 +1,40 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ConfigZookeeper;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => [
ClientInterface::class => Client::class,
],
'annotations' => [
'scan' => [
'paths' => [
__DIR__,
],
],
],
'publish' => [
[
'id' => 'config',
'description' => 'The config for zookeeper.',
'source' => __DIR__ . '/../publish/zookeeper.php',
'destination' => BASE_PATH . '/config/autoload/zookeeper.php',
],
],
];
}
}

View File

@ -0,0 +1,66 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ConfigZookeeper\Listener;
use Hyperf\ConfigZookeeper\PipeMessage;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Event\Annotation\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\OnPipeMessage;
/**
* @Listener
*/
class OnPipeMessageListener implements ListenerInterface
{
/**
* @var ConfigInterface
*/
private $config;
/**
* @var StdoutLoggerInterface
*/
private $logger;
public function __construct(ConfigInterface $config, StdoutLoggerInterface $logger)
{
$this->config = $config;
$this->logger = $logger;
}
/**
* @return string[] returns the events that you want to listen
*/
public function listen(): array
{
return [
OnPipeMessage::class,
];
}
/**
* Handle the Event when the event is triggered, all listeners will
* complete before the event is returned to the EventDispatcher.
*/
public function process(object $event)
{
if ($event instanceof OnPipeMessage && $event->data instanceof PipeMessage) {
foreach ($event->data->data ?? [] as $key => $value) {
$this->config->set($key, $value);
$this->logger->debug(sprintf('Config [%s] is updated', $key));
}
}
}
}

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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ConfigZookeeper;
class PipeMessage
{
/**
* @var array
*/
public $data;
public function __construct($data)
{
$this->data = $data;
}
}

View File

@ -0,0 +1,94 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ConfigZookeeper\Process;
use Hyperf\ConfigZookeeper\ClientInterface;
use Hyperf\ConfigZookeeper\PipeMessage;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\ExceptionHandler\Formatter\FormatterInterface;
use Hyperf\Process\AbstractProcess;
use Hyperf\Process\Annotation\Process;
use Psr\Container\ContainerInterface;
use Swoole\Server;
/**
* @Process(name="zookeeper-config-fetcher")
*/
class ConfigFetcherProcess extends AbstractProcess
{
// ext-swoole-zookeeper need use in coroutine
public $enableCoroutine = true;
/**
* @var Server
*/
private $server;
/**
* @var ClientInterface
*/
private $client;
/**
* @var ConfigInterface
*/
private $config;
/**
* @var string
*/
private $cacheConfig;
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->client = $container->get(ClientInterface::class);
$this->config = $container->get(ConfigInterface::class);
}
public function bind(Server $server): void
{
$this->server = $server;
parent::bind($server);
}
public function isEnable(): bool
{
return $this->config->get('zookeeper.enable', false);
}
public function handle(): void
{
while (true) {
try {
$config = $this->client->pull();
if ($config !== $this->cacheConfig) {
$this->cacheConfig = $config;
$workerCount = $this->server->setting['worker_num'] + $this->server->setting['task_worker_num'] - 1;
for ($workerId = 0; $workerId <= $workerCount; ++$workerId) {
$this->server->sendMessage(new PipeMessage($config), $workerId);
}
}
} catch (\Throwable $exception) {
if ($this->container->has(StdoutLoggerInterface::class) && $this->container->has(FormatterInterface::class)) {
$logger = $this->container->get(StdoutLoggerInterface::class);
$formatter = $this->container->get(FormatterInterface::class);
$logger->error($formatter->format($exception));
}
} finally {
sleep($this->config->get('zookeeper.interval', 5));
}
}
}
}

View File

@ -0,0 +1,86 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ConfigZookeeper;
use Hyperf\Config\Config;
use Hyperf\ConfigZookeeper\ClientInterface;
use Hyperf\ConfigZookeeper\Listener\OnPipeMessageListener;
use Hyperf\ConfigZookeeper\PipeMessage;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Di\Container;
use Hyperf\Framework\Event\OnPipeMessage;
use Hyperf\Guzzle\ClientFactory;
use Hyperf\Utils\ApplicationContext;
use Mockery;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ClientTest extends TestCase
{
public function testPull()
{
$container = $this->getContainer();
$client = $container->get(ClientInterface::class);
$fetchConfig = $client->pull();
$this->assertSame('after-value', $fetchConfig['zookeeper.test-key']);
}
public function testOnPipeMessageListener()
{
$container = $this->getContainer();
$container->shouldReceive('get')->with(StdoutLoggerInterface::class)->andReturn(value(function () {
$logger = Mockery::mock(StdoutLoggerInterface::class);
$logger->shouldReceive('debug')->with(Mockery::any())->andReturnUsing(function ($args) {
$this->assertSame('Config [zookeeper.test-key] is updated', $args);
});
return $logger;
}));
$listener = new OnPipeMessageListener($container->get(ConfigInterface::class), $container->get(StdoutLoggerInterface::class));
$client = $container->get(ClientInterface::class);
$config = $client->pull();
$event = Mockery::mock(OnPipeMessage::class);
$event->data = new PipeMessage($config);
$config = $container->get(ConfigInterface::class);
$this->assertSame('pre-value', $config->get('zookeeper.test-key'));
$listener->process($event);
$this->assertSame('after-value', $config->get('zookeeper.test-key'));
}
public function getContainer()
{
$container = Mockery::mock(Container::class);
// @TODO Add a test env.
$configInstance = new Config([
'zookeeper' => [
'server' => 'localhost:2181',
'path' => '/conf',
],
]);
$client = Mockery::mock(ClientInterface::class);
$client->shouldReceive('pull')->andReturn([
'zookeeper.test-key' => 'after-value',
]);
$configInstance->set('zookeeper.test-key', 'pre-value');
$container->shouldReceive('get')->with(ClientFactory::class)->andReturn(new ClientFactory($container));
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($configInstance);
$container->shouldReceive('get')->with(ClientInterface::class)->andReturn($client);
ApplicationContext::setContainer($container);
return $container;
}
}

View File

@ -0,0 +1,20 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ConfigZookeeper\Stub;
class Server extends \Swoole\Server
{
public function __construct()
{
}
}

View File

@ -0,0 +1,159 @@
<?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\Contract;
interface SessionInterface
{
/**
* Starts the session storage.
*
* @throws \RuntimeException if session fails to start
* @return bool True if session started
*/
public function start(): bool;
/**
* Returns the session ID.
*
* @return string The session ID
*/
public function getId(): string;
/**
* Sets the session ID.
*/
public function setId(string $id);
/**
* Returns the session name.
*/
public function getName(): string;
/**
* Sets the session name.
*/
public function setName(string $name);
/**
* Invalidates the current session.
*
* Clears all session attributes and flashes and regenerates the
* session and deletes the old session from persistence.
*
* @param int $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return bool True if session invalidated, false if error
*/
public function invalidate(?int $lifetime = null): bool;
/**
* Migrates the current session to a new session id while maintaining all
* session attributes.
*
* @param bool $destroy Whether to delete the old session or leave it to garbage collection
* @param int $lifetime Sets the cookie lifetime for the session cookie. A null value
* will leave the system settings unchanged, 0 sets the cookie
* to expire with browser session. Time is in seconds, and is
* not a Unix timestamp.
*
* @return bool True if session migrated, false if error
*/
public function migrate(bool $destroy = false, ?int $lifetime = null): bool;
/**
* Force the session to be saved and closed.
*
* This method is generally not required for real sessions as
* the session will be automatically saved at the end of
* code execution.
*/
public function save(): void;
/**
* Checks if an attribute is defined.
*
* @param string $name The attribute name
*
* @return bool true if the attribute is defined, false otherwise
*/
public function has(string $name): bool;
/**
* Returns an attribute.
*
* @param string $name The attribute name
* @param mixed $default The default value if not found
*/
public function get(string $name, $default = null);
/**
* Sets an attribute.
* @param mixed $value
*/
public function set(string $name, $value): void;
/**
* Put a key / value pair or array of key / value pairs in the session.
*
* @param array|string $key
* @param null|mixed $value
*/
public function put($key, $value = null): void;
/**
* Returns attributes.
*/
public function all(): array;
/**
* Sets attributes.
*/
public function replace(array $attributes): void;
/**
* Removes an attribute, returning its value.
*
* @return mixed The removed value or null when it does not exist
*/
public function remove(string $name);
/**
* Remove one or many items from the session.
*
* @param array|string $keys
*/
public function forget($keys): void;
/**
* Clears all attributes.
*/
public function clear(): void;
/**
* Checks if the session was started.
*/
public function isStarted(): bool;
/**
* Get the previous URL from the session.
*/
public function previousUrl(): ?string;
/**
* Set the "previous" URL in the session.
*/
public function setPreviousUrl(string $url): void;
}

View File

@ -53,6 +53,11 @@ class Crontab extends AbstractAnnotation
*/
public $mutexExpires;
/**
* @var bool
*/
public $onOneServer;
/**
* @var array|string
*/

View File

@ -46,6 +46,11 @@ class Crontab
*/
protected $mutexExpires = 3600;
/**
* @var bool
*/
protected $onOneServer = false;
/**
* @var mixed
*/
@ -116,6 +121,17 @@ class Crontab
return $this;
}
public function isOnOneServer(): bool
{
return $this->onOneServer;
}
public function setOnOneServer(bool $onOneServer): Crontab
{
$this->onOneServer = $onOneServer;
return $this;
}
public function getCallback()
{
return $this->callback;

View File

@ -96,6 +96,7 @@ class CrontabRegisterListener implements ListenerInterface
isset($annotation->singleton) && $crontab->setSingleton($annotation->singleton);
isset($annotation->mutexPool) && $crontab->setMutexPool($annotation->mutexPool);
isset($annotation->mutexExpires) && $crontab->setMutexExpires($annotation->mutexExpires);
isset($annotation->onOneServer) && $crontab->setOnOneServer($annotation->onOneServer);
isset($annotation->callback) && $crontab->setCallback($annotation->callback);
isset($annotation->memo) && $crontab->setMemo($annotation->memo);
return $crontab;

View File

@ -0,0 +1,86 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Crontab\Mutex;
use Hyperf\Crontab\Crontab;
use Hyperf\Redis\RedisFactory;
use Hyperf\Utils\Arr;
class RedisServerMutex implements ServerMutex
{
/**
* @var RedisFactory
*/
private $redisFactory;
/**
* @var string|null
*/
private $macAddress;
public function __construct(RedisFactory $redisFactory)
{
$this->redisFactory = $redisFactory;
$this->macAddress = $this->getMacAddress();
}
/**
* Attempt to obtain a server mutex for the given crontab.
*/
public function attempt(Crontab $crontab): bool
{
if ($this->macAddress === null) {
return false;
}
$redis = $this->redisFactory->get($crontab->getMutexPool());
$mutexName = $this->getMutexName($crontab);
$result = (bool) $redis->set($mutexName, $this->macAddress, ['NX', 'EX' => $crontab->getMutexExpires()]);
if ($result === true) {
return $result;
}
return $redis->get($mutexName) === $this->macAddress;
}
/**
* Get the server mutex for the given crontab.
*/
public function get(Crontab $crontab): string
{
return (string) $this->redisFactory->get($crontab->getMutexPool())->get(
$this->getMutexName($crontab)
);
}
protected function getMutexName(Crontab $crontab)
{
return 'hyperf' . DIRECTORY_SEPARATOR . 'crontab-' . sha1($crontab->getName() . $crontab->getRule()) . '-sv';
}
protected function getMacAddress(): ?string
{
$macAddresses = swoole_get_local_mac();
foreach (Arr::wrap($macAddresses) as $name => $address) {
if ($address && $address !== '00:00:00:00:00:00') {
return $name . ':' . str_replace(':', '', $address);
}
}
return null;
}
}

View File

@ -0,0 +1,28 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Crontab\Mutex;
use Hyperf\Crontab\Crontab;
interface ServerMutex
{
/**
* Attempt to obtain a server mutex for the given crontab.
*/
public function attempt(Crontab $crontab): bool;
/**
* Get the server mutex for the given crontab.
*/
public function get(Crontab $crontab): string;
}

View File

@ -17,7 +17,9 @@ use Closure;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Crontab\Crontab;
use Hyperf\Crontab\LoggerInterface;
use Hyperf\Crontab\Mutex\RedisServerMutex;
use Hyperf\Crontab\Mutex\RedisTaskMutex;
use Hyperf\Crontab\Mutex\ServerMutex;
use Hyperf\Crontab\Mutex\TaskMutex;
use Hyperf\Utils\Coroutine;
use Psr\Container\ContainerInterface;
@ -35,6 +37,16 @@ class Executor
*/
protected $logger;
/**
* @var \Hyperf\Crontab\Mutex\TaskMutex
*/
protected $taskMutex;
/**
* @var \Hyperf\Crontab\Mutex\ServerMutex
*/
protected $serverMutex;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
@ -58,7 +70,7 @@ class Executor
$parameters = $crontab->getCallback()[2] ?? null;
if ($class && $method && class_exists($class) && method_exists($class, $method)) {
$callback = function () use ($class, $method, $parameters, $crontab) {
$runable = function () use ($class, $method, $parameters, $crontab) {
$runnable = function () use ($class, $method, $parameters, $crontab) {
try {
$result = true;
$instance = make($class);
@ -81,10 +93,14 @@ class Executor
};
if ($crontab->isSingleton()) {
$runable = $this->runInSingleton($crontab, $runable);
$runnable = $this->runInSingleton($crontab, $runnable);
}
Coroutine::create($runable);
if ($crontab->isOnOneServer()) {
$runnable = $this->runOnOneServer($crontab, $runnable);
}
Coroutine::create($runnable);
};
}
break;
@ -102,9 +118,7 @@ class Executor
protected function runInSingleton(Crontab $crontab, Closure $runnable): Closure
{
return function () use ($crontab, $runnable) {
$taskMutex = $this->container->has(TaskMutex::class)
? $this->container->get(TaskMutex::class)
: $this->container->get(RedisTaskMutex::class);
$taskMutex = $this->getTaskMutex();
if ($taskMutex->exists($crontab) || ! $taskMutex->create($crontab)) {
$this->logger->info(sprintf('Crontab task [%s] skip to execute at %s.', $crontab->getName(), date('Y-m-d H:i:s')));
@ -118,4 +132,38 @@ class Executor
}
};
}
protected function getTaskMutex(): TaskMutex
{
if (! $this->taskMutex) {
$this->taskMutex = $this->container->has(TaskMutex::class)
? $this->container->get(TaskMutex::class)
: $this->container->get(RedisTaskMutex::class);
}
return $this->taskMutex;
}
protected function runOnOneServer(Crontab $crontab, Closure $runnable): Closure
{
return function () use ($crontab, $runnable) {
$taskMutex = $this->getServerMutex();
if (!$taskMutex->attempt($crontab)) {
$this->logger->info(sprintf('Crontab task [%s] skip to execute at %s.', $crontab->getName(), date('Y-m-d H:i:s')));
return;
}
$runnable();
};
}
protected function getServerMutex(): ServerMutex
{
if (! $this->serverMutex) {
$this->serverMutex = $this->container->has(ServerMutex::class)
? $this->container->get(ServerMutex::class)
: $this->container->get(RedisServerMutex::class);
}
return $this->serverMutex;
}
}

View File

@ -14,6 +14,7 @@ return [
'default' => [
'driver' => env('DB_DRIVER', 'mysql'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),

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

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

21
src/db/LICENSE Normal file
View File

@ -0,0 +1,21 @@
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.

49
src/db/composer.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "hyperf/db",
"type": "library",
"license": "MIT",
"keywords": [
"php",
"hyperf"
],
"description": "",
"autoload": {
"psr-4": {
"Hyperf\\DB\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\DB\\": "tests/"
}
},
"require": {
"php": ">=7.2",
"ext-swoole": ">=4.4",
"hyperf/config": "~1.1.0",
"hyperf/contract": "~1.1.0",
"hyperf/pool": "~1.1.0",
"hyperf/utils": "~1.1.0",
"psr/container": "^1.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
"phpstan/phpstan": "^0.10.5",
"hyperf/testing": "1.1.*",
"mockery/mockery": "^1.0",
"swoft/swoole-ide-helper": "dev-master"
},
"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": {
"hyperf": {
"config": "Hyperf\\DB\\ConfigProvider"
}
}
}

40
src/db/publish/db.php Normal file
View File

@ -0,0 +1,40 @@
<?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/hyperf/blob/master/LICENSE
*/
return [
'default' => [
'driver' => 'pdo',
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'fetch_mode' => PDO::FETCH_ASSOC,
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60),
],
'options' => [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
];

View File

@ -0,0 +1,75 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Pool\Connection;
use Hyperf\Pool\Exception\ConnectionException;
abstract class AbstractConnection extends Connection implements ConnectionInterface
{
use DetectsLostConnections;
use ManagesTransactions;
/**
* @var array
*/
protected $config = [];
public function getConfig(): array
{
return $this->config;
}
public function release(): void
{
if ($this->transactionLevel() > 0) {
$this->rollBack(0);
if ($this->container->has(StdoutLoggerInterface::class)) {
$logger = $this->container->get(StdoutLoggerInterface::class);
$logger->error('Maybe you\'ve forgotten to commit or rollback the MySQL transaction.');
}
}
$this->pool->release($this);
}
public function getActiveConnection()
{
if ($this->check()) {
return $this;
}
if (! $this->reconnect()) {
throw new ConnectionException('Connection reconnect failed.');
}
return $this;
}
public function retry(\Throwable $throwable, $name, $arguments)
{
if ($this->causedByLostConnection($throwable)) {
try {
$this->reconnect();
return $this->{$name}(...$arguments);
} catch (\Throwable $throwable) {
if ($this->container->has(StdoutLoggerInterface::class)) {
$logger = $this->container->get(StdoutLoggerInterface::class);
$logger->error('Connection execute retry failed. message = ' . $throwable->getMessage());
}
}
}
throw $throwable;
}
}

View File

@ -0,0 +1,41 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => [
],
'commands' => [
],
'annotations' => [
'scan' => [
'paths' => [
__DIR__,
],
],
],
'publish' => [
[
'id' => 'db',
'description' => 'The config for db.',
'source' => __DIR__ . '/../publish/db.php',
'destination' => BASE_PATH . '/config/autoload/db.php',
],
],
];
}
}

View File

@ -0,0 +1,64 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
interface ConnectionInterface
{
/**
* Start a new database transaction.
*/
public function beginTransaction(): void;
/**
* Commit the active database transaction.
*/
public function commit(): void;
/**
* Rollback the active database transaction.
*/
public function rollBack(?int $toLevel = null): void;
/**
* Run an insert statement against the database.
*
* @return int last insert id
*/
public function insert(string $query, array $bindings = []): int;
/**
* Run an execute statement against the database.
*
* @return int affected rows
*/
public function execute(string $query, array $bindings = []): int;
/**
* Execute an SQL statement and return the number of affected rows.
*
* @return int affected rows
*/
public function exec(string $sql): int;
/**
* Run a select statement against the database.
*/
public function query(string $query, array $bindings = []): array;
/**
* Run a select statement and return a single result.
*/
public function fetch(string $query, array $bindings = []);
public function call(string $method, array $argument = []);
}

118
src/db/src/DB.php Normal file
View File

@ -0,0 +1,118 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\DB\Pool\PoolFactory;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Context;
use Throwable;
/**
* @method beginTransaction()
* @method commit()
* @method rollback()
* @method insert(string $query, array $bindings = [])
* @method execute(string $query, array $bindings = [])
* @method query(string $query, array $bindings = [])
* @method fetch(string $query, array $bindings = [])
*/
class DB
{
/**
* @var PoolFactory
*/
protected $factory;
/**
* @var string
*/
protected $poolName;
public function __construct(PoolFactory $factory, string $poolName = 'default')
{
$this->factory = $factory;
$this->poolName = $poolName;
}
public function __call($name, $arguments)
{
$hasContextConnection = Context::has($this->getContextKey());
$connection = $this->getConnection($hasContextConnection);
try {
$connection = $connection->getConnection();
$result = $connection->{$name}(...$arguments);
} catch (Throwable $exception) {
$result = $connection->retry($exception, $name, $arguments);
} finally {
if (! $hasContextConnection) {
if ($this->shouldUseSameConnection($name)) {
// Should storage the connection to coroutine context, then use defer() to release the connection.
Context::set($this->getContextKey(), $connection);
defer(function () use ($connection) {
$connection->release();
});
} else {
// Release the connection after command executed.
$connection->release();
}
}
}
return $result;
}
public static function __callStatic($name, $arguments)
{
$container = ApplicationContext::getContainer();
$db = $container->get(static::class);
return $db->{$name}(...$arguments);
}
/**
* Define the commands that needs same connection to execute.
* When these commands executed, the connection will storage to coroutine context.
*/
protected function shouldUseSameConnection(string $methodName): bool
{
return in_array($methodName, [
'beginTransaction',
'commit',
'rollBack',
]);
}
/**
* Get a connection from coroutine context, or from mysql connectio pool.
*/
protected function getConnection(bool $hasContextConnection): AbstractConnection
{
$connection = null;
if ($hasContextConnection) {
$connection = Context::get($this->getContextKey());
}
if (! $connection instanceof AbstractConnection) {
$pool = $this->factory->getPool($this->poolName);
$connection = $pool->get();
}
return $connection;
}
/**
* The key to identify the connection object in coroutine context.
*/
private function getContextKey(): string
{
return sprintf('db.connection.%s', $this->poolName);
}
}

View File

@ -0,0 +1,49 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\Utils\Str;
use Throwable;
trait DetectsLostConnections
{
/**
* Determine if the given exception was caused by a lost connection.
*/
protected function causedByLostConnection(Throwable $e): bool
{
$message = $e->getMessage();
return Str::contains($message, [
'server has gone away',
'no connection to the server',
'Lost connection',
'is dead or not enabled',
'Error while sending',
'decryption failed or bad record mac',
'server closed the connection unexpectedly',
'SSL connection has been closed unexpectedly',
'Error writing data to the connection',
'Resource deadlock avoided',
'Transaction() on null',
'child connection forced to terminate due to client_idle_limit',
'query_wait_timeout',
'reset by peer',
'Physical connection is not usable',
'TCP Provider: Error code 0x68',
'Name or service not known',
'ORA-03114',
'Packets out of order. Expected',
]);
}
}

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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Exception;
class DriverNotFoundException extends RuntimeException
{
}

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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Exception;
class QueryException extends \PDOException
{
}

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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Exception;
class RuntimeException extends \RuntimeException
{
}

19
src/db/src/Frequency.php Normal file
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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\Pool\Frequency as DefaultFrequency;
class Frequency extends DefaultFrequency
{
}

View File

@ -0,0 +1,172 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Throwable;
trait ManagesTransactions
{
/**
* The number of active transactions.
*
* @var int
*/
protected $transactions = 0;
/**
* Start a new database transaction.
* @throws Throwable
*/
public function beginTransaction(): void
{
$this->createTransaction();
++$this->transactions;
}
/**
* Commit the active database transaction.
*/
public function commit(): void
{
if ($this->transactions == 1) {
$this->call('commit');
}
$this->transactions = max(0, $this->transactions - 1);
}
/**
* Rollback the active database transaction.
*
* @throws Throwable
*/
public function rollBack(?int $toLevel = null): void
{
// We allow developers to rollback to a certain transaction level. We will verify
// that this given transaction level is valid before attempting to rollback to
// that level. If it's not we will just return out and not attempt anything.
$toLevel = is_null($toLevel)
? $this->transactions - 1
: $toLevel;
if ($toLevel < 0 || $toLevel >= $this->transactions) {
return;
}
// Next, we will actually perform this rollback within this database and fire the
// rollback event. We will also set the current transaction level to the given
// level that was passed into this method so it will be right from here out.
try {
$this->performRollBack($toLevel);
} catch (Throwable $e) {
$this->handleRollBackException($e);
}
$this->transactions = $toLevel;
}
/**
* Get the number of active transactions.
*/
public function transactionLevel(): int
{
return $this->transactions;
}
/**
* Create a transaction within the database.
*/
protected function createTransaction(): void
{
if ($this->transactions == 0) {
try {
$this->call('beginTransaction');
} catch (Throwable $e) {
$this->handleBeginTransactionException($e);
}
} elseif ($this->transactions >= 1) {
$this->createSavepoint();
}
}
/**
* Create a save point within the database.
*/
protected function createSavepoint()
{
$this->exec(
$this->compileSavepoint('trans' . ($this->transactions + 1))
);
}
/**
* Handle an exception from a transaction beginning.
*
* @throws Throwable
*/
protected function handleBeginTransactionException(Throwable $e)
{
if ($this->causedByLostConnection($e)) {
$this->reconnect();
$this->call('beginTransaction');
} else {
throw $e;
}
}
/**
* Perform a rollback within the database.
*/
protected function performRollBack(int $toLevel)
{
if ($toLevel == 0) {
$this->call('rollBack');
} else {
$this->exec(
$this->compileSavepointRollBack('trans' . ($toLevel + 1))
);
}
}
/**
* Handle an exception from a rollback.
*
* @throws Throwable
*/
protected function handleRollBackException(Throwable $e)
{
if ($this->causedByLostConnection($e)) {
$this->transactions = 0;
}
throw $e;
}
/**
* Compile the SQL statement to define a savepoint.
*/
protected function compileSavepoint(string $name): string
{
return 'SAVEPOINT ' . $name;
}
/**
* Compile the SQL statement to execute a savepoint rollback.
*/
protected function compileSavepointRollBack(string $name): string
{
return 'ROLLBACK TO SAVEPOINT ' . $name;
}
}

View File

@ -0,0 +1,167 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\DB\Exception\RuntimeException;
use Hyperf\Pool\Pool;
use Psr\Container\ContainerInterface;
use Swoole\Coroutine\MySQL;
use Swoole\Coroutine\MySQL\Statement;
class MySQLConnection extends AbstractConnection
{
/**
* @var MySQL
*/
protected $connection;
/**
* @var array
*/
protected $config = [
'driver' => 'pdo',
'host' => 'localhost',
'port' => 3306,
'database' => 'hyperf',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => 60.0,
],
];
public function __construct(ContainerInterface $container, Pool $pool, array $config)
{
parent::__construct($container, $pool);
$this->config = array_replace_recursive($this->config, $config);
$this->reconnect();
}
public function __call($name, $arguments)
{
return $this->connection->{$name}(...$arguments);
}
/**
* Reconnect the connection.
*/
public function reconnect(): bool
{
$connection = new MySQL();
$connection->connect([
'host' => $this->config['host'],
'port' => $this->config['port'],
'user' => $this->config['username'],
'password' => $this->config['password'],
'database' => $this->config['database'],
'timeout' => $this->config['pool']['connect_timeout'],
'charset' => $this->config['charset'],
'fetch_mode' => true,
]);
$this->connection = $connection;
$this->lastUseTime = microtime(true);
return true;
}
/**
* Close the connection.
*/
public function close(): bool
{
unset($this->connection);
return true;
}
public function insert(string $query, array $bindings = []): int
{
$statement = $this->prepare($query);
$statement->execute($bindings);
return $statement->insert_id;
}
public function execute(string $query, array $bindings = []): int
{
$statement = $this->prepare($query);
$statement->execute($bindings);
return $statement->affected_rows;
}
public function exec(string $sql): int
{
$res = $this->connection->query($sql);
if ($res === false) {
throw new RuntimeException($this->connection->error);
}
return $this->connection->affected_rows;
}
public function query(string $query, array $bindings = []): array
{
// For select statements, we'll simply execute the query and return an array
// of the database result set. Each element in the array will be a single
// row from the database table, and will either be an array or objects.
$statement = $this->prepare($query);
$statement->execute($bindings);
return $statement->fetchAll();
}
public function fetch(string $query, array $bindings = [])
{
$records = $this->query($query, $bindings);
return array_shift($records);
}
public function call(string $method, array $argument = [])
{
$timeout = $this->config['pool']['wait_timeout'];
switch ($method) {
case 'beginTransaction':
return $this->connection->begin($timeout);
case 'rollBack':
return $this->connection->rollback($timeout);
case 'commit':
return $this->connection->commit($timeout);
}
return $this->connection->{$method}(...$argument);
}
protected function prepare(string $query): Statement
{
$statement = $this->connection->prepare($query);
if ($statement === false) {
throw new RuntimeException($this->connection->error);
}
return $statement;
}
}

View File

@ -0,0 +1,176 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\Pool\Exception\ConnectionException;
use Hyperf\Pool\Pool;
use PDO;
use PDOStatement;
use Psr\Container\ContainerInterface;
class PDOConnection extends AbstractConnection
{
/**
* @var PDO
*/
protected $connection;
/**
* @var array
*/
protected $config = [
'driver' => 'pdo',
'host' => 'localhost',
'port' => 3306,
'database' => 'hyperf',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'fetch_mode' => PDO::FETCH_ASSOC,
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => 60.0,
],
'options' => [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
],
];
/**
* Current mysql database.
* @var null|int
*/
protected $database;
public function __construct(ContainerInterface $container, Pool $pool, array $config)
{
parent::__construct($container, $pool);
$this->config = array_replace_recursive($this->config, $config);
$this->reconnect();
}
public function __call($name, $arguments)
{
return $this->connection->{$name}(...$arguments);
}
/**
* Reconnect the connection.
*/
public function reconnect(): bool
{
$host = $this->config['host'];
$dbName = $this->config['database'];
$username = $this->config['username'];
$password = $this->config['password'];
$dsn = "mysql:host={$host};dbname={$dbName}";
try {
$pdo = new \PDO($dsn, $username, $password, $this->config['options']);
} catch (\Throwable $e) {
throw new ConnectionException('Connection reconnect failed.:' . $e->getMessage());
}
$this->connection = $pdo;
$this->lastUseTime = microtime(true);
return true;
}
/**
* Close the connection.
*/
public function close(): bool
{
unset($this->connection);
return true;
}
public function query(string $query, array $bindings = []): array
{
// For select statements, we'll simply execute the query and return an array
// of the database result set. Each element in the array will be a single
// row from the database table, and will either be an array or objects.
$statement = $this->connection->prepare($query);
$this->bindValues($statement, $bindings);
$statement->execute();
$fetchModel = $this->config['fetch_mode'];
return $statement->fetchAll($fetchModel);
}
public function fetch(string $query, array $bindings = [])
{
$records = $this->query($query, $bindings);
return array_shift($records);
}
public function execute(string $query, array $bindings = []): int
{
$statement = $this->connection->prepare($query);
$this->bindValues($statement, $bindings);
$statement->execute();
return $statement->rowCount();
}
public function exec(string $sql): int
{
return $this->connection->exec($sql);
}
public function insert(string $query, array $bindings = []): int
{
$statement = $this->connection->prepare($query);
$this->bindValues($statement, $bindings);
$statement->execute();
return (int) $this->connection->lastInsertId();
}
public function call(string $method, array $argument = [])
{
return $this->connection->{$method}(...$argument);
}
/**
* Bind values to their parameters in the given statement.
*/
protected function bindValues(PDOStatement $statement, array $bindings): void
{
foreach ($bindings as $key => $value) {
$statement->bindValue(
is_string($key) ? $key : $key + 1,
$value,
is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR
);
}
}
}

View File

@ -0,0 +1,24 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Pool;
use Hyperf\Contract\ConnectionInterface;
use Hyperf\DB\MySQLConnection;
class MySQLPool extends Pool
{
protected function createConnection(): ConnectionInterface
{
return new MySQLConnection($this->container, $this, $this->config);
}
}

View File

@ -0,0 +1,24 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Pool;
use Hyperf\Contract\ConnectionInterface;
use Hyperf\DB\PDOConnection;
class PDOPool extends Pool
{
protected function createConnection(): ConnectionInterface
{
return new PDOConnection($this->container, $this, $this->config);
}
}

58
src/db/src/Pool/Pool.php Normal file
View File

@ -0,0 +1,58 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Pool;
use Hyperf\Contract\ConfigInterface;
use Hyperf\DB\Frequency;
use Hyperf\Pool\Pool as HyperfPool;
use Hyperf\Utils\Arr;
use Psr\Container\ContainerInterface;
abstract class Pool extends HyperfPool
{
/**
* @var string
*/
protected $name;
/**
* @var array
*/
protected $config;
public function __construct(ContainerInterface $container, string $name)
{
$config = $container->get(ConfigInterface::class);
$key = sprintf('db.%s', $name);
if (! $config->has($key)) {
throw new \InvalidArgumentException(sprintf('config[%s] is not exist!', $key));
}
$this->name = $name;
$this->config = $config->get($key);
$options = Arr::get($this->config, 'pool', []);
$this->frequency = make(Frequency::class);
parent::__construct($container, $options);
}
public function getName(): string
{
return $this->name;
}
public function getConfig(): array
{
return $this->config;
}
}

View File

@ -0,0 +1,60 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Pool;
use Hyperf\Contract\ConfigInterface;
use Hyperf\DB\Exception\DriverNotFoundException;
use Psr\Container\ContainerInterface;
class PoolFactory
{
/**
* @var Pool[]
*/
protected $pools = [];
/**
* @var ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getPool(string $name)
{
if (isset($this->pools[$name])) {
return $this->pools[$name];
}
$config = $this->container->get(ConfigInterface::class);
$driver = $config->get(sprintf('db.%s.driver', $name), 'pdo');
$class = $this->getPoolName($driver);
return $this->pools[$name] = make($class, [$this->container, $name]);
}
protected function getPoolName(string $driver)
{
switch (strtolower($driver)) {
case 'mysql':
return MySQLPool::class;
case 'pdo':
return PDOPool::class;
}
throw new DriverNotFoundException(sprintf('Driver %s is not found.', $driver));
}
}

View File

@ -0,0 +1,79 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\DB\Cases;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\DB\DB;
use Hyperf\DB\Frequency;
use Hyperf\DB\Pool\MySQLPool;
use Hyperf\DB\Pool\PDOPool;
use Hyperf\DB\Pool\PoolFactory;
use Hyperf\Di\Container;
use Hyperf\Pool\Channel;
use Hyperf\Pool\PoolOption;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Context;
use Mockery;
use PHPUnit\Framework\TestCase;
/**
* Class AbstractTestCase.
*/
abstract class AbstractTestCase extends TestCase
{
protected $driver = 'pdo';
protected function tearDown()
{
Mockery::close();
Context::set('db.connection.default', null);
}
protected function getContainer($options = [])
{
$container = Mockery::mock(Container::class);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn(new Config([
'db' => [
'default' => [
'driver' => $this->driver,
'password' => '',
'database' => 'hyperf',
'pool' => [
'max_connections' => 20,
],
'options' => $options,
],
],
]));
$container->shouldReceive('make')->with(PDOPool::class, Mockery::any())->andReturnUsing(function ($_, $args) {
return new PDOPool(...array_values($args));
});
$container->shouldReceive('make')->with(MySQLPool::class, Mockery::any())->andReturnUsing(function ($_, $args) {
return new MySQLPool(...array_values($args));
});
$container->shouldReceive('make')->with(Frequency::class, Mockery::any())->andReturn(new Frequency());
$container->shouldReceive('make')->with(PoolOption::class, Mockery::any())->andReturnUsing(function ($_, $args) {
return new PoolOption(...array_values($args));
});
$container->shouldReceive('make')->with(Channel::class, Mockery::any())->andReturnUsing(function ($_, $args) {
return new Channel(...array_values($args));
});
$container->shouldReceive('get')->with(PoolFactory::class)->andReturn($factory = new PoolFactory($container));
$container->shouldReceive('get')->with(DB::class)->andReturn(new DB($factory, 'default'));
$container->shouldReceive('has')->with(StdoutLoggerInterface::class)->andReturn(false);
ApplicationContext::setContainer($container);
return $container;
}
}

View File

@ -0,0 +1,52 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\DB\Cases;
/**
* @internal
* @coversNothing
*/
class MySQLDriverTest extends PDODriverTest
{
protected $driver = 'mysql';
public function testFetch()
{
parent::testFetch();
}
public function testQuery()
{
parent::testQuery();
}
public function testInsertAndExecute()
{
parent::testInsertAndExecute();
}
public function testTransaction()
{
parent::testTransaction();
}
public function testConfig()
{
parent::testConfig();
}
public function testMultiTransaction()
{
parent::testMultiTransaction();
}
}

View File

@ -0,0 +1,118 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\DB\Cases;
use Hyperf\DB\DB;
use Hyperf\DB\Pool\PoolFactory;
/**
* @internal
* @coversNothing
*/
class PDODriverTest extends AbstractTestCase
{
public function testFetch()
{
$db = $this->getContainer()->get(DB::class);
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [2]);
$this->assertSame('Hyperflex', $res['name']);
}
public function testQuery()
{
$db = $this->getContainer()->get(DB::class);
$res = $db->query('SELECT * FROM `user` WHERE id = ?;', [2]);
$this->assertSame('Hyperflex', $res[0]['name']);
}
public function testInsertAndExecute()
{
$db = $this->getContainer()->get(DB::class);
$id = $db->insert('INSERT INTO `user` (`name`, `gender`) VALUES (?,?);', [$name = uniqid(), $gender = rand(0, 2)]);
$this->assertTrue($id > 0);
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id]);
$this->assertSame($name, $res['name']);
$this->assertSame($gender, $res['gender']);
$res = $db->execute('UPDATE `user` SET `name` = ? WHERE id = ?', [$name = uniqid(), $id]);
$this->assertTrue($res > 0);
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id]);
$this->assertSame($name, $res['name']);
}
public function testTransaction()
{
$db = $this->getContainer()->get(DB::class);
$db->beginTransaction();
$id = $db->insert('INSERT INTO `user` (`name`, `gender`) VALUES (?,?);', [$name = uniqid(), $gender = rand(0, 2)]);
$this->assertTrue($id > 0);
$db->commit();
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id]);
$this->assertSame($name, $res['name']);
$this->assertSame($gender, $res['gender']);
$db->beginTransaction();
$id = $db->insert('INSERT INTO `user` (`name`, `gender`) VALUES (?,?);', [$name = uniqid(), $gender = rand(0, 2)]);
$this->assertTrue($id > 0);
$db->rollBack();
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id]);
$this->assertNull($res);
}
public function testConfig()
{
$factory = $this->getContainer()->get(PoolFactory::class);
$pool = $factory->getPool('default');
$this->assertSame('hyperf', $pool->getConfig()['database']);
$this->assertSame([], $pool->getConfig()['options']);
$connection = $pool->get();
$this->assertSame(6, count($connection->getConfig()['pool']));
$this->assertSame(20, $connection->getConfig()['pool']['max_connections']);
}
public function testMultiTransaction()
{
$db = $this->getContainer()->get(DB::class);
$db->beginTransaction();
$id = $db->insert('INSERT INTO `user` (`name`, `gender`) VALUES (?,?);', [$name = 'trans' . uniqid(), $gender = rand(0, 2)]);
$this->assertTrue($id > 0);
$db->beginTransaction();
$id2 = $db->insert('INSERT INTO `user` (`name`, `gender`) VALUES (?,?);', ['rollback' . uniqid(), rand(0, 2)]);
$this->assertTrue($id2 > 0);
$db->rollBack();
$db->commit();
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id2]);
$this->assertNull($res);
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id]);
$this->assertNotNull($res);
}
public function testStaticCall()
{
$this->getContainer();
$res = DB::fetch('SELECT * FROM `user` WHERE id = ?;', [1]);
$this->assertSame('Hyperf', $res['name']);
}
}

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/hyperf/blob/master/LICENSE
*/
require_once dirname(dirname(__FILE__)) . '/vendor/autoload.php';

View File

@ -17,6 +17,7 @@ use Hyperf\Config\ProviderConfig;
use Hyperf\Di\Annotation\Scanner;
use Hyperf\Di\Container;
use Psr\Container\ContainerInterface;
use Swoole\Timer;
use Symfony\Component\Console\Exception\LogicException;
class InitProxyCommand extends Command
@ -51,6 +52,8 @@ class InitProxyCommand extends Command
$this->createAopProxies();
Timer::clearAll();
$this->output->writeln('<info>Proxy class create success.</info>');
}

View File

@ -54,7 +54,6 @@ class Container implements ContainerInterface
/**
* Container constructor.
* @param Definition\DefinitionSourceInterface $definitionSource
*/
public function __construct(Definition\DefinitionSourceInterface $definitionSource)
{

View File

@ -58,7 +58,7 @@ abstract class AbstractLazyProxyBuilder
public function addClassBoilerplate(string $proxyClassName, string $originalClassName): AbstractLazyProxyBuilder
{
$namespace = join(array_slice(explode('\\', $proxyClassName), 0, -1), '\\');
$namespace = join('\\', array_slice(explode('\\', $proxyClassName), 0, -1));
$this->namespace = $namespace;
$this->proxyClassName = $proxyClassName;
$this->originalClassName = $originalClassName;
@ -67,7 +67,7 @@ abstract class AbstractLazyProxyBuilder
->addStmt(new ClassConst([new Const_('PROXY_TARGET', new String_($originalClassName))]))
->addStmt($this->factory->useTrait('\\Hyperf\\Di\\LazyLoader\\LazyProxyTrait'))
->setDocComment("/**
* Be careful: This is a lazy proxy, not the real $originalClassName from container.
* Be careful: This is a lazy proxy, not the real {$originalClassName} from container.
*
* {@inheritdoc}
*/");
@ -84,7 +84,6 @@ abstract class AbstractLazyProxyBuilder
public function getNode(): Node
{
if ($this->namespace) {
return $this->factory
->namespace($this->namespace)

View File

@ -12,10 +12,6 @@ declare(strict_types=1);
namespace Hyperf\Di\LazyLoader;
use PhpParser\Node\Const_;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\ClassConst;
class ClassLazyProxyBuilder extends AbstractLazyProxyBuilder
{
public function addClassRelationship(): AbstractLazyProxyBuilder

View File

@ -12,10 +12,6 @@ declare(strict_types=1);
namespace Hyperf\Di\LazyLoader;
use PhpParser\Node\Const_;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\ClassConst;
class FallbackLazyProxyBuilder extends AbstractLazyProxyBuilder
{
public function addClassRelationship(): AbstractLazyProxyBuilder

View File

@ -12,10 +12,6 @@ declare(strict_types=1);
namespace Hyperf\Di\LazyLoader;
use PhpParser\Node\Const_;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\ClassConst;
class InterfaceLazyProxyBuilder extends AbstractLazyProxyBuilder
{
public function addClassRelationship(): AbstractLazyProxyBuilder

View File

@ -14,19 +14,21 @@ namespace Hyperf\Di\LazyLoader;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Utils\Coroutine\Locker as CoLocker;
use Hyperf\Di\LazyLoader\PublicMethodVisitor;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\ParserFactory;
class LazyLoader {
class LazyLoader
{
public const CONFIG_FILE_NAME = 'lazy_loader';
/**
* Indicates if a loader has been registered.
*
* @var bool
*/
protected $registered = false;
/**
* The singleton instance of the loader.
*
@ -35,7 +37,7 @@ class LazyLoader {
protected static $instance;
/**
* The Configuration object
* The Configuration object.
*
* @var ConfigInterface
*/
@ -46,6 +48,7 @@ class LazyLoader {
$this->config = $config->get(self::CONFIG_FILE_NAME, []);
$this->register();
}
/**
* Get or create the singleton lazy loader instance.
*
@ -58,6 +61,7 @@ class LazyLoader {
}
return static::$instance;
}
/**
* Load a class proxy if it is registered.
*
@ -65,11 +69,12 @@ class LazyLoader {
*/
public function load(string $proxy)
{
if (array_key_exists($proxy, $this->config)) {
if ($this->config->get($proxy, false)) {
$this->loadProxy($proxy);
return true;
}
}
/**
* Register the loader on the auto-loader stack.
*/
@ -80,6 +85,7 @@ class LazyLoader {
$this->registered = true;
}
}
/**
* Load a real-time facade for the given proxy.
*/
@ -87,6 +93,7 @@ class LazyLoader {
{
require_once $this->ensureProxyExists($proxy);
}
/**
* Ensure that the given proxy has an existing real-time facade class.
*/
@ -103,7 +110,7 @@ class LazyLoader {
$targetPath = $path . '.' . uniqid();
$code = $this->generatorLazyProxy(
$proxy,
$this->config[$proxy]
$this->config->get($proxy)
);
file_put_contents($targetPath, $code);
rename($targetPath, $path);
@ -111,10 +118,9 @@ class LazyLoader {
}
return $path;
}
/**
* Format the lazy proxy with the proper namespace and class.
*
* @return string
*/
protected function generatorLazyProxy(string $proxy, string $target): string
{
@ -137,6 +143,16 @@ class LazyLoader {
return $this->buildNewCode($builder, $code, $proxy, $target);
}
/**
* Prepend the load method to the auto-loader stack.
*/
protected function prependToLoaderStack(): void
{
/** @var callable(string): void*/
$load = [$this, 'load'];
spl_autoload_register($load, true, true);
}
/**
* These conditions are really hard to proxy via inheritence.
* Luckily these conditions are very rarely met.
@ -144,9 +160,10 @@ class LazyLoader {
* TODO: implement some of them.
*
* @param \ReflectionClass $targetReflection [description]
* @return boolean [description]
* @return bool [description]
*/
private function isUnsupportedReflectionType(\ReflectionClass $targetReflection): bool {
private function isUnsupportedReflectionType(\ReflectionClass $targetReflection): bool
{
//Final class
if ($targetReflection->isFinal()) {
return true;
@ -184,14 +201,6 @@ class LazyLoader {
$builder->addNodes($visitor->nodes);
$prettyPrinter = new \PhpParser\PrettyPrinter\Standard();
$stmts = [$builder->getNode()];
$newCode = $prettyPrinter->prettyPrintFile($stmts);
return $newCode;
}
/**
* Prepend the load method to the auto-loader stack.
*/
protected function prependToLoaderStack(): void
{
spl_autoload_register([$this, 'load'], true, true);
return $prettyPrinter->prettyPrintFile($stmts);
}
}

View File

@ -12,51 +12,44 @@ declare(strict_types=1);
namespace Hyperf\Di\LazyLoader;
use Closure;
use Hyperf\Di\Annotation\AnnotationCollector;
use Hyperf\Di\Annotation\AspectCollector;
use Hyperf\Di\ReflectionManager;
use Hyperf\Utils\ApplicationContext;
trait LazyProxyTrait
{
public function __construct(){
public function __construct()
{
$vars = get_object_vars($this);
foreach (array_keys($vars) as $var) {
unset($this->{$var});
}
}
/**
* Return The Proxy Target
* @return mixed
*/
public function getInstance()
{
return ApplicationContext::getContainer()->get(Self::PROXY_TARGET);
}
public function __call(string $method, array $arguments)
{
$obj = $this->getInstance();
return call_user_func([$obj, $method], ...$arguments);
}
public function __get($name)
{
return $this->getInstance()->{$name};
}
public function __set($name, $value)
{
$this->getInstance()->{$name} = $value;
}
public function __isset($name)
{
return isset($this->getInstance()->{$name});
}
public function __unset($name)
{
unset($this->getInstance()->{$name});
}
public function __wakeup()
{
$vars = get_object_vars($this);
@ -64,4 +57,13 @@ trait LazyProxyTrait
unset($this->{$var});
}
}
/**
* Return The Proxy Target.
* @return mixed
*/
public function getInstance()
{
return ApplicationContext::getContainer()->get(self::PROXY_TARGET);
}
}

View File

@ -13,16 +13,15 @@ declare(strict_types=1);
namespace Hyperf\Di\LazyLoader;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\MagicConst\Function_ as MagicConstFunction;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use PhpParser\NodeVisitorAbstract;
class PublicMethodVisitor extends NodeVisitorAbstract
{

View File

@ -22,7 +22,6 @@ interface MethodDefinitionCollectorInterface
/**
* Retrieve the metadata for the return value of the method.
* @return ReflectionType
*/
public function getReturnType(string $class, string $method): ReflectionType;
}

View File

@ -12,15 +12,12 @@ declare(strict_types=1);
namespace HyperfTest\Di;
use Hyperf\Di\Container;
use Hyperf\Utils\ApplicationContext;
use HyperfTest\Di\Stub\LazyProxy;
use HyperfTest\Di\Stub\Proxied;
use Hyperf\Di\Container;
use Hyperf\Di\LazyLoader\PublicMethodVisitor;
use Hyperf\Utils\ApplicationContext;
use Mockery;
use PHPUnit\Framework\TestCase;
use PhpParser\NodeTraverser;
use PhpParser\ParserFactory;
/**
* @internal
@ -28,6 +25,74 @@ use PhpParser\ParserFactory;
*/
class LazyProxyTraitTest extends TestCase
{
public function testLaziness()
{
$lp = new LazyProxy();
$this->assertFalse(Proxied::$isInitialized);
}
public function testSet()
{
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$lp->id = '12';
$this->assertEquals('12', $proxied->id);
}
public function testGet()
{
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$this->assertEquals('20', $lp->id);
}
public function testUnset()
{
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
unset($lp->id);
$this->assertFalse(isset($proxied->id));
}
public function testIsset()
{
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$this->assertTrue(isset($lp->id));
}
public function testCallMethod()
{
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$lp->setId('1');
$this->assertEquals('1', $proxied->id);
}
public function testClone()
{
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$lp2 = clone $lp;
$this->assertEquals($proxied, $lp2->getInstance());
}
public function testSerialize()
{
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$s = serialize($lp);
$lp2 = unserialize($s);
$this->assertEquals($lp, $lp2);
}
private function mockContainer()
{
$container = Mockery::mock(Container::class);
@ -37,64 +102,4 @@ class LazyProxyTraitTest extends TestCase
->andReturn(new Proxied('20', 'hello'));
ApplicationContext::setContainer($container);
}
public function testLaziness(){
$lp = new LazyProxy();
$this->assertFalse(Proxied::$isInitialized);
}
public function testSet(){
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$lp->id = '12';
$this->assertEquals('12', $proxied->id);
}
public function testGet(){
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$this->assertEquals('20', $lp->id);
}
public function testUnset(){
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
unset($lp->id);
$this->assertFalse(isset($proxied->id));
}
public function testIsset(){
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$this->assertTrue(isset($lp->id));
}
public function testCallMethod(){
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$lp->setId('1');
$this->assertEquals('1', $proxied->id);
}
public function testClone(){
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$lp2 = clone $lp;
$this->assertEquals($proxied, $lp2->getInstance());
}
public function testSerialize(){
$lp = new LazyProxy();
$this->mockContainer();
$proxied = ApplicationContext::getContainer()->get(Proxied::class);
$s = serialize($lp);
$lp2 = unserialize($s);
$this->assertEquals($lp, $lp2);
}
}

View File

@ -17,6 +17,6 @@ use Hyperf\Di\LazyLoader\LazyProxyTrait;
class LazyProxy extends Proxied
{
use LazyProxyTrait;
const PROXY_TARGET = 'HyperfTest\\Di\\Stub\\Proxied';
const PROXY_TARGET = 'HyperfTest\\Di\\Stub\\Proxied';
}

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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ExceptionHandler\Listener;
use ErrorException;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BootApplication;
class ErrorExceptionHandler implements ListenerInterface
{
public function listen(): array
{
return [
BootApplication::class,
];
}
public function process(object $event)
{
set_error_handler(function ($level, $message, $file = '', $line = 0, $context = []) {
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
});
}
}

View File

@ -0,0 +1,38 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ExceptionHandler;
use Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ErrorExceptionHandlerTest extends TestCase
{
public function testHandleError()
{
$listener = new ErrorExceptionHandler();
$listener->process((object) []);
$this->expectException(\ErrorException::class);
$this->expectExceptionMessage('Undefined offset: 1');
try {
$array = [];
$array[1];
} finally {
restore_error_handler();
}
}
}

View File

@ -14,6 +14,7 @@ namespace Hyperf\GraphQL\Annotation;
use Hyperf\Di\Annotation\AnnotationCollector;
use Hyperf\Di\ReflectionManager;
use Hyperf\GraphQL\ClassCollector;
use ReflectionProperty;
trait AnnotationTrait
@ -31,16 +32,19 @@ trait AnnotationTrait
public function collectClass(string $className): void
{
AnnotationCollector::collectClass($className, static::class, $this);
ClassCollector::collect($className);
}
public function collectMethod(string $className, ?string $target): void
{
AnnotationCollector::collectMethod($className, $target, static::class, $this);
ClassCollector::collect($className);
}
public function collectProperty(string $className, ?string $target): void
{
AnnotationCollector::collectProperty($className, $target, static::class, $this);
ClassCollector::collect($className);
}
protected function bindMainProperty(string $key, array $value)

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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\GraphQL;
class ClassCollector
{
private static $classes = [];
public static function collect(string $class)
{
if (! in_array($class, self::$classes)) {
self::$classes[] = $class;
}
}
public static function getClasses()
{
return self::$classes;
}
}

View File

@ -18,12 +18,13 @@ use TheCodingMachine\GraphQLite\GraphQLException;
use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface;
use TheCodingMachine\GraphQLite\Types\ArgumentResolver;
use TheCodingMachine\GraphQLite\Types\ResolvableInputInterface;
use TheCodingMachine\GraphQLite\Types\ResolvableInputObjectType as TheCodingMachineResolvableInputObjectType;
use function get_class;
/**
* A GraphQL input object that can be resolved using a factory.
*/
class ResolvableInputObjectType extends InputObjectType implements ResolvableInputInterface
class ResolvableInputObjectType extends TheCodingMachineResolvableInputObjectType implements ResolvableInputInterface
{
/**
* @var ArgumentResolver
@ -59,7 +60,7 @@ class ResolvableInputObjectType extends InputObjectType implements ResolvableInp
}
$config += $additionalConfig;
parent::__construct($config);
InputObjectType::__construct($config);
}
/**

View File

@ -14,7 +14,6 @@ namespace Hyperf\GraphQL;
use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\OutputType;
use Hyperf\Di\Annotation\AnnotationCollector;
use Hyperf\GraphQL\Annotation\Type;
use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
@ -544,8 +543,8 @@ class TypeMapper implements TypeMapperInterface
{
if ($this->classes === null) {
$this->classes = [];
$classes = AnnotationCollector::getClassByAnnotation(Type::class);
foreach (array_keys($classes) as $className) {
$classes = ClassCollector::getClasses();
foreach ($classes as $className) {
if (! \class_exists($className)) {
continue;
}

View File

@ -192,7 +192,6 @@ trait MessageTrait
}
/**
* @param array $headers
* @return static
*/
public function withHeaders(array $headers)
@ -341,7 +340,6 @@ trait MessageTrait
}
/**
* @param array $headers
* @return static
*/
private function setHeaders(array $headers)

View File

@ -274,7 +274,6 @@ class CookieJar implements CookieJarInterface
*
* @see https://tools.ietf.org/html/rfc6265#section-5.1.4
*
* @param RequestInterface $request
* @return string
*/
private function getCookiePathFromRequest(RequestInterface $request)
@ -299,8 +298,6 @@ class CookieJar implements CookieJarInterface
/**
* If a cookie already exists and the server asks to set it again with a
* null value, the cookie must be deleted.
*
* @param SetCookie $cookie
*/
private function removeCookieIfEmpty(SetCookie $cookie)
{

View File

@ -66,7 +66,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
/**
* Load a swoole request, and transfer to a swoft request object.
*
* @param \Swoole\Http\Request $swooleRequest
* @return \Hyperf\HttpMessage\Server\Request
*/
public static function loadFromSwooleRequest(\Swoole\Http\Request $swooleRequest)
@ -103,7 +102,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
/**
* Return an instance with the specified server params.
*
* @param array $serverParams
* @return static
*/
public function withServerParams(array $serverParams)
@ -118,8 +116,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
* Retrieves cookies sent by the client to the server.
* The data MUST be compatible with the structure of the $_COOKIE
* superglobal.
*
* @return array
*/
public function getCookieParams(): array
{
@ -154,8 +150,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
* params. If you need to ensure you are only getting the original
* values, you may need to parse the query string from `getUri()->getQuery()`
* or from the `QUERY_STRING` server param.
*
* @return array
*/
public function getQueryParams(): array
{
@ -416,8 +410,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
/**
* Get the full URL for the request.
*
* @return string
*/
public function fullUrl(): string
{
@ -451,16 +443,12 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
return $this->hasHeader('X-Requested-With') == 'XMLHttpRequest';
}
/**
* @return \Swoole\Http\Request
*/
public function getSwooleRequest(): \Swoole\Http\Request
{
return $this->swooleRequest;
}
/**
* @param \Swoole\Http\Request $swooleRequest
* @return $this
*/
public function setSwooleRequest(\Swoole\Http\Request $swooleRequest)
@ -532,7 +520,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
* Loops through all nested files and returns a normalized array of
* UploadedFileInterface instances.
*
* @param array $files
* @return UploadedFileInterface[]
*/
private static function normalizeNestedFileSpec(array $files = [])
@ -555,7 +542,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
/**
* Get a Uri populated with values from $swooleRequest->server.
* @param \Swoole\Http\Request $swooleRequest
* @throws \InvalidArgumentException
* @return \Psr\Http\Message\UriInterface
*/
@ -579,11 +565,10 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
} elseif (isset($server['server_addr'])) {
$uri = $uri->withHost($server['server_addr']);
} elseif (isset($header['host'])) {
if (\strpos($header['host'], ':')) {
$hasPort = true;
if (\strpos($header['host'], ':')) {
[$host, $port] = explode(':', $header['host'], 2);
if ($port !== '80') {
if ($port != $uri->getDefaultPort()) {
$uri = $uri->withPort($port);
}
} else {

View File

@ -29,9 +29,6 @@ class Response extends \Hyperf\HttpMessage\Base\Response implements Sendable
*/
protected $cookies = [];
/**
* @param null|\Swoole\Http\Response $response
*/
public function __construct(\Swoole\Http\Response $response = null)
{
$this->swooleResponse = $response;
@ -74,6 +71,14 @@ class Response extends \Hyperf\HttpMessage\Base\Response implements Sendable
return $clone;
}
/**
* Return all cookies.
*/
public function getCookies(): array
{
return $this->cookies;
}
public function getSwooleResponse(): ?\Swoole\Http\Response
{
return $this->swooleResponse;

View File

@ -28,8 +28,6 @@ class SwooleStream implements StreamInterface
/**
* SwooleStream constructor.
*
* @param string $contents
*/
public function __construct(string $contents = '')
{

View File

@ -66,13 +66,6 @@ class UploadedFile extends \SplFileInfo implements UploadedFileInterface
*/
private $mimeType;
/**
* @param string $tmpFile
* @param null|int $size
* @param int $errorStatus
* @param null|string $clientFilename
* @param null|string $clientMediaType
*/
public function __construct(
string $tmpFile,
?int $size,
@ -254,9 +247,6 @@ class UploadedFile extends \SplFileInfo implements UploadedFileInterface
return $this->clientMediaType;
}
/**
* @return array
*/
public function toArray(): array
{
return [

View File

@ -545,8 +545,6 @@ class Uri implements UriInterface
* Whether the URI has the default port of the current scheme.
* `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used
* independently of the implementation.
*
* @return bool
*/
public function isDefaultPort(): bool
{
@ -708,7 +706,6 @@ class Uri implements UriInterface
}
/**
* @param array $match
* @return string
*/
private function rawurlencodeMatchZero(array $match)

View File

@ -12,11 +12,14 @@ declare(strict_types=1);
namespace HyperfTest\HttpMessage;
use Hyperf\HttpMessage\Server\Request;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Utils\Codec\Json;
use HyperfTest\HttpMessage\Stub\Server\RequestStub;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Swoole\Http\Request as SwooleRequest;
/**
* @internal
@ -60,4 +63,24 @@ class ServerRequestTest extends TestCase
$request->shouldReceive('getBody')->andReturn(new SwooleStream(json_encode($json)));
$this->assertSame($json, RequestStub::normalizeParsedBody($data, $request));
}
public function testGetUriFromGlobals()
{
$swooleRequest = Mockery::mock(SwooleRequest::class);
$data = ['name' => 'Hyperf'];
$swooleRequest->shouldReceive('rawContent')->andReturn(Json::encode($data));
$swooleRequest->server = ['server_port' => 9501];
$request = Request::loadFromSwooleRequest($swooleRequest);
$uri = $request->getUri();
$this->assertSame(9501, $uri->getPort());
$swooleRequest = Mockery::mock(SwooleRequest::class);
$data = ['name' => 'Hyperf'];
$swooleRequest->shouldReceive('rawContent')->andReturn(Json::encode($data));
$swooleRequest->header = ['host' => '127.0.0.1'];
$swooleRequest->server = ['server_port' => 9501];
$request = Request::loadFromSwooleRequest($swooleRequest);
$uri = $request->getUri();
$this->assertSame(null, $uri->getPort());
}
}

View File

@ -0,0 +1,55 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ModelCache;
use Hyperf\Database\Model\Builder as ModelBuilder;
use Hyperf\Utils\ApplicationContext;
class Builder extends ModelBuilder
{
public function delete()
{
return $this->deleteCache(function () {
return parent::delete();
});
}
public function update(array $values)
{
return $this->deleteCache(function () use ($values) {
return parent::update($values);
});
}
protected function deleteCache(\Closure $closure)
{
$queryBuilder = clone $this;
$primaryKey = $this->model->getKeyName();
$ids = [];
$models = $queryBuilder->get([$primaryKey]);
foreach ($models as $model) {
$ids[] = $model->{$primaryKey};
}
if (empty($ids)) {
return 0;
}
$result = $closure();
$manger = ApplicationContext::getContainer()->get(Manager::class);
$manger->destroy($ids, get_class($this->model));
return $result;
}
}

View File

@ -12,12 +12,20 @@ declare(strict_types=1);
namespace Hyperf\ModelCache;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Collection;
use Hyperf\Database\Model\Model;
use Hyperf\Database\Query\Builder as QueryBuilder;
use Hyperf\ModelCache\Builder as ModelCacheBuilder;
use Hyperf\Utils\ApplicationContext;
trait Cacheable
{
/**
* @var bool
*/
protected $useCacheBuilder = false;
/**
* Fetch a model from cache.
* @param mixed $id
@ -33,10 +41,8 @@ trait Cacheable
/**
* Fetch models from cache.
* @param mixed $ids
* @return \Hyperf\Database\Model\Collection
*/
public static function findManyFromCache($ids): Collection
public static function findManyFromCache(array $ids): Collection
{
$container = ApplicationContext::getContainer();
$manager = $container->get(Manager::class);
@ -47,7 +53,6 @@ trait Cacheable
/**
* Delete model from cache.
* @return bool
*/
public function deleteCache(): bool
{
@ -58,8 +63,8 @@ trait Cacheable
/**
* Increment a column's value by a given amount.
* @param mixed $column
* @param mixed $amount
* @param string $column
* @param float|int $amount
* @return int
*/
public function increment($column, $amount = 1, array $extra = [])
@ -81,8 +86,8 @@ trait Cacheable
/**
* Decrement a column's value by a given amount.
* @param mixed $column
* @param mixed $amount
* @param string $column
* @param float|int $amount
* @return int
*/
public function decrement($column, $amount = 1, array $extra = [])
@ -101,4 +106,31 @@ trait Cacheable
}
return $res;
}
/**
* Create a new Model query builder for the model.
* @param QueryBuilder $query
*/
public function newModelBuilder($query): Builder
{
if ($this->useCacheBuilder) {
return new ModelCacheBuilder($query);
}
return parent::newModelBuilder($query);
}
public function newQuery(bool $cache = false): Builder
{
$this->useCacheBuilder = $cache;
return parent::newQuery();
}
/**
* @param bool $cache Whether to delete the model cache when batch update
*/
public static function query(bool $cache = false): Builder
{
return (new static())->newQuery($cache);
}
}

View File

@ -19,7 +19,7 @@ interface CacheableInterface
{
public static function findFromCache($id): ?Model;
public static function findManyFromCache($ids): Collection;
public static function findManyFromCache(array $ids): Collection;
public function deleteCache(): bool;
}

View File

@ -96,18 +96,11 @@ class Config
return $this;
}
/**
* @return string
*/
public function getPool(): string
{
return $this->pool;
}
/**
* @param string $pool
* @return Config
*/
public function setPool(string $pool): Config
{
$this->pool = $pool;

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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ModelCache\Exception;
class OperatorNotFoundException extends \RuntimeException
{
}

View File

@ -14,7 +14,9 @@ namespace Hyperf\ModelCache\Handler;
use Hyperf\ModelCache\Config;
use Hyperf\ModelCache\Exception\CacheException;
use Hyperf\ModelCache\Redis\HashsGetMultiple;
use Hyperf\ModelCache\Redis\HashGetMultiple;
use Hyperf\ModelCache\Redis\HashIncr;
use Hyperf\ModelCache\Redis\LuaManager;
use Hyperf\Redis\RedisProxy;
use Hyperf\Utils\Contracts\Arrayable;
use Psr\Container\ContainerInterface;
@ -38,11 +40,9 @@ class RedisHandler implements HandlerInterface
protected $config;
/**
* @var HashsGetMultiple
* @var LuaManager
*/
protected $multiple;
protected $luaSha = '';
protected $manager;
protected $defaultKey = 'HF-DATA';
@ -57,7 +57,7 @@ class RedisHandler implements HandlerInterface
$this->redis = make(RedisProxy::class, ['pool' => $config->getPool()]);
$this->config = $config;
$this->multiple = new HashsGetMultiple();
$this->manager = make(LuaManager::class, [$config]);
}
public function get($key, $default = null)
@ -107,25 +107,14 @@ class RedisHandler implements HandlerInterface
public function getMultiple($keys, $default = null)
{
if ($this->config->isLoadScript()) {
$sha = $this->getLuaSha();
}
if (! empty($sha)) {
$list = $this->redis->evalSha($sha, $keys, count($keys));
} else {
$script = $this->multiple->getScript();
$list = $this->redis->eval($script, $keys, count($keys));
}
$data = $this->manager->handle(HashGetMultiple::class, $keys);
$result = [];
foreach ($this->multiple->parseResponse($list) as $item) {
foreach ($data as $item) {
unset($item[$this->defaultKey]);
if ($item) {
if (! empty($item)) {
$result[] = $item;
}
}
return $result;
}
@ -151,18 +140,8 @@ class RedisHandler implements HandlerInterface
public function incr($key, $column, $amount): bool
{
$ret = $this->redis->hIncrByFloat($key, $column, (float) $amount);
return is_float($ret);
}
$data = $this->manager->handle(HashIncr::class, [$key, $column, $amount]);
protected function getLuaSha()
{
if (! empty($this->luaSha)) {
return $this->luaSha;
}
$sha = $this->redis->script('load', $this->multiple->getScript());
return $this->luaSha = $sha;
return is_float($data);
}
}

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Hyperf\ModelCache\Redis;
class HashsGetMultiple implements OperatorInterface
class HashGetMultiple implements OperatorInterface
{
public function getScript(): string
{

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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ModelCache\Redis;
class HashIncr implements OperatorInterface
{
public function getScript(): string
{
return <<<'LUA'
if(redis.call('type',KEYS[1]).ok == 'hash') then
return redis.call('HINCRBYFLOAT',KEYS[1] , KEYS[2] , KEYS[3]);
end
return false;
LUA;
}
public function parseResponse($data)
{
return $data;
}
}

View File

@ -0,0 +1,85 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ModelCache\Redis;
use Hyperf\ModelCache\Config;
use Hyperf\ModelCache\Exception\OperatorNotFoundException;
use Hyperf\Redis\RedisProxy;
class LuaManager
{
/**
* @var array<string,OperatorInterface>
*/
protected $operators = [];
/**
* @var array<string,string>
*/
protected $luaShas = [];
/**
* @var Config
*/
protected $config;
/**
* @var \Redis|RedisProxy
*/
protected $redis;
public function __construct(Config $config)
{
$this->config = $config;
$this->redis = make(RedisProxy::class, ['pool' => $config->getPool()]);
$this->operators[HashGetMultiple::class] = new HashGetMultiple();
$this->operators[HashIncr::class] = new HashIncr();
}
public function handle(string $key, array $keys)
{
if ($this->config->isLoadScript()) {
$sha = $this->getLuaSha($key);
}
$operator = $this->getOperator($key);
if (! empty($sha)) {
$luaData = $this->redis->evalSha($sha, $keys, count($keys));
} else {
$script = $operator->getScript();
$luaData = $this->redis->eval($script, $keys, count($keys));
}
return $operator->parseResponse($luaData);
}
public function getOperator(string $key): OperatorInterface
{
if (! isset($this->operators[$key])) {
throw new OperatorNotFoundException(sprintf('The operator %s is not found.', $key));
}
if (! $this->operators[$key] instanceof OperatorInterface) {
throw new OperatorNotFoundException(sprintf('The operator %s is not instanceof OperatorInterface.', $key));
}
return $this->operators[$key];
}
public function getLuaSha(string $key): string
{
if (empty($this->luaShas[$key])) {
$this->luaShas[$key] = $this->redis->script('load', $this->getOperator($key)->getScript());
}
return $this->luaShas[$key];
}
}

View File

@ -0,0 +1,176 @@
<?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/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ModelCache;
use Hyperf\Redis\RedisProxy;
use HyperfTest\ModelCache\Stub\ContainerStub;
use HyperfTest\ModelCache\Stub\UserModel;
use Mockery;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ModelCacheTest extends TestCase
{
public function tearDown()
{
Mockery::close();
}
public function testFindByCache()
{
ContainerStub::mockContainer();
$user = UserModel::findFromCache(1);
$expect = UserModel::query()->find(1);
$this->assertEquals($expect, $user);
}
public function testFindManyByCache()
{
ContainerStub::mockContainer();
$users = UserModel::findManyFromCache([1, 2, 3]);
$expects = UserModel::query()->findMany([1, 2, 3]);
$this->assertTrue(count($users) == 2);
$this->assertEquals([1, 2], array_keys($users->getDictionary()));
$this->assertEquals($expects, $users);
}
public function testDeleteByBuilder()
{
$container = ContainerStub::mockContainer();
$ids = [200, 201, 202];
foreach ($ids as $id) {
UserModel::query()->firstOrCreate(['id' => $id], [
'name' => uniqid(),
'gender' => 1,
]);
}
UserModel::findManyFromCache($ids);
/** @var \Redis $redis */
$redis = $container->make(RedisProxy::class, ['pool' => 'default']);
foreach ($ids as $id) {
$this->assertEquals(1, $redis->exists('{mc:default:m:user}:id:' . $id));
}
UserModel::query(true)->whereIn('id', $ids)->delete();
foreach ($ids as $id) {
$this->assertEquals(0, $redis->exists('{mc:default:m:user}:id:' . $id));
}
foreach ($ids as $id) {
$this->assertNull(UserModel::query()->find($id));
}
}
public function testUpdateByBuilder()
{
$container = ContainerStub::mockContainer();
$ids = [203, 204, 205];
foreach ($ids as $id) {
UserModel::query()->firstOrCreate(['id' => $id], [
'name' => uniqid(),
'gender' => 1,
]);
}
UserModel::findManyFromCache($ids);
/** @var \Redis $redis */
$redis = $container->make(RedisProxy::class, ['pool' => 'default']);
foreach ($ids as $id) {
$this->assertEquals(1, $redis->exists('{mc:default:m:user}:id:' . $id));
}
UserModel::query(true)->whereIn('id', $ids)->update(['gender' => 2]);
foreach ($ids as $id) {
$this->assertEquals(0, $redis->exists('{mc:default:m:user}:id:' . $id));
}
foreach ($ids as $id) {
$this->assertSame(2, UserModel::query()->find($id)->gender);
}
UserModel::query(true)->whereIn('id', $ids)->delete();
}
public function testIncr()
{
$container = ContainerStub::mockContainer();
$id = 206;
UserModel::query()->firstOrCreate(['id' => $id], [
'name' => uniqid(),
'gender' => 1,
]);
$model = UserModel::findFromCache($id);
/** @var \Redis $redis */
$redis = $container->make(RedisProxy::class, ['pool' => 'default']);
$this->assertEquals(1, $redis->exists('{mc:default:m:user}:id:' . $id));
$this->assertEquals(1, $model->increment('gender', 1));
$this->assertEquals(1, $redis->exists('{mc:default:m:user}:id:' . $id));
$this->assertEquals(2, $redis->hGet('{mc:default:m:user}:id:' . $id, 'gender'));
$this->assertEquals(2, UserModel::findFromCache($id)->gender);
$this->assertEquals(2, UserModel::query()->find($id)->gender);
UserModel::query(true)->where('id', $id)->delete();
}
public function testFindNullBeforeCreate()
{
$container = ContainerStub::mockContainer();
$id = 207;
$model = UserModel::findFromCache($id);
/** @var \Redis $redis */
$redis = $container->make(RedisProxy::class, ['pool' => 'default']);
$this->assertEquals(1, $redis->exists('{mc:default:m:user}:id:' . $id));
$this->assertNull($model);
$this->assertEquals(1, $redis->del('{mc:default:m:user}:id:' . $id));
UserModel::query(true)->where('id', $id)->delete();
}
public function testIncrNotExist()
{
$container = ContainerStub::mockContainer();
$id = 206;
UserModel::query()->firstOrCreate(['id' => $id], [
'name' => uniqid(),
'gender' => 1,
]);
$model = UserModel::query()->find($id);
/** @var \Redis $redis */
$redis = $container->make(RedisProxy::class, ['pool' => 'default']);
$this->assertEquals(0, $redis->exists('{mc:default:m:user}:id:' . $id));
$this->assertEquals(1, $model->increment('gender', 1));
$this->assertEquals(0, $redis->exists('{mc:default:m:user}:id:' . $id));
$this->assertEquals(2, UserModel::query()->find($id)->gender);
$this->assertEquals(2, UserModel::findFromCache($id)->gender);
UserModel::query(true)->where('id', $id)->delete();
}
}

Some files were not shown because too many files have changed in this diff Show More