Merge branch 'master' into utils

This commit is contained in:
谷溪 2020-04-23 12:10:45 +08:00 committed by GitHub
commit cb096e8a04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 355 additions and 50 deletions

View File

@ -1,6 +1,15 @@
# v1.1.27 - TBD # 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 # v1.1.26 - 2020-04-16

View File

@ -82,6 +82,21 @@ Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面
</table> </table>
<!--gold end--> <!--gold end-->
# 性能
### 阿里云 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) 开源的软件。 Hyperf 是一个基于 [MIT 协议](https://github.com/hyperf/hyperf/blob/master/LICENSE) 开源的软件。

View File

@ -70,6 +70,21 @@ Support this project with your organization or company. Your logo will show up h
</table> </table>
<!--gold end--> <!--gold end-->
# 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 # License
The Hyperf framework is open-source software licensed under the MIT license. The Hyperf framework is open-source software licensed under the MIT license.

View File

@ -73,6 +73,7 @@
"swoole/ide-helper": "dev-master", "swoole/ide-helper": "dev-master",
"sy-records/think-template": "^2.0", "sy-records/think-template": "^2.0",
"symfony/console": "^4.2", "symfony/console": "^4.2",
"symfony/event-dispatcher": "^4.3",
"symfony/finder": "^4.1", "symfony/finder": "^4.1",
"symfony/property-access": "^4.3", "symfony/property-access": "^4.3",
"symfony/serializer": "^4.3", "symfony/serializer": "^4.3",
@ -135,6 +136,9 @@
"hyperf/websocket-server": "self.version" "hyperf/websocket-server": "self.version"
}, },
"suggest": {}, "suggest": {},
"conflict": {
"symfony/event-dispatcher": "<4.3"
},
"autoload": { "autoload": {
"files": [ "files": [
"src/config/src/Functions.php", "src/config/src/Functions.php",

View File

@ -47,7 +47,7 @@ return [
'login_response' => null, 'login_response' => null,
'locale' => 'en_US', 'locale' => 'en_US',
'connection_timeout' => 3.0, 'connection_timeout' => 3.0,
'read_write_timeout' => 3.0, 'read_write_timeout' => 6.0,
'context' => null, 'context' => null,
'keepalive' => false, 'keepalive' => false,
'heartbeat' => 3, 'heartbeat' => 3,

View File

@ -39,14 +39,23 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
class IndexController class IndexController extends AbstractController
{ {
public function example(\League\Flysystem\Filesystem $filesystem) 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 // Write Files
$filesystem->write('path/to/file.txt', 'contents'); $filesystem->write('path/to/file.txt', 'contents');
// Write Use writeStream // Add local file
$stream = fopen('local/path/to/file.txt', 'r+'); $stream = fopen('local/path/to/file.txt', 'r+');
$result = $filesystem->writeStream('path/to/file.txt', $stream); $result = $filesystem->writeStream('path/to/file.txt', $stream);
if (is_resource($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 以上版本。 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 适配器。 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 ```bash
ab -k -c 10 -n 1000 http://127.0.0.1:9501/ ab -k -c 10 -n 1000 http://127.0.0.1:9501/

View File

@ -62,30 +62,6 @@ vendor/bin/init-proxy.sh && composer test
vendor/bin/init-proxy.sh && php bin/hyperf.php start 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` 中的方法没有执行,请先检查以下几种情况: 如果在使用 `async-queue` 组件时,发现 `handle` 中的方法没有执行,请先检查以下几种情况:
@ -97,3 +73,27 @@ RUN composer install --no-dev \
1. killall php 1. killall php
2. 修改 `async-queue` 配置 `channel` 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"
},
```

View File

@ -6,6 +6,7 @@
``` ```
curl -sSL https://get.daocloud.io/docker | sh curl -sSL https://get.daocloud.io/docker | sh
# curl -sSL https://get.docker.com/ | sh
``` ```
修改文件 `/lib/systemd/system/docker.service`,允许使用 `TCP` 连接 `Docker` 修改文件 `/lib/systemd/system/docker.service`,允许使用 `TCP` 连接 `Docker`

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
return [ return [
'default' => [ 'default' => [
'host' => env('AMQP_HOST', 'localhost'), 'host' => env('AMQP_HOST', 'localhost'),
'port' => env('AMQP_PORT', 5672), 'port' => (int) env('AMQP_PORT', 5672),
'user' => env('AMQP_USER', 'guest'), 'user' => env('AMQP_USER', 'guest'),
'password' => env('AMQP_PASSWORD', 'guest'), 'password' => env('AMQP_PASSWORD', 'guest'),
'vhost' => env('AMQP_VHOST', '/'), 'vhost' => env('AMQP_VHOST', '/'),

View File

@ -30,7 +30,7 @@
}, },
"suggest": { "suggest": {
"doctrine/dbal": "Required to rename columns (^2.6).", "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": { "autoload": {
"psr-4": { "psr-4": {

View File

@ -12,13 +12,51 @@ declare(strict_types=1);
namespace Hyperf\Database\Commands\Ast; namespace Hyperf\Database\Commands\Ast;
use Hyperf\Database\Commands\ModelOption; 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 Hyperf\Utils\Str;
use PhpParser\Comment\Doc; use PhpParser\Comment\Doc;
use PhpParser\Node; use PhpParser\Node;
use PhpParser\NodeVisitorAbstract; 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 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 * @var array
*/ */
@ -29,31 +67,48 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
*/ */
protected $option; 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->columns = $columns;
$this->option = $option; $this->option = $option;
$this->initPropertiesFromMethods();
} }
public function leaveNode(Node $node) public function leaveNode(Node $node)
{ {
switch ($node) { switch ($node) {
case $node instanceof Node\Stmt\PropertyProperty: 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); $node = $this->rewriteFillable($node);
} elseif ($node->name == 'casts') { } elseif ((string) $node->name === 'casts') {
$node = $this->rewriteCasts($node); $node = $this->rewriteCasts($node);
} }
return $node; return $node;
case $node instanceof Node\Stmt\Class_: case $node instanceof Node\Stmt\Class_:
$doc = '/**' . PHP_EOL; $node->setDocComment(new Doc($this->parseProperty()));
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));
return $node; return $node;
} }
} }
@ -91,6 +146,147 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
return $node; 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 get<name>Attribute
$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 set<name>Attribute
$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 protected function getProperty($column): array
{ {
$name = $this->option->isCamelCase() ? Str::camel($column['column_name']) : $column['column_name']; $name = $this->option->isCamelCase() ? Str::camel($column['column_name']) : $column['column_name'];
@ -142,4 +338,35 @@ class ModelUpdateVisitor extends NodeVisitorAbstract
return $cast; 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();
}
} }

View File

@ -183,6 +183,7 @@ class ModelCommand extends Command
$stms = $this->astParser->parse(file_get_contents($path)); $stms = $this->astParser->parse(file_get_contents($path));
$traverser = new NodeTraverser(); $traverser = new NodeTraverser();
$traverser->addVisitor(make(ModelUpdateVisitor::class, [ $traverser->addVisitor(make(ModelUpdateVisitor::class, [
'class' => $class,
'columns' => $columns, 'columns' => $columns,
'option' => $option, 'option' => $option,
])); ]));

View File

@ -101,15 +101,16 @@ class RoutesCommand extends HyperfCommand
} else { } else {
$action = $handler->callback; $action = $handler->callback;
} }
if (isset($data[$uri])) { $unique = "{$serverName}|{$action}";
$data[$uri]['method'][] = $method; if (isset($data[$unique])) {
$data[$unique]['method'][] = $method;
} else { } else {
// method,uri,name,action,middleware // method,uri,name,action,middleware
$registedMiddlewares = MiddlewareManager::get($serverName, $uri, $method); $registedMiddlewares = MiddlewareManager::get($serverName, $uri, $method);
$middlewares = $this->config->get('middlewares.' . $serverName, []); $middlewares = $this->config->get('middlewares.' . $serverName, []);
$middlewares = array_merge($middlewares, $registedMiddlewares); $middlewares = array_merge($middlewares, $registedMiddlewares);
$data[$uri] = [ $data[$unique] = [
'server' => $serverName, 'server' => $serverName,
'method' => [$method], 'method' => [$method],
'uri' => $uri, 'uri' => $uri,

View File

@ -40,6 +40,9 @@
"hyperf/command": "Required to use Command annotation.", "hyperf/command": "Required to use Command annotation.",
"symfony/event-dispatcher": "Required to use symfony event dispatcher (^4.3)." "symfony/event-dispatcher": "Required to use symfony event dispatcher (^4.3)."
}, },
"conflict": {
"symfony/event-dispatcher": "<4.3"
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Hyperf\\Framework\\": "src/" "Hyperf\\Framework\\": "src/"

View File

@ -230,14 +230,14 @@ abstract class AbstractServiceClient
$services = $health->service($this->serviceName)->json(); $services = $health->service($this->serviceName)->json();
$nodes = []; $nodes = [];
foreach ($services as $node) { foreach ($services as $node) {
$passing = true; $passing = false;
$service = $node['Service'] ?? []; $service = $node['Service'] ?? [];
$checks = $node['Checks'] ?? []; $checks = $node['Checks'] ?? [];
foreach ($checks as $check) { foreach ($checks as $check) {
$status = $check['Status'] ?? false; $status = $check['Status'] ?? false;
if ($status !== 'passing') { if ($status === 'passing' && $this->protocol === $service['Meta']['Protocol']) {
$passing = false; $passing = true;
} }
} }

View File

@ -279,16 +279,24 @@ if (! function_exists('call')) {
} }
if (! function_exists('go')) { if (! function_exists('go')) {
/**
* @return bool|int
*/
function go(callable $callable) function go(callable $callable)
{ {
Coroutine::create($callable); $id = Coroutine::create($callable);
return $id > 0 ? $id : false;
} }
} }
if (! function_exists('co')) { if (! function_exists('co')) {
/**
* @return bool|int
*/
function co(callable $callable) function co(callable $callable)
{ {
Coroutine::create($callable); $id = Coroutine::create($callable);
return $id > 0 ? $id : false;
} }
} }

View File

@ -29,6 +29,17 @@ class FunctionTest extends TestCase
$this->assertSame(2, $result); $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() public function testDataGet()
{ {
$data = ['id' => 1]; $data = ['id' => 1];