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

@ -2,7 +2,7 @@
Hyperf 是基于 `Swoole 4.4+` 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 `PHP-FPM` 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均基于 [PSR 标准](https://www.php-fig.org/psr) 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是 `可替换``可复用` 的。
框架组件库除了常见的协程版的 `MySQL 客户端`、`Redis 客户端`,还为您准备了协程版的 `Eloquent ORM`、`WebSocket 服务端及客户端`、`JSON RPC 服务端及客户端`、`GRPC 服务端及客户端`、`Zipkin/Jaeger (OpenTracing) 客户端`、`Guzzle HTTP 客户端`、`Elasticsearch 客户端`、`Consul 客户端`、`ETCD 客户端`、`AMQP 组件`、`Apollo 配置中心`、`阿里云 ACM 应用配置管理`、`ETCD 配置中心`、`基于令牌桶算法的限流器`、`通用连接池`、`熔断器`、`Swagger 文档生成`、`Swoole Tracker`、`Blade 和 Smarty 视图引擎`、`Snowflake 全局ID生成器` 等组件,省去了自己实现对应协程版本的麻烦。
框架组件库除了常见的协程版的 `MySQL 客户端`、`Redis 客户端`,还为您准备了协程版的 `Eloquent ORM`、`WebSocket 服务端及客户端`、`JSON RPC 服务端及客户端`、`GRPC 服务端及客户端`、`Zipkin/Jaeger (OpenTracing) 客户端`、`Guzzle HTTP 客户端`、`Elasticsearch 客户端`、`Consul 客户端`、`ETCD 客户端`、`AMQP 组件`、`Apollo 配置中心`、`阿里云 ACM 应用配置管理`、`ETCD 配置中心`、`基于令牌桶算法的限流器`、`通用连接池`、`熔断器`、`Swagger 文档生成`、`Swoole Tracker`、`Blade 和 Smarty 视图引擎`、`Snowflake 全局 ID 生成器` 等组件,省去了自己实现对应协程版本的麻烦。
Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面向切面编程`、`基于 PSR-15 的中间件`、`自定义进程`、`基于 PSR-14 的事件管理器`、`Redis/RabbitMQ 消息队列`、`自动模型缓存`、`基于 PSR-16 的缓存`、`Crontab 秒级定时任务`、`国际化`、`Validation 表单验证器` 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。

View File

@ -1,4 +1,4 @@
# AMQP组件
# AMQP 组件
[hyperf/amqp](https://github.com/hyperf/amqp) 是实现 AMQP 标准的组件,主要适用于对 RabbitMQ 的使用。

View File

@ -42,7 +42,7 @@ return [
## 使用注解
注解一共有3种应用对象分别是 `类`、`类方法` 和 `类属性`
注解一共有 3 种应用对象,分别是 `类`、`类方法` 和 `类属性`
### 使用类注解

View File

@ -18,7 +18,7 @@ composer require hyperf/async-queue
|:----------------:|:---------:|:-------------------------------------------:|:---------------------------------------:|
| driver | string | Hyperf\AsyncQueue\Driver\RedisDriver::class | 无 |
| channel | string | queue | 队列前缀 |
| timeout | int | 2 | pop消息的超时时间 |
| timeout | int | 2 | pop 消息的超时时间 |
| retry_seconds | int,array | 5 | 失败后重新尝试间隔 |
| handle_timeout | int | 10 | 消息处理超时时间 |
| processes | int | 1 | 消费进程数 |

View File

@ -38,7 +38,7 @@
## 依赖注入容器
- [hyperf/di](https://github.com/hyperf/di) Hyperf 官方提供的支持注解及 AOP 的依赖注入容器
- [reasno/lazy-loader](https://github.com/Reasno/LazyLoader) 为Hyperf DI补充基于类型提示的懒加载注入。
- [reasno/lazy-loader](https://github.com/Reasno/LazyLoader) 为 Hyperf DI 补充基于类型提示的懒加载注入。
## 服务
@ -120,6 +120,6 @@
## 第三方 SDK
- [yurunsoft/pay-sdk](https://github.com/Yurunsoft/PaySDK) 支持 Swoole 协程的支付宝/微信支付 SDK
- [yurunsoft/yurun-oauth-login](https://github.com/Yurunsoft/YurunOAuthLogin) 支持 Swoole 协程的第三方登录授权 SDKQQ、微信、微博、Github、Gitee等
- [yurunsoft/yurun-oauth-login](https://github.com/Yurunsoft/YurunOAuthLogin) 支持 Swoole 协程的第三方登录授权 SDKQQ、微信、微博、Github、Gitee 等)
- [overtrue/wechat](zh/sdks/wechat) EasyWeChat一个流行的非官方微信 SDK
- [Yurunsoft/PHPMailer-Swoole](https://github.com/Yurunsoft/PHPMailer-Swoole) Swoole 协程环境下的可用的 PHPMailer

View File

@ -11,7 +11,7 @@ composer require hyperf/cache
| 配置 | 默认值 | 备注 |
|:------:|:----------------------------------------:|:---------------------:|
| driver | Hyperf\Cache\Driver\RedisDriver | 缓存驱动默认为Redis |
| driver | Hyperf\Cache\Driver\RedisDriver | 缓存驱动,默认为 Redis |
| packer | Hyperf\Utils\Packer\PhpSerializer | 打包器 |
| prefix | c: | 缓存前缀 |
@ -42,7 +42,7 @@ $cache = $container->get(\Psr\SimpleCache\CacheInterface::class);
### 注解方式
组件提供 `Hyperf\Cache\Annotation\Cacheable` 注解,作用于类方法,可以配置对应的缓存前缀、失效时间、监听器和缓存组。
例如UserService 提供一个 user 方法可以查询对应id的用户信息。当加上 `Hyperf\Cache\Annotation\Cacheable` 注解后会自动生成对应的Redis缓存key值为`user:id`,超时时间为 `9000` 秒。首次查询时,会从数据库中查,后面查询时,会从缓存中查。
例如UserService 提供一个 user 方法,可以查询对应 id 的用户信息。当加上 `Hyperf\Cache\Annotation\Cacheable` 注解后,会自动生成对应的 Redis 缓存key 值为 `user:id` ,超时时间为 `9000` 秒。首次查询时,会从数据库中查,后面查询时,会从缓存中查。
> 缓存注解基于 [aop](zh/aop.md) 和 [di](zh/di.md),所以只有在 `Container` 中获取到的对象实例才有效,比如通过 `$container->get``make` 方法所获得的对象,直接 `new` 出来的对象无法使用。
@ -188,13 +188,13 @@ public function updateUserBook(int $id)
## 缓存驱动
### Redis驱动
### Redis 驱动
`Hyperf\Cache\Driver\RedisDriver` 会把缓存数据存放到 `Redis` 中,需要用户配置相应的 `Redis配置`。此方式为默认方式。
`Hyperf\Cache\Driver\RedisDriver` 会把缓存数据存放到 `Redis` 中,需要用户配置相应的 `Redis 配置`。此方式为默认方式。
### 协程内存驱动
> 本驱动乃Beta版本请谨慎使用。
> 本驱动乃 Beta 版本,请谨慎使用。
如果您需要将数据缓存到 `Context` 中,可以尝试此驱动。例如以下应用场景 `Demo::get` 会在多个地方调用多次,但是又不想每次都到 `Redis` 中进行查询。

View File

@ -46,8 +46,8 @@
- [#779](https://github.com/hyperf/hyperf/pull/779) 修复 `JPG` 文件验证不通过的问题。
- [#787](https://github.com/hyperf/hyperf/pull/787) 修复 `db:seed` 参数 `--class` 多余,导致报错的问题。
- [#795](https://github.com/hyperf/hyperf/pull/795) 修复自定义进程在异常抛出后无法正常重启的BUG。
- [#796](https://github.com/hyperf/hyperf/pull/796) 修复 `etcd` 配置中心 `enable` 即时设为 `false`在项目启动时依然会拉取配置的BUG。
- [#795](https://github.com/hyperf/hyperf/pull/795) 修复自定义进程在异常抛出后,无法正常重启的 BUG。
- [#796](https://github.com/hyperf/hyperf/pull/796) 修复 `etcd` 配置中心 `enable` 即时设为 `false`,在项目启动时,依然会拉取配置的 BUG。
## 优化
@ -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

@ -49,7 +49,7 @@ class UserService
```
默认熔断策略为`超时策略`,如果您想要自己实现熔断策略,只需要自己实现 `Handler` 继承于 `Hyperf\CircuitBreaker\Handler\AbstractHandler` 即可。
默认熔断策略为 `超时策略` ,如果您想要自己实现熔断策略,只需要自己实现 `Handler` 继承于 `Hyperf\CircuitBreaker\Handler\AbstractHandler` 即可。
```php
<?php

View File

@ -91,7 +91,7 @@ return [
## 接入 Etcd 配置中心
- 安装 `Etcd客户端`
- 安装 `Etcd 客户端`
```
composer require hyperf/etcd
@ -106,7 +106,7 @@ composer require start-point/etcd-php
composer require linkorb/etcd-php
```
- 添加 `Etcd客户端` 配置文件 `etcd.php`
- 添加 `Etcd 客户端` 配置文件 `etcd.php`
```php
<?php
@ -119,13 +119,13 @@ return [
];
```
- 安装 `Etcd配置中心`
- 安装 `Etcd 配置中心`
```
composer require hyperf/config-etcd
```
- 添加 `Etcd配置中心` 配置文件 `config_etcd.php`
- 添加 `Etcd 配置中心` 配置文件 `config_etcd.php`
> mapping 为 `Etcd``Config` 的映射关系。映射中不存在的 `key`,则不会被同步到 `Config` 中。

View File

@ -6,11 +6,11 @@ Hyperf 是运行于 `Swoole 4` 的协程之上的,这也是 Hyperf 能提供
### PHP-FPM 的运作模式
在聊协程是什么之前,我们先聊聊传统 `PHP-FPM` 架构的运作模式,`PHP-FPM` 是一个多进程的 `FastCGI` 管理程序,是绝大多数 `PHP` 应用所使用的运行模式。假设我们使用 `Nginx` 提供 `HTTP` 服务(`Apache` 同理),所有客户端发起的请求最先抵达的都是 `Nginx`,然后 `Nginx` 通过 `FastCGI` 协议将请求转发给 `PHP-FPM` 处理,`PHP-FPM` 的 `Worker进程` 会抢占式的获得 CGI 请求进行处理,这个处理指的就是,等待 `PHP` 脚本的解析,等待业务处理的结果返回,完成后回收子进程,这整个的过程是阻塞等待的,也就意味着 `PHP-FPM` 的进程数有多少能处理的请求也就是多少,假设 `PHP-FPM``200``Worker进程`,一个请求将耗费 `1` 秒的时间,那么简单的来说整个服务器理论上最多可以处理的请求也就是 `200` 个,`QPS` 即为 `200/s`,在高并发的场景下,这样的性能往往是不够的,尽管可以利用 `Nginx` 作为负载均衡配合多台 `PHP-FPM` 服务器来提供服务,但由于 `PHP-FPM` 的阻塞等待的工作模型,一个请求会占用至少一个 `MySQL` 连接,多节点高并发下会产生大量的 `MySQL` 连接,而 `MySQL` 的最大连接数默认值为 `100`,尽管可以修改,但显而易见该模式没法很好的应对高并发的场景。
在聊协程是什么之前,我们先聊聊传统 `PHP-FPM` 架构的运作模式,`PHP-FPM` 是一个多进程的 `FastCGI` 管理程序,是绝大多数 `PHP` 应用所使用的运行模式。假设我们使用 `Nginx` 提供 `HTTP` 服务(`Apache` 同理),所有客户端发起的请求最先抵达的都是 `Nginx`,然后 `Nginx` 通过 `FastCGI` 协议将请求转发给 `PHP-FPM` 处理,`PHP-FPM` 的 `Worker 进程` 会抢占式的获得 CGI 请求进行处理,这个处理指的就是,等待 `PHP` 脚本的解析,等待业务处理的结果返回,完成后回收子进程,这整个的过程是阻塞等待的,也就意味着 `PHP-FPM` 的进程数有多少能处理的请求也就是多少,假设 `PHP-FPM``200``Worker 进程`,一个请求将耗费 `1` 秒的时间,那么简单的来说整个服务器理论上最多可以处理的请求也就是 `200` 个,`QPS` 即为 `200/s`,在高并发的场景下,这样的性能往往是不够的,尽管可以利用 `Nginx` 作为负载均衡配合多台 `PHP-FPM` 服务器来提供服务,但由于 `PHP-FPM` 的阻塞等待的工作模型,一个请求会占用至少一个 `MySQL` 连接,多节点高并发下会产生大量的 `MySQL` 连接,而 `MySQL` 的最大连接数默认值为 `100`,尽管可以修改,但显而易见该模式没法很好的应对高并发的场景。
### 异步非阻塞系统
在高并发的场景下,异步非阻塞就显得优势明显了,直观的优点表现就是 `Worker进程` 不再同步阻塞的去处理一个请求,而是可以同时处理多个请求,无需 `I/O` 等待,并发能力极强,可以同时发起或维护大量的请求。那么最直观的缺点大家可能也都知道,就是永无止境的回调,业务逻辑必须在对应的回调函数内实现,如果业务逻辑存在多次的 `I/O` 请求,则会存在很多层的回调函数,下面示例一段 `Swoole 1.x` 下的异步回调型的伪代码片段。
在高并发的场景下,异步非阻塞就显得优势明显了,直观的优点表现就是 `Worker 进程` 不再同步阻塞的去处理一个请求,而是可以同时处理多个请求,无需 `I/O` 等待,并发能力极强,可以同时发起或维护大量的请求。那么最直观的缺点大家可能也都知道,就是永无止境的回调,业务逻辑必须在对应的回调函数内实现,如果业务逻辑存在多次的 `I/O` 请求,则会存在很多层的回调函数,下面示例一段 `Swoole 1.x` 下的异步回调型的伪代码片段。
```php
$db = new swoole_mysql();
@ -48,9 +48,9 @@ Swoole 协程也是对异步回调的一种解决方案,在 `PHP` 语言下,
### 协程是什么?
我们已经知道了协程可以很好的解决异步非阻塞系统的开发问题,那么协程本身到底是什么呢?从定义上来说,*协程是一种轻量级的线程,由用户代码来调度和管理,而不是由操作系统内核来进行调度,也就是在用户态进行*。可以直接的理解为就是一个非标准的线程实现,但什么时候切换由用户自己来实现,而不是由操作系统分配 `CPU` 时间决定。具体来说,`Swoole` 的每个 `Worker进程` 会存在一个协程调度器来调度协程,协程切换的时机就是遇到 `I/O` 操作或代码显性切换时,进程内以单线程的形式运行协程,也就意味着一个进程内同一时间只会有一个协程在运行且切换时机明确,也就无需处理像多线程编程下的各种同步锁的问题。
单个协程内的代码运行仍是串行的,放在一个 HTTP 协程服务上来理解就是每一个请求是一个协程举个例子假设为请求A创建了 `协程A`,为 `请求B` 创建了 `协程B`,那么在处理 `协程A` 的时候代码跑到了查询 `MySQL` 的语句上,这个时候 `协程A` 则会触发协程切换,`协程A` 就继续等待 `I/O` 设备返回结果,那么此时就会切换到 `协程B`,开始处理 `协程B` 的逻辑,当又遇到了一个 `I/O` 操作便又触发协程切换,再回过来从 `协程A` 刚才切走的地方继续执行,如此反复,遇到 `I/O` 操作就切换到另一个协程去继续执行而非一直阻塞等待。
这里可以发现一个问题就是,*`协程A` 的 `MySQL` 查询操作必须得是一个异步非阻塞的操作,否则会由于阻塞导致协程调度器没法切换到另一个协程继续执行*,这个也是要在协程编程下需要规避的问题之一。
我们已经知道了协程可以很好的解决异步非阻塞系统的开发问题,那么协程本身到底是什么呢?从定义上来说,*协程是一种轻量级的线程,由用户代码来调度和管理,而不是由操作系统内核来进行调度,也就是在用户态进行*。可以直接的理解为就是一个非标准的线程实现,但什么时候切换由用户自己来实现,而不是由操作系统分配 `CPU` 时间决定。具体来说,`Swoole` 的每个 `Worker 进程` 会存在一个协程调度器来调度协程,协程切换的时机就是遇到 `I/O` 操作或代码显性切换时,进程内以单线程的形式运行协程,也就意味着一个进程内同一时间只会有一个协程在运行且切换时机明确,也就无需处理像多线程编程下的各种同步锁的问题。
单个协程内的代码运行仍是串行的,放在一个 HTTP 协程服务上来理解就是每一个请求是一个协程,举个例子,假设为请求 A 创建了 `协程 A`,为 `请求 B` 创建了 `协程 B`,那么在处理 `协程 A` 的时候代码跑到了查询 `MySQL` 的语句上,这个时候 `协程 A` 则会触发协程切换,`协程 A` 就继续等待 `I/O` 设备返回结果,那么此时就会切换到 `协程 B`,开始处理 `协程 B` 的逻辑,当又遇到了一个 `I/O` 操作便又触发协程切换,再回过来从 `协程 A` 刚才切走的地方继续执行,如此反复,遇到 `I/O` 操作就切换到另一个协程去继续执行而非一直阻塞等待。
这里可以发现一个问题就是,*`协程 A` 的 `MySQL` 查询操作必须得是一个异步非阻塞的操作,否则会由于阻塞导致协程调度器没法切换到另一个协程继续执行*,这个也是要在协程编程下需要规避的问题之一。
### 协程与普通线程有哪些区别?
@ -60,7 +60,7 @@ Swoole 协程也是对异步回调的一种解决方案,在 `PHP` 语言下,
### 不能存在阻塞代码
协程内代码的阻塞会导致协程调度器无法切换到另一个协程继续执行代码,所以我们绝不能在协程内存在阻塞代码,假设我们启动了 `4``Worker` 来处理 `HTTP` 请求(通常启动的 `Worker` 数量与 `CPU` 核心数一致或 `2` 倍),如果代码中存在阻塞,暂且理论的认为每个请求都会阻塞`1` 秒,那么系统的 `QPS` 也将退化为 `4/s`,这无疑就是退化成了与 `PHP-FPM` 类似的情况,所以我们绝对不能在协程中存在阻塞代码。
协程内代码的阻塞会导致协程调度器无法切换到另一个协程继续执行代码,所以我们绝不能在协程内存在阻塞代码,假设我们启动了 `4``Worker` 来处理 `HTTP` 请求(通常启动的 `Worker` 数量与 `CPU` 核心数一致或 `2` 倍),如果代码中存在阻塞,暂且理论的认为每个请求都会阻塞 `1` 秒,那么系统的 `QPS` 也将退化为 `4/s` ,这无疑就是退化成了与 `PHP-FPM` 类似的情况,所以我们绝对不能在协程中存在阻塞代码。
那么到底哪些是阻塞代码呢?我们可以简单的认为绝大多数你所熟知的非 `Swoole` 提供的异步函数的 `MySQL`、`Redis`、`Memcache`、`MongoDB`、`HTTP`、`Socket`等客户端,文件操作、`sleep/usleep` 等均为阻塞函数,这几乎涵盖了所有日常操作,那么要如何解决呢?`Swoole` 提供了 `MySQL`、`PostgreSQL`、`Redis`、`HTTP`、`Socket` 的协程客户端可以使用,同时 `Swoole 4.1` 之后提供了一键协程化的方法 `\Swoole\Runtime::enableCoroutine()`,只需在使用协程前运行这一行代码,`Swoole` 会将 所有使用 `php_stream` 进行 `socket` 操作均变成协程调度的异步 `I/O`,可以理解为除了 `curl` 绝大部分原生的操作都可以适用,关于此部分可查阅 [Swoole 文档](https://wiki.swoole.com/wiki/page/965.html) 获得更具体的信息。
@ -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` 的调度去。
@ -273,7 +280,7 @@ $request = Context::override(ServerRequestInterface::class, function (ServerRequ
### Swoole Runtime Hook Level
框架在入口函数中提供了 `SWOOLE_HOOK_FLAGS` 常量,如果您需要修改整个项目的 `Runtime Hook` 等级,比如想要支持 `CURL协程`,可以修改这里的代码,如下。
框架在入口函数中提供了 `SWOOLE_HOOK_FLAGS` 常量,如果您需要修改整个项目的 `Runtime Hook` 等级,比如想要支持 `CURL 协程`,可以修改这里的代码,如下。
```php
<?php

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` 进程是否配置了支持协程。
##### 协程执行策略
@ -155,4 +159,4 @@ return [
## 运行定时任务
当您完成上述的配置后,以及定义了定时任务后,只需要直接启动 `Server`,定时任务便会一同启动。
在您启动后,即便您定义了足够短周期的定时任务,定时任务也不会马上开始执行,所有定时任务都会等到下一个分钟周期时才会开始执行,比如您启动的时候是 `10时11分12秒`,那么定时任务会在 `10时12分00秒` 才会正式开始执行。
在您启动后,即便您定义了足够短周期的定时任务,定时任务也不会马上开始执行,所有定时任务都会等到下一个分钟周期时才会开始执行,比如您启动的时候是 `10 11 12 秒`,那么定时任务会在 `10 12 00 秒` 才会正式开始执行。

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

@ -5,7 +5,7 @@
得益于 [hyperf/event](https://github.com/hyperf/event) 组件的支撑,用户可以很方便的对以下事件进行监听。
例如 `QueryExecuted` , `StatementPrepared` , `TransactionBeginning` , `TransactionCommitted` , `TransactionRolledBack`
接下来我们就实现一个记录SQL的监听器来说一下怎么使用。
接下来我们就实现一个记录 SQL 的监听器,来说一下怎么使用。
首先我们定义好 `DbQueryExecutedListener` ,实现 `Hyperf\Event\Contract\ListenerInterface` 接口并对类定义 `Hyperf\Event\Annotation\Listener` 注解,这样 Hyperf 就会自动把该监听器注册到事件调度器中,无需任何手动配置,示例代码如下:
```php
@ -76,7 +76,7 @@ class DbQueryExecutedListener implements ListenerInterface
|:------------:|:----------------:|:--------:|:-------------------------- --:|
| booting | 模型首次加载前 | 否 | 进程生命周期中只会触发一次 |
| booted | 模型首次加载后 | 否 | 进程生命周期中只会触发一次 |
| retrieved | 填充数据后 | 否 | 每当模型从DB或缓存查询出来后触发 |
| retrieved | 填充数据后 | 否 | 每当模型从 DB 或缓存查询出来后触发 |
| creating | 数据创建时 | 是 | |
| created | 数据创建后 | 否 | |
| updating | 数据更新时 | 是 | |

View File

@ -1,6 +1,6 @@
# 模型缓存
在高频场景下我们会频繁的查询数据库虽然有主键加持但也会影响到数据库性能。这种kv查询方式我们可以很方便的使用 `模型缓存` 来减缓数据库压力。本模块实现了自动缓存,删除和修改模型时,自动删除缓存。累加、减操作时,直接操作缓存进行对应累加、减。
在高频场景下,我们会频繁的查询数据库,虽然有主键加持,但也会影响到数据库性能。这种 kv 查询方式,我们可以很方便的使用 `模型缓存` 来减缓数据库压力。本模块实现了自动缓存,删除和修改模型时,自动删除缓存。累加、减操作时,直接操作缓存进行对应累加、减。
> 模型缓存暂支持 `Redis`存储,其他存储引擎会慢慢补充。
@ -17,11 +17,11 @@ composer require hyperf/model-cache
| 配置 | 类型 | 默认值 | 备注 |
|:---------------:|:------:|:-----------------------------------------------------:|:-----------------------------------:|
| handler | string | Hyperf\DbConnection\Cache\Handler\RedisHandler::class | 无 |
| cache_key | string | `mc:%s:m:%s:%s:%s` | `mc:缓存前缀:m:表名:主键KEY:主键值` |
| cache_key | string | `mc:%s:m:%s:%s:%s` | `mc:缓存前缀:m:表名:主键 KEY:主键值` |
| prefix | string | db connection name | 缓存前缀 |
| ttl | int | 3600 | 超时时间 |
| empty_model_ttl | int | 60 | 查询不到数据时的超时时间 |
| load_script | bool | true | Redis引擎下 是否使用evalSha代替eval |
| load_script | bool | true | Redis 引擎下 是否使用 evalSha 代替 eval |
```php
<?php
@ -58,7 +58,7 @@ return [
## 使用
模型缓存的使用十分简单只需要在对应Model中实现 `Hyperf\ModelCache\CacheableInterface` 接口,当然,框架已经提供了对应实现,只需要引入 `Hyperf\ModelCache\Cacheable` Trait 即可。
模型缓存的使用十分简单,只需要在对应 Model 中实现 `Hyperf\ModelCache\CacheableInterface` 接口,当然,框架已经提供了对应实现,只需要引入 `Hyperf\ModelCache\Cacheable` Trait 即可。
```php
<?php
@ -107,7 +107,7 @@ $models = User::findManyFromCache($ids);
```
对应Redis数据如下其中 `HF-DATA:DEFAULT` 作为占位符存在于 `HASH` 中,*所以用户不要使用 `HF-DATA` 作为数据库字段*。
对应 Redis 数据如下,其中 `HF-DATA:DEFAULT` 作为占位符存在于 `HASH` 中,*所以用户不要使用 `HF-DATA` 作为数据库字段*。
```
127.0.0.1:6379> hgetall "mc:default:m:user:id:1"
1) "id"
@ -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

@ -27,7 +27,7 @@ $ php bin/hyperf.php db:model table_name
| --inheritance | string | `Model` | 父类 |
| --uses | string | `App\Model\Model` | 配合 `inheritance` 使用 |
| --refresh-fillable | bool | `false` | 是否刷新 `fillable` 参数 |
| --table-mapping | array | `[]` | 为表名->模型增加映射关系 比如 ['users:Account'] |
| --table-mapping | array | `[]` | 为表名 -> 模型增加映射关系 比如 ['users:Account'] |
| --ignore-tables | array | `[]` | 不需要生成模型的表名 比如 ['users'] |
| --with-comments | bool | `false` | 是否增加字段注释 |
@ -335,7 +335,7 @@ use App\Model\User;
$count = User::query()->where('gender', 1)->count();
```
## 插入&更新模型
## 插入 & 更新模型
### 插入

View File

@ -19,7 +19,7 @@ $users = Db::table('user')->get();
$users = Db::table('user')->select('name', 'gender as user_gender')->get();
```
`Db::select()` 方法会返回一个array`get` 方法会返回 `Hyperf\Utils\Collection`。其中元素是 `stdClass`,所以可以通过以下代码返回各个元素的数据
`Db::select()` 方法会返回一个 array`get` 方法会返回 `Hyperf\Utils\Collection`。其中元素是 `stdClass`,所以可以通过以下代码返回各个元素的数据
```php
<?php
@ -207,7 +207,7 @@ $users = $query->addSelect('age')->get();
## 原始表达式
有时你需要在查询中使用原始表达式,例如实现 `COUNT(0) AS count`,这就需要用到`raw`方法。
有时你需要在查询中使用原始表达式,例如实现 `COUNT(0) AS count`,这就需要用到 `raw` 方法。
```php
use Hyperf\DbConnection\Db;

View File

@ -28,7 +28,7 @@ composer require hyperf/database
|:--------------------:|:------:|:---------------:|:------------------:|
| driver | string | 无 | 数据库引擎 |
| host | string | 无 | 数据库地址 |
| database | string | 无 | 数据库默认DB |
| database | string | 无 | 数据库默认 DB |
| username | string | 无 | 数据库用户名 |
| password | string | null | 数据库密码 |
| charset | string | utf8 | 数据库编码 |

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);
```
## 注意事项
### 容器仅管理长生命周期的对象
@ -319,7 +366,7 @@ $userService = make(UserService::class, ['enableCache' => true]);
## 获取容器对象
有些时候我们可能希望去实现一些更动态的需求时,会希望可以直接获取到 `容器(Container)` 对象在绝大部分情况下框架的入口类比如命令类、控制器、RPC服务提供者等都是由 `容器(Container)` 创建并维护的,也就意味着您所写的绝大部分业务代码都是在 `容器(Container)` 的管理作用之下的,也就意味着在绝大部分情况下您都可以通过在 `构造函数(Constructor)` 声明或通过 `@Inject` 注解注入 `Psr\Container\ContainerInterface` 接口类都能够获得 `Hyperf\Di\Container` 容器对象,我们通过代码来演示一下:
有些时候我们可能希望去实现一些更动态的需求时,会希望可以直接获取到 `容器(Container)` 对象在绝大部分情况下框架的入口类比如命令类、控制器、RPC 服务提供者等)都是由 `容器(Container)` 创建并维护的,也就意味着您所写的绝大部分业务代码都是在 `容器(Container)` 的管理作用之下的,也就意味着在绝大部分情况下您都可以通过在 `构造函数(Constructor)` 声明或通过 `@Inject` 注解注入 `Psr\Container\ContainerInterface` 接口类都能够获得 `Hyperf\Di\Container` 容器对象,我们通过代码来演示一下:
```php
<?php

