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
- [#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

View File

@ -82,6 +82,21 @@ Hyperf 还提供了 `基于 PSR-11 的依赖注入容器`、`注解`、`AOP 面
</table>
<!--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) 开源的软件。

View File

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

View File

@ -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",

View File

@ -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,

View File

@ -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/

View File

@ -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"
},
```

View File

@ -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`

View File

@ -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', '/'),

View File

@ -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": {

View File

@ -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 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
{
$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();
}
}

View File

@ -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,
]));

View File

@ -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,

View File

@ -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/"

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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];