diff --git a/.php_cs b/.php_cs
index 4f364e6ce..a17c17b6a 100644
--- a/.php_cs
+++ b/.php_cs
@@ -62,11 +62,13 @@ return PhpCsFixer\Config::create()
'multiline_whitespace_before_semicolons' => [
'strategy' => 'no_multi_line',
],
+ 'constant_case' => [
+ 'case' => 'lower',
+ ],
'class_attributes_separation' => true,
'combine_consecutive_unsets' => true,
'declare_strict_types' => true,
'linebreak_after_opening_tag' => true,
- 'lowercase_constants' => true,
'lowercase_static_reference' => true,
'no_useless_else' => true,
'no_unused_imports' => true,
diff --git a/.travis.yml b/.travis.yml
index bec131695..373bc48ee 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,17 +5,11 @@ sudo: required
matrix:
include:
- php: 7.2
- env: SW_VERSION="4.5.0"
+ env: SW_VERSION="4.5.1"
- php: 7.3
- env: SW_VERSION="4.5.0"
+ env: SW_VERSION="4.5.1"
- php: 7.4
- env: SW_VERSION="4.5.0"
- - php: 7.2
- env: SW_VERSION="4.4.18"
- - php: 7.3
- env: SW_VERSION="4.4.18"
- - php: 7.4
- env: SW_VERSION="4.4.18"
+ env: SW_VERSION="4.5.1"
services:
- mysql
@@ -41,7 +35,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 src/db src/retry src/grpc-client src/nsq src/filesystem
- - composer analyse src/load-balancer
+ - composer analyse src/amqp src/async-queue src/cache src/command src/config
- composer test -- --exclude-group NonCoroutine
- vendor/bin/phpunit --group NonCoroutine
diff --git a/.travis/hyperf.sql b/.travis/hyperf.sql
index 52c8b11c9..785f2001a 100644
--- a/.travis/hyperf.sql
+++ b/.travis/hyperf.sql
@@ -35,7 +35,8 @@ VALUES
(2,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',2,'user','2018-01-01 00:00:00','2018-01-01 00:00:00'),
(3,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',1,'book','2018-01-01 00:00:00','2018-01-01 00:00:00'),
(4,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',2,'book','2018-01-01 00:00:00','2018-01-01 00:00:00'),
- (5,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',3,'book','2018-01-01 00:00:00','2018-01-01 00:00:00');
+ (5,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',3,'book','2018-01-01 00:00:00','2018-01-01 00:00:00'),
+ (6,'https://avatars2.githubusercontent.com/u/44228082?s=200&v=4',0,'','2018-01-01 00:00:00','2018-01-01 00:00:00');
DROP TABLE IF EXISTS `role`;
diff --git a/CHANGELOG-2.0.md b/CHANGELOG-2.0.md
new file mode 100644
index 000000000..f0cbe1c6a
--- /dev/null
+++ b/CHANGELOG-2.0.md
@@ -0,0 +1,36 @@
+# v2.0 - TBD
+
+## Major Changes
+
+1. Refactor [hyperf/di](https://github.com/hyperf/di) component, in particular, AOP and Annotation Scanner are optimized, in v2.0, the component use a brand new loading mechanism to provided an incredible AOP function.
+ 1. The most significant functional differences compared to v1.x is that you can cut into any classes in any ways with Aspect. For example, in v1.x, you can only use AOP in the class instance that created by Hyperf DI container, you cannot cut into the class instance that created by `new` identifier. But now, in v2.0, it is available. But there is still has an exception, the classes that used in bootstrap stage still cannot works.
+ 2. In v1.x, the AOP ONLY available for the normal classes, not for Final class that cannot be inherited by a subclass. But now, in v2.0. it is available.
+ 3. In v1.x, you cannot use the property value that marked by `@Inject` or `@Value` annotation in the constructor of current class. But now, in v2.0, it is available.
+ 4. In v1.x, you can only use `@Inject` and `@Value` annotation in the class instance that created by Hyperf DI container. But now, in v2.0, it is available in any ways, such as the class instance that created by `new` identifier.
+ 5. In v1.x, you have to define the full namespace of Annotation class when you use the Annotation. But now, in v2.0, the component provide a global import mechanism, you cloud define an alias for Annotation to use the Annotation directly without using the namespace. For example, you cloud define `@Inject` annotation in any class without define `use Hyperf\Di\Annotation\Inject;`.
+ 6. In v1.x, the proxy class that created by the DI container is a subclass of the target class, this mechanism will cause the magic constant will return the value of proxy class but not original class, such as `__CLASS__`. But now, in v2.0, the proxy class will keep the same structure with the original class, will not change the class name or the class structure.
+ 7. In v1.x, the proxy class will not re-generate when the proxy file exists even the code of the proxy class changed, this strategy will improve the time-consuming of scan, but at the same time, this will lead to a certain degree of development inconvenience. And now, in v2.0, the file cache of proxy class will generated according to the code content of the proxy class, this changes will reduces the mental burden of development.
+ 8. Add `priority` parameter for Aspect, now you could define `priority` in Aspect class by class property or annotation property, to manage the order of the aspects.
+ 9. In v1.x, you can only define an Aspect class by `@Aspect` annotation, you cannot define the Aspect class by configuration file. But now, in v2.0, it is available to define the Aspect class by configuration file or ConfigProvider.
+ 10. In v1.x, you have to add `Hyperf\Di\Listener\LazyLoaderBootApplicationListener` to enable lazy loading. In 2.0, lazy loading can be used directly. This listener is therefore removed.
+ 11. Added `annotations.scan.class_map` configuration, now you could replace any content of class dynamically above the autoload rules.
+
+## Dependencies Upgrade
+
+- Upgraded `ext-swoole` to `>=4.5`;
+- Upgraded `psr/event-dispatcher` to `^1.0`;
+- Upgraded `monolog/monolog` to `^2.0`;
+- Upgraded `phpstan/phpstan` to `^0.12.18`;
+- Upgraded `vlucas/phpdotenv` to `^4.0`;
+- Upgraded `symfony/finder` to `^5.0`;
+- Upgraded `symfony/event-dispatcher` to `^5.0`;
+- Upgraded `symfony/console` to `^5.0`;
+- Upgraded `symfony/property-access` to `^5.0`;
+- Upgraded `symfony/serializer` to `^5.0`;
+- Upgraded `elasticsearch/elasticsearch` to `^7.0`;
+
+## Removed
+
+- Removed `Hyperf\Di\Aop\AstCollector`;
+- Removed `Hyperf\Di\Aop\ProxyClassNameVisitor`;
+- Removed `Hyperf\Di\Listener\LazyLoaderBootApplicationListener`
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4b390c7b..6f96cdad6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,55 @@
-# v1.1.30 - TBD
+# v1.1.33 - TBD
+
+# v1.1.32 - 2020-05-21
+
+## Fixed
+
+- [#1734](https://github.com/hyperf/hyperf/pull/1734) Fixed the bug that the morph association is empty and cannot be queried.
+- [#1739](https://github.com/hyperf/hyperf/pull/1739) Fixed the wrong bitwise operator in oss hook.
+- [#1743](https://github.com/hyperf/hyperf/pull/1743) Fixed the wrong `refId` for `grafana.json`.
+- [#1748](https://github.com/hyperf/hyperf/pull/1748) Fixed `concurrent.limit` does not works when using another pool.
+- [#1750](https://github.com/hyperf/hyperf/pull/1750) Fixed the incorrent number of current connections when close failed.
+- [#1754](https://github.com/hyperf/hyperf/pull/1754) Fixed the wrong start info for base server.
+- [#1764](https://github.com/hyperf/hyperf/pull/1764) Fixed datetime validate failed when the value is null.
+- [#1769](https://github.com/hyperf/hyperf/pull/1769) Fixed a notice when client initiate disconnects in `socketio-server`.
## Added
-- [#1616](https://github.com/hyperf/hyperf/pull/1616) Added ORM methods `morphWith` and `whereHasMorph`.
+- [#1724](https://github.com/hyperf/hyperf/pull/1724) Added `Model::orWhereHasMorph` ,`Model::whereDoesntHaveMorph` and `Model::orWhereDoesntHaveMorph`.
+- [#1741](https://github.com/hyperf/hyperf/pull/1741) Added `Hyperf\Command\Command::choiceMultiple(): array` method, because the return type of `choice` method is `string`, so the methed cannot handle the multiple selections, even though setted `$multiple` argument.
+- [#1742](https://github.com/hyperf/hyperf/pull/1742) Added Custom Casts for model.
+ - Added interface `Castable`, `CastsAttributes` and `CastsInboundAttributes`.
+ - Added `Model\Builder::withCasts`.
+ - Added `Model::loadMorph`, `Model::loadMorphCount` and `Model::syncAttributes`.
+
+# v1.1.31 - 2020-05-14
+
+## Added
+
+- [#1723](https://github.com/hyperf/hyperf/pull/1723) Added filp/whoops integration in hyperf/exception-handler component.
+- [#1730](https://github.com/hyperf/hyperf/pull/1730) Added shortcut `-R` of `--refresh-fillable` for command `gen:model`.
+
+## Fixed
+
+- [#1696](https://github.com/hyperf/hyperf/pull/1696) Fixed `Context::copy` does not works when use keys.
+- [#1708](https://github.com/hyperf/hyperf/pull/1708) [#1718](https://github.com/hyperf/hyperf/pull/1718) Fixed a series of issues for `hyperf/socketio-server`.
+
+## Optimized
+
+- [#1710](https://github.com/hyperf/hyperf/pull/1710) Don't set process title in Darwin OS.
+
+# v1.1.30 - 2020-05-07
+
+## Added
+
+- [#1616](https://github.com/hyperf/hyperf/pull/1616) Added `morphWith` and `whereHasMorph` for hyperf/database component.
+- [#1651](https://github.com/hyperf/hyperf/pull/1651) Added socket.io-server component.
+- [#1666](https://github.com/hyperf/hyperf/pull/1666) [#1669](https://github.com/hyperf/hyperf/pull/1669) Added support for AMQP RPC mode.
+
+## Fixed
+
+- [#1682](https://github.com/hyperf/hyperf/pull/1682) Fixed the connection pool does not works in JSONRPC pool transporter.
+- [#1683](https://github.com/hyperf/hyperf/pull/1683) Fixed JSONRPC client connection reset failed, when the connection was closed in context.
## Optimized
diff --git a/README-CN.md b/README-CN.md
index c99d8af3b..2e5943f4e 100644
--- a/README-CN.md
+++ b/README-CN.md
@@ -85,6 +85,7 @@ Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面
# 性能
### 阿里云 8 核 16G
+
命令: `wrk -c 1024 -t 8 http://127.0.0.1:9501/`
```bash
Running 10s test @ http://127.0.0.1:9501/
@@ -97,6 +98,10 @@ Requests/sec: 103921.49
Transfer/sec: 18.83MB
```
+# Hyperf 生态
+
+- 🧬 [Nano](https://github.com/hyperf/nano) 是一款零配置、无骨架、极小化的 Hyperf 发行版,通过 Nano 可以让您仅仅通过 1 个 PHP 文件即可快速搭建一个 Hyperf 应用。
+
# 开源协议
Hyperf 是一个基于 [MIT 协议](https://github.com/hyperf/hyperf/blob/master/LICENSE) 开源的软件。
diff --git a/README.md b/README.md
index 1f7148b46..2fcb17535 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ English | [中文](./README-CN.md)
# Introduction
-
+s
Hyperf is an extremely performant and flexible PHP CLI framework based on `Swoole 4.4+`, powered by the state-of-the-art coroutine server and a large number of battle-tested components. Aside from the decisive benchmark outmatching against PHP-FPM frameworks, Hyperf also distinct itself by its focus on flexibility and composability. Hyperf ships with an AOP-enabling dependency injector to ensure components and classes are pluggable and meta programmable. All of its core components strictly follow the PSR standards and thus can be used in other frameworks.
Hyperf's architecture is built upon the combination of `Coroutine`, `Dependency injection`, `Events`, `Annotation`, `AOP (aspect-oriented programming)`. Core components provided by Hyperf can be used out of the box in coroutine context. The set includes but not limited to: `MySQL coroutine client`, `Redis coroutine client`, `WebSocket server and client`, `JSON RPC server and client`, `gRPC server and client`, `Zipkin/Jaeger (OpenTracing) client`, `Guzzle HTTP client`, `Elasticsearch client`, `Consul client`, `ETCD client`, `AMQP component`, `Apollo configuration center`, `Aliyun ACM`, `ETCD configuration center`, `Token bucket algorithm-based limiter`, `Universal connection pool`, `Circuit breaker`, `Swagger`, `Swoole Tracker`, `Snowflake`, `Simply Redis MQ`, `RabbitMQ`, `NSQ`, `Nats`, `Seconds level crontab`, `Custom Processes`, etc. Be assured Hyperf is still a PHP framework. You will also find familiar packages such as `Middleware`, `Event Manager`, `Coroutine optimized Eloquent ORM` (And Model Cache!), `Translation`, `Validation`, `View engine (Blade/Smarty/Twig/Plates/ThinkTemplate)` and more at your command.
@@ -73,11 +73,12 @@ Support this project with your organization or company. Your logo will show up h
# Performance
### Aliyun 8 cores 16G ram
+
command: `wrk -c 1024 -t 8 http://127.0.0.1:9501/`
```bash
Running 10s test @ http://127.0.0.1:9501/
8 threads and 1024 connections
- Thread Stats Avg Stdev Max +/- Stdev
+ Thread Stats Avg Stdev Max +/- Stdevs
Latency 10.08ms 6.82ms 56.66ms 70.19%
Req/Sec 13.17k 5.94k 33.06k 84.12%
1049478 requests in 10.10s, 190.16MB read
@@ -85,6 +86,10 @@ Requests/sec: 103921.49
Transfer/sec: 18.83MB
```
+# The Hyperf Ecosystem
+
+- 🧬 [Nano](https://github.com/hyperf/nano) is a zero-config, no skeleton, minimal Hyperf distribution that allows you to quickly build a Hyperf application with just a single PHP file.
+
# License
The Hyperf framework is open-source software licensed under the MIT license.
diff --git a/bin/co-phpunit b/bin/co-phpunit
index 9d40ea1ba..1ff24a221 100755
--- a/bin/co-phpunit
+++ b/bin/co-phpunit
@@ -2,6 +2,12 @@
function(){
+ if (\Swoole\Coroutine::stats()['coroutine_num'] !== 0) {
+ trigger_error("Reactor exits prematurely. This indicates a coroutine deadlock in test cases", E_USER_ERROR);
+ }
+ return true;
+}]);
\Swoole\Coroutine\Run(function () use (&$code) {
if (version_compare('7.1.0', PHP_VERSION, '>')) {
fwrite(
@@ -55,6 +61,4 @@ $code = 0;
$code = PHPUnit\TextUI\Command::main(false);
});
-swoole_event_wait();
-
exit($code);
diff --git a/bin/release.sh b/bin/release.sh
index 48f2aa0fc..7ff953e0c 100755
--- a/bin/release.sh
+++ b/bin/release.sh
@@ -9,7 +9,7 @@ then
fi
NOW=$(date +%s)
-CURRENT_BRANCH="master"
+CURRENT_BRANCH="2.0"
VERSION=$1
BASEPATH=$(cd `dirname $0`; cd ../src/; pwd)
@@ -48,4 +48,4 @@ done
TIME=$(echo "$(date +%s) - $NOW" | bc)
-printf "Execution time: %f seconds" $TIME
\ No newline at end of file
+printf "Execution time: %f seconds" $TIME
diff --git a/bin/split-linux.sh b/bin/split-linux.sh
index f57574b9a..09d34d406 100755
--- a/bin/split-linux.sh
+++ b/bin/split-linux.sh
@@ -3,7 +3,7 @@
set -e
set -x
-CURRENT_BRANCH="master"
+CURRENT_BRANCH="2.0"
BASEPATH=$(cd `dirname $0`; cd ../src/; pwd)
REPOS=$@
diff --git a/bin/split.sh b/bin/split.sh
index d8cbe7d22..aef9da6b5 100755
--- a/bin/split.sh
+++ b/bin/split.sh
@@ -3,7 +3,7 @@
set -e
set -x
-CURRENT_BRANCH="master"
+CURRENT_BRANCH="2.0"
BASEPATH=$(cd `dirname $0`; cd ../src/; pwd)
REPOS=$@
diff --git a/bootstrap.php b/bootstrap.php
index 4b5da9186..d29855357 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -9,7 +9,14 @@ declare(strict_types=1);
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
+use Hyperf\Config\Listener\RegisterPropertyHandlerListener;
+use Hyperf\Config\ProviderConfig;
+
! defined('BASE_PATH') && define('BASE_PATH', __DIR__);
! defined('SWOOLE_HOOK_FLAGS') && define('SWOOLE_HOOK_FLAGS', SWOOLE_HOOK_ALL);
require_once BASE_PATH . '/vendor/autoload.php';
+
+ProviderConfig::load();
+
+(new RegisterPropertyHandlerListener())->process(new \stdClass());
diff --git a/composer.json b/composer.json
index b6989cae3..fda313964 100644
--- a/composer.json
+++ b/composer.json
@@ -20,10 +20,10 @@
"ext-bcmath": "*",
"ext-json": "*",
"ext-redis": "*",
- "ext-swoole": ">=4.4",
+ "ext-swoole": ">=4.5",
"psr/container": "^1.0",
- "psr/event-dispatcher": "^0.7",
- "psr/http-message": "^1.0.1",
+ "psr/event-dispatcher": "^1.0",
+ "psr/http-message": "^1.0",
"psr/http-server-middleware": "^1.0",
"psr/log": "^1.0",
"psr/simple-cache": "^1.0"
@@ -39,13 +39,14 @@
"elasticsearch/elasticsearch": "^7.0",
"endclothing/prometheus_client_php": "^0.9.1",
"fig/http-message-util": "^1.1.2",
+ "filp/whoops": "^2.7",
"friendsofphp/php-cs-fixer": "^2.14",
"google/protobuf": "^3.6.1",
"grpc/grpc": "^1.15",
"guzzlehttp/guzzle": "^6.3",
"influxdb/influxdb-php": "^1.15.0",
"ircmaxell/random-lib": "^1.2",
- "jcchavezs/zipkin-opentracing": "^0.1.4",
+ "jcchavezs/zipkin-opentracing": "^0.1.5",
"jean85/pretty-package-versions": "^1.2",
"jonahgeorge/jaeger-client-php": "^0.4.4",
"laminas/laminas-mime": "^2.7",
@@ -55,8 +56,9 @@
"league/plates": "^3.3",
"malukenho/docheader": "^0.1.6",
"markrogoyski/math-php": "^0.49.0",
+ "mix/redis-subscribe": "^2.1",
"mockery/mockery": "^1.0",
- "monolog/monolog": "^1.24",
+ "monolog/monolog": "^2.0",
"nesbot/carbon": "^2.0",
"nikic/fast-route": "^1.3",
"nikic/php-parser": "^4.1",
@@ -64,7 +66,7 @@
"php-amqplib/php-amqplib": "^2.7",
"php-di/php-di": "^6.0",
"php-di/phpdoc-reader": "^2.0.1",
- "phpstan/phpstan": "^0.11.15",
+ "phpstan/phpstan": "^0.12.18",
"phpunit/phpunit": "^7.0.0",
"roave/better-reflection": "^4.0",
"smarty/smarty": "^3.1",
@@ -72,13 +74,13 @@
"start-point/etcd-php": "^1.1",
"swoole/ide-helper": "dev-master",
"sy-records/think-template": "^2.0",
- "symfony/console": "^4.2",
- "symfony/event-dispatcher": "^4.3",
- "symfony/finder": "^4.1",
- "symfony/property-access": "^4.3",
- "symfony/serializer": "^4.3",
+ "symfony/event-dispatcher": "^5.0",
+ "symfony/console": "^5.0",
+ "symfony/finder": "^5.0",
+ "symfony/property-access": "^5.0",
+ "symfony/serializer": "^5.0",
"twig/twig": "^2.12",
- "vlucas/phpdotenv": "^3.1",
+ "vlucas/phpdotenv": "^4.0",
"xxtime/flysystem-aliyun-oss": "^1.5"
},
"replace": {
@@ -124,8 +126,8 @@
"hyperf/server": "self.version",
"hyperf/service-governance": "self.version",
"hyperf/session": "self.version",
+ "hyperf/socketio-server": "self.version",
"hyperf/swagger": "self.version",
- "hyperf/swoole-enterprise": "self.version",
"hyperf/task": "self.version",
"hyperf/tracer": "self.version",
"hyperf/translation": "self.version",
@@ -143,10 +145,10 @@
"files": [
"src/config/src/Functions.php",
"src/di/src/Functions.php",
+ "src/filesystem/src/Adapter/AliyunOssHook.php",
"src/nats/src/Functions.php",
"src/translation/src/Functions.php",
- "src/utils/src/Functions.php",
- "src/filesystem/src/Adapter/AliyunOssHook.php"
+ "src/utils/src/Functions.php"
],
"psr-4": {
"Hyperf\\Amqp\\": "src/amqp/src/",
@@ -205,10 +207,10 @@
"Hyperf\\ServiceGovernance\\": "src/service-governance/src/",
"Hyperf\\Session\\": "src/session/src/",
"Hyperf\\Snowflake\\": "src/snowflake/src/",
+ "Hyperf\\SocketIOServer\\": "src/socketio-server/src/",
"Hyperf\\Socket\\": "src/socket/src/",
"Hyperf\\SuperGlobals\\": "src/super-globals/src/",
"Hyperf\\Swagger\\": "src/swagger/src/",
- "Hyperf\\SwooleEnterprise\\": "src/swoole-enterprise/src/",
"Hyperf\\SwooleTracker\\": "src/swoole-tracker/src/",
"Hyperf\\Task\\": "src/task/src/",
"Hyperf\\Testing\\": "src/testing/src/",
@@ -274,6 +276,7 @@
"HyperfTest\\ServiceGovernance\\": "src/service-governance/tests/",
"HyperfTest\\Session\\": "src/session/tests/",
"HyperfTest\\Snowflake\\": "src/snowflake/tests/",
+ "HyperfTest\\SocketIOServer\\": "src/socketio-server/tests/",
"HyperfTest\\Socket\\": "src/socket/tests/",
"HyperfTest\\SuperGlobals\\": "src/super-globals/tests/",
"HyperfTest\\Task\\": "src/task/tests/",
@@ -317,6 +320,7 @@
"Hyperf\\GraphQL\\ConfigProvider",
"Hyperf\\GrpcClient\\ConfigProvider",
"Hyperf\\GrpcServer\\ConfigProvider",
+ "Hyperf\\Guzzle\\ConfigProvider",
"Hyperf\\HttpServer\\ConfigProvider",
"Hyperf\\JsonRpc\\ConfigProvider",
"Hyperf\\LoadBalancer\\ConfigProvider",
@@ -340,10 +344,10 @@
"Hyperf\\ServiceGovernance\\ConfigProvider",
"Hyperf\\Session\\ConfigProvider",
"Hyperf\\Snowflake\\ConfigProvider",
+ "Hyperf\\SocketIOServer\\ConfigProvider",
"Hyperf\\Socket\\ConfigProvider",
"Hyperf\\SuperGlobals\\ConfigProvider",
"Hyperf\\Swagger\\ConfigProvider",
- "Hyperf\\SwooleEnterprise\\ConfigProvider",
"Hyperf\\SwooleTracker\\ConfigProvider",
"Hyperf\\Task\\ConfigProvider",
"Hyperf\\Tracer\\ConfigProvider",
@@ -363,7 +367,7 @@
"license-check": "docheader check src/ test/",
"cs-fix": "php-cs-fixer fix $1",
"json-fix": "./bin/composer-json-fixer",
- "analyse": "phpstan analyse --memory-limit 300M -l 3 -c phpstan.neon"
+ "analyse": "phpstan analyse --memory-limit 1024M -l 5 -c phpstan.neon"
},
"minimum-stability": "dev",
"prefer-stable": true
diff --git a/doc/README.md b/doc/README.md
deleted file mode 100644
index 8fbe8748e..000000000
--- a/doc/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# 介绍
-
-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`、`视图引擎`、`Snowflake 全局 ID 生成器` 等组件,省去了自己实现对应协程版本的麻烦。
-
-Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面向切面编程`、`基于 PSR-15 的中间件`、`自定义进程`、`基于 PSR-14 的事件管理器`、`Redis/RabbitMQ 消息队列`、`自动模型缓存`、`基于 PSR-16 的缓存`、`Crontab 秒级定时任务`、`国际化`、`Validation 表单验证器` 等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。
-
-# 框架初衷
-
-尽管现在基于 PHP 语言开发的框架处于一个百家争鸣的时代,但仍旧未能看到一个优雅的设计与超高性能的共存的完美框架,亦没有看到一个真正为 PHP 微服务铺路的框架,此为 Hyperf 及其团队成员的初衷,我们将持续投入并为此付出努力,也欢迎你加入我们参与开源建设。
-
-# 设计理念
-
-`Hyperspeed + Flexibility = Hyperf`,从名字上我们就将 `超高速` 和 `灵活性` 作为 Hyperf 的基因。
-
-- 对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。
-- 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 [PSR 标准](https://www.php-fig.org/psr) 的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。
-
-基于以上的特点,Hyperf 将存在丰富的可能性,如实现 Web 服务,网关服务,分布式中间件,微服务架构,游戏服务器,物联网(IOT)等。
-
-# 生产可用
-
-我们为组件进行了大量的单元测试以保证逻辑的正确,同时维护了高质量的文档,在 Hyperf 正式对外开放之前,便已经过了严酷的生产环境的考验,我们才正式的对外开放该项目,至今,已有很多的大型/中小型互联网公司在生产环境使用 Hyperf。
diff --git a/doc/en/nano.md b/doc/en/nano.md
new file mode 100644
index 000000000..f80b654c4
--- /dev/null
+++ b/doc/en/nano.md
@@ -0,0 +1,251 @@
+
+`hyperf/nano` is a package that scales Hyperf down to a single file.
+
+## Example
+
+```php
+get('/', function () {
+
+ $user = $this->request->input('user', 'nano');
+ $method = $this->request->getMethod();
+
+ return [
+ 'message' => "hello {$user}",
+ 'method' => $method,
+ ];
+
+});
+
+$app->run();
+```
+
+Run
+
+```bash
+php index.php start
+```
+
+That's all you need.
+
+## Feature
+
+* No skeleton.
+* Fast startup.
+* Zero config.
+* Closure style.
+* Support all Hyperf features except annotations.
+* Compatible with all Hyperf components.
+
+## More Examples
+
+### Routing
+
+$app inherits all methods from hyperf router.
+
+```php
+addGroup('/nano', function () use ($app) {
+ $app->addRoute(['GET', 'POST'], '/{id:\d+}', function($id) {
+ return '/nano/'.$id;
+ });
+ $app->put('/{name:.+}', function($name) {
+ return '/nano/'.$name;
+ });
+});
+
+$app->run();
+```
+
+### DI Container
+```php
+getContainer()->set(Foo::class, new Foo());
+
+$app->get('/', function () {
+ /** @var ContainerProxy $this */
+ $foo = $this->get(Foo::class);
+ return $foo->bar();
+});
+
+$app->run();
+```
+> As a convention, $this is bind to ContainerProxy in all closures managed by nano, including middleware, exception handler and more.
+
+### Middleware
+```php
+get('/', function () {
+ return $this->request->getAttribute('key');
+});
+
+$app->addMiddleware(function ($request, $handler) {
+ $request = $request->withAttribute('key', 'value');
+ return $handler->handle($request);
+});
+
+$app->run();
+```
+
+> In addition to closure, all $app->addXXX() methods also accept class name as argument. You can pass any corresponding hyperf classes.
+
+### ExceptionHandler
+
+```php
+get('/', function () {
+ throw new \Exception();
+});
+
+$app->addExceptionHandler(function ($throwable, $response) {
+ return $response->withStatus('418')
+ ->withBody(new SwooleStream('I\'m a teapot'));
+});
+
+$app->run();
+```
+
+### Custom Command
+
+```php
+addCommand('echo', function(){
+ $this->get(StdoutLoggerInterface::class)->info('A new command called echo!');
+});
+
+$app->run();
+```
+
+To run this command, execute
+```bash
+php index.php echo
+```
+
+### Event Listener
+```php
+addListener(BootApplication::class, function($event){
+ $this->get(StdoutLoggerInterface::class)->info('App started');
+});
+
+$app->run();
+```
+
+### Custom Process
+```php
+addProcess(function(){
+ while (true) {
+ sleep(1);
+ $this->get(StdoutLoggerInterface::class)->info('Processing...');
+ }
+});
+
+$app->run();
+```
+
+### Crontab
+
+```php
+addCrontab('* * * * * *', function(){
+ $this->get(StdoutLoggerInterface::class)->info('execute every second!');
+});
+
+$app->run();
+```
+
+### Use Hyperf Component.
+
+```php
+config([
+ 'db.default' => [
+ 'host' => env('DB_HOST', 'localhost'),
+ 'port' => env('DB_PORT', 3306),
+ 'database' => env('DB_DATABASE', 'hyperf'),
+ 'username' => env('DB_USERNAME', 'root'),
+ 'password' => env('DB_PASSWORD', ''),
+ ]
+]);
+
+$app->get('/', function(){
+ return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]);
+});
+
+$app->run();
+```
diff --git a/doc/en/swoole-tracker.md b/doc/en/swoole-tracker.md
index a64b9db2d..98f02a7bb 100644
--- a/doc/en/swoole-tracker.md
+++ b/doc/en/swoole-tracker.md
@@ -112,14 +112,14 @@ ENTRYPOINT ["sh", ".build/entrypoint.sh"]
composer require hyperf/swoole-dashboard dev-master
```
-然后将以下 `Middleware` 写到 `middleware.php` 中。
+然后将以下 `Middleware` 写到 `config/autoload/middlewares.php` 配置文件中。
```php
[
- Hyperf\SwooleDashboard\Middleware\HttpServerMiddleware::class
+ Hyperf\SwooleDashboard\Middlewasre\HttpServerMiddleware::class
],
];
diff --git a/doc/index.html b/doc/index.html
index ef57bb496..0590609da 100644
--- a/doc/index.html
+++ b/doc/index.html
@@ -28,6 +28,7 @@
window.$docsify = {
name: 'Hyperf',
repo: 'hyperf/hyperf',
+ homepage: './zh-cn/README.md',
loadSidebar: 'summary.md',
loadNavbar: true,
fallbackLanguages: ['zh-cn', 'en'],
@@ -35,8 +36,8 @@
themeColor: '#3F51B5',
logo: '/logo.png',
auto2top: true,
- autoHeader: false,
subMaxLevel: 4,
+ topMargin: 20,
search: {
depth: 6,
noData: {
diff --git a/doc/zh-cn/async-queue.md b/doc/zh-cn/async-queue.md
index dd232e626..780c2d4e8 100644
--- a/doc/zh-cn/async-queue.md
+++ b/doc/zh-cn/async-queue.md
@@ -18,7 +18,7 @@ composer require hyperf/async-queue
|:----------------:|:---------:|:-------------------------------------------:|:---------------------------------------:|
| driver | string | Hyperf\AsyncQueue\Driver\RedisDriver::class | 无 |
| channel | string | queue | 队列前缀 |
-| redis.pool | string | default | redis连接池 |
+| redis.pool | string | default | redis 连接池 |
| timeout | int | 2 | pop 消息的超时时间 |
| retry_seconds | int,array | 5 | 失败后重新尝试间隔 |
| handle_timeout | int | 10 | 消息处理超时时间 |
diff --git a/doc/zh-cn/changelog.md b/doc/zh-cn/changelog.md
index 24f62fcbf..14b36d466 100644
--- a/doc/zh-cn/changelog.md
+++ b/doc/zh-cn/changelog.md
@@ -1,5 +1,60 @@
# 版本更新记录
+# v1.1.32 - 2020-05-21
+
+## 修复
+
+- [#1734](https://github.com/hyperf/hyperf/pull/1734) 修复模型多态查询,关联关系为空时,也会查询 SQL 的问题;
+- [#1739](https://github.com/hyperf/hyperf/pull/1739) 修复 `hyperf/filesystem` 组件 OSS HOOK 位运算错误,导致 resource 判断不准确的问题;
+- [#1743](https://github.com/hyperf/hyperf/pull/1743) 修复 `grafana.json` 中错误的`refId` 字段值;
+- [#1748](https://github.com/hyperf/hyperf/pull/1748) 修复 `hyperf/amqp` 组件在使用其他连接池时,对应的 `concurrent.limit` 配置不生效的问题;
+- [#1750](https://github.com/hyperf/hyperf/pull/1750) 修复连接池组件,在连接关闭失败时会导致计数有误的问题;
+- [#1754](https://github.com/hyperf/hyperf/pull/1754) 修复 BASE Server 服务,启动提示没有考虑 UDP 服务的情况;
+- [#1764](https://github.com/hyperf/hyperf/pull/1764) 修复当时间值为 null 时,datatime 验证器执行失败的 BUG;
+- [#1769](https://github.com/hyperf/hyperf/pull/1769) 修复 `hyperf/socketio-server` 组件中,客户端初始化断开连接操作时会报 Notice 的错误的问题;
+
+## 新增
+
+- [#1724](https://github.com/hyperf/hyperf/pull/1724) 新增模型方法 `Model::orWhereHasMorph` ,`Model::whereDoesntHaveMorph` and `Model::orWhereDoesntHaveMorph`;
+- [#1741](https://github.com/hyperf/hyperf/pull/1741) 新增 `Hyperf\Command\Command::choiceMultiple(): array` 方法,因为 `choice` 方法的返回类型为 `string,所以就算设置了 `$multiple` 参数也无法处理多个选择的情况;
+- [#1742](https://github.com/hyperf/hyperf/pull/1742) 新增模型 自定义类型转换器 功能;
+ - 新增 interface `Castable`, `CastsAttributes` 和 `CastsInboundAttributes`;
+ - 新增方法 `Model\Builder::withCasts`;
+ - 新增方法 `Model::loadMorph`, `Model::loadMorphCount` 和 `Model::syncAttributes`;
+
+# v1.1.31 - 2020-05-14
+
+## 新增
+
+- [#1723](https://github.com/hyperf/hyperf/pull/1723) 异常处理器集成了 filp/whoops 。
+- [#1730](https://github.com/hyperf/hyperf/pull/1730) 为命令 `gen:model` 可选项 `--refresh-fillable` 添加简写 `-R`。
+
+## 修复
+
+- [#1696](https://github.com/hyperf/hyperf/pull/1696) 修复方法 `Context::copy` 传入字段 `keys` 后无法正常使用的BUG。
+- [#1708](https://github.com/hyperf/hyperf/pull/1708) [#1718](https://github.com/hyperf/hyperf/pull/1718) 修复 `hyperf/socketio-server` 组件内存溢出等BUG。
+
+## 优化
+
+- [#1710](https://github.com/hyperf/hyperf/pull/1710) MAC系统下不再使用 `cli_set_process_title` 方法设置进程名。
+
+# v1.1.30 - 2020-05-07
+
+## 新增
+
+- [#1616](https://github.com/hyperf/hyperf/pull/1616) 新增 ORM 方法 `morphWith` 和 `whereHasMorph`。
+- [#1651](https://github.com/hyperf/hyperf/pull/1651) 新增 `socket.io-server` 组件。
+- [#1666](https://github.com/hyperf/hyperf/pull/1666) [#1669](https://github.com/hyperf/hyperf/pull/1669) 新增 AMQP RPC 客户端。
+
+## 修复
+
+- [#1682](https://github.com/hyperf/hyperf/pull/1682) 修复 `RpcPoolTransporter` 的连接池配置不生效的 BUG。
+- [#1683](https://github.com/hyperf/hyperf/pull/1683) 修复 `RpcConnection` 连接失败后,相同协程内无法正常重置连接的 BUG。
+
+## 优化
+
+- [#1670](https://github.com/hyperf/hyperf/pull/1670) 优化掉 `Cache 组件` 一条无意义的删除指令。
+
# v1.1.28 - 2020-04-30
## 新增
@@ -26,16 +81,16 @@
## 新增
- [#1575](https://github.com/hyperf/hyperf/pull/1575) 为脚本 `gen:model` 生成的模型,自动添加 `relation` `scope` 和 `attributes` 的变量注释。
-- [#1586](https://github.com/hyperf/hyperf/pull/1586) 添加 `symfony/event-dispatcher` 组件小于 `4.3` 时的 `conflict` 配置。用于解决用户使用了 `4.3` 以下版本时,导致 `SymfonyDispatcher` 实现冲突的BUG。
+- [#1586](https://github.com/hyperf/hyperf/pull/1586) 添加 `symfony/event-dispatcher` 组件小于 `4.3` 时的 `conflict` 配置。用于解决用户使用了 `4.3` 以下版本时,导致 `SymfonyDispatcher` 实现冲突的 BUG。
- [#1597](https://github.com/hyperf/hyperf/pull/1597) 为 `AMQP` 消费者,添加最大消费次数 `maxConsumption`。
- [#1603](https://github.com/hyperf/hyperf/pull/1603) 为 `WebSocket` 服务添加基于 `fd` 存储的 `Context`。
## 修复
-- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修复 `jsonrpc` 服务,发布了相同名字不同协议到 `consul` 后,客户端无法正常工作的BUG。
-- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修复了文件锁在协程下可能会造成死锁的BUG。
-- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修复了重写后的 `go` 方法,返回值与 `swoole` 原生方法不符的BUG。
-- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修复当路由 `Handler` 是匿名函数时,脚本 `describe:routes` 执行失败的BUG。
+- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修复 `jsonrpc` 服务,发布了相同名字不同协议到 `consul` 后,客户端无法正常工作的 BUG。
+- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修复了文件锁在协程下可能会造成死锁的 BUG。
+- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修复了重写后的 `go` 方法,返回值与 `swoole` 原生方法不符的 BUG。
+- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修复当路由 `Handler` 是匿名函数时,脚本 `describe:routes` 执行失败的 BUG。
# v1.1.26 - 2020-04-16
@@ -47,9 +102,9 @@
- [#1563](https://github.com/hyperf/hyperf/pull/1563) 修复服务关停后,定时器的 `onOneServer` 配置不会被重置。
- [#1565](https://github.com/hyperf/hyperf/pull/1565) 当 `DB` 组件重连 `Mysql` 时,重置事务等级为 0。
-- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修复 `Hyperf\GrpcServer\CoreMiddleware` 中,自定义类的父类找不到时报错的BUG。
-- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修复 `describe:routes` 脚本 `server` 配置不生效的BUG。
-- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修复 `migrate:refresh` 脚本 `step` 参数不为 `int` 时会报错的BUG。
+- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修复 `Hyperf\GrpcServer\CoreMiddleware` 中,自定义类的父类找不到时报错的 BUG。
+- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修复 `describe:routes` 脚本 `server` 配置不生效的 BUG。
+- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修复 `migrate:refresh` 脚本 `step` 参数不为 `int` 时会报错的 BUG。
## 变更
diff --git a/doc/zh-cn/db/mutators.md b/doc/zh-cn/db/mutators.md
new file mode 100644
index 000000000..14ecc3a59
--- /dev/null
+++ b/doc/zh-cn/db/mutators.md
@@ -0,0 +1,555 @@
+# 修改器
+
+> 本文档大量借鉴于 [LearnKu](https://learnku.com) 十分感谢 LearnKu 对 PHP 社区做出的贡献。
+
+当你在模型实例中获取或设置某些属性值的时候,访问器和修改器允许你对模型属性值进行格式化。
+
+## 访问器 & 修改器
+
+### 定义一个访问器
+
+若要定义一个访问器, 则需在模型上创建一个 `getFooAttribute` 方法,要访问的 `Foo` 字段需使用「驼峰式」命名。 在这个示例中,我们将为 `first_name` 属性定义一个访问器。当模型尝试获取 `first_name` 属性时,将自动调用此访问器:
+
+```php
+first_name;
+```
+
+当然,你也可以通过已有的属性值,使用访问器返回新的计算值:
+
+```php
+namespace App;
+
+use Hyperf\DbConnection\Model\Model;
+
+class User extends Model
+{
+ /**
+ * 获取用户的姓名.
+ *
+ * @return string
+ */
+ public function getFullNameAttribute()
+ {
+ return "{$this->first_name} {$this->last_name}";
+ }
+}
+```
+
+### 定义一个修改器
+
+若要定义一个修改器,则需在模型上面定义 `setFooAttribute` 方法。要访问的 `Foo` 字段使用「驼峰式」命名。让我们再来定义一个 `first_name` 属性的修改器。当我们尝试在模式上在设置 `first_name` 属性值时,该修改器将被自动调用:
+
+```php
+attributes['first_name'] = strtolower($value);
+ }
+}
+```
+
+修改器会获取属性已经被设置的值,允许你修改并且将其值设置到模型内部的 `$attributes` 属性上。举个例子,如果我们尝试将 `first_name` 属性的值设置为 `Sally`:
+
+```php
+$user = App\User::find(1);
+
+$user->first_name = 'Sally';
+```
+
+在这个例子中,`setFirstNameAttribute` 方法在调用的时候接受 `Sally` 这个值作为参数。接着修改器会应用 `strtolower` 函数并将处理的结果设置到内部的 `$attributes` 数组。
+
+## 日期转化器
+
+默认情况下,模型会将 `created_at` 和 `updated_at` 字段转换为 `Carbon` 实例,它继承了 `PHP` 原生的 `DateTime` 类并提供了各种有用的方法。你可以通过设置模型的 `$dates` 属性来添加其他日期属性:
+
+```php
+ Tip: 你可以通过将模型的公有属性 $timestamps 值设置为 false 来禁用默认的 created_at 和 updated_at 时间戳。
+
+当某个字段是日期格式时,你可以将值设置为一个 `UNIX` 时间戳,日期时间 `(Y-m-d)` 字符串,或者 `DateTime` / `Carbon` 实例。日期值会被正确格式化并保存到你的数据库中:
+
+就如上面所说,当获取到的属性包含在 `$dates` 属性中时,都会自动转换为 `Carbon` 实例,允许你在属性上使用任意的 `Carbon` 方法:
+
+```php
+$user = App\User::find(1);
+
+return $user->deleted_at->getTimestamp();
+```
+
+### 时间格式
+
+时间戳都将以 `Y-m-d H:i:s` 形式格式化。如果你需要自定义时间戳格式,可在模型中设置 `$dateFormat` 属性。这个属性决定了日期属性将以何种形式保存在数据库中,以及当模型序列化成数组或 `JSON` 时的格式:
+
+```php
+`, `string`, `boolean`, `object`, `array`, `collection`, `date`, `datetime` 和 `timestamp`。 当需要转换为 `decimal` 类型时,你需要定义小数位的个数,如: `decimal:2`。
+
+示例, 让我们把以整数( `0` 或 `1` )形式存储在数据库中的 `is_admin` 属性转成布尔值:
+
+```php
+ 'boolean',
+ ];
+}
+```
+
+现在当你访问 `is_admin` 属性时,虽然保存在数据库里的值是一个整数类型,但是返回值总是会被转换成布尔值类型:
+
+```php
+$user = App\User::find(1);
+
+if ($user->is_admin) {
+ //
+}
+```
+
+### 自定义类型转换
+
+模型内置了多种常用的类型转换。但是,用户偶尔会需要将数据转换成自定义类型。现在,该需求可以通过定义一个实现 `CastsAttributes` 接口的类来完成
+
+实现了该接口的类必须事先定义一个 `get` 和 `set` 方法。 `get` 方法负责将从数据库中获取的原始数据转换成对应的类型,而 `set` 方法则是将数据转换成对应的数据库类型以便存入数据库中。举个例子,下面我们将内置的 `json` 类型转换以自定义类型转换的形式重新实现一遍:
+
+```php
+ Json::class,
+ ];
+}
+```
+
+#### 值对象类型转换
+
+你不仅可以将数据转换成原生的数据类型,还可以将数据转换成对象。两种自定义类型转换的定义方式非常类似。但是将数据转换成对象的自定义转换类中的 `set` 方法需要返回键值对数组,用于设置原始、可存储的值到对应的模型中。
+
+举个例子,定义一个自定义类型转换类用于将多个模型属性值转换成单个 `Address` 值对象,假设 `Address` 对象有两个公有属性 `lineOne` 和 `lineTwo`:
+
+```php
+ $value->lineOne,
+ 'address_line_two' => $value->lineTwo,
+ ];
+ }
+}
+```
+
+进行值对象类型转换后,任何对值对象的数据变更将会自动在模型保存前同步回模型当中:
+
+```php
+address->lineOne = 'Updated Address Value';
+$user->address->lineTwo = '#10000';
+
+$user->save();
+
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Updated Address Value',
+// 'address_line_two' => '#10000'
+//];
+```
+
+**这里的实现与 Laravel 不同,如果出现以下用法,请需要格外注意**
+
+```php
+$user = App\User::find(1);
+
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Address Value',
+// 'address_line_two' => '#10000'
+//];
+
+$user->address->lineOne = 'Updated Address Value';
+$user->address->lineTwo = '#20000';
+
+// 直接修改 address 的字段后,是无法立马再 attributes 中生效的,但可以直接通过 $user->address 拿到修改后的数据。
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Address Value',
+// 'address_line_two' => '#10000'
+//];
+
+// 当我们保存数据或者删除数据后,attributes 便会改成修改后的数据。
+$user->save();
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Updated Address Value',
+// 'address_line_two' => '#20000'
+//];
+```
+
+如果修改 `address` 后,不想要保存,也不想通过 `address->lineOne` 获取 `address_line_one` 的数据,还可以使用以下 方法
+
+```php
+$user = App\User::find(1);
+$user->address->lineOne = 'Updated Address Value';
+$user->syncAttributes();
+var_dump($user->getAttributes());
+```
+
+当然,如果您仍然需要修改对应的 `value` 后,同步修改 `attributes` 的功能,可以尝试使用以下方式。首先,我们实现一个 `UserInfo` 并继承 `CastsValue`。
+
+```php
+namespace App\Caster;
+
+use Hyperf\Database\Model\CastsValue;
+
+/**
+ * @property string $name
+ * @property int $gender
+ */
+class UserInfo extends CastsValue
+{
+}
+```
+
+然后实现对应的 `UserInfoCaster`
+
+```php
+ $value->name,
+ 'gender' => $value->gender,
+ ];
+ }
+}
+
+```
+
+当我们再使用以下方式修改 UserInfo 时,便可以同步修改到 attributes 的数据。
+
+```php
+/** @var User $user */
+$user = User::query()->find(100);
+$user->userInfo->name = 'John1';
+var_dump($user->getAttributes()); // ['name' => 'John1']
+```
+
+#### 入站类型转换
+
+有时候,你可能只需要对写入模型的属性值进行类型转换而不需要对从模型中获取的属性值进行任何处理。一个典型入站类型转换的例子就是「hashing」。入站类型转换类需要实现 `CastsInboundAttributes` 接口,只需要实现 `set` 方法。
+
+```php
+algorithm = $algorithm;
+ }
+
+ /**
+ * 转换成将要进行存储的值
+ */
+ public function set($model, $key, $value, $attributes)
+ {
+ return hash($this->algorithm, $value);
+ }
+}
+```
+
+#### 类型转换参数
+
+当将自定义类型转换附加到模型时,可以指定传入的类型转换参数。传入类型转换参数需使用 `:` 将参数与类名分隔,多个参数之间使用逗号分隔。这些参数将会传递到类型转换类的构造函数中:
+
+```php
+ Hash::class.':sha256',
+ ];
+}
+```
+
+### 数组 & `JSON` 转换
+
+当你在数据库存储序列化的 `JSON` 的数据时,`array` 类型的转换非常有用。比如:如果你的数据库具有被序列化为 `JSON` 的 `JSON` 或 `TEXT` 字段类型,并且在模型中加入了 `array` 类型转换,那么当你访问的时候就会自动被转换为 `PHP` 数组:
+
+```php
+ 'array',
+ ];
+}
+```
+
+一旦定义了转换,你访问 `options` 属性时他会自动从 `JSON` 类型反序列化为 `PHP` 数组。当你设置了 `options` 属性的值时,给定的数组也会自动序列化为 `JSON` 类型存储:
+
+```php
+$user = App\User::find(1);
+
+$options = $user->options;
+
+$options['key'] = 'value';
+
+$user->options = $options;
+
+$user->save();
+```
+
+### Date 类型转换
+
+当使用 `date` 或 `datetime` 属性时,可以指定日期的格式。 这种格式会被用在模型序列化为数组或者 `JSON`:
+
+```php
+ 'datetime:Y-m-d',
+ ];
+}
+```
+
+### 查询时类型转换
+
+有时候需要在查询执行过程中对特定属性进行类型转换,例如需要从数据库表中获取数据的时候。举个例子,请参考以下查询:
+
+```php
+use App\Post;
+use App\User;
+
+$users = User::select([
+ 'users.*',
+ 'last_posted_at' => Post::selectRaw('MAX(created_at)')
+ ->whereColumn('user_id', 'users.id')
+])->get();
+```
+
+在该查询获取到的结果集中,`last_posted_at` 属性将会是一个字符串。假如我们在执行查询时进行 `date` 类型转换将更方便。你可以通过使用 `withCasts` 方法来完成上述操作:
+
+```php
+$users = User::select([
+ 'users.*',
+ 'last_posted_at' => Post::selectRaw('MAX(created_at)')
+ ->whereColumn('user_id', 'users.id')
+])->withCasts([
+ 'last_posted_at' => 'date'
+])->get();
+```
+
diff --git a/doc/zh-cn/exception-handler.md b/doc/zh-cn/exception-handler.md
index 5900d8858..aa7fb72ea 100644
--- a/doc/zh-cn/exception-handler.md
+++ b/doc/zh-cn/exception-handler.md
@@ -106,6 +106,33 @@ class IndexController extends Controller
```
在上面这个例子,我们先假设 `FooException` 是存在的一个异常,以及假设已经完成了该处理器的配置,那么当业务抛出一个没有被捕获处理的异常时,就会根据配置的顺序依次传递,整一个处理流程可以理解为一个管道,若前一个异常处理器调用 `$this->stopPropagation()` 则不再往后传递,若最后一个配置的异常处理器仍不对该异常进行捕获处理,那么就会交由 Hyperf 的默认异常处理器处理了。
+## 集成 Whoops
+
+框架提供了 Whoops 集成。
+
+首先安装 Whoops
+```php
+composer require --dev filp/whoops
+```
+
+然后配置 Whoops 专用异常处理器。
+
+```php
+// config/autoload/exceptions.php
+return [
+ 'handler' => [
+ 'http' => [
+ \Hyperf\ExceptionHandler\Handler\WhoopsExceptionHandler::class,
+ ],
+ ],
+];
+```
+
+效果如图:
+
+![whoops](/imgs/whoops.png)
+
+
## Error 监听器
框架提供了 `error_reporting()` 错误级别的监听器 `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`。
diff --git a/doc/zh-cn/filesystem.md b/doc/zh-cn/filesystem.md
index 6ca871576..b2354058b 100644
--- a/doc/zh-cn/filesystem.md
+++ b/doc/zh-cn/filesystem.md
@@ -130,7 +130,7 @@ return [
1. S3 存储请确认安装 `hyperf/guzzle` 组件以提供协程化支持。阿里云、七牛云存储请[开启 Curl Hook](/zh-cn/coroutine?id=swoole-runtime-hook-level)来使用协程。因 Curl Hook 的参数支持性问题,请使用 Swoole 4.4.13 以上版本。
2. minIO, ceph radosgw 等私有对象存储方案均支持 S3 协议,可以使用 S3 适配器。
-3. 使用Local驱动时,根目录是配置好的地址,而不是操作系统的根目录。例如,Local驱动 `root` 设置为 `/var/www`, 则本地磁盘上的 `/var/www/public/file.txt` 通过 flysystem API 访问时应使用 `/public/file.txt` 或 `public/file.txt` 。
+3. 使用 Local 驱动时,根目录是配置好的地址,而不是操作系统的根目录。例如,Local 驱动 `root` 设置为 `/var/www`, 则本地磁盘上的 `/var/www/public/file.txt` 通过 flysystem API 访问时应使用 `/public/file.txt` 或 `public/file.txt` 。
4. 以阿里云 OSS 为例,1 核 1 进程读操作性能对比:
```bash
@@ -239,7 +239,7 @@ return [
'accessKey' => env('QINIU_ACCESS_KEY'),
'secretKey' => env('QINIU_SECRET_KEY'),
'bucket' => env('QINIU_BUCKET'),
- 'domain' => env('QINBIU_DOMAIN'),
+ 'domain' => env('QINIU_DOMAIN'),
],
],
];
diff --git a/doc/zh-cn/imgs/whoops.png b/doc/zh-cn/imgs/whoops.png
new file mode 100644
index 000000000..6a52b314a
Binary files /dev/null and b/doc/zh-cn/imgs/whoops.png differ
diff --git a/doc/zh-cn/nano.md b/doc/zh-cn/nano.md
new file mode 100644
index 000000000..2e8e10f23
--- /dev/null
+++ b/doc/zh-cn/nano.md
@@ -0,0 +1,260 @@
+
+通过 `hyperf/nano` 可以在无骨架、零配置的情况下快速搭建 Hyperf 应用。
+
+## 安装
+
+```php
+composer install hyperf/nano
+```
+
+## 快速开始
+
+```php
+get('/', function () {
+
+ $user = $this->request->input('user', 'nano');
+ $method = $this->request->getMethod();
+
+ return [
+ 'message' => "hello {$user}",
+ 'method' => $method,
+ ];
+
+});
+
+$app->run();
+```
+
+启动:
+
+```bash
+php index.php start
+```
+
+简洁如此。
+
+## 特性
+
+* 无骨架
+* 零配置
+* 快速启动
+* 闭包风格
+* 支持注解外的全部 Hyperf 功能
+* 兼容全部 Hyperf 组件
+* Phar 友好
+
+## 更多示例
+
+### 路由
+
+$app 集成了 Hyperf 路由器的所有方法。
+
+```php
+addGroup('/nano', function () use ($app) {
+ $app->addRoute(['GET', 'POST'], '/{id:\d+}', function($id) {
+ return '/nano/'.$id;
+ });
+ $app->put('/{name:.+}', function($name) {
+ return '/nano/'.$name;
+ });
+});
+
+$app->run();
+```
+
+### DI 容器
+```php
+getContainer()->set(Foo::class, new Foo());
+
+$app->get('/', function () {
+ /** @var ContainerProxy $this */
+ $foo = $this->get(Foo::class);
+ return $foo->bar();
+});
+
+$app->run();
+```
+> 所有 $app 管理的闭包回调中,$this 都被绑定到了 `Hyperf\Nano\ContainerProxy` 上。
+
+### 中间件
+```php
+get('/', function () {
+ return $this->request->getAttribute('key');
+});
+
+$app->addMiddleware(function ($request, $handler) {
+ $request = $request->withAttribute('key', 'value');
+ return $handler->handle($request);
+});
+
+$app->run();
+```
+
+> 除了闭包之外,所有 $app->addXXX() 方法还接受类名作为参数。可以传入对应的 Hyperf 类。
+
+### 异常处理
+
+```php
+get('/', function () {
+ throw new \Exception();
+});
+
+$app->addExceptionHandler(function ($throwable, $response) {
+ return $response->withStatus('418')
+ ->withBody(new SwooleStream('I\'m a teapot'));
+});
+
+$app->run();
+```
+
+### 命令行
+
+```php
+addCommand('echo', function(){
+ $this->get(StdoutLoggerInterface::class)->info('A new command called echo!');
+});
+
+$app->run();
+```
+
+执行
+
+```bash
+php index.php echo
+```
+
+### 事件监听
+
+```php
+addListener(BootApplication::class, function($event){
+ $this->get(StdoutLoggerInterface::class)->info('App started');
+});
+
+$app->run();
+```
+
+### 自定义进程
+```php
+addProcess(function(){
+ while (true) {
+ sleep(1);
+ $this->get(StdoutLoggerInterface::class)->info('Processing...');
+ }
+});
+
+$app->run();
+```
+
+### 定时任务
+
+```php
+addCrontab('* * * * * *', function(){
+ $this->get(StdoutLoggerInterface::class)->info('execute every second!');
+});
+
+$app->run();
+```
+
+### 使用 Hyperf 组件.
+
+```php
+config([
+ 'db.default' => [
+ 'host' => env('DB_HOST', 'localhost'),
+ 'port' => env('DB_PORT', 3306),
+ 'database' => env('DB_DATABASE', 'hyperf'),
+ 'username' => env('DB_USERNAME', 'root'),
+ 'password' => env('DB_PASSWORD', ''),
+ ]
+]);
+
+$app->get('/', function(){
+ return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]);
+});
+
+$app->run();
+```
diff --git a/doc/zh-cn/paginator.md b/doc/zh-cn/paginator.md
index 2558f6156..590fc3ccd 100644
--- a/doc/zh-cn/paginator.md
+++ b/doc/zh-cn/paginator.md
@@ -21,6 +21,7 @@ namespace App\Controller;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Paginator\Paginator;
+use Hyperf\Utils\Collection;
/**
* @AutoController()
@@ -29,15 +30,20 @@ class UserController
{
public function index(RequestInterface $request)
{
- $currentPage = $request->input('page', 1);
- $perPage = $request->input('per_page', 2);
- $users = [
+ $currentPage = (int) $request->input('page', 1);
+ $perPage = (int) $request->input('per_page', 2);
+
+ // 这里根据 $currentPage 和 $perPage 进行数据查询,以下使用 Collection 代替
+ $collection = new Collection([
['id' => 1, 'name' => 'Tom'],
['id' => 2, 'name' => 'Sam'],
['id' => 3, 'name' => 'Tim'],
['id' => 4, 'name' => 'Joe'],
- ];
- return new Paginator($users, (int) $perPage, (int) $currentPage);
+ ]);
+
+ $users = array_values($collection->forPage($currentPage, $perPage)->toArray());
+
+ return new Paginator($users, $perPage, $currentPage);
}
}
```
diff --git a/doc/zh-cn/sdks/wechat.md b/doc/zh-cn/sdks/wechat.md
index c2ca5de48..6e4a892ce 100644
--- a/doc/zh-cn/sdks/wechat.md
+++ b/doc/zh-cn/sdks/wechat.md
@@ -49,7 +49,6 @@ AbstractProvider::setGuzzleOptions([
request->getBody()->getContents();
$app['request'] = new Request($get,$post,[],$cookie,$files,$server,$xml);
// Do something...
+
+```
+
+3. 服务器配置
+
+如果需要使用微信公众平台的服务器配置功能,可以使用以下代码。
+
+> 以下 `$response` 为 `Symfony\Component\HttpFoundation\Response` 并非 `Hyperf\HttpMessage\Server\Response`
+> 所以只需将 `Body` 内容直接返回,即可通过微信验证。
+
+```php
+$response = $app->server->serve();
+
+return $response->getBody()->getContents();
```
## 如何替换缓存
@@ -92,5 +105,4 @@ use EasyWeChat\Factory;
$app = Factory::miniProgram([]);
$app['cache'] = ApplicationContext::getContainer()->get(CacheInterface::class);
-
```
diff --git a/doc/zh-cn/session.md b/doc/zh-cn/session.md
index 31a5c3ffa..a803dadd9 100644
--- a/doc/zh-cn/session.md
+++ b/doc/zh-cn/session.md
@@ -14,7 +14,7 @@ Session 组件的配置储存于 `config/autoload/session.php` 文件中,如
## 配置 Session 中间件
-在使用 Session 之前,您需要将 `Hyperf\Session\Middleware\SessionMiddleware` 中间件配置为 HTTP Server 的全局中间件,这样组件才能介入到请求流程进行对应的处理,`config/autoload/middleware.php` 配置文件示例如下:
+在使用 Session 之前,您需要将 `Hyperf\Session\Middleware\SessionMiddleware` 中间件配置为 HTTP Server 的全局中间件,这样组件才能介入到请求流程进行对应的处理,`config/autoload/middlewares.php` 配置文件示例如下:
```php
'socket-io',
+ 'type' => Server::SERVER_WEBSOCKET,
+ 'host' => '0.0.0.0',
+ 'port' => 9502,
+ 'sock_type' => SWOOLE_SOCK_TCP,
+ 'callbacks' => [
+ SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
+ SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
+ SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
+ ],
+ ],
+```
+
+
+## 快速开始
+
+### 服务端
+```php
+join($data);
+ // 向房间内其他用户推送(不含当前用户)
+ $socket->to($data)->emit('event', $socket->getSid() . "has joined {$data}");
+ // 向房间内所有人广播(含当前用户)
+ $this->emit('event', 'There are ' . count($socket->getAdapter()->clients($data)) . " players in {$data}");
+ }
+
+ /**
+ * @Event("say")
+ * @param string $data
+ */
+ public function onSay(Socket $socket, $data)
+ {
+ $data = Json::decode($data);
+ $socket->to($data['room'])->emit('event', $socket->getSid() . " say: {$data['message']}");
+ }
+}
+
+```
+
+> 每个 socket 会自动加入以自己 `sid` 命名的房间(`$socket->getSid()`),发送私聊信息就推送到对应 `sid` 即可。
+
+> 框架会自动触发 `connect` 和 `disconnect` 两个事件。
+
+### 客户端
+
+由于服务端只实现了WebSocket通讯,所以客户端要加上 `{transports:["websocket"]}` 。
+
+```html
+
+
+```
+
+## API 清单
+
+```php
+emit('hello', 'can you hear me?', 1, 2, 'abc');
+
+ // sending to all clients except sender
+ $socket->broadcast->emit('broadcast', 'hello friends!');
+
+ // sending to all clients in 'game' room except sender
+ $socket->to('game')->emit('nice game', "let's play a game");
+
+ // sending to all clients in 'game1' and/or in 'game2' room, except sender
+ $socket->to('game1')->to('game2')->emit('nice game', "let's play a game (too)");
+
+ // WARNING: `$socket->to($socket->getSid())->emit()` will NOT work, as it will send to everyone in the room
+ // named `$socket->getSid()` but the sender. Please use the classic `$socket->emit()` instead.
+
+ // sending with acknowledgement
+ $reply = $socket->emit('question', 'do you think so?')->reply();
+
+ // sending without compression
+ $socket->compress(false)->emit('uncompressed', "that's rough");
+
+ $io = \Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\SocketIOServer\SocketIO::class);
+
+ // sending to all clients in 'game' room, including sender
+ $io->in('game')->emit('big-announcement', 'the game will start soon');
+
+ // sending to all clients in namespace 'myNamespace', including sender
+ $io->of('/myNamespace')->emit('bigger-announcement', 'the tournament will start soon');
+
+ // sending to a specific room in a specific namespace, including sender
+ $io->of('/myNamespace')->to('room')->emit('event', 'message');
+
+ // sending to individual socketid (private message)
+ $io->to('socketId')->emit('hey', 'I just met you');
+
+ // sending to all clients on this node (when using multiple nodes)
+ $io->local->emit('hi', 'my lovely babies');
+
+ // sending to all connected clients
+ $io->emit('an event sent to all connected clients');
+
+};
+```
+
+## 进阶教程
+
+### 设置 Socket.io 命名空间
+
+Socket.io 通过自定义命名空间实现多路复用。(注意:不是 PHP 的命名空间)
+
+1. 可以通过 `@SocketIONamespace("/xxx")` 将控制器映射为 xxx 的命名空间,
+
+2. 也可通过
+
+```php
+ swoole 4.4.17 及以下版本只能读取 http 创建好的Cookie,4.4.18 及以上版本可以在WebSocket握手时创建Cookie
+
+### 调整房间适配器
+
+默认的房间功能通过 Redis 适配器实现,可以适应多进程乃至分布式场景。
+
+1. 可以替换为内存适配器,只适用于单 worker 场景。
+```php
+ \Hyperf\SocketIOServer\Room\MemoryAdapter::class,
+];
+```
+
+2. 可以替换为空适配器,不需要房间功能时可以降低消耗。
+```php
+ \Hyperf\SocketIOServer\Room\NullAdapter::class,
+];
+```
+
+### 调整 SocketID (`sid`)
+
+默认 SocketID 使用 `ServerID#FD` 的格式,可以适应分布式场景。
+
+1. 可以替换为直接使用 Fd 。
+
+```php
+ \Hyperf\SocketIOServer\SidProvider\LocalSidProvider::class,
+];
+```
+
+2. 也可以替换为 SessionID 。
+
+```php
+ \Hyperf\SocketIOServer\SidProvider\SessionSidProvider::class,
+];
+```
+
+### 其他事件分发方法
+
+1. 可以手动注册事件,不使用注解。
+
+```php
+on('event', [$this, 'echo']);
+ }
+
+ public function echo(Socket $socket, $data)
+ {
+ $socket->emit('event', $data);
+ }
+}
+```
+
+2. 可以在控制器上添加 `@Event()` 注解,以方法名作为事件名来分发。此时应注意其他公有方法可能会和事件名冲突。
+
+```php
+emit('event', $data);
+ }
+}
+```
diff --git a/doc/zh-cn/summary.md b/doc/zh-cn/summary.md
index 484c58b40..c447c4476 100644
--- a/doc/zh-cn/summary.md
+++ b/doc/zh-cn/summary.md
@@ -52,6 +52,7 @@
* [模型缓存](zh-cn/db/model-cache.md)
* [数据库迁移](zh-cn/db/migration.md)
* [极简 DB 组件](zh-cn/db/db.md)
+ * [修改器](zh-cn/db/mutators.md)
* 微服务
@@ -83,6 +84,7 @@
* [ETCD 协程客户端](zh-cn/etcd.md)
* [WebSocket 服务](zh-cn/websocket-server.md)
* [WebSocket 协程客户端](zh-cn/websocket-client.md)
+ * [Socket.io 服务](zh-cn/socketio-server.md)
* [自定义进程](zh-cn/process.md)
* [开发者工具](zh-cn/devtool.md)
* [辅助类](zh-cn/utils.md)
diff --git a/doc/zh-hk/amqp.md b/doc/zh-hk/amqp.md
index a05b63eb0..87f23696c 100644
--- a/doc/zh-hk/amqp.md
+++ b/doc/zh-hk/amqp.md
@@ -209,3 +209,88 @@ class DemoConsumer extends ConsumerMessage
| \Hyperf\Amqp\Result::NACK | 消息沒有被正確消費掉,以 `basic_nack` 方法來響應 |
| \Hyperf\Amqp\Result::REQUEUE | 消息沒有被正確消費掉,以 `basic_reject` 方法來響應,並使消息重新入列 |
| \Hyperf\Amqp\Result::DROP | 消息沒有被正確消費掉,以 `basic_reject` 方法來響應 |
+
+## RPC 遠程過程調用
+
+除了典型的消息隊列場景,我們還可以通過 AMQP 來實現 RPC 遠程過程調用,本組件也為這個實現提供了對應的支持。
+
+### 創建消費者
+
+RPC 使用的消費者,與典型消息隊列場景的消費者實現基本無差,唯一的區別是需要通過調用 `reply` 方法返回數據給生產者。
+
+```php
+reply($data, $message);
+
+ return Result::ACK;
+ }
+}
+```
+
+### 發起 RPC 調用
+
+作為生成者發起一次 RPC 遠程過程調用也非常的簡單,只需通過依賴注入容器獲得 `Hyperf\Amqp\RpcClient` 對象並調用其中的 `call` 方法即可,返回的結果是消費者 reply 的數據,如下所示:
+
+```php
+get(RpcClient::class);
+// 在 DynamicRpcMessage 上設置與 Consumer 一致的 Exchange 和 RoutingKey
+$result = $rpcClient->call(new DynamicRpcMessage('hyperf', 'hyperf', ['message' => 'Hello Hyperf']));
+
+// $result:
+// array(1) {
+// ["message"]=>
+// string(18) "Reply:Hello Hyperf"
+// }
+```
+
+### 抽象 RpcMessage
+
+上面的 RPC 調用過程是直接通過 `Hyperf\Amqp\Message\DynamicRpcMessage` 類來完成 Exchange 和 RoutingKey 的定義,並傳遞消息數據,在生產項目的設計上,我們可以對 RpcMessage 進行一層抽象,以統一 Exchange 和 RoutingKey 的定義。
+
+我們可以創建對應的 RpcMessage 類如 `App\Amqp\FooRpcMessage` 如下:
+
+```php
+payload = $data;
+ }
+
+}
+```
+
+這樣我們進行 RPC 調用時,只需直接傳遞 `FooRpcMessage` 實例到 `call` 方法即可,無需每次調用時都去定義 Exchange 和 RoutingKey。
\ No newline at end of file
diff --git a/doc/zh-hk/async-queue.md b/doc/zh-hk/async-queue.md
index 13b15a339..16adcf643 100644
--- a/doc/zh-hk/async-queue.md
+++ b/doc/zh-hk/async-queue.md
@@ -18,7 +18,7 @@ composer require hyperf/async-queue
|:----------------:|:---------:|:-------------------------------------------:|:---------------------------------------:|
| driver | string | Hyperf\AsyncQueue\Driver\RedisDriver::class | 無 |
| channel | string | queue | 隊列前綴 |
-| redis.pool | string | default | redis連接池 |
+| redis.pool | string | default | redis 連接池 |
| timeout | int | 2 | pop 消息的超時時間 |
| retry_seconds | int,array | 5 | 失敗後重新嘗試間隔 |
| handle_timeout | int | 10 | 消息處理超時時間 |
diff --git a/doc/zh-hk/changelog.md b/doc/zh-hk/changelog.md
index c02ff3558..0bf79c59a 100644
--- a/doc/zh-hk/changelog.md
+++ b/doc/zh-hk/changelog.md
@@ -1,5 +1,59 @@
# 版本更新記錄
+# v1.1.32 - 2020-05-21
+
+## 修復
+
+- [#1734](https://github.com/hyperf/hyperf/pull/1734) 修復模型多態查詢,關聯關係為空時,也會查詢 SQL 的問題;
+- [#1739](https://github.com/hyperf/hyperf/pull/1739) 修復 `hyperf/filesystem` 組件 OSS HOOK 位運算錯誤,導致 resource 判斷不準確的問題;
+- [#1743](https://github.com/hyperf/hyperf/pull/1743) 修復 `grafana.json` 中錯誤的`refId` 字段值;
+- [#1748](https://github.com/hyperf/hyperf/pull/1748) 修復 `hyperf/amqp` 組件在使用其他連接池時,對應的 `concurrent.limit` 配置不生效的問題;
+- [#1750](https://github.com/hyperf/hyperf/pull/1750) 修復連接池組件,在連接關閉失敗時會導致計數有誤的問題;
+- [#1754](https://github.com/hyperf/hyperf/pull/1754) 修復 BASE Server 服務,啟動提示沒有考慮 UDP 服務的情況;
+- [#1764](https://github.com/hyperf/hyperf/pull/1764) 修復當時間值為 null 時,datatime 驗證器執行失敗的 BUG;
+- [#1769](https://github.com/hyperf/hyperf/pull/1769) 修復 `hyperf/socketio-server` 組件中,客户端初始化斷開連接操作時會報 Notice 的錯誤的問題;
+
+## 新增
+
+- [#1724](https://github.com/hyperf/hyperf/pull/1724) 新增模型方法 `Model::orWhereHasMorph` ,`Model::whereDoesntHaveMorph` and `Model::orWhereDoesntHaveMorph`;
+- [#1742](https://github.com/hyperf/hyperf/pull/1742) 新增模型 自定義類型轉換器 功能;
+ - 新增 interface `Castable`, `CastsAttributes` 和 `CastsInboundAttributes`;
+ - 新增方法 `Model\Builder::withCasts`;
+ - 新增方法 `Model::loadMorph`, `Model::loadMorphCount` 和 `Model::syncAttributes`;
+
+# v1.1.31 - 2020-05-14
+
+## 新增
+
+- [#1723](https://github.com/hyperf/hyperf/pull/1723) 異常處理器集成了 filp/whoops 。
+- [#1730](https://github.com/hyperf/hyperf/pull/1730) 為命令 `gen:model` 可選項 `--refresh-fillable` 添加簡寫 `-R`。
+
+## 修復
+
+- [#1696](https://github.com/hyperf/hyperf/pull/1696) 修復方法 `Context::copy` 傳入字段 `keys` 後無法正常使用的BUG。
+- [#1708](https://github.com/hyperf/hyperf/pull/1708) [#1718](https://github.com/hyperf/hyperf/pull/1718) 修復 `hyperf/socketio-server` 組件內存溢出等BUG。
+
+## 優化
+
+- [#1710](https://github.com/hyperf/hyperf/pull/1710) MAC系統下不再使用 `cli_set_process_title` 方法設置進程名。
+
+# v1.1.30 - 2020-05-07
+
+## 新增
+
+- [#1616](https://github.com/hyperf/hyperf/pull/1616) 新增 ORM 方法 `morphWith` 和 `whereHasMorph`。
+- [#1651](https://github.com/hyperf/hyperf/pull/1651) 新增 `socket.io-server` 組件。
+- [#1666](https://github.com/hyperf/hyperf/pull/1666) [#1669](https://github.com/hyperf/hyperf/pull/1669) 新增 AMQP RPC 客户端。
+
+## 修復
+
+- [#1682](https://github.com/hyperf/hyperf/pull/1682) 修復 `RpcPoolTransporter` 的連接池配置不生效的 BUG。
+- [#1683](https://github.com/hyperf/hyperf/pull/1683) 修復 `RpcConnection` 連接失敗後,相同協程內無法正常重置連接的 BUG。
+
+## 優化
+
+- [#1670](https://github.com/hyperf/hyperf/pull/1670) 優化掉 `Cache 組件` 一條無意義的刪除指令。
+
# v1.1.28 - 2020-04-30
## 新增
@@ -26,16 +80,16 @@
## 新增
- [#1575](https://github.com/hyperf/hyperf/pull/1575) 為腳本 `gen:model` 生成的模型,自動添加 `relation` `scope` 和 `attributes` 的變量註釋。
-- [#1586](https://github.com/hyperf/hyperf/pull/1586) 添加 `symfony/event-dispatcher` 組件小於 `4.3` 時的 `conflict` 配置。用於解決用户使用了 `4.3` 以下版本時,導致 `SymfonyDispatcher` 實現衝突的BUG。
+- [#1586](https://github.com/hyperf/hyperf/pull/1586) 添加 `symfony/event-dispatcher` 組件小於 `4.3` 時的 `conflict` 配置。用於解決用户使用了 `4.3` 以下版本時,導致 `SymfonyDispatcher` 實現衝突的 BUG。
- [#1597](https://github.com/hyperf/hyperf/pull/1597) 為 `AMQP` 消費者,添加最大消費次數 `maxConsumption`。
- [#1603](https://github.com/hyperf/hyperf/pull/1603) 為 `WebSocket` 服務添加基於 `fd` 存儲的 `Context`。
## 修復
-- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修復 `jsonrpc` 服務,發佈了相同名字不同協議到 `consul` 後,客户端無法正常工作的BUG。
-- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修復了文件鎖在協程下可能會造成死鎖的BUG。
-- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修復了重寫後的 `go` 方法,返回值與 `swoole` 原生方法不符的BUG。
-- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修復當路由 `Handler` 是匿名函數時,腳本 `describe:routes` 執行失敗的BUG。
+- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修復 `jsonrpc` 服務,發佈了相同名字不同協議到 `consul` 後,客户端無法正常工作的 BUG。
+- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修復了文件鎖在協程下可能會造成死鎖的 BUG。
+- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修復了重寫後的 `go` 方法,返回值與 `swoole` 原生方法不符的 BUG。
+- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修復當路由 `Handler` 是匿名函數時,腳本 `describe:routes` 執行失敗的 BUG。
# v1.1.26 - 2020-04-16
@@ -47,9 +101,9 @@
- [#1563](https://github.com/hyperf/hyperf/pull/1563) 修復服務關停後,定時器的 `onOneServer` 配置不會被重置。
- [#1565](https://github.com/hyperf/hyperf/pull/1565) 當 `DB` 組件重連 `Mysql` 時,重置事務等級為 0。
-- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修復 `Hyperf\GrpcServer\CoreMiddleware` 中,自定義類的父類找不到時報錯的BUG。
-- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修復 `describe:routes` 腳本 `server` 配置不生效的BUG。
-- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修復 `migrate:refresh` 腳本 `step` 參數不為 `int` 時會報錯的BUG。
+- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修復 `Hyperf\GrpcServer\CoreMiddleware` 中,自定義類的父類找不到時報錯的 BUG。
+- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修復 `describe:routes` 腳本 `server` 配置不生效的 BUG。
+- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修復 `migrate:refresh` 腳本 `step` 參數不為 `int` 時會報錯的 BUG。
## 變更
diff --git a/doc/zh-hk/db/mutators.md b/doc/zh-hk/db/mutators.md
new file mode 100644
index 000000000..358c0f66e
--- /dev/null
+++ b/doc/zh-hk/db/mutators.md
@@ -0,0 +1,555 @@
+# 修改器
+
+> 本文檔大量借鑑於 [LearnKu](https://learnku.com) 十分感謝 LearnKu 對 PHP 社區做出的貢獻。
+
+當你在模型實例中獲取或設置某些屬性值的時候,訪問器和修改器允許你對模型屬性值進行格式化。
+
+## 訪問器 & 修改器
+
+### 定義一個訪問器
+
+若要定義一個訪問器, 則需在模型上創建一個 `getFooAttribute` 方法,要訪問的 `Foo` 字段需使用「駝峯式」命名。 在這個示例中,我們將為 `first_name` 屬性定義一個訪問器。當模型嘗試獲取 `first_name` 屬性時,將自動調用此訪問器:
+
+```php
+first_name;
+```
+
+當然,你也可以通過已有的屬性值,使用訪問器返回新的計算值:
+
+```php
+namespace App;
+
+use Hyperf\DbConnection\Model\Model;
+
+class User extends Model
+{
+ /**
+ * 獲取用户的姓名.
+ *
+ * @return string
+ */
+ public function getFullNameAttribute()
+ {
+ return "{$this->first_name} {$this->last_name}";
+ }
+}
+```
+
+### 定義一個修改器
+
+若要定義一個修改器,則需在模型上面定義 `setFooAttribute` 方法。要訪問的 `Foo` 字段使用「駝峯式」命名。讓我們再來定義一個 `first_name` 屬性的修改器。當我們嘗試在模式上在設置 `first_name` 屬性值時,該修改器將被自動調用:
+
+```php
+attributes['first_name'] = strtolower($value);
+ }
+}
+```
+
+修改器會獲取屬性已經被設置的值,允許你修改並且將其值設置到模型內部的 `$attributes` 屬性上。舉個例子,如果我們嘗試將 `first_name` 屬性的值設置為 `Sally`:
+
+```php
+$user = App\User::find(1);
+
+$user->first_name = 'Sally';
+```
+
+在這個例子中,`setFirstNameAttribute` 方法在調用的時候接受 `Sally` 這個值作為參數。接着修改器會應用 `strtolower` 函數並將處理的結果設置到內部的 `$attributes` 數組。
+
+## 日期轉化器
+
+默認情況下,模型會將 `created_at` 和 `updated_at` 字段轉換為 `Carbon` 實例,它繼承了 `PHP` 原生的 `DateTime` 類並提供了各種有用的方法。你可以通過設置模型的 `$dates` 屬性來添加其他日期屬性:
+
+```php
+ Tip: 你可以通過將模型的公有屬性 $timestamps 值設置為 false 來禁用默認的 created_at 和 updated_at 時間戳。
+
+當某個字段是日期格式時,你可以將值設置為一個 `UNIX` 時間戳,日期時間 `(Y-m-d)` 字符串,或者 `DateTime` / `Carbon` 實例。日期值會被正確格式化並保存到你的數據庫中:
+
+就如上面所説,當獲取到的屬性包含在 `$dates` 屬性中時,都會自動轉換為 `Carbon` 實例,允許你在屬性上使用任意的 `Carbon` 方法:
+
+```php
+$user = App\User::find(1);
+
+return $user->deleted_at->getTimestamp();
+```
+
+### 時間格式
+
+時間戳都將以 `Y-m-d H:i:s` 形式格式化。如果你需要自定義時間戳格式,可在模型中設置 `$dateFormat` 屬性。這個屬性決定了日期屬性將以何種形式保存在數據庫中,以及當模型序列化成數組或 `JSON` 時的格式:
+
+```php
+`, `string`, `boolean`, `object`, `array`, `collection`, `date`, `datetime` 和 `timestamp`。 當需要轉換為 `decimal` 類型時,你需要定義小數位的個數,如: `decimal:2`。
+
+示例, 讓我們把以整數( `0` 或 `1` )形式存儲在數據庫中的 `is_admin` 屬性轉成布爾值:
+
+```php
+ 'boolean',
+ ];
+}
+```
+
+現在當你訪問 `is_admin` 屬性時,雖然保存在數據庫裏的值是一個整數類型,但是返回值總是會被轉換成布爾值類型:
+
+```php
+$user = App\User::find(1);
+
+if ($user->is_admin) {
+ //
+}
+```
+
+### 自定義類型轉換
+
+模型內置了多種常用的類型轉換。但是,用户偶爾會需要將數據轉換成自定義類型。現在,該需求可以通過定義一個實現 `CastsAttributes` 接口的類來完成
+
+實現了該接口的類必須事先定義一個 `get` 和 `set` 方法。 `get` 方法負責將從數據庫中獲取的原始數據轉換成對應的類型,而 `set` 方法則是將數據轉換成對應的數據庫類型以便存入數據庫中。舉個例子,下面我們將內置的 `json` 類型轉換以自定義類型轉換的形式重新實現一遍:
+
+```php
+ Json::class,
+ ];
+}
+```
+
+#### 值對象類型轉換
+
+你不僅可以將數據轉換成原生的數據類型,還可以將數據轉換成對象。兩種自定義類型轉換的定義方式非常類似。但是將數據轉換成對象的自定義轉換類中的 `set` 方法需要返回鍵值對數組,用於設置原始、可存儲的值到對應的模型中。
+
+舉個例子,定義一個自定義類型轉換類用於將多個模型屬性值轉換成單個 `Address` 值對象,假設 `Address` 對象有兩個公有屬性 `lineOne` 和 `lineTwo`:
+
+```php
+ $value->lineOne,
+ 'address_line_two' => $value->lineTwo,
+ ];
+ }
+}
+```
+
+進行值對象類型轉換後,任何對值對象的數據變更將會自動在模型保存前同步回模型當中:
+
+```php
+address->lineOne = 'Updated Address Value';
+$user->address->lineTwo = '#10000';
+
+$user->save();
+
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Updated Address Value',
+// 'address_line_two' => '#10000'
+//];
+```
+
+**這裏的實現與 Laravel 不同,如果出現以下用法,請需要格外注意**
+
+```php
+$user = App\User::find(1);
+
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Address Value',
+// 'address_line_two' => '#10000'
+//];
+
+$user->address->lineOne = 'Updated Address Value';
+$user->address->lineTwo = '#20000';
+
+// 直接修改 address 的字段後,是無法立馬再 attributes 中生效的,但可以直接通過 $user->address 拿到修改後的數據。
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Address Value',
+// 'address_line_two' => '#10000'
+//];
+
+// 當我們保存數據或者刪除數據後,attributes 便會改成修改後的數據。
+$user->save();
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Updated Address Value',
+// 'address_line_two' => '#20000'
+//];
+```
+
+如果修改 `address` 後,不想要保存,也不想通過 `address->lineOne` 獲取 `address_line_one` 的數據,還可以使用以下 方法
+
+```php
+$user = App\User::find(1);
+$user->address->lineOne = 'Updated Address Value';
+$user->syncAttributes();
+var_dump($user->getAttributes());
+```
+
+當然,如果您仍然需要修改對應的 `value` 後,同步修改 `attributes` 的功能,可以嘗試使用以下方式。首先,我們實現一個 `UserInfo` 並繼承 `CastsValue`。
+
+```php
+namespace App\Caster;
+
+use Hyperf\Database\Model\CastsValue;
+
+/**
+ * @property string $name
+ * @property int $gender
+ */
+class UserInfo extends CastsValue
+{
+}
+```
+
+然後實現對應的 `UserInfoCaster`
+
+```php
+ $value->name,
+ 'gender' => $value->gender,
+ ];
+ }
+}
+
+```
+
+當我們再使用以下方式修改 UserInfo 時,便可以同步修改到 attributes 的數據。
+
+```php
+/** @var User $user */
+$user = User::query()->find(100);
+$user->userInfo->name = 'John1';
+var_dump($user->getAttributes()); // ['name' => 'John1']
+```
+
+#### 入站類型轉換
+
+有時候,你可能只需要對寫入模型的屬性值進行類型轉換而不需要對從模型中獲取的屬性值進行任何處理。一個典型入站類型轉換的例子就是「hashing」。入站類型轉換類需要實現 `CastsInboundAttributes` 接口,只需要實現 `set` 方法。
+
+```php
+algorithm = $algorithm;
+ }
+
+ /**
+ * 轉換成將要進行存儲的值
+ */
+ public function set($model, $key, $value, $attributes)
+ {
+ return hash($this->algorithm, $value);
+ }
+}
+```
+
+#### 類型轉換參數
+
+當將自定義類型轉換附加到模型時,可以指定傳入的類型轉換參數。傳入類型轉換參數需使用 `:` 將參數與類名分隔,多個參數之間使用逗號分隔。這些參數將會傳遞到類型轉換類的構造函數中:
+
+```php
+ Hash::class.':sha256',
+ ];
+}
+```
+
+### 數組 & `JSON` 轉換
+
+當你在數據庫存儲序列化的 `JSON` 的數據時,`array` 類型的轉換非常有用。比如:如果你的數據庫具有被序列化為 `JSON` 的 `JSON` 或 `TEXT` 字段類型,並且在模型中加入了 `array` 類型轉換,那麼當你訪問的時候就會自動被轉換為 `PHP` 數組:
+
+```php
+ 'array',
+ ];
+}
+```
+
+一旦定義了轉換,你訪問 `options` 屬性時他會自動從 `JSON` 類型反序列化為 `PHP` 數組。當你設置了 `options` 屬性的值時,給定的數組也會自動序列化為 `JSON` 類型存儲:
+
+```php
+$user = App\User::find(1);
+
+$options = $user->options;
+
+$options['key'] = 'value';
+
+$user->options = $options;
+
+$user->save();
+```
+
+### Date 類型轉換
+
+當使用 `date` 或 `datetime` 屬性時,可以指定日期的格式。 這種格式會被用在模型序列化為數組或者 `JSON`:
+
+```php
+ 'datetime:Y-m-d',
+ ];
+}
+```
+
+### 查詢時類型轉換
+
+有時候需要在查詢執行過程中對特定屬性進行類型轉換,例如需要從數據庫表中獲取數據的時候。舉個例子,請參考以下查詢:
+
+```php
+use App\Post;
+use App\User;
+
+$users = User::select([
+ 'users.*',
+ 'last_posted_at' => Post::selectRaw('MAX(created_at)')
+ ->whereColumn('user_id', 'users.id')
+])->get();
+```
+
+在該查詢獲取到的結果集中,`last_posted_at` 屬性將會是一個字符串。假如我們在執行查詢時進行 `date` 類型轉換將更方便。你可以通過使用 `withCasts` 方法來完成上述操作:
+
+```php
+$users = User::select([
+ 'users.*',
+ 'last_posted_at' => Post::selectRaw('MAX(created_at)')
+ ->whereColumn('user_id', 'users.id')
+])->withCasts([
+ 'last_posted_at' => 'date'
+])->get();
+```
+
diff --git a/doc/zh-hk/db/relationship.md b/doc/zh-hk/db/relationship.md
index 31c30741f..0f04879f6 100644
--- a/doc/zh-hk/db/relationship.md
+++ b/doc/zh-hk/db/relationship.md
@@ -212,7 +212,6 @@ return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
```
-
## 預加載
當以屬性方式訪問 `Hyperf` 關聯時,關聯數據「懶加載」。這着直到第一次訪問屬性時關聯數據才會被真實加載。不過 `Hyperf` 能在查詢父模型時「預先載入」子關聯。預加載可以緩解 N + 1 查詢問題。為了説明 N + 1 查詢問題,考慮 `User` 模型關聯到 `Role` 的情形:
@@ -264,3 +263,249 @@ SELECT * FROM `user`;
SELECT * FROM `role` WHERE id in (1, 2, 3, ...);
```
+
+## 多態關聯
+
+多態關聯允許目標模型藉助關聯關係,關聯多個模型。
+
+### 一對一(多態)
+
+#### 表結構
+
+一對一多態關聯與簡單的一對一關聯類似;不過,目標模型能夠在一個關聯上從屬於多個模型。
+例如,Book 和 User 可能共享一個關聯到 Image 模型的關係。使用一對一多態關聯允許使用一個唯一圖片列表同時用於 Book 和 User。讓我們先看看錶結構:
+
+```
+book
+ id - integer
+ title - string
+
+user
+ id - integer
+ name - string
+
+image
+ id - integer
+ url - string
+ imageable_id - integer
+ imageable_type - string
+```
+
+image 表中的 imageable_id 字段會根據 imageable_type 的不同代表不同含義,默認情況下,imageable_type 直接是相關模型類名。
+
+#### 模型示例
+
+```php
+morphTo();
+ }
+}
+
+class Book extends Model
+{
+ public function image()
+ {
+ return $this->morphOne(Image::class, 'imageable');
+ }
+}
+
+class User extends Model
+{
+ public function image()
+ {
+ return $this->morphOne(Image::class, 'imageable');
+ }
+}
+```
+
+#### 獲取關聯
+
+按照上述定義模型後,我們就可以通過模型關係獲取對應的模型。
+
+比如,我們獲取某用户的圖片。
+
+```php
+use App\Model\User;
+
+$user = User::find(1);
+
+$image = $user->image;
+```
+
+或者我們獲取某個圖片對應用户或書本。`imageable` 會根據 `imageable_type` 獲取對應的 `User` 或者 `Book`。
+
+```php
+use App\Model\Image;
+
+$image = Image::find(1);
+
+$imageable = $image->imageable;
+```
+
+### 一對多(多態)
+
+#### 模型示例
+
+```php
+morphTo();
+ }
+}
+
+class Book extends Model
+{
+ public function images()
+ {
+ return $this->morphMany(Image::class, 'imageable');
+ }
+}
+
+class User extends Model
+{
+ public function images()
+ {
+ return $this->morphMany(Image::class, 'imageable');
+ }
+}
+```
+
+#### 獲取關聯
+
+獲取用户所有的圖片
+
+```php
+use App\Model\User;
+
+$user = User::query()->find(1);
+foreach ($user->images as $image) {
+ // ...
+}
+```
+
+### 自定義多態映射
+
+默認情況下,框架要求 `type` 必須存儲對應模型類名,比如上述 `imageable_type` 必須是對應的 `User::class` 和 `Book::class`,但顯然在實際應用中,這是十分不方便的。所以我們可以自定義映射關係,來解耦數據庫與應用內部結構。
+
+```php
+use App\Model;
+use Hyperf\Database\Model\Relations\Relation;
+Relation::morphMap([
+ 'user' => Model\User::class,
+ 'book' => Model\Book::class,
+]);
+```
+
+因為 `Relation::morphMap` 修改後會常駐內存,所以我們可以在項目啟動時,就創建好對應的關係映射。我們可以創建以下監聽器:
+
+```php
+ Model\User::class,
+ 'book' => Model\Book::class,
+ ]);
+ }
+}
+
+```
+
+### 嵌套預加載 `morphTo` 關聯
+
+如果你希望加載一個 `morphTo` 關係,以及該關係可能返回的各種實體的嵌套關係,可以將 `with` 方法與 `morphTo` 關係的 `morphWith` 方法結合使用。
+
+比如我們打算預加載 image 的 book.user 的關係。
+
+```php
+
+use App\Model\Book;
+use App\Model\Image;
+use Hyperf\Database\Model\Relations\MorphTo;
+
+$images = Image::query()->with([
+ 'imageable' => function (MorphTo $morphTo) {
+ $morphTo->morphWith([
+ Book::class => ['user'],
+ ]);
+ },
+])->get();
+```
+
+對應的SQL查詢如下:
+
+```sql
+// 查詢所有圖片
+select * from `images`;
+// 查詢圖片對應的用户列表
+select * from `user` where `user`.`id` in (1, 2);
+// 查詢圖片對應的書本列表
+select * from `book` where `book`.`id` in (1, 2, 3);
+// 查詢書本列表對應的用户列表
+select * from `user` where `user`.`id` in (1, 2);
+```
+
+### 多態關聯查詢
+
+要查詢 `MorphTo` 關聯的存在,可以使用 `whereHasMorph` 方法及其相應的方法:
+
+以下示例會查詢,書本或用户 `ID` 為 1 的圖片列表。
+
+```php
+use App\Model\Book;
+use App\Model\Image;
+use App\Model\User;
+use Hyperf\Database\Model\Builder;
+
+$images = Image::query()->whereHasMorph(
+ 'imageable',
+ [
+ User::class,
+ Book::class,
+ ],
+ function (Builder $query) {
+ $query->where('imageable_id', 1);
+ }
+)->get();
+```
diff --git a/doc/zh-hk/exception-handler.md b/doc/zh-hk/exception-handler.md
index de561b033..36334a75e 100644
--- a/doc/zh-hk/exception-handler.md
+++ b/doc/zh-hk/exception-handler.md
@@ -106,6 +106,33 @@ class IndexController extends Controller
```
在上面這個例子,我們先假設 `FooException` 是存在的一個異常,以及假設已經完成了該處理器的配置,那麼當業務拋出一個沒有被捕獲處理的異常時,就會根據配置的順序依次傳遞,整一個處理流程可以理解為一個管道,若前一個異常處理器調用 `$this->stopPropagation()` 則不再往後傳遞,若最後一個配置的異常處理器仍不對該異常進行捕獲處理,那麼就會交由 Hyperf 的默認異常處理器處理了。
+## 集成 Whoops
+
+框架提供了 Whoops 集成。
+
+首先安裝 Whoops
+```php
+composer require --dev filp/whoops
+```
+
+然後配置 Whoops 專用異常處理器。
+
+```php
+// config/autoload/exceptions.php
+return [
+ 'handler' => [
+ 'http' => [
+ \Hyperf\ExceptionHandler\Handler\WhoopsExceptionHandler::class,
+ ],
+ ],
+];
+```
+
+效果如圖:
+
+![whoops](/imgs/whoops.png)
+
+
## Error 監聽器
框架提供了 `error_reporting()` 錯誤級別的監聽器 `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`。
diff --git a/doc/zh-hk/filesystem.md b/doc/zh-hk/filesystem.md
index 07c9d996a..5b9807d7d 100644
--- a/doc/zh-hk/filesystem.md
+++ b/doc/zh-hk/filesystem.md
@@ -130,7 +130,7 @@ return [
1. S3 存儲請確認安裝 `hyperf/guzzle` 組件以提供協程化支持。阿里雲、七牛雲存儲請[開啟 Curl Hook](/zh-cn/coroutine?id=swoole-runtime-hook-level)來使用協程。因 Curl Hook 的參數支持性問題,請使用 Swoole 4.4.13 以上版本。
2. minIO, ceph radosgw 等私有對象存儲方案均支持 S3 協議,可以使用 S3 適配器。
-3. 使用Local驅動時,根目錄是配置好的地址,而不是操作系統的根目錄。例如,Local驅動 `root` 設置為 `/var/www`, 則本地磁盤上的 `/var/www/public/file.txt` 通過 flysystem API 訪問時應使用 `/public/file.txt` 或 `public/file.txt` 。
+3. 使用 Local 驅動時,根目錄是配置好的地址,而不是操作系統的根目錄。例如,Local 驅動 `root` 設置為 `/var/www`, 則本地磁盤上的 `/var/www/public/file.txt` 通過 flysystem API 訪問時應使用 `/public/file.txt` 或 `public/file.txt` 。
4. 以阿里雲 OSS 為例,1 核 1 進程讀操作性能對比:
```bash
@@ -239,7 +239,7 @@ return [
'accessKey' => env('QINIU_ACCESS_KEY'),
'secretKey' => env('QINIU_SECRET_KEY'),
'bucket' => env('QINIU_BUCKET'),
- 'domain' => env('QINBIU_DOMAIN'),
+ 'domain' => env('QINIU_DOMAIN'),
],
],
];
diff --git a/doc/zh-hk/imgs/whoops.png b/doc/zh-hk/imgs/whoops.png
new file mode 100644
index 000000000..6a52b314a
Binary files /dev/null and b/doc/zh-hk/imgs/whoops.png differ
diff --git a/doc/zh-hk/nano.md b/doc/zh-hk/nano.md
new file mode 100644
index 000000000..10ae00fe4
--- /dev/null
+++ b/doc/zh-hk/nano.md
@@ -0,0 +1,260 @@
+
+通過 `hyperf/nano` 可以在無骨架、零配置的情況下快速搭建 Hyperf 應用。
+
+## 安裝
+
+```php
+composer install hyperf/nano
+```
+
+## 快速開始
+
+```php
+get('/', function () {
+
+ $user = $this->request->input('user', 'nano');
+ $method = $this->request->getMethod();
+
+ return [
+ 'message' => "hello {$user}",
+ 'method' => $method,
+ ];
+
+});
+
+$app->run();
+```
+
+啟動:
+
+```bash
+php index.php start
+```
+
+簡潔如此。
+
+## 特性
+
+* 無骨架
+* 零配置
+* 快速啟動
+* 閉包風格
+* 支持註解外的全部 Hyperf 功能
+* 兼容全部 Hyperf 組件
+* Phar 友好
+
+## 更多示例
+
+### 路由
+
+$app 集成了 Hyperf 路由器的所有方法。
+
+```php
+addGroup('/nano', function () use ($app) {
+ $app->addRoute(['GET', 'POST'], '/{id:\d+}', function($id) {
+ return '/nano/'.$id;
+ });
+ $app->put('/{name:.+}', function($name) {
+ return '/nano/'.$name;
+ });
+});
+
+$app->run();
+```
+
+### DI 容器
+```php
+getContainer()->set(Foo::class, new Foo());
+
+$app->get('/', function () {
+ /** @var ContainerProxy $this */
+ $foo = $this->get(Foo::class);
+ return $foo->bar();
+});
+
+$app->run();
+```
+> 所有 $app 管理的閉包回調中,$this 都被綁定到了 `Hyperf\Nano\ContainerProxy` 上。
+
+### 中間件
+```php
+get('/', function () {
+ return $this->request->getAttribute('key');
+});
+
+$app->addMiddleware(function ($request, $handler) {
+ $request = $request->withAttribute('key', 'value');
+ return $handler->handle($request);
+});
+
+$app->run();
+```
+
+> 除了閉包之外,所有 $app->addXXX() 方法還接受類名作為參數。可以傳入對應的 Hyperf 類。
+
+### 異常處理
+
+```php
+get('/', function () {
+ throw new \Exception();
+});
+
+$app->addExceptionHandler(function ($throwable, $response) {
+ return $response->withStatus('418')
+ ->withBody(new SwooleStream('I\'m a teapot'));
+});
+
+$app->run();
+```
+
+### 命令行
+
+```php
+addCommand('echo', function(){
+ $this->get(StdoutLoggerInterface::class)->info('A new command called echo!');
+});
+
+$app->run();
+```
+
+執行
+
+```bash
+php index.php echo
+```
+
+### 事件監聽
+
+```php
+addListener(BootApplication::class, function($event){
+ $this->get(StdoutLoggerInterface::class)->info('App started');
+});
+
+$app->run();
+```
+
+### 自定義進程
+```php
+addProcess(function(){
+ while (true) {
+ sleep(1);
+ $this->get(StdoutLoggerInterface::class)->info('Processing...');
+ }
+});
+
+$app->run();
+```
+
+### 定時任務
+
+```php
+addCrontab('* * * * * *', function(){
+ $this->get(StdoutLoggerInterface::class)->info('execute every second!');
+});
+
+$app->run();
+```
+
+### 使用 Hyperf 組件.
+
+```php
+config([
+ 'db.default' => [
+ 'host' => env('DB_HOST', 'localhost'),
+ 'port' => env('DB_PORT', 3306),
+ 'database' => env('DB_DATABASE', 'hyperf'),
+ 'username' => env('DB_USERNAME', 'root'),
+ 'password' => env('DB_PASSWORD', ''),
+ ]
+]);
+
+$app->get('/', function(){
+ return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]);
+});
+
+$app->run();
+```
diff --git a/doc/zh-hk/paginator.md b/doc/zh-hk/paginator.md
index 3b769cc77..338d630b1 100644
--- a/doc/zh-hk/paginator.md
+++ b/doc/zh-hk/paginator.md
@@ -21,6 +21,7 @@ namespace App\Controller;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Paginator\Paginator;
+use Hyperf\Utils\Collection;
/**
* @AutoController()
@@ -29,15 +30,20 @@ class UserController
{
public function index(RequestInterface $request)
{
- $currentPage = $request->input('page', 1);
- $perPage = $request->input('per_page', 2);
- $users = [
+ $currentPage = (int) $request->input('page', 1);
+ $perPage = (int) $request->input('per_page', 2);
+
+ // 這裏根據 $currentPage 和 $perPage 進行數據查詢,以下使用 Collection 代替
+ $collection = new Collection([
['id' => 1, 'name' => 'Tom'],
['id' => 2, 'name' => 'Sam'],
['id' => 3, 'name' => 'Tim'],
['id' => 4, 'name' => 'Joe'],
- ];
- return new Paginator($users, (int) $perPage, (int) $currentPage);
+ ]);
+
+ $users = array_values($collection->forPage($currentPage, $perPage)->toArray());
+
+ return new Paginator($users, $perPage, $currentPage);
}
}
```
diff --git a/doc/zh-hk/sdks/wechat.md b/doc/zh-hk/sdks/wechat.md
index 351140b0d..c5a272b09 100644
--- a/doc/zh-hk/sdks/wechat.md
+++ b/doc/zh-hk/sdks/wechat.md
@@ -49,7 +49,6 @@ AbstractProvider::setGuzzleOptions([
request->getBody()->getContents();
$app['request'] = new Request($get,$post,[],$cookie,$files,$server,$xml);
// Do something...
+
+```
+
+3. 服務器配置
+
+如果需要使用微信公眾平台的服務器配置功能,可以使用以下代碼。
+
+> 以下 `$response` 為 `Symfony\Component\HttpFoundation\Response` 並非 `Hyperf\HttpMessage\Server\Response`
+> 所以只需將 `Body` 內容直接返回,即可通過微信驗證。
+
+```php
+$response = $app->server->serve();
+
+return $response->getBody()->getContents();
```
## 如何替換緩存
@@ -92,5 +105,4 @@ use EasyWeChat\Factory;
$app = Factory::miniProgram([]);
$app['cache'] = ApplicationContext::getContainer()->get(CacheInterface::class);
-
```
diff --git a/doc/zh-hk/session.md b/doc/zh-hk/session.md
index 6da4f1e8f..c755a4417 100644
--- a/doc/zh-hk/session.md
+++ b/doc/zh-hk/session.md
@@ -14,7 +14,7 @@ Session 組件的配置儲存於 `config/autoload/session.php` 文件中,如
## 配置 Session 中間件
-在使用 Session 之前,您需要將 `Hyperf\Session\Middleware\SessionMiddleware` 中間件配置為 HTTP Server 的全局中間件,這樣組件才能介入到請求流程進行對應的處理,`config/autoload/middleware.php` 配置文件示例如下:
+在使用 Session 之前,您需要將 `Hyperf\Session\Middleware\SessionMiddleware` 中間件配置為 HTTP Server 的全局中間件,這樣組件才能介入到請求流程進行對應的處理,`config/autoload/middlewares.php` 配置文件示例如下:
```php
'socket-io',
+ 'type' => Server::SERVER_WEBSOCKET,
+ 'host' => '0.0.0.0',
+ 'port' => 9502,
+ 'sock_type' => SWOOLE_SOCK_TCP,
+ 'callbacks' => [
+ SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
+ SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
+ SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
+ ],
+ ],
+```
+
+
+## 快速開始
+
+### 服務端
+```php
+join($data);
+ // 向房間內其他用户推送(不含當前用户)
+ $socket->to($data)->emit('event', $socket->getSid() . "has joined {$data}");
+ // 向房間內所有人廣播(含當前用户)
+ $this->emit('event', 'There are ' . count($socket->getAdapter()->clients($data)) . " players in {$data}");
+ }
+
+ /**
+ * @Event("say")
+ * @param string $data
+ */
+ public function onSay(Socket $socket, $data)
+ {
+ $data = Json::decode($data);
+ $socket->to($data['room'])->emit('event', $socket->getSid() . " say: {$data['message']}");
+ }
+}
+
+```
+
+> 每個 socket 會自動加入以自己 `sid` 命名的房間(`$socket->getSid()`),發送私聊信息就推送到對應 `sid` 即可。
+
+> 框架會自動觸發 `connect` 和 `disconnect` 兩個事件。
+
+### 客户端
+
+由於服務端只實現了WebSocket通訊,所以客户端要加上 `{transports:["websocket"]}` 。
+
+```html
+
+
+```
+
+## API 清單
+
+```php
+emit('hello', 'can you hear me?', 1, 2, 'abc');
+
+ // sending to all clients except sender
+ $socket->broadcast->emit('broadcast', 'hello friends!');
+
+ // sending to all clients in 'game' room except sender
+ $socket->to('game')->emit('nice game', "let's play a game");
+
+ // sending to all clients in 'game1' and/or in 'game2' room, except sender
+ $socket->to('game1')->to('game2')->emit('nice game', "let's play a game (too)");
+
+ // WARNING: `$socket->to($socket->getSid())->emit()` will NOT work, as it will send to everyone in the room
+ // named `$socket->getSid()` but the sender. Please use the classic `$socket->emit()` instead.
+
+ // sending with acknowledgement
+ $reply = $socket->emit('question', 'do you think so?')->reply();
+
+ // sending without compression
+ $socket->compress(false)->emit('uncompressed', "that's rough");
+
+ $io = \Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\SocketIOServer\SocketIO::class);
+
+ // sending to all clients in 'game' room, including sender
+ $io->in('game')->emit('big-announcement', 'the game will start soon');
+
+ // sending to all clients in namespace 'myNamespace', including sender
+ $io->of('/myNamespace')->emit('bigger-announcement', 'the tournament will start soon');
+
+ // sending to a specific room in a specific namespace, including sender
+ $io->of('/myNamespace')->to('room')->emit('event', 'message');
+
+ // sending to individual socketid (private message)
+ $io->to('socketId')->emit('hey', 'I just met you');
+
+ // sending to all clients on this node (when using multiple nodes)
+ $io->local->emit('hi', 'my lovely babies');
+
+ // sending to all connected clients
+ $io->emit('an event sent to all connected clients');
+
+};
+```
+
+## 進階教程
+
+### 設置 Socket.io 命名空間
+
+Socket.io 通過自定義命名空間實現多路複用。(注意:不是 PHP 的命名空間)
+
+1. 可以通過 `@SocketIONamespace("/xxx")` 將控制器映射為 xxx 的命名空間,
+
+2. 也可通過
+
+```php
+ swoole 4.4.17 及以下版本只能讀取 http 創建好的Cookie,4.4.18 及以上版本可以在WebSocket握手時創建Cookie
+
+### 調整房間適配器
+
+默認的房間功能通過 Redis 適配器實現,可以適應多進程乃至分佈式場景。
+
+1. 可以替換為內存適配器,只適用於單 worker 場景。
+```php
+ \Hyperf\SocketIOServer\Room\MemoryAdapter::class,
+];
+```
+
+2. 可以替換為空適配器,不需要房間功能時可以降低消耗。
+```php
+ \Hyperf\SocketIOServer\Room\NullAdapter::class,
+];
+```
+
+### 調整 SocketID (`sid`)
+
+默認 SocketID 使用 `ServerID#FD` 的格式,可以適應分佈式場景。
+
+1. 可以替換為直接使用 Fd 。
+
+```php
+ \Hyperf\SocketIOServer\SidProvider\LocalSidProvider::class,
+];
+```
+
+2. 也可以替換為 SessionID 。
+
+```php
+ \Hyperf\SocketIOServer\SidProvider\SessionSidProvider::class,
+];
+```
+
+### 其他事件分發方法
+
+1. 可以手動註冊事件,不使用註解。
+
+```php
+on('event', [$this, 'echo']);
+ }
+
+ public function echo(Socket $socket, $data)
+ {
+ $socket->emit('event', $data);
+ }
+}
+```
+
+2. 可以在控制器上添加 `@Event()` 註解,以方法名作為事件名來分發。此時應注意其他公有方法可能會和事件名衝突。
+
+```php
+emit('event', $data);
+ }
+}
+```
diff --git a/doc/zh-hk/summary.md b/doc/zh-hk/summary.md
index f04e098ad..4c09fd3f7 100644
--- a/doc/zh-hk/summary.md
+++ b/doc/zh-hk/summary.md
@@ -52,6 +52,7 @@
* [模型緩存](zh-hk/db/model-cache.md)
* [數據庫遷移](zh-hk/db/migration.md)
* [極簡 DB 組件](zh-hk/db/db.md)
+ * [修改器](zh-hk/db/mutators.md)
* 微服務
@@ -83,6 +84,7 @@
* [ETCD 協程客户端](zh-hk/etcd.md)
* [WebSocket 服務](zh-hk/websocket-server.md)
* [WebSocket 協程客户端](zh-hk/websocket-client.md)
+ * [Socket.io 服務](zh-hk/socketio-server.md)
* [自定義進程](zh-hk/process.md)
* [開發者工具](zh-hk/devtool.md)
* [輔助類](zh-hk/utils.md)
diff --git a/doc/zh-tw/amqp.md b/doc/zh-tw/amqp.md
index 162cb96af..2b0ac71d1 100644
--- a/doc/zh-tw/amqp.md
+++ b/doc/zh-tw/amqp.md
@@ -209,3 +209,88 @@ class DemoConsumer extends ConsumerMessage
| \Hyperf\Amqp\Result::NACK | 訊息沒有被正確消費掉,以 `basic_nack` 方法來響應 |
| \Hyperf\Amqp\Result::REQUEUE | 訊息沒有被正確消費掉,以 `basic_reject` 方法來響應,並使訊息重新入列 |
| \Hyperf\Amqp\Result::DROP | 訊息沒有被正確消費掉,以 `basic_reject` 方法來響應 |
+
+## RPC 遠端過程呼叫
+
+除了典型的訊息佇列場景,我們還可以通過 AMQP 來實現 RPC 遠端過程呼叫,本元件也為這個實現提供了對應的支援。
+
+### 建立消費者
+
+RPC 使用的消費者,與典型訊息佇列場景的消費者實現基本無差,唯一的區別是需要通過呼叫 `reply` 方法返回資料給生產者。
+
+```php
+reply($data, $message);
+
+ return Result::ACK;
+ }
+}
+```
+
+### 發起 RPC 呼叫
+
+作為生成者發起一次 RPC 遠端過程呼叫也非常的簡單,只需通過依賴注入容器獲得 `Hyperf\Amqp\RpcClient` 物件並呼叫其中的 `call` 方法即可,返回的結果是消費者 reply 的資料,如下所示:
+
+```php
+get(RpcClient::class);
+// 在 DynamicRpcMessage 上設定與 Consumer 一致的 Exchange 和 RoutingKey
+$result = $rpcClient->call(new DynamicRpcMessage('hyperf', 'hyperf', ['message' => 'Hello Hyperf']));
+
+// $result:
+// array(1) {
+// ["message"]=>
+// string(18) "Reply:Hello Hyperf"
+// }
+```
+
+### 抽象 RpcMessage
+
+上面的 RPC 呼叫過程是直接通過 `Hyperf\Amqp\Message\DynamicRpcMessage` 類來完成 Exchange 和 RoutingKey 的定義,並傳遞訊息資料,在生產專案的設計上,我們可以對 RpcMessage 進行一層抽象,以統一 Exchange 和 RoutingKey 的定義。
+
+我們可以建立對應的 RpcMessage 類如 `App\Amqp\FooRpcMessage` 如下:
+
+```php
+payload = $data;
+ }
+
+}
+```
+
+這樣我們進行 RPC 呼叫時,只需直接傳遞 `FooRpcMessage` 例項到 `call` 方法即可,無需每次呼叫時都去定義 Exchange 和 RoutingKey。
\ No newline at end of file
diff --git a/doc/zh-tw/async-queue.md b/doc/zh-tw/async-queue.md
index 6aea885c0..e371f42f0 100644
--- a/doc/zh-tw/async-queue.md
+++ b/doc/zh-tw/async-queue.md
@@ -18,7 +18,7 @@ composer require hyperf/async-queue
|:----------------:|:---------:|:-------------------------------------------:|:---------------------------------------:|
| driver | string | Hyperf\AsyncQueue\Driver\RedisDriver::class | 無 |
| channel | string | queue | 佇列字首 |
-| redis.pool | string | default | redis連線池 |
+| redis.pool | string | default | redis 連線池 |
| timeout | int | 2 | pop 訊息的超時時間 |
| retry_seconds | int,array | 5 | 失敗後重新嘗試間隔 |
| handle_timeout | int | 10 | 訊息處理超時時間 |
diff --git a/doc/zh-tw/changelog.md b/doc/zh-tw/changelog.md
index bc64f63bf..5391ac81d 100644
--- a/doc/zh-tw/changelog.md
+++ b/doc/zh-tw/changelog.md
@@ -1,5 +1,59 @@
# 版本更新記錄
+# v1.1.32 - 2020-05-21
+
+## 修復
+
+- [#1734](https://github.com/hyperf/hyperf/pull/1734) 修復模型多型查詢,關聯關係為空時,也會查詢 SQL 的問題;
+- [#1739](https://github.com/hyperf/hyperf/pull/1739) 修復 `hyperf/filesystem` 元件 OSS HOOK 位運算錯誤,導致 resource 判斷不準確的問題;
+- [#1743](https://github.com/hyperf/hyperf/pull/1743) 修復 `grafana.json` 中錯誤的`refId` 欄位值;
+- [#1748](https://github.com/hyperf/hyperf/pull/1748) 修復 `hyperf/amqp` 元件在使用其他連線池時,對應的 `concurrent.limit` 配置不生效的問題;
+- [#1750](https://github.com/hyperf/hyperf/pull/1750) 修復連線池元件,在連線關閉失敗時會導致計數有誤的問題;
+- [#1754](https://github.com/hyperf/hyperf/pull/1754) 修復 BASE Server 服務,啟動提示沒有考慮 UDP 服務的情況;
+- [#1764](https://github.com/hyperf/hyperf/pull/1764) 修復當時間值為 null 時,datatime 驗證器執行失敗的 BUG;
+- [#1769](https://github.com/hyperf/hyperf/pull/1769) 修復 `hyperf/socketio-server` 元件中,客戶端初始化斷開連線操作時會報 Notice 的錯誤的問題;
+
+## 新增
+
+- [#1724](https://github.com/hyperf/hyperf/pull/1724) 新增模型方法 `Model::orWhereHasMorph` ,`Model::whereDoesntHaveMorph` and `Model::orWhereDoesntHaveMorph`;
+- [#1742](https://github.com/hyperf/hyperf/pull/1742) 新增模型 自定義型別轉換器 功能;
+ - 新增 interface `Castable`, `CastsAttributes` 和 `CastsInboundAttributes`;
+ - 新增方法 `Model\Builder::withCasts`;
+ - 新增方法 `Model::loadMorph`, `Model::loadMorphCount` 和 `Model::syncAttributes`;
+
+# v1.1.31 - 2020-05-14
+
+## 新增
+
+- [#1723](https://github.com/hyperf/hyperf/pull/1723) 異常處理器集成了 filp/whoops 。
+- [#1730](https://github.com/hyperf/hyperf/pull/1730) 為命令 `gen:model` 可選項 `--refresh-fillable` 新增簡寫 `-R`。
+
+## 修復
+
+- [#1696](https://github.com/hyperf/hyperf/pull/1696) 修復方法 `Context::copy` 傳入欄位 `keys` 後無法正常使用的BUG。
+- [#1708](https://github.com/hyperf/hyperf/pull/1708) [#1718](https://github.com/hyperf/hyperf/pull/1718) 修復 `hyperf/socketio-server` 元件記憶體溢位等BUG。
+
+## 優化
+
+- [#1710](https://github.com/hyperf/hyperf/pull/1710) MAC系統下不再使用 `cli_set_process_title` 方法設定程序名。
+
+# v1.1.30 - 2020-05-07
+
+## 新增
+
+- [#1616](https://github.com/hyperf/hyperf/pull/1616) 新增 ORM 方法 `morphWith` 和 `whereHasMorph`。
+- [#1651](https://github.com/hyperf/hyperf/pull/1651) 新增 `socket.io-server` 元件。
+- [#1666](https://github.com/hyperf/hyperf/pull/1666) [#1669](https://github.com/hyperf/hyperf/pull/1669) 新增 AMQP RPC 客戶端。
+
+## 修復
+
+- [#1682](https://github.com/hyperf/hyperf/pull/1682) 修復 `RpcPoolTransporter` 的連線池配置不生效的 BUG。
+- [#1683](https://github.com/hyperf/hyperf/pull/1683) 修復 `RpcConnection` 連線失敗後,相同協程內無法正常重置連線的 BUG。
+
+## 優化
+
+- [#1670](https://github.com/hyperf/hyperf/pull/1670) 優化掉 `Cache 元件` 一條無意義的刪除指令。
+
# v1.1.28 - 2020-04-30
## 新增
@@ -26,16 +80,16 @@
## 新增
- [#1575](https://github.com/hyperf/hyperf/pull/1575) 為指令碼 `gen:model` 生成的模型,自動新增 `relation` `scope` 和 `attributes` 的變數註釋。
-- [#1586](https://github.com/hyperf/hyperf/pull/1586) 新增 `symfony/event-dispatcher` 元件小於 `4.3` 時的 `conflict` 配置。用於解決使用者使用了 `4.3` 以下版本時,導致 `SymfonyDispatcher` 實現衝突的BUG。
+- [#1586](https://github.com/hyperf/hyperf/pull/1586) 新增 `symfony/event-dispatcher` 元件小於 `4.3` 時的 `conflict` 配置。用於解決使用者使用了 `4.3` 以下版本時,導致 `SymfonyDispatcher` 實現衝突的 BUG。
- [#1597](https://github.com/hyperf/hyperf/pull/1597) 為 `AMQP` 消費者,新增最大消費次數 `maxConsumption`。
- [#1603](https://github.com/hyperf/hyperf/pull/1603) 為 `WebSocket` 服務新增基於 `fd` 儲存的 `Context`。
## 修復
-- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修復 `jsonrpc` 服務,釋出了相同名字不同協議到 `consul` 後,客戶端無法正常工作的BUG。
-- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修復了檔案鎖在協程下可能會造成死鎖的BUG。
-- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修復了重寫後的 `go` 方法,返回值與 `swoole` 原生方法不符的BUG。
-- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修復當路由 `Handler` 是匿名函式時,指令碼 `describe:routes` 執行失敗的BUG。
+- [#1553](https://github.com/hyperf/hyperf/pull/1553) 修復 `jsonrpc` 服務,釋出了相同名字不同協議到 `consul` 後,客戶端無法正常工作的 BUG。
+- [#1589](https://github.com/hyperf/hyperf/pull/1589) 修復了檔案鎖在協程下可能會造成死鎖的 BUG。
+- [#1607](https://github.com/hyperf/hyperf/pull/1607) 修復了重寫後的 `go` 方法,返回值與 `swoole` 原生方法不符的 BUG。
+- [#1624](https://github.com/hyperf/hyperf/pull/1624) 修復當路由 `Handler` 是匿名函式時,指令碼 `describe:routes` 執行失敗的 BUG。
# v1.1.26 - 2020-04-16
@@ -47,9 +101,9 @@
- [#1563](https://github.com/hyperf/hyperf/pull/1563) 修復服務關停後,定時器的 `onOneServer` 配置不會被重置。
- [#1565](https://github.com/hyperf/hyperf/pull/1565) 當 `DB` 元件重連 `Mysql` 時,重置事務等級為 0。
-- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修復 `Hyperf\GrpcServer\CoreMiddleware` 中,自定義類的父類找不到時報錯的BUG。
-- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修復 `describe:routes` 指令碼 `server` 配置不生效的BUG。
-- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修復 `migrate:refresh` 指令碼 `step` 引數不為 `int` 時會報錯的BUG。
+- [#1572](https://github.com/hyperf/hyperf/pull/1572) 修復 `Hyperf\GrpcServer\CoreMiddleware` 中,自定義類的父類找不到時報錯的 BUG。
+- [#1577](https://github.com/hyperf/hyperf/pull/1577) 修復 `describe:routes` 指令碼 `server` 配置不生效的 BUG。
+- [#1579](https://github.com/hyperf/hyperf/pull/1579) 修復 `migrate:refresh` 指令碼 `step` 引數不為 `int` 時會報錯的 BUG。
## 變更
diff --git a/doc/zh-tw/db/mutators.md b/doc/zh-tw/db/mutators.md
new file mode 100644
index 000000000..56304a9ba
--- /dev/null
+++ b/doc/zh-tw/db/mutators.md
@@ -0,0 +1,555 @@
+# 修改器
+
+> 本文件大量借鑑於 [LearnKu](https://learnku.com) 十分感謝 LearnKu 對 PHP 社群做出的貢獻。
+
+當你在模型例項中獲取或設定某些屬性值的時候,訪問器和修改器允許你對模型屬性值進行格式化。
+
+## 訪問器 & 修改器
+
+### 定義一個訪問器
+
+若要定義一個訪問器, 則需在模型上建立一個 `getFooAttribute` 方法,要訪問的 `Foo` 欄位需使用「駝峰式」命名。 在這個示例中,我們將為 `first_name` 屬性定義一個訪問器。當模型嘗試獲取 `first_name` 屬性時,將自動呼叫此訪問器:
+
+```php
+first_name;
+```
+
+當然,你也可以通過已有的屬性值,使用訪問器返回新的計算值:
+
+```php
+namespace App;
+
+use Hyperf\DbConnection\Model\Model;
+
+class User extends Model
+{
+ /**
+ * 獲取使用者的姓名.
+ *
+ * @return string
+ */
+ public function getFullNameAttribute()
+ {
+ return "{$this->first_name} {$this->last_name}";
+ }
+}
+```
+
+### 定義一個修改器
+
+若要定義一個修改器,則需在模型上面定義 `setFooAttribute` 方法。要訪問的 `Foo` 欄位使用「駝峰式」命名。讓我們再來定義一個 `first_name` 屬性的修改器。當我們嘗試在模式上在設定 `first_name` 屬性值時,該修改器將被自動呼叫:
+
+```php
+attributes['first_name'] = strtolower($value);
+ }
+}
+```
+
+修改器會獲取屬性已經被設定的值,允許你修改並且將其值設定到模型內部的 `$attributes` 屬性上。舉個例子,如果我們嘗試將 `first_name` 屬性的值設定為 `Sally`:
+
+```php
+$user = App\User::find(1);
+
+$user->first_name = 'Sally';
+```
+
+在這個例子中,`setFirstNameAttribute` 方法在呼叫的時候接受 `Sally` 這個值作為引數。接著修改器會應用 `strtolower` 函式並將處理的結果設定到內部的 `$attributes` 陣列。
+
+## 日期轉化器
+
+預設情況下,模型會將 `created_at` 和 `updated_at` 欄位轉換為 `Carbon` 例項,它繼承了 `PHP` 原生的 `DateTime` 類並提供了各種有用的方法。你可以通過設定模型的 `$dates` 屬性來新增其他日期屬性:
+
+```php
+ Tip: 你可以通過將模型的公有屬性 $timestamps 值設定為 false 來禁用預設的 created_at 和 updated_at 時間戳。
+
+當某個欄位是日期格式時,你可以將值設定為一個 `UNIX` 時間戳,日期時間 `(Y-m-d)` 字串,或者 `DateTime` / `Carbon` 例項。日期值會被正確格式化並儲存到你的資料庫中:
+
+就如上面所說,當獲取到的屬性包含在 `$dates` 屬性中時,都會自動轉換為 `Carbon` 例項,允許你在屬性上使用任意的 `Carbon` 方法:
+
+```php
+$user = App\User::find(1);
+
+return $user->deleted_at->getTimestamp();
+```
+
+### 時間格式
+
+時間戳都將以 `Y-m-d H:i:s` 形式格式化。如果你需要自定義時間戳格式,可在模型中設定 `$dateFormat` 屬性。這個屬性決定了日期屬性將以何種形式儲存在資料庫中,以及當模型序列化成陣列或 `JSON` 時的格式:
+
+```php
+`, `string`, `boolean`, `object`, `array`, `collection`, `date`, `datetime` 和 `timestamp`。 當需要轉換為 `decimal` 型別時,你需要定義小數位的個數,如: `decimal:2`。
+
+示例, 讓我們把以整數( `0` 或 `1` )形式儲存在資料庫中的 `is_admin` 屬性轉成布林值:
+
+```php
+ 'boolean',
+ ];
+}
+```
+
+現在當你訪問 `is_admin` 屬性時,雖然儲存在資料庫裡的值是一個整數型別,但是返回值總是會被轉換成布林值型別:
+
+```php
+$user = App\User::find(1);
+
+if ($user->is_admin) {
+ //
+}
+```
+
+### 自定義型別轉換
+
+模型內建了多種常用的型別轉換。但是,使用者偶爾會需要將資料轉換成自定義型別。現在,該需求可以通過定義一個實現 `CastsAttributes` 介面的類來完成
+
+實現了該介面的類必須事先定義一個 `get` 和 `set` 方法。 `get` 方法負責將從資料庫中獲取的原始資料轉換成對應的型別,而 `set` 方法則是將資料轉換成對應的資料庫型別以便存入資料庫中。舉個例子,下面我們將內建的 `json` 型別轉換以自定義型別轉換的形式重新實現一遍:
+
+```php
+ Json::class,
+ ];
+}
+```
+
+#### 值物件型別轉換
+
+你不僅可以將資料轉換成原生的資料型別,還可以將資料轉換成物件。兩種自定義型別轉換的定義方式非常類似。但是將資料轉換成物件的自定義轉換類中的 `set` 方法需要返回鍵值對陣列,用於設定原始、可儲存的值到對應的模型中。
+
+舉個例子,定義一個自定義型別轉換類用於將多個模型屬性值轉換成單個 `Address` 值物件,假設 `Address` 物件有兩個公有屬性 `lineOne` 和 `lineTwo`:
+
+```php
+ $value->lineOne,
+ 'address_line_two' => $value->lineTwo,
+ ];
+ }
+}
+```
+
+進行值物件型別轉換後,任何對值物件的資料變更將會自動在模型儲存前同步回模型當中:
+
+```php
+address->lineOne = 'Updated Address Value';
+$user->address->lineTwo = '#10000';
+
+$user->save();
+
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Updated Address Value',
+// 'address_line_two' => '#10000'
+//];
+```
+
+**這裡的實現與 Laravel 不同,如果出現以下用法,請需要格外注意**
+
+```php
+$user = App\User::find(1);
+
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Address Value',
+// 'address_line_two' => '#10000'
+//];
+
+$user->address->lineOne = 'Updated Address Value';
+$user->address->lineTwo = '#20000';
+
+// 直接修改 address 的欄位後,是無法立馬再 attributes 中生效的,但可以直接通過 $user->address 拿到修改後的資料。
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Address Value',
+// 'address_line_two' => '#10000'
+//];
+
+// 當我們儲存資料或者刪除資料後,attributes 便會改成修改後的資料。
+$user->save();
+var_dump($user->getAttributes());
+//[
+// 'address_line_one' => 'Updated Address Value',
+// 'address_line_two' => '#20000'
+//];
+```
+
+如果修改 `address` 後,不想要儲存,也不想通過 `address->lineOne` 獲取 `address_line_one` 的資料,還可以使用以下 方法
+
+```php
+$user = App\User::find(1);
+$user->address->lineOne = 'Updated Address Value';
+$user->syncAttributes();
+var_dump($user->getAttributes());
+```
+
+當然,如果您仍然需要修改對應的 `value` 後,同步修改 `attributes` 的功能,可以嘗試使用以下方式。首先,我們實現一個 `UserInfo` 並繼承 `CastsValue`。
+
+```php
+namespace App\Caster;
+
+use Hyperf\Database\Model\CastsValue;
+
+/**
+ * @property string $name
+ * @property int $gender
+ */
+class UserInfo extends CastsValue
+{
+}
+```
+
+然後實現對應的 `UserInfoCaster`
+
+```php
+ $value->name,
+ 'gender' => $value->gender,
+ ];
+ }
+}
+
+```
+
+當我們再使用以下方式修改 UserInfo 時,便可以同步修改到 attributes 的資料。
+
+```php
+/** @var User $user */
+$user = User::query()->find(100);
+$user->userInfo->name = 'John1';
+var_dump($user->getAttributes()); // ['name' => 'John1']
+```
+
+#### 入站型別轉換
+
+有時候,你可能只需要對寫入模型的屬性值進行型別轉換而不需要對從模型中獲取的屬性值進行任何處理。一個典型入站型別轉換的例子就是「hashing」。入站型別轉換類需要實現 `CastsInboundAttributes` 介面,只需要實現 `set` 方法。
+
+```php
+algorithm = $algorithm;
+ }
+
+ /**
+ * 轉換成將要進行儲存的值
+ */
+ public function set($model, $key, $value, $attributes)
+ {
+ return hash($this->algorithm, $value);
+ }
+}
+```
+
+#### 型別轉換引數
+
+當將自定義型別轉換附加到模型時,可以指定傳入的型別轉換引數。傳入型別轉換引數需使用 `:` 將引數與類名分隔,多個引數之間使用逗號分隔。這些引數將會傳遞到型別轉換類的建構函式中:
+
+```php
+ Hash::class.':sha256',
+ ];
+}
+```
+
+### 陣列 & `JSON` 轉換
+
+當你在資料庫儲存序列化的 `JSON` 的資料時,`array` 型別的轉換非常有用。比如:如果你的資料庫具有被序列化為 `JSON` 的 `JSON` 或 `TEXT` 欄位型別,並且在模型中加入了 `array` 型別轉換,那麼當你訪問的時候就會自動被轉換為 `PHP` 陣列:
+
+```php
+ 'array',
+ ];
+}
+```
+
+一旦定義了轉換,你訪問 `options` 屬性時他會自動從 `JSON` 型別反序列化為 `PHP` 陣列。當你設定了 `options` 屬性的值時,給定的陣列也會自動序列化為 `JSON` 型別儲存:
+
+```php
+$user = App\User::find(1);
+
+$options = $user->options;
+
+$options['key'] = 'value';
+
+$user->options = $options;
+
+$user->save();
+```
+
+### Date 型別轉換
+
+當使用 `date` 或 `datetime` 屬性時,可以指定日期的格式。 這種格式會被用在模型序列化為陣列或者 `JSON`:
+
+```php
+ 'datetime:Y-m-d',
+ ];
+}
+```
+
+### 查詢時型別轉換
+
+有時候需要在查詢執行過程中對特定屬性進行型別轉換,例如需要從資料庫表中獲取資料的時候。舉個例子,請參考以下查詢:
+
+```php
+use App\Post;
+use App\User;
+
+$users = User::select([
+ 'users.*',
+ 'last_posted_at' => Post::selectRaw('MAX(created_at)')
+ ->whereColumn('user_id', 'users.id')
+])->get();
+```
+
+在該查詢獲取到的結果集中,`last_posted_at` 屬性將會是一個字串。假如我們在執行查詢時進行 `date` 型別轉換將更方便。你可以通過使用 `withCasts` 方法來完成上述操作:
+
+```php
+$users = User::select([
+ 'users.*',
+ 'last_posted_at' => Post::selectRaw('MAX(created_at)')
+ ->whereColumn('user_id', 'users.id')
+])->withCasts([
+ 'last_posted_at' => 'date'
+])->get();
+```
+
diff --git a/doc/zh-tw/db/relationship.md b/doc/zh-tw/db/relationship.md
index 3dd96b5f9..20920b56e 100644
--- a/doc/zh-tw/db/relationship.md
+++ b/doc/zh-tw/db/relationship.md
@@ -212,7 +212,6 @@ return $this->belongsToMany('App\Role')->wherePivot('approved', 1);
return $this->belongsToMany('App\Role')->wherePivotIn('priority', [1, 2]);
```
-
## 預載入
當以屬性方式訪問 `Hyperf` 關聯時,關聯資料「懶載入」。這著直到第一次訪問屬性時關聯資料才會被真實載入。不過 `Hyperf` 能在查詢父模型時「預先載入」子關聯。預載入可以緩解 N + 1 查詢問題。為了說明 N + 1 查詢問題,考慮 `User` 模型關聯到 `Role` 的情形:
@@ -264,3 +263,249 @@ SELECT * FROM `user`;
SELECT * FROM `role` WHERE id in (1, 2, 3, ...);
```
+
+## 多型關聯
+
+多型關聯允許目標模型藉助關聯關係,關聯多個模型。
+
+### 一對一(多型)
+
+#### 表結構
+
+一對一多型關聯與簡單的一對一關聯類似;不過,目標模型能夠在一個關聯上從屬於多個模型。
+例如,Book 和 User 可能共享一個關聯到 Image 模型的關係。使用一對一多型關聯允許使用一個唯一圖片列表同時用於 Book 和 User。讓我們先看看錶結構:
+
+```
+book
+ id - integer
+ title - string
+
+user
+ id - integer
+ name - string
+
+image
+ id - integer
+ url - string
+ imageable_id - integer
+ imageable_type - string
+```
+
+image 表中的 imageable_id 欄位會根據 imageable_type 的不同代表不同含義,預設情況下,imageable_type 直接是相關模型類名。
+
+#### 模型示例
+
+```php
+morphTo();
+ }
+}
+
+class Book extends Model
+{
+ public function image()
+ {
+ return $this->morphOne(Image::class, 'imageable');
+ }
+}
+
+class User extends Model
+{
+ public function image()
+ {
+ return $this->morphOne(Image::class, 'imageable');
+ }
+}
+```
+
+#### 獲取關聯
+
+按照上述定義模型後,我們就可以通過模型關係獲取對應的模型。
+
+比如,我們獲取某使用者的圖片。
+
+```php
+use App\Model\User;
+
+$user = User::find(1);
+
+$image = $user->image;
+```
+
+或者我們獲取某個圖片對應使用者或書本。`imageable` 會根據 `imageable_type` 獲取對應的 `User` 或者 `Book`。
+
+```php
+use App\Model\Image;
+
+$image = Image::find(1);
+
+$imageable = $image->imageable;
+```
+
+### 一對多(多型)
+
+#### 模型示例
+
+```php
+morphTo();
+ }
+}
+
+class Book extends Model
+{
+ public function images()
+ {
+ return $this->morphMany(Image::class, 'imageable');
+ }
+}
+
+class User extends Model
+{
+ public function images()
+ {
+ return $this->morphMany(Image::class, 'imageable');
+ }
+}
+```
+
+#### 獲取關聯
+
+獲取使用者所有的圖片
+
+```php
+use App\Model\User;
+
+$user = User::query()->find(1);
+foreach ($user->images as $image) {
+ // ...
+}
+```
+
+### 自定義多型對映
+
+預設情況下,框架要求 `type` 必須儲存對應模型類名,比如上述 `imageable_type` 必須是對應的 `User::class` 和 `Book::class`,但顯然在實際應用中,這是十分不方便的。所以我們可以自定義對映關係,來解耦資料庫與應用內部結構。
+
+```php
+use App\Model;
+use Hyperf\Database\Model\Relations\Relation;
+Relation::morphMap([
+ 'user' => Model\User::class,
+ 'book' => Model\Book::class,
+]);
+```
+
+因為 `Relation::morphMap` 修改後會常駐記憶體,所以我們可以在專案啟動時,就建立好對應的關係對映。我們可以建立以下監聽器:
+
+```php
+ Model\User::class,
+ 'book' => Model\Book::class,
+ ]);
+ }
+}
+
+```
+
+### 巢狀預載入 `morphTo` 關聯
+
+如果你希望載入一個 `morphTo` 關係,以及該關係可能返回的各種實體的巢狀關係,可以將 `with` 方法與 `morphTo` 關係的 `morphWith` 方法結合使用。
+
+比如我們打算預載入 image 的 book.user 的關係。
+
+```php
+
+use App\Model\Book;
+use App\Model\Image;
+use Hyperf\Database\Model\Relations\MorphTo;
+
+$images = Image::query()->with([
+ 'imageable' => function (MorphTo $morphTo) {
+ $morphTo->morphWith([
+ Book::class => ['user'],
+ ]);
+ },
+])->get();
+```
+
+對應的SQL查詢如下:
+
+```sql
+// 查詢所有圖片
+select * from `images`;
+// 查詢圖片對應的使用者列表
+select * from `user` where `user`.`id` in (1, 2);
+// 查詢圖片對應的書本列表
+select * from `book` where `book`.`id` in (1, 2, 3);
+// 查詢書本列表對應的使用者列表
+select * from `user` where `user`.`id` in (1, 2);
+```
+
+### 多型關聯查詢
+
+要查詢 `MorphTo` 關聯的存在,可以使用 `whereHasMorph` 方法及其相應的方法:
+
+以下示例會查詢,書本或使用者 `ID` 為 1 的圖片列表。
+
+```php
+use App\Model\Book;
+use App\Model\Image;
+use App\Model\User;
+use Hyperf\Database\Model\Builder;
+
+$images = Image::query()->whereHasMorph(
+ 'imageable',
+ [
+ User::class,
+ Book::class,
+ ],
+ function (Builder $query) {
+ $query->where('imageable_id', 1);
+ }
+)->get();
+```
diff --git a/doc/zh-tw/exception-handler.md b/doc/zh-tw/exception-handler.md
index 645079687..6f846adb9 100644
--- a/doc/zh-tw/exception-handler.md
+++ b/doc/zh-tw/exception-handler.md
@@ -106,6 +106,33 @@ class IndexController extends Controller
```
在上面這個例子,我們先假設 `FooException` 是存在的一個異常,以及假設已經完成了該處理器的配置,那麼當業務丟擲一個沒有被捕獲處理的異常時,就會根據配置的順序依次傳遞,整一個處理流程可以理解為一個管道,若前一個異常處理器呼叫 `$this->stopPropagation()` 則不再往後傳遞,若最後一個配置的異常處理器仍不對該異常進行捕獲處理,那麼就會交由 Hyperf 的預設異常處理器處理了。
+## 整合 Whoops
+
+框架提供了 Whoops 整合。
+
+首先安裝 Whoops
+```php
+composer require --dev filp/whoops
+```
+
+然後配置 Whoops 專用異常處理器。
+
+```php
+// config/autoload/exceptions.php
+return [
+ 'handler' => [
+ 'http' => [
+ \Hyperf\ExceptionHandler\Handler\WhoopsExceptionHandler::class,
+ ],
+ ],
+];
+```
+
+效果如圖:
+
+![whoops](/imgs/whoops.png)
+
+
## Error 監聽器
框架提供了 `error_reporting()` 錯誤級別的監聽器 `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`。
diff --git a/doc/zh-tw/filesystem.md b/doc/zh-tw/filesystem.md
index 9647f6ced..d0c2be201 100644
--- a/doc/zh-tw/filesystem.md
+++ b/doc/zh-tw/filesystem.md
@@ -130,7 +130,7 @@ return [
1. S3 儲存請確認安裝 `hyperf/guzzle` 元件以提供協程化支援。阿里雲、七牛雲端儲存請[開啟 Curl Hook](/zh-cn/coroutine?id=swoole-runtime-hook-level)來使用協程。因 Curl Hook 的引數支援性問題,請使用 Swoole 4.4.13 以上版本。
2. minIO, ceph radosgw 等私有物件儲存方案均支援 S3 協議,可以使用 S3 介面卡。
-3. 使用Local驅動時,根目錄是配置好的地址,而不是作業系統的根目錄。例如,Local驅動 `root` 設定為 `/var/www`, 則本地磁碟上的 `/var/www/public/file.txt` 通過 flysystem API 訪問時應使用 `/public/file.txt` 或 `public/file.txt` 。
+3. 使用 Local 驅動時,根目錄是配置好的地址,而不是作業系統的根目錄。例如,Local 驅動 `root` 設定為 `/var/www`, 則本地磁碟上的 `/var/www/public/file.txt` 通過 flysystem API 訪問時應使用 `/public/file.txt` 或 `public/file.txt` 。
4. 以阿里雲 OSS 為例,1 核 1 程序讀操作效能對比:
```bash
@@ -239,7 +239,7 @@ return [
'accessKey' => env('QINIU_ACCESS_KEY'),
'secretKey' => env('QINIU_SECRET_KEY'),
'bucket' => env('QINIU_BUCKET'),
- 'domain' => env('QINBIU_DOMAIN'),
+ 'domain' => env('QINIU_DOMAIN'),
],
],
];
diff --git a/doc/zh-tw/imgs/whoops.png b/doc/zh-tw/imgs/whoops.png
new file mode 100644
index 000000000..6a52b314a
Binary files /dev/null and b/doc/zh-tw/imgs/whoops.png differ
diff --git a/doc/zh-tw/nano.md b/doc/zh-tw/nano.md
new file mode 100644
index 000000000..e79eaade5
--- /dev/null
+++ b/doc/zh-tw/nano.md
@@ -0,0 +1,260 @@
+
+通過 `hyperf/nano` 可以在無骨架、零配置的情況下快速搭建 Hyperf 應用。
+
+## 安裝
+
+```php
+composer install hyperf/nano
+```
+
+## 快速開始
+
+```php
+get('/', function () {
+
+ $user = $this->request->input('user', 'nano');
+ $method = $this->request->getMethod();
+
+ return [
+ 'message' => "hello {$user}",
+ 'method' => $method,
+ ];
+
+});
+
+$app->run();
+```
+
+啟動:
+
+```bash
+php index.php start
+```
+
+簡潔如此。
+
+## 特性
+
+* 無骨架
+* 零配置
+* 快速啟動
+* 閉包風格
+* 支援註解外的全部 Hyperf 功能
+* 相容全部 Hyperf 元件
+* Phar 友好
+
+## 更多示例
+
+### 路由
+
+$app 集成了 Hyperf 路由器的所有方法。
+
+```php
+addGroup('/nano', function () use ($app) {
+ $app->addRoute(['GET', 'POST'], '/{id:\d+}', function($id) {
+ return '/nano/'.$id;
+ });
+ $app->put('/{name:.+}', function($name) {
+ return '/nano/'.$name;
+ });
+});
+
+$app->run();
+```
+
+### DI 容器
+```php
+getContainer()->set(Foo::class, new Foo());
+
+$app->get('/', function () {
+ /** @var ContainerProxy $this */
+ $foo = $this->get(Foo::class);
+ return $foo->bar();
+});
+
+$app->run();
+```
+> 所有 $app 管理的閉包回撥中,$this 都被繫結到了 `Hyperf\Nano\ContainerProxy` 上。
+
+### 中介軟體
+```php
+get('/', function () {
+ return $this->request->getAttribute('key');
+});
+
+$app->addMiddleware(function ($request, $handler) {
+ $request = $request->withAttribute('key', 'value');
+ return $handler->handle($request);
+});
+
+$app->run();
+```
+
+> 除了閉包之外,所有 $app->addXXX() 方法還接受類名作為引數。可以傳入對應的 Hyperf 類。
+
+### 異常處理
+
+```php
+get('/', function () {
+ throw new \Exception();
+});
+
+$app->addExceptionHandler(function ($throwable, $response) {
+ return $response->withStatus('418')
+ ->withBody(new SwooleStream('I\'m a teapot'));
+});
+
+$app->run();
+```
+
+### 命令列
+
+```php
+addCommand('echo', function(){
+ $this->get(StdoutLoggerInterface::class)->info('A new command called echo!');
+});
+
+$app->run();
+```
+
+執行
+
+```bash
+php index.php echo
+```
+
+### 事件監聽
+
+```php
+addListener(BootApplication::class, function($event){
+ $this->get(StdoutLoggerInterface::class)->info('App started');
+});
+
+$app->run();
+```
+
+### 自定義程序
+```php
+addProcess(function(){
+ while (true) {
+ sleep(1);
+ $this->get(StdoutLoggerInterface::class)->info('Processing...');
+ }
+});
+
+$app->run();
+```
+
+### 定時任務
+
+```php
+addCrontab('* * * * * *', function(){
+ $this->get(StdoutLoggerInterface::class)->info('execute every second!');
+});
+
+$app->run();
+```
+
+### 使用 Hyperf 元件.
+
+```php
+config([
+ 'db.default' => [
+ 'host' => env('DB_HOST', 'localhost'),
+ 'port' => env('DB_PORT', 3306),
+ 'database' => env('DB_DATABASE', 'hyperf'),
+ 'username' => env('DB_USERNAME', 'root'),
+ 'password' => env('DB_PASSWORD', ''),
+ ]
+]);
+
+$app->get('/', function(){
+ return DB::query('SELECT * FROM `user` WHERE gender = ?;', [1]);
+});
+
+$app->run();
+```
diff --git a/doc/zh-tw/paginator.md b/doc/zh-tw/paginator.md
index 663c87f1e..2e14409df 100644
--- a/doc/zh-tw/paginator.md
+++ b/doc/zh-tw/paginator.md
@@ -21,6 +21,7 @@ namespace App\Controller;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\Paginator\Paginator;
+use Hyperf\Utils\Collection;
/**
* @AutoController()
@@ -29,15 +30,20 @@ class UserController
{
public function index(RequestInterface $request)
{
- $currentPage = $request->input('page', 1);
- $perPage = $request->input('per_page', 2);
- $users = [
+ $currentPage = (int) $request->input('page', 1);
+ $perPage = (int) $request->input('per_page', 2);
+
+ // 這裡根據 $currentPage 和 $perPage 進行資料查詢,以下使用 Collection 代替
+ $collection = new Collection([
['id' => 1, 'name' => 'Tom'],
['id' => 2, 'name' => 'Sam'],
['id' => 3, 'name' => 'Tim'],
['id' => 4, 'name' => 'Joe'],
- ];
- return new Paginator($users, (int) $perPage, (int) $currentPage);
+ ]);
+
+ $users = array_values($collection->forPage($currentPage, $perPage)->toArray());
+
+ return new Paginator($users, $perPage, $currentPage);
}
}
```
diff --git a/doc/zh-tw/sdks/wechat.md b/doc/zh-tw/sdks/wechat.md
index 26f33feab..5b85e24c4 100644
--- a/doc/zh-tw/sdks/wechat.md
+++ b/doc/zh-tw/sdks/wechat.md
@@ -49,7 +49,6 @@ AbstractProvider::setGuzzleOptions([
request->getBody()->getContents();
$app['request'] = new Request($get,$post,[],$cookie,$files,$server,$xml);
// Do something...
+
+```
+
+3. 伺服器配置
+
+如果需要使用微信公眾平臺的伺服器配置功能,可以使用以下程式碼。
+
+> 以下 `$response` 為 `Symfony\Component\HttpFoundation\Response` 並非 `Hyperf\HttpMessage\Server\Response`
+> 所以只需將 `Body` 內容直接返回,即可通過微信驗證。
+
+```php
+$response = $app->server->serve();
+
+return $response->getBody()->getContents();
```
## 如何替換快取
@@ -92,5 +105,4 @@ use EasyWeChat\Factory;
$app = Factory::miniProgram([]);
$app['cache'] = ApplicationContext::getContainer()->get(CacheInterface::class);
-
```
diff --git a/doc/zh-tw/session.md b/doc/zh-tw/session.md
index 63702a2ce..4841221d1 100644
--- a/doc/zh-tw/session.md
+++ b/doc/zh-tw/session.md
@@ -14,7 +14,7 @@ Session 元件的配置儲存於 `config/autoload/session.php` 檔案中,如
## 配置 Session 中介軟體
-在使用 Session 之前,您需要將 `Hyperf\Session\Middleware\SessionMiddleware` 中介軟體配置為 HTTP Server 的全域性中介軟體,這樣元件才能介入到請求流程進行對應的處理,`config/autoload/middleware.php` 配置檔案示例如下:
+在使用 Session 之前,您需要將 `Hyperf\Session\Middleware\SessionMiddleware` 中介軟體配置為 HTTP Server 的全域性中介軟體,這樣元件才能介入到請求流程進行對應的處理,`config/autoload/middlewares.php` 配置檔案示例如下:
```php
'socket-io',
+ 'type' => Server::SERVER_WEBSOCKET,
+ 'host' => '0.0.0.0',
+ 'port' => 9502,
+ 'sock_type' => SWOOLE_SOCK_TCP,
+ 'callbacks' => [
+ SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
+ SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
+ SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
+ ],
+ ],
+```
+
+
+## 快速開始
+
+### 服務端
+```php
+join($data);
+ // 向房間內其他使用者推送(不含當前使用者)
+ $socket->to($data)->emit('event', $socket->getSid() . "has joined {$data}");
+ // 向房間內所有人廣播(含當前使用者)
+ $this->emit('event', 'There are ' . count($socket->getAdapter()->clients($data)) . " players in {$data}");
+ }
+
+ /**
+ * @Event("say")
+ * @param string $data
+ */
+ public function onSay(Socket $socket, $data)
+ {
+ $data = Json::decode($data);
+ $socket->to($data['room'])->emit('event', $socket->getSid() . " say: {$data['message']}");
+ }
+}
+
+```
+
+> 每個 socket 會自動加入以自己 `sid` 命名的房間(`$socket->getSid()`),傳送私聊資訊就推送到對應 `sid` 即可。
+
+> 框架會自動觸發 `connect` 和 `disconnect` 兩個事件。
+
+### 客戶端
+
+由於服務端只實現了WebSocket通訊,所以客戶端要加上 `{transports:["websocket"]}` 。
+
+```html
+
+
+```
+
+## API 清單
+
+```php
+emit('hello', 'can you hear me?', 1, 2, 'abc');
+
+ // sending to all clients except sender
+ $socket->broadcast->emit('broadcast', 'hello friends!');
+
+ // sending to all clients in 'game' room except sender
+ $socket->to('game')->emit('nice game', "let's play a game");
+
+ // sending to all clients in 'game1' and/or in 'game2' room, except sender
+ $socket->to('game1')->to('game2')->emit('nice game', "let's play a game (too)");
+
+ // WARNING: `$socket->to($socket->getSid())->emit()` will NOT work, as it will send to everyone in the room
+ // named `$socket->getSid()` but the sender. Please use the classic `$socket->emit()` instead.
+
+ // sending with acknowledgement
+ $reply = $socket->emit('question', 'do you think so?')->reply();
+
+ // sending without compression
+ $socket->compress(false)->emit('uncompressed', "that's rough");
+
+ $io = \Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\SocketIOServer\SocketIO::class);
+
+ // sending to all clients in 'game' room, including sender
+ $io->in('game')->emit('big-announcement', 'the game will start soon');
+
+ // sending to all clients in namespace 'myNamespace', including sender
+ $io->of('/myNamespace')->emit('bigger-announcement', 'the tournament will start soon');
+
+ // sending to a specific room in a specific namespace, including sender
+ $io->of('/myNamespace')->to('room')->emit('event', 'message');
+
+ // sending to individual socketid (private message)
+ $io->to('socketId')->emit('hey', 'I just met you');
+
+ // sending to all clients on this node (when using multiple nodes)
+ $io->local->emit('hi', 'my lovely babies');
+
+ // sending to all connected clients
+ $io->emit('an event sent to all connected clients');
+
+};
+```
+
+## 進階教程
+
+### 設定 Socket.io 名稱空間
+
+Socket.io 通過自定義名稱空間實現多路複用。(注意:不是 PHP 的名稱空間)
+
+1. 可以通過 `@SocketIONamespace("/xxx")` 將控制器對映為 xxx 的名稱空間,
+
+2. 也可通過
+
+```php
+ swoole 4.4.17 及以下版本只能讀取 http 建立好的Cookie,4.4.18 及以上版本可以在WebSocket握手時建立Cookie
+
+### 調整房間介面卡
+
+預設的房間功能通過 Redis 介面卡實現,可以適應多程序乃至分散式場景。
+
+1. 可以替換為記憶體介面卡,只適用於單 worker 場景。
+```php
+ \Hyperf\SocketIOServer\Room\MemoryAdapter::class,
+];
+```
+
+2. 可以替換為空介面卡,不需要房間功能時可以降低消耗。
+```php
+ \Hyperf\SocketIOServer\Room\NullAdapter::class,
+];
+```
+
+### 調整 SocketID (`sid`)
+
+預設 SocketID 使用 `ServerID#FD` 的格式,可以適應分散式場景。
+
+1. 可以替換為直接使用 Fd 。
+
+```php
+ \Hyperf\SocketIOServer\SidProvider\LocalSidProvider::class,
+];
+```
+
+2. 也可以替換為 SessionID 。
+
+```php
+ \Hyperf\SocketIOServer\SidProvider\SessionSidProvider::class,
+];
+```
+
+### 其他事件分發方法
+
+1. 可以手動註冊事件,不使用註解。
+
+```php
+on('event', [$this, 'echo']);
+ }
+
+ public function echo(Socket $socket, $data)
+ {
+ $socket->emit('event', $data);
+ }
+}
+```
+
+2. 可以在控制器上新增 `@Event()` 註解,以方法名作為事件名來分發。此時應注意其他公有方法可能會和事件名衝突。
+
+```php
+emit('event', $data);
+ }
+}
+```
diff --git a/doc/zh-tw/summary.md b/doc/zh-tw/summary.md
index d0ec32fb6..0c3b4f686 100644
--- a/doc/zh-tw/summary.md
+++ b/doc/zh-tw/summary.md
@@ -52,6 +52,7 @@
* [模型快取](zh-tw/db/model-cache.md)
* [資料庫遷移](zh-tw/db/migration.md)
* [極簡 DB 元件](zh-tw/db/db.md)
+ * [修改器](zh-tw/db/mutators.md)
* 微服務
@@ -83,6 +84,7 @@
* [ETCD 協程客戶端](zh-tw/etcd.md)
* [WebSocket 服務](zh-tw/websocket-server.md)
* [WebSocket 協程客戶端](zh-tw/websocket-client.md)
+ * [Socket.io 服務](zh-tw/socketio-server.md)
* [自定義程序](zh-tw/process.md)
* [開發者工具](zh-tw/devtool.md)
* [輔助類](zh-tw/utils.md)
diff --git a/phpstan.neon b/phpstan.neon
index 66f25de03..f43520281 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,93 +1,16 @@
# Magic behaviour with __get, __set, __call and __callStatic is not exactly static analyser-friendly :)
# Fortunately, You can ingore it by the following config.
#
-rules:
- - PHPStan\Rules\Arrays\DeadForeachRule
- - PHPStan\Rules\Comparison\BooleanOrConstantConditionRule
- - PHPStan\Rules\Comparison\ElseIfConstantConditionRule
- - PHPStan\Rules\Comparison\IfConstantConditionRule
- - PHPStan\Rules\Comparison\TernaryOperatorConstantConditionRule
parameters:
bootstrap: "bootstrap.php"
- checkFunctionArgumentTypes: true
- checkArgumentsPassedByReference: true
+ inferPrivatePropertyTypeFromConstructor: true
+ treatPhpDocTypesAsCertain: true
reportUnmatchedIgnoredErrors: false
- featureToggles:
- deadCatchesRule: false
- noopRule: false
- tooWideTypehints: false
- unreachableStatement: false
- ignoreErrors:
- - "#will always evaluate to false#"
excludes_analyse:
- %currentWorkingDirectory%/src/*/tests/*
-
-conditionalTags:
- PHPStan\Rules\Exceptions\DeadCatchRule:
- phpstan.rules.rule: %featureToggles.deadCatchesRule%
- PHPStan\Rules\DeadCode\NoopRule:
- phpstan.rules.rule: %featureToggles.noopRule%
- PHPStan\Rules\DeadCode\UnreachableStatementRule:
- phpstan.rules.rule: %featureToggles.unreachableStatement%
- PHPStan\Rules\TooWideTypehints\TooWideClosureReturnTypehintRule:
- phpstan.rules.rule: %featureToggles.tooWideTypehints%
- PHPStan\Rules\TooWideTypehints\TooWideFunctionReturnTypehintRule:
- phpstan.rules.rule: %featureToggles.tooWideTypehints%
- PHPStan\Rules\TooWideTypehints\TooWidePrivateMethodReturnTypehintRule:
- phpstan.rules.rule: %featureToggles.tooWideTypehints%
-
-services:
- -
- class: PHPStan\Rules\Classes\ImpossibleInstanceOfRule
- arguments:
- checkAlwaysTrueInstanceof: %checkAlwaysTrueInstanceof%
- tags:
- - phpstan.rules.rule
-
- -
- class: PHPStan\Rules\Comparison\ImpossibleCheckTypeFunctionCallRule
- arguments:
- checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall%
- tags:
- - phpstan.rules.rule
-
- -
- class: PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule
- arguments:
- checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall%
- tags:
- - phpstan.rules.rule
-
- -
- class: PHPStan\Rules\Comparison\ImpossibleCheckTypeStaticMethodCallRule
- arguments:
- checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall%
- tags:
- - phpstan.rules.rule
-
- -
- class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule
- arguments:
- checkAlwaysTrueStrictComparison: %checkAlwaysTrueStrictComparison%
- tags:
- - phpstan.rules.rule
-
- -
- class: PHPStan\Rules\Exceptions\DeadCatchRule
-
- -
- class: PHPStan\Rules\DeadCode\NoopRule
-
- -
- class: PHPStan\Rules\DeadCode\UnreachableStatementRule
-
- -
- class: PHPStan\Rules\TooWideTypehints\TooWideClosureReturnTypehintRule
-
- -
- class: PHPStan\Rules\TooWideTypehints\TooWideFunctionReturnTypehintRule
-
- -
- class: PHPStan\Rules\TooWideTypehints\TooWidePrivateMethodReturnTypehintRule
-
+ ignoreErrors:
+ - '#side of && is always#'
+ - '#method Redis::zRevRangeByScore\(\) expects int, string given#'
+ - '#Argument of an invalid type Hyperf\\AsyncQueue\\Job supplied for foreach, only iterables are supported#'
+ - '#Variable .* in isset\(\) always exists and is not nullable#'
diff --git a/phpunit.xml b/phpunit.xml
index 02f21612c..fead3d489 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -49,6 +49,7 @@
./src/session/tests
./src/snowflake/tests
./src/socket/tests
+ ./src/socketio-server/tests
./src/super-globals/tests
./src/task/tests
./src/testing/tests
diff --git a/src/amqp/composer.json b/src/amqp/composer.json
index fb39d19e1..1963980bc 100644
--- a/src/amqp/composer.json
+++ b/src/amqp/composer.json
@@ -16,10 +16,10 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0",
- "hyperf/process": "~1.1.0",
- "hyperf/pool": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
+ "hyperf/process": "~2.0.0",
+ "hyperf/pool": "~2.0.0",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
"psr/log": "^1.0",
@@ -27,9 +27,9 @@
"doctrine/instantiator": "^1.2.0"
},
"require-dev": {
- "hyperf/di": "~1.1.0",
- "hyperf/event": "~1.1.0",
- "hyperf/framework": "~1.1.0",
+ "hyperf/di": "~2.0.0",
+ "hyperf/event": "~2.0.0",
+ "hyperf/framework": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/amqp/src/Builder.php b/src/amqp/src/Builder.php
index 72c0af01c..a992b5ec8 100644
--- a/src/amqp/src/Builder.php
+++ b/src/amqp/src/Builder.php
@@ -20,6 +20,10 @@ use Psr\Container\ContainerInterface;
class Builder
{
+ /**
+ * @deprecated v2.0
+ * @var string
+ */
protected $name = 'default';
/**
diff --git a/src/amqp/src/Builder/Builder.php b/src/amqp/src/Builder/Builder.php
index d44767416..80790648d 100644
--- a/src/amqp/src/Builder/Builder.php
+++ b/src/amqp/src/Builder/Builder.php
@@ -50,6 +50,9 @@ class Builder
return $this->passive;
}
+ /**
+ * @return static
+ */
public function setPassive(bool $passive): self
{
$this->passive = $passive;
@@ -61,6 +64,9 @@ class Builder
return $this->durable;
}
+ /**
+ * @return static
+ */
public function setDurable(bool $durable): self
{
$this->durable = $durable;
@@ -72,6 +78,9 @@ class Builder
return $this->autoDelete;
}
+ /**
+ * @return static
+ */
public function setAutoDelete(bool $autoDelete): self
{
$this->autoDelete = $autoDelete;
@@ -83,6 +92,9 @@ class Builder
return $this->nowait;
}
+ /**
+ * @return static
+ */
public function setNowait(bool $nowait): self
{
$this->nowait = $nowait;
@@ -99,6 +111,7 @@ class Builder
/**
* @param AMQPTable|array $arguments
+ * @return static
*/
public function setArguments($arguments): self
{
@@ -116,6 +129,7 @@ class Builder
/**
* @param null|int $ticket
+ * @return static
*/
public function setTicket($ticket): self
{
diff --git a/src/amqp/src/Connection.php b/src/amqp/src/Connection.php
index 1a202a63f..a26be2f6b 100644
--- a/src/amqp/src/Connection.php
+++ b/src/amqp/src/Connection.php
@@ -31,7 +31,7 @@ class Connection extends BaseConnection implements ConnectionInterface
protected $pool;
/**
- * @var AbstractConnection
+ * @var null|AbstractConnection
*/
protected $connection;
@@ -56,12 +56,12 @@ class Connection extends BaseConnection implements ConnectionInterface
protected $lastHeartbeatTime = 0.0;
/**
- * @var \PhpAmqpLib\Channel\AMQPChannel
+ * @var null|AMQPChannel
*/
protected $channel;
/**
- * @var \PhpAmqpLib\Channel\AMQPChannel
+ * @var null|AMQPChannel
*/
protected $confirmChannel;
diff --git a/src/amqp/src/Connection/AMQPSwooleConnection.php b/src/amqp/src/Connection/AMQPSwooleConnection.php
index c3dfc0b91..ba1e66d41 100644
--- a/src/amqp/src/Connection/AMQPSwooleConnection.php
+++ b/src/amqp/src/Connection/AMQPSwooleConnection.php
@@ -47,7 +47,7 @@ class AMQPSwooleConnection extends AbstractConnection
$locale,
$io,
$heartbeat,
- $connectionTimeout
+ (int) $connectionTimeout
);
}
diff --git a/src/amqp/src/Connection/KeepaliveIO.php b/src/amqp/src/Connection/KeepaliveIO.php
index 8c5f1d811..f8e86441b 100644
--- a/src/amqp/src/Connection/KeepaliveIO.php
+++ b/src/amqp/src/Connection/KeepaliveIO.php
@@ -172,7 +172,6 @@ class KeepaliveIO extends AbstractIO
*/
public function check_heartbeat()
{
- return true;
}
public function close()
diff --git a/src/amqp/src/Connection/SwooleIO.php b/src/amqp/src/Connection/SwooleIO.php
index 0d9690d50..ec5d8af09 100644
--- a/src/amqp/src/Connection/SwooleIO.php
+++ b/src/amqp/src/Connection/SwooleIO.php
@@ -15,7 +15,6 @@ use InvalidArgumentException;
use PhpAmqpLib\Exception\AMQPRuntimeException;
use PhpAmqpLib\Wire\AMQPWriter;
use PhpAmqpLib\Wire\IO\AbstractIO;
-use Swoole;
use Swoole\Coroutine\Client;
class SwooleIO extends AbstractIO
@@ -58,12 +57,12 @@ class SwooleIO extends AbstractIO
protected $heartbeat;
/**
- * @var float
+ * @var null|float
*/
protected $lastRead;
/**
- * @var float
+ * @var null|float
*/
protected $lastWrite;
@@ -85,7 +84,9 @@ class SwooleIO extends AbstractIO
/** @var int */
private $initialHeartbeat;
- /** @var Swoole\Coroutine\Client */
+ /**
+ * @var null|Client
+ */
private $sock;
private $buffer = '';
@@ -181,8 +182,6 @@ class SwooleIO extends AbstractIO
$this->buffer .= $read_buffer;
} while (true);
-
- return false;
}
/**
@@ -236,7 +235,7 @@ class SwooleIO extends AbstractIO
}
/**
- * @return resource
+ * @return null|Client|resource
*/
public function get_socket()
{
diff --git a/src/amqp/src/Consumer.php b/src/amqp/src/Consumer.php
index 2eaba9c80..ede4b13c8 100644
--- a/src/amqp/src/Consumer.php
+++ b/src/amqp/src/Consumer.php
@@ -37,7 +37,7 @@ class Consumer extends Builder
protected $status = true;
/**
- * @var EventDispatcherInterface
+ * @var null|EventDispatcherInterface
*/
protected $eventDispatcher;
@@ -66,7 +66,7 @@ class Consumer extends Builder
$channel = $connection->getConfirmChannel();
$this->declare($consumerMessage, $channel);
- $concurrent = $this->getConcurrent();
+ $concurrent = $this->getConcurrent($consumerMessage->getPoolName());
$maxConsumption = $consumerMessage->getMaxConsumption();
$currentConsumption = 0;
@@ -87,7 +87,7 @@ class Consumer extends Builder
return parallel([$callback]);
}
- return $concurrent->create($callback);
+ $concurrent->create($callback);
}
);
@@ -137,10 +137,10 @@ class Consumer extends Builder
}
}
- protected function getConcurrent(): ?Concurrent
+ protected function getConcurrent(string $pool): ?Concurrent
{
$config = $this->container->get(ConfigInterface::class);
- $concurrent = (int) $config->get('amqp.' . $this->name . '.concurrent.limit', 0);
+ $concurrent = (int) $config->get('amqp.' . $pool . '.concurrent.limit', 0);
if ($concurrent > 1) {
return new Concurrent($concurrent);
}
diff --git a/src/amqp/src/ConsumerManager.php b/src/amqp/src/ConsumerManager.php
index 6380c490b..c67b3ff3c 100644
--- a/src/amqp/src/ConsumerManager.php
+++ b/src/amqp/src/ConsumerManager.php
@@ -34,7 +34,7 @@ class ConsumerManager
{
$classes = AnnotationCollector::getClassByAnnotation(ConsumerAnnotation::class);
/**
- * @var string
+ * @var string $class
* @var ConsumerAnnotation $annotation
*/
foreach ($classes as $class => $annotation) {
diff --git a/src/amqp/src/Listener/MainWorkerStartListener.php b/src/amqp/src/Listener/MainWorkerStartListener.php
index c407afae3..19eee8cd0 100644
--- a/src/amqp/src/Listener/MainWorkerStartListener.php
+++ b/src/amqp/src/Listener/MainWorkerStartListener.php
@@ -62,7 +62,7 @@ class MainWorkerStartListener implements ListenerInterface
$producer = $this->container->get(\Hyperf\Amqp\Producer::class);
$instantiator = $this->container->get(Instantiator::class);
/**
- * @var string
+ * @var string $producerMessageClass
* @var Producer $annotation
*/
foreach ($producerMessages as $producerMessageClass => $annotation) {
diff --git a/src/amqp/src/Message/DynamicRpcMessage.php b/src/amqp/src/Message/DynamicRpcMessage.php
index b3d09f644..0fad6d73f 100644
--- a/src/amqp/src/Message/DynamicRpcMessage.php
+++ b/src/amqp/src/Message/DynamicRpcMessage.php
@@ -1,16 +1,22 @@
exchange = $exchange;
$this->routingKey = $routingKey;
$this->payload = $data;
}
-
-}
\ No newline at end of file
+}
diff --git a/src/amqp/src/Message/RpcMessage.php b/src/amqp/src/Message/RpcMessage.php
index b515d620d..ce17b4785 100644
--- a/src/amqp/src/Message/RpcMessage.php
+++ b/src/amqp/src/Message/RpcMessage.php
@@ -30,9 +30,9 @@ abstract class RpcMessage extends Message implements RpcMessageInterface
public function getQueueBuilder(): QueueBuilder
{
return (new QueueBuilder())->setQueue($this->queue)
+ ->setExclusive(true)
->setPassive(false)
->setDurable(false)
- ->setExclusive(true)
->setAutoDelete(false);
}
diff --git a/src/amqp/src/Producer.php b/src/amqp/src/Producer.php
index d8e3139c7..a69686723 100644
--- a/src/amqp/src/Producer.php
+++ b/src/amqp/src/Producer.php
@@ -60,7 +60,7 @@ class Producer extends Builder
private function injectMessageProperty(ProducerMessageInterface $producerMessage)
{
if (class_exists(AnnotationCollector::class)) {
- /** @var \Hyperf\Amqp\Annotation\Producer $annotation */
+ /** @var null|\Hyperf\Amqp\Annotation\Producer $annotation */
$annotation = AnnotationCollector::getClassAnnotation(get_class($producerMessage), Annotation\Producer::class);
if ($annotation) {
$annotation->routingKey && $producerMessage->setRoutingKey($annotation->routingKey);
diff --git a/src/amqp/src/RpcClient.php b/src/amqp/src/RpcClient.php
index 243cf2ba0..ccdcf82e8 100644
--- a/src/amqp/src/RpcClient.php
+++ b/src/amqp/src/RpcClient.php
@@ -36,7 +36,7 @@ class RpcClient extends Builder
$body = $connection->getAMQPMessage($timeout)->getBody();
return $rpcMessage->unserialize($body);
} finally {
- $connection && $connection->release();
+ isset($connection) && $connection->release();
}
}
}
diff --git a/src/amqp/src/RpcConnection.php b/src/amqp/src/RpcConnection.php
index 7ed44acf7..e7a698e39 100644
--- a/src/amqp/src/RpcConnection.php
+++ b/src/amqp/src/RpcConnection.php
@@ -26,7 +26,7 @@ class RpcConnection extends Connection
protected $queue;
/**
- * @var AMQPMessage
+ * @var null|AMQPMessage
*/
protected $message;
diff --git a/src/amqp/tests/ConsumerManagerTest.php b/src/amqp/tests/ConsumerManagerTest.php
index 358cc3088..42e75ebf8 100644
--- a/src/amqp/tests/ConsumerManagerTest.php
+++ b/src/amqp/tests/ConsumerManagerTest.php
@@ -15,6 +15,7 @@ use Hyperf\Amqp\Annotation\Consumer;
use Hyperf\Amqp\ConsumerManager;
use Hyperf\Amqp\Message\ConsumerMessageInterface;
use Hyperf\Di\Annotation\AnnotationCollector;
+use Hyperf\Process\AbstractProcess;
use Hyperf\Process\ProcessManager;
use HyperfTest\Amqp\Stub\ContainerStub;
use HyperfTest\Amqp\Stub\DemoConsumer;
@@ -47,6 +48,7 @@ class ConsumerManagerTest extends TestCase
$manager->run();
$hasRegisted = false;
+ /** @var AbstractProcess $item */
foreach (ProcessManager::all() as $item) {
if (method_exists($item, 'getConsumerMessage')) {
$hasRegisted = true;
@@ -81,6 +83,7 @@ class ConsumerManagerTest extends TestCase
$manager->run();
$hasRegisted = false;
+ /** @var AbstractProcess $item */
foreach (ProcessManager::all() as $item) {
if (method_exists($item, 'getConsumerMessage')) {
$hasRegisted = true;
diff --git a/src/amqp/tests/ConsumerTest.php b/src/amqp/tests/ConsumerTest.php
new file mode 100644
index 000000000..9e9183ebd
--- /dev/null
+++ b/src/amqp/tests/ConsumerTest.php
@@ -0,0 +1,43 @@
+getMethod('getConcurrent');
+ $method->setAccessible(true);
+ /** @var Concurrent $concurrent */
+ $concurrent = $method->invokeArgs($consumer, ['default']);
+ $this->assertSame(10, $concurrent->getLimit());
+
+ /** @var Concurrent $concurrent */
+ $concurrent = $method->invokeArgs($consumer, ['co']);
+ $this->assertSame(5, $concurrent->getLimit());
+ }
+}
diff --git a/src/amqp/tests/Stub/ContainerStub.php b/src/amqp/tests/Stub/ContainerStub.php
index 3b45d5abb..cb706bb49 100644
--- a/src/amqp/tests/Stub/ContainerStub.php
+++ b/src/amqp/tests/Stub/ContainerStub.php
@@ -13,6 +13,8 @@ namespace HyperfTest\Amqp\Stub;
use Hyperf\Amqp\Consumer;
use Hyperf\Amqp\Pool\PoolFactory;
+use Hyperf\Config\Config;
+use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Di\Container;
use Hyperf\Utils\ApplicationContext;
@@ -45,6 +47,22 @@ class ContainerStub
$container->shouldReceive('get')->with(Consumer::class)->andReturnUsing(function () use ($container) {
return new Consumer($container, $container->get(PoolFactory::class), $container->get(StdoutLoggerInterface::class));
});
+ $container->shouldReceive('get')->with(ConfigInterface::class)->andReturnUsing(function () {
+ return new Config([
+ 'amqp' => [
+ 'default' => [
+ 'concurrent' => [
+ 'limit' => 10,
+ ],
+ ],
+ 'co' => [
+ 'concurrent' => [
+ 'limit' => 5,
+ ],
+ ],
+ ],
+ ]);
+ });
return $container;
}
diff --git a/src/async-queue/composer.json b/src/async-queue/composer.json
index 8d461f3c1..399344196 100644
--- a/src/async-queue/composer.json
+++ b/src/async-queue/composer.json
@@ -18,12 +18,12 @@
"php": ">=7.2",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/command": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/command": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
- "hyperf/process": "~1.1.0",
+ "hyperf/process": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/async-queue/src/Driver/DriverInterface.php b/src/async-queue/src/Driver/DriverInterface.php
index c65d0c4f9..e4b1ae45f 100644
--- a/src/async-queue/src/Driver/DriverInterface.php
+++ b/src/async-queue/src/Driver/DriverInterface.php
@@ -33,14 +33,14 @@ interface DriverInterface
/**
* Ack a job.
*
- * @param $data
+ * @param mixed $data
*/
public function ack($data): bool;
/**
* Push a job to failed queue.
*
- * @param $data
+ * @param mixed $data
*/
public function fail($data): bool;
diff --git a/src/async-queue/src/Driver/RedisDriver.php b/src/async-queue/src/Driver/RedisDriver.php
index b87cb5172..248cc0edd 100644
--- a/src/async-queue/src/Driver/RedisDriver.php
+++ b/src/async-queue/src/Driver/RedisDriver.php
@@ -176,7 +176,7 @@ class RedisDriver extends Driver
/**
* Remove data from reserved queue.
- * @param mixed $data
+ * @param string $data
*/
protected function remove($data): bool
{
diff --git a/src/async-queue/src/Job.php b/src/async-queue/src/Job.php
index c7b281ea2..80cc3a1b5 100644
--- a/src/async-queue/src/Job.php
+++ b/src/async-queue/src/Job.php
@@ -27,7 +27,7 @@ abstract class Job implements JobInterface, CompressInterface, UnCompressInterfa
}
/**
- * @return JobInterface
+ * @return static
*/
public function uncompress(): CompressInterface
{
@@ -41,7 +41,7 @@ abstract class Job implements JobInterface, CompressInterface, UnCompressInterfa
}
/**
- * @return JobInterface
+ * @return static
*/
public function compress(): UnCompressInterface
{
diff --git a/src/async-queue/src/Listener/QueueLengthListener.php b/src/async-queue/src/Listener/QueueLengthListener.php
index ddef4d8db..12c26d39c 100644
--- a/src/async-queue/src/Listener/QueueLengthListener.php
+++ b/src/async-queue/src/Listener/QueueLengthListener.php
@@ -45,6 +45,7 @@ class QueueLengthListener implements ListenerInterface
*/
public function process(object $event)
{
+ $value = 0;
foreach ($this->level as $level => $value) {
if ($event->length < $value) {
$message = sprintf('Queue lengh of %s is %d.', $event->key, $event->length);
diff --git a/src/async-queue/src/Message.php b/src/async-queue/src/Message.php
index 1f91ce37b..265dac3d9 100644
--- a/src/async-queue/src/Message.php
+++ b/src/async-queue/src/Message.php
@@ -18,7 +18,7 @@ use Serializable;
class Message implements MessageInterface, Serializable
{
/**
- * @var JobInterface
+ * @var CompressInterface|JobInterface|UnCompressInterface
*/
protected $job;
diff --git a/src/async-queue/tests/AsyncQueueAspectTest.php b/src/async-queue/tests/AsyncQueueAspectTest.php
index 167fb5e33..0ca116c87 100644
--- a/src/async-queue/tests/AsyncQueueAspectTest.php
+++ b/src/async-queue/tests/AsyncQueueAspectTest.php
@@ -20,6 +20,7 @@ use Hyperf\AsyncQueue\Environment;
use Hyperf\Di\Annotation\AnnotationCollector;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\Ast;
+use Hyperf\Di\BetterReflectionManager;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Context;
use HyperfTest\AsyncQueue\Stub\FooProxy;
@@ -37,6 +38,7 @@ class AsyncQueueAspectTest extends TestCase
{
Mockery::close();
Context::set(FooProxy::class, null);
+ BetterReflectionManager::clear();
}
public function testNotAsyncMessage()
@@ -80,29 +82,32 @@ class AsyncQueueAspectTest extends TestCase
$container = Mockery::mock(ContainerInterface::class);
ApplicationContext::setContainer($container);
+ BetterReflectionManager::initClassReflector([__DIR__ . '/Stub/']);
+
$aspect = new Aspect();
$aspect->collectClass(AsyncQueueAspect::class);
AnnotationCollector::collectMethod(FooProxy::class, 'async', AsyncQueueMessage::class, new AsyncQueueMessage());
AnnotationCollector::collectMethod(FooProxy::class, 'variadic', AsyncQueueMessage::class, new AsyncQueueMessage());
$ast = new Ast();
- $code = $ast->proxy(FooProxy::class, $proxy = FooProxy::class . 'Proxy');
+ $code = $ast->proxy(FooProxy::class);
if (! is_dir($dir = BASE_PATH . '/runtime/container/proxy/')) {
mkdir($dir, 0777, true);
}
file_put_contents($file = $dir . 'FooProxy.proxy.php', $code);
require_once $file;
- $container->shouldReceive('get')->with(FooProxy::class)->andReturn(new $proxy());
+ $container->shouldReceive('get')->with(FooProxy::class)->andReturn(new FooProxy());
$container->shouldReceive('get')->with(AsyncQueueAspect::class)->andReturnUsing(function ($_) use ($container) {
return new AsyncQueueAspect($container);
});
- $container->shouldReceive('get')->with(Environment::class)->andReturn(new Environment());
- $container->shouldReceive('get')->with(DriverFactory::class)->andReturnUsing(function ($_) {
+ $container->shouldReceive('get')->with(Environment::class)->andReturn($environment = new Environment());
+ $container->shouldReceive('get')->with(DriverFactory::class)->andReturnUsing(function ($_) use ($environment) {
$factory = Mockery::mock(DriverFactory::class);
$driver = Mockery::mock(DriverInterface::class);
- $driver->shouldReceive('push')->andReturnUsing(function ($job) {
+ $driver->shouldReceive('push')->andReturnUsing(function ($job) use ($environment) {
$this->assertInstanceOf(AnnotationJob::class, $job);
+ $environment->setAsyncQueue(true);
$origin = new FooProxy();
$origin->{$job->method}(...$job->params);
return true;
diff --git a/src/cache/composer.json b/src/cache/composer.json
index 67167e734..a5800c5a6 100644
--- a/src/cache/composer.json
+++ b/src/cache/composer.json
@@ -18,12 +18,12 @@
"php": ">=7.2",
"psr/container": "^1.0",
"psr/simple-cache": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
- "hyperf/di": "~1.1.0",
- "hyperf/event": "~1.1.0",
+ "hyperf/di": "~2.0.0",
+ "hyperf/event": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/cache/src/Annotation/Cacheable.php b/src/cache/src/Annotation/Cacheable.php
index 42d32199a..0ca914adb 100644
--- a/src/cache/src/Annotation/Cacheable.php
+++ b/src/cache/src/Annotation/Cacheable.php
@@ -67,7 +67,7 @@ class Cacheable extends AbstractAnnotation
public function collectMethod(string $className, ?string $target): void
{
if (isset($this->listener)) {
- CacheListenerCollector::set($this->listener, [
+ CacheListenerCollector::setListener($this->listener, [
'className' => $className,
'method' => $target,
]);
diff --git a/src/cache/src/Annotation/FailCache.php b/src/cache/src/Annotation/FailCache.php
index bd378344b..af497d00c 100644
--- a/src/cache/src/Annotation/FailCache.php
+++ b/src/cache/src/Annotation/FailCache.php
@@ -34,7 +34,7 @@ class FailCache extends AbstractAnnotation
public function collectMethod(string $className, ?string $target): void
{
if (isset($this->listener)) {
- CacheListenerCollector::set($this->listener, [
+ CacheListenerCollector::setListener($this->listener, [
'className' => $className,
'method' => $target,
]);
diff --git a/src/cache/src/Aspect/CacheableAspect.php b/src/cache/src/Aspect/CacheableAspect.php
index 3129ab9ef..eebe0554f 100644
--- a/src/cache/src/Aspect/CacheableAspect.php
+++ b/src/cache/src/Aspect/CacheableAspect.php
@@ -18,7 +18,6 @@ use Hyperf\Cache\Driver\KeyCollectorInterface;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
-use Psr\Container\ContainerInterface;
/**
* @Aspect
@@ -32,7 +31,7 @@ class CacheableAspect extends AbstractAspect
];
/**
- * @var ContainerInterface
+ * @var CacheManager
*/
protected $manager;
diff --git a/src/cache/src/Aspect/FailCacheAspect.php b/src/cache/src/Aspect/FailCacheAspect.php
index ffd5fe163..3506586ba 100644
--- a/src/cache/src/Aspect/FailCacheAspect.php
+++ b/src/cache/src/Aspect/FailCacheAspect.php
@@ -18,7 +18,6 @@ use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
-use Psr\Container\ContainerInterface;
/**
* @Aspect
@@ -32,7 +31,7 @@ class FailCacheAspect extends AbstractAspect
];
/**
- * @var ContainerInterface
+ * @var CacheManager
*/
protected $manager;
diff --git a/src/cache/src/CacheListenerCollector.php b/src/cache/src/CacheListenerCollector.php
index e9cee117d..8ad3be41b 100644
--- a/src/cache/src/CacheListenerCollector.php
+++ b/src/cache/src/CacheListenerCollector.php
@@ -19,4 +19,27 @@ class CacheListenerCollector extends MetadataCollector
* @var array
*/
protected static $container = [];
+
+ public static function setListener(string $listener, array $value)
+ {
+ static::$container[$listener] = $value;
+ }
+
+ public static function getListner(string $listener, $default = null)
+ {
+ return static::$container[$listener] ?? $default;
+ }
+
+ public static function clear(?string $className = null): void
+ {
+ if ($className) {
+ foreach (static::$container as $listener => $value) {
+ if (isset($value['className']) && $value['className'] === $className) {
+ unset(static::$container[$listener]);
+ }
+ }
+ } else {
+ static::$container = [];
+ }
+ }
}
diff --git a/src/cache/src/Driver/CoroutineMemoryDriver.php b/src/cache/src/Driver/CoroutineMemoryDriver.php
index 039d992cb..f60c599f7 100644
--- a/src/cache/src/Driver/CoroutineMemoryDriver.php
+++ b/src/cache/src/Driver/CoroutineMemoryDriver.php
@@ -51,6 +51,8 @@ class CoroutineMemoryDriver extends Driver implements KeyCollectorInterface
foreach ($values as $key => $value) {
$this->set($key, $values, $ttl);
}
+
+ return true;
}
public function deleteMultiple($keys)
@@ -58,6 +60,8 @@ class CoroutineMemoryDriver extends Driver implements KeyCollectorInterface
foreach ($keys as $key) {
$this->delete($key);
}
+
+ return true;
}
public function has($key)
diff --git a/src/cache/src/Driver/RedisDriver.php b/src/cache/src/Driver/RedisDriver.php
index d0cc8b3a8..6409bd17c 100644
--- a/src/cache/src/Driver/RedisDriver.php
+++ b/src/cache/src/Driver/RedisDriver.php
@@ -113,7 +113,7 @@ class RedisDriver extends Driver implements KeyCollectorInterface
return $this->getCacheKey($key);
}, $keys);
- return $this->redis->del(...$cacheKeys);
+ return (bool) $this->redis->del(...$cacheKeys);
}
public function has($key)
diff --git a/src/cache/src/Listener/DeleteListenerEvent.php b/src/cache/src/Listener/DeleteListenerEvent.php
index b3ccc4536..a0d735dbd 100644
--- a/src/cache/src/Listener/DeleteListenerEvent.php
+++ b/src/cache/src/Listener/DeleteListenerEvent.php
@@ -18,7 +18,7 @@ class DeleteListenerEvent extends DeleteEvent
{
public function __construct(string $listener, array $arguments)
{
- $config = CacheListenerCollector::get($listener, null);
+ $config = CacheListenerCollector::getListner($listener, null);
if (! $config) {
throw new CacheException(sprintf('listener %s is not defined.', $listener));
}
diff --git a/src/circuit-breaker/composer.json b/src/circuit-breaker/composer.json
index 0a3d32430..d2edf9b79 100644
--- a/src/circuit-breaker/composer.json
+++ b/src/circuit-breaker/composer.json
@@ -17,11 +17,11 @@
"require": {
"php": ">=7.2",
"psr/container": "^1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
- "hyperf/contract": "~1.1.0",
- "hyperf/di": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/di": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/command/composer.json b/src/command/composer.json
index 94d57ab50..7fa5ec898 100644
--- a/src/command/composer.json
+++ b/src/command/composer.json
@@ -20,9 +20,9 @@
},
"require": {
"php": ">=7.2",
- "hyperf/utils": "~1.1.0",
+ "hyperf/utils": "~2.0.0",
"psr/event-dispatcher": "^1.0",
- "symfony/console": "^4.2"
+ "symfony/console": "^5.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/command/src/Command.php b/src/command/src/Command.php
index 49a478bb5..543e6b9f8 100644
--- a/src/command/src/Command.php
+++ b/src/command/src/Command.php
@@ -40,7 +40,7 @@ abstract class Command extends SymfonyCommand
protected $input;
/**
- * @var OutputInterface|SymfonyStyle
+ * @var SymfonyStyle
*/
protected $output;
@@ -159,12 +159,26 @@ abstract class Command extends SymfonyCommand
return $this->output->askQuestion($question);
}
+ /**
+ * Give the user a multiple choice from an array of answers.
+ */
+ public function choiceMultiple(
+ string $question,
+ array $choices,
+ $default = null,
+ ?int $attempts = null
+ ): array {
+ $question = new ChoiceQuestion($question, $choices, $default);
+
+ $question->setMaxAttempts($attempts)->setMultiselect(true);
+
+ return $this->output->askQuestion($question);
+ }
+
/**
* Give the user a single choice from an array of answers.
*
- * @param null|mixed $default
- * @param null|mixed $attempts
- * @param null|mixed $multiple
+ * @param null|bool $multiple Deprecated: use choiceMultiple method instead.
*/
public function choice(
string $question,
@@ -173,11 +187,7 @@ abstract class Command extends SymfonyCommand
$attempts = null,
$multiple = null
): string {
- $question = new ChoiceQuestion($question, $choices, $default);
-
- $question->setMaxAttempts($attempts)->setMultiselect($multiple);
-
- return $this->output->askQuestion($question);
+ return $this->choiceMultiple($question, $choices, $default, $attempts)[0];
}
/**
diff --git a/src/config-aliyun-acm/composer.json b/src/config-aliyun-acm/composer.json
index 53e8806b1..4eb42d1d2 100644
--- a/src/config-aliyun-acm/composer.json
+++ b/src/config-aliyun-acm/composer.json
@@ -21,14 +21,14 @@
"require": {
"php": ">=7.2",
"psr/container": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/guzzle": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/guzzle": "~2.0.0"
},
"require-dev": {
- "hyperf/config": "~1.1.0",
- "hyperf/event": "~1.1.0",
- "hyperf/framework": "~1.1.0",
- "hyperf/process": "~1.1.0",
+ "hyperf/config": "~2.0.0",
+ "hyperf/event": "~2.0.0",
+ "hyperf/framework": "~2.0.0",
+ "hyperf/process": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/config-apollo/composer.json b/src/config-apollo/composer.json
index 852a52b77..92cd6f0b4 100644
--- a/src/config-apollo/composer.json
+++ b/src/config-apollo/composer.json
@@ -20,14 +20,14 @@
"require": {
"php": ">=7.2",
"psr/container": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
- "hyperf/config": "~1.1.0",
- "hyperf/event": "~1.1.0",
- "hyperf/framework": "~1.1.0",
- "hyperf/process": "~1.1.0",
+ "hyperf/config": "~2.0.0",
+ "hyperf/event": "~2.0.0",
+ "hyperf/framework": "~2.0.0",
+ "hyperf/process": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/config-etcd/composer.json b/src/config-etcd/composer.json
index 74925370a..92cd98640 100644
--- a/src/config-etcd/composer.json
+++ b/src/config-etcd/composer.json
@@ -21,8 +21,8 @@
},
"require": {
"php": ">=7.2",
- "hyperf/utils": "~1.1.0",
- "hyperf/etcd": "~1.1.0"
+ "hyperf/utils": "~2.0.0",
+ "hyperf/etcd": "~2.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14"
diff --git a/src/config-zookeeper/composer.json b/src/config-zookeeper/composer.json
index 45f878d24..40a2f4634 100644
--- a/src/config-zookeeper/composer.json
+++ b/src/config-zookeeper/composer.json
@@ -20,13 +20,13 @@
"require": {
"php": ">=7.2",
"psr/container": "^1.0",
- "hyperf/contract": "~1.1.0"
+ "hyperf/contract": "~2.0.0"
},
"require-dev": {
- "hyperf/config": "~1.1.0",
- "hyperf/event": "~1.1.0",
- "hyperf/framework": "~1.1.0",
- "hyperf/process": "~1.1.0",
+ "hyperf/config": "~2.0.0",
+ "hyperf/event": "~2.0.0",
+ "hyperf/framework": "~2.0.0",
+ "hyperf/process": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/config/composer.json b/src/config/composer.json
index 41cf76638..19c0bfb88 100644
--- a/src/config/composer.json
+++ b/src/config/composer.json
@@ -19,15 +19,15 @@
"require": {
"php": ">=7.2",
"psr/container": "^1.0",
- "vlucas/phpdotenv": "^3.1",
- "symfony/finder": "^4.2.8",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "vlucas/phpdotenv": "^4.0",
+ "symfony/finder": "^5.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
- "hyperf/di": "~1.1.0",
- "hyperf/event": "~1.1.0",
- "hyperf/framework": "~1.1.0",
+ "hyperf/di": "~2.0.0",
+ "hyperf/event": "~2.0.0",
+ "hyperf/framework": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/config/src/Annotation/ValueAspect.php b/src/config/src/Annotation/ValueAspect.php
new file mode 100644
index 000000000..b9b8988f0
--- /dev/null
+++ b/src/config/src/Annotation/ValueAspect.php
@@ -0,0 +1,28 @@
+process();
+ }
+}
diff --git a/src/config/src/ConfigFactory.php b/src/config/src/ConfigFactory.php
index 619257d57..49924db73 100644
--- a/src/config/src/ConfigFactory.php
+++ b/src/config/src/ConfigFactory.php
@@ -11,7 +11,6 @@ declare(strict_types=1);
*/
namespace Hyperf\Config;
-use Dotenv\Dotenv;
use Psr\Container\ContainerInterface;
use Symfony\Component\Finder\Finder;
@@ -19,11 +18,6 @@ class ConfigFactory
{
public function __invoke(ContainerInterface $container)
{
- // Load env before config.
- if (file_exists(BASE_PATH . '/.env')) {
- Dotenv::create([BASE_PATH])->load();
- }
-
$configPath = BASE_PATH . '/config/';
$config = $this->readConfig($configPath . 'config.php');
$serverConfig = $this->readConfig($configPath . 'server.php');
diff --git a/src/config/src/Listener/RegisterPropertyHandlerListener.php b/src/config/src/Listener/RegisterPropertyHandlerListener.php
index 42ae5e751..4fc892fe7 100644
--- a/src/config/src/Listener/RegisterPropertyHandlerListener.php
+++ b/src/config/src/Listener/RegisterPropertyHandlerListener.php
@@ -13,9 +13,8 @@ namespace Hyperf\Config\Listener;
use Hyperf\Config\Annotation\Value;
use Hyperf\Contract\ConfigInterface;
-use Hyperf\Di\Definition\ObjectDefinition;
use Hyperf\Di\Definition\PropertyHandlerManager;
-use Hyperf\Di\Definition\PropertyInjection;
+use Hyperf\Di\ReflectionManager;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BootApplication;
use Hyperf\Utils\ApplicationContext;
@@ -38,14 +37,13 @@ class RegisterPropertyHandlerListener implements ListenerInterface
*/
public function process(object $event)
{
- PropertyHandlerManager::register(Value::class, function (ObjectDefinition $definition, string $propertyName, $annotation) {
+ PropertyHandlerManager::register(Value::class, function ($object, $currentClassName, $targetClassName, $property, $annotation) {
if ($annotation instanceof Value && ApplicationContext::hasContainer()) {
+ $reflectionProperty = ReflectionManager::reflectProperty($currentClassName, $property);
+ $reflectionProperty->setAccessible(true);
$key = $annotation->key;
- $propertyInjection = new PropertyInjection($propertyName, function () use ($key) {
- $config = ApplicationContext::getContainer()->get(ConfigInterface::class);
- return $config->get($key, null);
- });
- $definition->addPropertyInjection($propertyInjection);
+ $config = ApplicationContext::getContainer()->get(ConfigInterface::class);
+ $reflectionProperty->setValue($object, $config->get($key, null));
}
});
}
diff --git a/src/config/src/ProviderConfig.php b/src/config/src/ProviderConfig.php
index c5913cce6..3968e5a03 100644
--- a/src/config/src/ProviderConfig.php
+++ b/src/config/src/ProviderConfig.php
@@ -59,6 +59,9 @@ class ProviderConfig
protected static function merge(...$arrays): array
{
+ if (empty($arrays)) {
+ return [];
+ }
$result = array_merge_recursive(...$arrays);
if (isset($result['dependencies'])) {
$dependencies = array_column($arrays, 'dependencies');
diff --git a/src/constants/composer.json b/src/constants/composer.json
index da93b9c08..f72b70617 100644
--- a/src/constants/composer.json
+++ b/src/constants/composer.json
@@ -16,8 +16,8 @@
},
"require": {
"php": ">=7.2",
- "hyperf/di": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/di": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/consul/composer.json b/src/consul/composer.json
index 7c9047406..0af438581 100644
--- a/src/consul/composer.json
+++ b/src/consul/composer.json
@@ -18,7 +18,7 @@
},
"require": {
"php": ">=7.2",
- "hyperf/guzzle": "~1.1.0"
+ "hyperf/guzzle": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/contract/src/Castable.php b/src/contract/src/Castable.php
new file mode 100644
index 000000000..e6b1cc836
--- /dev/null
+++ b/src/contract/src/Castable.php
@@ -0,0 +1,22 @@
+=7.2",
- "hyperf/utils": "~1.1.0",
- "hyperf/paginator": "~1.1.0",
+ "hyperf/utils": "~2.0.0",
+ "hyperf/paginator": "~2.0.0",
"psr/container": "^1.0",
"nesbot/carbon": "^2.0",
"psr/event-dispatcher": "^1.0"
diff --git a/src/database/src/Commands/Ast/ModelUpdateVisitor.php b/src/database/src/Commands/Ast/ModelUpdateVisitor.php
index c115e4c35..27f5587ad 100644
--- a/src/database/src/Commands/Ast/ModelUpdateVisitor.php
+++ b/src/database/src/Commands/Ast/ModelUpdateVisitor.php
@@ -11,6 +11,9 @@ declare(strict_types=1);
*/
namespace Hyperf\Database\Commands\Ast;
+use Hyperf\Contract\Castable;
+use Hyperf\Contract\CastsAttributes;
+use Hyperf\Contract\CastsInboundAttributes;
use Hyperf\Database\Commands\ModelOption;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Collection;
@@ -53,7 +56,7 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
];
/**
- * @var string
+ * @var Model
*/
protected $class;
@@ -91,7 +94,7 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
public function __construct($class, $columns, ModelOption $option)
{
- $this->class = $class;
+ $this->class = new $class();
$this->columns = $columns;
$this->option = $option;
$this->initPropertiesFromMethods();
@@ -129,9 +132,32 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
protected function rewriteCasts(Node\Stmt\PropertyProperty $node): Node\Stmt\PropertyProperty
{
$items = [];
+ $keys = [];
+ if ($node->default instanceof Node\Expr\Array_) {
+ $items = $node->default->items;
+ }
+
+ if ($this->option->isForceCasts()) {
+ $items = [];
+ $casts = $this->class->getCasts();
+ foreach ($node->default->items as $item) {
+ $caster = $this->class->getCasts()[$item->key->value] ?? null;
+ if ($caster && $this->isCaster($caster)) {
+ $items[] = $item;
+ }
+ }
+ }
+
+ foreach ($items as $item) {
+ $keys[] = $item->key->value;
+ }
+
foreach ($this->columns as $column) {
$name = $column['column_name'];
$type = $column['cast'] ?? null;
+ if (in_array($name, $keys)) {
+ continue;
+ }
if ($type || $type = $this->formatDatabaseType($column['data_type'])) {
$items[] = new Node\Expr\ArrayItem(
new Node\Scalar\String_($type),
@@ -146,6 +172,16 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
return $node;
}
+ /**
+ * @param object|string $caster
+ */
+ protected function isCaster($caster): bool
+ {
+ return is_subclass_of($caster, CastsAttributes::class) ||
+ is_subclass_of($caster, Castable::class) ||
+ is_subclass_of($caster, CastsInboundAttributes::class);
+ }
+
protected function parseProperty(): string
{
$doc = '/**' . PHP_EOL;
@@ -174,12 +210,10 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
protected function initPropertiesFromMethods()
{
/** @var ReflectionClass $reflection */
- $reflection = self::getReflector()->reflect($this->class);
+ $reflection = self::getReflector()->reflect(get_class($this->class));
$methods = $reflection->getImmediateMethods();
$namespace = $reflection->getDeclaringNamespaceAst();
- if (empty($methods)) {
- return;
- }
+ $casts = $this->class->getCasts();
sort($methods);
/** @var ReflectionMethod $method */
@@ -251,6 +285,26 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
}
}
}
+
+ // The custom caster.
+ foreach ($casts as $key => $caster) {
+ if (is_subclass_of($caster, Castable::class)) {
+ $caster = $caster::castUsing();
+ }
+
+ if (is_subclass_of($caster, CastsAttributes::class)) {
+ $ref = self::getReflector()->reflect($caster);
+ $method = $ref->getMethod('get');
+ if ($ast = $method->getReturnStatementsAst()[0]) {
+ if ($ast instanceof Node\Stmt\Return_
+ && $ast->expr instanceof Node\Expr\New_
+ && $ast->expr->class instanceof Node\Name\FullyQualified
+ ) {
+ $this->setProperty($key, [$ast->expr->class->toCodeString()], true, true);
+ }
+ }
+ }
+ }
}
protected function setProperty(string $name, array $type = null, bool $read = null, bool $write = null, string $comment = '', bool $nullable = false)
diff --git a/src/database/src/Commands/ModelCommand.php b/src/database/src/Commands/ModelCommand.php
index 5bf05c3ac..7d6babf3b 100644
--- a/src/database/src/Commands/ModelCommand.php
+++ b/src/database/src/Commands/ModelCommand.php
@@ -28,6 +28,7 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
class ModelCommand extends Command
{
@@ -57,7 +58,7 @@ class ModelCommand extends Command
protected $printer;
/**
- * @var OutputInterface
+ * @var SymfonyStyle
*/
protected $output;
@@ -118,7 +119,7 @@ class ModelCommand extends Command
$this->addOption('prefix', 'P', InputOption::VALUE_OPTIONAL, 'What prefix that you want the Model set.');
$this->addOption('inheritance', 'i', InputOption::VALUE_OPTIONAL, 'The inheritance that you want the Model extends.');
$this->addOption('uses', 'U', InputOption::VALUE_OPTIONAL, 'The default class uses of the Model.');
- $this->addOption('refresh-fillable', null, InputOption::VALUE_NONE, 'Whether generate fillable argement for model.');
+ $this->addOption('refresh-fillable', 'R', InputOption::VALUE_NONE, 'Whether generate fillable argement for model.');
$this->addOption('table-mapping', 'M', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Table mappings for model.');
$this->addOption('ignore-tables', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Ignore tables for creating models.');
$this->addOption('with-comments', null, InputOption::VALUE_NONE, 'Whether generate the property comments for model.');
diff --git a/src/database/src/Model/Builder.php b/src/database/src/Model/Builder.php
index 346161a12..92ebdf080 100755
--- a/src/database/src/Model/Builder.php
+++ b/src/database/src/Model/Builder.php
@@ -975,6 +975,19 @@ class Builder
);
}
+ /**
+ * Apply query-time casts to the model instance.
+ *
+ * @param array $casts
+ * @return $this
+ */
+ public function withCasts($casts)
+ {
+ $this->model->mergeCasts($casts);
+
+ return $this;
+ }
+
/**
* Get the underlying query builder instance.
*
diff --git a/src/database/src/Model/CastsValue.php b/src/database/src/Model/CastsValue.php
new file mode 100644
index 000000000..96411fb05
--- /dev/null
+++ b/src/database/src/Model/CastsValue.php
@@ -0,0 +1,62 @@
+model = $model;
+ $this->items = $itmes;
+ }
+
+ public function __get($name)
+ {
+ return $this->items[$name];
+ }
+
+ public function __set($name, $value)
+ {
+ $this->items[$name] = $value;
+ $this->isSynchronized = false;
+ $this->model->syncAttributes();
+ $this->isSynchronized = true;
+ }
+
+ public function isSynchronized(): bool
+ {
+ return $this->isSynchronized;
+ }
+
+ public function toArray(): array
+ {
+ return $this->items;
+ }
+}
diff --git a/src/database/src/Model/Concerns/HasAttributes.php b/src/database/src/Model/Concerns/HasAttributes.php
index b616bf141..718cd319a 100644
--- a/src/database/src/Model/Concerns/HasAttributes.php
+++ b/src/database/src/Model/Concerns/HasAttributes.php
@@ -14,6 +14,10 @@ namespace Hyperf\Database\Model\Concerns;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use DateTimeInterface;
+use Hyperf\Contract\Castable;
+use Hyperf\Contract\CastsAttributes;
+use Hyperf\Contract\CastsInboundAttributes;
+use Hyperf\Contract\Synchronized;
use Hyperf\Database\Model\JsonEncodingException;
use Hyperf\Database\Model\Relations\Relation;
use Hyperf\Utils\Arr;
@@ -53,12 +57,44 @@ trait HasAttributes
protected $changes = [];
/**
- * The attributes that should be cast to native types.
+ * The attributes that should be cast.
*
* @var array
*/
protected $casts = [];
+ /**
+ * The attributes that have been cast using custom classes.
+ *
+ * @var array
+ */
+ protected $classCastCache = [];
+
+ /**
+ * The built-in, primitive cast types supported.
+ *
+ * @var array
+ */
+ protected static $primitiveCastTypes = [
+ 'array',
+ 'bool',
+ 'boolean',
+ 'collection',
+ 'custom_datetime',
+ 'date',
+ 'datetime',
+ 'decimal',
+ 'double',
+ 'float',
+ 'int',
+ 'integer',
+ 'json',
+ 'object',
+ 'real',
+ 'string',
+ 'timestamp',
+ ];
+
/**
* The attributes that should be mutated to dates.
*
@@ -182,8 +218,9 @@ trait HasAttributes
// If the attribute exists in the attribute array or has a "get" mutator we will
// get the attribute's value. Otherwise, we will proceed as if the developers
// are asking for a relationship's value. This covers both types of values.
- if (array_key_exists($key, $this->attributes) ||
- $this->hasGetMutator($key)) {
+ if (array_key_exists($key, $this->getAttributes()) ||
+ $this->hasGetMutator($key) ||
+ $this->isClassCastable($key)) {
return $this->getAttributeValue($key);
}
@@ -204,31 +241,7 @@ trait HasAttributes
*/
public function getAttributeValue($key)
{
- $value = $this->getAttributeFromArray($key);
-
- // If the attribute has a get mutator, we will call that then return what
- // it returns as the value, which is useful for transforming values on
- // retrieval from the model to a form that is more useful for usage.
- if ($this->hasGetMutator($key)) {
- return $this->mutateAttribute($key, $value);
- }
-
- // If the attribute exists within the cast array, we will convert it to
- // an appropriate native PHP type dependant upon the associated value
- // given with the key in the pair. Dayle made this comment line up.
- if ($this->hasCast($key)) {
- return $this->castAttribute($key, $value);
- }
-
- // If the attribute is listed as a date, we will convert it to a DateTime
- // instance on retrieval, which makes it quite convenient to work with
- // date fields without having to create a mutator for each property.
- if (in_array($key, $this->getDates()) &&
- ! is_null($value)) {
- return $this->asDateTime($value);
- }
-
- return $value;
+ return $this->transformModelValue($key, $this->getAttributeFromArray($key));
}
/**
@@ -264,6 +277,16 @@ trait HasAttributes
return method_exists($this, 'get' . Str::studly($key) . 'Attribute');
}
+ /**
+ * Merge new casts with existing casts on the model.
+ *
+ * @param array $casts
+ */
+ public function mergeCasts($casts)
+ {
+ $this->casts = array_merge($this->casts, $casts);
+ }
+
/**
* Set a given attribute on the model.
*
@@ -286,6 +309,12 @@ trait HasAttributes
$value = $this->fromDateTime($value);
}
+ if ($this->isClassCastable($key)) {
+ $this->setClassCastableAttribute($key, $value);
+
+ return $this;
+ }
+
if ($this->isJsonCastable($key) && ! is_null($value)) {
$value = $this->castAttributeAsJson($key, $value);
}
@@ -385,8 +414,8 @@ trait HasAttributes
$defaults = [static::CREATED_AT, static::UPDATED_AT];
return $this->usesTimestamps()
- ? array_unique(array_merge($this->dates, $defaults))
- : $this->dates;
+ ? array_unique(array_merge($this->dates, $defaults))
+ : $this->dates;
}
/**
@@ -452,6 +481,15 @@ trait HasAttributes
return $this->attributes;
}
+ /**
+ * @return $this
+ */
+ public function syncAttributes()
+ {
+ $this->mergeAttributesFromClassCasts();
+ return $this;
+ }
+
/**
* Set the array of model attributes. No checking is done.
*
@@ -466,6 +504,8 @@ trait HasAttributes
$this->syncOriginal();
}
+ $this->classCastCache = [];
+
return $this;
}
@@ -478,7 +518,16 @@ trait HasAttributes
*/
public function getOriginal($key = null, $default = null)
{
- return Arr::get($this->original, $key, $default);
+ if ($key) {
+ return $this->transformModelValue(
+ $key,
+ Arr::get($this->original, $key, $default)
+ );
+ }
+
+ return collect($this->original)->mapWithKeys(function ($value, $key) {
+ return [$key => $this->transformModelValue($key, $value)];
+ })->all();
}
/**
@@ -505,7 +554,7 @@ trait HasAttributes
*/
public function syncOriginal()
{
- $this->original = $this->attributes;
+ $this->original = $this->getAttributes();
return $this;
}
@@ -531,8 +580,10 @@ trait HasAttributes
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
+ $modelAttributes = $this->getAttributes();
+
foreach ($attributes as $attribute) {
- $this->original[$attribute] = $this->attributes[$attribute];
+ $this->original[$attribute] = $modelAttributes[$attribute];
}
return $this;
@@ -630,25 +681,28 @@ trait HasAttributes
return false;
}
- $original = $this->getOriginal($key);
+ $original = Arr::get($this->original, $key);
if ($current === $original) {
return true;
}
+
if (is_null($current)) {
return false;
}
+
if ($this->isDateAttribute($key)) {
return $this->fromDateTime($current) ===
- $this->fromDateTime($original);
+ $this->fromDateTime($original);
}
- if ($this->hasCast($key)) {
+
+ if ($this->hasCast($key, static::$primitiveCastTypes)) {
return $this->castAttribute($key, $current) ===
- $this->castAttribute($key, $original);
+ $this->castAttribute($key, $original);
}
return is_numeric($current) && is_numeric($original)
- && strcmp((string) $current, (string) $original) === 0;
+ && strcmp((string) $current, (string) $original) === 0;
}
/**
@@ -784,6 +838,10 @@ trait HasAttributes
if ($attributes[$key] && $this->isCustomDateTimeCast($value)) {
$attributes[$key] = $attributes[$key]->format(explode(':', $value, 2)[1]);
}
+
+ if ($attributes[$key] instanceof Arrayable) {
+ $attributes[$key] = $attributes[$key]->toArray();
+ }
}
return $attributes;
@@ -796,7 +854,9 @@ trait HasAttributes
*/
protected function getArrayableAttributes()
{
- return $this->getArrayableItems($this->attributes);
+ $this->syncAttributes();
+
+ return $this->getArrayableItems($this->getAttributes());
}
/**
@@ -850,9 +910,7 @@ trait HasAttributes
*/
protected function getAttributeFromArray($key)
{
- if (isset($this->attributes[$key])) {
- return $this->attributes[$key];
- }
+ return $this->getAttributes()[$key] ?? null;
}
/**
@@ -867,6 +925,14 @@ trait HasAttributes
$relation = $this->{$method}();
if (! $relation instanceof Relation) {
+ if (is_null($relation)) {
+ throw new LogicException(sprintf(
+ '%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?',
+ static::class,
+ $method
+ ));
+ }
+
throw new LogicException(sprintf(
'%s::%s must return a relationship instance.',
static::class,
@@ -898,7 +964,9 @@ trait HasAttributes
*/
protected function mutateAttributeForArray($key, $value)
{
- $value = $this->mutateAttribute($key, $value);
+ $value = $this->isClassCastable($key)
+ ? $this->getClassCastableAttributeValue($key, $value)
+ : $this->mutateAttribute($key, $value);
return $value instanceof Arrayable ? $value->toArray() : $value;
}
@@ -911,11 +979,13 @@ trait HasAttributes
*/
protected function castAttribute($key, $value)
{
- if (is_null($value)) {
+ $castType = $this->getCastType($key);
+
+ if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) {
return $value;
}
- switch ($this->getCastType($key)) {
+ switch ($castType) {
case 'int':
case 'integer':
return (int) $value;
@@ -944,9 +1014,40 @@ trait HasAttributes
return $this->asDateTime($value);
case 'timestamp':
return $this->asTimestamp($value);
- default:
- return $value;
}
+
+ if ($this->isClassCastable($key)) {
+ return $this->getClassCastableAttributeValue($key, $value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Cast the given attribute using a custom cast class.
+ *
+ * @param string $key
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function getClassCastableAttributeValue($key, $value)
+ {
+ if (isset($this->classCastCache[$key])) {
+ return $this->classCastCache[$key];
+ }
+ $caster = $this->resolveCasterClass($key);
+
+ $value = $caster instanceof CastsInboundAttributes
+ ? $value
+ : $caster->get($this, $key, $value, $this->attributes);
+
+ if ($caster instanceof CastsInboundAttributes || ! is_object($value)) {
+ unset($this->classCastCache[$key]);
+ } else {
+ $this->classCastCache[$key] = $value;
+ }
+
+ return $value;
}
/**
@@ -977,7 +1078,7 @@ trait HasAttributes
protected function isCustomDateTimeCast($cast)
{
return strncmp($cast, 'date:', 5) === 0 ||
- strncmp($cast, 'datetime:', 9) === 0;
+ strncmp($cast, 'datetime:', 9) === 0;
}
/**
@@ -1010,8 +1111,48 @@ trait HasAttributes
*/
protected function isDateAttribute($key)
{
- return in_array($key, $this->getDates()) ||
- $this->isDateCastable($key);
+ return in_array($key, $this->getDates(), true) ||
+ $this->isDateCastable($key);
+ }
+
+ /**
+ * Set the value of a class castable attribute.
+ *
+ * @param string $key
+ * @param mixed $value
+ */
+ protected function setClassCastableAttribute($key, $value)
+ {
+ $caster = $this->resolveCasterClass($key);
+
+ if (is_null($value)) {
+ $this->attributes = array_merge($this->attributes, array_map(
+ function () {
+ },
+ $this->normalizeCastClassResponse($key, $caster->set(
+ $this,
+ $key,
+ $this->{$key},
+ $this->attributes
+ ))
+ ));
+ } else {
+ $this->attributes = array_merge(
+ $this->attributes,
+ $this->normalizeCastClassResponse($key, $caster->set(
+ $this,
+ $key,
+ $value,
+ $this->attributes
+ ))
+ );
+ }
+
+ if ($caster instanceof CastsInboundAttributes || ! is_object($value)) {
+ unset($this->classCastCache[$key]);
+ } else {
+ $this->classCastCache[$key] = $value;
+ }
}
/**
@@ -1038,7 +1179,7 @@ trait HasAttributes
protected function getArrayAttributeByKey($key)
{
return isset($this->attributes[$key]) ?
- $this->fromJson($this->attributes[$key]) : [];
+ $this->fromJson($this->attributes[$key]) : [];
}
/**
@@ -1136,13 +1277,16 @@ trait HasAttributes
return Carbon::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay());
}
+ $format = $this->getDateFormat();
+
// Finally, we will just assume this date is in the format used by default on
// the database connection and use that format to create the Carbon object
// that is returned back out to the developers after we convert it here.
- return Carbon::createFromFormat(
- str_replace('.v', '.u', $this->getDateFormat()),
- $value
- );
+ if (Carbon::hasFormat($value, $format)) {
+ return Carbon::createFromFormat($format, $value);
+ }
+
+ return Carbon::parse($value);
}
/**
@@ -1200,7 +1344,96 @@ trait HasAttributes
}
/**
- * Determine if the given attributes were changed.
+ * Determine if the given key is cast using a custom class.
+ *
+ * @param string $key
+ * @return bool
+ */
+ protected function isClassCastable($key)
+ {
+ return array_key_exists($key, $this->getCasts()) &&
+ class_exists($class = $this->parseCasterClass($this->getCasts()[$key])) &&
+ ! in_array($class, static::$primitiveCastTypes);
+ }
+
+ /**
+ * Resolve the custom caster class for a given key.
+ *
+ * @param string $key
+ * @return CastsAttributes|CastsInboundAttributes
+ */
+ protected function resolveCasterClass($key)
+ {
+ $castType = $this->getCasts()[$key];
+
+ $arguments = [];
+
+ if (is_string($castType) && strpos($castType, ':') !== false) {
+ $segments = explode(':', $castType, 2);
+
+ $castType = $segments[0];
+ $arguments = explode(',', $segments[1]);
+ }
+
+ if (is_subclass_of($castType, Castable::class)) {
+ $castType = $castType::castUsing();
+ }
+
+ if (is_object($castType)) {
+ return $castType;
+ }
+
+ return new $castType(...$arguments);
+ }
+
+ /**
+ * Parse the given caster class, removing any arguments.
+ *
+ * @param string $class
+ * @return string
+ */
+ protected function parseCasterClass($class)
+ {
+ return strpos($class, ':') === false
+ ? $class
+ : explode(':', $class, 2)[0];
+ }
+
+ /**
+ * Merge the cast class attributes back into the model.
+ */
+ protected function mergeAttributesFromClassCasts()
+ {
+ foreach ($this->classCastCache as $key => $value) {
+ if ($value instanceof Synchronized && $value->isSynchronized()) {
+ continue;
+ }
+
+ $caster = $this->resolveCasterClass($key);
+
+ $this->attributes = array_merge(
+ $this->attributes,
+ $caster instanceof CastsInboundAttributes
+ ? [$key => $value]
+ : $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes))
+ );
+ }
+ }
+
+ /**
+ * Normalize the response from a custom class caster.
+ *
+ * @param string $key
+ * @param mixed $value
+ * @return array
+ */
+ protected function normalizeCastClassResponse($key, $value)
+ {
+ return is_array($value) ? $value : [$key => $value];
+ }
+
+ /**
+ * Determine if any of the given attributes were changed.
*
* @param array $changes
* @param null|array|string $attributes
@@ -1227,6 +1460,40 @@ trait HasAttributes
return false;
}
+ /**
+ * Transform a raw model value using mutators, casts, etc.
+ *
+ * @param string $key
+ * @param mixed $value
+ * @return mixed
+ */
+ protected function transformModelValue($key, $value)
+ {
+ // If the attribute has a get mutator, we will call that then return what
+ // it returns as the value, which is useful for transforming values on
+ // retrieval from the model to a form that is more useful for usage.
+ if ($this->hasGetMutator($key)) {
+ return $this->mutateAttribute($key, $value);
+ }
+
+ // If the attribute exists within the cast array, we will convert it to
+ // an appropriate native PHP type dependent upon the associated value
+ // given with the key in the pair. Dayle made this comment line up.
+ if ($this->hasCast($key)) {
+ return $this->castAttribute($key, $value);
+ }
+
+ // If the attribute is listed as a date, we will convert it to a DateTime
+ // instance on retrieval, which makes it quite convenient to work with
+ // date fields without having to create a mutator for each property.
+ if ($value !== null
+ && \in_array($key, $this->getDates(), false)) {
+ return $this->asDateTime($value);
+ }
+
+ return $value;
+ }
+
/**
* Get all of the attribute mutator methods.
*
diff --git a/src/database/src/Model/Concerns/QueriesRelationships.php b/src/database/src/Model/Concerns/QueriesRelationships.php
index 8bc1c3385..862d70054 100644
--- a/src/database/src/Model/Concerns/QueriesRelationships.php
+++ b/src/database/src/Model/Concerns/QueriesRelationships.php
@@ -25,7 +25,7 @@ trait QueriesRelationships
/**
* Add a relationship count / exists condition to the query.
*
- * @param string $relation
+ * @param Relation|string $relation
* @param string $operator
* @param int $count
* @param string $boolean
@@ -245,13 +245,48 @@ trait QueriesRelationships
* @param array|string $types
* @param string $operator
* @param int $count
- * @return mixed
+ * @return $this
*/
public function whereHasMorph($relation, $types, Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->hasMorph($relation, $types, $operator, $count, 'and', $callback);
}
+ /**
+ * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
+ *
+ * @param array|string $types
+ * @param \Closure $callback
+ * @return $this
+ */
+ public function orWhereHasMorph(string $relation, $types, Closure $callback = null, string $operator = '>=', int $count = 1)
+ {
+ return $this->hasMorph($relation, $types, $operator, $count, 'or', $callback);
+ }
+
+ /**
+ * Add a polymorphic relationship count / exists condition to the query with where clauses.
+ *
+ * @param array|string $types
+ * @return $this
+ */
+ public function whereDoesntHaveMorph(string $relation, $types, Closure $callback = null)
+ {
+ return $this->doesntHaveMorph($relation, $types, 'and', $callback);
+ }
+
+ /**
+ * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or".
+ *
+ * @param array|string $types
+ * @param \Closure $callback
+ * @return $this
+ */
+ public function orWhereDoesntHaveMorph(string $relation, $types, Closure $callback = null)
+ {
+ return $this->doesntHaveMorph($relation, $types, 'or', $callback);
+ }
+
/**
* Add a polymorphic relationship count / exists condition to the query.
*
@@ -260,7 +295,7 @@ trait QueriesRelationships
* @param string $operator
* @param int $count
* @param string $boolean
- * @return mixed
+ * @return $this
*/
public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
{
@@ -269,7 +304,7 @@ trait QueriesRelationships
$types = (array) $types;
if ($types === ['*']) {
- $types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())->all();
+ $types = $this->model->newModelQuery()->distinct()->pluck($relation->getMorphType())->filter()->all();
foreach ($types as &$type) {
$type = Relation::getMorphedModel($type) ?? $type;
@@ -294,6 +329,18 @@ trait QueriesRelationships
}, null, null, $boolean);
}
+ /**
+ * Add a polymorphic relationship count / exists condition to the query.
+ *
+ * @param array|string $types
+ * @param string $boolean
+ * @return $this
+ */
+ public function doesntHaveMorph(string $relation, $types, $boolean = 'and', Closure $callback = null)
+ {
+ return $this->hasMorph($relation, $types, '<', 1, $boolean, $callback);
+ }
+
/**
* Add nested relationship count / exists conditions to the query.
*
diff --git a/src/database/src/Model/Model.php b/src/database/src/Model/Model.php
index 492d216e8..79ede6901 100644
--- a/src/database/src/Model/Model.php
+++ b/src/database/src/Model/Model.php
@@ -218,6 +218,20 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
return $this->toJson();
}
+ /**
+ * Prepare the object for serialization.
+ *
+ * @return array
+ */
+ public function __sleep()
+ {
+ $this->mergeAttributesFromClassCasts();
+
+ $this->classCastCache = [];
+
+ return array_keys(get_object_vars($this));
+ }
+
/**
* When a model is being unserialized, check if it needs to be booted.
*/
@@ -340,6 +354,8 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
$model->setTable($this->getTable());
+ $model->mergeCasts($this->casts);
+
return $model;
}
@@ -428,6 +444,22 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
return $this;
}
+ /**
+ * Eager load relationships on the polymorphic relation of a model.
+ *
+ * @param string $relation
+ * @param array $relations
+ * @return $this
+ */
+ public function loadMorph($relation, $relations)
+ {
+ $className = get_class($this->{$relation});
+
+ $this->{$relation}->load($relations[$className] ?? []);
+
+ return $this;
+ }
+
/**
* Eager load relations on the model if they are not already eager loaded.
*
@@ -458,6 +490,22 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
return $this;
}
+ /**
+ * Eager load relationship counts on the polymorphic relation of a model.
+ *
+ * @param string $relation
+ * @param array $relations
+ * @return $this
+ */
+ public function loadMorphCount($relation, $relations)
+ {
+ $className = get_class($this->{$relation});
+
+ $this->{$relation}->loadCount($relations[$className] ?? []);
+
+ return $this;
+ }
+
/**
* Update the model in the database.
*
@@ -504,6 +552,8 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
*/
public function save(array $options = []): bool
{
+ $this->mergeAttributesFromClassCasts();
+
$query = $this->newModelQuery();
// If the "saving" event returns false we'll bail out of the save and return
@@ -595,6 +645,8 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
*/
public function delete()
{
+ $this->mergeAttributesFromClassCasts();
+
if (is_null($this->getKeyName())) {
throw new Exception('No primary key defined on model.');
}
@@ -848,7 +900,7 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
$this->getUpdatedAtColumn(),
];
- $attributes = Arr::except($this->attributes, $except ? array_unique(array_merge($except, $defaults)) : $defaults);
+ $attributes = Arr::except($this->getAttributes(), $except ? array_unique(array_merge($except, $defaults)) : $defaults);
return tap(new static(), function ($instance) use ($attributes) {
// @var \Hyperf\Database\Model\Model $instance
@@ -927,7 +979,7 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
*/
public function getTable()
{
- return isset($this->table) ? $this->table : Str::snake(Str::pluralStudly(class_basename($this)));
+ return $this->table ?? Str::snake(Str::pluralStudly(class_basename($this)));
}
/**
diff --git a/src/database/tests/DatabaseModelCustomCastingTest.php b/src/database/tests/DatabaseModelCustomCastingTest.php
new file mode 100644
index 000000000..dc05010ed
--- /dev/null
+++ b/src/database/tests/DatabaseModelCustomCastingTest.php
@@ -0,0 +1,422 @@
+uppercase = 'taylor';
+
+ $this->assertSame('TAYLOR', $model->uppercase);
+ $this->assertSame('TAYLOR', $model->getAttributes()['uppercase']);
+ $this->assertSame('TAYLOR', $model->toArray()['uppercase']);
+
+ $unserializedModel = unserialize(serialize($model));
+
+ $this->assertSame('TAYLOR', $unserializedModel->uppercase);
+ $this->assertSame('TAYLOR', $unserializedModel->getAttributes()['uppercase']);
+ $this->assertSame('TAYLOR', $unserializedModel->toArray()['uppercase']);
+
+ $model->syncOriginal();
+ $model->uppercase = 'dries';
+ $this->assertEquals('TAYLOR', $model->getOriginal('uppercase'));
+
+ $model = new TestModelWithCustomCast();
+ $model->uppercase = 'taylor';
+ $model->syncOriginal();
+ $model->uppercase = 'dries';
+ $model->getOriginal();
+
+ $this->assertEquals('DRIES', $model->uppercase);
+
+ $model = new TestModelWithCustomCast();
+
+ $model->address = $address = new Address('110 Kingsbrook St.', 'My Childhood House');
+ $address->lineOne = '117 Spencer St.';
+ $this->assertSame('117 Spencer St.', $model->syncAttributes()->getAttributes()['address_line_one']);
+
+ $model = new TestModelWithCustomCast();
+
+ $model->setRawAttributes([
+ 'address_line_one' => '110 Kingsbrook St.',
+ 'address_line_two' => 'My Childhood House',
+ ]);
+
+ $this->assertSame('110 Kingsbrook St.', $model->address->lineOne);
+ $this->assertSame('My Childhood House', $model->address->lineTwo);
+
+ $this->assertSame('110 Kingsbrook St.', $model->toArray()['address_line_one']);
+ $this->assertSame('My Childhood House', $model->toArray()['address_line_two']);
+
+ $model->address->lineOne = '117 Spencer St.';
+
+ $this->assertFalse(isset($model->toArray()['address']));
+ $this->assertSame('117 Spencer St.', $model->toArray()['address_line_one']);
+ $this->assertSame('My Childhood House', $model->toArray()['address_line_two']);
+
+ $this->assertSame('117 Spencer St.', json_decode($model->toJson(), true)['address_line_one']);
+ $this->assertSame('My Childhood House', json_decode($model->toJson(), true)['address_line_two']);
+
+ $model->address = null;
+
+ $this->assertNull($model->toArray()['address_line_one']);
+ $this->assertNull($model->toArray()['address_line_two']);
+
+ $model->options = ['foo' => 'bar'];
+ $this->assertEquals(['foo' => 'bar'], $model->options);
+ $this->assertEquals(['foo' => 'bar'], $model->options);
+ $model->options = ['foo' => 'bar'];
+ $model->options = ['foo' => 'bar'];
+ $this->assertEquals(['foo' => 'bar'], $model->options);
+ $this->assertEquals(['foo' => 'bar'], $model->options);
+
+ $this->assertEquals(json_encode(['foo' => 'bar']), $model->getAttributes()['options']);
+
+ $model = new TestModelWithCustomCast(['options' => []]);
+ $model->syncOriginal();
+ $model->options = ['foo' => 'bar'];
+ $this->assertTrue($model->isDirty('options'));
+ }
+
+ public function testOneWayCasting()
+ {
+ // CastsInboundAttributes is used for casting that is unidirectional... only use case I can think of is one-way hashing...
+ $model = new TestModelWithCustomCast();
+
+ $model->password = 'secret';
+
+ $this->assertEquals(hash('sha256', 'secret'), $model->password);
+ $this->assertEquals(hash('sha256', 'secret'), $model->getAttributes()['password']);
+ $this->assertEquals(hash('sha256', 'secret'), $model->getAttributes()['password']);
+ $this->assertEquals(hash('sha256', 'secret'), $model->password);
+
+ $model->password = 'secret2';
+
+ $this->assertEquals(hash('sha256', 'secret2'), $model->password);
+ $this->assertEquals(hash('sha256', 'secret2'), $model->getAttributes()['password']);
+ $this->assertEquals(hash('sha256', 'secret2'), $model->getAttributes()['password']);
+ $this->assertEquals(hash('sha256', 'secret2'), $model->password);
+ }
+
+ public function testSettingRawAttributesClearsTheCastCache()
+ {
+ $model = new TestModelWithCustomCast();
+
+ $model->setRawAttributes([
+ 'address_line_one' => '110 Kingsbrook St.',
+ 'address_line_two' => 'My Childhood House',
+ ]);
+
+ $this->assertSame('110 Kingsbrook St.', $model->address->lineOne);
+
+ $model->setRawAttributes([
+ 'address_line_one' => '117 Spencer St.',
+ 'address_line_two' => 'My Childhood House',
+ ]);
+
+ $this->assertSame('117 Spencer St.', $model->address->lineOne);
+ }
+
+ public function testWithCastableInterface()
+ {
+ $model = new TestModelWithCustomCast();
+
+ $model->setRawAttributes([
+ 'value_object_with_caster' => serialize(new ValueObject('hello')),
+ ]);
+
+ $this->assertInstanceOf(ValueObject::class, $model->value_object_with_caster);
+
+ $model->setRawAttributes([
+ 'value_object_caster_with_argument' => null,
+ ]);
+
+ $this->assertEquals('argument', $model->value_object_caster_with_argument);
+
+ $model->setRawAttributes([
+ 'value_object_caster_with_caster_instance' => serialize(new ValueObject('hello')),
+ ]);
+
+ $this->assertInstanceOf(ValueObject::class, $model->value_object_caster_with_caster_instance);
+ }
+
+ public function testGetAttribute()
+ {
+ $model = new TestModelWithCustomCast();
+ $model->mergeCasts([
+ 'mockery' => MockeryAttribute::class,
+ ]);
+ $mockery = \Mockery::mock(CastsAttributes::class);
+ $mockery->shouldReceive('get')->withAnyArgs()->andReturn(function ($_, $key, $value, $attributes) {
+ $obj = new \stdClass();
+ $obj->value = $attributes[$key . '_origin'] - 1;
+
+ return $obj;
+ });
+ $mockery->shouldReceive('set')->withAnyArgs()->once()->andReturnUsing(function ($_, $key, $value, $attributes) {
+ return [
+ $key . '_origin' => $value->value + 1,
+ ];
+ });
+ MockeryAttribute::$attribute = $mockery;
+
+ $std = new \stdClass();
+ $std->value = 1;
+ $model->mockery = $std;
+
+ $this->assertSame(1, $model->mockery->value);
+ }
+
+ public function testResolveCasterClass()
+ {
+ $model = new TestModelWithCustomCast();
+ $ref = new \ReflectionClass($model);
+ $method = $ref->getMethod('resolveCasterClass');
+ $method->setAccessible(true);
+ CastUsing::$castsAttributes = UppercaseCaster::class;
+ $this->assertNotSame($method->invokeArgs($model, ['cast_using']), $method->invokeArgs($model, ['cast_using']));
+
+ CastUsing::$castsAttributes = new UppercaseCaster();
+ $this->assertSame($method->invokeArgs($model, ['cast_using']), $method->invokeArgs($model, ['cast_using']));
+ }
+
+ public function testIsSynchronized()
+ {
+ $model = new TestModelWithCustomCast();
+ $model->user = $user = new UserInfo($model, ['name' => 'Hyperf', 'gender' => 1]);
+ $model->syncOriginal();
+
+ $attributes = $model->getAttributes();
+ $this->assertSame(['name' => 'Hyperf', 'gender' => 1], $attributes);
+
+ $user->name = 'Nano';
+ $attributes = $model->getAttributes();
+ $this->assertSame(['name' => 'Nano', 'gender' => 1], $attributes);
+
+ $this->assertSame(['name' => 'Nano'], $model->getDirty());
+ $this->assertSame(2, UserInfoCaster::$setCount);
+ $this->assertSame(0, UserInfoCaster::$getCount);
+ }
+}
+
+class TestModelWithCustomCast extends Model
+{
+ /**
+ * The attributes that aren't mass assignable.
+ *
+ * @var array
+ */
+ protected $guarded = [];
+
+ /**
+ * The attributes that should be cast to native types.
+ *
+ * @var array
+ */
+ protected $casts = [
+ 'address' => AddressCaster::class,
+ 'user' => UserInfoCaster::class,
+ 'password' => HashCaster::class,
+ 'other_password' => HashCaster::class . ':md5',
+ 'uppercase' => UppercaseCaster::class,
+ 'options' => JsonCaster::class,
+ 'value_object_with_caster' => ValueObject::class,
+ 'value_object_caster_with_argument' => ValueObject::class . ':argument',
+ 'value_object_caster_with_caster_instance' => ValueObjectWithCasterInstance::class,
+ 'cast_using' => CastUsing::class,
+ ];
+}
+
+class CastUsing implements Castable
+{
+ /**
+ * @var CastsAttributes
+ */
+ public static $castsAttributes;
+
+ public static function castUsing()
+ {
+ return self::$castsAttributes;
+ }
+}
+
+class HashCaster implements CastsInboundAttributes
+{
+ public function __construct($algorithm = 'sha256')
+ {
+ $this->algorithm = $algorithm;
+ }
+
+ public function set($model, $key, $value, $attributes)
+ {
+ return [$key => hash($this->algorithm, $value)];
+ }
+}
+
+class UppercaseCaster implements CastsAttributes
+{
+ public function get($model, $key, $value, $attributes)
+ {
+ return strtoupper($value);
+ }
+
+ public function set($model, $key, $value, $attributes)
+ {
+ return [$key => strtoupper($value)];
+ }
+}
+
+class AddressCaster implements CastsAttributes
+{
+ public function get($model, $key, $value, $attributes)
+ {
+ return new Address($attributes['address_line_one'], $attributes['address_line_two']);
+ }
+
+ public function set($model, $key, $value, $attributes)
+ {
+ return ['address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo];
+ }
+}
+
+class UserInfoCaster implements CastsAttributes
+{
+ public static $setCount = 0;
+
+ public static $getCount = 0;
+
+ public function get($model, string $key, $value, array $attributes)
+ {
+ ++self::$getCount;
+ return new UserInfo($model, Arr::only($attributes, ['name', 'gender']));
+ }
+
+ public function set($model, string $key, $value, array $attributes)
+ {
+ ++self::$setCount;
+ return [
+ 'name' => $value->name,
+ 'gender' => $value->gender,
+ ];
+ }
+}
+
+class JsonCaster implements CastsAttributes
+{
+ public function get($model, $key, $value, $attributes)
+ {
+ return json_decode($value, true);
+ }
+
+ public function set($model, $key, $value, $attributes)
+ {
+ return json_encode($value);
+ }
+}
+
+class ValueObjectCaster implements CastsAttributes
+{
+ private $argument;
+
+ public function __construct($argument = null)
+ {
+ $this->argument = $argument;
+ }
+
+ public function get($model, $key, $value, $attributes)
+ {
+ if ($this->argument) {
+ return $this->argument;
+ }
+
+ return unserialize($value);
+ }
+
+ public function set($model, $key, $value, $attributes)
+ {
+ return serialize($value);
+ }
+}
+
+class MockeryAttribute implements CastsAttributes
+{
+ /**
+ * @var CastsAttributes
+ */
+ public static $attribute;
+
+ public function get($model, string $key, $value, array $attributes)
+ {
+ return self::$attribute->get($model, $key, $value, $attributes);
+ }
+
+ public function set($model, string $key, $value, array $attributes)
+ {
+ return self::$attribute->set($model, $key, $value, $attributes);
+ }
+}
+
+class ValueObject implements Castable
+{
+ public static function castUsing()
+ {
+ return ValueObjectCaster::class;
+ }
+}
+
+class ValueObjectWithCasterInstance extends ValueObject
+{
+ public static function castUsing()
+ {
+ return new ValueObjectCaster();
+ }
+}
+
+class Address
+{
+ public $lineOne;
+
+ public $lineTwo;
+
+ public function __construct($lineOne, $lineTwo)
+ {
+ $this->lineOne = $lineOne;
+ $this->lineTwo = $lineTwo;
+ }
+}
+
+/**
+ * @property string $name
+ * @property int $gender
+ */
+class UserInfo extends CastsValue
+{
+}
diff --git a/src/database/tests/ModelBuilderTest.php b/src/database/tests/ModelBuilderTest.php
index 458687240..263a0d0d6 100755
--- a/src/database/tests/ModelBuilderTest.php
+++ b/src/database/tests/ModelBuilderTest.php
@@ -1158,6 +1158,16 @@ class ModelBuilderTest extends TestCase
Carbon::setTestNow(null);
}
+ public function testWithCastsMethod()
+ {
+ $builder = new Builder($this->getMockQueryBuilder());
+ $model = $this->getMockModel();
+ $builder->setModel($model);
+
+ $model->shouldReceive('mergeCasts')->with(['foo' => 'bar'])->once();
+ $builder->withCasts(['foo' => 'bar']);
+ }
+
protected function mockConnectionForModel($model, $database)
{
$grammarClass = 'Hyperf\Database\Query\Grammars\\' . $database . 'Grammar';
diff --git a/src/database/tests/ModelMorphEagerLoadingTest.php b/src/database/tests/ModelMorphEagerLoadingTest.php
index d1f63126e..09da2f72e 100644
--- a/src/database/tests/ModelMorphEagerLoadingTest.php
+++ b/src/database/tests/ModelMorphEagerLoadingTest.php
@@ -98,6 +98,20 @@ class ModelMorphEagerLoadingTest extends TestCase
}
}
+ public function testMorphAssociationEmpty()
+ {
+ $this->getContainer();
+ $images = Image::query()->whereHasMorph(
+ 'imageable',
+ ['*'],
+ function (Builder $query) {
+ $query->where('imageable_id', 1);
+ }
+ )->get();
+
+ $this->assertSame(2, $images->count());
+ }
+
public function testWhereHasMorph()
{
$this->getContainer();
@@ -123,6 +137,117 @@ class ModelMorphEagerLoadingTest extends TestCase
}
}
+ public function testOrWhereHasMorph()
+ {
+ $this->getContainer();
+ $images = Image::query()
+ ->whereHasMorph(
+ 'imageable',
+ [
+ User::class,
+ ],
+ function (Builder $query) {
+ $query->where('id', '=', 1);
+ }
+ )
+ ->orWhereHasMorph(
+ 'imageable',
+ [
+ Book::class,
+ ],
+ function (Builder $query) {
+ $query->where('id', '=', 1);
+ }
+ )->get();
+ $this->assertSame(1, $images[0]->imageable->id);
+ $this->assertSame(1, $images[1]->imageable->id);
+ $sqls = [
+ ['select * from `images` where ((`imageable_type` = ? and exists (select * from `user` where `images`.`imageable_id` = `user`.`id` and `id` = ?))) or ((`imageable_type` = ? and exists (select * from `book` where `images`.`imageable_id` = `book`.`id` and `id` = ?)))', ['user', 1, 'book', 1]],
+ ['select * from `user` where `user`.`id` = ? limit 1', [1]],
+ ['select * from `book` where `book`.`id` = ? limit 1', [1]],
+ ];
+ while ($event = $this->channel->pop(0.001)) {
+ if ($event instanceof QueryExecuted) {
+ $this->assertSame([$event->sql, $event->bindings], array_shift($sqls));
+ }
+ }
+ }
+
+ public function testWhereDoesntHaveMorph()
+ {
+ $this->getContainer();
+ $images = Image::query()
+ ->whereDoesntHaveMorph(
+ 'imageable',
+ [
+ User::class,
+ Book::class,
+ ],
+ function (Builder $query, $type) {
+ if ($type === User::class) {
+ $query->where('id', '<>', 1);
+ }
+ if ($type === Book::class) {
+ $query->where('id', '<>', 1);
+ }
+ }
+ )
+ ->get();
+ $res = $images->every(function ($item, $key) {
+ return $item->imageable->id == 1;
+ });
+ $this->assertSame(true, $res);
+ $sqls = [
+ ['select * from `images` where ((`imageable_type` = ? and not exists (select * from `user` where `images`.`imageable_id` = `user`.`id` and `id` <> ?)) or (`imageable_type` = ? and not exists (select * from `book` where `images`.`imageable_id` = `book`.`id` and `id` <> ?)))', ['user', 1, 'book', 1]],
+ ['select * from `user` where `user`.`id` = ? limit 1', [1]],
+ ['select * from `book` where `book`.`id` = ? limit 1', [1]],
+ ];
+ while ($event = $this->channel->pop(0.001)) {
+ if ($event instanceof QueryExecuted) {
+ $this->assertSame([$event->sql, $event->bindings], array_shift($sqls));
+ }
+ }
+ }
+
+ public function testOrWhereDoesntHaveMorph()
+ {
+ $this->getContainer();
+ $images = Image::query()
+ ->whereDoesntHaveMorph(
+ 'imageable',
+ [
+ User::class,
+ ],
+ function (Builder $query) {
+ $query->where('id', '<>', 1);
+ }
+ )
+ ->orWhereDoesntHaveMorph(
+ 'imageable',
+ [
+ Book::class,
+ ],
+ function (Builder $query) {
+ $query->where('id', '<>', 1);
+ }
+ )
+ ->get();
+ $res = $images->every(function ($item, $key) {
+ return $item->imageable->id == 1;
+ });
+ $this->assertSame(true, $res);
+ $sqls = [
+ ['select * from `images` where ((`imageable_type` = ? and not exists (select * from `user` where `images`.`imageable_id` = `user`.`id` and `id` <> ?))) or ((`imageable_type` = ? and not exists (select * from `book` where `images`.`imageable_id` = `book`.`id` and `id` <> ?)))', ['user', 1, 'book', 1]],
+ ['select * from `user` where `user`.`id` = ? limit 1', [1]],
+ ['select * from `book` where `book`.`id` = ? limit 1', [1]],
+ ];
+ while ($event = $this->channel->pop(0.001)) {
+ if ($event instanceof QueryExecuted) {
+ $this->assertSame([$event->sql, $event->bindings], array_shift($sqls));
+ }
+ }
+ }
+
protected function getContainer()
{
$dispatcher = Mockery::mock(EventDispatcherInterface::class);
diff --git a/src/db-connection/composer.json b/src/db-connection/composer.json
index b83b71a04..683b7c4a0 100644
--- a/src/db-connection/composer.json
+++ b/src/db-connection/composer.json
@@ -17,12 +17,12 @@
},
"require": {
"php": ">=7.2",
- "hyperf/framework": "~1.1.0",
- "hyperf/database": "~1.1.0",
- "hyperf/di": "~1.1.0",
- "hyperf/model-listener": "~1.1.0",
- "hyperf/pool": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/framework": "~2.0.0",
+ "hyperf/database": "~2.0.0",
+ "hyperf/di": "~2.0.0",
+ "hyperf/model-listener": "~2.0.0",
+ "hyperf/pool": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/db/composer.json b/src/db/composer.json
index cc9d4bcc3..8c76f6edf 100644
--- a/src/db/composer.json
+++ b/src/db/composer.json
@@ -20,15 +20,15 @@
"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",
+ "hyperf/config": "~2.0.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/pool": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"psr/container": "^1.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
- "hyperf/testing": "1.1.*",
+ "hyperf/testing": "~2.0.0",
"mockery/mockery": "^1.0"
},
"config": {
@@ -39,6 +39,9 @@
"cs-fix": "php-cs-fixer fix $1"
},
"extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ },
"hyperf": {
"config": "Hyperf\\DB\\ConfigProvider"
}
diff --git a/src/devtool/composer.json b/src/devtool/composer.json
index 9b8cbafb1..9424178f6 100644
--- a/src/devtool/composer.json
+++ b/src/devtool/composer.json
@@ -17,10 +17,10 @@
},
"require": {
"php": ">=7.2",
- "hyperf/command": "~1.1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/di": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/command": "~2.0.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/di": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
@@ -37,6 +37,7 @@
},
"autoload-dev": {
"psr-4": {
+ "HyperfTest\\Devtool\\": "tests/"
}
},
"config": {
diff --git a/src/di/composer.json b/src/di/composer.json
index 0e2abcd64..a1dfae1f3 100644
--- a/src/di/composer.json
+++ b/src/di/composer.json
@@ -21,11 +21,11 @@
"psr/container": "^1.0",
"nikic/php-parser": "^4.1",
"doctrine/annotations": "^1.6",
- "symfony/finder": "^4.1",
+ "symfony/finder": "^5.0",
"php-di/phpdoc-reader": "^2.0.1",
"doctrine/instantiator": "^1.0",
- "hyperf/event": "~1.1.0",
- "hyperf/framework": "~1.1.0",
+ "hyperf/event": "~2.0.0",
+ "hyperf/framework": "~2.0.0",
"roave/better-reflection": "^4.0"
},
"require-dev": {
diff --git a/src/di/src/Annotation/AnnotationCollector.php b/src/di/src/Annotation/AnnotationCollector.php
index 92f7f43c4..c7ca58ebe 100644
--- a/src/di/src/Annotation/AnnotationCollector.php
+++ b/src/di/src/Annotation/AnnotationCollector.php
@@ -35,7 +35,24 @@ class AnnotationCollector extends MetadataCollector
static::$container[$class]['_m'][$method][$annotation] = $value;
}
+ public static function clear(?string $key = null): void
+ {
+ if ($key) {
+ unset(static::$container[$key]);
+ } else {
+ static::$container = [];
+ }
+ }
+
+ /**
+ * @deprecated v3.0
+ */
public static function getClassByAnnotation(string $annotation): array
+ {
+ return self::getClassesByAnnotation($annotation);
+ }
+
+ public static function getClassesByAnnotation(string $annotation)
{
$result = [];
foreach (static::$container as $class => $metadata) {
@@ -47,6 +64,40 @@ class AnnotationCollector extends MetadataCollector
return $result;
}
+ /**
+ * @deprecated v3.0
+ */
+ public static function getMethodByAnnotation(string $annotation): array
+ {
+ return static::getMethodsByAnnotation($annotation);
+ }
+
+ public static function getMethodsByAnnotation(string $annotation): array
+ {
+ $result = [];
+ foreach (static::$container as $class => $metadata) {
+ foreach ($metadata['_m'] ?? [] as $method => $_metadata) {
+ if ($value = $_metadata[$annotation] ?? null) {
+ $result[] = ['class' => $class, 'method' => $method, 'annotation' => $value];
+ }
+ }
+ }
+ return $result;
+ }
+
+ public static function getPropertiesByAnnotation(string $annotation): array
+ {
+ $properties = [];
+ foreach (static::$container as $class => $metadata) {
+ foreach ($metadata['_p'] ?? [] as $property => $_metadata) {
+ if ($value = $_metadata[$annotation] ?? null) {
+ $properties[] = ['class' => $class, 'property' => $property, 'annotation' => $value];
+ }
+ }
+ }
+ return $properties;
+ }
+
public static function getClassAnnotation(string $class, string $annotation)
{
return static::get($class . '._c.' . $annotation);
@@ -57,16 +108,13 @@ class AnnotationCollector extends MetadataCollector
return static::get($class . '._m.' . $method);
}
- public static function getMethodByAnnotation(string $annotation): array
+ public static function getClassPropertyAnnotation(string $class, string $property)
{
- $result = [];
- foreach (static::$container as $class => $metadata) {
- foreach ($metadata['_m'] ?? [] as $method => $_metadata) {
- if ($value = $_metadata[$annotation] ?? null) {
- $result[] = ['class' => $class, 'method' => $method, 'annotation' => $value];
- }
- }
- }
- return $result;
+ return static::get($class . '._p.' . $property);
+ }
+
+ public static function getContainer(): array
+ {
+ return static::$container;
}
}
diff --git a/src/di/src/Annotation/AnnotationReader.php b/src/di/src/Annotation/AnnotationReader.php
new file mode 100644
index 000000000..fd1746843
--- /dev/null
+++ b/src/di/src/Annotation/AnnotationReader.php
@@ -0,0 +1,385 @@
+ 'Doctrine\Common\Annotations\Annotation\IgnoreAnnotation',
+ ];
+
+ /**
+ * A list with annotations that are not causing exceptions when not resolved to an annotation class.
+ *
+ * The names are case sensitive.
+ *
+ * @var array
+ */
+ private static $globalIgnoredNames = [
+ // Annotation tags
+ 'Annotation' => true, 'Attribute' => true, 'Attributes' => true,
+ /* Can we enable this? 'Enum' => true, */
+ 'Required' => true,
+ 'Target' => true,
+ // Widely used tags (but not existent in phpdoc)
+ 'fix' => true, 'fixme' => true,
+ 'override' => true,
+ // PHPDocumentor 1 tags
+ 'abstract' => true, 'access' => true,
+ 'code' => true,
+ 'deprec' => true,
+ 'endcode' => true, 'exception' => true,
+ 'final' => true,
+ 'ingroup' => true, 'inheritdoc' => true, 'inheritDoc' => true,
+ 'magic' => true,
+ 'name' => true,
+ 'toc' => true, 'tutorial' => true,
+ 'private' => true,
+ 'static' => true, 'staticvar' => true, 'staticVar' => true,
+ 'throw' => true,
+ // PHPDocumentor 2 tags.
+ 'api' => true, 'author' => true,
+ 'category' => true, 'copyright' => true,
+ 'deprecated' => true,
+ 'example' => true,
+ 'filesource' => true,
+ 'global' => true,
+ 'ignore' => true, /* Can we enable this? 'index' => true, */ 'internal' => true,
+ 'license' => true, 'link' => true,
+ 'method' => true,
+ 'package' => true, 'param' => true, 'property' => true, 'property-read' => true, 'property-write' => true,
+ 'return' => true,
+ 'see' => true, 'since' => true, 'source' => true, 'subpackage' => true,
+ 'throws' => true, 'todo' => true, 'TODO' => true,
+ 'usedby' => true, 'uses' => true,
+ 'var' => true, 'version' => true,
+ // PHPUnit tags
+ 'codeCoverageIgnore' => true, 'codeCoverageIgnoreStart' => true, 'codeCoverageIgnoreEnd' => true,
+ // PHPCheckStyle
+ 'SuppressWarnings' => true,
+ // PHPStorm
+ 'noinspection' => true,
+ // PEAR
+ 'package_version' => true,
+ // PlantUML
+ 'startuml' => true, 'enduml' => true,
+ // Symfony 3.3 Cache Adapter
+ 'experimental' => true,
+ // Slevomat Coding Standard
+ 'phpcsSuppress' => true,
+ // PHP CodeSniffer
+ 'codingStandardsIgnoreStart' => true,
+ 'codingStandardsIgnoreEnd' => true,
+ // PHPStan
+ 'template' => true, 'implements' => true, 'extends' => true, 'use' => true,
+ ];
+
+ /**
+ * A list with annotations that are not causing exceptions when not resolved to an annotation class.
+ *
+ * The names are case sensitive.
+ *
+ * @var array
+ */
+ private static $globalIgnoredNamespaces = [];
+
+ /**
+ * Annotations parser.
+ *
+ * @var \Doctrine\Common\Annotations\DocParser
+ */
+ private $parser;
+
+ /**
+ * Annotations parser used to collect parsing metadata.
+ *
+ * @var \Doctrine\Common\Annotations\DocParser
+ */
+ private $preParser;
+
+ /**
+ * PHP parser used to collect imports.
+ *
+ * @var \Doctrine\Common\Annotations\PhpParser
+ */
+ private $phpParser;
+
+ /**
+ * In-memory cache mechanism to store imported annotations per class.
+ *
+ * @var array
+ */
+ private $imports = [];
+
+ /**
+ * In-memory cache mechanism to store ignored annotations per class.
+ *
+ * @var array
+ */
+ private $ignoredAnnotationNames = [];
+
+ /**
+ * Constructor.
+ *
+ * Initializes a new AnnotationReader.
+ *
+ * @param DocParser $parser
+ *
+ * @throws AnnotationException
+ */
+ public function __construct(DocParser $parser = null)
+ {
+ if (extension_loaded('Zend Optimizer+') && (ini_get('zend_optimizerplus.save_comments') === '0' || ini_get('opcache.save_comments') === '0')) {
+ throw AnnotationException::optimizerPlusSaveComments();
+ }
+
+ if (extension_loaded('Zend OPcache') && ini_get('opcache.save_comments') == 0) {
+ throw AnnotationException::optimizerPlusSaveComments();
+ }
+
+ // Make sure that the IgnoreAnnotation annotation is loaded
+ class_exists(IgnoreAnnotation::class);
+
+ $this->parser = $parser ?: new DocParser();
+
+ $this->preParser = new DocParser();
+
+ $this->preParser->setImports(self::$globalImports);
+ $this->preParser->setIgnoreNotImportedAnnotations(true);
+ $this->preParser->setIgnoredAnnotationNames(self::$globalIgnoredNames);
+
+ $this->phpParser = new PhpParser();
+ }
+
+ public static function addGlobalImports(string $alias, string $annotation)
+ {
+ self::$globalImports[$alias] = $annotation;
+ }
+
+ /**
+ * Add a new annotation to the globally ignored annotation names with regard to exception handling.
+ *
+ * @param string $name
+ */
+ public static function addGlobalIgnoredName($name)
+ {
+ self::$globalIgnoredNames[$name] = true;
+ }
+
+ /**
+ * Add a new annotation to the globally ignored annotation namespaces with regard to exception handling.
+ *
+ * @param string $namespace
+ */
+ public static function addGlobalIgnoredNamespace($namespace)
+ {
+ self::$globalIgnoredNamespaces[$namespace] = true;
+ }
+
+ public function getClassAnnotations(ReflectionClass $class)
+ {
+ $this->parser->setTarget(Target::TARGET_CLASS);
+ $this->parser->setImports($this->getClassImports($class));
+ $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class));
+ $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);
+
+ return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName());
+ }
+
+ public function getClassAnnotation(ReflectionClass $class, $annotationName)
+ {
+ $annotations = $this->getClassAnnotations($class);
+
+ foreach ($annotations as $annotation) {
+ if ($annotation instanceof $annotationName) {
+ return $annotation;
+ }
+ }
+
+ return null;
+ }
+
+ public function getPropertyAnnotations(ReflectionProperty $property)
+ {
+ $class = $property->getDeclaringClass();
+ $context = 'property ' . $class->getName() . '::$' . $property->getName();
+
+ $this->parser->setTarget(Target::TARGET_PROPERTY);
+ $this->parser->setImports($this->getPropertyImports($property));
+ $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class));
+ $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);
+
+ return $this->parser->parse($property->getDocComment(), $context);
+ }
+
+ public function getPropertyAnnotation(ReflectionProperty $property, $annotationName)
+ {
+ $annotations = $this->getPropertyAnnotations($property);
+
+ foreach ($annotations as $annotation) {
+ if ($annotation instanceof $annotationName) {
+ return $annotation;
+ }
+ }
+
+ return null;
+ }
+
+ public function getMethodAnnotations(ReflectionMethod $method)
+ {
+ $class = $method->getDeclaringClass();
+ $context = 'method ' . $class->getName() . '::' . $method->getName() . '()';
+
+ $this->parser->setTarget(Target::TARGET_METHOD);
+ $this->parser->setImports($this->getMethodImports($method));
+ $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class));
+ $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces);
+
+ return $this->parser->parse($method->getDocComment(), $context);
+ }
+
+ public function getMethodAnnotation(ReflectionMethod $method, $annotationName)
+ {
+ $annotations = $this->getMethodAnnotations($method);
+
+ foreach ($annotations as $annotation) {
+ if ($annotation instanceof $annotationName) {
+ return $annotation;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the ignored annotations for the given class.
+ *
+ * @return array
+ */
+ private function getIgnoredAnnotationNames(ReflectionClass $class)
+ {
+ $name = $class->getName();
+ if (isset($this->ignoredAnnotationNames[$name])) {
+ return $this->ignoredAnnotationNames[$name];
+ }
+
+ $this->collectParsingMetadata($class);
+
+ return $this->ignoredAnnotationNames[$name];
+ }
+
+ /**
+ * Retrieves imports.
+ *
+ * @return array
+ */
+ private function getClassImports(ReflectionClass $class)
+ {
+ $name = $class->getName();
+ if (isset($this->imports[$name])) {
+ return $this->imports[$name];
+ }
+
+ $this->collectParsingMetadata($class);
+
+ return $this->imports[$name];
+ }
+
+ /**
+ * Retrieves imports for methods.
+ *
+ * @return array
+ */
+ private function getMethodImports(ReflectionMethod $method)
+ {
+ $class = $method->getDeclaringClass();
+ $classImports = $this->getClassImports($class);
+
+ $traitImports = [];
+
+ foreach ($class->getTraits() as $trait) {
+ if ($trait->hasMethod($method->getName())
+ && $trait->getFileName() === $method->getFileName()
+ ) {
+ $traitImports = array_merge($traitImports, $this->phpParser->parseClass($trait));
+ }
+ }
+
+ return array_merge($classImports, $traitImports);
+ }
+
+ /**
+ * Retrieves imports for properties.
+ *
+ * @return array
+ */
+ private function getPropertyImports(ReflectionProperty $property)
+ {
+ $class = $property->getDeclaringClass();
+ $classImports = $this->getClassImports($class);
+
+ $traitImports = [];
+
+ foreach ($class->getTraits() as $trait) {
+ if ($trait->hasProperty($property->getName())) {
+ $traitImports = array_merge($traitImports, $this->phpParser->parseClass($trait));
+ }
+ }
+
+ return array_merge($classImports, $traitImports);
+ }
+
+ /**
+ * Collects parsing metadata for a given class.
+ */
+ private function collectParsingMetadata(ReflectionClass $class)
+ {
+ $ignoredAnnotationNames = self::$globalIgnoredNames;
+ $annotations = $this->preParser->parse($class->getDocComment(), 'class ' . $class->name);
+
+ foreach ($annotations as $annotation) {
+ if ($annotation instanceof IgnoreAnnotation) {
+ foreach ($annotation->names as $annot) {
+ $ignoredAnnotationNames[$annot] = true;
+ }
+ }
+ }
+
+ $name = $class->getName();
+
+ $this->imports[$name] = array_merge(
+ self::$globalImports,
+ $this->phpParser->parseClass($class),
+ ['__NAMESPACE__' => $class->getNamespaceName()]
+ );
+
+ $this->ignoredAnnotationNames[$name] = $ignoredAnnotationNames;
+ }
+}
diff --git a/src/di/src/Annotation/Aspect.php b/src/di/src/Annotation/Aspect.php
index 7cbe499f9..a60b82885 100644
--- a/src/di/src/Annotation/Aspect.php
+++ b/src/di/src/Annotation/Aspect.php
@@ -11,8 +11,8 @@ declare(strict_types=1);
*/
namespace Hyperf\Di\Annotation;
-use Doctrine\Instantiator\Instantiator;
-use Hyperf\Di\Aop\AroundInterface;
+use Hyperf\Di\BetterReflectionManager;
+use ReflectionProperty;
/**
* @Annotation
@@ -20,20 +20,58 @@ use Hyperf\Di\Aop\AroundInterface;
*/
class Aspect extends AbstractAnnotation
{
+ /**
+ * @var array
+ */
+ public $classes = [];
+
+ /**
+ * @var array
+ */
+ public $annotations = [];
+
+ /**
+ * @var int
+ */
+ public $priority;
+
public function collectClass(string $className): void
{
- // @TODO Add order property.
- if (class_exists($className)) {
- // Create the aspect instance without invoking their constructor.
- $instantitor = new Instantiator();
- $instance = $instantitor->instantiate($className);
- switch ($instance) {
- case $instance instanceof AroundInterface:
- $classes = property_exists($instance, 'classes') ? $instance->classes : [];
- $annotations = property_exists($instance, 'annotations') ? $instance->annotations : [];
- AspectCollector::setAround($className, $classes, $annotations);
- break;
+ parent::collectClass($className);
+ $this->collect($className);
+ }
+
+ protected function collect(string $className)
+ {
+ // Create the aspect instance without invoking their constructor.
+ $reflectionClass = BetterReflectionManager::reflectClass($className);
+ $properties = $reflectionClass->getImmediateProperties(ReflectionProperty::IS_PUBLIC);
+ $instanceClasses = $instanceAnnotations = [];
+ $instancePriority = null;
+ foreach ($properties as $property) {
+ if ($property->getName() === 'classes') {
+ $instanceClasses = $property->getDefaultValue();
+ } elseif ($property->getName() === 'annotations') {
+ $instanceAnnotations = $property->getDefaultValue();
+ } elseif ($property->getName() === 'priority') {
+ $instancePriority = $property->getDefaultValue();
}
}
+
+ // Classes
+ $classes = $this->classes;
+ $classes = $instanceClasses ? array_merge($classes, $instanceClasses) : $classes;
+ // Annotations
+ $annotations = $this->annotations;
+ $annotations = $instanceAnnotations ? array_merge($annotations, $instanceAnnotations) : $annotations;
+ // Priority
+ $annotationPriority = $this->priority;
+ $propertyPriority = $instancePriority ? $instancePriority : null;
+ if (! is_null($annotationPriority) && ! is_null($propertyPriority) && $annotationPriority !== $propertyPriority) {
+ throw new \InvalidArgumentException('Cannot define two difference priority of Aspect.');
+ }
+ $priority = $annotationPriority ?? $propertyPriority;
+ // Save the metadata to AspectCollector
+ AspectCollector::setAround($className, $classes, $annotations, $priority);
}
}
diff --git a/src/di/src/Annotation/AspectCollector.php b/src/di/src/Annotation/AspectCollector.php
index 759b42867..37b789776 100644
--- a/src/di/src/Annotation/AspectCollector.php
+++ b/src/di/src/Annotation/AspectCollector.php
@@ -25,14 +25,49 @@ class AspectCollector extends MetadataCollector
*/
protected static $aspectRules = [];
- public static function setAround(string $aspect, array $classes, array $annotations): void
+ /**
+ * @var \SplPriorityQueue[]
+ */
+ protected static $aspectQueues = [];
+
+ public static function setAround(string $aspect, array $classes, array $annotations, ?int $priority = null): void
{
- static::set('classes.' . $aspect, $classes);
- static::set('annotations.' . $aspect, $annotations);
- static::$aspectRules[$aspect] = [
- 'classes' => $classes,
- 'annotations' => $annotations,
- ];
+ if (! is_int($priority)) {
+ $priority = static::getDefaultPriority();
+ }
+ $setter = function ($key, $value) {
+ if (static::has($key)) {
+ $value = array_merge(static::get($key, []), $value);
+ static::set($key, $value);
+ } else {
+ static::set($key, $value);
+ }
+ };
+ $setter('classes.' . $aspect, $classes);
+ $setter('annotations.' . $aspect, $annotations);
+ if (isset(static::$aspectRules[$aspect])) {
+ static::$aspectRules[$aspect] = [
+ 'priority' => $priority,
+ 'classes' => array_merge(static::$aspectRules[$aspect]['classes'] ?? [], $classes),
+ 'annotations' => array_merge(static::$aspectRules[$aspect]['annotations'] ?? [], $annotations),
+ ];
+ } else {
+ static::$aspectRules[$aspect] = [
+ 'priority' => $priority,
+ 'classes' => $classes,
+ 'annotations' => $annotations,
+ ];
+ }
+ }
+
+ public static function clear(?string $key = null): void
+ {
+ if ($key) {
+ unset(static::$container['classes'][$key], static::$container['annotations'][$key], static::$aspectRules[$key]);
+ } else {
+ static::$container = [];
+ static::$aspectRules = [];
+ }
}
public static function getRule(string $aspect): array
@@ -40,8 +75,36 @@ class AspectCollector extends MetadataCollector
return static::$aspectRules[$aspect] ?? [];
}
+ public static function getPriority(string $aspect): int
+ {
+ return static::$aspectRules[$aspect]['priority'] ?? static::getDefaultPriority();
+ }
+
public static function getRules(): array
{
return static::$aspectRules;
}
+
+ public static function getContainer(): array
+ {
+ return static::$container;
+ }
+
+ public static function serialize(): string
+ {
+ return serialize([static::$aspectRules, static::$container]);
+ }
+
+ public static function deserialize(string $metadata): bool
+ {
+ [$rules, $container] = unserialize($metadata);
+ static::$aspectRules = $rules;
+ static::$container = $container;
+ return true;
+ }
+
+ private static function getDefaultPriority(): int
+ {
+ return (int) (PHP_INT_MAX / 2);
+ }
}
diff --git a/src/di/src/Annotation/Inject.php b/src/di/src/Annotation/Inject.php
index 3bd8c1dac..50c3851b0 100644
--- a/src/di/src/Annotation/Inject.php
+++ b/src/di/src/Annotation/Inject.php
@@ -11,9 +11,10 @@ declare(strict_types=1);
*/
namespace Hyperf\Di\Annotation;
-use Hyperf\Di\ReflectionManager;
+use Hyperf\Di\BetterReflectionManager;
+use Hyperf\Di\TypesFinderManager;
use PhpDocReader\AnnotationException;
-use PhpDocReader\PhpDocReader;
+use phpDocumentor\Reflection\Types\Object_;
/**
* @Annotation
@@ -36,25 +37,29 @@ class Inject extends AbstractAnnotation
*/
public $lazy = false;
- /**
- * @var PhpDocReader
- */
- private $docReader;
-
public function __construct($value = null)
{
parent::__construct($value);
- $this->docReader = make(PhpDocReader::class);
}
public function collectProperty(string $className, ?string $target): void
{
try {
- $this->value = $this->docReader->getPropertyClass(ReflectionManager::reflectClass($className)->getProperty($target));
- AnnotationCollector::collectProperty($className, $target, static::class, $this);
+ $reflectionClass = BetterReflectionManager::reflectClass($className);
+ $properties = $reflectionClass->getImmediateProperties();
+ $reflectionProperty = $properties[$target] ?? null;
+ if (! $reflectionProperty) {
+ $this->value = '';
+ return;
+ }
+ $reflectionTypes = TypesFinderManager::getPropertyFinder()->__invoke($reflectionProperty, $reflectionClass->getDeclaringNamespaceAst());
+ if ($reflectionTypes[0] instanceof Object_) {
+ $this->value = ltrim((string) $reflectionTypes[0], '\\');
+ }
if ($this->lazy) {
$this->value = 'HyperfLazy\\' . $this->value;
}
+ AnnotationCollector::collectProperty($className, $target, static::class, $this);
} catch (AnnotationException $e) {
if ($this->required) {
throw $e;
diff --git a/src/di/src/Annotation/InjectAspect.php b/src/di/src/Annotation/InjectAspect.php
new file mode 100644
index 000000000..4b630b2cb
--- /dev/null
+++ b/src/di/src/Annotation/InjectAspect.php
@@ -0,0 +1,28 @@
+process();
+ }
+}
diff --git a/src/di/src/Annotation/RelationCollector.php b/src/di/src/Annotation/RelationCollector.php
new file mode 100644
index 000000000..32ed1855f
--- /dev/null
+++ b/src/di/src/Annotation/RelationCollector.php
@@ -0,0 +1,38 @@
+appEnv = $appEnv;
+ $this->configDir = $configDir;
+ $this->paths = $paths;
+ $this->dependencies = $dependencies;
+ $this->ignoreAnnotations = $ignoreAnnotations;
+ $this->globalImports = $globalImports;
+ $this->collectors = $collectors;
+ $this->classMap = $classMap;
+ }
+
+ public function getConfigDir(): string
+ {
+ return $this->configDir;
+ }
+
+ public function getPaths(): array
+ {
+ return $this->paths;
+ }
+
+ public function getCollectors(): array
+ {
+ return $this->collectors;
+ }
+
+ public function getIgnoreAnnotations(): array
+ {
+ return $this->ignoreAnnotations;
+ }
+
+ public function getGlobalImports(): array
+ {
+ return $this->globalImports;
+ }
+
+ public function getDependencies(): array
+ {
+ return $this->dependencies;
+ }
+
+ public function getClassMap(): array
+ {
+ return $this->classMap;
+ }
+
+ public function getAppEnv(): string
+ {
+ return $this->appEnv;
+ }
+
+ public static function instance(string $configDir): self
+ {
+ if (self::$instance) {
+ return self::$instance;
+ }
+
+ $configDir = rtrim($configDir, '/');
+
+ [$config, $serverDependencies, $appEnv] = static::initConfigByFile($configDir);
+
+ return self::$instance = new self(
+ $appEnv,
+ $configDir,
+ $config['paths'] ?? [],
+ $serverDependencies ?? [],
+ $config['ignore_annotations'] ?? [],
+ $config['global_imports'] ?? [],
+ $config['collectors'] ?? [],
+ $config['class_map'] ?? []
+ );
+ }
+
+ private static function initConfigByFile(string $configDir): array
+ {
+ $config = [];
+ $configFromProviders = [];
+ $appEnv = 'dev';
+ if (class_exists(ProviderConfig::class)) {
+ $configFromProviders = ProviderConfig::load();
+ }
+
+ $serverDependencies = $configFromProviders['dependencies'] ?? [];
+ if (file_exists($configDir . '/autoload/dependencies.php')) {
+ $definitions = include $configDir . '/autoload/dependencies.php';
+ $serverDependencies = array_replace($serverDependencies, $definitions ?? []);
+ }
+
+ $config = static::allocateConfigValue($configFromProviders['annotations'] ?? [], $config);
+
+ // Load the config/autoload/annotations.php and merge the config
+ if (file_exists($configDir . '/autoload/annotations.php')) {
+ $annotations = include $configDir . '/autoload/annotations.php';
+ $config = static::allocateConfigValue($annotations, $config);
+ }
+
+ // Load the config/config.php and merge the config
+ if (file_exists($configDir . '/config.php')) {
+ $configContent = include $configDir . '/config.php';
+ $appEnv = $configContent['app_env'] ?? $appEnv;
+ if (isset($configContent['annotations'])) {
+ $config = static::allocateConfigValue($configContent['annotations'], $config);
+ }
+ }
+ return [$config, $serverDependencies, $appEnv];
+ }
+
+ private static function allocateConfigValue(array $content, array $config): array
+ {
+ if (! isset($content['scan'])) {
+ return [];
+ }
+ foreach ($content['scan'] as $key => $value) {
+ if (! isset($config[$key])) {
+ $config[$key] = [];
+ }
+ if (! is_array($value)) {
+ $value = [$value];
+ }
+ $config[$key] = array_merge($config[$key], $value);
+ }
+ return $config;
+ }
+}
diff --git a/src/di/src/Annotation/Scanner.php b/src/di/src/Annotation/Scanner.php
index 491fb0fe5..c836f942c 100644
--- a/src/di/src/Annotation/Scanner.php
+++ b/src/di/src/Annotation/Scanner.php
@@ -11,100 +11,153 @@ declare(strict_types=1);
*/
namespace Hyperf\Di\Annotation;
-use Doctrine\Common\Annotations\AnnotationReader;
-use Doctrine\Common\Annotations\AnnotationRegistry;
-use Hyperf\Di\Aop\Ast;
-use Hyperf\Di\ReflectionManager;
-use Symfony\Component\Finder\Finder;
+use Hyperf\Config\ProviderConfig;
+use Hyperf\Di\BetterReflectionManager;
+use Hyperf\Di\ClassLoader;
+use Hyperf\Di\MetadataCollector;
+use Hyperf\Utils\Filesystem\Filesystem;
+use ReflectionProperty;
+use Roave\BetterReflection\Reflection\Adapter;
+use Roave\BetterReflection\Reflection\ReflectionClass;
class Scanner
{
/**
- * @var Ast
+ * @var \Hyperf\Di\ClassLoader
*/
- private $parser;
+ protected $classloader;
- public function __construct(array $ignoreAnnotations = ['mixin'])
+ /**
+ * @var \Hyperf\Autoload\ScanConfig
+ */
+ protected $scanConfig;
+
+ /**
+ * @var Filesystem
+ */
+ protected $filesystem;
+
+ /**
+ * @var string
+ */
+ protected $path = BASE_PATH . '/runtime/container/collectors.cache';
+
+ public function __construct(ClassLoader $classloader, ScanConfig $scanConfig)
{
- $this->parser = new Ast();
+ $this->classloader = $classloader;
+ $this->scanConfig = $scanConfig;
+ $this->filesystem = new Filesystem();
- // TODO: this method is deprecated and will be removed in doctrine/annotations 2.0
- AnnotationRegistry::registerLoader('class_exists');
-
- foreach ($ignoreAnnotations as $annotation) {
+ foreach ($scanConfig->getIgnoreAnnotations() as $annotation) {
AnnotationReader::addGlobalIgnoredName($annotation);
}
+ foreach ($scanConfig->getGlobalImports() as $alias => $annotation) {
+ AnnotationReader::addGlobalImports($alias, $annotation);
+ }
}
- public function scan(array $paths): array
+ public function collect(AnnotationReader $reader, ReflectionClass $reflection)
{
+ $className = $reflection->getName();
+ if ($path = $this->scanConfig->getClassMap()[$className] ?? null) {
+ if ($reflection->getFileName() !== $path) {
+ // When the original class is dynamically replaced, the original class should not be collected.
+ return;
+ }
+ }
+ // Parse class annotations
+ $classAnnotations = $reader->getClassAnnotations(new Adapter\ReflectionClass($reflection));
+ if (! empty($classAnnotations)) {
+ foreach ($classAnnotations as $classAnnotation) {
+ if ($classAnnotation instanceof AnnotationInterface) {
+ $classAnnotation->collectClass($className);
+ }
+ }
+ }
+ // Parse properties annotations
+ $properties = $reflection->getImmediateProperties();
+ foreach ($properties as $property) {
+ $propertyAnnotations = $reader->getPropertyAnnotations(new Adapter\ReflectionProperty($property));
+ if (! empty($propertyAnnotations)) {
+ foreach ($propertyAnnotations as $propertyAnnotation) {
+ if ($propertyAnnotation instanceof AnnotationInterface) {
+ $propertyAnnotation->collectProperty($className, $property->getName());
+ }
+ }
+ }
+ }
+ // Parse methods annotations
+ $methods = $reflection->getImmediateMethods();
+ foreach ($methods as $method) {
+ $methodAnnotations = $reader->getMethodAnnotations(new Adapter\ReflectionMethod($method));
+ if (! empty($methodAnnotations)) {
+ foreach ($methodAnnotations as $methodAnnotation) {
+ if ($methodAnnotation instanceof AnnotationInterface) {
+ $methodAnnotation->collectMethod($className, $method->getName());
+ }
+ }
+ }
+ }
+
+ unset($reflection, $classAnnotations, $properties, $methods, $parentClassNames, $traitNames);
+ }
+
+ /**
+ * @return ReflectionClass[]
+ */
+ public function scan(): array
+ {
+ $paths = $this->scanConfig->getPaths();
+ $collectors = $this->scanConfig->getCollectors();
+ $classes = [];
if (! $paths) {
+ return $classes;
+ }
+
+ $annotationReader = new AnnotationReader();
+ $lastCacheModified = $this->deserializeCachedCollectors($collectors);
+ // TODO: The online mode won't init BetterReflectionManager when has cache.
+ if ($lastCacheModified > 0 && $this->scanConfig->getAppEnv() === 'prod') {
return [];
}
+
$paths = $this->normalizeDir($paths);
- $finder = new Finder();
- $finder->files()->in($paths)->name('*.php');
+ $reflector = BetterReflectionManager::initClassReflector($paths);
+ $classes = $reflector->getAllClasses();
+ // Initialize cache for BetterReflectionManager.
+ foreach ($classes as $class) {
+ BetterReflectionManager::reflectClass($class->getName(), $class);
+ }
- $meta = [];
- foreach ($finder as $file) {
- try {
- $stmts = $this->parser->parse($file->getContents());
- $className = $this->parser->parseClassByStmts($stmts);
- if (! $className) {
- continue;
+ $this->clearRemovedClasses($collectors, $classes);
+
+ foreach ($classes as $reflectionClass) {
+ if ($this->filesystem->lastModified($reflectionClass->getFileName()) > $lastCacheModified) {
+ /** @var MetadataCollector $collector */
+ foreach ($collectors as $collector) {
+ $collector::clear($reflectionClass->getName());
}
- $meta[$className] = $stmts;
- } catch (\RuntimeException $e) {
- continue;
+
+ $this->collect($annotationReader, $reflectionClass);
}
}
- $this->collect(array_keys($meta));
- return $meta;
- }
+ $this->loadAspects($lastCacheModified);
- public function collect($classCollection)
- {
- $reader = new AnnotationReader();
- // Because the annotation class should loaded before use it, so load file via $finder previous, and then parse annotation here.
- foreach ($classCollection as $className) {
- $reflectionClass = ReflectionManager::reflectClass($className);
- $classAnnotations = $reader->getClassAnnotations($reflectionClass);
- if (! empty($classAnnotations)) {
- foreach ($classAnnotations as $classAnnotation) {
- if ($classAnnotation instanceof AnnotationInterface) {
- $classAnnotation->collectClass($className);
- }
- }
- }
-
- // Parse properties annotations.
- $properties = $reflectionClass->getProperties();
- foreach ($properties as $property) {
- $propertyAnnotations = $reader->getPropertyAnnotations($property);
- if (! empty($propertyAnnotations)) {
- foreach ($propertyAnnotations as $propertyAnnotation) {
- if ($propertyAnnotation instanceof AnnotationInterface) {
- $propertyAnnotation->collectProperty($className, $property->getName());
- }
- }
- }
- }
-
- // Parse methods annotations.
- $methods = $reflectionClass->getMethods();
- foreach ($methods as $method) {
- $methodAnnotations = $reader->getMethodAnnotations($method);
- if (! empty($methodAnnotations)) {
- foreach ($methodAnnotations as $methodAnnotation) {
- if ($methodAnnotation instanceof AnnotationInterface) {
- $methodAnnotation->collectMethod($className, $method->getName());
- }
- }
- }
- }
+ $data = [];
+ /** @var MetadataCollector $collector */
+ foreach ($collectors as $collector) {
+ $data[$collector] = $collector::serialize();
}
+
+ if ($data) {
+ $this->putCache($this->path, serialize($data));
+ }
+
+ unset($annotationReader);
+
+ return $classes;
}
/**
@@ -121,4 +174,121 @@ class Scanner
return $result;
}
+
+ protected function deserializeCachedCollectors(array $collectors): int
+ {
+ if (! file_exists($this->path)) {
+ return 0;
+ }
+
+ $data = unserialize(file_get_contents($this->path));
+ foreach ($data as $collector => $deserialized) {
+ /** @var MetadataCollector $collector */
+ if (in_array($collector, $collectors)) {
+ $collector::deserialize($deserialized);
+ }
+ }
+
+ return $this->filesystem->lastModified($this->path);
+ }
+
+ /**
+ * @param ReflectionClass[] $reflections
+ */
+ protected function clearRemovedClasses(array $collectors, array $reflections): void
+ {
+ $path = BASE_PATH . '/runtime/container/classes.cache';
+ $classes = [];
+ foreach ($reflections as $reflection) {
+ $classes[] = $reflection->getName();
+ }
+
+ $data = [];
+ if ($this->filesystem->exists($path)) {
+ $data = unserialize($this->filesystem->get($path));
+ }
+
+ $this->putCache($path, serialize($classes));
+
+ $removed = array_diff($data, $classes);
+
+ foreach ($removed as $class) {
+ /** @var MetadataCollector $collector */
+ foreach ($collectors as $collector) {
+ $collector::clear($class);
+ }
+ }
+ }
+
+ protected function putCache($path, $data)
+ {
+ if (! $this->filesystem->isDirectory($dir = dirname($path))) {
+ $this->filesystem->makeDirectory($dir, 0755, true);
+ }
+
+ $this->filesystem->put($path, $data);
+ }
+
+ /**
+ * Load aspects to AspectCollector by configuration files and ConfigProvider.
+ */
+ protected function loadAspects(int $lastCacheModified): void
+ {
+ $configDir = $this->scanConfig->getConfigDir();
+ if (! $configDir) {
+ return;
+ }
+ if ($lastCacheModified > $this->filesystem->lastModified($configDir . '/autoload/aspects.php')
+ && $lastCacheModified > $this->filesystem->lastModified($configDir . '/config.php')
+ ) {
+ return;
+ }
+
+ $aspects = require $configDir . '/autoload/aspects.php';
+ $baseConfig = require $configDir . '/config.php';
+ $providerConfig = ProviderConfig::load();
+ if (! isset($aspects) || ! is_array($aspects)) {
+ $aspects = [];
+ }
+ if (! isset($baseConfig['aspects']) || ! is_array($baseConfig['aspects'])) {
+ $baseConfig['aspects'] = [];
+ }
+ if (! isset($providerConfig['aspects']) || ! is_array($providerConfig['aspects'])) {
+ $providerConfig['aspects'] = [];
+ }
+ $aspects = array_merge($providerConfig['aspects'], $baseConfig['aspects'], $aspects);
+
+ foreach ($aspects ?? [] as $key => $value) {
+ if (is_numeric($key)) {
+ $aspect = $value;
+ $priority = null;
+ } else {
+ $aspect = $key;
+ $priority = (int) $value;
+ }
+ // Create the aspect instance without invoking their constructor.
+ $reflectionClass = BetterReflectionManager::reflectClass($aspect);
+ $properties = $reflectionClass->getImmediateProperties(ReflectionProperty::IS_PUBLIC);
+ $instanceClasses = $instanceAnnotations = [];
+ $instancePriority = null;
+ foreach ($properties as $property) {
+ if ($property->getName() === 'classes') {
+ $instanceClasses = $property->getDefaultValue();
+ } elseif ($property->getName() === 'annotations') {
+ $instanceAnnotations = $property->getDefaultValue();
+ } elseif ($property->getName() === 'priority') {
+ $instancePriority = $property->getDefaultValue();
+ }
+ }
+
+ $classes = $instanceClasses ?: [];
+ // Annotations
+ $annotations = $instanceAnnotations ?: [];
+ // Priority
+ $priority = $priority ?: ($instancePriority ?? null);
+ // Save the metadata to AspectCollector
+ // TODO: When the aspect removed from config, it should removed from AspectCollector.
+ AspectCollector::setAround($aspect, $classes, $annotations, $priority);
+ }
+ }
}
diff --git a/src/di/src/Aop/AbstractAspect.php b/src/di/src/Aop/AbstractAspect.php
index 3f9c2b638..954da13df 100644
--- a/src/di/src/Aop/AbstractAspect.php
+++ b/src/di/src/Aop/AbstractAspect.php
@@ -26,4 +26,9 @@ abstract class AbstractAspect implements AroundInterface
* @var array
*/
public $annotations = [];
+
+ /**
+ * @var null|int
+ */
+ public $priority;
}
diff --git a/src/di/src/Aop/Ast.php b/src/di/src/Aop/Ast.php
index c7f74fdd4..c36091e53 100644
--- a/src/di/src/Aop/Ast.php
+++ b/src/di/src/Aop/Ast.php
@@ -12,8 +12,6 @@ declare(strict_types=1);
namespace Hyperf\Di\Aop;
use Hyperf\Utils\Composer;
-use PhpParser\Node\Stmt\Class_;
-use PhpParser\Node\Stmt\Namespace_;
use PhpParser\NodeTraverser;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter\Standard;
@@ -43,37 +41,23 @@ class Ast
return $this->astParser->parse($code);
}
- public function proxy(string $className, string $proxyClassName)
+ public function proxy(string $className)
{
- $stmts = AstCollector::get($className, value(function () use ($className) {
- $code = $this->getCodeByClassName($className);
- return $stmts = $this->astParser->parse($code);
- }));
+ $code = $this->getCodeByClassName($className);
+ $stmts = $this->astParser->parse($code);
$traverser = new NodeTraverser();
- // @TODO Allow user modify or replace node vistor.
- $traverser->addVisitor(new ProxyClassNameVisitor($proxyClassName));
- $traverser->addVisitor(new ProxyCallVisitor($className));
+ $visitorMetadata = new VisitorMetadata();
+ $visitorMetadata->className = $className;
+ // User could modify or replace the node vistors by Hyperf\Di\Aop\AstVisitorRegistry.
+ $queue = clone AstVisitorRegistry::getQueue();
+ foreach ($queue as $string) {
+ $visitor = new $string($visitorMetadata);
+ $traverser->addVisitor($visitor);
+ }
$modifiedStmts = $traverser->traverse($stmts);
return $this->printer->prettyPrintFile($modifiedStmts);
}
- public function parseClassByStmts(array $stmts): string
- {
- $namespace = $className = '';
- foreach ($stmts as $stmt) {
- if ($stmt instanceof Namespace_ && $stmt->name) {
- $namespace = $stmt->name->toString();
- foreach ($stmt->stmts as $node) {
- if ($node instanceof Class_ && $node->name) {
- $className = $node->name->toString();
- break;
- }
- }
- }
- }
- return ($namespace && $className) ? $namespace . '\\' . $className : '';
- }
-
private function getCodeByClassName(string $className): string
{
$file = Composer::getLoader()->findFile($className);
diff --git a/src/di/src/Aop/AstVisitorRegistry.php b/src/di/src/Aop/AstVisitorRegistry.php
new file mode 100644
index 000000000..703ec0e4c
--- /dev/null
+++ b/src/di/src/Aop/AstVisitorRegistry.php
@@ -0,0 +1,40 @@
+{$name}(...$arguments);
+ }
+ throw new \InvalidArgumentException('Invalid method for ' . __CLASS__);
+ }
+
+ public static function getQueue(): \SplPriorityQueue
+ {
+ if (! static::$queue instanceof \SplPriorityQueue) {
+ static::$queue = new \SplPriorityQueue();
+ }
+ return static::$queue;
+ }
+}
diff --git a/src/di/src/Aop/PropertyHandlerTrait.php b/src/di/src/Aop/PropertyHandlerTrait.php
new file mode 100644
index 000000000..db66fa5c0
--- /dev/null
+++ b/src/di/src/Aop/PropertyHandlerTrait.php
@@ -0,0 +1,67 @@
+getDefaultProperties();
+
+ // Inject the properties of parent class
+ $parentReflectionClass = $reflectionClass;
+ while ($parentReflectionClass = $parentReflectionClass->getParentClass()) {
+ $parentClassProperties = ReflectionManager::reflectClass($parentReflectionClass->getName())->getDefaultProperties();
+ $this->__handle($className, $parentReflectionClass->getName(), $propertyHandlers, $parentClassProperties);
+ $properties = array_diff_key($properties, $parentClassProperties);
+ }
+
+ // Inject the properties of traits
+ $traitNames = $reflectionClass->getTraitNames();
+ if (is_array($traitNames)) {
+ foreach ($traitNames ?? [] as $traitName) {
+ $traitProperties = ReflectionManager::reflectClass($traitName)->getDefaultProperties();
+ $this->__handle($className, $traitName, $propertyHandlers, $traitProperties);
+ $properties = array_diff_key($properties, $traitProperties);
+ }
+ }
+ // Inject the properties of current class
+ $this->__handle($className, $className, $propertyHandlers, $properties);
+ }
+
+ protected function __handle(string $currentClassName, string $targetClassName, array $propertyHandlers, array $properties)
+ {
+ foreach ($properties as $propertyName => $propertyDefaultValue) {
+ $propertyMetadata = AnnotationCollector::getClassPropertyAnnotation($targetClassName, $propertyName);
+ if (! $propertyMetadata) {
+ continue;
+ }
+ foreach ($propertyMetadata as $annotationName => $annotation) {
+ if (isset($propertyHandlers[$annotationName])) {
+ $callbacks = $propertyHandlers[$annotationName];
+ foreach ($callbacks as $callback) {
+ call($callback, [$this, $currentClassName, $targetClassName, $propertyName, $annotation]);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/di/src/Aop/PropertyHandlerVisitor.php b/src/di/src/Aop/PropertyHandlerVisitor.php
new file mode 100644
index 000000000..7c799cbfc
--- /dev/null
+++ b/src/di/src/Aop/PropertyHandlerVisitor.php
@@ -0,0 +1,146 @@
+visitorMetadata = $visitorMetadata;
+ }
+
+ public function setClassName(string $classname)
+ {
+ $this->visitorMetadata->className = $classname;
+ }
+
+ public function enterNode(Node $node)
+ {
+ if ($node instanceof Node\Stmt\ClassMethod) {
+ if ($node->name->toString() === '__construct') {
+ $this->visitorMetadata->hasConstructor = true;
+ $this->visitorMetadata->constructorNode = $node;
+ return;
+ }
+ }
+ }
+
+ public function leaveNode(Node $node)
+ {
+ if (! $this->visitorMetadata->hasConstructor && $node instanceof Node\Stmt\Class_ && ! $node->isAnonymous()) {
+ $constructor = $this->buildConstructor();
+ $constructor->stmts[] = $this->buildCallParentConstructorStatement();
+ $constructor->stmts[] = $this->buildStaticCallStatement();
+ $node->stmts = array_merge([$this->buildProxyTraitUseStatement()], [$constructor], $node->stmts);
+ $this->visitorMetadata->hasConstructor = true;
+ } else {
+ if ($node instanceof Node\Stmt\ClassMethod && $node->name->toString() === '__construct') {
+ $node->stmts = array_merge([$this->buildStaticCallStatement()], $node->stmts);
+ }
+ if ($node instanceof Node\Stmt\Class_ && ! $node->isAnonymous()) {
+ $node->stmts = array_merge([$this->buildProxyTraitUseStatement()], $node->stmts);
+ }
+ }
+ }
+
+ protected function buildConstructor(): Node\Stmt\ClassMethod
+ {
+ if ($this->visitorMetadata->constructorNode instanceof Node\Stmt\ClassMethod) {
+ // Returns the parsed constructor class method node.
+ $constructor = $this->visitorMetadata->constructorNode;
+ } else {
+ // Create a new constructor class method node.
+ $constructor = new Node\Stmt\ClassMethod('__construct');
+ $reflection = BetterReflectionManager::reflectClass($this->visitorMetadata->className);
+ try {
+ $parameters = $reflection->getMethod('__construct')->getParameters();
+ foreach ($parameters as $parameter) {
+ $constructor->params[] = $parameter->getAst();
+ }
+ } catch (OutOfBoundsException $exception) {
+ // Cannot found __construct method in parent class or traits, do noting.
+ }
+ }
+ return $constructor;
+ }
+
+ protected function buildCallParentConstructorStatement(): Node\Stmt
+ {
+ $left = new Node\Expr\FuncCall(new Name('get_parent_class'));
+ $right = new Node\Expr\FuncCall(new Name('method_exists'), [
+ new Node\Arg(new Node\Expr\ClassConstFetch(new Name('parent'), new Name('class'))),
+ new Node\Arg(new Node\Scalar\String_('__construct')),
+ ]);
+ return new Node\Stmt\If_(new Node\Expr\BinaryOp\BooleanAnd($left, $right), [
+ 'stmts' => [
+ new Node\Stmt\Expression(new Node\Expr\StaticCall(new Name('parent'), '__construct', [
+ new Node\Arg(new Node\Expr\FuncCall(new Name('func_get_args')), false, true),
+ ])),
+ ],
+ ]);
+ }
+
+ protected function buildStaticCallStatement(): Node\Stmt\Expression
+ {
+ return new Node\Stmt\Expression(new Node\Expr\StaticCall(new Name('self'), '__handlePropertyHandler', [
+ new Node\Arg(new Node\Scalar\MagicConst\Class_()),
+ ]));
+ }
+
+ /**
+ * Build `use InjectTrait;` statement.
+ */
+ protected function buildProxyTraitUseStatement(): TraitUse
+ {
+ $traits = [];
+ foreach ($this->proxyTraits as $proxyTrait) {
+ // Should not check the trait whether or not exist to avoid class autoload.
+ if (! is_string($proxyTrait)) {
+ continue;
+ }
+ // Add backslash prefix if the proxy trait does not start with backslash.
+ $proxyTrait[0] !== '\\' && $proxyTrait = '\\' . $proxyTrait;
+ $traits[] = new Name($proxyTrait);
+ }
+ return new TraitUse($traits);
+ }
+}
diff --git a/src/di/src/Aop/ProxyCallVisitor.php b/src/di/src/Aop/ProxyCallVisitor.php
index fe78872d5..b52b82dee 100644
--- a/src/di/src/Aop/ProxyCallVisitor.php
+++ b/src/di/src/Aop/ProxyCallVisitor.php
@@ -12,12 +12,9 @@ declare(strict_types=1);
namespace Hyperf\Di\Aop;
use PhpParser\Node;
-use PhpParser\Node\Expr\Assign;
-use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\StaticCall;
-use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
@@ -33,6 +30,11 @@ use PhpParser\NodeVisitorAbstract;
class ProxyCallVisitor extends NodeVisitorAbstract
{
+ /**
+ * @var \Hyperf\Di\Aop\VisitorMetadata
+ */
+ protected $visitorMetadata;
+
/**
* Determine if the class used proxy trait.
*
@@ -60,14 +62,9 @@ class ProxyCallVisitor extends NodeVisitorAbstract
*/
private $extends;
- /**
- * @var string
- */
- private $classname;
-
- public function __construct(string $classname)
+ public function __construct(VisitorMetadata $visitorMetadata)
{
- $this->classname = $classname;
+ $this->visitorMetadata = $visitorMetadata;
}
public function beforeTraverse(array $nodes)
@@ -92,7 +89,6 @@ class ProxyCallVisitor extends NodeVisitorAbstract
}
break;
case $class instanceof Class_ && ! $class->isAnonymous():
- $this->class = $class->name;
if ($class->extends) {
$this->extends = $class->extends;
}
@@ -120,7 +116,7 @@ class ProxyCallVisitor extends NodeVisitorAbstract
switch ($node) {
case $node instanceof ClassMethod:
if (! $this->shouldRewrite($node)) {
- return $this->formatMethod($node);
+ return $node;
}
// Rewrite the method to proxy call method.
return $this->rewriteMethod($node);
@@ -133,21 +129,6 @@ class ProxyCallVisitor extends NodeVisitorAbstract
unset($stmts);
return $node;
break;
- case $node instanceof StaticPropertyFetch && $this->extends:
- // Rewrite parent::$staticProperty to ParentClass::$staticProperty.
- if ($node->class instanceof Node\Name && $node->class->toString() === 'parent') {
- $node->class = new Name($this->extends->toCodeString());
- return $node;
- }
- break;
- case $node instanceof Node\Scalar\MagicConst\Function_:
- // Rewrite __FUNCTION__ to $__function__ variable.
- return new Variable('__function__');
- break;
- case $node instanceof Node\Scalar\MagicConst\Method:
- // Rewrite __METHOD__ to $__method__ variable.
- return new Variable('__method__');
- break;
}
}
@@ -188,26 +169,6 @@ class ProxyCallVisitor extends NodeVisitorAbstract
return new TraitUse($traits);
}
- /**
- * Format a normal class method of no need proxy call.
- */
- private function formatMethod(ClassMethod $node)
- {
- if ($node->name->toString() === '__construct') {
- // Rewrite parent::__construct to class::__construct.
- foreach ($node->stmts as $stmt) {
- if ($stmt instanceof Expression && $stmt->expr instanceof Node\Expr\StaticCall) {
- $class = $stmt->expr->class;
- if ($class instanceof Node\Name && $class->toString() === 'parent') {
- $stmt->expr->class = new Node\Name($this->extends->toCodeString());
- }
- }
- }
- }
-
- return $node;
- }
-
/**
* Rewrite a normal class method to a proxy call method,
* include normal class method and static method.
@@ -215,23 +176,19 @@ class ProxyCallVisitor extends NodeVisitorAbstract
private function rewriteMethod(ClassMethod $node): ClassMethod
{
// Build the static proxy call method base on the original method.
- if (! $this->class) {
- return $node;
- }
$shouldReturn = true;
$returnType = $node->getReturnType();
if ($returnType instanceof Identifier && $returnType->name === 'void') {
$shouldReturn = false;
}
- $class = $this->class->toString();
$staticCall = new StaticCall(new Name('self'), '__proxyCall', [
- // OriginalClass::class
- new Node\Arg(new ClassConstFetch(new Name($class), new Identifier('class'))),
+ // __CLASS__
+ new Node\Arg(new Node\Scalar\MagicConst\Class_()),
// __FUNCTION__
new Node\Arg(new MagicConstFunction()),
// self::getParamMap(OriginalClass::class, __FUNCTION, func_get_args())
- new Node\Arg(new StaticCall(new Name('self'), 'getParamsMap', [
- new Node\Arg(new ClassConstFetch(new Name($class), new Identifier('class'))),
+ new Node\Arg(new StaticCall(new Name('self'), '__getParamsMap', [
+ new Node\Arg(new Node\Scalar\MagicConst\Class_()),
new Node\Arg(new MagicConstFunction()),
new Node\Arg(new FuncCall(new Name('func_get_args'))),
])),
@@ -249,25 +206,15 @@ class ProxyCallVisitor extends NodeVisitorAbstract
}
return $params;
}),
- 'uses' => [
- new Variable('__function__'),
- new Variable('__method__'),
- ],
'stmts' => $node->stmts,
])),
]);
- $magicConstFunction = new Expression(new Assign(new Variable('__function__'), new Node\Scalar\MagicConst\Function_()));
- $magicConstMethod = new Expression(new Assign(new Variable('__method__'), new Node\Scalar\MagicConst\Method()));
if ($shouldReturn) {
$node->stmts = [
- $magicConstFunction,
- $magicConstMethod,
new Return_($staticCall),
];
} else {
$node->stmts = [
- $magicConstFunction,
- $magicConstMethod,
new Expression($staticCall),
];
}
@@ -280,7 +227,7 @@ class ProxyCallVisitor extends NodeVisitorAbstract
return false;
}
- $rewriteCollection = Aspect::parse($this->classname);
+ $rewriteCollection = Aspect::parse($this->visitorMetadata->className);
return $rewriteCollection->shouldRewrite($node->name->toString());
}
diff --git a/src/di/src/Aop/ProxyClassNameVisitor.php b/src/di/src/Aop/ProxyClassNameVisitor.php
deleted file mode 100644
index e77450716..000000000
--- a/src/di/src/Aop/ProxyClassNameVisitor.php
+++ /dev/null
@@ -1,42 +0,0 @@
-proxyClassName = $proxyClassName;
- }
-
- public function leaveNode(Node $node)
- {
- // Rewirte the class name and extends the original class.
- if ($node instanceof Node\Stmt\Class_ && ! $node->isAnonymous()) {
- $node->extends = new Node\Name($node->name->name);
- $node->name = new Node\Identifier($this->proxyClassName);
- return $node;
- }
- }
-}
diff --git a/src/di/src/Aop/ProxyManager.php b/src/di/src/Aop/ProxyManager.php
new file mode 100644
index 000000000..0559a8983
--- /dev/null
+++ b/src/di/src/Aop/ProxyManager.php
@@ -0,0 +1,248 @@
+classMap = $this->mergeClassMap($reflectionClassMap, $composerLoaderClassMap);
+ $this->proxyDir = $proxyDir;
+ $this->filesystem = new Filesystem();
+ $this->proxies = $this->generateProxyFiles($this->initProxiesByReflectionClassMap(
+ $this->classMap
+ ));
+ }
+
+ public function getProxies(): array
+ {
+ return $this->proxies;
+ }
+
+ public function getProxyDir(): string
+ {
+ return $this->proxyDir;
+ }
+
+ /**
+ * @param ReflectionClass[] $reflectionClassMap
+ */
+ protected function mergeClassMap(array $reflectionClassMap, array $composerLoaderClassMap): array
+ {
+ $classMap = [];
+ foreach ($reflectionClassMap as $class) {
+ $classMap[$class->getName()] = $class->getFileName();
+ }
+
+ return array_merge($classMap, $composerLoaderClassMap);
+ }
+
+ protected function generateProxyFiles(array $proxies = []): array
+ {
+ $proxyFiles = [];
+ if (! $proxies) {
+ return $proxyFiles;
+ }
+ if (! file_exists($this->getProxyDir())) {
+ mkdir($this->getProxyDir(), 0755, true);
+ }
+ // WARNING: Ast class SHOULD NOT use static instance, because it will read the code from file, then would be caused coroutine switch.
+ $ast = new Ast();
+ foreach ($proxies as $className => $aspects) {
+ $proxyFiles[$className] = $this->putProxyFile($ast, $className);
+ }
+ return $proxyFiles;
+ }
+
+ protected function putProxyFile(Ast $ast, $className)
+ {
+ $proxyFilePath = $this->getProxyFilePath($className);
+ $modified = true;
+ if (file_exists($proxyFilePath)) {
+ $modified = $this->isModified($className, $proxyFilePath);
+ }
+
+ if ($modified) {
+ $code = $ast->proxy($className);
+ file_put_contents($proxyFilePath, $code);
+ }
+
+ return $proxyFilePath;
+ }
+
+ protected function isModified(string $className, string $proxyFilePath = null): bool
+ {
+ $proxyFilePath = $proxyFilePath ?? $this->getProxyFilePath($className);
+ $time = $this->filesystem->lastModified($proxyFilePath);
+ $origin = $this->classMap[$className];
+ if ($time > $this->filesystem->lastModified($origin)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function getProxyFilePath($className)
+ {
+ return $this->getProxyDir() . str_replace('\\', '_', $className) . '.proxy.php';
+ }
+
+ protected function isMatch(string $rule, string $target): bool
+ {
+ if (strpos($rule, '::') !== false) {
+ [$rule,] = explode('::', $rule);
+ }
+ if (strpos($rule, '*') === false && $rule === $target) {
+ return true;
+ }
+ $preg = str_replace(['*', '\\'], ['.*', '\\\\'], $rule);
+ $pattern = "/^{$preg}$/";
+
+ if (preg_match($pattern, $target)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ protected function initProxiesByReflectionClassMap(array $reflectionClassMap = []): array
+ {
+ // According to the data of AspectCollector to parse all the classes that need proxy.
+ $proxies = [];
+ if (! $reflectionClassMap) {
+ return $proxies;
+ }
+ $classesAspects = AspectCollector::get('classes', []);
+ foreach ($classesAspects as $aspect => $rules) {
+ foreach ($rules as $rule) {
+ foreach ($reflectionClassMap as $class => $path) {
+ if (! $this->isMatch($rule, $class)) {
+ continue;
+ }
+ $proxies[$class][] = $aspect;
+ }
+ }
+ }
+
+ foreach ($reflectionClassMap as $class => $path) {
+ $className = $class;
+ // Aggregate the class annotations
+ $classAnnotations = $this->retrieveAnnotations($className . '._c');
+ // Aggregate all methods annotations
+ $methodAnnotations = $this->retrieveAnnotations($className . '._m');
+ // Aggregate all properties annotations
+ $propertyAnnotations = $this->retrieveAnnotations($className . '._p');
+ $annotations = array_unique(array_merge($classAnnotations, $methodAnnotations, $propertyAnnotations));
+ if ($annotations) {
+ $annotationsAspects = AspectCollector::get('annotations', []);
+ foreach ($annotationsAspects as $aspect => $rules) {
+ foreach ($rules as $rule) {
+ foreach ($annotations as $annotation) {
+ if ($this->isMatch($rule, $annotation)) {
+ $proxies[$className][] = $aspect;
+ }
+ }
+ }
+ }
+ }
+ }
+ return $proxies;
+ }
+
+ protected function initProxiesByComposerClassMap(array $classMap = []): array
+ {
+ $proxies = [];
+ if (! $classMap) {
+ return $proxies;
+ }
+ $classAspects = $this->getClassAspects();
+ if ($classAspects) {
+ foreach ($classMap as $className => $file) {
+ $match = [];
+ foreach ($classAspects as $aspect => $rules) {
+ foreach ($rules as $rule) {
+ if ($this->isMatch($rule, $className)) {
+ $match[] = $aspect;
+ }
+ }
+ }
+ if ($match) {
+ $match = array_flip(array_flip($match));
+ $proxies[$className] = $match;
+ }
+ }
+ }
+
+ return $proxies;
+ }
+
+ protected function getClassAspects(): array
+ {
+ $aspects = AspectCollector::get('classes', []);
+ // Remove the useless aspect rules
+ foreach ($aspects as $aspect => $rules) {
+ if (! $rules) {
+ unset($aspects[$aspect]);
+ }
+ }
+ return $aspects;
+ }
+
+ protected function retrieveAnnotations(string $annotationCollectorKey): array
+ {
+ $defined = [];
+ $annotations = AnnotationCollector::get($annotationCollectorKey, []);
+
+ foreach ($annotations as $k => $annotation) {
+ if (is_object($annotation)) {
+ $defined[] = $k;
+ } else {
+ $defined = array_merge($defined, array_keys($annotation));
+ }
+ }
+ return $defined;
+ }
+}
diff --git a/src/di/src/Aop/ProxyTrait.php b/src/di/src/Aop/ProxyTrait.php
index a46cc915b..a8881b8a9 100644
--- a/src/di/src/Aop/ProxyTrait.php
+++ b/src/di/src/Aop/ProxyTrait.php
@@ -19,13 +19,20 @@ use Hyperf\Utils\ApplicationContext;
trait ProxyTrait
{
+ /**
+ * Cache the aspects for the proxy class.
+ *
+ * @var array
+ */
+ protected static $aspects;
+
protected static function __proxyCall(
- string $originalClassName,
+ string $className,
string $method,
array $arguments,
Closure $closure
) {
- $proceedingJoinPoint = new ProceedingJoinPoint($closure, $originalClassName, $method, $arguments);
+ $proceedingJoinPoint = new ProceedingJoinPoint($closure, $className, $method, $arguments);
$result = self::handleAround($proceedingJoinPoint);
unset($proceedingJoinPoint);
return $result;
@@ -34,7 +41,7 @@ trait ProxyTrait
/**
* @TODO This method will be called everytime, should optimize it later.
*/
- protected static function getParamsMap(string $className, string $method, array $args): array
+ protected static function __getParamsMap(string $className, string $method, array $args): array
{
$map = [
'keys' => [],
@@ -55,30 +62,49 @@ trait ProxyTrait
return $map;
}
- private static function handleAround(ProceedingJoinPoint $proceedingJoinPoint)
+ protected static function handleAround(ProceedingJoinPoint $proceedingJoinPoint)
{
- $aspects = self::getAspects($proceedingJoinPoint->className, $proceedingJoinPoint->methodName);
- $annotationAspects = self::getAnnotationAspects($proceedingJoinPoint->className, $proceedingJoinPoint->methodName);
- $aspects = array_unique(array_merge($aspects, $annotationAspects));
- if (empty($aspects)) {
+ $className = $proceedingJoinPoint->className;
+ $methodName = $proceedingJoinPoint->methodName;
+ if (! isset(static::$aspects[$className][$methodName])) {
+ static::$aspects[$className][$methodName] = [];
+ $aspects = array_unique(array_merge(static::getClassesAspects($className, $methodName), static::getAnnotationAspects($className, $methodName)));
+ $queue = new \SplPriorityQueue();
+ foreach ($aspects as $aspect) {
+ $queue->insert($aspect, AspectCollector::getPriority($aspect));
+ }
+ while ($queue->valid()) {
+ static::$aspects[$className][$methodName][] = $queue->current();
+ $queue->next();
+ }
+
+ unset($annotationAspects, $aspects, $queue);
+ }
+
+ if (empty(static::$aspects[$className][$methodName])) {
return $proceedingJoinPoint->processOriginalMethod();
}
- $container = ApplicationContext::getContainer();
- if (method_exists($container, 'make')) {
- $pipeline = $container->make(Pipeline::class);
- } else {
- $pipeline = new Pipeline($container);
- }
- return $pipeline->via('process')
- ->through($aspects)
+ return static::makePipeline()->via('process')
+ ->through(static::$aspects[$className][$methodName])
->send($proceedingJoinPoint)
->then(function (ProceedingJoinPoint $proceedingJoinPoint) {
return $proceedingJoinPoint->processOriginalMethod();
});
}
- private static function getAspects(string $className, string $method): array
+ protected static function makePipeline(): Pipeline
+ {
+ $container = ApplicationContext::getContainer();
+ if (method_exists($container, 'make')) {
+ $pipeline = $container->make(Pipeline::class);
+ } else {
+ $pipeline = new Pipeline($container);
+ }
+ return $pipeline;
+ }
+
+ protected static function getClassesAspects(string $className, string $method): array
{
$aspects = AspectCollector::get('classes', []);
$matchedAspect = [];
@@ -94,7 +120,7 @@ trait ProxyTrait
return $matchedAspect;
}
- private static function getAnnotationAspects(string $className, string $method): array
+ protected static function getAnnotationAspects(string $className, string $method): array
{
$matchedAspect = $annotations = $rules = [];
diff --git a/src/di/src/Aop/RegisterInjectPropertyHandler.php b/src/di/src/Aop/RegisterInjectPropertyHandler.php
new file mode 100644
index 000000000..69ae5e4e0
--- /dev/null
+++ b/src/di/src/Aop/RegisterInjectPropertyHandler.php
@@ -0,0 +1,47 @@
+setAccessible(true);
+ $container = ApplicationContext::getContainer();
+ if ($container->has($annotation->value)) {
+ $reflectionProperty->setValue($object, $container->get($annotation->value));
+ } elseif ($annotation->required) {
+ throw new NotFoundException("No entry or class found for '{$annotation->value}'");
+ }
+ } catch (\Throwable $throwable) {
+ if ($annotation->required) {
+ throw $throwable;
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/src/di/src/Aop/VisitorMetadata.php b/src/di/src/Aop/VisitorMetadata.php
new file mode 100644
index 000000000..7781db4e0
--- /dev/null
+++ b/src/di/src/Aop/VisitorMetadata.php
@@ -0,0 +1,39 @@
+{$name} ?? null;
+ }
+
+ public function __set($name, $value)
+ {
+ $this->{$name} = $value;
+ }
+
+ public function __isset($name)
+ {
+ return isset($this->{$name});
+ }
+
+ public function __unset($name)
+ {
+ unset($this->{$name});
+ }
+}
diff --git a/src/di/src/BetterReflectionManager.php b/src/di/src/BetterReflectionManager.php
new file mode 100644
index 000000000..e6c35f8b2
--- /dev/null
+++ b/src/di/src/BetterReflectionManager.php
@@ -0,0 +1,98 @@
+astLocator();
+ $stubber = $reflection->sourceStubber();
+ $parser = $reflection->phpParser();
+ static::$instance = new ClassReflector(new MemoizingSourceLocator(new AggregateSourceLocator([
+ new DirectoriesSourceLocator($paths, $astLocator),
+ new PhpInternalSourceLocator($astLocator, $stubber),
+ new EvaledCodeSourceLocator($astLocator, $stubber),
+ new AutoloadSourceLocator($astLocator, $parser),
+ ])));
+ return static::$instance;
+ }
+
+ public static function reflectClass(string $className, ?ReflectionClass $reflection = null): ReflectionClass
+ {
+ if (! isset(static::$container['class'][$className])) {
+ static::$container['class'][$className] = $reflection ?? static::getClassReflector()->reflect($className);
+ }
+ return static::$container['class'][$className];
+ }
+
+ public static function reflectMethod(string $className, string $method): ReflectionMethod
+ {
+ $key = $className . '::' . $method;
+ if (! isset(static::$container['method'][$key])) {
+ $reflectionClass = static::reflectClass($className);
+ $methods = $reflectionClass->getImmediateMethods();
+ static::$container['method'][$key] = $methods($method);
+ }
+ return static::$container['method'][$key];
+ }
+
+ public static function reflectProperty(string $className, string $property): ReflectionProperty
+ {
+ $key = $className . '::' . $property;
+ if (! isset(static::$container['property'][$key])) {
+ $reflectionClass = static::reflectClass($className);
+ $properties = $reflectionClass->getImmediateProperties();
+ static::$container['property'][$key] = $properties[$property];
+ }
+ return static::$container['property'][$key];
+ }
+
+ public static function clear(?string $key = null): void
+ {
+ if ($key === null) {
+ static::$container = [];
+ static::$instance = null;
+ }
+ }
+}
diff --git a/src/di/src/ClassLoader.php b/src/di/src/ClassLoader.php
new file mode 100644
index 000000000..fe49364ac
--- /dev/null
+++ b/src/di/src/ClassLoader.php
@@ -0,0 +1,146 @@
+ ProxyFileAbsolutePath ].
+ *
+ * @var array
+ */
+ protected $proxies = [];
+
+ public function __construct(ComposerClassLoader $classLoader, string $proxyFileDir, string $configDir)
+ {
+ $this->setComposerClassLoader($classLoader);
+ if (file_exists(BASE_PATH . '/.env')) {
+ $this->loadDotenv();
+ }
+
+ // Scan by ScanConfig to generate the reflection class map
+ $scanner = new Scanner($this, $config = ScanConfig::instance($configDir));
+ $classLoader->addClassMap($config->getClassMap());
+ timepoint();
+ $reflectionClassMap = $scanner->scan();
+ timepoint('Scan');
+ // Get the class map of Composer loader
+ $composerLoaderClassMap = $this->getComposerClassLoader()->getClassMap();
+ $proxyManager = new ProxyManager($reflectionClassMap, $composerLoaderClassMap, $proxyFileDir);
+ timepoint('InitProxyManager');
+ $this->proxies = $proxyManager->getProxies();
+ }
+
+ public function loadClass(string $class): void
+ {
+ $path = $this->locateFile($class);
+
+ if ($path) {
+ include $path;
+ }
+ }
+
+ public static function init(?string $proxyFileDirPath = null, ?string $configDir = null): void
+ {
+ if (! $proxyFileDirPath) {
+ // This dir is the default proxy file dir path of Hyperf
+ $proxyFileDirPath = BASE_PATH . '/runtime/container/proxy/';
+ }
+
+ if (! $configDir) {
+ // This dir is the default proxy file dir path of Hyperf
+ $configDir = BASE_PATH . '/config/';
+ }
+
+ $loaders = spl_autoload_functions();
+
+ // Proxy the composer class loader
+ foreach ($loaders as &$loader) {
+ $unregisterLoader = $loader;
+ if (is_array($loader) && $loader[0] instanceof ComposerClassLoader) {
+ /** @var ComposerClassLoader $composerClassLoader */
+ $composerClassLoader = $loader[0];
+ AnnotationRegistry::registerLoader(function ($class) use ($composerClassLoader) {
+ return (bool) $composerClassLoader->findFile($class);
+ });
+ $loader[0] = new static($composerClassLoader, $proxyFileDirPath, $configDir);
+ }
+ spl_autoload_unregister($unregisterLoader);
+ }
+
+ unset($loader);
+
+ // Re-register the loaders
+ foreach ($loaders as $loader) {
+ spl_autoload_register($loader);
+ }
+
+ // Initialize Lazy Loader. This will prepend LazyLoader to the top of autoload queue.
+ LazyLoader::bootstrap($configDir);
+ }
+
+ public function setComposerClassLoader(ComposerClassLoader $classLoader): self
+ {
+ $this->composerClassLoader = $classLoader;
+ // Set the ClassLoader to Hyperf\Utils\Composer to avoid unnecessary find process.
+ Composer::setLoader($classLoader);
+ return $this;
+ }
+
+ public function getComposerClassLoader(): ComposerClassLoader
+ {
+ return $this->composerClassLoader;
+ }
+
+ protected function locateFile(string $className): ?string
+ {
+ if (isset($this->proxies[$className]) && file_exists($this->proxies[$className])) {
+ $file = $this->proxies[$className];
+ } else {
+ $file = $this->getComposerClassLoader()->findFile($className);
+ }
+
+ return is_string($file) ? $file : null;
+ }
+
+ private function loadDotenv(): void
+ {
+ $repository = RepositoryBuilder::create()
+ ->withReaders([
+ new Adapter\PutenvAdapter(),
+ ])
+ ->withWriters([
+ new Adapter\PutenvAdapter(),
+ ])
+ ->immutable()
+ ->make();
+
+ Dotenv::create($repository, [BASE_PATH])->load();
+ }
+}
diff --git a/src/di/src/Command/InitProxyCommand.php b/src/di/src/Command/InitProxyCommand.php
deleted file mode 100644
index e7a5e8f09..000000000
--- a/src/di/src/Command/InitProxyCommand.php
+++ /dev/null
@@ -1,105 +0,0 @@
-container = $container;
- $this->scanner = $scanner;
- }
-
- public function handle()
- {
- $this->warn('This command does not clear the runtime cache, If you want to delete them, use `vendor/bin/init-proxy.sh` instead.');
-
- $this->createAopProxies();
-
- Timer::clearAll();
-
- $this->output->writeln('Proxy class create success.');
- }
-
- protected function getScanDir()
- {
- if (! defined('BASE_PATH')) {
- throw new LogicException('BASE_PATH is not defined.');
- }
-
- $file = BASE_PATH . '/config/autoload/annotations.php';
- if (! file_exists($file)) {
- throw new LogicException(sprintf('Annotations config path[%s] is not exists.', $file));
- }
-
- $annotations = include $file;
- $scanDirs = $annotations['scan']['paths'] ?? [];
- if (class_exists(ProviderConfig::class)) {
- $configFromProviders = ProviderConfig::load();
- $scanDirs = array_merge($configFromProviders['annotations']['scan']['paths'] ?? [], $scanDirs);
- }
-
- return $scanDirs;
- }
-
- private function createAopProxies()
- {
- $scanDirs = $this->getScanDir();
-
- $meta = $this->scanner->scan($scanDirs);
- $classCollection = array_keys($meta);
-
- foreach ($classCollection as $item) {
- try {
- $this->container->get($item);
- } catch (\Throwable $ex) {
- // Entry cannot be resolved.
- }
- }
-
- if ($this->container instanceof Container) {
- foreach ($this->container->getDefinitionSource()->getDefinitions() as $key => $definition) {
- try {
- $this->container->get($key);
- } catch (\Throwable $ex) {
- // Entry cannot be resolved.
- }
- }
- }
- }
-}
diff --git a/src/di/src/ConfigProvider.php b/src/di/src/ConfigProvider.php
index 93bbe943a..d6180243c 100644
--- a/src/di/src/ConfigProvider.php
+++ b/src/di/src/ConfigProvider.php
@@ -13,23 +13,30 @@ namespace Hyperf\Di;
use Hyperf\Di\Annotation\AnnotationCollector;
use Hyperf\Di\Annotation\AspectCollector;
-use Hyperf\Di\Command\InitProxyCommand;
-use Hyperf\Di\Listener\BootApplicationListener;
+use Hyperf\Di\Annotation\InjectAspect;
+use Hyperf\Di\Aop\AstVisitorRegistry;
+use Hyperf\Di\Aop\PropertyHandlerVisitor;
+use Hyperf\Di\Aop\ProxyCallVisitor;
+use Hyperf\Di\Aop\RegisterInjectPropertyHandler;
class ConfigProvider
{
public function __invoke(): array
{
+ // Register AST visitors to the collector.
+ AstVisitorRegistry::insert(PropertyHandlerVisitor::class, PHP_INT_MAX / 2);
+ AstVisitorRegistry::insert(ProxyCallVisitor::class, PHP_INT_MAX / 2);
+
+ // Register Property Handler.
+ RegisterInjectPropertyHandler::register();
+
return [
'dependencies' => [
MethodDefinitionCollectorInterface::class => MethodDefinitionCollector::class,
ClosureDefinitionCollectorInterface::class => ClosureDefinitionCollector::class,
],
- 'commands' => [
- InitProxyCommand::class,
- ],
- 'listeners' => [
- BootApplicationListener::class,
+ 'aspects' => [
+ InjectAspect::class,
],
'annotations' => [
'scan' => [
diff --git a/src/di/src/Container.php b/src/di/src/Container.php
index 5fcd7aeec..b1404ad5b 100644
--- a/src/di/src/Container.php
+++ b/src/di/src/Container.php
@@ -43,12 +43,6 @@ class Container implements HyperfContainerInterface
*/
private $definitionResolver;
- /**
- * @TODO Extract ProxyFactory to a Interface.
- * @var ProxyFactory
- */
- private $proxyFactory;
-
/**
* Container constructor.
*/
@@ -56,13 +50,11 @@ class Container implements HyperfContainerInterface
{
$this->definitionSource = $definitionSource;
$this->definitionResolver = new ResolverDispatcher($this);
- $this->proxyFactory = new ProxyFactory();
// Auto-register the container.
$this->resolvedEntries = [
self::class => $this,
PsrContainerInterface::class => $this,
HyperfContainerInterface::class => $this,
- ProxyFactory::class => $this->proxyFactory,
];
}
@@ -156,11 +148,6 @@ class Container implements HyperfContainerInterface
return true;
}
- public function getProxyFactory(): ProxyFactory
- {
- return $this->proxyFactory;
- }
-
public function getDefinitionSource(): Definition\DefinitionSourceInterface
{
return $this->definitionSource;
diff --git a/src/di/src/Definition/DefinitionInterface.php b/src/di/src/Definition/DefinitionInterface.php
index 22a45a65f..27ba57ae5 100644
--- a/src/di/src/Definition/DefinitionInterface.php
+++ b/src/di/src/Definition/DefinitionInterface.php
@@ -27,9 +27,4 @@ interface DefinitionInterface
* Set the name of the entry in the container.
*/
public function setName(string $name);
-
- /**
- * Determine if the definition need to transfer to a proxy class.
- */
- public function isNeedProxy(): bool;
}
diff --git a/src/di/src/Definition/DefinitionSource.php b/src/di/src/Definition/DefinitionSource.php
index 8a72b07ba..e4dd77e62 100644
--- a/src/di/src/Definition/DefinitionSource.php
+++ b/src/di/src/Definition/DefinitionSource.php
@@ -11,69 +11,24 @@ declare(strict_types=1);
*/
namespace Hyperf\Di\Definition;
-use Hyperf\Di\Annotation\AnnotationCollector;
-use Hyperf\Di\Annotation\AspectCollector;
-use Hyperf\Di\Annotation\Inject;
-use Hyperf\Di\Annotation\Scanner;
-use Hyperf\Di\Aop\AstCollector;
use Hyperf\Di\ReflectionManager;
-use Hyperf\Utils\Str;
-use ReflectionClass;
use ReflectionFunctionAbstract;
-use Symfony\Component\Finder\Finder;
use function class_exists;
-use function count;
-use function explode;
-use function feof;
-use function fgets;
-use function file_exists;
-use function file_put_contents;
-use function filemtime;
-use function fopen;
-use function implode;
use function interface_exists;
use function is_callable;
-use function is_dir;
-use function is_readable;
use function is_string;
-use function md5;
use function method_exists;
-use function preg_match;
use function print_r;
-use function str_replace;
-use function trim;
class DefinitionSource implements DefinitionSourceInterface
{
- /**
- * @var bool
- */
- private $enableCache = false;
-
- /**
- * Path of annotation meta data cache.
- *
- * @var string
- */
- private $cachePath = BASE_PATH . '/runtime/container/annotations';
-
/**
* @var array
*/
private $source;
- /**
- * @var Scanner
- */
- private $scanner;
-
- public function __construct(array $source, ScanConfig $scanConfig, bool $enableCache = false)
+ public function __construct(array $source, ScanConfig $scanConfig)
{
- $this->scanner = new Scanner($scanConfig->getIgnoreAnnotations());
- $this->enableCache = $enableCache;
-
- // Scan the specified paths and collect the ast and annotations.
- $this->scan($scanConfig->getDirs(), $scanConfig->getCollectors());
$this->source = $this->normalizeSource($source);
}
@@ -184,198 +139,11 @@ class DefinitionSource implements DefinitionSourceInterface
$definition->completeConstructorInjection($constructorInjection);
}
- /**
- * Properties.
- */
- $propertiesMetadata = AnnotationCollector::get($className);
- $propertyHandlers = PropertyHandlerManager::all();
- if (isset($propertiesMetadata['_p'])) {
- foreach ($propertiesMetadata['_p'] as $propertyName => $value) {
- // Because `@Inject` is a internal logical of DI component, so leave the code here.
- /** @var Inject $injectAnnotation */
- if ($injectAnnotation = $value[Inject::class] ?? null) {
- $propertyInjection = new PropertyInjection($propertyName, new Reference($injectAnnotation->value));
- $definition->addPropertyInjection($propertyInjection);
- }
- // Handle PropertyHandler mechanism.
- foreach ($value as $annotationClassName => $annotationObject) {
- if (isset($propertyHandlers[$annotationClassName])) {
- foreach ($propertyHandlers[$annotationClassName] ?? [] as $callback) {
- call($callback, [$definition, $propertyName, $annotationObject]);
- }
- }
- }
- }
- }
-
- $definition->setNeedProxy($this->isNeedProxy($class));
-
return $definition;
}
- private function scan(array $paths, array $collectors): bool
- {
- $appPaths = $vendorPaths = [];
-
- /**
- * If you are a hyperf developer
- * this value will be your local path, like hyperf/src.
- * @var string
- */
- $ident = 'vendor';
- $isDefinedBasePath = defined('BASE_PATH');
-
- foreach ($paths as $path) {
- if ($isDefinedBasePath) {
- if (Str::startsWith($path, BASE_PATH . '/' . $ident)) {
- $vendorPaths[] = $path;
- } else {
- $appPaths[] = $path;
- }
- } else {
- if (strpos($path, $ident) !== false) {
- $vendorPaths[] = $path;
- } else {
- $appPaths[] = $path;
- }
- }
- }
-
- $this->loadMetadata($appPaths, 'app');
- $this->loadMetadata($vendorPaths, 'vendor');
-
- return true;
- }
-
- private function loadMetadata(array $paths, $type)
- {
- if (empty($paths)) {
- return true;
- }
- $cachePath = $this->cachePath . '.' . $type . '.cache';
- $pathsHash = md5(implode(',', $paths));
- if ($this->hasAvailableCache($paths, $pathsHash, $cachePath)) {
- $this->printLn('Detected an available cache, skip the ' . $type . ' scan process.');
- [, $serialized] = explode(PHP_EOL, file_get_contents($cachePath));
- $this->scanner->collect(unserialize($serialized));
- return false;
- }
- $this->printLn('Scanning ' . $type . ' ...');
- $startTime = microtime(true);
- $meta = $this->scanner->scan($paths);
- foreach ($meta as $className => $stmts) {
- AstCollector::set($className, $stmts);
- }
- $useTime = microtime(true) - $startTime;
- $this->printLn('Scan ' . $type . ' completed, took ' . $useTime * 1000 . ' milliseconds.');
- if (! $this->enableCache) {
- return true;
- }
- // enableCache: set cache
- if (! file_exists($cachePath)) {
- $exploded = explode('/', $cachePath);
- unset($exploded[count($exploded) - 1]);
- $dirPath = implode('/', $exploded);
- if (! is_dir($dirPath)) {
- mkdir($dirPath, 0755, true);
- }
- }
-
- $data = implode(PHP_EOL, [$pathsHash, serialize(array_keys($meta))]);
- file_put_contents($cachePath, $data);
- return true;
- }
-
- private function hasAvailableCache(array $paths, string $pathsHash, string $filename): bool
- {
- if (! $this->enableCache) {
- return false;
- }
- if (! file_exists($filename) || ! is_readable($filename)) {
- return false;
- }
- $handler = fopen($filename, 'r');
- while (! feof($handler)) {
- $line = fgets($handler);
- if (trim($line) !== $pathsHash) {
- return false;
- }
- break;
- }
- $cacheLastModified = filemtime($filename) ?? 0;
- $finder = new Finder();
- $finder->files()->in($paths)->name('*.php');
- foreach ($finder as $file) {
- if ($file->getMTime() > $cacheLastModified) {
- return false;
- }
- }
- return true;
- }
-
private function printLn(string $message): void
{
print_r($message . PHP_EOL);
}
-
- private function isNeedProxy(ReflectionClass $reflectionClass): bool
- {
- $className = $reflectionClass->getName();
- $classesAspects = AspectCollector::get('classes', []);
- foreach ($classesAspects as $aspect => $rules) {
- foreach ($rules as $rule) {
- if ($this->isMatch($rule, $className)) {
- return true;
- }
- }
- }
-
- // Get the controller annotations.
- $classAnnotations = value(function () use ($className) {
- $annotations = AnnotationCollector::get($className . '._c', []);
- return array_keys($annotations);
- });
- // Aggregate all methods annotations.
- $methodAnnotations = value(function () use ($className) {
- $defined = [];
- $annotations = AnnotationCollector::get($className . '._m', []);
- foreach ($annotations as $method => $annotation) {
- $defined = array_merge($defined, array_keys($annotation));
- }
- return $defined;
- });
- $annotations = array_unique(array_merge($classAnnotations, $methodAnnotations));
- if ($annotations) {
- $annotationsAspects = AspectCollector::get('annotations', []);
- foreach ($annotationsAspects as $aspect => $rules) {
- foreach ($rules as $rule) {
- foreach ($annotations as $annotation) {
- if ($this->isMatch($rule, $annotation)) {
- return true;
- }
- }
- }
- }
- }
-
- return false;
- }
-
- private function isMatch(string $rule, string $target): bool
- {
- if (strpos($rule, '::') !== false) {
- [$rule,] = explode('::', $rule);
- }
- if (strpos($rule, '*') === false && $rule === $target) {
- return true;
- }
- $preg = str_replace(['*', '\\'], ['.*', '\\\\'], $rule);
- $pattern = "/^{$preg}$/";
-
- if (preg_match($pattern, $target)) {
- return true;
- }
-
- return false;
- }
}
diff --git a/src/di/src/Definition/ObjectDefinition.php b/src/di/src/Definition/ObjectDefinition.php
index 1f9ed21fb..597858c5f 100644
--- a/src/di/src/Definition/ObjectDefinition.php
+++ b/src/di/src/Definition/ObjectDefinition.php
@@ -20,11 +20,6 @@ class ObjectDefinition implements DefinitionInterface
*/
protected $constructorInjection;
- /**
- * @var PropertyInjection[]
- */
- protected $propertyInjections = [];
-
/**
* @var string
*/
@@ -45,16 +40,6 @@ class ObjectDefinition implements DefinitionInterface
*/
private $instantiable = false;
- /**
- * @var bool
- */
- private $needProxy = false;
-
- /**
- * @var string
- */
- private $proxyClassName;
-
public function __construct(string $name, string $className = null)
{
$this->name = $name;
@@ -128,44 +113,6 @@ class ObjectDefinition implements DefinitionInterface
}
}
- /**
- * @return PropertyInjection[]
- */
- public function getPropertyInjections(): array
- {
- return $this->propertyInjections;
- }
-
- public function addPropertyInjection(PropertyInjection $propertyInjection): void
- {
- $this->propertyInjections[$propertyInjection->getPropertyName()] = $propertyInjection;
- }
-
- public function getProxyClassName(): string
- {
- return $this->proxyClassName;
- }
-
- public function setProxyClassName(string $proxyClassName): self
- {
- $this->proxyClassName = $proxyClassName;
- return $this;
- }
-
- /**
- * Determine if the definition need to transfer to a proxy class.
- */
- public function isNeedProxy(): bool
- {
- return $this->needProxy;
- }
-
- public function setNeedProxy(bool $needProxy): self
- {
- $this->needProxy = $needProxy;
- return $this;
- }
-
private function updateStatusCache(): void
{
$className = $this->getClassName();
diff --git a/src/di/src/Definition/PropertyInjection.php b/src/di/src/Definition/PropertyInjection.php
deleted file mode 100644
index ee280b11f..000000000
--- a/src/di/src/Definition/PropertyInjection.php
+++ /dev/null
@@ -1,53 +0,0 @@
-propertyName = $propertyName;
- $this->value = $value;
- }
-
- public function getPropertyName(): string
- {
- return $this->propertyName;
- }
-
- /**
- * @return mixed Value that should be injected in the property
- */
- public function getValue()
- {
- return $this->value;
- }
-}
diff --git a/src/di/src/LazyLoader/LazyLoader.php b/src/di/src/LazyLoader/LazyLoader.php
index dbe6c0239..5d7120181 100644
--- a/src/di/src/LazyLoader/LazyLoader.php
+++ b/src/di/src/LazyLoader/LazyLoader.php
@@ -11,7 +11,6 @@ declare(strict_types=1);
*/
namespace Hyperf\Di\LazyLoader;
-use Hyperf\Contract\ConfigInterface;
use Hyperf\Utils\Coroutine\Locker as CoLocker;
use Hyperf\Utils\Str;
use PhpParser\NodeTraverser;
@@ -22,7 +21,7 @@ use Roave\BetterReflection\Reflection\ReflectionClass;
class LazyLoader
{
- public const CONFIG_FILE_NAME = 'lazy_loader';
+ public const CONFIG_FILE_NAME = 'lazy_loader.php';
/**
* Indicates if a loader has been registered.
@@ -45,17 +44,25 @@ class LazyLoader
*/
protected $config;
- private function __construct(ConfigInterface $config)
+ private function __construct(array $config)
{
- $this->config = $config->get(self::CONFIG_FILE_NAME, []);
+ $this->config = $config;
$this->register();
}
/**
* Get or create the singleton lazy loader instance.
*/
- public static function bootstrap(ConfigInterface $config): LazyLoader
+ public static function bootstrap(string $configDir): LazyLoader
{
+ $path = $configDir . self::CONFIG_FILE_NAME;
+
+ if (file_exists($path)) {
+ $config = include $configDir . self::CONFIG_FILE_NAME;
+ } else {
+ $config = [];
+ }
+
if (is_null(static::$instance)) {
static::$instance = new static($config);
}
@@ -69,7 +76,7 @@ class LazyLoader
*/
public function load(string $proxy)
{
- if (array_key_exists($proxy, $this->config) || Str::startsWith($proxy, 'HyperfLazy\\')) {
+ if (array_key_exists($proxy, $this->config) || $this->startsWith($proxy, 'HyperfLazy\\')) {
$this->loadProxy($proxy);
return true;
}
@@ -103,15 +110,17 @@ class LazyLoader
if (! file_exists($dir)) {
mkdir($dir, 0755, true);
}
- $path = str_replace('\\', '_', $dir . $proxy . '.lazy.php');
+
+ $code = $this->generatorLazyProxy(
+ $proxy,
+ $this->config[$proxy] ?? Str::after($proxy, 'HyperfLazy\\')
+ );
+
+ $path = str_replace('\\', '_', $dir . $proxy . '_' . crc32($code) . '.php');
$key = md5($path);
// If the proxy file does not exist, then try to acquire the coroutine lock.
if (! file_exists($path) && CoLocker::lock($key)) {
$targetPath = $path . '.' . uniqid();
- $code = $this->generatorLazyProxy(
- $proxy,
- $this->config[$proxy] ?? Str::after($proxy, 'HyperfLazy\\')
- );
file_put_contents($targetPath, $code);
rename($targetPath, $path);
CoLocker::unlock($key);
@@ -147,6 +156,11 @@ class LazyLoader
spl_autoload_register($load, true, true);
}
+ private function startsWith($haystack, $needle): bool
+ {
+ return substr($haystack, 0, strlen($needle)) === (string) $needle;
+ }
+
/**
* These conditions are really hard to proxy via inheritence.
* Luckily these conditions are very rarely met.
diff --git a/src/di/src/MetadataCollector.php b/src/di/src/MetadataCollector.php
index 09bd9284d..15e23141b 100644
--- a/src/di/src/MetadataCollector.php
+++ b/src/di/src/MetadataCollector.php
@@ -49,6 +49,15 @@ abstract class MetadataCollector implements MetadataCollectorInterface
return Arr::has(static::$container, $key);
}
+ public static function clear(?string $key = null): void
+ {
+ if ($key) {
+ Arr::forget(static::$container, [$key]);
+ } else {
+ static::$container = [];
+ }
+ }
+
/**
* Serialize the all metadata to a string.
*/
diff --git a/src/di/src/MetadataCollectorInterface.php b/src/di/src/MetadataCollectorInterface.php
index c89aad40f..cb6df204c 100644
--- a/src/di/src/MetadataCollectorInterface.php
+++ b/src/di/src/MetadataCollectorInterface.php
@@ -25,6 +25,8 @@ interface MetadataCollectorInterface
*/
public static function set(string $key, $value): void;
+ public static function clear(?string $key = null): void;
+
/**
* Serialize the all metadata to a string.
*/
diff --git a/src/di/src/ProxyFactory.php b/src/di/src/ProxyFactory.php
deleted file mode 100644
index 0e8c0b8f3..000000000
--- a/src/di/src/ProxyFactory.php
+++ /dev/null
@@ -1,70 +0,0 @@
-ast = new Ast();
- }
-
- public function createProxyDefinition(ObjectDefinition $definition): ObjectDefinition
- {
- $identifier = $definition->getName();
- if (isset(static::$map[$identifier])) {
- return static::$map[$identifier];
- }
-
- $proxyIdentifier = $definition->getClassName() . '_' . md5($definition->getClassName());
- $definition->setProxyClassName($proxyIdentifier);
- $this->loadProxy($definition->getClassName(), $definition->getProxyClassName());
-
- static::$map[$identifier] = $definition;
- return static::$map[$identifier];
- }
-
- private function loadProxy(string $className, string $proxyClassName): void
- {
- $dir = BASE_PATH . '/runtime/container/proxy/';
- if (! file_exists($dir)) {
- mkdir($dir, 0755, true);
- }
- $proxyFileName = str_replace('\\', '_', $className);
- $path = $dir . $proxyFileName . '.proxy.php';
-
- $key = md5($path);
- // If the proxy file does not exist, then try to acquire the coroutine lock.
- if (! file_exists($path) && CoLocker::lock($key)) {
- $targetPath = $path . '.' . uniqid();
- $code = $this->ast->proxy($className, $proxyClassName);
- file_put_contents($targetPath, $code);
- rename($targetPath, $path);
- CoLocker::unlock($key);
- }
- include_once $path;
- }
-}
diff --git a/src/di/src/ReflectionManager.php b/src/di/src/ReflectionManager.php
index fd9055ee0..0ee57d863 100644
--- a/src/di/src/ReflectionManager.php
+++ b/src/di/src/ReflectionManager.php
@@ -26,7 +26,7 @@ class ReflectionManager extends MetadataCollector
public static function reflectClass(string $className): ReflectionClass
{
if (! isset(static::$container['class'][$className])) {
- if (! class_exists($className) && ! interface_exists($className)) {
+ if (! class_exists($className) && ! interface_exists($className) && ! trait_exists($className)) {
throw new InvalidArgumentException("Class {$className} not exist");
}
static::$container['class'][$className] = new ReflectionClass($className);
@@ -59,8 +59,10 @@ class ReflectionManager extends MetadataCollector
return static::$container['property'][$key];
}
- public static function clear(): void
+ public static function clear(?string $key = null): void
{
- static::$container = [];
+ if ($key === null) {
+ static::$container = [];
+ }
}
}
diff --git a/src/di/src/Resolver/ObjectResolver.php b/src/di/src/Resolver/ObjectResolver.php
index f2639e522..914c3e85b 100644
--- a/src/di/src/Resolver/ObjectResolver.php
+++ b/src/di/src/Resolver/ObjectResolver.php
@@ -13,22 +13,14 @@ namespace Hyperf\Di\Resolver;
use Hyperf\Di\Definition\DefinitionInterface;
use Hyperf\Di\Definition\ObjectDefinition;
-use Hyperf\Di\Definition\PropertyInjection;
-use Hyperf\Di\Definition\Reference;
use Hyperf\Di\Exception\DependencyException;
use Hyperf\Di\Exception\InvalidDefinitionException;
-use Hyperf\Di\ProxyFactory;
use Hyperf\Di\ReflectionManager;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
class ObjectResolver implements ResolverInterface
{
- /**
- * @var ProxyFactory
- */
- private $proxyFactory;
-
/**
* @var ParameterResolver
*/
@@ -51,7 +43,6 @@ class ObjectResolver implements ResolverInterface
{
$this->container = $container;
$this->definitionResolver = $definitionResolver;
- $this->proxyFactory = $container->get(ProxyFactory::class);
$this->parameterResolver = new ParameterResolver($definitionResolver);
}
@@ -86,14 +77,6 @@ class ObjectResolver implements ResolverInterface
return $definition->isInstantiable();
}
- protected function injectProperties($object, ObjectDefinition $objectDefinition): void
- {
- // Property injections
- foreach ($objectDefinition->getPropertyInjections() as $propertyInjection) {
- $this->injectProperty($object, $propertyInjection);
- }
- }
-
private function createInstance(ObjectDefinition $definition, array $parameters)
{
// Check that the class is instantiable
@@ -109,16 +92,11 @@ class ObjectResolver implements ResolverInterface
$classReflection = null;
try {
$className = $definition->getClassName();
- if ($definition->isNeedProxy()) {
- $definition = $this->proxyFactory->createProxyDefinition($definition);
- $className = $definition->getProxyClassName();
- }
$classReflection = ReflectionManager::reflectClass($className);
$constructorInjection = $definition->getConstructorInjection();
$args = $this->parameterResolver->resolveParameters($constructorInjection, $classReflection->getConstructor(), $parameters);
$object = new $className(...$args);
- $this->injectProperties($object, $definition);
} catch (NotFoundExceptionInterface $e) {
throw new DependencyException(sprintf('Error while injecting dependencies into %s: %s', $classReflection ? $classReflection->getName() : '', $e->getMessage()), 0, $e);
} catch (InvalidDefinitionException $e) {
@@ -126,23 +104,4 @@ class ObjectResolver implements ResolverInterface
}
return $object;
}
-
- private function injectProperty($object, PropertyInjection $propertyInjection): void
- {
- $property = ReflectionManager::reflectProperty(get_class($object), $propertyInjection->getPropertyName());
- if ($property->isStatic()) {
- return;
- }
- if (! $property->isPublic()) {
- $property->setAccessible(true);
- }
- $value = $propertyInjection->getValue();
- if ($value instanceof Reference) {
- $property->setValue($object, $this->container->get($value->getTargetEntryName()));
- } elseif (is_callable($value)) {
- $property->setValue($object, call($value));
- } else {
- $property->setValue($object, value($value));
- }
- }
}
diff --git a/src/di/src/TypesFinderManager.php b/src/di/src/TypesFinderManager.php
new file mode 100644
index 000000000..8a81c66b2
--- /dev/null
+++ b/src/di/src/TypesFinderManager.php
@@ -0,0 +1,31 @@
+collect([Ignore::class]);
+ BetterReflectionManager::initClassReflector([__DIR__ . '/Stub/']);
+
+ $scaner = new Scanner($loader = Mockery::mock(ClassLoader::class), new ScanConfig('dev', '/'));
+ $reader = new AnnotationReader();
+ $scaner->collect($reader, $ref = BetterReflectionManager::reflectClass(Ignore::class));
$annotations = AnnotationCollector::get(Ignore::class . '._c');
$this->assertArrayHasKey(IgnoreDemoAnnotation::class, $annotations);
AnnotationCollector::clear();
- $scaner = new Scanner(['IgnoreDemoAnnotation']);
- $scaner->collect([Ignore::class]);
+ $scaner = new Scanner($loader, new ScanConfig('dev', '/', [], [], ['IgnoreDemoAnnotation']));
+ $reader = new AnnotationReader();
+ $scaner->collect($reader, $ref);
$annotations = AnnotationCollector::get(Ignore::class . '._c');
$this->assertNull($annotations);
}
diff --git a/src/di/tests/AopAspectTest.php b/src/di/tests/AopAspectTest.php
index 540387d33..83e2a5220 100644
--- a/src/di/tests/AopAspectTest.php
+++ b/src/di/tests/AopAspectTest.php
@@ -14,6 +14,7 @@ namespace HyperfTest\Di;
use Hyperf\Di\Annotation\Aspect as AspectAnnotation;
use Hyperf\Di\Aop\Aspect;
use Hyperf\Di\Aop\RewriteCollection;
+use Hyperf\Di\BetterReflectionManager;
use HyperfTest\Di\Stub\AnnotationCollector;
use HyperfTest\Di\Stub\AspectCollector;
use HyperfTest\Di\Stub\DemoAnnotation;
@@ -32,6 +33,7 @@ class AopAspectTest extends TestCase
{
AspectCollector::clear();
AnnotationCollector::clear();
+ BetterReflectionManager::clear();
}
public function testParseMoreThanOneMethods()
@@ -209,17 +211,21 @@ class AopAspectTest extends TestCase
public function testAspectAnnotation()
{
+ BetterReflectionManager::initClassReflector([__DIR__ . '/Stub']);
+
$annotation = new AspectAnnotation();
$annotation->collectClass(FooAspect::class);
$annotation->collectClass(Foo2Aspect::class);
$this->assertSame([
+ 'priority' => 4611686018427387904,
'classes' => [Foo::class],
'annotations' => [DemoAnnotation::class],
], AspectCollector::getRule(FooAspect::class));
$this->assertSame([
+ 'priority' => 4611686018427387904,
'classes' => [Foo::class],
'annotations' => [],
], AspectCollector::getRule(Foo2Aspect::class));
diff --git a/src/di/tests/AstTest.php b/src/di/tests/AstTest.php
index db2142397..cb68a913a 100644
--- a/src/di/tests/AstTest.php
+++ b/src/di/tests/AstTest.php
@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace HyperfTest\Di;
use Hyperf\Di\Aop\Ast;
+use Hyperf\Di\BetterReflectionManager;
use HyperfTest\Di\Stub\AspectCollector;
use HyperfTest\Di\Stub\Ast\Bar2;
use HyperfTest\Di\Stub\Ast\Bar3;
@@ -25,11 +26,17 @@ use PHPUnit\Framework\TestCase;
*/
class AstTest extends TestCase
{
- public function testProxy()
+ protected function tearDown()
{
+ BetterReflectionManager::clear();
+ }
+
+ public function testAstProxy()
+ {
+ BetterReflectionManager::initClassReflector([__DIR__ . '/Stub']);
+
$ast = new Ast();
- $proxyClass = Foo::class . 'Proxy';
- $code = $ast->proxy(Foo::class, $proxyClass);
+ $code = $ast->proxy(Foo::class);
$this->assertEquals('proxy(Bar2::class, $proxyClass);
+ BetterReflectionManager::initClassReflector([__DIR__ . '/Stub']);
+ $ast = new Ast();
+ $code = $ast->proxy(Bar2::class);
$this->assertEquals('proxy(Bar3::class, $proxyClass);
+ $code = $ast->proxy(Bar3::class);
$this->assertEquals('get(DemoInject::class);
+ $this->getContainer();
+ $ast = new Ast();
+ $code = $ast->proxy(DemoInject::class);
+ if (! is_dir($dir = BASE_PATH . '/runtime/container/proxy/')) {
+ mkdir($dir, 0777, true);
+ }
+ file_put_contents($file = $dir . 'DemoInject.proxy.php', $code);
+ require $file;
+
+ $demoInject = new DemoInject();
$this->assertSame(Demo::class, get_class($demoInject->getDemo()));
$this->assertSame(null, $demoInject->getDemo1());
}
public function testInjectException()
{
+ $this->getContainer();
+ $ast = new Ast();
+ $code = $ast->proxy(DemoInjectException::class);
+ if (! is_dir($dir = BASE_PATH . '/runtime/container/proxy/')) {
+ mkdir($dir, 0777, true);
+ }
+ file_put_contents($file = $dir . 'DemoInjectException.proxy.php', $code);
+ require $file;
+
try {
- $container = new Container(new DefinitionSource([], new ScanConfig([__DIR__ . '/Stub', __DIR__ . '/ExceptionStub'])));
- $container->get(DemoInjectException::class);
+ new DemoInjectException();
} catch (\Exception $e) {
- $this->assertSame(true, $e instanceof AnnotationException);
+ $this->assertSame(true, $e instanceof NotFoundException);
}
}
+
+ protected function getContainer()
+ {
+ $container = Mockery::mock(ContainerInterface::class);
+ ApplicationContext::setContainer($container);
+
+ BetterReflectionManager::initClassReflector([__DIR__ . '/Stub']);
+
+ $scaner = new Scanner($loader = Mockery::mock(ClassLoader::class), new ScanConfig('dev', '/'));
+ $reader = new AnnotationReader();
+ $scaner->collect($reader, BetterReflectionManager::reflectClass(DemoInject::class));
+ $scaner->collect($reader, BetterReflectionManager::reflectClass(DemoInjectException::class));
+
+ $container->shouldReceive('get')->with(Demo::class)->andReturn(new Demo());
+ $container->shouldReceive('has')->with(Demo::class)->andReturn(true);
+ $container->shouldReceive('has')->with('HyperfTest\Di\ExceptionStub\Demo1')->andReturn(false);
+ return $container;
+ }
}
diff --git a/src/di/tests/Stub/AnnotationCollector.php b/src/di/tests/Stub/AnnotationCollector.php
index 820fd00ec..a3cbf0a95 100644
--- a/src/di/tests/Stub/AnnotationCollector.php
+++ b/src/di/tests/Stub/AnnotationCollector.php
@@ -13,8 +13,4 @@ namespace HyperfTest\Di\Stub;
class AnnotationCollector extends \Hyperf\Di\Annotation\AnnotationCollector
{
- public static function clear()
- {
- self::$container = [];
- }
}
diff --git a/src/di/tests/Stub/AspectCollector.php b/src/di/tests/Stub/AspectCollector.php
index 9d675bb46..613c5e1da 100644
--- a/src/di/tests/Stub/AspectCollector.php
+++ b/src/di/tests/Stub/AspectCollector.php
@@ -13,9 +13,4 @@ namespace HyperfTest\Di\Stub;
class AspectCollector extends \Hyperf\Di\Annotation\AspectCollector
{
- public static function clear()
- {
- self::$container = [];
- self::$aspectRules = [];
- }
}
diff --git a/src/di/tests/Stub/ProxyTraitObject.php b/src/di/tests/Stub/ProxyTraitObject.php
index 925145959..75ae4f8c3 100644
--- a/src/di/tests/Stub/ProxyTraitObject.php
+++ b/src/di/tests/Stub/ProxyTraitObject.php
@@ -19,24 +19,24 @@ class ProxyTraitObject
public function get(?int $id, string $str = '')
{
- return $this->getParamsMap(static::class, 'get', func_get_args());
+ return $this->__getParamsMap(static::class, 'get', func_get_args());
}
public function get2(?int $id = 1, string $str = '')
{
- return $this->getParamsMap(static::class, 'get2', func_get_args());
+ return $this->__getParamsMap(static::class, 'get2', func_get_args());
}
public function get3(?int $id = 1, string $str = '', float $num = 1.0)
{
- return $this->getParamsMap(static::class, 'get3', func_get_args());
+ return $this->__getParamsMap(static::class, 'get3', func_get_args());
}
public function incr()
{
$__function__ = __FUNCTION__;
$__method__ = __METHOD__;
- return self::__proxyCall(ProxyTraitObject::class, __FUNCTION__, self::getParamsMap(ProxyTraitObject::class, __FUNCTION__, func_get_args()), function () use ($__function__, $__method__) {
+ return self::__proxyCall(ProxyTraitObject::class, __FUNCTION__, self::__getParamsMap(ProxyTraitObject::class, __FUNCTION__, func_get_args()), function () use ($__function__, $__method__) {
return 1;
});
}
diff --git a/src/dispatcher/composer.json b/src/dispatcher/composer.json
index 627dc3499..9d3c2caaa 100644
--- a/src/dispatcher/composer.json
+++ b/src/dispatcher/composer.json
@@ -22,7 +22,7 @@
"psr/container": "^1.0",
"psr/http-server-middleware": "^1.0",
"psr/http-message": "^1.0",
- "hyperf/contract": "^1.0.0"
+ "hyperf/contract": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/elasticsearch/composer.json b/src/elasticsearch/composer.json
index 7a59fd9a9..720a021d7 100644
--- a/src/elasticsearch/composer.json
+++ b/src/elasticsearch/composer.json
@@ -20,8 +20,8 @@
},
"require": {
"php": ">=7.0",
- "hyperf/guzzle": "~1.1.0",
- "elasticsearch/elasticsearch": "^6.1"
+ "hyperf/guzzle": "~2.0.0",
+ "elasticsearch/elasticsearch": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7",
diff --git a/src/etcd/composer.json b/src/etcd/composer.json
index 9f2cf343e..de76424c7 100644
--- a/src/etcd/composer.json
+++ b/src/etcd/composer.json
@@ -20,8 +20,8 @@
},
"require": {
"php": ">=7.2",
- "hyperf/guzzle": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/guzzle": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"start-point/etcd-php": "^1.1"
},
"require-dev": {
diff --git a/src/event/composer.json b/src/event/composer.json
index a0241ea34..5e52b0bb1 100644
--- a/src/event/composer.json
+++ b/src/event/composer.json
@@ -18,10 +18,10 @@
"require": {
"php": ">=7.2",
"psr/event-dispatcher": "^1.0",
- "hyperf/contract": "~1.1.0"
+ "hyperf/contract": "~2.0.0"
},
"require-dev": {
- "hyperf/di": "~1.1.0",
+ "hyperf/di": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/exception-handler/composer.json b/src/exception-handler/composer.json
index cb0e98437..fde916e13 100644
--- a/src/exception-handler/composer.json
+++ b/src/exception-handler/composer.json
@@ -22,9 +22,9 @@
"php": ">=7.2",
"psr/container": "^1.0",
"psr/http-message": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/dispatcher": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/dispatcher": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/exception-handler/src/Handler/WhoopsExceptionHandler.php b/src/exception-handler/src/Handler/WhoopsExceptionHandler.php
new file mode 100644
index 000000000..d0d86ec8f
--- /dev/null
+++ b/src/exception-handler/src/Handler/WhoopsExceptionHandler.php
@@ -0,0 +1,95 @@
+ PrettyPageHandler::class,
+ 'application/json' => JsonResponseHandler::class,
+ 'application/xml' => XmlResponseHandler::class,
+ ];
+
+ public function handle(Throwable $throwable, ResponseInterface $response)
+ {
+ $whoops = new Run();
+ [$handler, $contentType] = $this->negotiateHandler();
+
+ $whoops->pushHandler($handler);
+ $whoops->allowQuit(false);
+ ob_start();
+ $whoops->{Run::EXCEPTION_HANDLER}($throwable);
+ $content = ob_get_clean();
+ return $response
+ ->withStatus(500)
+ ->withHeader('Content-Type', $contentType)
+ ->withBody(new SwooleStream($content));
+ }
+
+ public function isValid(Throwable $throwable): bool
+ {
+ return env('APP_ENV') !== 'prod' && class_exists(Run::class);
+ }
+
+ private function negotiateHandler()
+ {
+ /** @var ServerRequestInterface $request */
+ $request = Context::get(ServerRequestInterface::class);
+ $accepts = $request->getHeaderLine('accept');
+ foreach (self::$preference as $contentType => $handler) {
+ if (Str::contains($accepts, $contentType)) {
+ return [$this->setupHandler(new $handler()), $contentType];
+ }
+ }
+ return [new PlainTextHandler(), 'text/plain'];
+ }
+
+ private function setupHandler($handler)
+ {
+ if ($handler instanceof PrettyPageHandler) {
+ $handler->handleUnconditionally(true);
+
+ if (defined('BASE_PATH')) {
+ $handler->setApplicationRootPath(BASE_PATH);
+ }
+
+ $request = Context::get(ServerRequestInterface::class);
+ $handler->addDataTableCallback('PSR7 Query', [$request, 'getQueryParams']);
+ $handler->addDataTableCallback('PSR7 Post', [$request, 'getParsedBody']);
+ $handler->addDataTableCallback('PSR7 Server', [$request, 'getServerParams']);
+ $handler->addDataTableCallback('PSR7 Cookie', [$request, 'getCookieParams']);
+ $handler->addDataTableCallback('PSR7 File', [$request, 'getUploadedFiles']);
+ $handler->addDataTableCallback('PSR7 Attribute', [$request, 'getAttributes']);
+
+ $session = Context::get(SessionInterface::class);
+ if ($session) {
+ $handler->addDataTableCallback('Hyperf Session', [$session, 'all']);
+ }
+ }
+
+ return $handler;
+ }
+}
diff --git a/src/exception-handler/tests/WhoopsExceptionHandlerTest.php b/src/exception-handler/tests/WhoopsExceptionHandlerTest.php
new file mode 100644
index 000000000..2d88c7b27
--- /dev/null
+++ b/src/exception-handler/tests/WhoopsExceptionHandlerTest.php
@@ -0,0 +1,74 @@
+handle(new Exception(), new Response());
+ $this->assertInstanceOf(ResponseInterface::class, $response);
+ $this->assertEquals(500, $response->getStatusCode());
+ $this->assertEquals('text/plain', $response->getHeader('Content-Type')[0]);
+ }
+
+ public function testHtmlWhoops()
+ {
+ $request = new Request('GET', '/');
+ $request = $request->withHeader('accept', ['text/html,application/json,application/xml']);
+ Context::set(ServerRequestInterface::class, $request);
+ $handler = new WhoopsExceptionHandler();
+ $response = $handler->handle(new Exception(), new Response());
+ $this->assertInstanceOf(ResponseInterface::class, $response);
+ $this->assertEquals(500, $response->getStatusCode());
+ $this->assertEquals('text/html', $response->getHeader('Content-Type')[0]);
+ }
+
+ public function testJsonWhoops()
+ {
+ $request = new Request('GET', '/');
+ $request = $request->withHeader('accept', ['application/json,application/xml']);
+ Context::set(ServerRequestInterface::class, $request);
+ $handler = new WhoopsExceptionHandler();
+ $response = $handler->handle(new Exception(), new Response());
+ $this->assertInstanceOf(ResponseInterface::class, $response);
+ $this->assertEquals(500, $response->getStatusCode());
+ $this->assertEquals('application/json', $response->getHeader('Content-Type')[0]);
+ }
+
+ public function testXmlWhoops()
+ {
+ $request = new Request('GET', '/');
+ $request = $request->withHeader('accept', ['application/xml']);
+ Context::set(ServerRequestInterface::class, $request);
+ $handler = new WhoopsExceptionHandler();
+ $response = $handler->handle(new Exception(), new Response());
+ $this->assertInstanceOf(ResponseInterface::class, $response);
+ $this->assertEquals(500, $response->getStatusCode());
+ $this->assertEquals('application/xml', $response->getHeader('Content-Type')[0]);
+ }
+}
diff --git a/src/filesystem/composer.json b/src/filesystem/composer.json
index cfc80ae6b..d484e37ab 100644
--- a/src/filesystem/composer.json
+++ b/src/filesystem/composer.json
@@ -17,24 +17,23 @@
},
"autoload-dev": {
"psr-4": {
- "HyperfTest\\": "tests"
+ "HyperfTest\\Filesystem\\": "tests"
}
},
"require": {
"php": ">=7.2",
"ext-swoole": ">=4.4",
- "hyperf/di": "1.1.*",
+ "hyperf/di": "~2.0.0",
"league/flysystem": "^1.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
- "hyperf/config": "^1.1",
- "hyperf/guzzle": "^1.1",
- "hyperf/testing": "1.1.*",
+ "hyperf/config": "~2.0.0",
+ "hyperf/guzzle": "~2.0.0",
+ "hyperf/testing": "~2.0.0",
"league/flysystem-aws-s3-v3": "^1.0",
"league/flysystem-memory": "^1.0",
"overtrue/flysystem-qiniu": "^1.0",
- "phpstan/phpstan": "^0.10.5",
"swoft/swoole-ide-helper": "dev-master",
"xxtime/flysystem-aliyun-oss": "^1.5"
},
diff --git a/src/filesystem/src/Adapter/AliyunOssHook.php b/src/filesystem/src/Adapter/AliyunOssHook.php
index 8a2f4d82f..6a44bf345 100644
--- a/src/filesystem/src/Adapter/AliyunOssHook.php
+++ b/src/filesystem/src/Adapter/AliyunOssHook.php
@@ -13,10 +13,10 @@ namespace Oss\OssClient {
function is_resource($resource)
{
if (! function_exists('swoole_hook_flags')) {
- return true;
+ return \is_resource($resource) || $resource instanceof \Swoole\Curl\Handler;
}
- if (swoole_hook_flags() ^ SWOOLE_HOOK_CURL) {
- return true;
+ if (swoole_hook_flags() & SWOOLE_HOOK_CURL) {
+ return \is_resource($resource) || $resource instanceof \Swoole\Curl\Handler;
}
return \is_resource($resource);
}
@@ -26,10 +26,10 @@ namespace Oss\Http {
function is_resource($resource)
{
if (! function_exists('swoole_hook_flags')) {
- return true;
+ return \is_resource($resource) || $resource instanceof \Swoole\Curl\Handler;
}
- if (swoole_hook_flags() ^ SWOOLE_HOOK_CURL) {
- return true;
+ if (swoole_hook_flags() & SWOOLE_HOOK_CURL) {
+ return \is_resource($resource) || $resource instanceof \Swoole\Curl\Handler;
}
return \is_resource($resource);
}
diff --git a/src/filesystem/tests/Cases/AbstractTestCase.php b/src/filesystem/tests/Cases/AbstractTestCase.php
index 786a96352..cafea0821 100644
--- a/src/filesystem/tests/Cases/AbstractTestCase.php
+++ b/src/filesystem/tests/Cases/AbstractTestCase.php
@@ -9,7 +9,7 @@ declare(strict_types=1);
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
-namespace HyperfTest\Cases;
+namespace HyperfTest\Filesystem\Cases;
use PHPUnit\Framework\TestCase;
diff --git a/src/filesystem/tests/Cases/FilesystemFactoryTest.php b/src/filesystem/tests/Cases/FilesystemFactoryTest.php
index 313530425..b0789596e 100644
--- a/src/filesystem/tests/Cases/FilesystemFactoryTest.php
+++ b/src/filesystem/tests/Cases/FilesystemFactoryTest.php
@@ -9,7 +9,7 @@ declare(strict_types=1);
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
-namespace HyperfTest\Cases;
+namespace HyperfTest\Filesystem\Cases;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
diff --git a/src/framework/composer.json b/src/framework/composer.json
index 1d064d0a3..0163af66c 100644
--- a/src/framework/composer.json
+++ b/src/framework/composer.json
@@ -21,8 +21,8 @@
"php": ">=7.2",
"ext-swoole": ">=4.4",
"fig/http-message-util": "^1.1.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
"psr/log": "^1.0"
@@ -38,10 +38,7 @@
"hyperf/di": "Required to use Command annotation.",
"hyperf/dispatcher": "Required to use BootApplication event.",
"hyperf/command": "Required to use Command annotation.",
- "symfony/event-dispatcher": "Required to use symfony event dispatcher (^4.3)."
- },
- "conflict": {
- "symfony/event-dispatcher": "<4.3"
+ "symfony/event-dispatcher": "Required to use symfony event dispatcher (^5.0)."
},
"autoload": {
"psr-4": {
diff --git a/src/framework/src/ApplicationFactory.php b/src/framework/src/ApplicationFactory.php
index bd9615f59..426547ea8 100644
--- a/src/framework/src/ApplicationFactory.php
+++ b/src/framework/src/ApplicationFactory.php
@@ -33,7 +33,7 @@ class ApplicationFactory
// Append commands that defined by annotation.
$annotationCommands = [];
if (class_exists(AnnotationCollector::class) && class_exists(Command::class)) {
- $annotationCommands = AnnotationCollector::getClassByAnnotation(Command::class);
+ $annotationCommands = AnnotationCollector::getClassesByAnnotation(Command::class);
$annotationCommands = array_keys($annotationCommands);
}
diff --git a/src/framework/src/SymfonyEventDispatcher.php b/src/framework/src/SymfonyEventDispatcher.php
index 0a0caf4aa..0c639c8df 100644
--- a/src/framework/src/SymfonyEventDispatcher.php
+++ b/src/framework/src/SymfonyEventDispatcher.php
@@ -11,16 +11,14 @@ declare(strict_types=1);
*/
namespace Hyperf\Framework;
-use Hyperf\Framework\Exception\NotImplementedException;
use Psr\EventDispatcher\EventDispatcherInterface as PsrDispatcherInterface;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface as SymfonyDispatcherInterface;
-use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
-if (interface_exists(SymfonyDispatcherInterface::class)) {
+if (interface_exists(EventDispatcherInterface::class)) {
/**
* @internal
*/
- class SymfonyEventDispatcher implements SymfonyDispatcherInterface
+ class SymfonyEventDispatcher implements EventDispatcherInterface
{
/**
* @var PsrDispatcherInterface
@@ -32,44 +30,9 @@ if (interface_exists(SymfonyDispatcherInterface::class)) {
$this->psrDispatcher = $psrDispatcher;
}
- public function addListener($eventName, $listener, $priority = 0)
+ public function dispatch(object $event, string $eventName = null): object
{
- throw new NotImplementedException();
- }
-
- public function addSubscriber(EventSubscriberInterface $subscriber)
- {
- throw new NotImplementedException();
- }
-
- public function removeListener($eventName, $listener)
- {
- throw new NotImplementedException();
- }
-
- public function removeSubscriber(EventSubscriberInterface $subscriber)
- {
- throw new NotImplementedException();
- }
-
- public function getListeners($eventName = null)
- {
- throw new NotImplementedException();
- }
-
- public function dispatch($event)
- {
- $this->psrDispatcher->dispatch($event);
- }
-
- public function getListenerPriority($eventName, $listener)
- {
- throw new NotImplementedException();
- }
-
- public function hasListeners($eventName = null)
- {
- throw new NotImplementedException();
+ return $this->psrDispatcher->dispatch($event);
}
}
}
diff --git a/src/graphql/composer.json b/src/graphql/composer.json
index 78c0a5a68..fea0d6eb4 100644
--- a/src/graphql/composer.json
+++ b/src/graphql/composer.json
@@ -11,8 +11,8 @@
"require": {
"php": ">=7.2",
"thecodingmachine/graphqlite": "^3.0",
- "hyperf/contract": "^1.0",
- "hyperf/di": "^1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/di": "~2.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.15",
diff --git a/src/grpc-client/composer.json b/src/grpc-client/composer.json
index f68173f62..dd03536f4 100644
--- a/src/grpc-client/composer.json
+++ b/src/grpc-client/composer.json
@@ -18,8 +18,8 @@
},
"require": {
"php": ">=7.2",
- "hyperf/grpc": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/grpc": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"jean85/pretty-package-versions": "^1.2"
},
"require-dev": {
diff --git a/src/grpc-server/composer.json b/src/grpc-server/composer.json
index d1d9b306d..db290a56b 100644
--- a/src/grpc-server/composer.json
+++ b/src/grpc-server/composer.json
@@ -18,11 +18,11 @@
},
"require": {
"php": ">=7.2",
- "hyperf/di": "~1.1.0",
- "hyperf/utils": "~1.1.0",
- "hyperf/http-server": "~1.1.0",
- "hyperf/http-message": "~1.1.0",
- "hyperf/grpc": "~1.1.0"
+ "hyperf/di": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
+ "hyperf/http-server": "~2.0.0",
+ "hyperf/http-message": "~2.0.0",
+ "hyperf/grpc": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/guzzle/composer.json b/src/guzzle/composer.json
index e5aad1521..f384d7030 100644
--- a/src/guzzle/composer.json
+++ b/src/guzzle/composer.json
@@ -38,6 +38,9 @@
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
+ },
+ "hyperf": {
+ "config": "Hyperf\\Guzzle\\ConfigProvider"
}
},
"scripts": {
diff --git a/src/swoole-enterprise/src/ConfigProvider.php b/src/guzzle/src/ConfigProvider.php
similarity index 86%
rename from src/swoole-enterprise/src/ConfigProvider.php
rename to src/guzzle/src/ConfigProvider.php
index fcd9ac546..01ac39844 100644
--- a/src/swoole-enterprise/src/ConfigProvider.php
+++ b/src/guzzle/src/ConfigProvider.php
@@ -9,11 +9,11 @@ declare(strict_types=1);
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
-namespace Hyperf\SwooleEnterprise;
+namespace Hyperf\Guzzle;
class ConfigProvider
{
- public function __invoke(): array
+ public function __invoke()
{
return [
'annotations' => [
diff --git a/src/http-server/composer.json b/src/http-server/composer.json
index a7936cbb6..8baa524ff 100644
--- a/src/http-server/composer.json
+++ b/src/http-server/composer.json
@@ -20,16 +20,16 @@
"php": ">=7.2",
"psr/container": "^1.0",
"nikic/fast-route": "^1.3",
- "hyperf/contract": "~1.1.0",
- "hyperf/dispatcher": "~1.1.0",
- "hyperf/event": "~1.1.0",
- "hyperf/exception-handler": "~1.1.0",
- "hyperf/http-message": "~1.1.0",
- "hyperf/server": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/dispatcher": "~2.0.0",
+ "hyperf/event": "~2.0.0",
+ "hyperf/exception-handler": "~2.0.0",
+ "hyperf/http-message": "~2.0.0",
+ "hyperf/server": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
- "hyperf/di": "~1.1.0",
+ "hyperf/di": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/json-rpc/composer.json b/src/json-rpc/composer.json
index 7d957cd64..386f60c27 100644
--- a/src/json-rpc/composer.json
+++ b/src/json-rpc/composer.json
@@ -19,11 +19,11 @@
"php": ">=7.2",
"psr/container": "^1.0",
"psr/log": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/load-balancer": "~1.1.0",
- "hyperf/http-message": "~1.1.0",
- "hyperf/rpc": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/load-balancer": "~2.0.0",
+ "hyperf/http-message": "~2.0.0",
+ "hyperf/rpc": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/json-rpc/src/JsonRpcPoolTransporter.php b/src/json-rpc/src/JsonRpcPoolTransporter.php
index 37f92eae2..2081b9a51 100644
--- a/src/json-rpc/src/JsonRpcPoolTransporter.php
+++ b/src/json-rpc/src/JsonRpcPoolTransporter.php
@@ -111,8 +111,11 @@ class JsonRpcPoolTransporter implements TransporterInterface
public function getConnection(): RpcConnection
{
$class = spl_object_hash($this) . '.Connection';
- if (Context::has($class)) {
- return Context::get($class);
+ /** @var RpcConnection $connection */
+ if (Context::has($class) && $connection = Context::get($class)) {
+ if ($connection->check()) {
+ return $connection;
+ }
}
$connection = $this->getPool()->get();
@@ -130,6 +133,7 @@ class JsonRpcPoolTransporter implements TransporterInterface
$config = [
'connect_timeout' => $this->config['connect_timeout'],
'settings' => $this->config['settings'],
+ 'pool' => $this->config['pool'],
'node' => function () {
return $this->getNode();
},
diff --git a/src/json-rpc/src/Pool/RpcPool.php b/src/json-rpc/src/Pool/RpcPool.php
index d03750deb..287030cba 100644
--- a/src/json-rpc/src/Pool/RpcPool.php
+++ b/src/json-rpc/src/Pool/RpcPool.php
@@ -31,7 +31,7 @@ class RpcPool extends Pool
{
$this->name = $name;
$this->config = $config;
- $options = [];
+ $options = $config['pool'] ?? [];
$this->frequency = make(Frequency::class);
parent::__construct($container, $options);
}
diff --git a/src/json-rpc/tests/JsonRpcPoolTransporterTest.php b/src/json-rpc/tests/JsonRpcPoolTransporterTest.php
index 07b188e3a..912ac0fe7 100644
--- a/src/json-rpc/tests/JsonRpcPoolTransporterTest.php
+++ b/src/json-rpc/tests/JsonRpcPoolTransporterTest.php
@@ -54,6 +54,25 @@ class JsonRpcPoolTransporterTest extends TestCase
$this->assertSame($settings, $transporter->getConfig()['settings']);
}
+ public function testJsonRpcPoolTransporterGetPool()
+ {
+ $container = $this->getContainer();
+ $factory = new PoolFactory($container);
+ $transporter = new JsonRpcPoolTransporter($factory, [
+ 'pool' => ['min_connections' => 8, 'max_connections' => 88],
+ 'settings' => $settings = [
+ 'open_length_check' => true,
+ 'package_length_type' => 'N',
+ 'package_length_offset' => 0,
+ 'package_body_offset' => 4,
+ ],
+ ]);
+
+ $options = $transporter->getPool()->getOption();
+ $this->assertSame(8, $options->getMinConnections());
+ $this->assertSame(88, $options->getMaxConnections());
+ }
+
public function testJsonRpcPoolTransporterSendLengthCheck()
{
$container = $this->getContainer();
@@ -96,6 +115,28 @@ class JsonRpcPoolTransporterTest extends TestCase
$this->assertSame($data, $packer->unpack($string));
}
+ public function testGetConnection()
+ {
+ $container = $this->getContainer();
+ $factory = $container->get(PoolFactory::class);
+ $transporter = new JsonRpcPoolTransporter($factory, [
+ 'pool' => ['min_connections' => 10],
+ 'settings' => $settings = [
+ 'open_length_check' => true,
+ 'package_length_type' => 'N',
+ 'package_length_offset' => 0,
+ 'package_body_offset' => 4,
+ ],
+ ]);
+
+ $conn = $transporter->getConnection();
+ $conn2 = $transporter->getConnection();
+ $this->assertSame($conn, $conn2);
+ $conn->close();
+ $conn2 = $transporter->getConnection();
+ $this->assertNotEquals($conn, $conn2);
+ }
+
public function testsplObjectHash()
{
$class = new \stdClass();
diff --git a/src/json-rpc/tests/Stub/RpcConnectionStub.php b/src/json-rpc/tests/Stub/RpcConnectionStub.php
index c1d637a4a..74683a690 100644
--- a/src/json-rpc/tests/Stub/RpcConnectionStub.php
+++ b/src/json-rpc/tests/Stub/RpcConnectionStub.php
@@ -39,6 +39,13 @@ class RpcConnectionStub extends RpcConnection
public function reconnect(): bool
{
+ $this->lastUseTime = microtime(true);
+ return true;
+ }
+
+ public function close(): bool
+ {
+ $this->lastUseTime = 0.0;
return true;
}
}
diff --git a/src/logger/composer.json b/src/logger/composer.json
index 5ba39c681..0c07c907f 100644
--- a/src/logger/composer.json
+++ b/src/logger/composer.json
@@ -18,9 +18,9 @@
"php": ">=7.2",
"psr/log": "^1.0",
"psr/container": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0",
- "monolog/monolog": "^1.24"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
+ "monolog/monolog": "^2.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/logger/tests/Stub/FooHandler.php b/src/logger/tests/Stub/FooHandler.php
index 0809186bb..5c9a3e235 100644
--- a/src/logger/tests/Stub/FooHandler.php
+++ b/src/logger/tests/Stub/FooHandler.php
@@ -16,7 +16,7 @@ use Monolog\Handler\StreamHandler;
class FooHandler extends StreamHandler
{
- public function write(array $record)
+ public function write(array $record): void
{
Context::set('test.logger.foo_handler.record', $record);
}
diff --git a/src/metric/composer.json b/src/metric/composer.json
index 542461c5b..af1530b83 100644
--- a/src/metric/composer.json
+++ b/src/metric/composer.json
@@ -16,9 +16,9 @@
"endclothing/prometheus_client_php": "^0.9.1",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0",
- "hyperf/guzzle": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
+ "hyperf/guzzle": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/metric/grafana.json b/src/metric/grafana.json
index 3a8e1c855..f74cc2161 100644
--- a/src/metric/grafana.json
+++ b/src/metric/grafana.json
@@ -1194,7 +1194,7 @@
"instant": true,
"intervalFactor": 1,
"legendFormat": "",
- "refId": "H"
+ "refId": "F"
}
],
"timeFrom": null,
diff --git a/src/model-cache/composer.json b/src/model-cache/composer.json
index e716dfab1..691aa2b73 100644
--- a/src/model-cache/composer.json
+++ b/src/model-cache/composer.json
@@ -18,12 +18,12 @@
"php": ">=7.2",
"psr/simple-cache": "^1.0",
"psr/container": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/db-connection": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/db-connection": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
- "hyperf/event": "~1.1.0",
+ "hyperf/event": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/model-listener/composer.json b/src/model-listener/composer.json
index b77f699ec..e1f0708f7 100644
--- a/src/model-listener/composer.json
+++ b/src/model-listener/composer.json
@@ -17,11 +17,11 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/database": "~1.1.0",
- "hyperf/di": "~1.1.0",
- "hyperf/event": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/database": "~2.0.0",
+ "hyperf/di": "~2.0.0",
+ "hyperf/event": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"psr/container": "^1.0",
"psr/log": "^1.0"
},
diff --git a/src/model-listener/src/Collector/ListenerCollector.php b/src/model-listener/src/Collector/ListenerCollector.php
index c69ded0ae..6773bf5be 100644
--- a/src/model-listener/src/Collector/ListenerCollector.php
+++ b/src/model-listener/src/Collector/ListenerCollector.php
@@ -52,6 +52,20 @@ class ListenerCollector extends MetadataCollector
*/
public static function clearListeners(): void
{
- static::$container = [];
+ static::clear();
+ }
+
+ public static function clear(?string $listener = null): void
+ {
+ if ($listener) {
+ foreach (static::$container as $model => $listeners) {
+ if ($id = array_search($listener, $listeners)) {
+ unset($listeners[$id]);
+ static::$container[$model] = array_values($listeners);
+ }
+ }
+ } else {
+ static::$container = [];
+ }
}
}
diff --git a/src/nats/composer.json b/src/nats/composer.json
index 92ce15f67..7b3c65f6a 100644
--- a/src/nats/composer.json
+++ b/src/nats/composer.json
@@ -17,9 +17,9 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/pool": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/pool": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"ircmaxell/random-lib": "^1.2",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0"
diff --git a/src/nsq/composer.json b/src/nsq/composer.json
index 63ee45b3a..c421507ca 100644
--- a/src/nsq/composer.json
+++ b/src/nsq/composer.json
@@ -18,8 +18,8 @@
"require": {
"php": ">=7.2",
"ext-bcmath": "*",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"jean85/pretty-package-versions": "^1.2"
},
"require-dev": {
diff --git a/src/paginator/composer.json b/src/paginator/composer.json
index 76f70f818..ab3b46317 100644
--- a/src/paginator/composer.json
+++ b/src/paginator/composer.json
@@ -16,13 +16,13 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
- "hyperf/event": "~1.1.0",
- "hyperf/http-server": "~1.1.0",
- "hyperf/framework": "~1.1.0",
+ "hyperf/event": "~2.0.0",
+ "hyperf/http-server": "~2.0.0",
+ "hyperf/framework": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/pool/composer.json b/src/pool/composer.json
index 92460f4fc..5411bb4d8 100644
--- a/src/pool/composer.json
+++ b/src/pool/composer.json
@@ -18,8 +18,8 @@
"require": {
"php": ">=7.2",
"psr/container": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/pool/src/Pool.php b/src/pool/src/Pool.php
index 5baeb8916..abbd04134 100644
--- a/src/pool/src/Pool.php
+++ b/src/pool/src/Pool.php
@@ -15,6 +15,7 @@ use Hyperf\Contract\ConnectionInterface;
use Hyperf\Contract\FrequencyInterface;
use Hyperf\Contract\PoolInterface;
use Hyperf\Contract\PoolOptionInterface;
+use Hyperf\Contract\StdoutLoggerInterface;
use Psr\Container\ContainerInterface;
use RuntimeException;
use Throwable;
@@ -83,9 +84,16 @@ abstract class Pool implements PoolInterface
if ($num > 0) {
/** @var ConnectionInterface $conn */
while ($this->currentConnections > $this->option->getMinConnections() && $conn = $this->channel->pop($this->option->getWaitTimeout())) {
- $conn->close();
- --$this->currentConnections;
- --$num;
+ try {
+ $conn->close();
+ } catch (\Throwable $exception) {
+ if ($this->container->has(StdoutLoggerInterface::class) && $logger = $this->container->get(StdoutLoggerInterface::class)) {
+ $logger->error((string) $exception);
+ }
+ } finally {
+ --$this->currentConnections;
+ --$num;
+ }
if ($num <= 0) {
// Ignore connections queued during flushing.
diff --git a/src/pool/tests/PoolTest.php b/src/pool/tests/PoolTest.php
new file mode 100644
index 000000000..0ed3a5da3
--- /dev/null
+++ b/src/pool/tests/PoolTest.php
@@ -0,0 +1,64 @@
+getContainer();
+ $container->shouldReceive('has')->with(StdoutLoggerInterface::class)->andReturn(true);
+ $container->shouldReceive('get')->with(StdoutLoggerInterface::class)->andReturn(value(function () {
+ $logger = Mockery::mock(StdoutLoggerInterface::class);
+ $logger->shouldReceive('error')->withAnyArgs()->times(4)->andReturn(true);
+ return $logger;
+ }));
+ $pool = new FooPool($container, []);
+
+ $conns = [];
+ for ($i = 0; $i < 5; ++$i) {
+ $conns[] = $pool->get();
+ }
+
+ foreach ($conns as $conn) {
+ $pool->release($conn);
+ }
+
+ $pool->flush();
+ $this->assertSame(1, $pool->getConnectionsInChannel());
+ $this->assertSame(1, $pool->getCurrentConnections());
+ }
+
+ protected function getContainer()
+ {
+ $container = Mockery::mock(ContainerInterface::class);
+ ApplicationContext::setContainer($container);
+
+ return $container;
+ }
+}
diff --git a/src/pool/tests/Stub/FooPool.php b/src/pool/tests/Stub/FooPool.php
new file mode 100644
index 000000000..484b87a6e
--- /dev/null
+++ b/src/pool/tests/Stub/FooPool.php
@@ -0,0 +1,24 @@
+=7.2",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
- "hyperf/contract": "~1.1.0"
+ "hyperf/contract": "~2.0.0"
},
"require-dev": {
- "hyperf/di": "~1.1.0",
- "hyperf/event": "~1.1.0",
+ "hyperf/di": "~2.0.0",
+ "hyperf/event": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/process/src/Listener/BootProcessListener.php b/src/process/src/Listener/BootProcessListener.php
index c7d73d15a..c69939579 100644
--- a/src/process/src/Listener/BootProcessListener.php
+++ b/src/process/src/Listener/BootProcessListener.php
@@ -87,6 +87,6 @@ class BootProcessListener implements ListenerInterface
private function getAnnotationProcesses()
{
- return AnnotationCollector::getClassByAnnotation(Process::class);
+ return AnnotationCollector::getClassesByAnnotation(Process::class);
}
}
diff --git a/src/protocol/composer.json b/src/protocol/composer.json
index e4f238aec..1f287a153 100644
--- a/src/protocol/composer.json
+++ b/src/protocol/composer.json
@@ -17,7 +17,7 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0"
+ "hyperf/contract": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/rate-limit/composer.json b/src/rate-limit/composer.json
index effcd07b1..d0bd6a8fb 100644
--- a/src/rate-limit/composer.json
+++ b/src/rate-limit/composer.json
@@ -19,7 +19,7 @@
"php": ">=7.2",
"psr/container": "^1.0",
"psr/simple-cache": "^1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/utils": "~2.0.0",
"bandwidth-throttle/token-bucket": "^2.0"
},
"require-dev": {
@@ -27,10 +27,10 @@
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
"friendsofphp/php-cs-fixer": "^2.9",
- "hyperf/contract": "~1.1.0",
- "hyperf/di": "~1.1.0",
- "hyperf/http-server": "~1.1.0",
- "hyperf/redis": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/di": "~2.0.0",
+ "hyperf/http-server": "~2.0.0",
+ "hyperf/redis": "~2.0.0"
},
"suggest": {
"hyperf/contract": "Required to use annotations.",
diff --git a/src/redis/composer.json b/src/redis/composer.json
index b23f95e13..2becdd899 100644
--- a/src/redis/composer.json
+++ b/src/redis/composer.json
@@ -18,13 +18,13 @@
"require": {
"php": ">=7.2",
"ext-redis": "*",
- "hyperf/contract": "~1.1.0",
- "hyperf/pool": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/pool": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"psr/container": "^1.0"
},
"require-dev": {
- "hyperf/di": "~1.1.0",
+ "hyperf/di": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/retry/composer.json b/src/retry/composer.json
index e1568be83..5e3846db4 100644
--- a/src/retry/composer.json
+++ b/src/retry/composer.json
@@ -17,11 +17,11 @@
"require": {
"php": ">=7.2",
"psr/container": "^1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
- "hyperf/contract": "~1.1.0",
- "hyperf/di": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/di": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/rpc-client/composer.json b/src/rpc-client/composer.json
index 993c1a4f7..aad215320 100644
--- a/src/rpc-client/composer.json
+++ b/src/rpc-client/composer.json
@@ -20,9 +20,9 @@
"require": {
"php": ">=7.2",
"psr/container": "^1.0",
- "hyperf/rpc": "~1.1.0",
- "hyperf/load-balancer": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/rpc": "~2.0.0",
+ "hyperf/load-balancer": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"roave/better-reflection": "^4.0"
},
"require-dev": {
diff --git a/src/rpc-server/composer.json b/src/rpc-server/composer.json
index 1b96537db..9aef9f562 100644
--- a/src/rpc-server/composer.json
+++ b/src/rpc-server/composer.json
@@ -18,8 +18,8 @@
},
"require": {
"php": ">=7.2",
- "hyperf/http-server": "~1.1.0",
- "hyperf/rpc": "~1.1.0"
+ "hyperf/http-server": "~2.0.0",
+ "hyperf/rpc": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/rpc/composer.json b/src/rpc/composer.json
index d32e2a0b2..42ef91dff 100644
--- a/src/rpc/composer.json
+++ b/src/rpc/composer.json
@@ -17,8 +17,8 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/server/composer.json b/src/server/composer.json
index dbd974b11..62a2e7834 100644
--- a/src/server/composer.json
+++ b/src/server/composer.json
@@ -20,9 +20,9 @@
"psr/container": "^1.0",
"psr/log": "^1.0",
"psr/event-dispatcher": "^1.0",
- "symfony/console": "^4.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "symfony/console": "^5.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/server/src/Listener/AfterWorkerStartListener.php b/src/server/src/Listener/AfterWorkerStartListener.php
index 3b9fa13ca..361a07ad0 100644
--- a/src/server/src/Listener/AfterWorkerStartListener.php
+++ b/src/server/src/Listener/AfterWorkerStartListener.php
@@ -54,18 +54,22 @@ class AfterWorkerStartListener implements ListenerInterface
/** @var Port $server */
foreach (ServerManager::list() as $name => [$type, $server]) {
$listen = $server->host . ':' . $server->port;
- $type = value(function () use ($type) {
+ $sockType = $server->type;
+ $type = value(function () use ($type, $sockType) {
switch ($type) {
case Server::SERVER_BASE:
- return 'TCP';
- break;
+ if (($sockType === SWOOLE_SOCK_TCP) || ($sockType === SWOOLE_SOCK_TCP6)) {
+ return 'TCP';
+ }
+ if (($sockType === SWOOLE_SOCK_UDP) || ($sockType === SWOOLE_SOCK_UDP6)) {
+ return 'UDP';
+ }
+ return 'UNKNOWN';
case Server::SERVER_WEBSOCKET:
return 'WebSocket';
- break;
case Server::SERVER_HTTP:
default:
return 'HTTP';
- break;
}
});
$serverType = $isCoroutineServer ? 'Coroutine' : '';
diff --git a/src/server/src/Listener/InitProcessTitleListener.php b/src/server/src/Listener/InitProcessTitleListener.php
index 1ed451caa..f86fe3d95 100644
--- a/src/server/src/Listener/InitProcessTitleListener.php
+++ b/src/server/src/Listener/InitProcessTitleListener.php
@@ -81,6 +81,15 @@ class InitProcessTitleListener implements ListenerInterface
protected function setTitle(string $title)
{
- @cli_set_process_title($title);
+ if ($this->isSupportedOS()) {
+ @cli_set_process_title($title);
+ }
+ }
+
+ protected function isSupportedOS(): bool
+ {
+ return ! in_array(PHP_OS, [
+ 'Darwin',
+ ]);
}
}
diff --git a/src/server/tests/Stub/InitProcessTitleListenerStub.php b/src/server/tests/Stub/InitProcessTitleListenerStub.php
index 24b3fba97..c76a82ce7 100644
--- a/src/server/tests/Stub/InitProcessTitleListenerStub.php
+++ b/src/server/tests/Stub/InitProcessTitleListenerStub.php
@@ -18,6 +18,8 @@ class InitProcessTitleListenerStub extends InitProcessTitleListener
{
public function setTitle(string $title)
{
- Context::set('test.server.process.title', $title);
+ if ($this->isSupportedOS()) {
+ Context::set('test.server.process.title', $title);
+ }
}
}
diff --git a/src/server/tests/Stub/InitProcessTitleListenerStub2.php b/src/server/tests/Stub/InitProcessTitleListenerStub2.php
index 6945fa04a..5360f831b 100644
--- a/src/server/tests/Stub/InitProcessTitleListenerStub2.php
+++ b/src/server/tests/Stub/InitProcessTitleListenerStub2.php
@@ -20,6 +20,8 @@ class InitProcessTitleListenerStub2 extends InitProcessTitleListener
public function setTitle(string $title)
{
- Context::set('test.server.process.title', $title);
+ if ($this->isSupportedOS()) {
+ Context::set('test.server.process.title', $title);
+ }
}
}
diff --git a/src/service-governance/composer.json b/src/service-governance/composer.json
index dff9e77d8..100fe4131 100644
--- a/src/service-governance/composer.json
+++ b/src/service-governance/composer.json
@@ -17,11 +17,11 @@
},
"require": {
"php": ">=7.2",
- "hyperf/consul": "~1.1.0",
- "hyperf/contract": "~1.1.0"
+ "hyperf/consul": "~2.0.0",
+ "hyperf/contract": "~2.0.0"
},
"require-dev": {
- "hyperf/event": "~1.1.0",
+ "hyperf/event": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/session/composer.json b/src/session/composer.json
index 5ce858afb..620c36690 100644
--- a/src/session/composer.json
+++ b/src/session/composer.json
@@ -16,7 +16,7 @@
"require": {
"php": ">=7.2",
"psr/http-server-middleware": "^1.0",
- "hyperf/utils": "^1.1"
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.9",
diff --git a/src/socket/composer.json b/src/socket/composer.json
index f3e61a63b..39ca10782 100644
--- a/src/socket/composer.json
+++ b/src/socket/composer.json
@@ -17,9 +17,9 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/protocol": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/protocol": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/socketio-server/.gitattributes b/src/socketio-server/.gitattributes
new file mode 100644
index 000000000..bdd4ea29c
--- /dev/null
+++ b/src/socketio-server/.gitattributes
@@ -0,0 +1 @@
+/tests export-ignore
\ No newline at end of file
diff --git a/src/swoole-enterprise/LICENSE b/src/socketio-server/LICENSE
similarity index 100%
rename from src/swoole-enterprise/LICENSE
rename to src/socketio-server/LICENSE
diff --git a/src/socketio-server/README.md b/src/socketio-server/README.md
new file mode 100644
index 000000000..b4542b463
--- /dev/null
+++ b/src/socketio-server/README.md
@@ -0,0 +1,290 @@
+Socket.io是一款非常流行的应用层实时通讯协议和框架,可以轻松实现应答、分组、广播。hyperf/socketio-server支持了Socket.io的WebSocket传输协议。
+
+## 安装
+
+```bash
+composer require hyperf/socketio-server
+```
+
+hyperf/socketio-server 是基于WebSocket实现的,请确保服务端已经添加了WebSocket服务配置。
+
+```php
+ [
+ 'name' => 'socket-io',
+ 'type' => Server::SERVER_WEBSOCKET,
+ 'host' => '0.0.0.0',
+ 'port' => 9502,
+ 'sock_type' => SWOOLE_SOCK_TCP,
+ 'callbacks' => [
+ SwooleEvent::ON_HAND_SHAKE => [Hyperf\WebSocketServer\Server::class, 'onHandShake'],
+ SwooleEvent::ON_MESSAGE => [Hyperf\WebSocketServer\Server::class, 'onMessage'],
+ SwooleEvent::ON_CLOSE => [Hyperf\WebSocketServer\Server::class, 'onClose'],
+ ],
+ ],
+```
+
+
+## 快速开始
+
+### 服务端
+```php
+join($data);
+ // 向房间内其他用户推送(不含当前用户)
+ $socket->to($data)->emit('event', $socket->getSid() . "has joined {$data}");
+ // 向房间内所有人广播(含当前用户)
+ $this->emit('event', 'There are ' . count($socket->getAdapter()->clients($data)) . " players in {$data}");
+ }
+
+ /**
+ * @Event("say")
+ * @param string $data
+ */
+ public function onSay(Socket $socket, $data)
+ {
+ $data = Json::decode($data);
+ $socket->to($data['room'])->emit('event', $socket->getSid() . " say: {$data['message']}");
+ }
+}
+
+```
+
+> 每个 socket 会自动加入以自己 `sid` 命名的房间(`$socket->getSid()`),发送私聊信息就推送到对应 `sid` 即可。
+
+> 框架会触发 `connect` 和 `disconnect` 两个事件。
+
+### 客户端
+
+由于服务端只实现了WebSocket通讯,所以客户端要加上 `{transports:["websocket"]}` 。
+
+```html
+
+
+```
+
+## API 清单
+
+```php
+emit('hello', 'can you hear me?', 1, 2, 'abc');
+
+ // sending to all clients except sender
+ $socket->broadcast->emit('broadcast', 'hello friends!');
+
+ // sending to all clients in 'game' room except sender
+ $socket->to('game')->emit('nice game', "let's play a game");
+
+ // sending to all clients in 'game1' and/or in 'game2' room, except sender
+ $socket->to('game1')->to('game2')->emit('nice game', "let's play a game (too)");
+
+ // WARNING: `$socket->to($socket->getSid())->emit()` will NOT work, as it will send to everyone in the room
+ // named `$socket->getSid()` but the sender. Please use the classic `$socket->emit()` instead.
+
+ // sending with acknowledgement
+ $reply = $socket->emit('question', 'do you think so?')->reply();
+
+ // sending without compression
+ $socket->compress(false)->emit('uncompressed', "that's rough");
+
+ $io = \Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\SocketIOServer\SocketIO::class);
+
+ // sending to all clients in 'game' room, including sender
+ $io->in('game')->emit('big-announcement', 'the game will start soon');
+
+ // sending to all clients in namespace 'myNamespace', including sender
+ $io->of('/myNamespace')->emit('bigger-announcement', 'the tournament will start soon');
+
+ // sending to a specific room in a specific namespace, including sender
+ $io->of('/myNamespace')->to('room')->emit('event', 'message');
+
+ // sending to individual socketid (private message)
+ $io->to('socketId')->emit('hey', 'I just met you');
+
+ // sending to all clients on this node (when using multiple nodes)
+ $io->local->emit('hi', 'my lovely babies');
+
+ // sending to all connected clients
+ $io->emit('an event sent to all connected clients');
+
+};
+```
+
+## 进阶教程
+
+### 设置 Socket.io 命名空间
+
+Socket.io 通过自定义命名空间实现多路复用。(注意:不是 PHP 的命名空间)
+
+1. 可以通过 `@SocketIONamespace("/xxx")` 将控制器映射为 xxx 的命名空间,
+
+2. 也可通过
+
+```php
+ swoole 4.4.17 及以下版本只能读取 http 创建好的Cookie,4.4.18 及以上版本可以在WebSocket握手时创建Cookie
+
+### 调整房间适配器
+
+默认的房间功能通过 Redis 适配器实现,可以适应多进程乃至分布式场景。
+
+1. 可以替换为内存适配器,只适用于单 worker 场景。
+```php
+ \Hyperf\SocketIOServer\Room\MemoryAdapter::class,
+];
+```
+
+2. 可以替换为空适配器,不需要房间功能时可以降低消耗。
+```php
+ \Hyperf\SocketIOServer\Room\NullAdapter::class,
+];
+```
+
+### 调整 SocketID (`sid`)
+
+默认 SocketID 使用 `ServerID#FD` 的格式,可以适应分布式场景。
+
+1. 可以替换为直接使用 Fd 。
+
+```php
+ \Hyperf\SocketIOServer\SidProvider\LocalSidProvider::class,
+];
+```
+
+2. 也可以替换为 SessionID 。
+
+```php
+ \Hyperf\SocketIOServer\SidProvider\SessionSidProvider::class,
+];
+```
+
+### 其他事件分发方法
+
+1. 可以手动注册事件,不使用注解。
+
+```php
+on('event', [$this, 'echo']);
+ }
+
+ public function echo(Socket $socket, $data)
+ {
+ $socket->emit('event', $data);
+ }
+}
+```
+
+2. 可以在控制器上添加 `@Event()` 注解,以方法名作为事件名来分发。此时应注意其他公有方法可能会和事件名冲突。
+
+```php
+emit('event', $data);
+ }
+}
+```
diff --git a/src/socketio-server/composer.json b/src/socketio-server/composer.json
new file mode 100644
index 000000000..5cbdf498b
--- /dev/null
+++ b/src/socketio-server/composer.json
@@ -0,0 +1,59 @@
+{
+ "name": "hyperf/socketio-server",
+ "type": "library",
+ "license": "MIT",
+ "keywords": [
+ "php",
+ "hyperf"
+ ],
+ "description": "Socket.io implementation for hyperf",
+ "autoload": {
+ "psr-4": {
+ "Hyperf\\SocketIOServer\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "HyperfTest\\SocketIOServer\\": "tests"
+ }
+ },
+ "require": {
+ "php": ">=7.2",
+ "ext-json": "*",
+ "ext-redis": "*",
+ "ext-swoole": ">=4.4",
+ "hyperf/di": "~2.0.0",
+ "hyperf/redis": "~2.0.0",
+ "hyperf/websocket-server": "~2.0.0",
+ "mix/redis-subscribe": "^2.1"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.14",
+ "hyperf/command": "~2.0.0",
+ "hyperf/config": "~2.0.0",
+ "hyperf/session": "~2.0.0",
+ "hyperf/testing": "~2.0.0",
+ "mockery/mockery": "^1.3",
+ "phpstan/phpstan": "^0.10.5"
+ },
+ "suggest": {
+ "hyperf/command": "Required to use RemoveRedisGarbage command",
+ "hyperf/session": "Required to use session"
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "scripts": {
+ "test": "co-phpunit -c phpunit.xml --colors=always",
+ "analyse": "phpstan analyse --memory-limit 300M -l 3 ./src",
+ "cs-fix": "php-cs-fixer fix $1"
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ },
+ "hyperf": {
+ "config": "Hyperf\\SocketIOServer\\ConfigProvider"
+ }
+ }
+}
diff --git a/src/socketio-server/src/Annotation/Event.php b/src/socketio-server/src/Annotation/Event.php
new file mode 100644
index 000000000..99f4a8272
--- /dev/null
+++ b/src/socketio-server/src/Annotation/Event.php
@@ -0,0 +1,48 @@
+bindMainProperty('event', $value);
+ }
+
+ public function collectMethod(string $className, ?string $target): void
+ {
+ EventAnnotationCollector::collectEvent($className, $target, $this);
+ parent::collectMethod($className, $target);
+ }
+
+ public function collectClass(string $className): void
+ {
+ $methods = ReflectionManager::reflectClass($className)->getMethods(ReflectionMethod::IS_PUBLIC);
+ foreach ($methods as $method) {
+ $target = $method->getName();
+ EventAnnotationCollector::collectEvent($className, $target, new Event(['value' => $target]));
+ }
+ parent::collectClass($className);
+ }
+}
diff --git a/src/socketio-server/src/Annotation/SocketIONamespace.php b/src/socketio-server/src/Annotation/SocketIONamespace.php
new file mode 100644
index 000000000..58933700c
--- /dev/null
+++ b/src/socketio-server/src/Annotation/SocketIONamespace.php
@@ -0,0 +1,36 @@
+bindMainProperty('namespace', $value);
+ }
+
+ public function collectClass(string $className): void
+ {
+ SocketIORouter::addNamespace($this->namespace, $className);
+ parent::collectClass($className);
+ }
+}
diff --git a/src/socketio-server/src/Aspect/SessionAspect.php b/src/socketio-server/src/Aspect/SessionAspect.php
new file mode 100644
index 000000000..f021463c9
--- /dev/null
+++ b/src/socketio-server/src/Aspect/SessionAspect.php
@@ -0,0 +1,63 @@
+sessionManager = $sessionManager;
+ $this->config = $config;
+ }
+
+ public function process(ProceedingJoinPoint $proceedingJoinPoint)
+ {
+ if (! $this->isSessionAvailable()) {
+ return $proceedingJoinPoint->process();
+ }
+ $request = Context::get(ServerRequestInterface::class);
+ $session = $this->sessionManager->start($request);
+ defer(function () use ($session) {
+ $this->sessionManager->end($session);
+ });
+ return $proceedingJoinPoint->process();
+ }
+
+ private function isSessionAvailable(): bool
+ {
+ return $this->config->has('session.handler');
+ }
+}
diff --git a/src/socketio-server/src/BaseNamespace.php b/src/socketio-server/src/BaseNamespace.php
new file mode 100644
index 000000000..a89bd371f
--- /dev/null
+++ b/src/socketio-server/src/BaseNamespace.php
@@ -0,0 +1,68 @@
+
+ */
+ private $eventHandlers = [];
+
+ public function __construct(Sender $sender, SidProviderInterface $sidProvider)
+ {
+ /* @var AdapterInterface adapter */
+ $this->adapter = make(AdapterInterface::class, ['sender' => $sender, 'nsp' => $this]);
+ $this->sidProvider = $sidProvider;
+ $this->sender = $sender;
+ $this->broadcast = true;
+ $this->on('connect', function (Socket $socket) {
+ $this->adapter->add($socket->getSid(), $socket->getSid());
+ });
+ $this->on('disconnect', function (Socket $socket) {
+ $this->adapter->del($socket->getSid());
+ });
+ }
+
+ /**
+ * Register socket.io event.
+ */
+ public function on(string $event, callable $callback)
+ {
+ $this->eventHandlers[$event][] = $callback;
+ }
+
+ /**
+ * Retrieves all callbacks for any events.
+ * @return array
+ */
+ public function getEventHandlers()
+ {
+ return $this->eventHandlers;
+ }
+
+ /**
+ * Returns the current namespace in string form.
+ */
+ public function getNamespace(): string
+ {
+ return SocketIORouter::getNamespace(static::class);
+ }
+}
diff --git a/src/socketio-server/src/Collector/EventAnnotationCollector.php b/src/socketio-server/src/Collector/EventAnnotationCollector.php
new file mode 100644
index 000000000..90fdf3176
--- /dev/null
+++ b/src/socketio-server/src/Collector/EventAnnotationCollector.php
@@ -0,0 +1,32 @@
+event)) {
+ static::$container[$class][$value->event][] = [$class, $method];
+ } else {
+ static::$container[$class][$value->event] = [[$class, $method]];
+ }
+ }
+}
diff --git a/src/socketio-server/src/Collector/SocketIORouter.php b/src/socketio-server/src/Collector/SocketIORouter.php
new file mode 100644
index 000000000..48b08e52f
--- /dev/null
+++ b/src/socketio-server/src/Collector/SocketIORouter.php
@@ -0,0 +1,60 @@
+has($class)) {
+ throw new RouteNotFoundException("namespace {$nsp} cannot be instantiated.");
+ }
+
+ $instance = ApplicationContext::getContainer()->get($class);
+
+ if (! ($instance instanceof NamespaceInterface)) {
+ throw new RouteNotFoundException("namespace {$nsp} must be an instance of NamespaceInterface");
+ }
+
+ return $instance->getAdapter();
+ }
+}
diff --git a/src/socketio-server/src/Command/RemoveRedisGarbage.php b/src/socketio-server/src/Command/RemoveRedisGarbage.php
new file mode 100644
index 000000000..2d6e55ba1
--- /dev/null
+++ b/src/socketio-server/src/Command/RemoveRedisGarbage.php
@@ -0,0 +1,61 @@
+redis = $factory->get($this->connection);
+ }
+
+ public function handle()
+ {
+ $nsp = $this->input->getArgument('namespace');
+ $prefix = join(':', [
+ $this->redisPrefix,
+ $nsp,
+ ]);
+ $iterator = null;
+ while (true) {
+ $keys = $this->redis->scan($iterator, "{$prefix}*");
+ if ($keys === false) {
+ return;
+ }
+ if (! empty($keys)) {
+ $this->redis->del(...$keys);
+ }
+ }
+ }
+
+ protected function getArguments()
+ {
+ return [
+ ['namespace', InputArgument::OPTIONAL, 'The namespace to be cleaned up.'],
+ ];
+ }
+}
diff --git a/src/socketio-server/src/ConfigProvider.php b/src/socketio-server/src/ConfigProvider.php
new file mode 100644
index 000000000..dcee82453
--- /dev/null
+++ b/src/socketio-server/src/ConfigProvider.php
@@ -0,0 +1,58 @@
+ [
+ Subscriber::class => SubscriberFactory::class,
+ AdapterInterface::class => RedisAdapter::class,
+ SidProviderInterface::class => DistributedSidProvider::class,
+ ],
+ 'listeners' => [
+ AddRouteListener::class,
+ ServerIdListener::class,
+ StartSubscriberListener::class,
+ ],
+ 'commands' => [
+ RemoveRedisGarbage::class,
+ ],
+ 'annotations' => [
+ 'scan' => [
+ 'paths' => [
+ __DIR__,
+ ],
+ 'collectors' => [
+ EventAnnotationCollector::class,
+ SocketIORouter::class,
+ ],
+ ],
+ ],
+ ];
+ }
+}
diff --git a/src/socketio-server/src/Emitter/Emitter.php b/src/socketio-server/src/Emitter/Emitter.php
new file mode 100644
index 000000000..039d79edd
--- /dev/null
+++ b/src/socketio-server/src/Emitter/Emitter.php
@@ -0,0 +1,178 @@
+{$flag} = true;
+ return $copy;
+ }
+ return $flag;
+ }
+
+ public function broadcast(bool $broadcast): self
+ {
+ $copy = clone $this;
+ $copy->broadcast = true;
+ return $copy;
+ }
+
+ public function compress(bool $compress): self
+ {
+ $copy = clone $this;
+ $copy->compress = $compress;
+ return $copy;
+ }
+
+ public function volatile(bool $volatile): self
+ {
+ $copy = clone $this;
+ $copy->volatile = $volatile;
+ return $copy;
+ }
+
+ public function binary(bool $binary): self
+ {
+ $copy = clone $this;
+ $copy->binary = $binary;
+ return $copy;
+ }
+
+ public function local(bool $local): self
+ {
+ $copy = clone $this;
+ $copy->local = $local;
+ return $copy;
+ }
+
+ /**
+ * @param int|string $room
+ */
+ public function to($room): self
+ {
+ $copy = clone $this;
+ $copy->to[] = (string) $room;
+ return $copy;
+ }
+
+ /**
+ * @param int|string $room
+ */
+ public function in($room): self
+ {
+ return $this->to($room);
+ }
+
+ /**
+ * @param mixed ...$data
+ * @return Future|void
+ */
+ public function emit(string $event, ...$data)
+ {
+ if ($this->broadcast || ! empty($this->to)) {
+ return $this->adapter->broadcast(
+ $this->encode('', $event, $data),
+ [
+ 'except' => [$this->sidProvider->getSid($this->fd)],
+ 'rooms' => $this->to,
+ 'flag' => [
+ 'compress' => $this->compress,
+ 'volatile' => $this->volatile,
+ 'local' => $this->local,
+ ],
+ ]
+ );
+ }
+
+ return make(Future::class, [
+ 'fd' => $this->fd,
+ 'event' => $event,
+ 'data' => $data,
+ 'encode' => function ($i, $event, $data) {
+ return $this->encode($i, $event, $data);
+ },
+ 'opcode' => SWOOLE_WEBSOCKET_OPCODE_TEXT,
+ 'flag' => $this->guessFlags($this->compress),
+ ]);
+ }
+
+ public function getAdapter(): AdapterInterface
+ {
+ return $this->adapter;
+ }
+
+ protected function encode(string $id, $event, $data)
+ {
+ $encoder = ApplicationContext::getContainer()->get(Encoder::class);
+ $packet = Packet::create([
+ 'type' => Packet::EVENT,
+ 'nsp' => method_exists($this, 'getNamespace') ? $this->getNamespace() : '/',
+ 'id' => $id,
+ 'data' => array_merge([$event], $data),
+ ]);
+ return Engine::MESSAGE . $encoder->encode($packet);
+ }
+}
diff --git a/src/socketio-server/src/Emitter/Flagger.php b/src/socketio-server/src/Emitter/Flagger.php
new file mode 100644
index 000000000..5ef901710
--- /dev/null
+++ b/src/socketio-server/src/Emitter/Flagger.php
@@ -0,0 +1,32 @@
+socketIO = $socketIO;
+ $this->sender = $sender;
+ $this->fd = $fd;
+ $this->id = '';
+ $this->event = $event;
+ $this->data = $data;
+ $this->encode = $encode;
+ $this->opcode = $opcode;
+ $this->flag = $flag;
+ $this->sent = false;
+ }
+
+ public function __destruct()
+ {
+ $this->send();
+ }
+
+ public function channel(?int $timeout = null): Channel
+ {
+ $channel = new Channel(1);
+ $this->id = strval(SocketIO::$messageId->get());
+ SocketIO::$messageId->add();
+ $this->socketIO->addCallback($this->id, $channel, $timeout);
+ return $channel;
+ }
+
+ public function reply(?int $timeout = null)
+ {
+ $channel = $this->channel($timeout);
+ $this->send();
+ return $channel->pop();
+ }
+
+ private function send()
+ {
+ if ($this->sent) {
+ return;
+ }
+ $message = ($this->encode)($this->id, $this->event, $this->data);
+ $this->sent = true;
+ $this->sender->push($this->fd, $message, $this->opcode, $this->flag);
+ }
+}
diff --git a/src/socketio-server/src/Exception/RouteNotFoundException.php b/src/socketio-server/src/Exception/RouteNotFoundException.php
new file mode 100644
index 000000000..b1cdc23fa
--- /dev/null
+++ b/src/socketio-server/src/Exception/RouteNotFoundException.php
@@ -0,0 +1,16 @@
+container = $container;
}
- /**
- * @return string[] returns the events that you want to listen
- */
public function listen(): array
{
return [
@@ -40,16 +40,17 @@ class BootApplicationListener implements ListenerInterface
}
/**
- * Handle the Event when the event is triggered, all listeners will
- * complete before the event is returned to the EventDispatcher.
+ * @param BeforeMainServerStart $event
*/
public function process(object $event)
{
- $configs = $this->container->get(ConfigInterface::class)->get('aspects', []);
- $aspect = new Aspect();
- foreach ($configs as $config) {
- if (is_string($config)) {
- $aspect->collectClass($config);
+ $serverConfig = $this->container->get(ConfigInterface::class)->get('server.servers', []);
+ foreach ($serverConfig as $port) {
+ if ($port['type'] === Server::SERVER_WEBSOCKET) {
+ $factory = $this->container->get(DispatcherFactory::class);
+ $factory
+ ->getRouter($port['name'])
+ ->addRoute('GET', '/socket.io/', SocketIO::class);
}
}
}
diff --git a/src/socketio-server/src/Listener/ServerIdListener.php b/src/socketio-server/src/Listener/ServerIdListener.php
new file mode 100644
index 000000000..13b025e4f
--- /dev/null
+++ b/src/socketio-server/src/Listener/ServerIdListener.php
@@ -0,0 +1,33 @@
+container->get(ConfigInterface::class);
-
- LazyLoader::bootstrap($configs);
+ foreach (SocketIORouter::get('forward') ?? [] as $class) {
+ $instance = $this->container->get($class);
+ if ($instance->getAdapter() instanceof RedisAdapter) {
+ $instance->getAdapter()->subscribe();
+ }
+ }
}
}
diff --git a/src/socketio-server/src/NamespaceInterface.php b/src/socketio-server/src/NamespaceInterface.php
new file mode 100644
index 000000000..069c0709d
--- /dev/null
+++ b/src/socketio-server/src/NamespaceInterface.php
@@ -0,0 +1,34 @@
+
+ */
+ public function getEventHandlers();
+
+ /**
+ * getNamespace method retrieves a string representation of this namespace.
+ */
+ public function getNamespace(): string;
+
+ /**
+ * getAdapter method retrieves an adapter to be used in this namespace.
+ * The same adapter will not be reused in other namespace.
+ */
+ public function getAdapter(): AdapterInterface;
+}
diff --git a/src/socketio-server/src/Parser/Decoder.php b/src/socketio-server/src/Parser/Decoder.php
new file mode 100644
index 000000000..0d26b7319
--- /dev/null
+++ b/src/socketio-server/src/Parser/Decoder.php
@@ -0,0 +1,52 @@
+ $i && filter_var($payload[$i], FILTER_VALIDATE_INT) !== false) {
+ $id .= $payload[$i];
+ ++$i;
+ }
+
+ // data
+ $data = json_decode(mb_substr($payload, $i), true) ?? [];
+ return Packet::create([
+ 'type' => $type,
+ 'nsp' => $nsp,
+ 'id' => $id,
+ 'data' => $data,
+ ]);
+ }
+}
diff --git a/src/socketio-server/src/Parser/Encoder.php b/src/socketio-server/src/Parser/Encoder.php
new file mode 100644
index 000000000..8b142ce3f
--- /dev/null
+++ b/src/socketio-server/src/Parser/Encoder.php
@@ -0,0 +1,29 @@
+data)) {
+ $noData = true;
+ }
+ return implode('', [
+ $packet->type,
+ $packet->nsp === '/' ? '' : $packet->nsp . ',',
+ $packet->id,
+ $noData ? '' : json_encode($packet->data),
+ ]);
+ }
+}
diff --git a/src/socketio-server/src/Parser/Engine.php b/src/socketio-server/src/Parser/Engine.php
new file mode 100644
index 000000000..b0b5cd8f1
--- /dev/null
+++ b/src/socketio-server/src/Parser/Engine.php
@@ -0,0 +1,29 @@
+id = $decoded['id'] ?? '';
+ $new->type = $decoded['type'];
+ if (isset($decoded['nsp'])) {
+ $new->nsp = $decoded['nsp'] ?: '/';
+ } else {
+ $new->nsp = '/';
+ }
+ $new->data = $decoded['data'] ?? null;
+ return $new;
+ }
+
+ public function offsetExists($offset)
+ {
+ return isset($this->{$offset});
+ }
+
+ public function offsetGet($offset)
+ {
+ return $this->{$offset};
+ }
+
+ public function offsetSet($offset, $value)
+ {
+ $this->{$offset} = $value;
+ }
+
+ public function offsetUnset($offset)
+ {
+ unset($this->{$offset});
+ }
+}
diff --git a/src/socketio-server/src/Room/AdapterInterface.php b/src/socketio-server/src/Room/AdapterInterface.php
new file mode 100644
index 000000000..9b9dd297a
--- /dev/null
+++ b/src/socketio-server/src/Room/AdapterInterface.php
@@ -0,0 +1,43 @@
+sender = $sender;
+ $this->sidProvider = $sidProvider;
+ }
+
+ public function add(string $sid, string ...$rooms)
+ {
+ $this->sids[$sid] = $this->sids[$sid] ?? [];
+ foreach ($rooms as $room) {
+ $this->sids[$sid][$room] = true;
+ $this->rooms[$room] = $this->rooms[$room] ?? make(MemoryRoom::class);
+ $this->rooms[$room]->add($sid);
+ }
+ }
+
+ public function del(string $sid, string ...$rooms)
+ {
+ if (count($rooms) === 0) {
+ $this->del($sid, ...$this->clientRooms($sid));
+ unset($this->sids[$sid]);
+ }
+
+ foreach ($rooms as $room) {
+ if (isset($this->sids[$sid])) {
+ unset($this->sids[$sid][$room]);
+ }
+ if (isset($this->rooms[$room])) {
+ $this->rooms[$room]->del($sid);
+ if ($this->rooms[$room]->size() === 0) {
+ unset($this->rooms[$room]);
+ }
+ }
+ }
+ }
+
+ public function broadcast($packet, $opts)
+ {
+ $rooms = data_get($opts, 'rooms', []);
+ $except = data_get($opts, 'except', []);
+ $volatile = data_get($opts, 'flag.volatile', false);
+ $compress = data_get($opts, 'flag.compress', false);
+ $wsFlag = $this->guessFlags((bool) $compress);
+ $pushed = [];
+ if (! empty($rooms)) {
+ foreach ($rooms as $room) {
+ $room = $this->rooms[$room] ?? null;
+ if (! $room) {
+ continue;
+ }
+ foreach ($room->list() as $sid) {
+ $sid = strval($sid);
+ if (in_array($sid, $except)) {
+ continue;
+ }
+ $fd = $this->sidProvider->getFd($sid);
+ $this->sender->push(
+ $fd,
+ $packet,
+ SWOOLE_WEBSOCKET_OPCODE_TEXT,
+ $wsFlag
+ );
+ $pushed[$fd] = true;
+ }
+ }
+ } else {
+ foreach (array_keys($this->sids) as $sid) {
+ $sid = strval($sid);
+ if (in_array($sid, $except)) {
+ continue;
+ }
+ $fd = $this->sidProvider->getFd($sid);
+ $this->sender->push($fd, $packet, SWOOLE_WEBSOCKET_OPCODE_TEXT, $wsFlag);
+ }
+ }
+ }
+
+ public function clients(string ...$rooms): array
+ {
+ $pushed = [];
+ $result = [];
+ if (! empty($rooms)) {
+ foreach ($rooms as $room) {
+ if (! isset($this->rooms[$room])) {
+ continue;
+ }
+ $room = $this->rooms[$room];
+ foreach ($room->list() as $sid) {
+ $sid = strval($sid);
+ if (isset($pushed[$sid])) {
+ continue;
+ }
+ $result[] = $sid;
+ $pushed[$sid] = true;
+ }
+ }
+ } else {
+ foreach (array_keys($this->sids) as $sid) {
+ $result[] = strval($sid);
+ }
+ }
+ return $result;
+ }
+
+ public function clientRooms(string $sid): array
+ {
+ return array_map('strval', array_keys($this->sids[$sid] ?? []));
+ }
+}
diff --git a/src/socketio-server/src/Room/MemoryRoom.php b/src/socketio-server/src/Room/MemoryRoom.php
new file mode 100644
index 000000000..6abd1a2ec
--- /dev/null
+++ b/src/socketio-server/src/Room/MemoryRoom.php
@@ -0,0 +1,37 @@
+container[$sid] = true;
+ }
+
+ public function del(string $sid)
+ {
+ unset($this->container[$sid]);
+ }
+
+ public function size(): int
+ {
+ return count(array_keys($this->container));
+ }
+
+ public function list(): array
+ {
+ return array_keys($this->container);
+ }
+}
diff --git a/src/socketio-server/src/Room/NullAdapter.php b/src/socketio-server/src/Room/NullAdapter.php
new file mode 100644
index 000000000..dda291a9d
--- /dev/null
+++ b/src/socketio-server/src/Room/NullAdapter.php
@@ -0,0 +1,37 @@
+sender = $sender;
+ $this->nsp = $nsp;
+ $this->redis = $redis->get($this->connection);
+ $this->sidProvider = $sidProvider;
+ }
+
+ public function add(string $sid, string ...$rooms)
+ {
+ $this->redis->multi();
+ $this->redis->sAdd($this->getSidKey($sid), ...$rooms);
+ foreach ($rooms as $room) {
+ $this->redis->sAdd($this->getRoomKey($room), $sid);
+ }
+ $this->redis->sAdd($this->getStatKey(), $sid);
+ $this->redis->exec();
+ }
+
+ public function del(string $sid, string ...$rooms)
+ {
+ if (count($rooms) === 0) {
+ $clientRooms = $this->clientRooms($sid);
+ if (empty($clientRooms)) {
+ return;
+ }
+ $this->del($sid, ...$clientRooms);
+ $this->redis->multi();
+ $this->redis->del($this->getSidKey($sid));
+ $this->redis->sRem($this->getStatKey(), $sid);
+ $this->redis->exec();
+ return;
+ }
+ $this->redis->multi();
+ $this->redis->sRem($this->getSidKey($sid), ...$rooms);
+ foreach ($rooms as $room) {
+ $this->redis->sRem($this->getRoomKey($room), $sid);
+ }
+ $this->redis->exec();
+ }
+
+ public function broadcast($packet, $opts)
+ {
+ $local = data_get($opts, 'flag.local', false);
+ if ($local) {
+ $this->doBroadcast($packet, $opts);
+ return;
+ }
+ $this->redis->publish($this->getChannelKey(), serialize([$packet, $opts]));
+ }
+
+ public function clients(string ...$rooms): array
+ {
+ $pushed = [];
+ $result = [];
+ if (! empty($rooms)) {
+ foreach ($rooms as $room) {
+ $sids = $this->redis->sMembers($this->getRoomKey($room));
+ if (! $sids) {
+ continue;
+ }
+ foreach ($sids as $sid) {
+ if (isset($pushed[$sid])) {
+ continue;
+ }
+ $result[] = $sid;
+ $pushed[$sid] = true;
+ }
+ }
+ } else {
+ $sids = $this->redis->sMembers($this->getStatKey());
+ foreach ($sids as $sid) {
+ $result[] = $sid;
+ }
+ }
+ return $result;
+ }
+
+ public function clientRooms(string $sid): array
+ {
+ return $this->redis->sMembers($this->getSidKey($sid));
+ }
+
+ public function subscribe()
+ {
+ Coroutine::create(function () {
+ CoordinatorManager::get(Constants::ON_WORKER_START)->yield();
+ retry(PHP_INT_MAX, function () {
+ $container = ApplicationContext::getContainer();
+ try {
+ $sub = $container->get(Subscriber::class);
+ if ($sub) {
+ $this->mixSubscribe($sub);
+ } else {
+ // Fallback to PhpRedis, which has a very bad blocking subscribe model.
+ $this->phpRedisSubscribe();
+ }
+ } catch (\Throwable $e) {
+ if ($container->has(StdoutLoggerInterface::class)) {
+ $logger = $container->get(StdoutLoggerInterface::class);
+ $logger->error($this->formatThrowable($e));
+ }
+ throw $e;
+ }
+ }, $this->retryInterval);
+ });
+ }
+
+ public function cleanUp(): void
+ {
+ $prefix = join(':', [
+ $this->redisPrefix,
+ $this->nsp->getNamespace(),
+ ]);
+ $iterator = null;
+ while (true) {
+ $keys = $this->redis->scan($iterator, "{$prefix}*");
+ if ($keys === false) {
+ return;
+ }
+ if (! empty($keys)) {
+ $this->redis->del(...$keys);
+ }
+ }
+ }
+
+ protected function doBroadcast($packet, $opts)
+ {
+ $rooms = data_get($opts, 'rooms', []);
+ $except = data_get($opts, 'except', []);
+ $volatile = data_get($opts, 'flag.volatile', false);
+ $compress = data_get($opts, 'flag.compress', false);
+ $wsFlag = $this->guessFlags((bool) $compress);
+
+ $pushed = [];
+ if (! empty($rooms)) {
+ foreach ($rooms as $room) {
+ $sids = $this->redis->sMembers($this->getRoomKey($room));
+ foreach ($sids as $sid) {
+ $fd = $this->getFd($sid);
+ if (in_array($sid, $except)) {
+ continue;
+ }
+ if ($this->isLocal($sid)) {
+ $this->sender->push(
+ $fd,
+ $packet,
+ SWOOLE_WEBSOCKET_OPCODE_TEXT,
+ $wsFlag
+ );
+ $pushed[$fd] = true;
+ }
+ }
+ }
+ } else {
+ $sids = $this->redis->sMembers($this->getStatKey());
+
+ foreach ($sids as $sid) {
+ $fd = $this->getFd($sid);
+ if (in_array($sid, $except)) {
+ continue;
+ }
+ if ($this->isLocal($sid)) {
+ $this->sender->push(
+ $fd,
+ $packet,
+ SWOOLE_WEBSOCKET_OPCODE_TEXT,
+ $wsFlag
+ );
+ }
+ }
+ }
+ }
+
+ protected function isLocal(string $sid): bool
+ {
+ return $this->sidProvider->isLocal($sid);
+ }
+
+ protected function getRoomKey(string $room): string
+ {
+ return join(':', [
+ $this->redisPrefix,
+ $this->nsp->getNamespace(),
+ 'rooms',
+ $room,
+ ]);
+ }
+
+ protected function getStatKey(): string
+ {
+ return join(':', [
+ $this->redisPrefix,
+ $this->nsp->getNamespace(),
+ 'stat',
+ ]);
+ }
+
+ protected function getSidKey(string $sid): string
+ {
+ return join(':', [
+ $this->redisPrefix,
+ $this->nsp->getNamespace(),
+ 'fds',
+ $sid,
+ ]);
+ }
+
+ protected function getChannelKey(): string
+ {
+ return join(':', [
+ $this->redisPrefix,
+ $this->nsp->getNamespace(),
+ 'channel',
+ ]);
+ }
+
+ protected function getFd(string $sid): int
+ {
+ return $this->sidProvider->getFd($sid);
+ }
+
+ private function formatThrowable(\Throwable $throwable): string
+ {
+ sprintf(
+ "%s:%s(%s) in %s:%s\nStack trace:\n%s",
+ get_class($throwable),
+ $throwable->getMessage(),
+ $throwable->getCode(),
+ $throwable->getFile(),
+ $throwable->getLine(),
+ $throwable->getTraceAsString()
+ );
+ }
+
+ private function phpRedisSubscribe()
+ {
+ $redis = $this->redis;
+ /** @var string $callback */
+ $callback = function ($redis, $chan, $msg) {
+ Coroutine::create(function () use ($msg) {
+ [$packet, $opts] = unserialize($msg);
+ $this->doBroadcast($packet, $opts);
+ });
+ };
+ // cast to string because PHPStan asked so.
+ $redis->subscribe([$this->getChannelKey()], $callback);
+ }
+
+ private function mixSubscribe(Subscriber $sub)
+ {
+ $sub->subscribe($this->getChannelKey());
+ $chan = $sub->channel();
+ if (! $chan) {
+ return;
+ }
+ Coroutine::create(function () use ($sub) {
+ CoordinatorManager::get(Constants::ON_WORKER_EXIT)->yield();
+ $sub->close();
+ });
+ while (true) {
+ $data = $chan->pop();
+ if (empty($data)) { // 手动close与redis异常断开都会导致返回false
+ if (! $sub->closed) {
+ throw new RuntimeException('Redis subscriber disconnected from Redis.');
+ }
+ break;
+ }
+
+ Coroutine::create(function () use ($data) {
+ [$packet, $opts] = unserialize($data->payload);
+ $this->doBroadcast($packet, $opts);
+ });
+ }
+ }
+}
diff --git a/src/socketio-server/src/Room/RoomInterface.php b/src/socketio-server/src/Room/RoomInterface.php
new file mode 100644
index 000000000..658c472b4
--- /dev/null
+++ b/src/socketio-server/src/Room/RoomInterface.php
@@ -0,0 +1,23 @@
+get(Redis::class);
+ $host = $redis->getHost();
+ $port = $redis->getPort();
+ $pass = $redis->getAuth();
+
+ try {
+ $sub = new Subscriber($host, $port, $pass ?? '', 5);
+ defer(function () use ($sub) {
+ $sub->close();
+ });
+ return $sub;
+ } catch (\Throwable $e) {
+ return null;
+ }
+ }
+}
diff --git a/src/socketio-server/src/SidProvider/DistributedSidProvider.php b/src/socketio-server/src/SidProvider/DistributedSidProvider.php
new file mode 100644
index 000000000..176418517
--- /dev/null
+++ b/src/socketio-server/src/SidProvider/DistributedSidProvider.php
@@ -0,0 +1,32 @@
+container = $container;
+ $this->config = $container->get(ConfigInterface::class);
+ $this->session = $container->get(SessionInterface::class);
+ }
+
+ public function getSid(int $fd): string
+ {
+ if ($fd === -1 || $fd === 0) {
+ return '';
+ }
+ $this->session->set('fd', $fd);
+ $this->session->set('server', SocketIO::$serverId);
+ $this->session->save();
+ return $this->session->getId();
+ }
+
+ public function isLocal(string $sid): bool
+ {
+ $session = $this->getSession($sid);
+ return $session->get('server') === SocketIO::$serverId;
+ }
+
+ public function getFd(string $sid): int
+ {
+ $session = $this->getSession($sid);
+ return (int) $session->get('fd');
+ }
+
+ protected function getSession(string $sid): SessionInterface
+ {
+ $session = new Session($this->getSessionName(), $this->buildSessionHandler(), $sid);
+ $session->start();
+ return $session;
+ }
+
+ protected function getSessionName(): string
+ {
+ return $this->config->get('session.options.session_name', 'HYPERF_SESSION_ID');
+ }
+
+ 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/socketio-server/src/SidProvider/SidProviderInterface.php b/src/socketio-server/src/SidProvider/SidProviderInterface.php
new file mode 100644
index 000000000..e0c19928f
--- /dev/null
+++ b/src/socketio-server/src/SidProvider/SidProviderInterface.php
@@ -0,0 +1,21 @@
+adapter = $adapter;
+ $this->sender = $sender;
+ $this->addCallback = $addCallback;
+ $this->fd = $fd;
+ $this->nsp = $nsp;
+ $this->sidProvider = $sidProvider;
+ $this->encoder = $encoder;
+ }
+
+ public function getFd(): int
+ {
+ return $this->fd;
+ }
+
+ public function getSid(): string
+ {
+ return $this->sidProvider->getSid($this->fd);
+ }
+
+ public function join(string ...$rooms)
+ {
+ $this->adapter->add($this->getSid(), ...$rooms);
+ }
+
+ public function leave(string ...$rooms)
+ {
+ $this->adapter->del($this->getSid(), ...$rooms);
+ }
+
+ public function leaveAll()
+ {
+ $this->adapter->del($this->getSid());
+ }
+
+ public function disconnect()
+ {
+ $closePacket = Packet::create([
+ 'type' => Packet::CLOSE,
+ 'nsp' => $this->nsp,
+ ]);
+ //notice client is about to disconnect
+ $this->sender->push($this->fd, Engine::MESSAGE . $this->encoder->encode($closePacket));
+ /** @var \Swoole\WebSocket\Server $server */
+ $server = ApplicationContext::getContainer()->get(Server::class);
+ $server->disconnect($this->fd);
+ }
+
+ public function getNamespace(): string
+ {
+ return $this->nsp;
+ }
+}
diff --git a/src/socketio-server/src/SocketIO.php b/src/socketio-server/src/SocketIO.php
new file mode 100644
index 000000000..d078ba2aa
--- /dev/null
+++ b/src/socketio-server/src/SocketIO.php
@@ -0,0 +1,311 @@
+ $socket->emit("message", "hello world");
+ * => sprintf('%d%d%s', $packetType, $packetDataType, json_encode([$event, $data]))
+ * => 42["message", "hello world"].
+ * @mixin BaseNamespace
+ */
+class SocketIO implements OnMessageInterface, OnOpenInterface, OnCloseInterface
+{
+ public static $isMainWorker = false;
+
+ /**
+ * @var string
+ */
+ public static $serverId;
+
+ /**
+ * @var \Swoole\Atomic
+ */
+ public static $messageId;
+
+ /**
+ * @var Channel[]
+ */
+ protected $clientCallbacks = [];
+
+ /**
+ * @var int
+ */
+ protected $clientCallbackTimeout = 10000;
+
+ /**
+ * @var int
+ */
+ protected $pingInterval = 10000;
+
+ /**
+ * @var int
+ */
+ protected $pingTimeout = 100;
+
+ /**
+ * @var StdoutLoggerInterface
+ */
+ protected $stdoutLogger;
+
+ /**
+ * @var Decoder
+ */
+ protected $decoder;
+
+ /**
+ * @var SidProviderInterface
+ */
+ private $sidProvider;
+
+ /**
+ * @var Encoder
+ */
+ private $encoder;
+
+ /**
+ * @var Sender
+ */
+ private $sender;
+
+ /**
+ * @var int[]
+ */
+ private $clientCallbackTimers;
+
+ public function __construct(StdoutLoggerInterface $stdoutLogger, Sender $sender, Decoder $decoder, Encoder $encoder, SidProviderInterface $sidProvider)
+ {
+ $this->stdoutLogger = $stdoutLogger;
+ $this->decoder = $decoder;
+ $this->encoder = $encoder;
+ $this->sender = $sender;
+ $this->sidProvider = $sidProvider;
+ }
+
+ public function __call($method, $args)
+ {
+ return $this->of('/')->{$method}(...$args);
+ }
+
+ public function onMessage(WebSocketServer $server, Frame $frame): void
+ {
+ if ($frame->data[0] === Engine::PING) {
+ $server->push($frame->fd, Engine::PONG); //sever pong
+ return;
+ }
+ if ($frame->data[0] !== Engine::MESSAGE) {
+ $this->stdoutLogger->error("EngineIO event type {$frame->data[0]} not supported");
+ return;
+ }
+ $packet = $this->decoder->decode(substr($frame->data, 1));
+ switch ($packet->type) {
+ case Packet::OPEN: //client open
+ $responsePacket = Packet::create([
+ 'type' => Packet::OPEN,
+ 'nsp' => $packet->nsp,
+ ]);
+ $server->push($frame->fd, Engine::MESSAGE . $this->encoder->encode($responsePacket)); //sever open
+ $this->dispatch($frame->fd, $packet->nsp, 'connect', $packet->data);
+ break;
+ case Packet::CLOSE: //client disconnect
+ $server->disconnect($frame->fd);
+ $this->dispatch($frame->fd, $packet->nsp, 'disconnect', $packet->data);
+ break;
+ case Packet::EVENT: // client message with ack
+ if ($packet->id !== '') {
+ $packet->data[] = function ($data) use ($frame, $packet) {
+ $responsePacket = Packet::create([
+ 'id' => $packet->id,
+ 'nsp' => $packet->nsp,
+ 'type' => Packet::ACK,
+ 'data' => $data,
+ ]);
+ $this->sender->push($frame->fd, Engine::MESSAGE . $this->encoder->encode($responsePacket));
+ };
+ }
+ $this->dispatch($frame->fd, $packet->nsp, ...$packet->data);
+ break;
+ case Packet::ACK: // server ack
+ $ackId = $packet->id;
+ if (isset($this->clientCallbacks[$ackId]) && $this->clientCallbacks[$ackId] instanceof Channel) {
+ if (is_array($packet->data)) {
+ foreach ($packet->data as $piece) {
+ $this->clientCallbacks[$ackId]->push($piece);
+ }
+ } else {
+ $this->clientCallbacks[$ackId]->push($packet->data);
+ }
+ unset($this->clientCallbacks[$ackId]);
+ Timer::clear($this->clientCallbackTimers[$ackId]);
+ }
+ break;
+ default:
+ $this->stdoutLogger->error("SocketIO packet type {$packet->type} not supported");
+ }
+ }
+
+ public function onOpen(WebSocketServer $server, Request $request): void
+ {
+ $data = [
+ 'sid' => $this->sidProvider->getSid($request->fd),
+ 'upgrades' => ['websocket'],
+ 'pingInterval' => $this->pingInterval,
+ 'pingTimeout' => $this->pingTimeout,
+ ];
+ $server->push($request->fd, Engine::OPEN . json_encode($data)); //socket is open
+ $server->push($request->fd, Engine::MESSAGE . Packet::OPEN); //server open
+
+ $this->dispatchEventInAllNamespaces($request->fd, 'connect');
+ }
+
+ public function onClose(Server $server, int $fd, int $reactorId): void
+ {
+ $this->dispatchEventInAllNamespaces($fd, 'disconnect');
+ }
+
+ /**
+ * @return NamespaceInterface | BaseNamespace possibly a BaseNamespace, but allow user to use any NamespaceInterface implementation instead
+ */
+ public function of(string $nsp): NamespaceInterface
+ {
+ $class = SocketIORouter::getClassName($nsp);
+ if (! $class) {
+ throw new RouteNotFoundException("namespace {$nsp} is not registered.");
+ }
+ if (! ApplicationContext::getContainer()->has($class)) {
+ throw new RouteNotFoundException("namespace {$nsp} cannot be instantiated.");
+ }
+ return ApplicationContext::getContainer()->get($class);
+ }
+
+ public function addCallback(string $ackId, Channel $channel, int $timeoutMs = null)
+ {
+ $this->clientCallbacks[$ackId] = $channel;
+ // Clean up using timer to avoid memory leak.
+ $timerId = Timer::after($timeoutMs ?? $this->clientCallbackTimeout, function () use ($ackId) {
+ if (! isset($this->clientCallbacks[$ackId])) {
+ return;
+ }
+ $this->clientCallbacks[$ackId]->close();
+ unset($this->clientCallbacks[$ackId]);
+ });
+ $this->clientCallbackTimers[$ackId] = $timerId;
+ }
+
+ private function dispatch(int $fd, string $nsp, string $event, ...$payloads)
+ {
+ $socket = $this->makeSocket($fd, $nsp);
+ $ack = null;
+
+ // Check if ack is required
+ $last = array_pop($payloads);
+ if ($last instanceof \Closure) {
+ $ack = $last;
+ } else {
+ array_push($payloads, $last);
+ }
+
+ $handlers = $this->getEventHandlers($nsp, $event);
+ foreach ($handlers as $handler) {
+ $result = $handler($socket, ...$payloads);
+ $ack && $ack([$result]);
+ }
+ }
+
+ private function getEventHandlers(string $nsp, string $event): array
+ {
+ $class = SocketIORouter::getClassName($nsp);
+ /** @var NamespaceInterface $instance */
+ $instance = ApplicationContext::getContainer()->get($class);
+
+ /** @var callable[] $output */
+ $output = [];
+
+ foreach (EventAnnotationCollector::get($class . '.' . $event, []) as [, $method]) {
+ $output[] = [$instance, $method];
+ }
+
+ foreach ($instance->getEventHandlers() as $key => $callbacks) {
+ if ($key === $event) {
+ $output = array_merge($output, $callbacks);
+ }
+ }
+
+ return $output;
+ }
+
+ private function makeSocket(int $fd, string $nsp = '/'): Socket
+ {
+ return make(Socket::class, [
+ 'adapter' => SocketIORouter::getAdapter($nsp),
+ 'sender' => $this->sender,
+ 'fd' => $fd,
+ 'nsp' => $nsp,
+ 'addCallback' => function (string $ackId, Channel $channel, ?int $timeout = null) {
+ $this->addCallback($ackId, $channel, $timeout);
+ }, ]);
+ }
+
+ private function dispatchEventInAllNamespaces(int $fd, string $event)
+ {
+ $all = SocketIORouter::list();
+ if (! array_key_exists('forward', $all)) {
+ return;
+ }
+ foreach (array_keys($all['forward']) as $nsp) {
+ $this->dispatch($fd, $nsp, $event, null);
+ }
+ }
+}
diff --git a/src/socketio-server/tests/Cases/AbstractTestCase.php b/src/socketio-server/tests/Cases/AbstractTestCase.php
new file mode 100644
index 000000000..6a90e08e2
--- /dev/null
+++ b/src/socketio-server/tests/Cases/AbstractTestCase.php
@@ -0,0 +1,61 @@
+define(StdoutLoggerInterface::class, StdoutLogger::class);
+ $container->define(RoomInterface::class, MemoryRoom::class);
+ $container->define(AdapterInterface::class, MemoryAdapter::class);
+ $container->define(SidProviderInterface::class, LocalSidProvider::class);
+ $container->define(ConfigInterface::class, Config::class);
+ $container->define(Encoder::class, Encoder::class);
+ $container->set(ConfigInterface::class, new Config([]));
+ SocketIO::$messageId = new Atomic();
+ ApplicationContext::setContainer($container);
+ return $container;
+ }
+}
diff --git a/src/socketio-server/tests/Cases/DecoderTest.php b/src/socketio-server/tests/Cases/DecoderTest.php
new file mode 100644
index 000000000..c4f5be9be
--- /dev/null
+++ b/src/socketio-server/tests/Cases/DecoderTest.php
@@ -0,0 +1,56 @@
+decode('2["foo","bar"]');
+ $this->assertEquals('', $packet['id']);
+ $this->assertEquals('2', $packet['type']);
+ $this->assertEquals('/', $packet['nsp']);
+ $this->assertEquals(['foo', 'bar'], $packet['data']);
+ $packet = $decoder->decode('2/ws,["foo","bar"]');
+ $this->assertEquals('', $packet['id']);
+ $this->assertEquals('2', $packet['type']);
+ $this->assertEquals('/ws', $packet['nsp']);
+ $this->assertEquals(['foo', 'bar'], $packet['data']);
+ $packet = $decoder->decode('2/ws,15["foo","bar"]');
+ $this->assertEquals('15', $packet['id']);
+ $this->assertEquals('2', $packet['type']);
+ $this->assertEquals('/ws', $packet['nsp']);
+ $this->assertEquals(['foo', 'bar'], $packet['data']);
+ $packet = $decoder->decode('215["foo","bar"]');
+ $this->assertEquals('15', $packet['id']);
+ $this->assertEquals('2', $packet['type']);
+ $this->assertEquals('/', $packet['nsp']);
+ $this->assertEquals(['foo', 'bar'], $packet['data']);
+ $packet = $decoder->decode('215');
+ $this->assertEquals('15', $packet['id']);
+ $this->assertEquals('2', $packet['type']);
+ $this->assertEquals('/', $packet['nsp']);
+ $this->assertEquals([], $packet['data']);
+ $packet = $decoder->decode('1');
+ $this->assertEquals('', $packet['id']);
+ $this->assertEquals('1', $packet['type']);
+ $this->assertEquals('/', $packet['nsp']);
+ $this->assertEquals([], $packet['data']);
+ }
+}
diff --git a/src/socketio-server/tests/Cases/EncoderTest.php b/src/socketio-server/tests/Cases/EncoderTest.php
new file mode 100644
index 000000000..1f8eada1d
--- /dev/null
+++ b/src/socketio-server/tests/Cases/EncoderTest.php
@@ -0,0 +1,73 @@
+ '',
+ 'nsp' => '/',
+ 'type' => Packet::OPEN,
+ 'data' => '',
+ ]);
+ $this->assertEquals('0', $encoder->encode($packet));
+ $packet = Packet::create([
+ 'id' => '',
+ 'nsp' => '',
+ 'type' => Packet::OPEN,
+ 'data' => '',
+ ]);
+ $this->assertEquals('0', $encoder->encode($packet));
+ $packet = Packet::create([
+ 'id' => '12',
+ 'nsp' => '/ws',
+ 'type' => Packet::EVENT,
+ 'data' => ['fake', 'data'],
+ ]);
+ $this->assertEquals('2/ws,12["fake","data"]', $encoder->encode($packet));
+ $packet = Packet::create([
+ 'id' => '12',
+ 'nsp' => '/ws',
+ 'type' => Packet::ACK,
+ 'data' => ['fake', 'data'],
+ ]);
+ $this->assertEquals('3/ws,12["fake","data"]', $encoder->encode($packet));
+ $packet = Packet::create([
+ 'id' => '12',
+ 'nsp' => '/ws',
+ 'type' => Packet::EVENT,
+ 'data' => false,
+ ]);
+ $this->assertEquals('2/ws,12', $encoder->encode($packet));
+ $packet = Packet::create([
+ 'id' => '12',
+ 'nsp' => '/-!@#$%^&*()',
+ 'type' => Packet::EVENT,
+ 'data' => false,
+ ]);
+ $this->assertEquals('2/-!@#$%^&*(),12', $encoder->encode($packet));
+ $packet = Packet::create([
+ 'type' => Packet::CLOSE,
+ ]);
+ $this->assertEquals('1', $encoder->encode($packet));
+ }
+}
diff --git a/src/socketio-server/tests/Cases/FutureTest.php b/src/socketio-server/tests/Cases/FutureTest.php
new file mode 100644
index 000000000..9b2c87c30
--- /dev/null
+++ b/src/socketio-server/tests/Cases/FutureTest.php
@@ -0,0 +1,91 @@
+getContainer();
+ }
+
+ public function testDestruct()
+ {
+ /** @var ContainerInterface $container */
+ $container = ApplicationContext::getContainer();
+ $mock = Mockery::mock(Sender::class);
+ $mock->shouldReceive('push')->with(1, Mockery::any(), Mockery::any(), Mockery::any())->once();
+ $container->set(Sender::class, $mock);
+ $future = make(Future::class, ['fd' => 1,
+ 'event' => 'event',
+ 'data' => [''],
+ 'encode' => function () {
+ return '';
+ },
+ 'opcode' => 0,
+ 'flag' => 0, ]);
+ unset($future);
+ $this->assertTrue(true);
+ }
+
+ public function testChannel()
+ {
+ /** @var ContainerInterface $container */
+ $container = ApplicationContext::getContainer();
+ $mock = Mockery::mock(Sender::class);
+ $mock->shouldReceive('push')->with(1, Mockery::any(), Mockery::any(), Mockery::any())->once();
+ $container->set(Sender::class, $mock);
+ /** @var Future $future */
+ $future = make(Future::class, ['fd' => 1,
+ 'event' => 'event',
+ 'data' => [''],
+ 'encode' => function () {
+ return '';
+ },
+ 'opcode' => 0,
+ 'flag' => 0, ]);
+ $ch = $future->channel();
+ $this->assertInstanceOf(Channel::class, $ch);
+ }
+
+ public function testReply()
+ {
+ /** @var ContainerInterface $container */
+ $container = ApplicationContext::getContainer();
+ $mock = Mockery::mock(Sender::class);
+ $mock->shouldReceive('push')->with(1, Mockery::any(), Mockery::any(), Mockery::any())->once();
+ $container->set(Sender::class, $mock);
+ /** @var Future $future */
+ $future = make(Future::class, ['fd' => 1,
+ 'event' => 'event',
+ 'data' => [''],
+ 'encode' => function () {
+ return '';
+ },
+ 'opcode' => 0,
+ 'flag' => 0, ]);
+ $ch = $future->reply(1);
+ $this->assertTrue(true);
+ }
+}
diff --git a/src/socketio-server/tests/Cases/IONamespaceTest.php b/src/socketio-server/tests/Cases/IONamespaceTest.php
new file mode 100644
index 000000000..88407a566
--- /dev/null
+++ b/src/socketio-server/tests/Cases/IONamespaceTest.php
@@ -0,0 +1,111 @@
+getContainer();
+ }
+
+ public function testEmit()
+ {
+ $sender = Mockery::Spy(Sender::class);
+ $sidProvider = new LocalSidProvider();
+ $io = new BaseNamespace($sender, $sidProvider);
+ $io->getAdapter()->add('1');
+ $io->getAdapter()->add('2');
+ $io->emit('hello', 'world');
+ $sender->shouldHaveReceived('push')->twice();
+ $this->assertTrue(true);
+ }
+
+ public function testGetNsp()
+ {
+ $sender = Mockery::Mock(Sender::class);
+ $sidProvider = new LocalSidProvider();
+ $io = new BaseNamespace($sender, $sidProvider);
+ $this->assertEquals('/', $io->getNamespace());
+ }
+
+ public function testGetAdapter()
+ {
+ $sender = Mockery::Mock(Sender::class);
+ $sidProvider = new LocalSidProvider();
+ $io = new BaseNamespace($sender, $sidProvider);
+ $this->assertInstanceOf(AdapterInterface::class, $io->getAdapter());
+ }
+
+ public function testEmitResponse()
+ {
+ $sender = Mockery::Spy(Sender::class);
+ $sidProvider = new LocalSidProvider();
+ $io = new BaseNamespace($sender, $sidProvider);
+ SocketIORouter::addNamespace('/', BaseNamespace::class);
+ SocketIO::$messageId = new Atomic();
+ $io->getAdapter()->add('1');
+ $io->getAdapter()->add('2');
+ $io->emit('hello', 'world', true);
+ $sender->shouldHaveReceived('push')->twice();
+ $this->assertTrue(true);
+ }
+
+ public function testBroadcast()
+ {
+ SocketIO::$messageId = new Atomic();
+ $sender = Mockery::Mock(Sender::class);
+ $sender->shouldNotReceive('push')->withAnyArgs();
+ $sidProvider = new LocalSidProvider();
+ $io = new BaseNamespace($sender, $sidProvider);
+ $io->broadcast->emit('hello', 'world', true);
+ $this->assertTrue(true);
+ }
+
+ public function testNonExistRoom()
+ {
+ SocketIO::$messageId = new Atomic();
+ $sender = Mockery::Mock(Sender::class);
+ $sender->shouldNotReceive('push')->withAnyArgs();
+ $sidProvider = new LocalSidProvider();
+ $io = new BaseNamespace($sender, $sidProvider);
+ $io->to('non-exist')->emit('hello', 'world', false);
+ $this->assertTrue(true);
+ }
+
+ public function testExistRoom()
+ {
+ SocketIO::$messageId = new Atomic();
+ $sender = Mockery::spy(Sender::class);
+ $sidProvider = new LocalSidProvider();
+ $io = new BaseNamespace($sender, $sidProvider);
+ $io->getAdapter()->add('1', 'room');
+ $io->getAdapter()->add('2', 'room');
+ $io->to('room')->emit('hello', 'world', false);
+ $sender->shouldHaveReceived('push')->withAnyArgs()->twice();
+ $this->assertTrue(true);
+ }
+}
diff --git a/src/socketio-server/tests/Cases/RoomAdapterTest.php b/src/socketio-server/tests/Cases/RoomAdapterTest.php
new file mode 100644
index 000000000..e2146e416
--- /dev/null
+++ b/src/socketio-server/tests/Cases/RoomAdapterTest.php
@@ -0,0 +1,149 @@
+shouldReceive('push')->twice();
+ $room = new MemoryAdapter($server, $sidProvider);
+ $room->add('42', 'universe', '42');
+ $room->add('43', 'universe', '43');
+ $this->assertContains('universe', $room->clientRooms('43'));
+ $this->assertContains('43', $room->clientRooms('43'));
+ $this->assertContains('42', $room->clientRooms('42'));
+ $this->assertContains('universe', $room->clientRooms('42'));
+ $this->assertContains('42', $room->clients('universe'));
+ $this->assertContains('43', $room->clients('universe'));
+ $room->broadcast('', ['rooms' => ['universe']]);
+ $room->del('42', 'universe');
+ $this->assertContains('42', $room->clientRooms('42'));
+ $this->assertNotContains('universe', $room->clientRooms('42'));
+ $this->assertNotEmpty($room->clientRooms('42'));
+ $room->del('43');
+ $this->assertNotContains('43', $room->clientRooms('43'));
+ $this->assertNotContains('universe', $room->clientRooms('43'));
+ $this->assertEmpty($room->clientRooms('43'));
+ $room->broadcast('', ['rooms' => ['universe']]);
+ }
+
+ public function testRedisAdapter()
+ {
+ $nsp = Mockery::Mock(NamespaceInterface::class);
+ $nsp->shouldReceive('getNamespace')->andReturn('test');
+ $redis = $this->getRedis();
+ $server = Mockery::Mock(Sender::class);
+ $server->shouldReceive('push')->twice();
+ $sidProvider = new LocalSidProvider();
+ $room = new RedisAdapter($redis, $server, $nsp, $sidProvider);
+ $room->add('42', 'universe', '42');
+ $room->add('43', 'universe', '43');
+ $this->assertContains('universe', $room->clientRooms('43'));
+ $this->assertContains('43', $room->clientRooms('43'));
+ $this->assertContains('42', $room->clientRooms('42'));
+ $this->assertContains('universe', $room->clientRooms('42'));
+ $this->assertContains('42', $room->clients('universe'));
+ $this->assertContains('43', $room->clients('universe'));
+ $room->broadcast('', ['rooms' => ['universe'], 'flag' => ['local' => true]]);
+ $room->del('42', 'universe');
+ $this->assertContains('42', $room->clientRooms('42'));
+ $this->assertNotContains('universe', $room->clientRooms('42'));
+ $this->assertNotEmpty($room->clientRooms('42'));
+ $room->del('43');
+ $this->assertNotContains('43', $room->clientRooms('43'));
+ $this->assertNotContains('universe', $room->clientRooms('43'));
+ $this->assertEmpty($room->clientRooms('43'));
+ $room->broadcast('', ['rooms' => ['universe'], 'flag' => ['local' => true]]);
+ $room->cleanUp();
+ $this->assertNotContains('42', $room->clientRooms('42'));
+
+ // Test empty room
+ try {
+ $room->del('non-exist');
+ } catch (\Throwable $t) {
+ $this->assertTrue(false);
+ }
+ }
+
+ private function getRedis($options = [])
+ {
+ $container = Mockery::mock(Container::class);
+ $container->shouldReceive('get')->once()->with(ConfigInterface::class)->andReturn(new Config([
+ 'redis' => [
+ 'default' => [
+ 'host' => 'localhost',
+ 'auth' => null,
+ 'port' => 6379,
+ 'db' => 0,
+ 'options' => $options,
+ 'pool' => [
+ 'min_connections' => 1,
+ 'max_connections' => 30,
+ 'connect_timeout' => 10.0,
+ 'wait_timeout' => 3.0,
+ 'heartbeat' => -1,
+ 'max_idle_time' => 60,
+ ],
+ ],
+ ],
+ ]));
+ $pool = new RedisPool($container, 'default');
+ $frequency = Mockery::mock(LowFrequencyInterface::class);
+ $frequency->shouldReceive('isLowFrequency')->andReturn(false);
+ $subscriber = Mockery::mock(Subscriber::class);
+ $subscriber->shouldReceive('subscribe')->withAnyArgs()->andReturn();
+ $subscriber->shouldReceive('channel')->andReturn(false);
+ $container->shouldReceive('has')->andReturn(false);
+ $container->shouldReceive('get')->with(StdoutLoggerInterface::class)->andReturn(new StdoutLogger(new Config([])));
+ $container->shouldReceive('get')->with(Subscriber::class)->andReturn($subscriber);
+ $container->shouldReceive('make')->with(Frequency::class, Mockery::any())->andReturn($frequency);
+ $container->shouldReceive('make')->with(RedisPool::class, ['name' => 'default'])->andReturn($pool);
+ $container->shouldReceive('make')->with(Channel::class, ['size' => 30])->andReturn(new Channel(30));
+ $container->shouldReceive('make')->with(PoolOption::class, Mockery::any())->andReturnUsing(function ($class, $args) {
+ return new PoolOption(...array_values($args));
+ });
+ ApplicationContext::setContainer($container);
+ $factory = new PoolFactory($container);
+ $mock = Mockery::mock(RedisFactory::class);
+ $mock->shouldReceive('get')->andReturn(new Redis($factory));
+ return $mock;
+ }
+}
diff --git a/src/socketio-server/tests/Cases/SocketTest.php b/src/socketio-server/tests/Cases/SocketTest.php
new file mode 100644
index 000000000..998a34c1f
--- /dev/null
+++ b/src/socketio-server/tests/Cases/SocketTest.php
@@ -0,0 +1,114 @@
+getContainer();
+ }
+
+ public function testJoin()
+ {
+ /** @var Socket $socket */
+ $socket = make(Socket::class, [
+ 'fd' => 1,
+ 'nsp' => '/',
+ ]);
+ $socket->join('room');
+ $adapter = ApplicationContext::getContainer()->get(AdapterInterface::class);
+ $this->assertEquals(['room'], $adapter->clientRooms($socket->getSid()));
+ }
+
+ public function testLeave()
+ {
+ /** @var Socket $socket */
+ $socket = make(Socket::class, [
+ 'fd' => 1,
+ 'nsp' => '/',
+ ]);
+ $socket->join('room', 'another_room');
+ $socket->leave('room');
+ $adapter = ApplicationContext::getContainer()->get(AdapterInterface::class);
+ $this->assertEquals(['another_room'], $adapter->clientRooms($socket->getSid()));
+ }
+
+ public function testLeaveAll()
+ {
+ /** @var Socket $socket */
+ $socket = make(Socket::class, [
+ 'fd' => 1,
+ 'nsp' => '/',
+ ]);
+ $socket->join('room', 'room2', 'room3');
+ $socket->leaveAll();
+ $adapter = ApplicationContext::getContainer()->get(AdapterInterface::class);
+ $this->assertEquals([], $adapter->clientRooms($socket->getSid()));
+ }
+
+ public function testTo()
+ {
+ /** @var ContainerInterface $container */
+ $container = ApplicationContext::getContainer();
+ $mock = Mockery::mock(Sender::class);
+ $mock->shouldNotReceive('push')->with(1, Mockery::any(), Mockery::any(), Mockery::any());
+ $mock->shouldReceive('push')->with(2, Mockery::any(), Mockery::any(), Mockery::any())->once();
+ $mock->shouldReceive('push')->with(3, Mockery::any(), Mockery::any(), Mockery::any())->once();
+ $container->set(Sender::class, $mock);
+ /** @var Socket $socket1 */
+ $socket1 = make(Socket::class, [
+ 'fd' => 1,
+ 'nsp' => '/',
+ ]);
+ /** @var Socket $socket2 */
+ $socket2 = make(Socket::class, [
+ 'fd' => 2,
+ 'nsp' => '/',
+ ]);
+ /** @var Socket $socket3 */
+ $socket3 = make(Socket::class, [
+ 'fd' => 3,
+ 'nsp' => '/',
+ ]);
+ $socket1->join('room');
+ $socket2->join('room');
+ $socket3->join('room');
+ $socket1->to('room')->emit('hello');
+ $this->assertTrue(true);
+ }
+
+ public function testBroadcast()
+ {
+ $socket1 = make(Socket::class, [
+ 'fd' => 1,
+ 'nsp' => '/',
+ ]);
+ $reflection = new \ReflectionClass(Socket::class);
+ $prop = $reflection->getProperty('broadcast');
+ $prop->setAccessible(true);
+ $this->assertFalse($prop->getValue($socket1));
+ $this->assertTrue($prop->getValue($socket1->broadcast));
+ }
+}
diff --git a/src/super-globals/composer.json b/src/super-globals/composer.json
index 72e802ce1..500c3f06d 100644
--- a/src/super-globals/composer.json
+++ b/src/super-globals/composer.json
@@ -17,8 +17,8 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/swagger/composer.json b/src/swagger/composer.json
index 91787fbba..4498ff746 100644
--- a/src/swagger/composer.json
+++ b/src/swagger/composer.json
@@ -17,7 +17,7 @@
},
"require": {
"php": ">=7.2",
- "hyperf/command": "~1.1.0",
+ "hyperf/command": "~2.0.0",
"zircote/swagger-php": "^3.0"
},
"require-dev": {
diff --git a/src/swoole-enterprise/composer.json b/src/swoole-enterprise/composer.json
deleted file mode 100644
index b41c0a2ce..000000000
--- a/src/swoole-enterprise/composer.json
+++ /dev/null
@@ -1,60 +0,0 @@
-{
- "name": "hyperf/swoole-enterprise",
- "description": "A swoole dashboard library for Hyperf.",
- "license": "MIT",
- "keywords": [
- "php",
- "swoole",
- "hyperf",
- "swoole-enterprise"
- ],
- "homepage": "https://hyperf.io",
- "support": {
- "docs": "https://hyperf.wiki",
- "issues": "https://github.com/hyperf/hyperf/issues",
- "pull-request": "https://github.com/hyperf/hyperf/pulls",
- "source": "https://github.com/hyperf/hyperf"
- },
- "require": {
- "php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0",
- "psr/container": "^1.0",
- "psr/http-server-middleware": "^1.0",
- "psr/log": "^1.0"
- },
- "require-dev": {
- "friendsofphp/php-cs-fixer": "^2.9",
- "malukenho/docheader": "^0.1.6",
- "mockery/mockery": "^1.0",
- "phpunit/phpunit": "^7.0"
- },
- "suggest": {
- },
- "autoload": {
- "psr-4": {
- "Hyperf\\SwooleEnterprise\\": "src/"
- }
- },
- "autoload-dev": {
- "psr-4": {
- }
- },
- "config": {
- "sort-packages": true
- },
- "extra": {
- "branch-alias": {
- "dev-master": "1.1-dev"
- },
- "hyperf": {
- "config": "Hyperf\\SwooleEnterprise\\ConfigProvider"
- }
- },
- "bin": [
- ],
- "scripts": {
- "cs-fix": "php-cs-fixer fix $1",
- "test": "phpunit --colors=always"
- }
-}
diff --git a/src/swoole-enterprise/src/Middleware/HttpServerMiddleware.php b/src/swoole-enterprise/src/Middleware/HttpServerMiddleware.php
deleted file mode 100644
index 5c61b2476..000000000
--- a/src/swoole-enterprise/src/Middleware/HttpServerMiddleware.php
+++ /dev/null
@@ -1,53 +0,0 @@
-name = $config->get('app_name', 'hyperf-skeleton');
- }
-
- public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
- {
- if (class_exists(StatsCenter::class)) {
- $path = $request->getUri()->getPath();
- $ip = current(swoole_get_local_ip());
-
- $tick = StatsCenter::beforeExecRpc($path, $this->name, $ip);
- try {
- $response = $handler->handle($request);
- StatsCenter::afterExecRpc($tick, true, $response->getStatusCode());
- } catch (\Throwable $exception) {
- StatsCenter::afterExecRpc($tick, false, $exception->getCode());
- throw $exception;
- }
- } else {
- $response = $handler->handle($request);
- }
-
- return $response;
- }
-}
diff --git a/src/swoole-tracker/composer.json b/src/swoole-tracker/composer.json
index d0d28a525..97b08239b 100644
--- a/src/swoole-tracker/composer.json
+++ b/src/swoole-tracker/composer.json
@@ -17,8 +17,8 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"psr/container": "^1.0",
"psr/http-server-middleware": "^1.0",
"psr/log": "^1.0"
diff --git a/src/task/composer.json b/src/task/composer.json
index b2f7baf37..f6c5cba41 100644
--- a/src/task/composer.json
+++ b/src/task/composer.json
@@ -17,14 +17,14 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/framework": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/framework": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
"psr/log": "^1.0",
- "symfony/property-access": "^4.3",
- "symfony/serializer": "^4.3"
+ "symfony/property-access": "^5.0",
+ "symfony/serializer": "^5.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.9",
diff --git a/src/task/src/TaskExecutor.php b/src/task/src/TaskExecutor.php
index 10f12b100..f663b8cc1 100644
--- a/src/task/src/TaskExecutor.php
+++ b/src/task/src/TaskExecutor.php
@@ -55,7 +55,7 @@ class TaskExecutor
public function execute(Task $task, float $timeout = 10)
{
if (! $this->server instanceof Server) {
- throw new TaskExecuteException('The server not support task.');
+ throw new TaskExecuteException('The server does not support task.');
}
$taskId = $this->server->task($task);
diff --git a/src/testing/composer.json b/src/testing/composer.json
index d7801c4c4..d6281daf3 100644
--- a/src/testing/composer.json
+++ b/src/testing/composer.json
@@ -22,10 +22,10 @@
"php": ">=7.2",
"psr/container": "^1.0",
"phpunit/phpunit": "^7.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/http-message": "~1.1.0",
- "hyperf/http-server": "~1.1.0",
- "hyperf/utils": "~1.1.0"
+ "hyperf/contract": "~2.0.0",
+ "hyperf/http-message": "~2.0.0",
+ "hyperf/http-server": "~2.0.0",
+ "hyperf/utils": "~2.0.0"
},
"require-dev": {
"malukenho/docheader": "^0.1.6",
diff --git a/src/tracer/composer.json b/src/tracer/composer.json
index 91b4af8d1..33cad334e 100644
--- a/src/tracer/composer.json
+++ b/src/tracer/composer.json
@@ -18,15 +18,15 @@
"require": {
"php": ">=7.2",
"psr/http-message": "^1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/di": "~1.1.0",
- "hyperf/guzzle": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/di": "~2.0.0",
+ "hyperf/guzzle": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"jcchavezs/zipkin-opentracing":"^0.1.3",
"opentracing/opentracing":"1.0.*"
},
"require-dev": {
- "hyperf/event": "~1.1.0",
+ "hyperf/event": "~2.0.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
diff --git a/src/translation/composer.json b/src/translation/composer.json
index 7e8547624..861624462 100644
--- a/src/translation/composer.json
+++ b/src/translation/composer.json
@@ -22,8 +22,8 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"psr/container": "^1.0"
},
"require-dev": {
diff --git a/src/utils/composer.json b/src/utils/composer.json
index 35baacab1..74f63f570 100644
--- a/src/utils/composer.json
+++ b/src/utils/composer.json
@@ -19,21 +19,21 @@
"php": ">=7.2",
"ext-swoole": ">=4.4",
"doctrine/inflector": "^1.3",
- "hyperf/contract": "~1.1.0"
+ "hyperf/contract": "~2.0.0"
},
"require-dev": {
- "symfony/var-dumper": "^4.1",
- "symfony/property-access": "^4.3",
- "symfony/serializer": "^4.3",
+ "symfony/var-dumper": "^5.0",
+ "symfony/property-access": "^5.0",
+ "symfony/serializer": "^5.0",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0.0",
"friendsofphp/php-cs-fixer": "^2.9"
},
"suggest": {
- "symfony/var-dumper": "Required to use the dd function (^4.1).",
- "symfony/serializer": "Required to use SymfonyNormalizer (^4.3)",
- "symfony/property-access": "Required to use SymfonyNormalizer (^4.3)",
+ "symfony/var-dumper": "Required to use the dd function (^5.0).",
+ "symfony/serializer": "Required to use SymfonyNormalizer (^5.0)",
+ "symfony/property-access": "Required to use SymfonyNormalizer (^5.0)",
"hyperf/di": "Required to use ExceptionNormalizer"
},
"autoload": {
diff --git a/src/utils/src/ApplicationContext.php b/src/utils/src/ApplicationContext.php
index 6a7ef4ff4..e60317bc3 100644
--- a/src/utils/src/ApplicationContext.php
+++ b/src/utils/src/ApplicationContext.php
@@ -16,10 +16,13 @@ use Psr\Container\ContainerInterface;
class ApplicationContext
{
/**
- * @var ContainerInterface
+ * @var null|ContainerInterface
*/
private static $container;
+ /**
+ * @throws \TypeError
+ */
public static function getContainer(): ContainerInterface
{
return self::$container;
diff --git a/src/utils/src/Composer.php b/src/utils/src/Composer.php
index 7f863af96..33985735f 100644
--- a/src/utils/src/Composer.php
+++ b/src/utils/src/Composer.php
@@ -40,6 +40,11 @@ class Composer
*/
private static $versions = [];
+ /**
+ * @var ClassLoaders
+ */
+ private static $classLoader;
+
/**
* @throws \RuntimeException When composer.lock does not exist.
*/
@@ -125,7 +130,16 @@ class Composer
public static function getLoader(): ClassLoader
{
- return self::findLoader();
+ if (! self::$classLoader) {
+ self::$classLoader = self::findLoader();
+ }
+ return self::$classLoader;
+ }
+
+ public static function setLoader(ClassLoader $classLoader): ClassLoader
+ {
+ self::$classLoader = $classLoader;
+ return $classLoader;
}
private static function findLoader(): ClassLoader
diff --git a/src/utils/src/ConfigProvider.php b/src/utils/src/ConfigProvider.php
index 31f36cd65..b79be1ee6 100644
--- a/src/utils/src/ConfigProvider.php
+++ b/src/utils/src/ConfigProvider.php
@@ -33,6 +33,13 @@ class ConfigProvider
NormalizerInterface::class => SimpleNormalizer::class,
];
}),
+ 'annotations' => [
+ 'scan' => [
+ 'paths' => [
+ __DIR__,
+ ],
+ ],
+ ],
];
}
}
diff --git a/src/utils/src/Context.php b/src/utils/src/Context.php
index 6bcba9475..0c10f689b 100644
--- a/src/utils/src/Context.php
+++ b/src/utils/src/Context.php
@@ -70,7 +70,7 @@ class Context
*/
$from = SwCoroutine::getContext($fromCoroutineId);
$current = SwCoroutine::getContext();
- $current->exchangeArray($keys ? array_fill_keys($keys, $from->getArrayCopy()) : $from->getArrayCopy());
+ $current->exchangeArray($keys ? Arr::only($from->getArrayCopy(), $keys) : $from->getArrayCopy());
}
/**
diff --git a/src/utils/src/Functions.php b/src/utils/src/Functions.php
index 53ebfa466..ac6c44a22 100644
--- a/src/utils/src/Functions.php
+++ b/src/utils/src/Functions.php
@@ -445,3 +445,14 @@ if (! function_exists('swoole_hook_flags')) {
return defined('SWOOLE_HOOK_FLAGS') ? SWOOLE_HOOK_FLAGS : SWOOLE_HOOK_ALL;
}
}
+
+if (! function_exists('timepoint')) {
+ function timepoint(?string $key = null) {
+ if (! isset($GLOBALS['__timepoint_beginTime__'])) {
+ $GLOBALS['__timepoint_beginTime__'] = microtime(true);
+ return;
+ }
+ echo '[DEBUG] Timepoint ' . $key . ': ' . round(microtime(true) - $GLOBALS['__timepoint_beginTime__'], 3) . 's' . PHP_EOL;
+ $GLOBALS['__timepoint_beginTime__'] = microtime(true);
+ }
+}
diff --git a/src/utils/src/Traits/Container.php b/src/utils/src/Traits/Container.php
index 1f04d11b2..60aa33682 100644
--- a/src/utils/src/Traits/Container.php
+++ b/src/utils/src/Traits/Container.php
@@ -19,7 +19,8 @@ trait Container
protected static $container = [];
/**
- * {@inheritdoc}
+ * Add a value to container by identifier.
+ * @param mixed $value
*/
public static function set(string $id, $value)
{
@@ -27,7 +28,9 @@ trait Container
}
/**
- * {@inheritdoc}
+ * Finds an entry of the container by its identifier and returns it,
+ * Retunrs $default when does not exists in the container.
+ * @param null|mixed $default
*/
public static function get(string $id, $default = null)
{
@@ -35,18 +38,27 @@ trait Container
}
/**
- * {@inheritdoc}
+ * Returns true if the container can return an entry for the given identifier.
+ * Returns false otherwise.
*/
- public static function has(string $id)
+ public static function has(string $id): bool
{
return isset(static::$container[$id]);
}
/**
- * {@inheritdoc}
+ * Returns the container.
*/
public static function list(): array
{
return static::$container;
}
+
+ /**
+ * Clear the container.
+ */
+ public static function clear(): void
+ {
+ static::$container = [];
+ }
}
diff --git a/src/utils/tests/ContextTest.php b/src/utils/tests/ContextTest.php
index 150cf77bd..cb6a40c53 100644
--- a/src/utils/tests/ContextTest.php
+++ b/src/utils/tests/ContextTest.php
@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace HyperfTest\Utils;
use Hyperf\Utils\Context;
+use Hyperf\Utils\Coroutine;
use PHPUnit\Framework\TestCase;
/**
@@ -44,4 +45,46 @@ class ContextTest extends TestCase
Context::set('test.store.id', null);
$this->assertSame(1, Context::getOrSet('test.store.id', 1));
}
+
+ public function testCopy()
+ {
+ Context::set('test.store.id', $uid = uniqid());
+ $id = Coroutine::id();
+ parallel([function () use ($id, $uid) {
+ Context::copy($id, ['test.store.id']);
+ $this->assertSame($uid, Context::get('test.store.id'));
+ }]);
+ }
+
+ public function testCopyAfterSet()
+ {
+ Context::set('test.store.id', $uid = uniqid());
+ $id = Coroutine::id();
+ parallel([function () use ($id, $uid) {
+ Context::set('test.store.name', 'Hyperf');
+ Context::copy($id, ['test.store.id']);
+ $this->assertSame($uid, Context::get('test.store.id'));
+
+ // TODO: Context::copy will delete origin values.
+ $this->assertNull(Context::get('test.store.name'));
+ }]);
+ }
+
+ public function testContextChangeAfterCopy()
+ {
+ $obj = new \stdClass();
+ $obj->id = $uid = uniqid();
+
+ Context::set('test.store.id', $obj);
+ $id = Coroutine::id();
+ $tid = uniqid();
+ parallel([function () use ($id, $uid, $tid) {
+ Context::copy($id, ['test.store.id']);
+ $obj = Context::get('test.store.id');
+ $this->assertSame($uid, $obj->id);
+ $obj->id = $tid;
+ }]);
+
+ $this->assertSame($tid, Context::get('test.store.id')->id);
+ }
}
diff --git a/src/validation/composer.json b/src/validation/composer.json
index 95f76c565..2663aad8f 100755
--- a/src/validation/composer.json
+++ b/src/validation/composer.json
@@ -21,15 +21,15 @@
"php": ">=7.2",
"ext-swoole": ">=4.3",
"egulias/email-validator": "^2.1",
- "hyperf/command": "~1.1.0",
- "hyperf/contract": "~1.1.0",
- "hyperf/database": "~1.1.0",
- "hyperf/devtool": "~1.1.0",
- "hyperf/di": "~1.1.0",
- "hyperf/framework": "~1.1.0",
- "hyperf/http-server": "~1.1.0",
- "hyperf/utils": "~1.1.0",
- "hyperf/translation": "~1.1.0",
+ "hyperf/command": "~2.0.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/database": "~2.0.0",
+ "hyperf/devtool": "~2.0.0",
+ "hyperf/di": "~2.0.0",
+ "hyperf/framework": "~2.0.0",
+ "hyperf/http-server": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
+ "hyperf/translation": "~2.0.0",
"nesbot/carbon": "^2.21",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
@@ -37,8 +37,8 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
- "hyperf/db-connection": "^1.0",
- "hyperf/testing": "1.0.*",
+ "hyperf/db-connection": "~2.0.0",
+ "hyperf/testing": "~2.0.0",
"mockery/mockery": "^1.2"
},
"config": {
diff --git a/src/validation/src/Concerns/ValidatesAttributes.php b/src/validation/src/Concerns/ValidatesAttributes.php
index 839a1f5e0..2483ea87b 100755
--- a/src/validation/src/Concerns/ValidatesAttributes.php
+++ b/src/validation/src/Concerns/ValidatesAttributes.php
@@ -1215,8 +1215,11 @@ trait ValidatesAttributes
*
* @return null|\DateTime
*/
- protected function getDateTimeWithOptionalFormat(string $format, string $value)
+ protected function getDateTimeWithOptionalFormat(string $format, ?string $value)
{
+ if (is_null($value)) {
+ return null;
+ }
if ($date = DateTime::createFromFormat('!' . $format, $value)) {
return $date;
}
diff --git a/src/validation/tests/Cases/ValidationValidatorTest.php b/src/validation/tests/Cases/ValidationValidatorTest.php
index 23de2d10c..d2ab1208b 100755
--- a/src/validation/tests/Cases/ValidationValidatorTest.php
+++ b/src/validation/tests/Cases/ValidationValidatorTest.php
@@ -4540,6 +4540,59 @@ class ValidationValidatorTest extends TestCase
];
}
+ public function testValidateAfter()
+ {
+ $trans = $this->getIlluminateArrayTranslator();
+
+ $v = new Validator(
+ $trans,
+ [
+ 'end_time' => '2020-04-09 19:09:05',
+ ],
+ [
+ 'start_time' => 'date_format:Y-m-d H:i:s|after:2020-04-09 16:09:05',
+ 'end_time' => 'date_format:Y-m-d H:i:s|after:start_time',
+ ]
+ );
+ $this->assertFalse($v->passes());
+
+ $v = new Validator(
+ $trans,
+ [
+ 'start_time' => '2020-04-09 17:09:05',
+ 'end_time' => '2020-04-09 19:09:05',
+ ],
+ [
+ 'start_time' => 'date_format:Y-m-d H:i:s|after:2020-04-09 18:09:05',
+ 'end_time' => 'date_format:Y-m-d H:i:s|after:start_time',
+ ]
+ );
+ $this->assertFalse($v->passes());
+
+ $v = new Validator(
+ $trans,
+ [],
+ [
+ 'start_time' => 'date_format:Y-m-d H:i:s|after:2020-04-09 16:09:05',
+ 'end_time' => 'date_format:Y-m-d H:i:s|after:start_time',
+ ]
+ );
+ $this->assertTrue($v->passes());
+
+ $v = new Validator(
+ $trans,
+ [
+ 'start_time' => '2020-04-09 17:09:05',
+ 'end_time' => '2020-04-09 19:09:05',
+ ],
+ [
+ 'start_time' => 'date_format:Y-m-d H:i:s|after:2020-04-09 16:09:05',
+ 'end_time' => 'date_format:Y-m-d H:i:s|after:start_time',
+ ]
+ );
+ $this->assertTrue($v->passes());
+ }
+
public function getIlluminateArrayTranslator()
{
return new Translator(
diff --git a/src/view/composer.json b/src/view/composer.json
index 987c2e537..9dd8504d0 100644
--- a/src/view/composer.json
+++ b/src/view/composer.json
@@ -17,8 +17,8 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"psr/container": "^1.0",
"psr/log": "^1.0"
},
diff --git a/src/websocket-client/composer.json b/src/websocket-client/composer.json
index bfebad156..f953ce0ea 100644
--- a/src/websocket-client/composer.json
+++ b/src/websocket-client/composer.json
@@ -17,9 +17,9 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/http-message": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/http-message": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"psr/container": "^1.0",
"psr/log": "^1.0"
},
diff --git a/src/websocket-server/composer.json b/src/websocket-server/composer.json
index d60e632cc..e498cebfb 100644
--- a/src/websocket-server/composer.json
+++ b/src/websocket-server/composer.json
@@ -17,10 +17,10 @@
},
"require": {
"php": ">=7.2",
- "hyperf/contract": "~1.1.0",
- "hyperf/exception-handler": "~1.1.0",
- "hyperf/http-server": "~1.1.0",
- "hyperf/utils": "~1.1.0",
+ "hyperf/contract": "~2.0.0",
+ "hyperf/exception-handler": "~2.0.0",
+ "hyperf/http-server": "~2.0.0",
+ "hyperf/utils": "~2.0.0",
"psr/container": "^1.0",
"psr/event-dispatcher": "^1.0",
"psr/log": "^1.0"
diff --git a/src/websocket-server/src/Context.php b/src/websocket-server/src/Context.php
index cf0bacef3..d2b16c833 100644
--- a/src/websocket-server/src/Context.php
+++ b/src/websocket-server/src/Context.php
@@ -11,6 +11,7 @@ declare(strict_types=1);
*/
namespace Hyperf\WebSocketServer;
+use Hyperf\Utils\Arr;
use Hyperf\Utils\Context as CoContext;
class Context
@@ -60,7 +61,7 @@ class Context
{
$fd = CoContext::get(Context::FD, 0);
$from = self::$container[$fromFd];
- self::$container[$fd] = ($keys ? array_fill_keys($keys, $from) : $from);
+ self::$container[$fd] = ($keys ? Arr::only($from, $keys) : $from);
}
public static function override(string $id, \Closure $closure)
diff --git a/src/websocket-server/tests/ContextTest.php b/src/websocket-server/tests/ContextTest.php
index 71a4bb927..2736381cd 100644
--- a/src/websocket-server/tests/ContextTest.php
+++ b/src/websocket-server/tests/ContextTest.php
@@ -48,11 +48,15 @@ class ContextTest extends \PHPUnit\Framework\TestCase
{
CoContext::set(Context::FD, 2);
Context::set('a', 42);
- go(function () {
+ parallel([function () {
CoContext::set(Context::FD, 3);
Context::copy(2);
$this->assertEquals(42, Context::get('a'));
- });
+ }, function () {
+ CoContext::set(Context::FD, 3);
+ Context::copy(2, ['a']);
+ $this->assertEquals(42, Context::get('a'));
+ }]);
$this->assertEquals(42, Context::get('a', 0, 3));
}