View File

@ -176,7 +176,7 @@ class UserService
1. 这个时候还不是协程环境,如果 `Listener` 中注入了可能会触发协程切换的类,就会导致框架启动失败。
2. 运行 `di:init-proxy` 脚本时,因为实例化了 `EventDispatcherInterface`,进而导致所有的 `Listener` 实例化,一旦这个过程生成了代理对象(.proxy.php 扩展名的类),而脚本内部又有删除代理类的逻辑,就会导致代理类生成有误。
3. 条件与上述一致,只不过代理类又配置了别名,会导致生成这个别名对象时,因为判断代理类不存在,则会重新生成,但 `Ast` 已经生成了注解树并被修改为代理类的注解树Ast注解树内部节点使用了引用则会导致代理类生成有误。【上述两个问题会在后面的版本修复修改 `di:init-proxy` 脚本不再删除缓存】
3. 条件与上述一致,只不过代理类又配置了别名,会导致生成这个别名对象时,因为判断代理类不存在,则会重新生成,但 `Ast` 已经生成了注解树并被修改为代理类的注解树Ast 注解树内部节点使用了引用),则会导致代理类生成有误。【上述两个问题会在后面的版本修复,修改 `di:init-proxy` 脚本不再删除缓存】
### `BootApplication` 事件尽量避免 IO 操作

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

