diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c65528f0..22b427af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # v1.1.27 - TBD -- [#1589](https://github.com/hyperf/hyperf/pull/1589) Fixed file locks not safe in coroutines. +## Added + +- [#1575](https://github.com/hyperf/hyperf/pull/1575) Added document of property with relation, scope and attributes. +- [#1586](https://github.com/hyperf/hyperf/pull/1586) Added conflict of symfony/event-dispatcher which < 4.3. + +## Fixed + +- [#1553](https://github.com/hyperf/hyperf/pull/1553) Fixed the rpc client do not work, when jsonrpc server register the same service to consul with jsonrpc and jsonrpc-http protocol. +- [#1589](https://github.com/hyperf/hyperf/pull/1589) Fixed unsafe file locks in coroutines. +- [#1607](https://github.com/hyperf/hyperf/pull/1607) Fixed bug that the return value of function `go` is not adaptive with `swoole`. # v1.1.26 - 2020-04-16 diff --git a/README-CN.md b/README-CN.md index 6ac27e1d9..c99d8af3b 100644 --- a/README-CN.md +++ b/README-CN.md @@ -82,6 +82,21 @@ 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/ + 8 threads and 1024 connections + Thread Stats Avg Stdev Max +/- Stdev + 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 +Requests/sec: 103921.49 +Transfer/sec: 18.83MB +``` + # 开源协议 Hyperf 是一个基于 [MIT 协议](https://github.com/hyperf/hyperf/blob/master/LICENSE) 开源的软件。 diff --git a/README.md b/README.md index ea538c826..1f7148b46 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,21 @@ 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 + 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 +Requests/sec: 103921.49 +Transfer/sec: 18.83MB +``` + # License The Hyperf framework is open-source software licensed under the MIT license. diff --git a/composer.json b/composer.json index 440d34667..6ca77c896 100644 --- a/composer.json +++ b/composer.json @@ -73,6 +73,7 @@ "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", @@ -135,6 +136,9 @@ "hyperf/websocket-server": "self.version" }, "suggest": {}, + "conflict": { + "symfony/event-dispatcher": "<4.3" + }, "autoload": { "files": [ "src/config/src/Functions.php", diff --git a/doc/zh-cn/amqp.md b/doc/zh-cn/amqp.md index 031f00a71..eef64a90a 100644 --- a/doc/zh-cn/amqp.md +++ b/doc/zh-cn/amqp.md @@ -47,7 +47,7 @@ return [ 'login_response' => null, 'locale' => 'en_US', 'connection_timeout' => 3.0, - 'read_write_timeout' => 3.0, + 'read_write_timeout' => 6.0, 'context' => null, 'keepalive' => false, 'heartbeat' => 3, diff --git a/doc/zh-cn/filesystem.md b/doc/zh-cn/filesystem.md index 9bae2b381..296484eaa 100644 --- a/doc/zh-cn/filesystem.md +++ b/doc/zh-cn/filesystem.md @@ -39,14 +39,23 @@ declare(strict_types=1); namespace App\Controller; -class IndexController +class IndexController extends AbstractController { public function example(\League\Flysystem\Filesystem $filesystem) { + // Process Upload + $file = $this->request->file('upload'); + $stream = fopen($file->getRealPath(), 'r+'); + $filesystem->writeStream( + 'uploads/'.$file->getClientFilename(), + $stream + ); + fclose($stream); + // Write Files $filesystem->write('path/to/file.txt', 'contents'); - // Write Use writeStream + // Add local file $stream = fopen('local/path/to/file.txt', 'r+'); $result = $filesystem->writeStream('path/to/file.txt', $stream); if (is_resource($stream)) { @@ -122,7 +131,8 @@ 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. 以阿里云 OSS 为例,1 核 1 进程读操作性能对比: +3. 使用Local驱动时,根目录是配置好的地址,而不是操作系统的根目录。例如,Local驱动 `root` 设置为 `/var/www`, 则本地磁盘上的 `/var/www/public/file.txt` 通过 flysystem API 访问时应使用 `/public/file.txt` 或 `public/file.txt` 。 +4. 以阿里云 OSS 为例,1 核 1 进程读操作性能对比: ```bash ab -k -c 10 -n 1000 http://127.0.0.1:9501/ diff --git a/doc/zh-cn/quick-start/questions.md b/doc/zh-cn/quick-start/questions.md index 6a9547812..349975573 100644 --- a/doc/zh-cn/quick-start/questions.md +++ b/doc/zh-cn/quick-start/questions.md @@ -62,30 +62,6 @@ vendor/bin/init-proxy.sh && composer test vendor/bin/init-proxy.sh && php bin/hyperf.php start ``` -## PHP7.3 下预先生成代理的脚本 执行失败 - -`php bin/hyperf.php di:init-proxy` 脚本在 `PHP7.3` 的 `Docker` 打包时,会因为返回码是 `1` 而失败。 - -> 具体原因还在定位中 - -以下通过重写 `init-proxy.sh` 脚本绕过这个问题。 - -```bash -#!/usr/bin/env bash - -php /opt/www/bin/hyperf.php di:init-proxy - -echo Started. -``` - -对应的 `Dockerfile` 修改以下代码,省略无用的代码展示。 - -```dockerfile -RUN composer install --no-dev \ - && composer dump-autoload -o \ - && ./init-proxy.sh -``` - ## 异步队列消息丢失 如果在使用 `async-queue` 组件时,发现 `handle` 中的方法没有执行,请先检查以下几种情况: @@ -97,3 +73,27 @@ RUN composer install --no-dev \ 1. killall php 2. 修改 `async-queue` 配置 `channel` + +## 1.1.24 - 1.1.26 版本 SymfonyEventDispatcher 报错 + +因为 `symfony/console` 默认使用的 `^4.2` 版本,而 `symfony/event-dispatcher` 的 `^4.3` 版本与 `<4.3` 版本不兼容。 + +`hyperf/framework` 默认推荐使用 `^4.3` 版本的 `symfony/event-dispatcher`,就有一定概率导致实现上的冲突。 + +如果有类似的情况出现,可以尝试以下操作 + +``` +rm -rf vendor +rm -rf composer.lock +composer require "symfony/event-dispatcher:^4.3" +``` + +1.1.27 版本中,会在 `composer.json` 中添加以下配置,来处理这个问题。 + +``` + "conflict": { + "symfony/event-dispatcher": "<4.3" + }, +``` + + diff --git a/doc/zh-cn/tutorial/docker-swarm.md b/doc/zh-cn/tutorial/docker-swarm.md index a806cdc6c..055a20731 100644 --- a/doc/zh-cn/tutorial/docker-swarm.md +++ b/doc/zh-cn/tutorial/docker-swarm.md @@ -6,6 +6,7 @@ ``` curl -sSL https://get.daocloud.io/docker | sh +# curl -sSL https://get.docker.com/ | sh ``` 修改文件 `/lib/systemd/system/docker.service`,允许使用 `TCP` 连接 `Docker` diff --git a/src/amqp/publish/amqp.php b/src/amqp/publish/amqp.php index dd5bc8c06..21651e29b 100644 --- a/src/amqp/publish/amqp.php +++ b/src/amqp/publish/amqp.php @@ -12,7 +12,7 @@ declare(strict_types=1); return [ 'default' => [ 'host' => env('AMQP_HOST', 'localhost'), - 'port' => env('AMQP_PORT', 5672), + 'port' => (int) env('AMQP_PORT', 5672), 'user' => env('AMQP_USER', 'guest'), 'password' => env('AMQP_PASSWORD', 'guest'), 'vhost' => env('AMQP_VHOST', '/'), diff --git a/src/database/composer.json b/src/database/composer.json index d25704a7a..55b3e8055 100644 --- a/src/database/composer.json +++ b/src/database/composer.json @@ -30,7 +30,7 @@ }, "suggest": { "doctrine/dbal": "Required to rename columns (^2.6).", - "nikic/php-parser": "Required to use ModelCommand (^4.1)." + "roave/better-reflection": "Required to use ModelCommand (^4.0)." }, "autoload": { "psr-4": { diff --git a/src/database/src/Commands/Ast/ModelUpdateVisitor.php b/src/database/src/Commands/Ast/ModelUpdateVisitor.php index 94b4d5f12..c115e4c35 100644 --- a/src/database/src/Commands/Ast/ModelUpdateVisitor.php +++ b/src/database/src/Commands/Ast/ModelUpdateVisitor.php @@ -12,13 +12,51 @@ declare(strict_types=1); namespace Hyperf\Database\Commands\Ast; use Hyperf\Database\Commands\ModelOption; +use Hyperf\Database\Model\Builder; +use Hyperf\Database\Model\Collection; +use Hyperf\Database\Model\Model; +use Hyperf\Database\Model\Relations\BelongsTo; +use Hyperf\Database\Model\Relations\BelongsToMany; +use Hyperf\Database\Model\Relations\HasMany; +use Hyperf\Database\Model\Relations\HasManyThrough; +use Hyperf\Database\Model\Relations\HasOne; +use Hyperf\Database\Model\Relations\HasOneThrough; +use Hyperf\Database\Model\Relations\MorphMany; +use Hyperf\Database\Model\Relations\MorphOne; +use Hyperf\Database\Model\Relations\MorphTo; +use Hyperf\Database\Model\Relations\MorphToMany; +use Hyperf\Database\Model\Relations\Relation; use Hyperf\Utils\Str; use PhpParser\Comment\Doc; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use Roave\BetterReflection\BetterReflection; +use Roave\BetterReflection\Reflection\ReflectionClass; +use Roave\BetterReflection\Reflection\ReflectionMethod; +use Roave\BetterReflection\Reflector\ClassReflector; +use Roave\BetterReflection\TypesFinder\FindReturnType; class ModelUpdateVisitor extends NodeVisitorAbstract { + const RELATION_METHODS = [ + 'hasMany' => HasMany::class, + 'hasManyThrough' => HasManyThrough::class, + 'hasOneThrough' => HasOneThrough::class, + 'belongsToMany' => BelongsToMany::class, + 'hasOne' => HasOne::class, + 'belongsTo' => BelongsTo::class, + 'morphOne' => MorphOne::class, + 'morphTo' => MorphTo::class, + 'morphMany' => MorphMany::class, + 'morphToMany' => MorphToMany::class, + 'morphedByMany' => MorphToMany::class, + ]; + + /** + * @var string + */ + protected $class; + /** * @var array */ @@ -29,31 +67,48 @@ class ModelUpdateVisitor extends NodeVisitorAbstract */ protected $option; - public function __construct($columns = [], ModelOption $option) + /** + * @var array + */ + protected $methods = []; + + /** + * @var array + */ + protected $properties = []; + + /** + * @deprecated v2.0 + * @var ClassReflector + */ + protected static $reflector; + + /** + * @deprecated v2.0 + * @var FindReturnType + */ + protected static $return; + + public function __construct($class, $columns, ModelOption $option) { + $this->class = $class; $this->columns = $columns; $this->option = $option; + $this->initPropertiesFromMethods(); } public function leaveNode(Node $node) { switch ($node) { case $node instanceof Node\Stmt\PropertyProperty: - if ($node->name == 'fillable' && $this->option->isRefreshFillable()) { + if ((string) $node->name === 'fillable' && $this->option->isRefreshFillable()) { $node = $this->rewriteFillable($node); - } elseif ($node->name == 'casts') { + } elseif ((string) $node->name === 'casts') { $node = $this->rewriteCasts($node); } - return $node; case $node instanceof Node\Stmt\Class_: - $doc = '/**' . PHP_EOL; - foreach ($this->columns as $column) { - [$name, $type, $comment] = $this->getProperty($column); - $doc .= sprintf(' * @property %s $%s %s', $type, $name, $comment) . PHP_EOL; - } - $doc .= ' */'; - $node->setDocComment(new Doc($doc)); + $node->setDocComment(new Doc($this->parseProperty())); return $node; } } @@ -91,6 +146,147 @@ class ModelUpdateVisitor extends NodeVisitorAbstract return $node; } + protected function parseProperty(): string + { + $doc = '/**' . PHP_EOL; + foreach ($this->columns as $column) { + [$name, $type, $comment] = $this->getProperty($column); + $doc .= sprintf(' * @property %s $%s %s', $type, $name, $comment) . PHP_EOL; + } + foreach ($this->properties as $name => $property) { + if ($property['read'] && $property['write']) { + $doc .= sprintf(' * @property %s $%s', $property['type'], $name) . PHP_EOL; + continue; + } + if ($property['read']) { + $doc .= sprintf(' * @property-read %s $%s', $property['type'], $name) . PHP_EOL; + continue; + } + if ($property['write']) { + $doc .= sprintf(' * @property-write %s $%s', $property['type'], $name) . PHP_EOL; + continue; + } + } + $doc .= ' */'; + return $doc; + } + + protected function initPropertiesFromMethods() + { + /** @var ReflectionClass $reflection */ + $reflection = self::getReflector()->reflect($this->class); + $methods = $reflection->getImmediateMethods(); + $namespace = $reflection->getDeclaringNamespaceAst(); + if (empty($methods)) { + return; + } + + sort($methods); + /** @var ReflectionMethod $method */ + foreach ($methods as $method) { + if (Str::startsWith($method->getName(), 'get') && Str::endsWith($method->getName(), 'Attribute')) { + // Magic getAttribute + $name = Str::snake(substr($method->getName(), 3, -9)); + if (! empty($name)) { + $type = self::getReturnFinder()->__invoke($method, $namespace); + $this->setProperty($name, $type, true, null); + } + continue; + } + + if (Str::startsWith($method->getName(), 'set') && Str::endsWith($method->getName(), 'Attribute')) { + // Magic setAttribute + $name = Str::snake(substr($method->getName(), 3, -9)); + if (! empty($name)) { + $this->setProperty($name, null, null, true); + } + continue; + } + + if (Str::startsWith($method->getName(), 'scope') && $method->getName() !== 'scopeQuery') { + $name = Str::camel(substr($method->getName(), 5)); + if (! empty($name)) { + $args = $method->getParameters(); + // Remove the first ($query) argument + array_shift($args); + $this->setMethod($name, [Builder::class, $method->getDeclaringClass()->getName()], $args); + } + continue; + } + + if ($method->getNumberOfParameters() > 0) { + continue; + } + + $return = $method->getReturnStatementsAst(); + // Magic Relation + if (count($return) === 1 && $return[0] instanceof Node\Stmt\Return_) { + $expr = $return[0]->expr; + if ( + $expr instanceof Node\Expr\MethodCall + && $expr->name instanceof Node\Identifier + && is_string($expr->name->name) + && isset($expr->args[0]) + && $expr->args[0] instanceof Node\Arg + ) { + $name = $expr->name->name; + if (array_key_exists($name, self::RELATION_METHODS)) { + if ($expr->args[0]->value instanceof Node\Expr\ClassConstFetch) { + $related = $expr->args[0]->value->class->toCodeString(); + } else { + $related = (string) ($expr->args[0]->value); + } + + if (strpos($name, 'Many') !== false) { + // Collection or array of models (because Collection is Arrayable) + $this->setProperty($method->getName(), [$this->getCollectionClass($related), $related . '[]'], true); + } elseif ($name === 'morphTo') { + // Model isn't specified because relation is polymorphic + $this->setProperty($method->getName(), [Model::class], true); + } else { + // Single model is returned + $this->setProperty($method->getName(), [$related], true); + } + } + } + } + } + } + + protected function setProperty(string $name, array $type = null, bool $read = null, bool $write = null, string $comment = '', bool $nullable = false) + { + if (! isset($this->properties[$name])) { + $this->properties[$name] = []; + $this->properties[$name]['type'] = 'mixed'; + $this->properties[$name]['read'] = false; + $this->properties[$name]['write'] = false; + $this->properties[$name]['comment'] = (string) $comment; + } + if ($type !== null) { + if ($nullable) { + $type[] = 'null'; + } + $this->properties[$name]['type'] = implode('|', array_unique($type)); + } + if ($read !== null) { + $this->properties[$name]['read'] = $read; + } + if ($write !== null) { + $this->properties[$name]['write'] = $write; + } + } + + protected function setMethod(string $name, array $type = [], array $arguments = []) + { + $methods = array_change_key_case($this->methods, CASE_LOWER); + + if (! isset($methods[strtolower($name)])) { + $this->methods[$name] = []; + $this->methods[$name]['type'] = implode('|', $type); + $this->methods[$name]['arguments'] = $arguments; + } + } + protected function getProperty($column): array { $name = $this->option->isCamelCase() ? Str::camel($column['column_name']) : $column['column_name']; @@ -142,4 +338,35 @@ class ModelUpdateVisitor extends NodeVisitorAbstract return $cast; } + + protected function getCollectionClass($className): string + { + // Return something in the very very unlikely scenario the model doesn't + // have a newCollection() method. + if (! method_exists($className, 'newCollection')) { + return Collection::class; + } + + /** @var Model $model */ + $model = new $className(); + return '\\' . get_class($model->newCollection()); + } + + protected static function getReturnFinder(): FindReturnType + { + if (static::$return instanceof FindReturnType) { + return static::$return; + } + + return static::$return = new FindReturnType(); + } + + protected static function getReflector(): ClassReflector + { + if (self::$reflector instanceof ClassReflector) { + return self::$reflector; + } + + return self::$reflector = (new BetterReflection())->classReflector(); + } } diff --git a/src/database/src/Commands/ModelCommand.php b/src/database/src/Commands/ModelCommand.php index b055eee47..5bf05c3ac 100644 --- a/src/database/src/Commands/ModelCommand.php +++ b/src/database/src/Commands/ModelCommand.php @@ -183,6 +183,7 @@ class ModelCommand extends Command $stms = $this->astParser->parse(file_get_contents($path)); $traverser = new NodeTraverser(); $traverser->addVisitor(make(ModelUpdateVisitor::class, [ + 'class' => $class, 'columns' => $columns, 'option' => $option, ])); diff --git a/src/devtool/src/Describe/RoutesCommand.php b/src/devtool/src/Describe/RoutesCommand.php index 04d928d65..baf134ee7 100644 --- a/src/devtool/src/Describe/RoutesCommand.php +++ b/src/devtool/src/Describe/RoutesCommand.php @@ -101,15 +101,16 @@ class RoutesCommand extends HyperfCommand } else { $action = $handler->callback; } - if (isset($data[$uri])) { - $data[$uri]['method'][] = $method; + $unique = "{$serverName}|{$action}"; + if (isset($data[$unique])) { + $data[$unique]['method'][] = $method; } else { // method,uri,name,action,middleware $registedMiddlewares = MiddlewareManager::get($serverName, $uri, $method); $middlewares = $this->config->get('middlewares.' . $serverName, []); $middlewares = array_merge($middlewares, $registedMiddlewares); - $data[$uri] = [ + $data[$unique] = [ 'server' => $serverName, 'method' => [$method], 'uri' => $uri, diff --git a/src/framework/composer.json b/src/framework/composer.json index da83d061d..1d064d0a3 100644 --- a/src/framework/composer.json +++ b/src/framework/composer.json @@ -40,6 +40,9 @@ "hyperf/command": "Required to use Command annotation.", "symfony/event-dispatcher": "Required to use symfony event dispatcher (^4.3)." }, + "conflict": { + "symfony/event-dispatcher": "<4.3" + }, "autoload": { "psr-4": { "Hyperf\\Framework\\": "src/" diff --git a/src/rpc-client/src/AbstractServiceClient.php b/src/rpc-client/src/AbstractServiceClient.php index 0d5b412d5..76cc47368 100644 --- a/src/rpc-client/src/AbstractServiceClient.php +++ b/src/rpc-client/src/AbstractServiceClient.php @@ -230,14 +230,14 @@ abstract class AbstractServiceClient $services = $health->service($this->serviceName)->json(); $nodes = []; foreach ($services as $node) { - $passing = true; + $passing = false; $service = $node['Service'] ?? []; $checks = $node['Checks'] ?? []; foreach ($checks as $check) { $status = $check['Status'] ?? false; - if ($status !== 'passing') { - $passing = false; + if ($status === 'passing' && $this->protocol === $service['Meta']['Protocol']) { + $passing = true; } } diff --git a/src/utils/src/Functions.php b/src/utils/src/Functions.php index 721225930..5fe07928c 100644 --- a/src/utils/src/Functions.php +++ b/src/utils/src/Functions.php @@ -279,16 +279,24 @@ if (! function_exists('call')) { } if (! function_exists('go')) { + /** + * @return bool|int + */ function go(callable $callable) { - Coroutine::create($callable); + $id = Coroutine::create($callable); + return $id > 0 ? $id : false; } } if (! function_exists('co')) { + /** + * @return bool|int + */ function co(callable $callable) { - Coroutine::create($callable); + $id = Coroutine::create($callable); + return $id > 0 ? $id : false; } } diff --git a/src/utils/tests/FunctionTest.php b/src/utils/tests/FunctionTest.php index 97339a7b5..bd44ecb35 100644 --- a/src/utils/tests/FunctionTest.php +++ b/src/utils/tests/FunctionTest.php @@ -29,6 +29,17 @@ class FunctionTest extends TestCase $this->assertSame(2, $result); } + public function testReturnOfGo() + { + $uniqid = uniqid(); + $id = go(function () use (&$uniqid) { + $uniqid = 'Hyperf'; + }); + + $this->assertTrue(is_int($id)); + $this->assertSame('Hyperf', $uniqid); + } + public function testDataGet() { $data = ['id' => 1];