diff --git a/.gitignore b/.gitignore index f6f2b9d5c..da8f9b4a3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .idea/ .git/ runtime/ +codeCoverage/ vendor/ .phpintel/ .env diff --git a/.travis.yml b/.travis.yml index c6a7ccca1..8f341ff1b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index f4644424d..5ba1afaa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/bin/md-format b/bin/md-format new file mode 100755 index 000000000..4b804dc9c --- /dev/null +++ b/bin/md-format @@ -0,0 +1,91 @@ +#!/usr/bin/env php +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; +} \ No newline at end of file diff --git a/composer.json b/composer.json index b5f570d86..28288752c 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/doc/zh/README.md b/doc/zh/README.md index b878eda7b..2050cfe12 100644 --- a/doc/zh/README.md +++ b/doc/zh/README.md @@ -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 表单验证器` 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。 diff --git a/doc/zh/amqp.md b/doc/zh/amqp.md index 03aa8741f..da2cf6a56 100644 --- a/doc/zh/amqp.md +++ b/doc/zh/amqp.md @@ -1,4 +1,4 @@ -# AMQP组件 +# AMQP 组件 [hyperf/amqp](https://github.com/hyperf/amqp) 是实现 AMQP 标准的组件,主要适用于对 RabbitMQ 的使用。 diff --git a/doc/zh/annotation.md b/doc/zh/annotation.md index 92efe6d7c..932ed22d1 100644 --- a/doc/zh/annotation.md +++ b/doc/zh/annotation.md @@ -42,7 +42,7 @@ return [ ## 使用注解 -注解一共有3种应用对象,分别是 `类`、`类方法` 和 `类属性`。 +注解一共有 3 种应用对象,分别是 `类`、`类方法` 和 `类属性`。 ### 使用类注解 diff --git a/doc/zh/async-queue.md b/doc/zh/async-queue.md index 973ddced7..a0dfadfd4 100644 --- a/doc/zh/async-queue.md +++ b/doc/zh/async-queue.md @@ -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 | 消费进程数 | diff --git a/doc/zh/awesome-components.md b/doc/zh/awesome-components.md index d2d411157..6c7a7c139 100644 --- a/doc/zh/awesome-components.md +++ b/doc/zh/awesome-components.md @@ -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 协程的第三方登录授权 SDK(QQ、微信、微博、Github、Gitee等) +- [yurunsoft/yurun-oauth-login](https://github.com/Yurunsoft/YurunOAuthLogin) 支持 Swoole 协程的第三方登录授权 SDK(QQ、微信、微博、Github、Gitee 等) - [overtrue/wechat](zh/sdks/wechat) EasyWeChat,一个流行的非官方微信 SDK - [Yurunsoft/PHPMailer-Swoole](https://github.com/Yurunsoft/PHPMailer-Swoole) Swoole 协程环境下的可用的 PHPMailer diff --git a/doc/zh/cache.md b/doc/zh/cache.md index 26bfc832e..b9ae84edb 100644 --- a/doc/zh/cache.md +++ b/doc/zh/cache.md @@ -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` 中进行查询。 diff --git a/doc/zh/changelog.md b/doc/zh/changelog.md index a126a0288..09b172619 100644 --- a/doc/zh/changelog.md +++ b/doc/zh/changelog.md @@ -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` 属性的值与继承的父类一致,那么创建的模型类将不会包含此属性; ## 移除 diff --git a/doc/zh/circuit-breaker.md b/doc/zh/circuit-breaker.md index aa677a57b..2a610770c 100644 --- a/doc/zh/circuit-breaker.md +++ b/doc/zh/circuit-breaker.md @@ -49,7 +49,7 @@ class UserService ``` -默认熔断策略为`超时策略`,如果您想要自己实现熔断策略,只需要自己实现 `Handler` 继承于 `Hyperf\CircuitBreaker\Handler\AbstractHandler` 即可。 +默认熔断策略为 `超时策略` ,如果您想要自己实现熔断策略,只需要自己实现 `Handler` 继承于 `Hyperf\CircuitBreaker\Handler\AbstractHandler` 即可。 ```php mapping 为 `Etcd` 与 `Config` 的映射关系。映射中不存在的 `key`,则不会被同步到 `Config` 中。 diff --git a/doc/zh/coroutine.md b/doc/zh/coroutine.md index 2d4df836f..242769e28 100644 --- a/doc/zh/coroutine.md +++ b/doc/zh/coroutine.md @@ -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 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 压测对比 database 1800qps,db 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 +get(DB::class); + +$res = $db->query('SELECT * FROM `user` WHERE gender = ?;',[1]); + +``` + +### 使用静态方法 + +```php + 模型缓存暂支持 `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 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 +where('gender', '>', 1)->delete(); +``` diff --git a/doc/zh/db/model.md b/doc/zh/db/model.md index d599af1fd..bbd1ad168 100644 --- a/doc/zh/db/model.md +++ b/doc/zh/db/model.md @@ -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(); ``` -## 插入&更新模型 +## 插入 & 更新模型 ### 插入 diff --git a/doc/zh/db/querybuilder.md b/doc/zh/db/querybuilder.md index e697c283f..ca8e6594d 100644 --- a/doc/zh/db/querybuilder.md +++ b/doc/zh/db/querybuilder.md @@ -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 addSelect('age')->get(); ## 原始表达式 -有时你需要在查询中使用原始表达式,例如实现 `COUNT(0) AS count`,这就需要用到`raw`方法。 +有时你需要在查询中使用原始表达式,例如实现 `COUNT(0) AS count`,这就需要用到 `raw` 方法。 ```php use Hyperf\DbConnection\Db; diff --git a/doc/zh/db/quick-start.md b/doc/zh/db/quick-start.md index a42a4a64c..c0793a763 100644 --- a/doc/zh/db/quick-start.md +++ b/doc/zh/db/quick-start.md @@ -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 | 数据库编码 | diff --git a/doc/zh/di.md b/doc/zh/di.md index d3b62c6c2..638552070 100644 --- a/doc/zh/di.md +++ b/doc/zh/di.md @@ -300,6 +300,53 @@ return [ > 当然在该场景中可以通过 `@Value` 注解来更便捷的注入配置而无需构建工厂类,此仅为举例 +### 懒加载 + +Hyperf的长生命周期依赖注入在项目启动时完成。这意味着长生命周期的类需要注意: + +* 构造函数时还不是协程环境,如果注入了可能会触发协程切换的类,就会导致框架启动失败。 + +* 构造函数中要避免循坏依赖(比较典型的例子为 `Listener` 和 `EventDispatcherInterface`),不然也会启动失败。 + +目前解决方案是:只在实例中注入 `ContainerInterface` ,而其他的组件在非构造函数执行时通过 `container` 获取。PSR-11中指出: + +> 「用户不应该将容器作为参数传入对象然后在对象中通过容器获得对象的依赖。这样是把容器当作服务定位器来使用,而服务定位器是一种反模式」 + +也就是说这样的做法虽然有效,但是从设计模式角度来说并不推荐。 + +另一个方案是使用PHP中常用的惰性代理模式,注入一个代理对象,在使用时再实例化目标对象。Hyperf DI组件设计了基于类型提示(TypeHint)的懒加载注入功能。 + +添加 `config/autoload/lazy_loader.php` 文件并绑定懒加载关系: + +```php + \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 stopPropagation()` 则不再往后传递,若最后一个配置的异常处理器仍不对该异常进行捕获处理,那么就会交由 Hyperf 的默认异常处理器处理了。 + +## Error 监听器 + +框架提供了 `error_reporting()` 错误级别的监听器 `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`。 + +### 配置 + +在 `config/autoload/listeners.php` 中添加监听器 + +```php +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 +``` + diff --git a/doc/zh/guzzle.md b/doc/zh/guzzle.md index d3d5df630..4e8b8dbf9 100644 --- a/doc/zh/guzzle.md +++ b/doc/zh/guzzle.md @@ -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 连接进行复用。 ### 使用 diff --git a/doc/zh/json-rpc.md b/doc/zh/json-rpc.md index 47f5f4131..2d906dc9b 100644 --- a/doc/zh/json-rpc.md +++ b/doc/zh/json-rpc.md @@ -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[]` 对象数组。 diff --git a/doc/zh/metric.md b/doc/zh/metric.md index eb962140b..d313dfe3e 100644 --- a/doc/zh/metric.md +++ b/doc/zh/metric.md @@ -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 本节只适用于 Prometheus 驱动 + +当您在使用 Prometheus 的 Histogram 时,有时会有自定义 Bucket 的需求。您可以在服务启动前,依赖注入 Registry 并自行注册 Histogram ,设置所需 Bucket 。稍后使用时 `MetricFactory` 就会调用您注册好同名 Histogram 。示例如下: + +```php +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 了。 \ No newline at end of file diff --git a/doc/zh/middleware/middleware.md b/doc/zh/middleware/middleware.md index 9a0b0a06e..415d305c1 100644 --- a/doc/zh/middleware/middleware.md +++ b/doc/zh/middleware/middleware.md @@ -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/) 实现。 ## 原理 diff --git a/doc/zh/nats.md b/doc/zh/nats.md index 2d4ab59ba..2c850937f 100644 --- a/doc/zh/nats.md +++ b/doc/zh/nats.md @@ -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. 企业级队列。 ## 使用 diff --git a/doc/zh/paginator.md b/doc/zh/paginator.md index f10d56ab0..a287edf1e 100644 --- a/doc/zh/paginator.md +++ b/doc/zh/paginator.md @@ -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) 章节。 # 安装 diff --git a/doc/zh/quick-start/overview.md b/doc/zh/quick-start/overview.md index 15cfe195c..cc5138d9b 100644 --- a/doc/zh/quick-start/overview.md +++ b/doc/zh/quick-start/overview.md @@ -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 服务 diff --git a/doc/zh/quick-start/questions.md b/doc/zh/quick-start/questions.md index 9c8872bec..487e42340 100644 --- a/doc/zh/quick-start/questions.md +++ b/doc/zh/quick-start/questions.md @@ -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` 而失败。 diff --git a/doc/zh/rate-limit.md b/doc/zh/rate-limit.md index 3584dd133..f1196778c 100644 --- a/doc/zh/rate-limit.md +++ b/doc/zh/rate-limit.md @@ -13,7 +13,7 @@ composer require hyperf/rate-limit | consume | 1 | 每次请求消耗令牌数 | | capacity | 2 | 令牌桶最大容量 | | limitCallback | NULL | 触发限流时回调方法 | -| key | NULL | 生成令牌桶的key | +| key | NULL | 生成令牌桶的 key | | waitTimeout | 3 | 排队超时时间 | ```php diff --git a/doc/zh/redis.md b/doc/zh/redis.md index ae94c3c6c..8d3d52d06 100644 --- a/doc/zh/redis.md +++ b/doc/zh/redis.md @@ -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 | diff --git a/doc/zh/request.md b/doc/zh/request.md index 3247cf773..9ff2d78b8 100644 --- a/doc/zh/request.md +++ b/doc/zh/request.md @@ -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 diff --git a/doc/zh/router.md b/doc/zh/router.md index 08f4609fa..33cfc3a86 100644 --- a/doc/zh/router.md +++ b/doc/zh/router.md @@ -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;` 命名空间; diff --git a/doc/zh/snowflake.md b/doc/zh/snowflake.md index 796b93bf0..17a3ef331 100644 --- a/doc/zh/snowflake.md +++ b/doc/zh/snowflake.md @@ -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` 命令创建默认配置,配置文件内容如下: diff --git a/doc/zh/summary.md b/doc/zh/summary.md index 90d03b73b..9a4f8180f 100644 --- a/doc/zh/summary.md +++ b/doc/zh/summary.md @@ -49,6 +49,7 @@ * [模型事件](zh/db/event.md) * [模型缓存](zh/db/model-cache.md) * [数据库迁移](zh/db/migration.md) + * [极简的 DB 组件](zh/db/db.md) * 微服务 diff --git a/doc/zh/swoole-tracker.md b/doc/zh/swoole-tracker.md index b94e6dc85..cbd3ca1f0 100644 --- a/doc/zh/swoole-tracker.md +++ b/doc/zh/swoole-tracker.md @@ -16,17 +16,17 @@ Swoole Tracker 能够帮助企业自动分析并汇总统计关键系统调用 - 拥有强大的调试工具链 > 本系统支持远程调试,可在系统后台远程开启检测内存泄漏、阻塞检测、代码性能分析和查看调用栈;也支持手动埋点进行调试,后台统一查看结果 -- 同时支持FPM和Swoole -> 完美支持PHP-FPM环境,不仅限于在Swoole中使用 +- 同时支持 FPM 和 Swoole +> 完美支持 PHP-FPM 环境,不仅限于在 Swoole 中使用 - 完善的系统监控 -> 支持完善的系统监控,零成本部署,监控机器的CPU、内存、网络、磁盘等资源,可以很方便的集成到现有报警系统 +> 支持完善的系统监控,零成本部署,监控机器的 CPU、内存、网络、磁盘等资源,可以很方便的集成到现有报警系统 - 一键安装和零成本接入 -> 规避与减小整体投资风险,本系统的客户端提供脚本可一键部署,服务端可在Docker环境中运行,简单快捷 +> 规避与减小整体投资风险,本系统的客户端提供脚本可一键部署,服务端可在 Docker 环境中运行,简单快捷 - 提高各部门生产效率 -> 在复杂系统中追踪服务及代码层级性能瓶颈,帮助IT、开发等部门提升工作效率,将重点聚焦在核心工作中 +> 在复杂系统中追踪服务及代码层级性能瓶颈,帮助 IT、开发等部门提升工作效率,将重点聚焦在核心工作中 ## 安装 diff --git a/doc/zh/task.md b/doc/zh/task.md index b0fb58744..8569b9df8 100644 --- a/doc/zh/task.md +++ b/doc/zh/task.md @@ -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 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` 方法。 \ No newline at end of file +当然除了全局函数 `trans_choice()`,您也可以使用 `Hyperf\Contract\TranslatorInterface` 的 `transChoice` 方法。 diff --git a/doc/zh/tutorial/aliyun-logger.md b/doc/zh/tutorial/aliyun-logger.md index 79e82cd1a..c2f274883 100644 --- a/doc/zh/tutorial/aliyun-logger.md +++ b/doc/zh/tutorial/aliyun-logger.md @@ -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` | | diff --git a/doc/zh/tutorial/daocloud.md b/doc/zh/tutorial/daocloud.md index 66bcec175..9d1a6496a 100644 --- a/doc/zh/tutorial/daocloud.md +++ b/doc/zh/tutorial/daocloud.md @@ -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 就没有任何问题。 diff --git a/doc/zh/tutorial/docker-swarm.md b/doc/zh/tutorial/docker-swarm.md index 9628761f6..2b35dd189 100644 --- a/doc/zh/tutorial/docker-swarm.md +++ b/doc/zh/tutorial/docker-swarm.md @@ -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` 作为网关,默认情况下就会复用后端的连接,所以会极大减缓上述问题。 ### 安装数据库 diff --git a/doc/zh/validation.md b/doc/zh/validation.md index 213749dce..136cf60c4 100644 --- a/doc/zh/validation.md +++ b/doc/zh/validation.md @@ -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 diff --git a/doc/zh/view.md b/doc/zh/view.md index 0e031bd4c..a9f09bcb8 100644 --- a/doc/zh/view.md +++ b/doc/zh/view.md @@ -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` 接口。 diff --git a/phpunit.xml b/phpunit.xml index f66cb0e1b..00b7a3792 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -14,9 +14,11 @@ ./src/async-queue/tests ./src/cache/tests ./src/config/tests + ./src/config-zookeeper/tests ./src/constants/tests ./src/consul/tests ./src/database/tests + ./src/db/tests ./src/db-connection/tests ./src/di/tests ./src/dispatcher/tests @@ -40,6 +42,7 @@ ./src/rpc/tests ./src/server/tests ./src/service-governance/tests + ./src/session/tests ./src/snowflake/tests ./src/socket/tests ./src/task/tests @@ -53,7 +56,35 @@ - ./src/database + ./src/amqp/src + ./src/async-queue/src + ./src/cache/src + ./src/config/src + ./src/constants/src + ./src/consul/src + ./src/database/src + ./src/db-connection/src + ./src/di/src + ./src/dispatcher/src + ./src/elasticsearch/src + ./src/event/src + ./src/grpc-client/src + ./src/guzzle/src + ./src/http-message/src + ./src/http-server/src + ./src/json-rpc/src + ./src/logger/src + ./src/model-cache/src + ./src/paginator/src + ./src/redis/src + ./src/rpc/src + ./src/server/src + ./src/service-governance/src + ./src/session/src + ./src/snowflake/src + ./src/task/src + ./src/utils/src + ./src/websocket-client/src diff --git a/src/config-apollo/publish/apollo.php b/src/config-apollo/publish/apollo.php index 9d0298b02..a92e08686 100644 --- a/src/config-apollo/publish/apollo.php +++ b/src/config-apollo/publish/apollo.php @@ -19,4 +19,5 @@ return [ 'application', ], 'interval' => 5, + 'strict_mode' => false, ]; diff --git a/src/config-apollo/src/Listener/OnPipeMessageListener.php b/src/config-apollo/src/Listener/OnPipeMessageListener.php index 213a82fbc..7f49145ca 100644 --- a/src/config-apollo/src/Listener/OnPipeMessageListener.php +++ b/src/config-apollo/src/Listener/OnPipeMessageListener.php @@ -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; + } + } diff --git a/src/config-zookeeper/.gitattributes b/src/config-zookeeper/.gitattributes new file mode 100644 index 000000000..bdd4ea29c --- /dev/null +++ b/src/config-zookeeper/.gitattributes @@ -0,0 +1 @@ +/tests export-ignore \ No newline at end of file diff --git a/src/config-zookeeper/LICENSE b/src/config-zookeeper/LICENSE new file mode 100644 index 000000000..c35d3f5a8 --- /dev/null +++ b/src/config-zookeeper/LICENSE @@ -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. diff --git a/src/config-zookeeper/composer.json b/src/config-zookeeper/composer.json new file mode 100644 index 000000000..37c610ff7 --- /dev/null +++ b/src/config-zookeeper/composer.json @@ -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" + } +} diff --git a/src/config-zookeeper/publish/zookeeper.php b/src/config-zookeeper/publish/zookeeper.php new file mode 100644 index 000000000..8c3d32510 --- /dev/null +++ b/src/config-zookeeper/publish/zookeeper.php @@ -0,0 +1,18 @@ + false, + 'interval' => 5, + 'server' => env('ZOOKEEPER_SERVER', '127.0.0.1:2181'), + 'path' => env('ZOOKEEPER_CONFIG_PATH', '/conf'), +]; diff --git a/src/config-zookeeper/src/Client.php b/src/config-zookeeper/src/Client.php new file mode 100644 index 000000000..f9b92bd7a --- /dev/null +++ b/src/config-zookeeper/src/Client.php @@ -0,0 +1,38 @@ +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); + } +} diff --git a/src/config-zookeeper/src/ClientInterface.php b/src/config-zookeeper/src/ClientInterface.php new file mode 100644 index 000000000..3b4160b84 --- /dev/null +++ b/src/config-zookeeper/src/ClientInterface.php @@ -0,0 +1,21 @@ + [ + 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', + ], + ], + ]; + } +} diff --git a/src/config-zookeeper/src/Listener/OnPipeMessageListener.php b/src/config-zookeeper/src/Listener/OnPipeMessageListener.php new file mode 100644 index 000000000..e689f74f9 --- /dev/null +++ b/src/config-zookeeper/src/Listener/OnPipeMessageListener.php @@ -0,0 +1,66 @@ +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)); + } + } + } +} diff --git a/src/config-zookeeper/src/PipeMessage.php b/src/config-zookeeper/src/PipeMessage.php new file mode 100644 index 000000000..dc1469b1e --- /dev/null +++ b/src/config-zookeeper/src/PipeMessage.php @@ -0,0 +1,26 @@ +data = $data; + } +} diff --git a/src/config-zookeeper/src/Process/ConfigFetcherProcess.php b/src/config-zookeeper/src/Process/ConfigFetcherProcess.php new file mode 100644 index 000000000..37148c6f5 --- /dev/null +++ b/src/config-zookeeper/src/Process/ConfigFetcherProcess.php @@ -0,0 +1,94 @@ +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)); + } + } + } +} diff --git a/src/config-zookeeper/tests/ClientTest.php b/src/config-zookeeper/tests/ClientTest.php new file mode 100644 index 000000000..2736bd521 --- /dev/null +++ b/src/config-zookeeper/tests/ClientTest.php @@ -0,0 +1,86 @@ +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; + } +} diff --git a/src/config-zookeeper/tests/Stub/Server.php b/src/config-zookeeper/tests/Stub/Server.php new file mode 100644 index 000000000..6efab9d08 --- /dev/null +++ b/src/config-zookeeper/tests/Stub/Server.php @@ -0,0 +1,20 @@ +onOneServer; + } + + public function setOnOneServer(bool $onOneServer): Crontab + { + $this->onOneServer = $onOneServer; + return $this; + } + public function getCallback() { return $this->callback; diff --git a/src/crontab/src/Listener/CrontabRegisterListener.php b/src/crontab/src/Listener/CrontabRegisterListener.php index ecf50e75f..0b9bdec98 100644 --- a/src/crontab/src/Listener/CrontabRegisterListener.php +++ b/src/crontab/src/Listener/CrontabRegisterListener.php @@ -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; diff --git a/src/crontab/src/Mutex/RedisServerMutex.php b/src/crontab/src/Mutex/RedisServerMutex.php new file mode 100644 index 000000000..3dd59afc0 --- /dev/null +++ b/src/crontab/src/Mutex/RedisServerMutex.php @@ -0,0 +1,86 @@ +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; + } +} diff --git a/src/crontab/src/Mutex/ServerMutex.php b/src/crontab/src/Mutex/ServerMutex.php new file mode 100644 index 000000000..cd98551fb --- /dev/null +++ b/src/crontab/src/Mutex/ServerMutex.php @@ -0,0 +1,28 @@ +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; + } } diff --git a/src/db-connection/publish/databases.php b/src/db-connection/publish/databases.php index 445aceaeb..dec8fc91a 100644 --- a/src/db-connection/publish/databases.php +++ b/src/db-connection/publish/databases.php @@ -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', ''), diff --git a/src/db/.gitattributes b/src/db/.gitattributes new file mode 100644 index 000000000..bdd4ea29c --- /dev/null +++ b/src/db/.gitattributes @@ -0,0 +1 @@ +/tests export-ignore \ No newline at end of file diff --git a/src/db/LICENSE b/src/db/LICENSE new file mode 100644 index 000000000..c35d3f5a8 --- /dev/null +++ b/src/db/LICENSE @@ -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. diff --git a/src/db/composer.json b/src/db/composer.json new file mode 100644 index 000000000..7e3e596cf --- /dev/null +++ b/src/db/composer.json @@ -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" + } + } +} diff --git a/src/db/publish/db.php b/src/db/publish/db.php new file mode 100644 index 000000000..1025b891f --- /dev/null +++ b/src/db/publish/db.php @@ -0,0 +1,40 @@ + [ + '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, + ], + ], +]; diff --git a/src/db/src/AbstractConnection.php b/src/db/src/AbstractConnection.php new file mode 100644 index 000000000..19c09d9bd --- /dev/null +++ b/src/db/src/AbstractConnection.php @@ -0,0 +1,75 @@ +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; + } +} diff --git a/src/db/src/ConfigProvider.php b/src/db/src/ConfigProvider.php new file mode 100644 index 000000000..64ada6636 --- /dev/null +++ b/src/db/src/ConfigProvider.php @@ -0,0 +1,41 @@ + [ + ], + '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', + ], + ], + ]; + } +} diff --git a/src/db/src/ConnectionInterface.php b/src/db/src/ConnectionInterface.php new file mode 100644 index 000000000..a903fc667 --- /dev/null +++ b/src/db/src/ConnectionInterface.php @@ -0,0 +1,64 @@ +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); + } +} diff --git a/src/db/src/DetectsLostConnections.php b/src/db/src/DetectsLostConnections.php new file mode 100644 index 000000000..9ff692825 --- /dev/null +++ b/src/db/src/DetectsLostConnections.php @@ -0,0 +1,49 @@ +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', + ]); + } +} diff --git a/src/db/src/Exception/DriverNotFoundException.php b/src/db/src/Exception/DriverNotFoundException.php new file mode 100644 index 000000000..9927ce738 --- /dev/null +++ b/src/db/src/Exception/DriverNotFoundException.php @@ -0,0 +1,17 @@ +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; + } +} diff --git a/src/db/src/MySQLConnection.php b/src/db/src/MySQLConnection.php new file mode 100644 index 000000000..489107464 --- /dev/null +++ b/src/db/src/MySQLConnection.php @@ -0,0 +1,167 @@ + '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; + } +} diff --git a/src/db/src/PDOConnection.php b/src/db/src/PDOConnection.php new file mode 100644 index 000000000..50f00fa56 --- /dev/null +++ b/src/db/src/PDOConnection.php @@ -0,0 +1,176 @@ + '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 + ); + } + } +} diff --git a/src/db/src/Pool/MySQLPool.php b/src/db/src/Pool/MySQLPool.php new file mode 100644 index 000000000..f62b85ce6 --- /dev/null +++ b/src/db/src/Pool/MySQLPool.php @@ -0,0 +1,24 @@ +container, $this, $this->config); + } +} diff --git a/src/db/src/Pool/PDOPool.php b/src/db/src/Pool/PDOPool.php new file mode 100644 index 000000000..f0ccb7462 --- /dev/null +++ b/src/db/src/Pool/PDOPool.php @@ -0,0 +1,24 @@ +container, $this, $this->config); + } +} diff --git a/src/db/src/Pool/Pool.php b/src/db/src/Pool/Pool.php new file mode 100644 index 000000000..aece43f4b --- /dev/null +++ b/src/db/src/Pool/Pool.php @@ -0,0 +1,58 @@ +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; + } +} diff --git a/src/db/src/Pool/PoolFactory.php b/src/db/src/Pool/PoolFactory.php new file mode 100644 index 000000000..e1dbb7d47 --- /dev/null +++ b/src/db/src/Pool/PoolFactory.php @@ -0,0 +1,60 @@ +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)); + } +} diff --git a/src/db/tests/Cases/AbstractTestCase.php b/src/db/tests/Cases/AbstractTestCase.php new file mode 100644 index 000000000..cc6417d87 --- /dev/null +++ b/src/db/tests/Cases/AbstractTestCase.php @@ -0,0 +1,79 @@ +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; + } +} diff --git a/src/db/tests/Cases/MySQLDriverTest.php b/src/db/tests/Cases/MySQLDriverTest.php new file mode 100644 index 000000000..4f20bab90 --- /dev/null +++ b/src/db/tests/Cases/MySQLDriverTest.php @@ -0,0 +1,52 @@ +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']); + } +} diff --git a/src/db/tests/bootstrap.php b/src/db/tests/bootstrap.php new file mode 100644 index 000000000..3d1876ee6 --- /dev/null +++ b/src/db/tests/bootstrap.php @@ -0,0 +1,13 @@ +createAopProxies(); + Timer::clearAll(); + $this->output->writeln('Proxy class create success.'); } diff --git a/src/di/src/Container.php b/src/di/src/Container.php index 3fd98c376..ec6d17936 100644 --- a/src/di/src/Container.php +++ b/src/di/src/Container.php @@ -54,7 +54,6 @@ class Container implements ContainerInterface /** * Container constructor. - * @param Definition\DefinitionSourceInterface $definitionSource */ public function __construct(Definition\DefinitionSourceInterface $definitionSource) { diff --git a/src/di/src/LazyLoader/AbstractLazyProxyBuilder.php b/src/di/src/LazyLoader/AbstractLazyProxyBuilder.php index df288c912..db29619d6 100644 --- a/src/di/src/LazyLoader/AbstractLazyProxyBuilder.php +++ b/src/di/src/LazyLoader/AbstractLazyProxyBuilder.php @@ -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) diff --git a/src/di/src/LazyLoader/ClassLazyProxyBuilder.php b/src/di/src/LazyLoader/ClassLazyProxyBuilder.php index cf6f70351..c370281d9 100644 --- a/src/di/src/LazyLoader/ClassLazyProxyBuilder.php +++ b/src/di/src/LazyLoader/ClassLazyProxyBuilder.php @@ -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; } diff --git a/src/di/src/LazyLoader/FallbackLazyProxyBuilder.php b/src/di/src/LazyLoader/FallbackLazyProxyBuilder.php index 170cd326d..f719f60c7 100644 --- a/src/di/src/LazyLoader/FallbackLazyProxyBuilder.php +++ b/src/di/src/LazyLoader/FallbackLazyProxyBuilder.php @@ -12,10 +12,6 @@ declare(strict_types=1); namespace Hyperf\Di\LazyLoader; -use PhpParser\Node\Const_; -use PhpParser\Node\Scalar\String_; -use PhpParser\Node\Stmt\ClassConst; - class FallbackLazyProxyBuilder extends AbstractLazyProxyBuilder { public function addClassRelationship(): AbstractLazyProxyBuilder diff --git a/src/di/src/LazyLoader/InterfaceLazyProxyBuilder.php b/src/di/src/LazyLoader/InterfaceLazyProxyBuilder.php index 5fb6a20b9..f984caccb 100644 --- a/src/di/src/LazyLoader/InterfaceLazyProxyBuilder.php +++ b/src/di/src/LazyLoader/InterfaceLazyProxyBuilder.php @@ -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 InterfaceLazyProxyBuilder extends AbstractLazyProxyBuilder { public function addClassRelationship(): AbstractLazyProxyBuilder { if (strpos($this->originalClassName, '\\') !== 0) { - $originalClassName = '\\'.$this->originalClassName; + $originalClassName = '\\' . $this->originalClassName; } else { $originalClassName = $this->originalClassName; } diff --git a/src/di/src/LazyLoader/LazyLoader.php b/src/di/src/LazyLoader/LazyLoader.php index 225e30149..ab495b031 100644 --- a/src/di/src/LazyLoader/LazyLoader.php +++ b/src/di/src/LazyLoader/LazyLoader.php @@ -14,19 +14,21 @@ namespace Hyperf\Di\LazyLoader; use Hyperf\Contract\ConfigInterface; use Hyperf\Utils\Coroutine\Locker as CoLocker; -use Hyperf\Di\LazyLoader\PublicMethodVisitor; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use PhpParser\ParserFactory; -class LazyLoader { +class LazyLoader +{ public const CONFIG_FILE_NAME = 'lazy_loader'; + /** * Indicates if a loader has been registered. * * @var bool */ protected $registered = false; + /** * The singleton instance of the loader. * @@ -35,7 +37,7 @@ class LazyLoader { protected static $instance; /** - * The Configuration object + * The Configuration object. * * @var ConfigInterface */ @@ -43,9 +45,10 @@ class LazyLoader { private function __construct(ConfigInterface $config) { - $this->config = $config->get(self::CONFIG_FILE_NAME, []); - $this->register(); + $this->config = $config->get(self::CONFIG_FILE_NAME, []); + $this->register(); } + /** * Get or create the singleton lazy loader instance. * @@ -58,6 +61,7 @@ class LazyLoader { } return static::$instance; } + /** * Load a class proxy if it is registered. * @@ -65,11 +69,12 @@ class LazyLoader { */ public function load(string $proxy) { - if (array_key_exists($proxy, $this->config)) { + if ($this->config->get($proxy, false)) { $this->loadProxy($proxy); return true; } } + /** * Register the loader on the auto-loader stack. */ @@ -80,6 +85,7 @@ class LazyLoader { $this->registered = true; } } + /** * Load a real-time facade for the given proxy. */ @@ -87,11 +93,12 @@ class LazyLoader { { require_once $this->ensureProxyExists($proxy); } + /** * Ensure that the given proxy has an existing real-time facade class. */ protected function ensureProxyExists(string $proxy): string - { + { $dir = BASE_PATH . '/runtime/container/proxy/'; if (! file_exists($dir)) { mkdir($dir, 0755, true); @@ -102,8 +109,8 @@ class LazyLoader { if (! file_exists($path) && CoLocker::lock($key)) { $targetPath = $path . '.' . uniqid(); $code = $this->generatorLazyProxy( - $proxy, - $this->config[$proxy] + $proxy, + $this->config->get($proxy) ); file_put_contents($targetPath, $code); rename($targetPath, $path); @@ -111,59 +118,69 @@ class LazyLoader { } return $path; } + /** * Format the lazy proxy with the proper namespace and class. - * - * @return string */ protected function generatorLazyProxy(string $proxy, string $target): string { - $targetReflection = new \ReflectionClass($target); - $fileName = $targetReflection->getFileName(); - if (!$fileName){ - $code = ''; // Classes and Interfaces from PHP internals - } else { + $targetReflection = new \ReflectionClass($target); + $fileName = $targetReflection->getFileName(); + if (! $fileName) { + $code = ''; // Classes and Interfaces from PHP internals + } else { $code = file_get_contents($fileName); } - if ($this->isUnsupportedReflectionType($targetReflection)){ + if ($this->isUnsupportedReflectionType($targetReflection)) { $builder = new FallbackLazyProxyBuilder(); return $this->buildNewCode($builder, $code, $proxy, $target); } - if ($targetReflection->isInterface()){ - $builder = new InterfaceLazyProxyBuilder(); - return $this->buildNewCode($builder, $code, $proxy, $target); - } + if ($targetReflection->isInterface()) { + $builder = new InterfaceLazyProxyBuilder(); + return $this->buildNewCode($builder, $code, $proxy, $target); + } $builder = new ClassLazyProxyBuilder(); return $this->buildNewCode($builder, $code, $proxy, $target); } /** - * These conditions are really hard to proxy via inheritence. - * Luckily these conditions are very rarely met. - * - * TODO: implement some of them. - * - * @param \ReflectionClass $targetReflection [description] - * @return boolean [description] + * Prepend the load method to the auto-loader stack. */ - private function isUnsupportedReflectionType(\ReflectionClass $targetReflection): bool { + protected function prependToLoaderStack(): void + { + /** @var callable(string): void*/ + $load = [$this, 'load']; + spl_autoload_register($load, true, true); + } + + /** + * These conditions are really hard to proxy via inheritence. + * Luckily these conditions are very rarely met. + * + * TODO: implement some of them. + * + * @param \ReflectionClass $targetReflection [description] + * @return bool [description] + */ + private function isUnsupportedReflectionType(\ReflectionClass $targetReflection): bool + { //Final class - if ($targetReflection->isFinal()){ + if ($targetReflection->isFinal()) { return true; } // Internal Interface - if ($targetReflection->isInterface() && $targetReflection->isInternal()){ + if ($targetReflection->isInterface() && $targetReflection->isInternal()) { return true; } // Nested Interface - if ($targetReflection->isInterface() && !empty($targetReflection->getInterfaces())){ + if ($targetReflection->isInterface() && ! empty($targetReflection->getInterfaces())) { return true; } // Nested AbstractClass - if ($targetReflection->isAbstract() + if ($targetReflection->isAbstract() && $targetReflection->getParentClass() && $targetReflection->getParentClass()->isAbstract() - ){ + ) { return true; } return false; @@ -171,27 +188,19 @@ class LazyLoader { private function buildNewCode(AbstractLazyProxyBuilder $builder, string $code, string $proxy, string $target): string { - $builder->addClassBoilerplate($proxy, $target); - $builder->addClassRelationship(); - $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); - $ast = $parser->parse($code); - $traverser = new NodeTraverser(); - $visitor = new PublicMethodVisitor(); - $nameResolver = new NameResolver(); - $traverser->addVisitor($nameResolver); - $traverser->addVisitor($visitor); - $ast = $traverser->traverse($ast); - $builder->addNodes($visitor->nodes); - $prettyPrinter = new \PhpParser\PrettyPrinter\Standard(); - $stmts = [$builder->getNode()]; - $newCode = $prettyPrinter->prettyPrintFile($stmts); - return $newCode; - } - /** - * Prepend the load method to the auto-loader stack. - */ - protected function prependToLoaderStack(): void - { - spl_autoload_register([$this, 'load'], true, true); + $builder->addClassBoilerplate($proxy, $target); + $builder->addClassRelationship(); + $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); + $ast = $parser->parse($code); + $traverser = new NodeTraverser(); + $visitor = new PublicMethodVisitor(); + $nameResolver = new NameResolver(); + $traverser->addVisitor($nameResolver); + $traverser->addVisitor($visitor); + $ast = $traverser->traverse($ast); + $builder->addNodes($visitor->nodes); + $prettyPrinter = new \PhpParser\PrettyPrinter\Standard(); + $stmts = [$builder->getNode()]; + return $prettyPrinter->prettyPrintFile($stmts); } } diff --git a/src/di/src/LazyLoader/LazyProxyTrait.php b/src/di/src/LazyLoader/LazyProxyTrait.php index 70164da2b..bfbd818ce 100644 --- a/src/di/src/LazyLoader/LazyProxyTrait.php +++ b/src/di/src/LazyLoader/LazyProxyTrait.php @@ -12,51 +12,44 @@ declare(strict_types=1); namespace Hyperf\Di\LazyLoader; -use Closure; -use Hyperf\Di\Annotation\AnnotationCollector; -use Hyperf\Di\Annotation\AspectCollector; -use Hyperf\Di\ReflectionManager; use Hyperf\Utils\ApplicationContext; trait LazyProxyTrait { - public function __construct(){ + public function __construct() + { $vars = get_object_vars($this); foreach (array_keys($vars) as $var) { unset($this->{$var}); } } - /** - * Return The Proxy Target - * @return mixed - */ - public function getInstance() - { - return ApplicationContext::getContainer()->get(Self::PROXY_TARGET); - } - public function __call(string $method, array $arguments) { $obj = $this->getInstance(); return call_user_func([$obj, $method], ...$arguments); } + public function __get($name) { return $this->getInstance()->{$name}; } + public function __set($name, $value) { $this->getInstance()->{$name} = $value; } + public function __isset($name) { return isset($this->getInstance()->{$name}); } + public function __unset($name) { unset($this->getInstance()->{$name}); } + public function __wakeup() { $vars = get_object_vars($this); @@ -64,4 +57,13 @@ trait LazyProxyTrait unset($this->{$var}); } } + + /** + * Return The Proxy Target. + * @return mixed + */ + public function getInstance() + { + return ApplicationContext::getContainer()->get(self::PROXY_TARGET); + } } diff --git a/src/di/src/LazyLoader/PublicMethodVisitor.php b/src/di/src/LazyLoader/PublicMethodVisitor.php index 6fae47620..f418fa553 100644 --- a/src/di/src/LazyLoader/PublicMethodVisitor.php +++ b/src/di/src/LazyLoader/PublicMethodVisitor.php @@ -13,16 +13,15 @@ declare(strict_types=1); namespace Hyperf\Di\LazyLoader; use PhpParser\Node; -use PhpParser\NodeVisitorAbstract; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Name; use PhpParser\Node\Scalar\MagicConst\Function_ as MagicConstFunction; -use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Return_; +use PhpParser\NodeVisitorAbstract; class PublicMethodVisitor extends NodeVisitorAbstract { diff --git a/src/di/src/MethodDefinitionCollectorInterface.php b/src/di/src/MethodDefinitionCollectorInterface.php index 24d410403..a0ff81744 100644 --- a/src/di/src/MethodDefinitionCollectorInterface.php +++ b/src/di/src/MethodDefinitionCollectorInterface.php @@ -22,7 +22,6 @@ interface MethodDefinitionCollectorInterface /** * Retrieve the metadata for the return value of the method. - * @return ReflectionType */ public function getReturnType(string $class, string $method): ReflectionType; } diff --git a/src/di/tests/InterfaceLazyProxyBuilderTest.php b/src/di/tests/InterfaceLazyProxyBuilderTest.php index 13ebb0245..2782f8241 100644 --- a/src/di/tests/InterfaceLazyProxyBuilderTest.php +++ b/src/di/tests/InterfaceLazyProxyBuilderTest.php @@ -86,4 +86,4 @@ CODETEMPLATE; $newCode = $prettyPrinter->prettyPrintFile($stmts); $this->assertEquals($expected, $newCode); } -} \ No newline at end of file +} diff --git a/src/di/tests/LazyProxyTraitTest.php b/src/di/tests/LazyProxyTraitTest.php index 4f86c58d8..a4cc1ee93 100644 --- a/src/di/tests/LazyProxyTraitTest.php +++ b/src/di/tests/LazyProxyTraitTest.php @@ -12,15 +12,12 @@ declare(strict_types=1); namespace HyperfTest\Di; +use Hyperf\Di\Container; +use Hyperf\Utils\ApplicationContext; use HyperfTest\Di\Stub\LazyProxy; use HyperfTest\Di\Stub\Proxied; -use Hyperf\Di\Container; -use Hyperf\Di\LazyLoader\PublicMethodVisitor; -use Hyperf\Utils\ApplicationContext; use Mockery; use PHPUnit\Framework\TestCase; -use PhpParser\NodeTraverser; -use PhpParser\ParserFactory; /** * @internal @@ -28,6 +25,74 @@ use PhpParser\ParserFactory; */ class LazyProxyTraitTest extends TestCase { + public function testLaziness() + { + $lp = new LazyProxy(); + $this->assertFalse(Proxied::$isInitialized); + } + + public function testSet() + { + $lp = new LazyProxy(); + $this->mockContainer(); + $proxied = ApplicationContext::getContainer()->get(Proxied::class); + $lp->id = '12'; + $this->assertEquals('12', $proxied->id); + } + + public function testGet() + { + $lp = new LazyProxy(); + $this->mockContainer(); + $proxied = ApplicationContext::getContainer()->get(Proxied::class); + $this->assertEquals('20', $lp->id); + } + + public function testUnset() + { + $lp = new LazyProxy(); + $this->mockContainer(); + $proxied = ApplicationContext::getContainer()->get(Proxied::class); + unset($lp->id); + $this->assertFalse(isset($proxied->id)); + } + + public function testIsset() + { + $lp = new LazyProxy(); + $this->mockContainer(); + $proxied = ApplicationContext::getContainer()->get(Proxied::class); + $this->assertTrue(isset($lp->id)); + } + + public function testCallMethod() + { + $lp = new LazyProxy(); + $this->mockContainer(); + $proxied = ApplicationContext::getContainer()->get(Proxied::class); + $lp->setId('1'); + $this->assertEquals('1', $proxied->id); + } + + public function testClone() + { + $lp = new LazyProxy(); + $this->mockContainer(); + $proxied = ApplicationContext::getContainer()->get(Proxied::class); + $lp2 = clone $lp; + $this->assertEquals($proxied, $lp2->getInstance()); + } + + public function testSerialize() + { + $lp = new LazyProxy(); + $this->mockContainer(); + $proxied = ApplicationContext::getContainer()->get(Proxied::class); + $s = serialize($lp); + $lp2 = unserialize($s); + $this->assertEquals($lp, $lp2); + } + private function mockContainer() { $container = Mockery::mock(Container::class); @@ -37,64 +102,4 @@ class LazyProxyTraitTest extends TestCase ->andReturn(new Proxied('20', 'hello')); ApplicationContext::setContainer($container); } - - public function testLaziness(){ - $lp = new LazyProxy(); - $this->assertFalse(Proxied::$isInitialized); - } - - public function testSet(){ - $lp = new LazyProxy(); - $this->mockContainer(); - $proxied = ApplicationContext::getContainer()->get(Proxied::class); - $lp->id = '12'; - $this->assertEquals('12', $proxied->id); - } - - public function testGet(){ - $lp = new LazyProxy(); - $this->mockContainer(); - $proxied = ApplicationContext::getContainer()->get(Proxied::class); - $this->assertEquals('20', $lp->id); - } - - public function testUnset(){ - $lp = new LazyProxy(); - $this->mockContainer(); - $proxied = ApplicationContext::getContainer()->get(Proxied::class); - unset($lp->id); - $this->assertFalse(isset($proxied->id)); - } - - public function testIsset(){ - $lp = new LazyProxy(); - $this->mockContainer(); - $proxied = ApplicationContext::getContainer()->get(Proxied::class); - $this->assertTrue(isset($lp->id)); - } - - public function testCallMethod(){ - $lp = new LazyProxy(); - $this->mockContainer(); - $proxied = ApplicationContext::getContainer()->get(Proxied::class); - $lp->setId('1'); - $this->assertEquals('1', $proxied->id); - } - - public function testClone(){ - $lp = new LazyProxy(); - $this->mockContainer(); - $proxied = ApplicationContext::getContainer()->get(Proxied::class); - $lp2 = clone $lp; - $this->assertEquals($proxied, $lp2->getInstance()); - } - - public function testSerialize(){ - $lp = new LazyProxy(); - $this->mockContainer(); - $proxied = ApplicationContext::getContainer()->get(Proxied::class); - $s = serialize($lp); - $lp2 = unserialize($s); - $this->assertEquals($lp, $lp2); - } } diff --git a/src/di/tests/Stub/LazyProxy.php b/src/di/tests/Stub/LazyProxy.php index c85efb058..4f50615e5 100644 --- a/src/di/tests/Stub/LazyProxy.php +++ b/src/di/tests/Stub/LazyProxy.php @@ -16,7 +16,7 @@ use Hyperf\Di\LazyLoader\LazyProxyTrait; class LazyProxy extends Proxied { - use LazyProxyTrait; - const PROXY_TARGET = 'HyperfTest\\Di\\Stub\\Proxied'; - + use LazyProxyTrait; + + const PROXY_TARGET = 'HyperfTest\\Di\\Stub\\Proxied'; } diff --git a/src/di/tests/Stub/Proxied.php b/src/di/tests/Stub/Proxied.php index b9514ee27..d0da23458 100644 --- a/src/di/tests/Stub/Proxied.php +++ b/src/di/tests/Stub/Proxied.php @@ -29,11 +29,11 @@ class Proxied public function setId(string $a) { - return $this->id = $a; + return $this->id = $a; } public function getId() { - return $this->id; + return $this->id; } } diff --git a/src/exception-handler/src/Listener/ErrorExceptionHandler.php b/src/exception-handler/src/Listener/ErrorExceptionHandler.php new file mode 100644 index 000000000..fc43734fc --- /dev/null +++ b/src/exception-handler/src/Listener/ErrorExceptionHandler.php @@ -0,0 +1,36 @@ +process((object) []); + + $this->expectException(\ErrorException::class); + $this->expectExceptionMessage('Undefined offset: 1'); + try { + $array = []; + $array[1]; + } finally { + restore_error_handler(); + } + } +} diff --git a/src/graphql/src/Annotation/AnnotationTrait.php b/src/graphql/src/Annotation/AnnotationTrait.php index 98407cc57..cbfb90ee5 100644 --- a/src/graphql/src/Annotation/AnnotationTrait.php +++ b/src/graphql/src/Annotation/AnnotationTrait.php @@ -14,6 +14,7 @@ namespace Hyperf\GraphQL\Annotation; use Hyperf\Di\Annotation\AnnotationCollector; use Hyperf\Di\ReflectionManager; +use Hyperf\GraphQL\ClassCollector; use ReflectionProperty; trait AnnotationTrait @@ -31,16 +32,19 @@ trait AnnotationTrait public function collectClass(string $className): void { AnnotationCollector::collectClass($className, static::class, $this); + ClassCollector::collect($className); } public function collectMethod(string $className, ?string $target): void { AnnotationCollector::collectMethod($className, $target, static::class, $this); + ClassCollector::collect($className); } public function collectProperty(string $className, ?string $target): void { AnnotationCollector::collectProperty($className, $target, static::class, $this); + ClassCollector::collect($className); } protected function bindMainProperty(string $key, array $value) diff --git a/src/graphql/src/ClassCollector.php b/src/graphql/src/ClassCollector.php new file mode 100644 index 000000000..4e3d71a70 --- /dev/null +++ b/src/graphql/src/ClassCollector.php @@ -0,0 +1,30 @@ +classes === null) { $this->classes = []; - $classes = AnnotationCollector::getClassByAnnotation(Type::class); - foreach (array_keys($classes) as $className) { + $classes = ClassCollector::getClasses(); + foreach ($classes as $className) { if (! \class_exists($className)) { continue; } diff --git a/src/http-message/src/Base/MessageTrait.php b/src/http-message/src/Base/MessageTrait.php index e872fee11..e9214891c 100755 --- a/src/http-message/src/Base/MessageTrait.php +++ b/src/http-message/src/Base/MessageTrait.php @@ -192,7 +192,6 @@ trait MessageTrait } /** - * @param array $headers * @return static */ public function withHeaders(array $headers) @@ -341,7 +340,6 @@ trait MessageTrait } /** - * @param array $headers * @return static */ private function setHeaders(array $headers) diff --git a/src/http-message/src/Cookie/CookieJar.php b/src/http-message/src/Cookie/CookieJar.php index d4f7021f0..003adca88 100755 --- a/src/http-message/src/Cookie/CookieJar.php +++ b/src/http-message/src/Cookie/CookieJar.php @@ -274,7 +274,6 @@ class CookieJar implements CookieJarInterface * * @see https://tools.ietf.org/html/rfc6265#section-5.1.4 * - * @param RequestInterface $request * @return string */ private function getCookiePathFromRequest(RequestInterface $request) @@ -299,8 +298,6 @@ class CookieJar implements CookieJarInterface /** * If a cookie already exists and the server asks to set it again with a * null value, the cookie must be deleted. - * - * @param SetCookie $cookie */ private function removeCookieIfEmpty(SetCookie $cookie) { diff --git a/src/http-message/src/Server/Request.php b/src/http-message/src/Server/Request.php index 431abdb38..91d03b27b 100755 --- a/src/http-message/src/Server/Request.php +++ b/src/http-message/src/Server/Request.php @@ -66,7 +66,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI /** * Load a swoole request, and transfer to a swoft request object. * - * @param \Swoole\Http\Request $swooleRequest * @return \Hyperf\HttpMessage\Server\Request */ public static function loadFromSwooleRequest(\Swoole\Http\Request $swooleRequest) @@ -103,7 +102,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI /** * Return an instance with the specified server params. * - * @param array $serverParams * @return static */ public function withServerParams(array $serverParams) @@ -118,8 +116,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI * Retrieves cookies sent by the client to the server. * The data MUST be compatible with the structure of the $_COOKIE * superglobal. - * - * @return array */ public function getCookieParams(): array { @@ -154,8 +150,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI * params. If you need to ensure you are only getting the original * values, you may need to parse the query string from `getUri()->getQuery()` * or from the `QUERY_STRING` server param. - * - * @return array */ public function getQueryParams(): array { @@ -416,8 +410,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI /** * Get the full URL for the request. - * - * @return string */ public function fullUrl(): string { @@ -451,16 +443,12 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI return $this->hasHeader('X-Requested-With') == 'XMLHttpRequest'; } - /** - * @return \Swoole\Http\Request - */ public function getSwooleRequest(): \Swoole\Http\Request { return $this->swooleRequest; } /** - * @param \Swoole\Http\Request $swooleRequest * @return $this */ public function setSwooleRequest(\Swoole\Http\Request $swooleRequest) @@ -532,7 +520,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI * Loops through all nested files and returns a normalized array of * UploadedFileInterface instances. * - * @param array $files * @return UploadedFileInterface[] */ private static function normalizeNestedFileSpec(array $files = []) @@ -555,7 +542,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI /** * Get a Uri populated with values from $swooleRequest->server. - * @param \Swoole\Http\Request $swooleRequest * @throws \InvalidArgumentException * @return \Psr\Http\Message\UriInterface */ @@ -579,11 +565,10 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI } elseif (isset($server['server_addr'])) { $uri = $uri->withHost($server['server_addr']); } elseif (isset($header['host'])) { + $hasPort = true; if (\strpos($header['host'], ':')) { - $hasPort = true; [$host, $port] = explode(':', $header['host'], 2); - - if ($port !== '80') { + if ($port != $uri->getDefaultPort()) { $uri = $uri->withPort($port); } } else { diff --git a/src/http-message/src/Server/Response.php b/src/http-message/src/Server/Response.php index 631f74e30..ff645c74c 100755 --- a/src/http-message/src/Server/Response.php +++ b/src/http-message/src/Server/Response.php @@ -29,9 +29,6 @@ class Response extends \Hyperf\HttpMessage\Base\Response implements Sendable */ protected $cookies = []; - /** - * @param null|\Swoole\Http\Response $response - */ public function __construct(\Swoole\Http\Response $response = null) { $this->swooleResponse = $response; @@ -74,6 +71,14 @@ class Response extends \Hyperf\HttpMessage\Base\Response implements Sendable return $clone; } + /** + * Return all cookies. + */ + public function getCookies(): array + { + return $this->cookies; + } + public function getSwooleResponse(): ?\Swoole\Http\Response { return $this->swooleResponse; diff --git a/src/http-message/src/Stream/SwooleStream.php b/src/http-message/src/Stream/SwooleStream.php index 4e705347a..7539bd834 100755 --- a/src/http-message/src/Stream/SwooleStream.php +++ b/src/http-message/src/Stream/SwooleStream.php @@ -28,8 +28,6 @@ class SwooleStream implements StreamInterface /** * SwooleStream constructor. - * - * @param string $contents */ public function __construct(string $contents = '') { diff --git a/src/http-message/src/Upload/UploadedFile.php b/src/http-message/src/Upload/UploadedFile.php index 2ed2df53b..519a9220a 100755 --- a/src/http-message/src/Upload/UploadedFile.php +++ b/src/http-message/src/Upload/UploadedFile.php @@ -66,13 +66,6 @@ class UploadedFile extends \SplFileInfo implements UploadedFileInterface */ private $mimeType; - /** - * @param string $tmpFile - * @param null|int $size - * @param int $errorStatus - * @param null|string $clientFilename - * @param null|string $clientMediaType - */ public function __construct( string $tmpFile, ?int $size, @@ -254,9 +247,6 @@ class UploadedFile extends \SplFileInfo implements UploadedFileInterface return $this->clientMediaType; } - /** - * @return array - */ public function toArray(): array { return [ diff --git a/src/http-message/src/Uri/Uri.php b/src/http-message/src/Uri/Uri.php index 0d52baa0c..19ff9e60d 100755 --- a/src/http-message/src/Uri/Uri.php +++ b/src/http-message/src/Uri/Uri.php @@ -545,8 +545,6 @@ class Uri implements UriInterface * Whether the URI has the default port of the current scheme. * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used * independently of the implementation. - * - * @return bool */ public function isDefaultPort(): bool { @@ -708,7 +706,6 @@ class Uri implements UriInterface } /** - * @param array $match * @return string */ private function rawurlencodeMatchZero(array $match) diff --git a/src/http-message/tests/ServerRequestTest.php b/src/http-message/tests/ServerRequestTest.php index 97ce5bc44..7f7612b31 100644 --- a/src/http-message/tests/ServerRequestTest.php +++ b/src/http-message/tests/ServerRequestTest.php @@ -12,11 +12,14 @@ declare(strict_types=1); namespace HyperfTest\HttpMessage; +use Hyperf\HttpMessage\Server\Request; use Hyperf\HttpMessage\Stream\SwooleStream; +use Hyperf\Utils\Codec\Json; use HyperfTest\HttpMessage\Stub\Server\RequestStub; use Mockery; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; +use Swoole\Http\Request as SwooleRequest; /** * @internal @@ -60,4 +63,24 @@ class ServerRequestTest extends TestCase $request->shouldReceive('getBody')->andReturn(new SwooleStream(json_encode($json))); $this->assertSame($json, RequestStub::normalizeParsedBody($data, $request)); } + + public function testGetUriFromGlobals() + { + $swooleRequest = Mockery::mock(SwooleRequest::class); + $data = ['name' => 'Hyperf']; + $swooleRequest->shouldReceive('rawContent')->andReturn(Json::encode($data)); + $swooleRequest->server = ['server_port' => 9501]; + $request = Request::loadFromSwooleRequest($swooleRequest); + $uri = $request->getUri(); + $this->assertSame(9501, $uri->getPort()); + + $swooleRequest = Mockery::mock(SwooleRequest::class); + $data = ['name' => 'Hyperf']; + $swooleRequest->shouldReceive('rawContent')->andReturn(Json::encode($data)); + $swooleRequest->header = ['host' => '127.0.0.1']; + $swooleRequest->server = ['server_port' => 9501]; + $request = Request::loadFromSwooleRequest($swooleRequest); + $uri = $request->getUri(); + $this->assertSame(null, $uri->getPort()); + } } diff --git a/src/model-cache/src/Builder.php b/src/model-cache/src/Builder.php new file mode 100644 index 000000000..0ace5f095 --- /dev/null +++ b/src/model-cache/src/Builder.php @@ -0,0 +1,55 @@ +deleteCache(function () { + return parent::delete(); + }); + } + + public function update(array $values) + { + return $this->deleteCache(function () use ($values) { + return parent::update($values); + }); + } + + protected function deleteCache(\Closure $closure) + { + $queryBuilder = clone $this; + $primaryKey = $this->model->getKeyName(); + $ids = []; + $models = $queryBuilder->get([$primaryKey]); + foreach ($models as $model) { + $ids[] = $model->{$primaryKey}; + } + if (empty($ids)) { + return 0; + } + + $result = $closure(); + + $manger = ApplicationContext::getContainer()->get(Manager::class); + + $manger->destroy($ids, get_class($this->model)); + + return $result; + } +} diff --git a/src/model-cache/src/Cacheable.php b/src/model-cache/src/Cacheable.php index 2b5828278..e727dff58 100644 --- a/src/model-cache/src/Cacheable.php +++ b/src/model-cache/src/Cacheable.php @@ -12,12 +12,20 @@ declare(strict_types=1); namespace Hyperf\ModelCache; +use Hyperf\Database\Model\Builder; use Hyperf\Database\Model\Collection; use Hyperf\Database\Model\Model; +use Hyperf\Database\Query\Builder as QueryBuilder; +use Hyperf\ModelCache\Builder as ModelCacheBuilder; use Hyperf\Utils\ApplicationContext; trait Cacheable { + /** + * @var bool + */ + protected $useCacheBuilder = false; + /** * Fetch a model from cache. * @param mixed $id @@ -33,10 +41,8 @@ trait Cacheable /** * Fetch models from cache. - * @param mixed $ids - * @return \Hyperf\Database\Model\Collection */ - public static function findManyFromCache($ids): Collection + public static function findManyFromCache(array $ids): Collection { $container = ApplicationContext::getContainer(); $manager = $container->get(Manager::class); @@ -47,7 +53,6 @@ trait Cacheable /** * Delete model from cache. - * @return bool */ public function deleteCache(): bool { @@ -58,8 +63,8 @@ trait Cacheable /** * Increment a column's value by a given amount. - * @param mixed $column - * @param mixed $amount + * @param string $column + * @param float|int $amount * @return int */ public function increment($column, $amount = 1, array $extra = []) @@ -81,8 +86,8 @@ trait Cacheable /** * Decrement a column's value by a given amount. - * @param mixed $column - * @param mixed $amount + * @param string $column + * @param float|int $amount * @return int */ public function decrement($column, $amount = 1, array $extra = []) @@ -101,4 +106,31 @@ trait Cacheable } return $res; } + + /** + * Create a new Model query builder for the model. + * @param QueryBuilder $query + */ + public function newModelBuilder($query): Builder + { + if ($this->useCacheBuilder) { + return new ModelCacheBuilder($query); + } + + return parent::newModelBuilder($query); + } + + public function newQuery(bool $cache = false): Builder + { + $this->useCacheBuilder = $cache; + return parent::newQuery(); + } + + /** + * @param bool $cache Whether to delete the model cache when batch update + */ + public static function query(bool $cache = false): Builder + { + return (new static())->newQuery($cache); + } } diff --git a/src/model-cache/src/CacheableInterface.php b/src/model-cache/src/CacheableInterface.php index 8d0b106bf..0ac7aa5b4 100644 --- a/src/model-cache/src/CacheableInterface.php +++ b/src/model-cache/src/CacheableInterface.php @@ -19,7 +19,7 @@ interface CacheableInterface { public static function findFromCache($id): ?Model; - public static function findManyFromCache($ids): Collection; + public static function findManyFromCache(array $ids): Collection; public function deleteCache(): bool; } diff --git a/src/model-cache/src/Config.php b/src/model-cache/src/Config.php index 17c60cdb2..958d15cd0 100644 --- a/src/model-cache/src/Config.php +++ b/src/model-cache/src/Config.php @@ -96,18 +96,11 @@ class Config return $this; } - /** - * @return string - */ public function getPool(): string { return $this->pool; } - /** - * @param string $pool - * @return Config - */ public function setPool(string $pool): Config { $this->pool = $pool; diff --git a/src/model-cache/src/Exception/OperatorNotFoundException.php b/src/model-cache/src/Exception/OperatorNotFoundException.php new file mode 100644 index 000000000..7227c3cee --- /dev/null +++ b/src/model-cache/src/Exception/OperatorNotFoundException.php @@ -0,0 +1,17 @@ +redis = make(RedisProxy::class, ['pool' => $config->getPool()]); $this->config = $config; - $this->multiple = new HashsGetMultiple(); + $this->manager = make(LuaManager::class, [$config]); } public function get($key, $default = null) @@ -107,25 +107,14 @@ class RedisHandler implements HandlerInterface public function getMultiple($keys, $default = null) { - if ($this->config->isLoadScript()) { - $sha = $this->getLuaSha(); - } - - if (! empty($sha)) { - $list = $this->redis->evalSha($sha, $keys, count($keys)); - } else { - $script = $this->multiple->getScript(); - $list = $this->redis->eval($script, $keys, count($keys)); - } - + $data = $this->manager->handle(HashGetMultiple::class, $keys); $result = []; - foreach ($this->multiple->parseResponse($list) as $item) { + foreach ($data as $item) { unset($item[$this->defaultKey]); - if ($item) { + if (! empty($item)) { $result[] = $item; } } - return $result; } @@ -151,18 +140,8 @@ class RedisHandler implements HandlerInterface public function incr($key, $column, $amount): bool { - $ret = $this->redis->hIncrByFloat($key, $column, (float) $amount); - return is_float($ret); - } + $data = $this->manager->handle(HashIncr::class, [$key, $column, $amount]); - protected function getLuaSha() - { - if (! empty($this->luaSha)) { - return $this->luaSha; - } - - $sha = $this->redis->script('load', $this->multiple->getScript()); - - return $this->luaSha = $sha; + return is_float($data); } } diff --git a/src/model-cache/src/Redis/HashsGetMultiple.php b/src/model-cache/src/Redis/HashGetMultiple.php similarity index 95% rename from src/model-cache/src/Redis/HashsGetMultiple.php rename to src/model-cache/src/Redis/HashGetMultiple.php index c273066cb..86f89ddec 100644 --- a/src/model-cache/src/Redis/HashsGetMultiple.php +++ b/src/model-cache/src/Redis/HashGetMultiple.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace Hyperf\ModelCache\Redis; -class HashsGetMultiple implements OperatorInterface +class HashGetMultiple implements OperatorInterface { public function getScript(): string { diff --git a/src/model-cache/src/Redis/HashIncr.php b/src/model-cache/src/Redis/HashIncr.php new file mode 100644 index 000000000..b723d46cf --- /dev/null +++ b/src/model-cache/src/Redis/HashIncr.php @@ -0,0 +1,31 @@ + + */ + protected $operators = []; + + /** + * @var array + */ + protected $luaShas = []; + + /** + * @var Config + */ + protected $config; + + /** + * @var \Redis|RedisProxy + */ + protected $redis; + + public function __construct(Config $config) + { + $this->config = $config; + $this->redis = make(RedisProxy::class, ['pool' => $config->getPool()]); + $this->operators[HashGetMultiple::class] = new HashGetMultiple(); + $this->operators[HashIncr::class] = new HashIncr(); + } + + public function handle(string $key, array $keys) + { + if ($this->config->isLoadScript()) { + $sha = $this->getLuaSha($key); + } + + $operator = $this->getOperator($key); + if (! empty($sha)) { + $luaData = $this->redis->evalSha($sha, $keys, count($keys)); + } else { + $script = $operator->getScript(); + $luaData = $this->redis->eval($script, $keys, count($keys)); + } + return $operator->parseResponse($luaData); + } + + public function getOperator(string $key): OperatorInterface + { + if (! isset($this->operators[$key])) { + throw new OperatorNotFoundException(sprintf('The operator %s is not found.', $key)); + } + + if (! $this->operators[$key] instanceof OperatorInterface) { + throw new OperatorNotFoundException(sprintf('The operator %s is not instanceof OperatorInterface.', $key)); + } + + return $this->operators[$key]; + } + + public function getLuaSha(string $key): string + { + if (empty($this->luaShas[$key])) { + $this->luaShas[$key] = $this->redis->script('load', $this->getOperator($key)->getScript()); + } + return $this->luaShas[$key]; + } +} diff --git a/src/model-cache/tests/ModelCacheTest.php b/src/model-cache/tests/ModelCacheTest.php new file mode 100644 index 000000000..44546d839 --- /dev/null +++ b/src/model-cache/tests/ModelCacheTest.php @@ -0,0 +1,176 @@ +find(1); + + $this->assertEquals($expect, $user); + } + + public function testFindManyByCache() + { + ContainerStub::mockContainer(); + + $users = UserModel::findManyFromCache([1, 2, 3]); + $expects = UserModel::query()->findMany([1, 2, 3]); + + $this->assertTrue(count($users) == 2); + $this->assertEquals([1, 2], array_keys($users->getDictionary())); + $this->assertEquals($expects, $users); + } + + public function testDeleteByBuilder() + { + $container = ContainerStub::mockContainer(); + + $ids = [200, 201, 202]; + foreach ($ids as $id) { + UserModel::query()->firstOrCreate(['id' => $id], [ + 'name' => uniqid(), + 'gender' => 1, + ]); + } + + UserModel::findManyFromCache($ids); + /** @var \Redis $redis */ + $redis = $container->make(RedisProxy::class, ['pool' => 'default']); + foreach ($ids as $id) { + $this->assertEquals(1, $redis->exists('{mc:default:m:user}:id:' . $id)); + } + + UserModel::query(true)->whereIn('id', $ids)->delete(); + foreach ($ids as $id) { + $this->assertEquals(0, $redis->exists('{mc:default:m:user}:id:' . $id)); + } + + foreach ($ids as $id) { + $this->assertNull(UserModel::query()->find($id)); + } + } + + public function testUpdateByBuilder() + { + $container = ContainerStub::mockContainer(); + + $ids = [203, 204, 205]; + foreach ($ids as $id) { + UserModel::query()->firstOrCreate(['id' => $id], [ + 'name' => uniqid(), + 'gender' => 1, + ]); + } + + UserModel::findManyFromCache($ids); + /** @var \Redis $redis */ + $redis = $container->make(RedisProxy::class, ['pool' => 'default']); + foreach ($ids as $id) { + $this->assertEquals(1, $redis->exists('{mc:default:m:user}:id:' . $id)); + } + + UserModel::query(true)->whereIn('id', $ids)->update(['gender' => 2]); + foreach ($ids as $id) { + $this->assertEquals(0, $redis->exists('{mc:default:m:user}:id:' . $id)); + } + + foreach ($ids as $id) { + $this->assertSame(2, UserModel::query()->find($id)->gender); + } + + UserModel::query(true)->whereIn('id', $ids)->delete(); + } + + public function testIncr() + { + $container = ContainerStub::mockContainer(); + + $id = 206; + UserModel::query()->firstOrCreate(['id' => $id], [ + 'name' => uniqid(), + 'gender' => 1, + ]); + + $model = UserModel::findFromCache($id); + /** @var \Redis $redis */ + $redis = $container->make(RedisProxy::class, ['pool' => 'default']); + $this->assertEquals(1, $redis->exists('{mc:default:m:user}:id:' . $id)); + + $this->assertEquals(1, $model->increment('gender', 1)); + $this->assertEquals(1, $redis->exists('{mc:default:m:user}:id:' . $id)); + $this->assertEquals(2, $redis->hGet('{mc:default:m:user}:id:' . $id, 'gender')); + $this->assertEquals(2, UserModel::findFromCache($id)->gender); + $this->assertEquals(2, UserModel::query()->find($id)->gender); + + UserModel::query(true)->where('id', $id)->delete(); + } + + public function testFindNullBeforeCreate() + { + $container = ContainerStub::mockContainer(); + + $id = 207; + + $model = UserModel::findFromCache($id); + /** @var \Redis $redis */ + $redis = $container->make(RedisProxy::class, ['pool' => 'default']); + $this->assertEquals(1, $redis->exists('{mc:default:m:user}:id:' . $id)); + $this->assertNull($model); + + $this->assertEquals(1, $redis->del('{mc:default:m:user}:id:' . $id)); + UserModel::query(true)->where('id', $id)->delete(); + } + + public function testIncrNotExist() + { + $container = ContainerStub::mockContainer(); + + $id = 206; + UserModel::query()->firstOrCreate(['id' => $id], [ + 'name' => uniqid(), + 'gender' => 1, + ]); + + $model = UserModel::query()->find($id); + /** @var \Redis $redis */ + $redis = $container->make(RedisProxy::class, ['pool' => 'default']); + $this->assertEquals(0, $redis->exists('{mc:default:m:user}:id:' . $id)); + + $this->assertEquals(1, $model->increment('gender', 1)); + $this->assertEquals(0, $redis->exists('{mc:default:m:user}:id:' . $id)); + $this->assertEquals(2, UserModel::query()->find($id)->gender); + $this->assertEquals(2, UserModel::findFromCache($id)->gender); + + UserModel::query(true)->where('id', $id)->delete(); + } +} diff --git a/src/model-cache/tests/Stub/ContainerStub.php b/src/model-cache/tests/Stub/ContainerStub.php new file mode 100644 index 000000000..6034d34e8 --- /dev/null +++ b/src/model-cache/tests/Stub/ContainerStub.php @@ -0,0 +1,154 @@ +shouldReceive('get')->with(PoolFactory::class)->andReturn($factory); + + $resolver = new ConnectionResolver($container); + $container->shouldReceive('get')->with(ConnectionResolver::class)->andReturn($resolver); + + $config = new Config([ + StdoutLoggerInterface::class => [ + 'log_level' => [ + LogLevel::ALERT, + LogLevel::CRITICAL, + LogLevel::DEBUG, + LogLevel::EMERGENCY, + LogLevel::ERROR, + LogLevel::INFO, + LogLevel::NOTICE, + LogLevel::WARNING, + ], + ], + 'databases' => [ + 'default' => [ + 'driver' => 'mysql', + 'host' => 'localhost', + 'database' => 'hyperf', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + 'collation' => 'utf8_unicode_ci', + 'prefix' => '', + 'cache' => [ + 'handler' => RedisHandler::class, + 'cache_key' => '{mc:%s:m:%s}:%s:%s', + 'prefix' => 'default', + 'pool' => 'default', + 'ttl' => 3600 * 24, + 'empty_model_ttl' => 3600, + 'load_script' => true, + ], + 'pool' => [ + 'min_connections' => 1, + 'max_connections' => 10, + 'connect_timeout' => 10.0, + 'wait_timeout' => 3.0, + 'heartbeat' => -1, + 'max_idle_time' => 60.0, + ], + ], + ], + 'redis' => [ + 'default' => [ + 'host' => 'localhost', + 'auth' => null, + 'port' => 6379, + 'db' => 0, + 'pool' => [ + 'min_connections' => 1, + 'max_connections' => 30, + 'connect_timeout' => 10.0, + 'wait_timeout' => 3.0, + 'heartbeat' => -1, + 'max_idle_time' => 60, + ], + ], + ], + ]); + $container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config); + + $logger = new StdoutLogger($config); + $container->shouldReceive('get')->with(StdoutLoggerInterface::class)->andReturn($logger); + + $connectionFactory = new ConnectionFactory($container); + $container->shouldReceive('get')->with(ConnectionFactory::class)->andReturn($connectionFactory); + + $eventDispatcher = new EventDispatcher(new ListenerProvider(), $logger); + $container->shouldReceive('get')->with(EventDispatcherInterface::class)->andReturn($eventDispatcher); + + $container->shouldReceive('get')->with('db.connector.mysql')->andReturn(new MySqlConnector()); + $container->shouldReceive('has')->andReturn(true); + $container->shouldReceive('make')->with(Frequency::class, Mockery::any())->andReturn(new Frequency()); + $container->shouldReceive('make')->with(DbPool::class, Mockery::any())->andReturnUsing(function ($_, $args) use ($container) { + return new DbPool($container, $args['name']); + }); + + ApplicationContext::setContainer($container); + $container->shouldReceive('make')->with(LuaManager::class, Mockery::any())->andReturnUsing(function ($_, $args) { + return new LuaManager(...$args); + }); + $container->shouldReceive('make')->with(Channel::class, Mockery::any())->andReturnUsing(function ($_, $args) { + return new Channel($args['size']); + }); + $container->shouldReceive('make')->with(PoolOption::class, Mockery::any())->andReturnUsing(function ($_, $args) { + return new PoolOption(...array_values($args)); + }); + $container->shouldReceive('make')->with(\Hyperf\Redis\Frequency::class, Mockery::any())->andReturn(new \Hyperf\Redis\Frequency()); + $container->shouldReceive('make')->with(RedisPool::class, Mockery::any())->andReturnUsing(function ($_, $args) use ($container) { + return new RedisPool($container, $args['name']); + }); + $poolFactory = new \Hyperf\Redis\Pool\PoolFactory($container); + $container->shouldReceive('make')->with(RedisProxy::class, Mockery::any())->andReturnUsing(function ($_, $args) use ($poolFactory) { + return new RedisProxy($poolFactory, $args['pool']); + }); + $container->shouldReceive('make')->with(RedisHandler::class, Mockery::any())->andReturnUsing(function ($_, $args) use ($container) { + return new RedisHandler($container, $args['config']); + }); + $container->shouldReceive('get')->with(Manager::class)->andReturn(new Manager($container)); + + return $container; + } +} diff --git a/src/model-cache/tests/Stub/UserModel.php b/src/model-cache/tests/Stub/UserModel.php new file mode 100644 index 000000000..34aba07ca --- /dev/null +++ b/src/model-cache/tests/Stub/UserModel.php @@ -0,0 +1,50 @@ + 'integer', 'gender' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime']; +} diff --git a/src/nats/composer.json b/src/nats/composer.json index 0b106a605..94f60e2de 100644 --- a/src/nats/composer.json +++ b/src/nats/composer.json @@ -16,7 +16,8 @@ "hyperf/pool": "~1.1.0", "hyperf/utils": "~1.1.0", "ircmaxell/random-lib": "^1.2", - "psr/container": "^1.0" + "psr/container": "^1.0", + "psr/event-dispatcher": "^1.0" }, "require-dev": { "malukenho/docheader": "^0.1.6", diff --git a/src/nats/src/ConsumerManager.php b/src/nats/src/ConsumerManager.php index d77e4a869..6f647773a 100644 --- a/src/nats/src/ConsumerManager.php +++ b/src/nats/src/ConsumerManager.php @@ -15,9 +15,15 @@ namespace Hyperf\Nats; use Hyperf\Di\Annotation\AnnotationCollector; use Hyperf\Nats\Annotation\Consumer as ConsumerAnnotation; use Hyperf\Nats\Driver\DriverFactory; +use Hyperf\Nats\Event\AfterConsume; +use Hyperf\Nats\Event\AfterSubscribe; +use Hyperf\Nats\Event\BeforeConsume; +use Hyperf\Nats\Event\BeforeSubscribe; +use Hyperf\Nats\Event\FailToConsume; use Hyperf\Process\AbstractProcess; use Hyperf\Process\ProcessManager; use Psr\Container\ContainerInterface; +use Psr\EventDispatcher\EventDispatcherInterface; class ConsumerManager { @@ -69,6 +75,11 @@ class ConsumerManager */ private $subscriber; + /** + * @var null|EventDispatcherInterface + */ + private $dispatcher; + public function __construct(ContainerInterface $container, AbstractConsumer $consumer) { parent::__construct($container); @@ -76,17 +87,32 @@ class ConsumerManager $pool = $this->consumer->getPool(); $this->subscriber = $this->container->get(DriverFactory::class)->get($pool); + if ($container->has(EventDispatcherInterface::class)) { + $this->dispatcher = $container->get(EventDispatcherInterface::class); + } } public function handle(): void { - $this->subscriber->subscribe( - $this->consumer->getSubject(), - $this->consumer->getQueue(), - function ($data) { - $this->consumer->consume($data); - } - ); + while (true) { + $this->dispatcher && $this->dispatcher->dispatch(new BeforeSubscribe($this->consumer)); + $this->subscriber->subscribe( + $this->consumer->getSubject(), + $this->consumer->getQueue(), + function ($data) { + try { + $this->dispatcher && $this->dispatcher->dispatch(new BeforeConsume($this->consumer, $data)); + $this->consumer->consume($data); + $this->dispatcher && $this->dispatcher->dispatch(new AfterConsume($this->consumer, $data)); + } catch (\Throwable $throwable) { + $this->dispatcher && $this->dispatcher->dispatch(new FailToConsume($this->consumer, $data, $throwable)); + } + } + ); + + $this->dispatcher && $this->dispatcher->dispatch(new AfterSubscribe($this->consumer)); + usleep(1000); + } } }; } diff --git a/src/nats/src/Driver/NatsDriver.php b/src/nats/src/Driver/NatsDriver.php index a95665d79..94a89bbbb 100644 --- a/src/nats/src/Driver/NatsDriver.php +++ b/src/nats/src/Driver/NatsDriver.php @@ -82,12 +82,11 @@ class NatsDriver extends AbstractDriver /** @var NatsConnection $client */ $client = $connection->getConnection(); $channel = new Channel(1); - $timeout = floatval($this->config['timeout'] ?? 1.0); $client->request($subject, $payload, function (Message $message) use ($channel) { $channel->push($message); }); - $message = $channel->pop($timeout); + $message = $channel->pop(0.001); if (! $message instanceof Message) { throw new TimeoutException('Request timeout.'); } diff --git a/src/nats/src/Event/AfterConsume.php b/src/nats/src/Event/AfterConsume.php new file mode 100644 index 000000000..1806a8c62 --- /dev/null +++ b/src/nats/src/Event/AfterConsume.php @@ -0,0 +1,17 @@ +data = $data; + } + + /** + * @return mixed + */ + public function getData() + { + return $this->data; + } +} diff --git a/src/nats/src/Event/Event.php b/src/nats/src/Event/Event.php new file mode 100644 index 000000000..855a3c32a --- /dev/null +++ b/src/nats/src/Event/Event.php @@ -0,0 +1,33 @@ +consumer = $consumer; + } + + public function getConsumer(): AbstractConsumer + { + return $this->consumer; + } +} diff --git a/src/nats/src/Event/FailToConsume.php b/src/nats/src/Event/FailToConsume.php new file mode 100644 index 000000000..9deaac599 --- /dev/null +++ b/src/nats/src/Event/FailToConsume.php @@ -0,0 +1,34 @@ +throwable = $throwable; + } + + public function getThrowable(): \Throwable + { + return $this->throwable; + } +} diff --git a/src/nats/src/Listener/AfterSubscribeListener.php b/src/nats/src/Listener/AfterSubscribeListener.php new file mode 100644 index 000000000..56d5efc2c --- /dev/null +++ b/src/nats/src/Listener/AfterSubscribeListener.php @@ -0,0 +1,48 @@ +logger = $logger; + } + + public function listen(): array + { + return [ + AfterSubscribe::class, + ]; + } + + /** + * @param AfterSubscribe $event + */ + public function process(object $event) + { + $this->logger->warning(sprintf( + 'NatsConsumer[%s] subscribe timeout. Try again after 1 ms.', + $event->getConsumer()->getName() + )); + } +} diff --git a/src/process/src/AbstractProcess.php b/src/process/src/AbstractProcess.php index 092477f35..e2548e513 100644 --- a/src/process/src/AbstractProcess.php +++ b/src/process/src/AbstractProcess.php @@ -18,6 +18,8 @@ use Hyperf\ExceptionHandler\Formatter\FormatterInterface; use Hyperf\Process\Event\AfterProcessHandle; use Hyperf\Process\Event\BeforeProcessHandle; use Hyperf\Process\Event\PipeMessage; +use Hyperf\Process\Exception\SocketAcceptException; +use Hyperf\Utils\Coroutine; use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; use Swoole\Coroutine\Channel; @@ -111,6 +113,8 @@ abstract class AbstractProcess implements ProcessInterface $this->handle(); $this->event && $this->event->dispatch(new AfterProcessHandle($this, $i)); + } catch (\Throwable $throwable) { + $this->logThrowable($throwable); } finally { if (isset($quit)) { $quit->push(true); @@ -131,24 +135,45 @@ abstract class AbstractProcess implements ProcessInterface */ protected function listen(Channel $quit) { - go(function () use ($quit) { + Coroutine::create(function () use ($quit) { while ($quit->pop(0.001) !== true) { try { /** @var \Swoole\Coroutine\Socket $sock */ $sock = $this->process->exportSocket(); $recv = $sock->recv($this->recvLength, $this->recvTimeout); + if ($recv === '') { + throw new SocketAcceptException('Socket is closed', $sock->errCode); + } + + if ($recv === false && $sock->errCode !== SOCKET_ETIMEDOUT) { + throw new SocketAcceptException('Socket is closed', $sock->errCode); + } + if ($this->event && $recv !== false && $data = unserialize($recv)) { $this->event->dispatch(new PipeMessage($data)); } } 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)); + $this->logThrowable($exception); + if ($exception instanceof SocketAcceptException) { + // TODO: Reconnect the socket. + break; } } } $quit->close(); }); } + + protected function logThrowable(\Throwable $throwable): void + { + 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($throwable)); + + if ($throwable instanceof SocketAcceptException) { + $logger->critical('Socket of process is unavailable, please restart the server'); + } + } + } } diff --git a/src/process/src/Exception/SocketAcceptException.php b/src/process/src/Exception/SocketAcceptException.php new file mode 100644 index 000000000..be249e2e2 --- /dev/null +++ b/src/process/src/Exception/SocketAcceptException.php @@ -0,0 +1,21 @@ +getCode() === SOCKET_ETIMEDOUT; + } +} diff --git a/src/redis/src/RedisConnection.php b/src/redis/src/RedisConnection.php index 6a05e47c0..b91c9d455 100644 --- a/src/redis/src/RedisConnection.php +++ b/src/redis/src/RedisConnection.php @@ -158,7 +158,7 @@ class RedisConnection extends BaseConnection implements ConnectionInterface $redis = new \RedisCluster($name, $seeds, $timeout); } catch (\Throwable $e) { - throw new ConnectionException('Connection reconnect failed. ' . $e->getMessage()); + throw new ConnectionException('Connection reconnect failed ' . $e->getMessage()); } return $redis; @@ -167,7 +167,7 @@ class RedisConnection extends BaseConnection implements ConnectionInterface protected function retry($name, $arguments, \Throwable $exception) { $logger = $this->container->get(StdoutLoggerInterface::class); - $logger->warning(sprintf('Redis::__call failed, bacause ' . $exception->getMessage())); + $logger->warning(sprintf('Redis::__call failed, because ' . $exception->getMessage())); try { $this->reconnect(); diff --git a/src/redis/src/RedisFactory.php b/src/redis/src/RedisFactory.php index 6630b1f5a..57067a5b7 100644 --- a/src/redis/src/RedisFactory.php +++ b/src/redis/src/RedisFactory.php @@ -38,7 +38,7 @@ class RedisFactory { $proxy = $this->proxies[$poolName] ?? null; if (! $proxy instanceof RedisProxy) { - throw new InvalidRedisProxyException('Redis proxy is invalid.'); + throw new InvalidRedisProxyException('Invalid Redis proxy.'); } return $proxy; diff --git a/src/server/src/Command/StartServer.php b/src/server/src/Command/StartServer.php index 95aabe758..b12630394 100644 --- a/src/server/src/Command/StartServer.php +++ b/src/server/src/Command/StartServer.php @@ -39,8 +39,6 @@ class StartServer extends Command protected function execute(InputInterface $input, OutputInterface $output) { - Runtime::enableCoroutine(true, swoole_hook_flags()); - $this->checkEnvironment($output); $serverFactory = $this->container->get(ServerFactory::class) @@ -53,6 +51,9 @@ class StartServer extends Command } $serverFactory->configure($serverConfig); + + Runtime::enableCoroutine(true, swoole_hook_flags()); + $serverFactory->start(); } diff --git a/src/session/.gitattributes b/src/session/.gitattributes new file mode 100644 index 000000000..bdd4ea29c --- /dev/null +++ b/src/session/.gitattributes @@ -0,0 +1 @@ +/tests export-ignore \ No newline at end of file diff --git a/src/session/LICENSE.md b/src/session/LICENSE.md new file mode 100644 index 000000000..eb14702a3 --- /dev/null +++ b/src/session/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) Hyperf + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/src/session/composer.json b/src/session/composer.json new file mode 100644 index 000000000..cd2009b03 --- /dev/null +++ b/src/session/composer.json @@ -0,0 +1,53 @@ +{ + "name": "hyperf/session", + "description": "A session library", + "license": "MIT", + "keywords": [ + "php", + "session" + ], + "support": { + }, + "require": { + "php": ">=7.2", + "psr/http-server-middleware": "^1.0", + "hyperf/utils": "^1.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.9", + "malukenho/docheader": "^0.1.6", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "psr/container": "Required to use SessionFactory.", + "hyperf/config": "Required to load session config." + }, + "autoload": { + "psr-4": { + "Hyperf\\Session\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "HyperfTest\\Session\\": "tests/" + } + }, + "config": { + "sort-packages": true + }, + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + }, + "hyperf": { + "config": "Hyperf\\Session\\ConfigProvider" + } + }, + "bin": [ + ], + "scripts": { + "cs-fix": "php-cs-fixer fix $1", + "test": "phpunit --colors=always" + } +} diff --git a/src/session/publish/session.php b/src/session/publish/session.php new file mode 100644 index 000000000..3233a271c --- /dev/null +++ b/src/session/publish/session.php @@ -0,0 +1,22 @@ + Handler\FileHandler::class, + 'options' => [ + 'connection' => 'default', + 'path' => BASE_PATH . '/runtime/session', + 'gc_lifetime' => 1200, + ], +]; diff --git a/src/session/src/ConfigProvider.php b/src/session/src/ConfigProvider.php new file mode 100644 index 000000000..1e0c096e2 --- /dev/null +++ b/src/session/src/ConfigProvider.php @@ -0,0 +1,48 @@ + [ + 'scan' => [ + 'paths' => [ + __DIR__, + ], + ], + ], + 'dependencies' => [ + FileHandler::class => FileHandlerFactory::class, + RedisHandler::class => RedisHandlerFactory::class, + SessionInterface::class => SessionProxy::class, + ], + 'publish' => [ + [ + 'id' => 'config', + 'description' => 'The config of session.', + 'source' => __DIR__ . '/../publish/session.php', + 'destination' => BASE_PATH . '/config/autoload/session.php', + ], + ], + ]; + } +} diff --git a/src/session/src/FlashTrait.php b/src/session/src/FlashTrait.php new file mode 100644 index 000000000..be56b75e1 --- /dev/null +++ b/src/session/src/FlashTrait.php @@ -0,0 +1,100 @@ +put($key, $value); + + $this->push('_flash.new', $key); + + $this->removeFromOldFlashData([$key]); + } + + /** + * Flash a key / value pair to the session for immediate use. + * @param mixed $value + */ + public function now(string $key, $value): void + { + $this->put($key, $value); + + $this->push('_flash.old', $key); + } + + /** + * Reflash all of the session flash data. + */ + public function reflash(): void + { + $this->mergeNewFlashes($this->get('_flash.old', [])); + + $this->put('_flash.old', []); + } + + /** + * Reflash a subset of the current flash data. + * + * @param array|mixed $keys + */ + public function keep($keys = null): void + { + $this->mergeNewFlashes($keys = is_array($keys) ? $keys : func_get_args()); + + $this->removeFromOldFlashData($keys); + } + + /** + * Flash an input array to the session. + */ + public function flashInput(array $value): void + { + $this->flash('_old_input', $value); + } + + /** + * Age the flash data for the session. + */ + public function ageFlashData(): void + { + $this->forget($this->get('_flash.old', [])); + + $this->put('_flash.old', $this->get('_flash.new', [])); + + $this->put('_flash.new', []); + } + + /** + * Merge new flash keys into the new flash array. + */ + protected function mergeNewFlashes(array $keys): void + { + $values = array_unique(array_merge($this->get('_flash.new', []), $keys)); + + $this->put('_flash.new', $values); + } + + /** + * Remove the given keys from the old flash data. + */ + protected function removeFromOldFlashData(array $keys): void + { + $this->put('_flash.old', array_diff($this->get('_flash.old', []), $keys)); + } +} diff --git a/src/session/src/Handler/FileHandler.php b/src/session/src/Handler/FileHandler.php new file mode 100644 index 000000000..d33071c7d --- /dev/null +++ b/src/session/src/Handler/FileHandler.php @@ -0,0 +1,176 @@ +files = $files; + $this->path = $path; + $this->minutes = $minutes; + if (! file_exists($path)) { + $files->makeDirectory($path, 0755, true); + } + } + + /** + * Close the session. + * + * @see https://php.net/manual/en/sessionhandlerinterface.close.php + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function close() + { + return true; + } + + /** + * Destroy a session. + * + * @see https://php.net/manual/en/sessionhandlerinterface.destroy.php + * @param string $session_id the session ID being destroyed + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function destroy($session_id) + { + $this->files->delete($this->path . '/' . $session_id); + return true; + } + + /** + * Cleanup old sessions. + * + * @see https://php.net/manual/en/sessionhandlerinterface.gc.php + * @param int $maxlifetime