@ -37,7 +37,7 @@ class Foo {
}
```
## 使用Swoole配置
## 使用 Swoole 配置
有时候我们想直接修改 `Swoole` 配置,所以我们也提供了相关配置项,不过这项配置在 `Curl Guzzle 客户端` 中是无法生效的,所以谨慎使用。
@ -69,7 +69,7 @@ Hyperf 除了实现了 `Hyperf\Guzzle\CoroutineHandler` 外,还基于 `Hyperf\
### 原因
简单来说,主机 TCP连接数 是有上限的当我们并发大到超过这个上限值时就导致请求无法正常建立。另外TCP连接结束后还会有一个 TIME-WAIT 阶段,所以也无法实时释放连接。这就导致了实际并发可能远低于 TCP 上限值。所以,我们需要一个连接池来维持这个阶段,尽量减少 TIME-WAIT 造成的影响让TCP连接进行复用。
简单来说,主机 TCP 连接数 是有上限的当我们并发大到超过这个上限值时就导致请求无法正常建立。另外TCP 连接结束后还会有一个 TIME-WAIT 阶段,所以也无法实时释放连接。这就导致了实际并发可能远低于 TCP 上限值。所以,我们需要一个连接池来维持这个阶段,尽量减少 TIME-WAIT 造成的影响,让 TCP 连接进行复用。
### 使用

View File

@ -296,7 +296,7 @@ return [
];
```
### 返回PHP对象
### 返回 PHP 对象
当框架导入 `symfony/serialize (^4.3)``symfony/property-access (^4.3)` 后,`Hyperf\Contract\NormalizerInterface` 的实现会自动使用 `Hyperf\Utils\Serializer\SymfonyNormalizer` 而非 `Hyperf\Utils\Serializer\SimpleNormalizer`
`SymfonyNormalizer` 支持对象的序列化和反序列化。暂时不支持这种 `MathValue[]` 对象数组。

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进程` 中处理指标收集与上报。
* `use_standalone_process`: 是否使用 `独立监控进程`。推荐开启。关闭后将在 `Worker 进程` 中处理指标收集与上报。
```php
'use_standalone_process' => env('TELEMETRY_USE_STANDALONE_PROCESS', true),
```
* `enable_default_metric`: 是否统计默认指标。默认指标包括内存占用、系统 CPU 负载以及Swoole Server 指标和 Swoole Coroutine 指标。
* `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),
```
@ -89,7 +91,7 @@ Prometheus 有两种工作模式,爬模式与推模式(通过 Prometheus Pus
'mode' => Constants::SCRAPE_MODE
```
并配置爬取地址 `scrape_host`、爬取端口 `scrape_port`、爬取路径 `scrape_path`。Prometheus 可以在对应配置下以HTTP访问形式拉取全部指标。
并配置爬取地址 `scrape_host`、爬取端口 `scrape_port`、爬取路径 `scrape_path`。Prometheus 可以在对应配置下以 HTTP 访问形式拉取全部指标。
> 注意:爬模式下,必须启用独立进程,即 use_standalone_process = true。
@ -124,7 +126,7 @@ return [
];
```
StatsD 目前只支持 UDP 模式,需要配置 UDP 地址 `udp_host`UDP 端口 `udp_port`、是否批量推送 `enable_batch`(减少请求次数)、批量推送间隔 `push_interval` 以及采样率`sample_rate`。
StatsD 目前只支持 UDP 模式,需要配置 UDP 地址 `udp_host`UDP 端口 `udp_port`、是否批量推送 `enable_batch`(减少请求次数)、批量推送间隔 `push_interval` 以及采样率 `sample_rate`
#### 配置 InfluxDB
@ -150,7 +152,7 @@ return [
];
```
InfluxDB 使用默认的 HTTP 模式,需要配置地址 `host`UDP端口 `port`、用户名 `username`、密码 `password`、`dbname` 数据表以及批量推送间隔 `push_interval`
InfluxDB 使用默认的 HTTP 模式,需要配置地址 `host`UDP 端口 `port`、用户名 `username`、密码 `password`、`dbname` 数据表以及批量推送间隔 `push_interval`
### 基本抽象
@ -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
@ -182,7 +184,7 @@ interface GaugeInterface
}
```
* 直方图(Histogram)用于描述对某一事件的持续观测后产生的统计学分布通常表示为百分位数或分桶。如HTTP请求延迟。
* 直方图(Histogram):用于描述对某一事件的持续观测后产生的统计学分布,通常表示为百分位数或分桶。如 HTTP 请求延迟。
```php
interface HistogramInterface
@ -212,7 +214,7 @@ return [
### 自定义使用
通过HTTP中间件遥测仅仅是本组件用途的冰山一角您可以注入 `Hyperf\Metric\Contract\MetricFactoryInterface` 类来自行遥测业务数据。比如:创建的订单数量、广告的点击数量等。
通过 HTTP 中间件遥测仅仅是本组件用途的冰山一角,您可以注入 `Hyperf\Metric\Contract\MetricFactoryInterface` 类来自行遥测业务数据。比如:创建的订单数量、广告的点击数量等。
```php
<?php
@ -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

@ -1,6 +1,6 @@
# 中间件
这里的中间件指的是"中间件模式",该功能属于 [hyperf/http-server](https://github.com/hyperf/http-server) 组件内的一项主要功能,主要用于编织从 `请求(Request)``响应(Response)` 的整个流程,该功能完成基于 [PSR-15](https://www.php-fig.org/psr/psr-15/) 实现。
这里的中间件指的是 "中间件模式",该功能属于 [hyperf/http-server](https://github.com/hyperf/http-server) 组件内的一项主要功能,主要用于编织从 `请求(Request)``响应(Response)` 的整个流程,该功能完成基于 [PSR-15](https://www.php-fig.org/psr/psr-15/) 实现。
## 原理

View File

@ -1,6 +1,6 @@
# NATS
NATS是一个开源、轻量级、高性能的分布式消息中间件实现了高可伸缩性和优雅的 `Publish` / `Subscribe` 模型,使用 `Golang` 语言开发。NATS的开发哲学认为高质量的QoS应该在客户端构建故只建立了 `Request-Reply`,不提供 1.持久化 2.事务处理 3.增强的交付模式 4.企业级队列。
NATS 是一个开源、轻量级、高性能的分布式消息中间件,实现了高可伸缩性和优雅的 `Publish` / `Subscribe` 模型,使用 `Golang` 语言开发。NATS 的开发哲学认为高质量的 QoS 应该在客户端构建,故只建立了 `Request-Reply`,不提供 1. 持久化 2. 事务处理 3. 增强的交付模式 4. 企业级队列。
## 使用

View File

@ -1,7 +1,7 @@
# 分页器
在您需要对数据进行分页处理时,可以借助 [hyperf/paginator](https://github.com/hyperf/paginator) 组件很方便的解决您的问题,您可对您的数据查询进行一定的封装处理,以便更好的使用分页功能,该组件也可用于其它框架上。
通常情况下,您对分页器的需求可能都是存在于数据库查询上,[hyperf/database](https://github.com/hyperf/database) 数据库组件已经与分页器组件进行了结合,您可以在进行数据查询时很方便的调用分页器来实现分页,具体可查阅 [数据库模型-分页](zh/db/paginator.md) 章节。
通常情况下,您对分页器的需求可能都是存在于数据库查询上,[hyperf/database](https://github.com/hyperf/database) 数据库组件已经与分页器组件进行了结合,您可以在进行数据查询时很方便的调用分页器来实现分页,具体可查阅 [数据库模型 - 分页](zh/db/paginator.md) 章节。
# 安装

View File

@ -5,7 +5,7 @@
## 定义访问路由
Hyperf 使用 [nikic/fast-route](https://github.com/nikic/FastRoute) 作为默认的路由组件并提供服务,您可以很方便的在 `config/routes.php` 中定义您的路由。
不仅如此,框架还提供了极其强大和方便灵活的`注解路由`功能,关于路由的详情文档请查阅 [路由](zh/router.md) 章节
不仅如此,框架还提供了极其强大和方便灵活的 `注解路由` 功能,关于路由的详情文档请查阅 [路由](zh/router.md) 章节
### 通过配置文件定义路由
路由的文件位于 [hyperf-skeleton](https://github.com/hyperf/hyperf-skeleton) 项目的 `config/routes.php` ,下面是一些常用的用法示例。
@ -65,8 +65,8 @@ class IndexController
```
### 通过 `@Controller` 注解定义路由
`@Controller` 为满足更细致的路由定义需求而存在,使用 `@Controller` 注解用于表明当前类为一个 `Controller类`,同时需配合 `@RequestMapping` 注解来对请求方法和请求路径进行更详细的定义。
我们也提供了多种快速便捷的 `Mapping注解`,如 `@GetMapping`、`@PostMapping`、`@PutMapping`、`@PatchMapping`、`@DeleteMapping` 5种便捷的注解用于表明允许不同的请求方法。
`@Controller` 为满足更细致的路由定义需求而存在,使用 `@Controller` 注解用于表明当前类为一个 `Controller 类`,同时需配合 `@RequestMapping` 注解来对请求方法和请求路径进行更详细的定义。
我们也提供了多种快速便捷的 `Mapping 注解`,如 `@GetMapping`、`@PostMapping`、`@PutMapping`、`@PatchMapping`、`@DeleteMapping` 5 种便捷的注解用于表明允许不同的请求方法。
> 使用 `@Controller` 注解时需 `use Hyperf\HttpServer\Annotation\Controller;` 命名空间;
> 使用 `@RequestMapping` 注解时需 `use Hyperf\HttpServer\Annotation\RequestMapping;` 命名空间;
@ -106,8 +106,8 @@ class IndexController
## 处理 HTTP 请求
`Hyperf` 是完全开放的,本质上没有规定您必须基于某种模式下去实现请求的处理,您可以采用传统的 `MVC模式`,亦可以采用 `RequestHandler模式` 来进行开发。
我们以 `MVC模式` 来举个例子:
`Hyperf` 是完全开放的,本质上没有规定您必须基于某种模式下去实现请求的处理,您可以采用传统的 `MVC 模式`,亦可以采用 `RequestHandler 模式` 来进行开发。
我们以 `MVC 模式` 来举个例子:
`app` 文件夹内创建一个 `Controller` 文件夹并创建 `IndexController.php` 如下,`index` 方法内从请求中获取了 `id` 参数,并转换为 `字符串` 类型返回到客户端。
```php
@ -216,7 +216,7 @@ class IndexController
通过上面的示例我们不难发现 `$userService` 在没有实例化的情况下, 属性对应的类对象被自动注入了。
不过这里的案例并未真正体现出依赖自动注入的好处及其强大之处,我们假设一下 `UserService` 也存在很多的依赖,而这些依赖同时又存在很多其它的依赖时,`new` 实例化的方式就需要手动实例化很多的对象并调整好对应的参数位,而在 `Hyperf` 里我们就无须手动管理这些依赖,只需要声明一下最终使用的类即可。
而当 `UserService` 需要发生替换等剧烈的内部变化时,比如从一个本地服务替换成了一个 RPC 远程服务,也只需要通过配置调整依赖中 `UserService` 这个键值对应的类为新的RPC服务类即可。
而当 `UserService` 需要发生替换等剧烈的内部变化时,比如从一个本地服务替换成了一个 RPC 远程服务,也只需要通过配置调整依赖中 `UserService` 这个键值对应的类为新的 RPC 服务类即可。
## 启动 Hyperf 服务

View File

@ -53,7 +53,7 @@ vendor/bin/init-proxy.sh && composer test
vendor/bin/init-proxy.sh && php bin/hyperf.php start
```
## PHP7.3下预先生成代理的脚本 执行失败
## PHP7.3 下预先生成代理的脚本 执行失败
`php bin/hyperf.php di:init-proxy` 脚本在 `PHP7.3``Docker` 打包时,会因为返回码是 `1` 而失败。

View File

@ -13,7 +13,7 @@ composer require hyperf/rate-limit
| consume | 1 | 每次请求消耗令牌数 |
| capacity | 2 | 令牌桶最大容量 |
| limitCallback | NULL | 触发限流时回调方法 |
| key | NULL | 生成令牌桶的key |
| key | NULL | 生成令牌桶的 key |
| waitTimeout | 3 | 排队超时时间 |
```php

View File

@ -10,7 +10,7 @@ composer require hyperf/redis
| 配置项 | 类型 | 默认值 | 备注 |
|:--------------:|:-------:|:-----------:|:------------------------------:|
| host | string | 'localhost' | Redis地址 |
| host | string | 'localhost' | Redis 地址 |
| auth | string | 无 | 密码 |
| port | integer | 6379 | 端口 |
| db | integer | 0 | DB |

View File

@ -172,7 +172,7 @@ $name = $request->query();
### 获取 `JSON` 输入信息
如果请求的 `Body` 数据格式是 `JSON`,则只要 `请求对象(Request)``Content-Type` `Header值` 正确设置为 `application/json`,就可以通过 `input(string $key, $default = null)` 方法访问 `JSON` 数据,你甚至可以使用 「点」语法来读取 `JSON` 数组:
如果请求的 `Body` 数据格式是 `JSON`,则只要 `请求对象(Request)``Content-Type` `Header 值` 正确设置为 `application/json`,就可以通过 `input(string $key, $default = null)` 方法访问 `JSON` 数据,你甚至可以使用 「点」语法来读取 `JSON` 数组:
```php
// 存在则返回,不存在则返回 null

View File

@ -117,7 +117,7 @@ class UserController
#### `@Controller` 注解
`@Controller` 为满足更细致的路由定义需求而存在,使用 `@Controller` 注解用于表明当前类为一个 `Controller` 类,同时需配合 `@RequestMapping` 注解来对请求方法和请求路径进行更详细的定义。
我们也提供了多种快速便捷的 `Mapping` 注解,如 `@GetMapping`、`@PostMapping`、`@PutMapping`、`@PatchMapping`、`@DeleteMapping` 5种便捷的注解用于表明允许不同的请求方法。
我们也提供了多种快速便捷的 `Mapping` 注解,如 `@GetMapping`、`@PostMapping`、`@PutMapping`、`@PatchMapping`、`@DeleteMapping` 5 种便捷的注解用于表明允许不同的请求方法。
> 使用 `@Controller` 注解时需 `use Hyperf\HttpServer\Annotation\Controller;` 命名空间;
> 使用 `@RequestMapping` 注解时需 `use Hyperf\HttpServer\Annotation\RequestMapping;` 命名空间;

View File

@ -6,18 +6,18 @@
![snowflake](./imgs/snowflake.jpeg)
- `1位`,不用。
- `1 位`,不用。
- 二进制中最高位为符号位,我们生成的 `ID` 一般都是正整数,所以这个最高位固定是 0。
- `41位`,用来记录时间戳(毫秒)。
- `41位` 可以表示 `2^41 - 1` 个数字。
- 也就是说 `41位` 可以表示 `2^41 - 1` 个毫秒的值,转化成单位年则是 `(2^41 - 1) / (1000 * 60 * 60 * 24 * 365)` 约为 `69` 年。
- `41 位`,用来记录时间戳(毫秒)。
- `41 位` 可以表示 `2^41 - 1` 个数字。
- 也就是说 `41 位` 可以表示 `2^41 - 1` 个毫秒的值,转化成单位年则是 `(2^41 - 1) / (1000 * 60 * 60 * 24 * 365)` 约为 `69` 年。
- `10位`,用来记录工作机器 `ID`
- `10 位`,用来记录工作机器 `ID`
- 可以部署在 `2^10``1024` 个节点,包括 `5``DatacenterId``5``WorkerId`
- `12位`,序列号,用来记录同毫秒内产生的不同 `id`
- `12位` 可以表示的最大正整数是 `2^12 - 1``4095` 个数字,来表示同一机器同一时间截(毫秒)内产生的 `4095``ID` 序号。
- `12 位`,序列号,用来记录同毫秒内产生的不同 `id`
- `12 位` 可以表示的最大正整数是 `2^12 - 1``4095` 个数字,来表示同一机器同一时间截(毫秒)内产生的 `4095``ID` 序号。
`Snowflake` 可以保证:
@ -34,7 +34,7 @@ composer require hyperf/snowflake
## 使用
框架提供了 `MetaGeneratorInterface``IdGeneratorInterface``MetaGeneratorInterface` 会生成 `ID``Meta` 文件,`IdGeneratorInterface` 则会根据对应的 `Meta` 文件生成 `分布式ID`。
框架提供了 `MetaGeneratorInterface``IdGeneratorInterface``MetaGeneratorInterface` 会生成 `ID``Meta` 文件,`IdGeneratorInterface` 则会根据对应的 `Meta` 文件生成 `分布式 ID`。
框架默认使用的 `MetaGeneratorInterface` 是基于 `Redis` 实现的 `毫秒级别生成器`
配置文件位于 `config/autoload/snowflake.php`,如配置文件不存在可通过执行 `php bin/hyperf.php vendor:publish hyperf/snowflake` 命令创建默认配置,配置文件内容如下:

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

@ -16,17 +16,17 @@ Swoole Tracker 能够帮助企业自动分析并汇总统计关键系统调用
- 拥有强大的调试工具链
> 本系统支持远程调试,可在系统后台远程开启检测内存泄漏、阻塞检测、代码性能分析和查看调用栈;也支持手动埋点进行调试,后台统一查看结果
- 同时支持FPM和Swoole
> 完美支持PHP-FPM环境不仅限于在Swoole中使用
- 同时支持 FPM Swoole
> 完美支持 PHP-FPM 环境,不仅限于在 Swoole 中使用
- 完善的系统监控
> 支持完善的系统监控零成本部署监控机器的CPU、内存、网络、磁盘等资源可以很方便的集成到现有报警系统
> 支持完善的系统监控,零成本部署,监控机器的 CPU、内存、网络、磁盘等资源可以很方便的集成到现有报警系统
- 一键安装和零成本接入
> 规避与减小整体投资风险本系统的客户端提供脚本可一键部署服务端可在Docker环境中运行简单快捷
> 规避与减小整体投资风险,本系统的客户端提供脚本可一键部署,服务端可在 Docker 环境中运行,简单快捷
- 提高各部门生产效率
> 在复杂系统中追踪服务及代码层级性能瓶颈帮助IT、开发等部门提升工作效率将重点聚焦在核心工作中
> 在复杂系统中追踪服务及代码层级性能瓶颈,帮助 IT、开发等部门提升工作效率将重点聚焦在核心工作中
## 安装

View File

@ -118,8 +118,8 @@ Swoole 暂时没有协程化的函数列表
> 因为 `MongoDB` 没有办法被 `hook`,所以我们可以通过 `Task` 来调用,下面就简单介绍一下如何通过注解方式调用 `MongoDB`
以下我们实现两个方法 `insert``query`,其中需要注意的是 `manager` 方法不能使用 `Task`
因为 `Task` 会在对应的 `Task进程` 中处理,然后将数据从 `Task进程` 返回到 `Worker进程` 。
所以 `Task方法` 的入参和出参最好不要携带任何 `IO`,比如返回一个实例化后的 `Redis` 等等。
因为 `Task` 会在对应的 `Task 进程` 中处理,然后将数据从 `Task 进程` 返回到 `Worker 进程` 。
所以 `Task 方法` 的入参和出参最好不要携带任何 `IO`,比如返回一个实例化后的 `Redis` 等等。
```php
<?php

View File

@ -142,7 +142,7 @@ class ExampleTest extends TestCase
## 调试代码
在FPM场景下我们通常改完代码然后打开浏览器访问对应接口所以我们通常会需要两个函数 `dd``dump`,但 `Hyperf` 跑在 `CLI` 模式下,就算提供了这两个函数,也需要在 `CLI` 中重启 `Server`,然后再到浏览器中调用对应接口查看结果。这样其实并没有简化流程,反而更麻烦了。
FPM 场景下,我们通常改完代码,然后打开浏览器访问对应接口,所以我们通常会需要两个函数 `dd``dump`,但 `Hyperf` 跑在 `CLI` 模式下,就算提供了这两个函数,也需要在 `CLI` 中重启 `Server`,然后再到浏览器中调用对应接口查看结果。这样其实并没有简化流程,反而更麻烦了。
接下来,我来介绍如何通过配合 `testing`,来快速调试代码,顺便完成单元测试。
@ -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

@ -189,9 +189,9 @@ return [
当我们在使用阿里云的链路追踪服务时,由于对端也是支持 `Zipkin` 的协议的,故可以直接通过在 `condif/autoload/opentracing.php` 配置文件内修改 `endpoint_url` 的值为您对应的阿里云 `region` 的地址,具体地址可在阿里云的链路追踪服务内得到,更多细节可参考 [阿里云链路追踪服务帮助文档](https://help.aliyun.com/document_detail/100031.html?spm=a2c4g.11186623.6.547.68f974dcZlg4Mv)。
### 使用其他Tracer驱动
### 使用其他 Tracer 驱动
您也可以使用其他任意符合OpenTracing协议的Tracer驱动。在Driver项中填写任意实现了`Hyperf\Tracer\Contract\NamedFactoryInterface`的类就可以了。该接口只有一个make函数参数为驱动名称需返回一个实现了OpenTracing\Tracer的实例。
您也可以使用其他任意符合 OpenTracing 协议的 Tracer 驱动。在 Driver 项中,填写任意实现了 `Hyperf\Tracer\Contract\NamedFactoryInterface` 的类就可以了。该接口只有一个 make 函数,参数为驱动名称,需返回一个实现了 OpenTracing\Tracer 的实例。
## Reference

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 翻译
@ -82,7 +108,7 @@ class FooController
## 通过全局函数翻译
您也可以通过全局函数 `__()``trans()` 来对字符串进行翻译。
函数的第一个参数使用 `键`(指使用翻译字符串作为键的键) 或者是 `文件.键` 的形式。
函数的第一个参数使用 `键`(指使用翻译字符串作为键的键) 或者是 `文件. 键` 的形式。
```php
echo __('messages.welcome');
@ -135,4 +161,4 @@ echo __('messages.welcome', ['name' => 'Hyperf']);
echo trans_choice('messages.apples', 10);
```
当然除了全局函数 `trans_choice()`,您也可以使用 `Hyperf\Contract\TranslatorInterface``transChoice` 方法。
当然除了全局函数 `trans_choice()`,您也可以使用 `Hyperf\Contract\TranslatorInterface``transChoice` 方法。

View File

@ -1,6 +1,6 @@
# 阿里云日志服务
`Docker集群` 部署项目时,收集日志会是一个比较麻烦的问题,但阿里云提供了十分好用的 `日志收集系统`,本篇文档就是简略介绍一下阿里云日志收集的使用方法。
`Docker 集群` 部署项目时,收集日志会是一个比较麻烦的问题,但阿里云提供了十分好用的 `日志收集系统`,本篇文档就是简略介绍一下阿里云日志收集的使用方法。
* [Docker Swarm 集群搭建](zh/tutorial/docker-swarm.md)
@ -18,8 +18,8 @@
| 参数 | 说明 |
|:-------------------------------------:|:------------------------------------------:|
| ${your_region_name} | 区域ID 比如华东1区域是 cn-hangzhou |
| ${your_aliyun_user_id} | 用户标识请替换为您的阿里云主账号用户ID。 |
| ${your_region_name} | 区域 ID 比如华东 1 区域是 cn-hangzhou |
| ${your_aliyun_user_id} | 用户标识,请替换为您的阿里云主账号用户 ID。 |
| ${your_machine_group_user_defined_id} | 集群的机器组自定义标识 以下使用 Hyperf |
```
@ -38,11 +38,11 @@ registry.cn-hangzhou.aliyuncs.com/log-service/logtail
| 参数 | 填写示例 |
|:------------:|:----------------:|
| Project名称 | hyperf |
| Project 名称 | hyperf |
| 注释 | 用于日志系统演示 |
| 所属区域 | 华东1杭州 |
| 所属区域 | 华东 1杭州 |
| 开通服务 | 详细日志 |
| 日志存储位置 | 当前Project |
| 日志存储位置 | 当前 Project |
### 创建 Logstore
@ -50,7 +50,7 @@ registry.cn-hangzhou.aliyuncs.com/log-service/logtail
| 参数 | 填写示例 |
|:------------:|:---------------:|
| Logstore名称 | hyperf-demo-api |
| Logstore 名称 | hyperf-demo-api |
| 永久保存 | false |
| 数据保存时间 | 60 |
@ -80,7 +80,7 @@ registry.cn-hangzhou.aliyuncs.com/log-service/logtail
|:--------------:|:-------------------------------------------------:|:---------------:|
| 配置名称 | hyperf-demo-api | |
| 日志路径 | /opt/www/runtime/logs | *.log |
| Label白名单 | app.name | hyperf-demo-api |
| Label 白名单 | app.name | hyperf-demo-api |
| 模式 | 完整正则模式 | |
| 单行模式 | false | |
| 日志样例 | `[2019-03-07 11:58:57] hyperf.WARNING: xxx` | |

View File

@ -14,7 +14,7 @@
首先我们需要在 `项目` 里新建一个项目。DaoCloud 支持多种镜像仓库,这个可以按需选择。
这里我以 [hyperf-demo](https://github.com/limingxinleo/hyperf-demo) 仓库为例配置。当创建成功后,在对应 `Github仓库` 的 `WebHooks` 下面就会有对应的url。
这里我以 [hyperf-demo](https://github.com/limingxinleo/hyperf-demo) 仓库为例配置。当创建成功后,在对应 `Github 仓库` 的 `WebHooks` 下面就会有对应的 url。
接下来我们修改一下仓库里的 `Dockerfile`,在 `apk add` 下面增加 `&& apk add wget \`。这里具体原因不是很清楚,如果不更新 `wget`, 使用时就会有问题。但是自建 Gitlab CI 就没有任何问题。

View File

@ -1,6 +1,6 @@
# Docker Swarm 集群搭建
现阶段Docker容器技术已经相当成熟就算是中小型公司也可以基于 Gitlab、Aliyun镜像服务、Docker Swarm 轻松搭建自己的 Docker集群服务。
现阶段Docker 容器技术已经相当成熟,就算是中小型公司也可以基于 Gitlab、Aliyun 镜像服务、Docker Swarm 轻松搭建自己的 Docker 集群服务。
## 安装 Docker
@ -14,9 +14,9 @@ curl -sSL https://get.daocloud.io/docker | sh
ExecStart=/usr/bin/dockerd -H unix:// -H tcp://0.0.0.0:2375
```
## 搭建自己的Gitlab
## 搭建自己的 Gitlab
### 安装Gitlab
### 安装 Gitlab
首先我们修改一下端口号,把 `sshd` 服务的 `22` 端口改为 `2222`,让 `gitlab` 可以使用 `22` 端口。
@ -49,7 +49,7 @@ gitlab/gitlab-ce:latest
首次登录 `Gitlab` 会重置密码,用户名是 `root`
### 安装gitlab-runner
### 安装 gitlab-runner
[官方地址](https://docs.gitlab.com/runner/install/linux-repository.html)
@ -150,7 +150,7 @@ docker service create \
portainer/portainer
```
## 创建一个Demo项目
## 创建一个 Demo 项目
登录 Gitlab 创建一个 Demo 项目。并导入我们的项目 [hyperf-skeleton](https://github.com/hyperf/hyperf-skeleton)
@ -212,7 +212,7 @@ networks:
external: true
```
然后在我们的 portainer 中,创建对应的 Config demo_v1.0。当然以下参数需要根据实际情况调整因为我们的Demo中没有任何IO操作所以填默认的即可。
然后在我们的 portainer 中,创建对应的 Config demo_v1.0。当然,以下参数需要根据实际情况调整,因为我们的 Demo 中,没有任何 IO 操作,所以填默认的即可。
```
APP_NAME=demo
@ -233,7 +233,7 @@ REDIS_PORT=6379
REDIS_DB=0
```
因为我们配置的 gitlab-ci.yml 会检测 test 分支和 tags所以我们把修改的内容合并到test分支然后推到gitlab上。
因为我们配置的 gitlab-ci.yml 会检测 test 分支和 tags所以我们把修改的内容合并到 test 分支,然后推到 gitlab 上。
接下来我们就可以访问集群任意一台机器的 9501 端口。进行测试了
@ -243,8 +243,8 @@ curl http://127.0.0.1:9501/
## 安装 KONG 网关
通常情况下Swarm集群是不会直接对外的所以我们这里推荐使用 `KONG` 作为网关。
还有另外一个原因,那就是 `Swarm``Ingress网络` 设计上有缺陷,所以在连接不复用的情况下,会有并发瓶颈,具体请查看对应 `Issue` [#35082](https://github.com/moby/moby/issues/35082)
通常情况下Swarm 集群是不会直接对外的,所以我们这里推荐使用 `KONG` 作为网关。
还有另外一个原因,那就是 `Swarm``Ingress 网络` 设计上有缺陷,所以在连接不复用的情况下,会有并发瓶颈,具体请查看对应 `Issue` [#35082](https://github.com/moby/moby/issues/35082)
`KONG` 作为网关,默认情况下就会复用后端的连接,所以会极大减缓上述问题。
### 安装数据库

View File

@ -432,7 +432,7 @@ if ($errors->has('foo')) {
##### date_format:format
验证字段必须匹配指定格式,可以使用 PHP 函数date 或 date_format 验证该字段。
验证字段必须匹配指定格式,可以使用 PHP 函数 date 或 date_format 验证该字段。
##### different:field
@ -573,19 +573,19 @@ $validator = $this->validationFactory->make($data, [
##### ip
验证字段必须是IP地址。
验证字段必须是 IP 地址。
##### ipv4
验证字段必须是IPv4地址。
验证字段必须是 IPv4 地址。
##### ipv6
验证字段必须是IPv6地址。
验证字段必须是 IPv6 地址。
##### json
验证字段必须是有效的JSON字符串
验证字段必须是有效的 JSON 字符串
##### lt:field

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,8 +84,7 @@ abstract class AbstractLazyProxyBuilder
public function getNode(): Node
{
if ($this->namespace){
if ($this->namespace) {
return $this->factory
->namespace($this->namespace)
->addStmt($this->builder)

View File

@ -12,16 +12,12 @@ 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
{
if (strpos($this->originalClassName, '\\') !== 0) {
$originalClassName = '\\'.$this->originalClassName;
$originalClassName = '\\' . $this->originalClassName;
} else {
$originalClassName = $this->originalClassName;
}

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