+ * Sessions that have not updated for + * the last maxlifetime seconds will be removed. + *

+ * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function gc($maxlifetime) + { + $files = Finder::create() + ->in($this->path) + ->files() + ->ignoreDotFiles(true) + ->date('<= now - ' . $maxlifetime . ' seconds'); + + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + $this->files->delete($file->getRealPath()); + } + return true; + } + + /** + * Initialize session. + * + * @see https://php.net/manual/en/sessionhandlerinterface.open.php + * @param string $save_path the path where to store/retrieve the session + * @param string $name the session name + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function open($save_path, $name) + { + return true; + } + + /** + * Read session data. + * + * @see https://php.net/manual/en/sessionhandlerinterface.read.php + * @param string $session_id the session id to read data for + * @return string

+ * Returns an encoded string of the read data. + * If nothing was read, it must return an empty string. + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function read($session_id) + { + if ($this->files->isFile($path = $this->path . '/' . $session_id)) { + if ($this->files->lastModified($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp()) { + return $this->files->sharedGet($path); + } + } + + return ''; + } + + /** + * Write session data. + * + * @see https://php.net/manual/en/sessionhandlerinterface.write.php + * @param string $session_id the session id + * @param string $session_data

+ * The encoded session data. This data is the + * result of the PHP internally encoding + * the $_SESSION superglobal to a serialized + * string and passing it as this parameter. + * Please note sessions use an alternative serialization method. + *

+ * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function write($session_id, $session_data) + { + $this->files->put($this->path . '/' . $session_id, $session_data, true); + + return true; + } +} diff --git a/src/session/src/Handler/FileHandlerFactory.php b/src/session/src/Handler/FileHandlerFactory.php new file mode 100644 index 000000000..b118358b1 --- /dev/null +++ b/src/session/src/Handler/FileHandlerFactory.php @@ -0,0 +1,31 @@ +get(ConfigInterface::class); + $path = $config->get('session.options.path'); + $minutes = $config->get('session.options.gc_maxlifetime', 1200); + if (! $path) { + throw new \InvalidArgumentException('Invalid session path.'); + } + return new FileHandler($container->get(Filesystem::class), $path, $minutes); + } +} diff --git a/src/session/src/Handler/NullHandler.php b/src/session/src/Handler/NullHandler.php new file mode 100644 index 000000000..96243d572 --- /dev/null +++ b/src/session/src/Handler/NullHandler.php @@ -0,0 +1,125 @@ + + * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function close() + { + return true; + } + + /** + * Destroy a session. + * + * @see https://php.net/manual/en/sessionhandlerinterface.destroy.php + * @param string $session_id the session ID being destroyed + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function destroy($session_id) + { + return true; + } + + /** + * Cleanup old sessions. + * + * @see https://php.net/manual/en/sessionhandlerinterface.gc.php + * @param int $maxlifetime

+ * Sessions that have not updated for + * the last maxlifetime seconds will be removed. + *

+ * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function gc($maxlifetime) + { + return true; + } + + /** + * Initialize session. + * + * @see https://php.net/manual/en/sessionhandlerinterface.open.php + * @param string $save_path the path where to store/retrieve the session + * @param string $name the session name + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function open($save_path, $name) + { + return true; + } + + /** + * Read session data. + * + * @see https://php.net/manual/en/sessionhandlerinterface.read.php + * @param string $session_id the session id to read data for + * @return string

+ * Returns an encoded string of the read data. + * If nothing was read, it must return an empty string. + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function read($session_id) + { + return ''; + } + + /** + * Write session data. + * + * @see https://php.net/manual/en/sessionhandlerinterface.write.php + * @param string $session_id the session id + * @param string $session_data

+ * The encoded session data. This data is the + * result of the PHP internally encoding + * the $_SESSION superglobal to a serialized + * string and passing it as this parameter. + * Please note sessions use an alternative serialization method. + *

+ * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function write($session_id, $session_data) + { + return true; + } +} diff --git a/src/session/src/Handler/RedisHandler.php b/src/session/src/Handler/RedisHandler.php new file mode 100644 index 000000000..a0c479f35 --- /dev/null +++ b/src/session/src/Handler/RedisHandler.php @@ -0,0 +1,147 @@ +redis = $redis; + $this->gcMaxLifeTime = $gcMaxLifeTime; + } + + /** + * Close the session. + * + * @see https://php.net/manual/en/sessionhandlerinterface.close.php + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function close() + { + return true; + } + + /** + * Destroy a session. + * + * @see https://php.net/manual/en/sessionhandlerinterface.destroy.php + * @param string $session_id the session ID being destroyed + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function destroy($session_id) + { + $this->redis->del($session_id); + return true; + } + + /** + * Cleanup old sessions. + * + * @see https://php.net/manual/en/sessionhandlerinterface.gc.php + * @param int $maxlifetime

+ * Sessions that have not updated for + * the last maxlifetime seconds will be removed. + *

+ * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function gc($maxlifetime) + { + return true; + } + + /** + * Initialize session. + * + * @see https://php.net/manual/en/sessionhandlerinterface.open.php + * @param string $save_path the path where to store/retrieve the session + * @param string $name the session name + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function open($save_path, $name) + { + return true; + } + + /** + * Read session data. + * + * @see https://php.net/manual/en/sessionhandlerinterface.read.php + * @param string $session_id the session id to read data for + * @return string

+ * Returns an encoded string of the read data. + * If nothing was read, it must return an empty string. + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function read($session_id) + { + return $this->redis->get($session_id) ?: ''; + } + + /** + * Write session data. + * + * @see https://php.net/manual/en/sessionhandlerinterface.write.php + * @param string $session_id the session id + * @param string $session_data

+ * The encoded session data. This data is the + * result of the PHP internally encoding + * the $_SESSION superglobal to a serialized + * string and passing it as this parameter. + * Please note sessions use an alternative serialization method. + *

+ * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function write($session_id, $session_data) + { + return (bool) $this->redis->setEx($session_id, (int) $this->gcMaxLifeTime, $session_data); + } +} diff --git a/src/session/src/Handler/RedisHandlerFactory.php b/src/session/src/Handler/RedisHandlerFactory.php new file mode 100644 index 000000000..5d108f988 --- /dev/null +++ b/src/session/src/Handler/RedisHandlerFactory.php @@ -0,0 +1,30 @@ +get(ConfigInterface::class); + $connection = $config->get('session.options.connection'); + $gcMaxLifetime = $config->get('session.options.gc_maxlifetime', 1200); + $redisFactory = $container->get(RedisFactory::class); + $redis = $redisFactory->get($connection); + return new RedisHandler($redis, $gcMaxLifetime); + } +} diff --git a/src/session/src/Middleware/SessionMiddleware.php b/src/session/src/Middleware/SessionMiddleware.php new file mode 100644 index 000000000..3350098d8 --- /dev/null +++ b/src/session/src/Middleware/SessionMiddleware.php @@ -0,0 +1,137 @@ +sessionManager = $sessionManager; + $this->config = $config; + } + + /** + * Process an incoming server request. + * Processes an incoming server request in order to produce a response. + * If unable to produce the response itself, it may delegate to the provided + * request handler to do so. + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if (! $this->isSessionAvailable()) { + return $handler->handle($request); + } + + $session = $this->sessionManager->start($request); + + $response = $handler->handle($request); + + $this->storeCurrentUrl($request, $session); + + $response = $this->addCookieToResponse($request, $response, $session); + + // @TODO Use defer + $this->sessionManager->end($session); + + return $response; + } + + private function isSessionAvailable(): bool + { + return $this->config->has('session.handler'); + } + + /** + * Get the URL (no query string) for the request. + */ + private function url(RequestInterface $request): string + { + return rtrim(preg_replace('/\?.*/', '', $request->getUri()), '/'); + } + + /** + * Store the current URL for the request if necessary. + */ + private function storeCurrentUrl(RequestInterface $request, SessionInterface $session) + { + if ($request->getMethod() === 'GET') { + $session->setPreviousUrl($this->fullUrl($request)); + } + } + + /** + * Get the session lifetime in seconds. + */ + private function getCookieExpirationDate(): int + { + if ($this->config->get('session.options.expire_on_close')) { + $expirationDate = 0; + } else { + $expirationDate = Carbon::now()->addMinutes(5 * 60)->getTimestamp(); + } + return $expirationDate; + } + + /** + * Add the session cookie to the response·. + */ + private function addCookieToResponse( + ServerRequestInterface $request, + ResponseInterface $response, + SessionInterface $session + ): ResponseInterface { + $uri = $request->getUri(); + $path = '/'; + $domain = $uri->getHost(); + $secure = strtolower($uri->getScheme()) === 'https'; + $httpOnly = true; + if (! method_exists($response, 'withCookie')) { + // @TODO Adapte original response object. + throw new \RuntimeException('Unsupport response object.'); + } + /* @var \Hyperf\HttpMessage\Server\Response $response */ + return $response->withCookie(new Cookie($session->getName(), $session->getId(), $this->getCookieExpirationDate(), $path, $domain, $secure, $httpOnly)); + } + + /** + * Get the full URL for the request. + */ + private function fullUrl(RequestInterface $request): string + { + $uri = $request->getUri(); + $query = $uri->getQuery(); + $question = $uri->getHost() . $uri->getPath() == '/' ? '/?' : '?'; + return $query ? $this->url($request) . $question . $query : $this->url($request); + } +} diff --git a/src/session/src/Session.php b/src/session/src/Session.php new file mode 100644 index 000000000..020059f5f --- /dev/null +++ b/src/session/src/Session.php @@ -0,0 +1,372 @@ +isValidId($id)) { + $id = $this->generateSessionId(); + } + $this->setId($id); + $this->setName($name); + $this->handler = $handler; + } + + /** + * Determine if this is a valid session ID. + */ + public function isValidId(string $id): bool + { + return is_string($id) && ctype_alnum($id) && strlen($id) === 40; + } + + /** + * Starts the session storage. + */ + public function start(): bool + { + $this->loadSession(); + + return $this->started = true; + } + + /** + * Returns the session ID. + * + * @return string The session ID + */ + public function getId(): string + { + return $this->id; + } + + /** + * Sets the session ID. + */ + public function setId(string $id): void + { + $this->id = $id; + } + + /** + * Returns the session name. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Sets the session name. + */ + public function setName(string $name): void + { + $this->name = $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 + { + $this->clear(); + + return $this->migrate(true, $lifetime); + } + + /** + * 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 + { + if ($destroy) { + $this->handler->destroy($this->getId()); + } + + $this->setId($this->generateSessionId()); + + return true; + } + + /** + * 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 + { + $this->ageFlashData(); + + $this->handler->write($this->getId(), $this->prepareForStorage(serialize($this->attributes))); + + $this->started = false; + } + + /** + * 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 + { + return Arr::exists($this->attributes, $name); + } + + /** + * 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) + { + return data_get($this->attributes, $name, $default); + } + + /** + * Sets an attribute. + * + * @param mixed $value + */ + public function set(string $name, $value): void + { + data_set($this->attributes, $name, $value); + } + + /** + * 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 + { + if (! is_array($key)) { + $key = [$key => $value]; + } + + foreach ($key as $arrayKey => $arrayValue) { + Arr::set($this->attributes, $arrayKey, $arrayValue); + } + } + + /** + * Returns attributes. + */ + public function all(): array + { + return $this->attributes; + } + + /** + * Sets attributes. + */ + public function replace(array $attributes): void + { + foreach ($attributes as $name => $value) { + data_set($this->attributes, $name, $value); + } + } + + /** + * Removes an attribute, returning its value. + * + * @return mixed The removed value or null when it does not exist + */ + public function remove(string $name) + { + return Arr::pull($this->attributes, $name); + } + + /** + * Remove one or many items from the session. + * + * @param array|string $keys + */ + public function forget($keys): void + { + Arr::forget($this->attributes, $keys); + } + + /** + * Clears all attributes. + */ + public function clear(): void + { + $this->attributes = []; + } + + /** + * Checks if the session was started. + */ + public function isStarted(): bool + { + return $this->started; + } + + /** + * Get the CSRF token value. + */ + public function token(): string + { + return (string) $this->get('_token'); + } + + /** + * Regenerate the CSRF token value. + */ + public function regenerateToken(): string + { + $this->put('_token', $token = Str::random(40)); + return $token; + } + + /** + * Get the previous URL from the session. + */ + public function previousUrl(): ?string + { + $previousUrl = $this->get('_previous.url'); + if (! is_string($previousUrl)) { + $previousUrl = null; + } + return $previousUrl; + } + + /** + * Set the "previous" URL in the session. + */ + public function setPreviousUrl(string $url): void + { + $this->set('_previous.url', $url); + } + + /** + * Push a value onto a session array. + * + * @param mixed $value + */ + public function push(string $key, $value): void + { + $array = $this->get($key, []); + + $array[] = $value; + + $this->put($key, $array); + } + + /** + * Generate a new random sessoion ID. + */ + protected function generateSessionId(): string + { + return Str::random(40); + } + + /** + * Load the session data from the handler. + */ + protected function loadSession(): void + { + $this->attributes = array_merge($this->attributes, $this->readFromHandler()); + } + + /** + * Read the session data from the handler. + */ + protected function readFromHandler(): array + { + if ($data = $this->handler->read($this->getId())) { + $data = @unserialize($this->prepareForUnserialize($data)); + + if ($data !== false && ! is_null($data) && is_array($data)) { + return $data; + } + } + + return []; + } + + /** + * Prepare the raw string data from the session for unserialization. + */ + protected function prepareForUnserialize(string $data): string + { + return $data; + } + + /** + * Prepare the serialized session data for storage. + */ + protected function prepareForStorage(string $data): string + { + return $data; + } +} diff --git a/src/session/src/SessionManager.php b/src/session/src/SessionManager.php new file mode 100644 index 000000000..390f98f8a --- /dev/null +++ b/src/session/src/SessionManager.php @@ -0,0 +1,92 @@ +container = $container; + $this->config = $config; + } + + public function getSessionName(): string + { + return 'HYPERF_SESSION_ID'; + } + + public function start(ServerRequestInterface $request): SessionInterface + { + $sessionId = $this->parseSessionId($request); + // @TODO Use make() function to create Session object. + $session = new Session($this->getSessionName(), $this->buildSessionHandler(), $sessionId); + if (! $session->start()) { + throw new \RuntimeException('Start session failed.'); + } + $this->setSession($session); + return $session; + } + + public function end(SessionInterface $session): void + { + $session->save(); + } + + public function getSession(): SessionInterface + { + return Context::get(SessionInterface::class); + } + + public function setSession(SessionInterface $session): self + { + Context::set(SessionInterface::class, $session); + return $this; + } + + protected function parseSessionId(ServerRequestInterface $request): ?string + { + $cookies = $request->getCookieParams(); + foreach ($cookies as $key => $value) { + if ($key === $this->getSessionName()) { + return (string) $value; + } + } + return null; + } + + protected function buildSessionHandler(): SessionHandlerInterface + { + $handler = $this->config->get('session.handler'); + if (! $handler || ! class_exists($handler)) { + throw new \InvalidArgumentException('Invalid handler of session'); + } + return $this->container->get($handler); + } +} diff --git a/src/session/src/SessionProxy.php b/src/session/src/SessionProxy.php new file mode 100644 index 000000000..c25faee53 --- /dev/null +++ b/src/session/src/SessionProxy.php @@ -0,0 +1,214 @@ +getSession()->flash($key, $value); + } + + public function now(string $key, $value): void + { + $this->getSession()->now($key, $value); + } + + public function reflash(): void + { + $this->getSession()->reflash(); + } + + public function keep($keys = null): void + { + $this->getSession()->keep($keys); + } + + public function flashInput(array $value): void + { + $this->getSession()->flashInput($value); + } + + public function ageFlashData(): void + { + $this->getSession()->ageFlashData(); + } + + public function isValidId(string $id): bool + { + return $this->getSession()->isValidId($id); + } + + public function start(): bool + { + return $this->getSession()->start(); + } + + public function getId(): string + { + return $this->getSession()->getId(); + } + + public function setId(string $id): void + { + $this->getSession()->setId($id); + } + + public function getName(): string + { + return $this->getSession()->getName(); + } + + public function setName(string $name): void + { + $this->getSession()->setName($name); + } + + public function invalidate(?int $lifetime = null): bool + { + return $this->getSession()->invalidate($lifetime); + } + + public function migrate(bool $destroy = false, ?int $lifetime = null): bool + { + return $this->getSession->migrate($destroy, $lifetime); + } + + public function save(): void + { + $this->getSession->save(); + } + + public function has(string $name): bool + { + return $this->getSession->has($name); + } + + public function get(string $name, $default = null) + { + return $this->getSession()->get($name, $default); + } + + public function set(string $name, $value): void + { + $this->getSession()->set($name, $value); + } + + public function put($key, $value = null): void + { + $this->getSession()->put($key, $value); + } + + public function all(): array + { + return $this->getSession()->all(); + } + + public function replace(array $attributes): void + { + $this->getSession()->replace($attributes); + } + + public function remove(string $name) + { + return $this->getSession()->remove($name); + } + + public function forget($keys): void + { + $this->getSession()->forget($keys); + } + + public function clear(): void + { + $this->getSession()->clear(); + } + + public function isStarted(): bool + { + return $this->getSession()->isStarted(); + } + + public function token(): string + { + return $this->getSession()->token(); + } + + public function regenerateToken(): string + { + return $this->getSession()->regenerateToken(); + } + + public function previousUrl(): ?string + { + return $this->getSession()->previousUrl(); + } + + public function setPreviousUrl(string $url): void + { + $this->getSession()->setPreviousUrl($url); + } + + public function push(string $key, $value): void + { + $this->getSession()->push($key, $value); + } + + protected function getSession(): Session + { + return Context::get(SessionInterface::class); + } + + protected function mergeNewFlashes(array $keys): void + { + $this->getSession()->mergeNewFlashes($keys); + } + + protected function removeFromOldFlashData(array $keys): void + { + $this->getSession()->removeFromOldFlashData($keys); + } + + protected function generateSessionId(): string + { + return $this->getSession()->generateSessionId(); + } + + protected function loadSession(): void + { + $this->getSession()->loadSession(); + } + + protected function readFromHandler(): array + { + return $this->getSession()->readFromHandler(); + } + + protected function prepareForUnserialize(string $data): string + { + return $this->getSession()->prepareForUnserialize($data); + } + + protected function prepareForStorage(string $data): string + { + return $this->getSession()->prepareForStorage($data); + } +} diff --git a/src/session/tests/ConfigProviderTest.php b/src/session/tests/ConfigProviderTest.php new file mode 100644 index 000000000..35a939856 --- /dev/null +++ b/src/session/tests/ConfigProviderTest.php @@ -0,0 +1,30 @@ +assertArrayHasKey('annotations', $provider()); + $this->assertArrayHasKey('publish', $provider()); + } +} diff --git a/src/session/tests/FileHandlerTest.php b/src/session/tests/FileHandlerTest.php new file mode 100644 index 000000000..5c811f958 --- /dev/null +++ b/src/session/tests/FileHandlerTest.php @@ -0,0 +1,73 @@ +assertTrue($handler->open('', '')); + $this->assertTrue($handler->close('', '')); + + $id = Str::random(40); + $data = [ + 'int' => 1, + 'true' => true, + 'false' => false, + 'float' => 1.23, + 'string' => 'foo', + 0 => 1, + 'array' => [ + 'int' => 1, + 'true' => true, + 'false' => false, + 'float' => 1.23, + 'string' => 'foo', + 0 => 1, + ], + ]; + $this->assertTrue($handler->write($id, serialize($data))); + $this->assertSame($data, unserialize($handler->read($id))); + $this->assertFileExists('/tmp/' . $id); + $handler->destroy($id); + $this->assertFileNotExists('/tmp/' . $id); + } + + public function testReadNotExistsSessionId() + { + $handler = new FileHandler(new Filesystem(), $path = '/tmp', 10); + $this->assertSame('', $handler->read('not-exist')); + } + + public function testGc() + { + $handler = new FileHandler(new Filesystem(), $path = __DIR__ . '/runtime/session', 1); + $id = Str::random(40); + $handler->write($id, 'foo'); + sleep(1); + $handler->gc(1); + $this->assertFileNotExists($path . '/' . $id); + } +} diff --git a/src/session/tests/SessionManagerTest.php b/src/session/tests/SessionManagerTest.php new file mode 100644 index 000000000..203176a9d --- /dev/null +++ b/src/session/tests/SessionManagerTest.php @@ -0,0 +1,60 @@ +setSession($mockSession = Mockery::mock(Session::class)); + $this->assertInstanceOf(SessionManager::class, $sessionManager); + + $session = $sessionManager->getSession(); + $this->assertInstanceOf(Session::class, $session); + $this->assertSame($mockSession, $session); + } + + public function testParseSessionId() + { + $request = new Request('get', '/'); + $sessionManager = new SessionManager(Mockery::mock(ContainerInterface::class), Mockery::mock(ConfigInterface::class)); + $reflectionClass = new ReflectionClass(SessionManager::class); + $parseSessionIdMethod = $reflectionClass->getMethod('parseSessionId'); + $parseSessionIdMethod->setAccessible(true); + $id = Str::random(40); + $this->assertSame($id, $parseSessionIdMethod->invoke($sessionManager, $request->withCookieParams([ + 'HYPERF_SESSION_ID' => $id, + ]))); + $this->assertSame('123', $parseSessionIdMethod->invoke($sessionManager, $request->withCookieParams([ + 'HYPERF_SESSION_ID' => 123, + ]))); + $this->assertNull($parseSessionIdMethod->invoke($sessionManager, $request->withCookieParams([ + 'foo' => 'bar', + ]))); + } +} diff --git a/src/session/tests/SessionMiddlewareTest.php b/src/session/tests/SessionMiddlewareTest.php new file mode 100644 index 000000000..0a0882d52 --- /dev/null +++ b/src/session/tests/SessionMiddlewareTest.php @@ -0,0 +1,112 @@ +withCookieParams(['HYPERF_SESSION_ID' => 'foo123']); + $requestHandler = Mockery::mock(RequestHandlerInterface::class); + $requestHandler->shouldReceive('handle')->andReturn(new Response()); + + $container = Mockery::mock(ContainerInterface::class); + $container->shouldReceive('has')->with(SessionInterface::class)->andReturnTrue(); + $container->shouldReceive('has')->with(FileHandler::class)->andReturnTrue(); + $fileHandler = new FileHandler(new Filesystem(), '/tmp', 10); + $container->shouldReceive('get')->with(FileHandler::class)->andReturn($fileHandler); + /** @var Session $session */ + $session = new Session('HYPERF_SESSION_ID', $fileHandler); + $container->shouldReceive('get')->with(SessionInterface::class)->andReturn($session); + $container->shouldReceive('get')->with(FooHandler::class)->andReturn(new FooHandler()); + + $config = Mockery::mock(ConfigInterface::class); + $config->shouldReceive('get')->with('session.handler')->andReturn(FooHandler::class); + $config->shouldReceive('has')->with('session.handler')->andReturn(true); + $config->shouldReceive('get')->with('session.options.expire_on_close')->andReturn(1); + $sessionManager = new SessionManager($container, $config); + $middleware = new SessionMiddleware($sessionManager, $config); + $response = $middleware->process($request, $requestHandler); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertInstanceOf(SessionInterface::class, $session = Context::get(SessionInterface::class)); + $this->assertIsString($session->getId()); + $this->assertTrue(ctype_alnum($session->getId())); + $this->assertSame(40, strlen($session->getId())); + $this->assertArrayHasKey('', $response->getCookies()); + $this->assertArrayHasKey('/', $response->getCookies()['']); + $this->assertArrayHasKey($session->getName(), $response->getCookies()['']['/']); + $this->assertInstanceOf(Cookie::class, $response->getCookies()['']['/'][$session->getName()]); + /** @var Cookie $cookie */ + $cookie = $response->getCookies()['']['/'][$session->getName()]; + $this->assertSame('HYPERF_SESSION_ID', $cookie->getName()); + $this->assertSame($session->getId(), $cookie->getValue()); + $this->assertSame('/', $cookie->getPath()); + $this->assertSame(0, $cookie->getExpiresTime()); + } + + public function testSessionWithExpiresTime() + { + $request = new Request('GET', '/test'); + $request = $request->withCookieParams(['HYPERF_SESSION_ID' => 'foo123']); + $requestHandler = Mockery::mock(RequestHandlerInterface::class); + $requestHandler->shouldReceive('handle')->andReturn(new Response()); + + $container = Mockery::mock(ContainerInterface::class); + $container->shouldReceive('has')->with(SessionInterface::class)->andReturnTrue(); + $container->shouldReceive('has')->with(FileHandler::class)->andReturnTrue(); + $fileHandler = new FileHandler(new Filesystem(), '/tmp', 10); + $container->shouldReceive('get')->with(FileHandler::class)->andReturn($fileHandler); + /** @var Session $session */ + $session = new Session('HYPERF_SESSION_ID', $fileHandler); + $container->shouldReceive('get')->with(SessionInterface::class)->andReturn($session); + $container->shouldReceive('get')->with(FooHandler::class)->andReturn(new FooHandler()); + + $config = Mockery::mock(ConfigInterface::class); + $config->shouldReceive('get')->with('session.handler')->andReturn(FooHandler::class); + $config->shouldReceive('has')->with('session.handler')->andReturn(true); + $config->shouldReceive('get')->with('session.options.expire_on_close')->andReturn(0); + $sessionManager = new SessionManager($container, $config); + $middleware = new SessionMiddleware($sessionManager, $config); + $time = time(); + $response = $middleware->process($request, $requestHandler); + + /** @var Cookie $cookie */ + $cookie = $response->getCookies()['']['/'][$session->getName()]; + $this->assertSame($time + (5 * 60 * 60), $cookie->getExpiresTime()); + } +} diff --git a/src/session/tests/SessionTest.php b/src/session/tests/SessionTest.php new file mode 100644 index 000000000..7e6312b94 --- /dev/null +++ b/src/session/tests/SessionTest.php @@ -0,0 +1,161 @@ +assertSame($name, $session->getName()); + $this->assertSame($id, $session->getId()); + $this->assertTrue($session->isValidId($id)); + + $session = new Session('HYPERF_SESSION_ID', new NullHandler()); + $this->assertTrue($session->isValidId($session->getId())); + } + + public function testSessionAttributes() + { + $id = Str::random(40); + $session = new Session('HYPERF_SESSION_ID', new NullHandler(), $id); + $data = [ + 'int' => 1, + 'true' => true, + 'false' => false, + 'float' => 1.23, + 'string' => 'foo', + ]; + foreach ($data as $key => $value) { + $session->set($key, $value); + } + foreach ($data as $key => $value) { + $this->assertTrue($session->has($key)); + $this->assertSame($value, $session->get($key)); + } + $this->assertFalse($session->has('not-exist')); + $this->assertSame($data, $session->all()); + + foreach ($data as $key => $value) { + $this->assertSame($value, $session->remove($key)); + } + + $session->put($data); + $this->assertSame($data, $session->all()); + + $session->clear(); + $this->assertSame([], $session->all()); + + $session->put('foo', 'bar'); + $this->assertSame(['foo' => 'bar'], $session->all()); + + $session->put($data); + $session->forget('foo'); + $this->assertSame($data, $session->all()); + + $session->replace(['int' => 2, 'foo' => 'baz']); + $this->assertSame([ + 'int' => 2, + 'true' => true, + 'false' => false, + 'float' => 1.23, + 'string' => 'foo', + 'foo' => 'baz', + ], $session->all()); + } + + public function testSessionPush() + { + $session = new Session('HYPERF_SESSION_ID', new NullHandler()); + $session->push('foo', 'bar'); + $this->assertSame([ + 'foo' => ['bar'], + ], $session->all()); + $session->push('foo', 'baz'); + $this->assertSame([ + 'foo' => ['bar', 'baz'], + ], $session->all()); + } + + public function testToken() + { + $session = new Session('HYPERF_SESSION_ID', new NullHandler()); + $this->assertSame('', $session->token()); + + $token = $session->regenerateToken(); + $this->assertSame($token, $session->token()); + $this->assertTrue($session->isValidId($token)); + } + + public function testPreviousUrl() + { + $session = new Session('HYPERF_SESSION_ID', new NullHandler()); + $url = 'http://127.0.0.1:9501/foo/bar'; + $session->setPreviousUrl($url); + $this->assertSame($url, $session->previousUrl()); + + $session->set('_previous.url', 123); + $this->assertNull($session->previousUrl()); + } + + public function testStartSession() + { + $fileHandler = new FileHandler(new Filesystem(), '/tmp', 1); + $session = new Session('HYPERF_SESSION_ID', $fileHandler); + $this->assertTrue($session->start()); + $this->assertTrue($session->isStarted()); + + $id = $session->getId(); + $session->set('foo', 'bar'); + $session->save(); + $this->assertFileExists('/tmp/' . $id); + + $session = new Session('HYPERF_SESSION_ID', $fileHandler, $id); + $session->start(); + $this->assertSame('bar', $session->get('foo')); + + $this->assertTrue($session->invalidate()); + $this->assertFileNotExists('/tmp/' . $id); + } + + public function testFlash() + { + $fileHandler = new FileHandler(new Filesystem(), '/tmp', 1); + $session = new Session('HYPERF_SESSION_ID', $fileHandler); + $id = $session->getId(); + $session->flash('foo', 'bar'); + $this->assertSame([ + 'foo' => 'bar', + '_flash' => [ + 'new' => ['foo'], + 'old' => [], + ], + ], $session->all()); + $session->save(); + + $session = new Session('HYPERF_SESSION_ID', $fileHandler, $id); + $this->assertSame([], $session->all()); + } +} diff --git a/src/session/tests/Stub/FooHandler.php b/src/session/tests/Stub/FooHandler.php new file mode 100644 index 000000000..3cb8e171d --- /dev/null +++ b/src/session/tests/Stub/FooHandler.php @@ -0,0 +1,119 @@ + + * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function close() + { + } + + /** + * Destroy a session. + * + * @see https://php.net/manual/en/sessionhandlerinterface.destroy.php + * @param string $session_id the session ID being destroyed + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function destroy($session_id) + { + } + + /** + * Cleanup old sessions. + * + * @see https://php.net/manual/en/sessionhandlerinterface.gc.php + * @param int $maxlifetime

+ * Sessions that have not updated for + * the last maxlifetime seconds will be removed. + *

+ * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function gc($maxlifetime) + { + } + + /** + * Initialize session. + * + * @see https://php.net/manual/en/sessionhandlerinterface.open.php + * @param string $save_path the path where to store/retrieve the session + * @param string $name the session name + * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function open($save_path, $name) + { + } + + /** + * Read session data. + * + * @see https://php.net/manual/en/sessionhandlerinterface.read.php + * @param string $session_id the session id to read data for + * @return string

+ * Returns an encoded string of the read data. + * If nothing was read, it must return an empty string. + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function read($session_id) + { + } + + /** + * Write session data. + * + * @see https://php.net/manual/en/sessionhandlerinterface.write.php + * @param string $session_id the session id + * @param string $session_data

+ * The encoded session data. This data is the + * result of the PHP internally encoding + * the $_SESSION superglobal to a serialized + * string and passing it as this parameter. + * Please note sessions use an alternative serialization method. + *

+ * @return bool

+ * The return value (usually TRUE on success, FALSE on failure). + * Note this value is returned internally to PHP for processing. + *

+ * @since 5.4.0 + */ + public function write($session_id, $session_data) + { + } +} diff --git a/src/session/tests/Stub/NonSessionHandler.php b/src/session/tests/Stub/NonSessionHandler.php new file mode 100644 index 000000000..34382c327 --- /dev/null +++ b/src/session/tests/Stub/NonSessionHandler.php @@ -0,0 +1,17 @@ +localeArray($locale) - : [$locale ?: $this->locale]; + : [$locale ?: $this->locale()]; foreach ($locales as $locale) { if (! is_null($line = $this->getLine( @@ -138,7 +139,7 @@ class Translator implements TranslatorInterface */ public function getFromJson(string $key, array $replace = [], ?string $locale = null) { - $locale = $locale ?: $this->locale; + $locale = $locale ?: $this->locale(); // For JSON translations, there is only one file per locale, so we will simply load // that file and then we will be ready to check the array for the key. These are @@ -315,12 +316,22 @@ class Translator implements TranslatorInterface return $this->getLocale(); } + /** + * Get the context locale key. + */ + public function getLocaleContextKey(): string + { + return sprintf('%s::%s', TranslatorInterface::class, 'locale'); + } + /** * Get the default locale being used. */ public function getLocale(): string { - return $this->locale; + $locale = Context::get($this->getLocaleContextKey()); + + return (string) ($locale ?? $this->locale); } /** @@ -328,7 +339,7 @@ class Translator implements TranslatorInterface */ public function setLocale(string $locale) { - $this->locale = $locale; + Context::set($this->getLocaleContextKey(), $locale); } /** @@ -371,7 +382,7 @@ class Translator implements TranslatorInterface */ protected function localeForChoice(?string $locale): string { - return $locale ?: $this->locale ?: $this->fallback; + return $locale ?: $this->locale() ?: $this->fallback; } /** @@ -453,16 +464,13 @@ class Translator implements TranslatorInterface */ protected function localeArray(?string $locale): array { - return array_filter([$locale ?: $this->locale, $this->fallback]); + return array_filter([$locale ?: $this->locale(), $this->fallback]); } /** * Parse an array of basic segments. - * - * @param array $segments - * @return array */ - protected function parseBasicSegments(array $segments) + protected function parseBasicSegments(array $segments): array { // The first segment in a basic array will always be the group, so we can go // ahead and grab that segment. If there is only one total segment we are diff --git a/src/translation/tests/TranslatorTest.php b/src/translation/tests/TranslatorTest.php index dda21efe2..9ec70c08c 100755 --- a/src/translation/tests/TranslatorTest.php +++ b/src/translation/tests/TranslatorTest.php @@ -286,6 +286,25 @@ class TranslatorTest extends TestCase $this->assertEquals('foo baz', $t->getFromJson('foo :message', ['message' => 'baz'])); } + public function testSetLocale() + { + $t = new Translator($this->getLoader(), 'en'); + $this->assertEquals('en', $t->getLocale()); + parallel([ + function () use ($t) { + $this->assertEquals('en', $t->getLocale()); + $t->setLocale('zh_CN'); + $this->assertEquals('zh_CN', $t->getLocale()); + }, + function () use ($t) { + $this->assertEquals('en', $t->getLocale()); + $t->setLocale('zh_HK'); + $this->assertEquals('zh_HK', $t->getLocale()); + }, + ]); + $this->assertEquals('en', $t->getLocale()); + } + protected function getLoader() { return Mockery::mock(TranslatorLoaderInterface::class); diff --git a/src/utils/src/Exception/ParallelExecutionException.php b/src/utils/src/Exception/ParallelExecutionException.php new file mode 100644 index 000000000..22f9a0681 --- /dev/null +++ b/src/utils/src/Exception/ParallelExecutionException.php @@ -0,0 +1,46 @@ +results; + } + + public function setResults(array $results) + { + $this->results = $results; + } + + public function getThrowables() + { + return $this->throwables; + } + + public function setThrowables(array $throwables) + { + return $this->throwables = $throwables; + } +} diff --git a/src/utils/src/Parallel.php b/src/utils/src/Parallel.php index 00c50a51f..00bb2c126 100644 --- a/src/utils/src/Parallel.php +++ b/src/utils/src/Parallel.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Hyperf\Utils; +use Hyperf\Utils\Exception\ParallelExecutionException; use Swoole\Coroutine\Channel; class Parallel @@ -45,20 +46,32 @@ class Parallel } } - public function wait(): array + public function wait(bool $throw = true): array { - $result = []; + $result = $throwables = []; $wg = new WaitGroup(); $wg->add(count($this->callbacks)); foreach ($this->callbacks as $key => $callback) { $this->concurrentChannel && $this->concurrentChannel->push(true); - Coroutine::create(function () use ($callback, $key, $wg, &$result) { - $result[$key] = call($callback); - $this->concurrentChannel && $this->concurrentChannel->pop(); - $wg->done(); + Coroutine::create(function () use ($callback, $key, $wg, &$result, &$throwables) { + try { + $result[$key] = call($callback); + } catch (\Throwable $throwable) { + $throwables[$key] = $throwable; + } finally { + $this->concurrentChannel && $this->concurrentChannel->pop(); + $wg->done(); + } }); } $wg->wait(); + if ($throw && count($throwables) > 0) { + $message = 'At least one throwable occurred during parallel execution:' . PHP_EOL . $this->formatThrowables($throwables); + $executionException = new ParallelExecutionException($message); + $executionException->setResults($result); + $executionException->setThrowables($throwables); + throw $executionException; + } return $result; } @@ -66,4 +79,16 @@ class Parallel { $this->callbacks = []; } + + /** + * Format throwables into a nice list. + */ + private function formatThrowables(array $exception): string + { + $output = ''; + foreach ($exception as $key => $value) { + $output .= \sprintf('(%s) %s: %s' . PHP_EOL, $key, get_class($value), $value->getMessage()); + } + return $output; + } } diff --git a/src/utils/src/WaitGroup.php b/src/utils/src/WaitGroup.php index 6b66e5a00..4090c0e68 100644 --- a/src/utils/src/WaitGroup.php +++ b/src/utils/src/WaitGroup.php @@ -12,39 +12,8 @@ declare(strict_types=1); namespace Hyperf\Utils; -use Swoole\Coroutine\Channel as SwooleChannel; +use Swoole\Coroutine\WaitGroup as SwooleWaitGroup; -class WaitGroup +class WaitGroup extends SwooleWaitGroup { - /** - * @var int - */ - private $counter = 0; - - /** - * @var SwooleChannel - */ - private $channel; - - public function __construct() - { - $this->channel = new SwooleChannel(); - } - - public function add(int $incr = 1): void - { - $this->counter += $incr; - } - - public function done(): void - { - $this->channel->push(true); - } - - public function wait(): void - { - while ($this->counter--) { - $this->channel->pop(); - } - } } diff --git a/src/utils/tests/ParallelTest.php b/src/utils/tests/ParallelTest.php index a92e4fce0..c78a2b202 100644 --- a/src/utils/tests/ParallelTest.php +++ b/src/utils/tests/ParallelTest.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace HyperfTest\Utils; use Hyperf\Utils\Coroutine; +use Hyperf\Utils\Exception\ParallelExecutionException; use Hyperf\Utils\Parallel; use PHPUnit\Framework\TestCase; @@ -115,6 +116,28 @@ class ParallelTest extends TestCase $this->assertEquals(count($res), 4); } + public function testParallelThrows() + { + $parallel = new Parallel(); + + $err = function () { + Coroutine::sleep(0.001); + throw new \RuntimeException('something bad happened'); + }; + + $ok = function () { + Coroutine::sleep(0.001); + return 1; + }; + + $parallel->add($err); + for ($i = 0; $i < 4; ++$i) { + $parallel->add($ok); + } + $this->expectException(ParallelExecutionException::class); + $res = $parallel->wait(); + } + public function returnCoId() { return Coroutine::id(); diff --git a/src/utils/tests/WaitGroupTest.php b/src/utils/tests/WaitGroupTest.php new file mode 100644 index 000000000..25f25f9c8 --- /dev/null +++ b/src/utils/tests/WaitGroupTest.php @@ -0,0 +1,55 @@ +add(2); + $result = []; + $i = 2; + while ($i--) { + Coroutine::create(function () use ($wg, &$result) { + Coroutine::sleep(0.001); + $result[] = true; + $wg->done(); + }); + } + $wg->wait(1); + $this->assertTrue(count($result) === 2); + + $wg->add(); + $wg->add(); + $result = []; + $i = 2; + while ($i--) { + Coroutine::create(function () use ($wg, &$result) { + Coroutine::sleep(0.001); + $result[] = true; + $wg->done(); + }); + } + $wg->wait(1); + $this->assertTrue(count($result) === 2); + } +} diff --git a/src/view/composer.json b/src/view/composer.json index 89d9baca3..0fb793a49 100644 --- a/src/view/composer.json +++ b/src/view/composer.json @@ -26,7 +26,9 @@ "suggest": { "hyperf/task": "Required to use task as a view render mode.", "smarty/smarty": "Required to use smarty as a view render engine.", - "duncan3dc/blade": "Required to use blade as a view render engine." + "twig/twig": "Required to use twig as a view render engine.", + "duncan3dc/blade": "Required to use blade as a view render engine.", + "league/plates": "Required to use plates as a view render engine." }, "autoload": { "psr-4": { @@ -35,6 +37,7 @@ }, "autoload-dev": { "psr-4": { + "HyperfTest\\View\\": "tests/" } }, "config": { diff --git a/src/view/src/Engine/PlatesEngine.php b/src/view/src/Engine/PlatesEngine.php new file mode 100644 index 000000000..bcc8dfc2d --- /dev/null +++ b/src/view/src/Engine/PlatesEngine.php @@ -0,0 +1,25 @@ +render($template, $data); + } +} diff --git a/src/view/src/Engine/TwigEngine.php b/src/view/src/Engine/TwigEngine.php new file mode 100644 index 000000000..f535a8880 --- /dev/null +++ b/src/view/src/Engine/TwigEngine.php @@ -0,0 +1,27 @@ + $config['cache_path']]); + + return $twig->render($template, $data); + } +} diff --git a/src/view/tests/PlatesTest.php b/src/view/tests/PlatesTest.php new file mode 100644 index 000000000..b6a8dc5a9 --- /dev/null +++ b/src/view/tests/PlatesTest.php @@ -0,0 +1,46 @@ + __DIR__ . '/tpl', + 'cache_path' => __DIR__ . '/runtime', + 'file_extension' => 'plates', + ]; + + $engine = new PlatesEngine(); + $res = $engine->render('index', ['name' => 'Hyperf'], $config); + + $this->assertEquals(' + + + + Hyperf + + +Hello, Hyperf. You are using plates template now. + +', $res); + } +} diff --git a/src/view/tests/SmartyTest.php b/src/view/tests/SmartyTest.php new file mode 100644 index 000000000..36e686a5f --- /dev/null +++ b/src/view/tests/SmartyTest.php @@ -0,0 +1,45 @@ + __DIR__ . '/tpl', + 'cache_path' => __DIR__ . '/runtime', + ]; + + $engine = new SmartyEngine(); + $res = $engine->render('index.tpl', ['name' => 'Hyperf'], $config); + + $this->assertEquals(' + + + + Hyperf + + +Hello, Hyperf. You are using smarty template now. + +', $res); + } +} diff --git a/src/view/tests/TwigTest.php b/src/view/tests/TwigTest.php new file mode 100644 index 000000000..a7ec67295 --- /dev/null +++ b/src/view/tests/TwigTest.php @@ -0,0 +1,45 @@ + __DIR__ . '/tpl', + 'cache_path' => __DIR__ . '/runtime', + ]; + + $engine = new TwigEngine(); + $res = $engine->render('index.twig', ['name' => 'Hyperf'], $config); + + $this->assertEquals(' + + + + Hyperf + + +Hello, Hyperf. You are using twig template now. + +', $res); + } +} diff --git a/src/view/tests/tpl/index.plates b/src/view/tests/tpl/index.plates new file mode 100644 index 000000000..13cc6b8dc --- /dev/null +++ b/src/view/tests/tpl/index.plates @@ -0,0 +1,10 @@ + + + + + Hyperf + + +Hello, e($name)?>. You are using plates template now. + + \ No newline at end of file diff --git a/src/view/tests/tpl/index.tpl b/src/view/tests/tpl/index.tpl new file mode 100644 index 000000000..5891589d8 --- /dev/null +++ b/src/view/tests/tpl/index.tpl @@ -0,0 +1,10 @@ + + + + + Hyperf + + +Hello, {$name}. You are using smarty template now. + + \ No newline at end of file diff --git a/src/view/tests/tpl/index.twig b/src/view/tests/tpl/index.twig new file mode 100644 index 000000000..0de285f90 --- /dev/null +++ b/src/view/tests/tpl/index.twig @@ -0,0 +1,10 @@ + + + + + Hyperf + + +Hello, {{ name }}. You are using twig template now. + + \ No newline at end of file diff --git a/src/websocket-client/src/Client.php b/src/websocket-client/src/Client.php index 6072bf023..5c05ce88a 100644 --- a/src/websocket-client/src/Client.php +++ b/src/websocket-client/src/Client.php @@ -67,9 +67,15 @@ class Client return $ret; } - public function push(string $data, int $opcode = WEBSOCKET_OPCODE_TEXT, bool $finish = true): bool + /** + * @param bool|int $finish TODO: When swoole version >= 4.4.12, `finish` is SWOOLE_WEBSOCKET_FLAG_FIN or SWOOLE_WEBSOCKET_FLAG_COMPRESS + */ + public function push(string $data, int $opcode = WEBSOCKET_OPCODE_TEXT, $finish = null): bool { - return $this->client->push($data, $opcode, $finish); + if (isset($finish)) { + return $this->client->push($data, $opcode, $finish); + } + return $this->client->push($data, $opcode); } public function close(): bool