Merge branch 'master' into 2.1-merge

# Conflicts:
#	src/guzzle/src/CoroutineHandler.php
#	src/guzzle/tests/Stub/CoroutineHandlerStub.php
This commit is contained in:
李铭昕 2020-11-25 16:46:33 +08:00
commit 8921c2defc
71 changed files with 1292 additions and 116 deletions

29
.github/workflows/gitee.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Mirror to Gitee Repo
on: [ push, delete, create ]
jobs:
git-mirror:
runs-on: ubuntu-latest
steps:
- name: Configure Private Key
if: github.repository_owner == 'hyperf'
env:
SSH_PRIVATE_KEY: ${{ secrets.GITEE_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "StrictHostKeyChecking no" >> ~/.ssh/config
- name: Push Mirror
if: github.repository_owner == 'hyperf'
env:
SOURCE_REPO: 'https://github.com/hyperf/hyperf.git'
DESTINATION_REPO: 'git@gitee.com:hyperf/hyperf.git'
run: |
git clone --mirror "$SOURCE_REPO" && cd `basename "$SOURCE_REPO"`
git remote set-url --push origin "$DESTINATION_REPO"
git fetch -p origin
git for-each-ref --format 'delete %(refname)' refs/pull | git update-ref --stdin
git push --mirror

View File

@ -73,7 +73,7 @@ VALUES
DROP TABLE IF EXISTS `user_ext`;
CREATE TABLE `user_ext` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`id` bigint(20) unsigned NOT NULL,
`count` int(10) unsigned NOT NULL DEFAULT '0',
`float_num` decimal(10,2) DEFAULT '0.00',
`str` varchar(16) DEFAULT NULL,

View File

@ -1,4 +1,59 @@
# v2.0.19 - TBD
# v2.0.21 - TBD
## Added
- [#2857](https://github.com/hyperf/hyperf/pull/2857) Support Consul ACL Token for Service Governance.
## Changed
- [#2851](https://github.com/hyperf/hyperf/pull/2851) Changed default engine of view config.
## Optimized
- [#2861](https://github.com/hyperf/hyperf/pull/2861) Optimized guzzle coroutine handler which throw exception when the status code below zero.
- [#2868](https://github.com/hyperf/hyperf/pull/2868) Optimized code for guzzle sink, which support resource not only string.
# v2.0.20 - 2020-11-23
## Added
- [#2824](https://github.com/hyperf/hyperf/pull/2824) Added method `simplePaginate()` which return `PaginatorInterface` in `Hyperf\Database\Query\Builder`.
## Fixed
- [#2820](https://github.com/hyperf/hyperf/pull/2820) Fixed amqp consumer does not works when using fanout exchange.
- [#2831](https://github.com/hyperf/hyperf/pull/2831) Fixed bug that amqp connection always be closed by client.
- [#2848](https://github.com/hyperf/hyperf/pull/2848) Fixed database connection has already been bound to another coroutine when used in defer.
## Changed
- [#2824](https://github.com/hyperf/hyperf/pull/2824) Changed the result from `PaginatorInterface` to `LengthAwarePaginatorInterface` for method `paginate()` in `Hyperf\Database\Query\Builder`.
## Optimized
- [#2766](https://github.com/hyperf/hyperf/pull/2766) Safely finish spans in case of exception for tracer.
- [#2805](https://github.com/hyperf/hyperf/pull/2805) Optimized nacos process which can stop safely.
- [#2821](https://github.com/hyperf/hyperf/pull/2821) Optimized the exceptions thrown by `Json` and `Xml`.
- [#2827](https://github.com/hyperf/hyperf/pull/2827) Optimized `Hyperf\Server\ServerConfig` which return type of `__set` should be void.
- [#2839](https://github.com/hyperf/hyperf/pull/2839) Optimized comments for `Hyperf\Database\Schema\ColumnDefinition`.
# v2.0.19 - 2020-11-17
## Added
- [#2794](https://github.com/hyperf/hyperf/pull/2794) [#2802](https://github.com/hyperf/hyperf/pull/2802) Added `options.cookie_lifetime` for `hyperf/session`, you can use it to control the expire time for cookies.
## Fixed
- [#2783](https://github.com/hyperf/hyperf/pull/2783) Fixed nsq consumer does not works in coroutine style server.
- [#2788](https://github.com/hyperf/hyperf/pull/2788) Fixed call non-static method `__handlePropertyHandler()` statically in class proxy.
- [#2790](https://github.com/hyperf/hyperf/pull/2790) Fixed `BootProcessListener` of `config-etcd` does not works in coroutine style server.
- [#2803](https://github.com/hyperf/hyperf/pull/2803) Fixed response body does not exists when bad request.
- [#2807](https://github.com/hyperf/hyperf/pull/2807) Fixed Middleware does not work as expected when repeatedly configured.
## Optimized
- [#2750](https://github.com/hyperf/hyperf/pull/2750) Use elastic `index` instead of `type` for `searchableAs`, when the config of `index` is `null` or the elastic version is more than `7.0.0`.
# v2.0.18 - 2020-11-09

View File

@ -104,6 +104,7 @@
"hyperf/di": "self.version",
"hyperf/dispatcher": "self.version",
"hyperf/elasticsearch": "self.version",
"hyperf/encryption": "self.version",
"hyperf/etcd": "self.version",
"hyperf/event": "self.version",
"hyperf/exception-handler": "self.version",
@ -180,6 +181,7 @@
"Hyperf\\Di\\": "src/di/src/",
"Hyperf\\Dispatcher\\": "src/dispatcher/src/",
"Hyperf\\Elasticsearch\\": "src/elasticsearch/src/",
"Hyperf\\Encryption\\": "src/encryption/src/",
"Hyperf\\Etcd\\": "src/etcd/src/",
"Hyperf\\Event\\": "src/event/src/",
"Hyperf\\ExceptionHandler\\": "src/exception-handler/src/",
@ -262,6 +264,7 @@
"HyperfTest\\Di\\": "src/di/tests/",
"HyperfTest\\Dispatcher\\": "src/dispatcher/tests/",
"HyperfTest\\Elasticsearch\\": "src/elasticsearch/tests/",
"HyperfTest\\Encryption\\": "src/encryption/tests/",
"HyperfTest\\Etcd\\": "src/etcd/tests/",
"HyperfTest\\Event\\": "src/event/tests/",
"HyperfTest\\ExceptionHandler\\": "src/exception-handler/tests/",
@ -338,6 +341,7 @@
"Hyperf\\Devtool\\ConfigProvider",
"Hyperf\\Di\\ConfigProvider",
"Hyperf\\Dispatcher\\ConfigProvider",
"Hyperf\\Encryption\\ConfigProvider",
"Hyperf\\Etcd\\ConfigProvider",
"Hyperf\\Event\\ConfigProvider",
"Hyperf\\ExceptionHandler\\ConfigProvider",

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh">
<head>
<meta charset="UTF-8">

View File

@ -126,6 +126,7 @@
- [firstphp/wsdebug](https://github.com/lamplife/wsdebug) 通过 `WebSocket` 实时观测异常错误的开发调试组件
- [qbhy/hyperf-multi-env](https://github.com/qbhy/hyperf-multi-env) 支持与 laravel 类似的多 env 配置文件功能,通过 `APP_ENV=testing` 可以加载 `.env.testing` 配置覆盖默认的 `.env`
- [qiutuleng/hyperf-dump-server](https://github.com/qiutuleng/hyperf-dump-server) 提供一个 `dump` 函数,可以将程序内的变量或数据打印到另一个命令行窗口中,基于 Symfony 的 `Var-Dump Server` 组件
- [leearvin/hyperf-tinker](https://github.com/Arvin-Lee/hyperf-tinker) 基于 PsySH 提供一个交互式的 Hyperf shell 容器
## 权限认证
@ -134,6 +135,7 @@
- [96qbhy/hyperf-auth](https://github.com/qbhy/hyperf-auth) 参考 laravel 的 auth 组件设计,支持 jwt 和 session 驱动,更轻巧更好用
- [hyperf-ext/jwt](https://github.com/hyperf-ext/jwt) JWT 组件,实现了完整用于 JWT 认证的能力
- [hyperf-ext/auth](https://github.com/hyperf-ext/auth) 移植自 `illuminate/auth`,基本完整的实现了 Laravel Auth 的功能特性
- [donjan-deng/hyperf-casbin](https://github.com/donjan-deng/hyperf-casbin) 适配于 Hyperf 的开源访问控制框架 [Casbin](https://casbin.org/docs/zh-CN/overview)
## 第三方 SDK

View File

@ -1,5 +1,47 @@
# 版本更新记录
# v2.0.20 - 2020-11-23
## 新增
- [#2824](https://github.com/hyperf/hyperf/pull/2824) 为 `Hyperf\Database\Query\Builder` 增加方法 `simplePaginate()`
## 修复
- [#2820](https://github.com/hyperf/hyperf/pull/2820) 修复使用 `fanout` 交换机时,`AMQP` 消费者无法正常工作的问题。
- [#2831](https://github.com/hyperf/hyperf/pull/2831) 修复 `AMQP` 连接会被客户端意外关闭的问题。
- [#2848](https://github.com/hyperf/hyperf/pull/2848) 修复在 `defer` 中使用数据库组件时,会导致数据库连接会同时被其他协程绑定的问题。
## 变更
- [#2824](https://github.com/hyperf/hyperf/pull/2824) 修改 `Hyperf\Database\Query\Builder` 方法 `paginate()` 返回值类型,由 `PaginatorInterface` 变更为 `LengthAwarePaginatorInterface`
## 优化
- [#2766](https://github.com/hyperf/hyperf/pull/2766) 优化 `Tracer` 组件,在抛出异常的情况下,也可以执行 `finish` 方法,记录链路。
- [#2805](https://github.com/hyperf/hyperf/pull/2805) 优化 `Nacos` 进程,可以安全停止。
- [#2821](https://github.com/hyperf/hyperf/pull/2821) 优化工具类 `Json``Xml`,使其抛出一致的异常。
- [#2827](https://github.com/hyperf/hyperf/pull/2827) 优化 `Hyperf\Server\ServerConfig`,解决方法 `__set` 因返回值不为 `void`,导致不兼容 `PHP8` 的问题。
- [#2839](https://github.com/hyperf/hyperf/pull/2839) 优化 `Hyperf\Database\Schema\ColumnDefinition` 的注释。
# v2.0.19 - 2020-11-17
## 新增
- [#2794](https://github.com/hyperf/hyperf/pull/2794) [#2802](https://github.com/hyperf/hyperf/pull/2802) 为 `Session` 组件新增配置项 `options.cookie_lifetime`, 允许用户自己设置 `Cookies` 的超时时间。
## 修复
- [#2783](https://github.com/hyperf/hyperf/pull/2783) 修复 `NSQ` 消费者无法在协程风格下正常使用的问题。
- [#2788](https://github.com/hyperf/hyperf/pull/2788) 修复非静态方法 `__handlePropertyHandler()` 在代理类中,被静态调用的问题。
- [#2790](https://github.com/hyperf/hyperf/pull/2790) 修复 `ETCD` 配置中心,`BootProcessListener` 监听器无法在协程风格下正常使用的问题。
- [#2803](https://github.com/hyperf/hyperf/pull/2803) 修复当 `Request` 无法实例化时,`HTTP` 响应数据被清除的问题。
- [#2807](https://github.com/hyperf/hyperf/pull/2807) 修复当存在重复的中间件时,中间件的表现会与预期不符的问题。
## 优化
- [#2750](https://github.com/hyperf/hyperf/pull/2750) 优化 `Scout` 组件,当没有配置搜索引擎 `index``Elasticsearch` 版本高于 `7.0` 时,使用 `index` 而非 `type` 作为模型的搜索条件。
# v2.0.18 - 2020-11-09
## 新增

View File

@ -54,7 +54,7 @@ Swoole 协程也是对异步回调的一种解决方案,在 `PHP` 语言下,
### 协程与普通线程有哪些区别?
都说协程是一个轻量级的线程,协程和线程都适用于多任务的场景下,从这个角度上来说,协程与线程很相似,都有自己的上下文,可以共享全局变量,但不同之处在于,在同一时间可以有多个线程处于运行状态,但对于 `Swoole` 协程来说只能有一个,其它的协程都会处于暂停的状态。此外,普通线程是抢占式的,个线程能得到资源由操作系统决定,而协程是协作式的,执行权由用户态自行分配。
都说协程是一个轻量级的线程,协程和线程都适用于多任务的场景下,从这个角度上来说,协程与线程很相似,都有自己的上下文,可以共享全局变量,但不同之处在于,在同一时间可以有多个线程处于运行状态,但对于 `Swoole` 协程来说只能有一个,其它的协程都会处于暂停的状态。此外,普通线程是抢占式的,个线程能得到资源由操作系统决定,而协程是协作式的,执行权由用户态自行分配。
## 协程编程注意事项

View File

@ -96,8 +96,8 @@ class DbQueryExecutedListener implements ListenerInterface
| updated | 数据更新后 | 否 | |
| saving | 数据创建或更新时 | 是 | |
| saved | 数据创建或更新后 | 否 | |
| restoring | 软删除数据复时 | 是 | |
| restored | 软删除数据复后 | 否 | |
| restoring | 软删除数据复时 | 是 | |
| restored | 软删除数据复后 | 否 | |
| deleting | 数据删除时 | 是 | |
| deleted | 数据删除后 | 否 | |
| forceDeleted | 数据强制删除后 | 否 | |

37
docs/zh-cn/encryption.md Normal file
View File

@ -0,0 +1,37 @@
# 加密解密
[hyperf/encryption](https://github.com/hyperf/encryption) 借鉴于 `Laravel Encryption` 组件,十分感谢 `Laravel` 开发组对 `PHP` 社区的贡献。
## 简介
`Encryption` 是基于 `OpenSSL` 实现加密解密组件。
## 配置
加密器可以根据实际情况,配置多组 `key``cipher`
```php
<?php
return [
'default' => [
'key' => 'Hyperf',
'cipher' => 'AES-128-CBC',
],
];
```
## 使用
```php
<?php
use Hyperf\Utils\ApplicationContext;
use Hyperf\Encryption\Contract\EncrypterInterface;
$input = 'Hello Word.';
$container = ApplicationContext::getContainer();
$encrypter = $container->get(EncrypterInterface::class);
$encrypt = $encrypter->encrypt($input);
$raw = $encrypter->decrypt($encrypt);
```

View File

@ -214,7 +214,7 @@ class FooMiddleware implements MiddlewareInterface
[
'code' => -1,
'data' => [
'error' => '中间验证token无效阻止继续向下执行',
'error' => '中间验证token无效阻止继续向下执行',
],
]
);

View File

@ -145,10 +145,32 @@ $id = $generator->generate($userId);
```php
<?php
use Hyperf\Database\Model\Model;
use Hyperf\Snowflake\Concern\Snowflake;
class User extends \Hyperf\Database\Model\Model {
use \Hyperf\Snowflake\Concern\Snowflake;
class User extends Model {
use Snowflake;
}
```
上述 User 模型在创建时便会默认使用 Snowflake 算法生成主键。
因为 Snowflake 中会复写 `creating` 方法,而用户有需要自己设置 `creating` 方法时,就会出现无法生成 `ID` 的问题。这里需要用户按照以下方式自行处理即可
```php
<?php
use Hyperf\Database\Model\Model;
use Hyperf\Snowflake\Concern\Snowflake;
class User extends Model {
use Snowflake {
creating as create;
}
public function creating()
{
$this->create();
// Do something ...
}
}
```

View File

@ -116,7 +116,8 @@
* [Watcher](zh-cn/watcher.md)
* [开发者工具](zh-cn/devtool.md)
* [Swoole Tracker](zh-cn/swoole-tracker.md)
* [加密解密](zh-cn/encryption.md)
* 应用部署
* [Docker Swarm 集群搭建](zh-cn/tutorial/docker-swarm.md)

View File

@ -73,6 +73,16 @@ return [
### 安装 Blade 引擎
```bash
composer require hyperf/view-engine
```
详细方式见文档 [视图引擎](zh-cn/view-engine.md)
或者使用
> duncan3dc/blade 因为使用了 Laravel 的 Support 库,所以会导致某些函数不兼容,暂时不推荐使用
```bash
composer require duncan3dc/blade
```

View File

@ -25,6 +25,7 @@
<directory suffix="Test.php">./src/di/tests</directory>
<directory suffix="Test.php">./src/dispatcher/tests</directory>
<directory suffix="Test.php">./src/elasticsearch/tests</directory>
<directory suffix="Test.php">./src/encryption/tests</directory>
<directory suffix="Test.php">./src/etcd/tests</directory>
<directory suffix="Test.php">./src/event/tests</directory>
<directory suffix="Test.php">./src/exception-handler/tests</directory>
@ -80,6 +81,7 @@
<directory suffix=".php">./src/di/src</directory>
<directory suffix=".php">./src/dispatcher/src</directory>
<directory suffix=".php">./src/elasticsearch/src</directory>
<directory suffix=".php">./src/encryption/src</directory>
<directory suffix=".php">./src/event/src</directory>
<directory suffix=".php">./src/grpc-client/src</directory>
<directory suffix=".php">./src/guzzle/src</directory>

View File

@ -85,10 +85,6 @@ class Connection extends BaseConnection implements ConnectionInterface
public function getActiveConnection(): AbstractConnection
{
if ($this->check()) {
// The connection is valid, reset the last heartbeat time.
$currentTime = microtime(true);
$this->lastHeartbeatTime = $currentTime;
return $this->connection;
}
@ -128,7 +124,14 @@ class Connection extends BaseConnection implements ConnectionInterface
public function check(): bool
{
return isset($this->connection) && $this->connection instanceof AbstractConnection && $this->connection->isConnected() && ! $this->isHeartbeatTimeout();
$result = isset($this->connection) && $this->connection instanceof AbstractConnection && $this->connection->isConnected() && ! $this->isHeartbeatTimeout();
if ($result) {
// The connection is valid, reset the last heartbeat time.
$currentTime = microtime(true);
$this->lastHeartbeatTime = $currentTime;
}
return $result;
}
public function close(): bool

View File

@ -18,6 +18,7 @@ use Hyperf\Amqp\Event\WaitTimeout;
use Hyperf\Amqp\Exception\MessageException;
use Hyperf\Amqp\Message\ConsumerMessageInterface;
use Hyperf\Amqp\Message\MessageInterface;
use Hyperf\Amqp\Message\Type;
use Hyperf\Amqp\Pool\PoolFactory;
use Hyperf\Contract\ConfigInterface;
use Hyperf\ExceptionHandler\Formatter\FormatterInterface;
@ -132,6 +133,10 @@ class Consumer extends Builder
$channel->queue_bind($message->getQueue(), $message->getExchange(), $routingKey);
}
if (empty($routineKeys) && $message->getType() === Type::FANOUT) {
$channel->queue_bind($message->getQueue(), $message->getExchange());
}
if (is_array($qos = $message->getQos())) {
$size = $qos['prefetch_size'] ?? null;
$count = $qos['prefetch_count'] ?? null;

View File

@ -20,6 +20,7 @@ use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BeforeWorkerStart;
use Hyperf\Process\Event\BeforeProcessHandle;
use Hyperf\Server\Event\MainCoroutineServerStart;
use Hyperf\Utils\Coordinator\Constants;
use Hyperf\Utils\Coordinator\CoordinatorManager;
use Hyperf\Utils\Coroutine;
@ -69,6 +70,7 @@ class BootProcessListener implements ListenerInterface
BeforeWorkerStart::class,
BeforeProcessHandle::class,
BeforeHandle::class,
MainCoroutineServerStart::class,
];
}

View File

@ -11,4 +11,5 @@ declare(strict_types=1);
*/
return [
'uri' => 'http://127.0.0.1:8500',
'token' => '',
];

View File

@ -13,6 +13,7 @@ namespace Hyperf\Database\Query;
use Closure;
use DateTimeInterface;
use Hyperf\Contract\LengthAwarePaginatorInterface;
use Hyperf\Contract\PaginatorInterface;
use Hyperf\Database\Concerns\BuildsQueries;
use Hyperf\Database\ConnectionInterface;
@ -1867,13 +1868,31 @@ class Builder
* @param string $pageName
* @param null|int $page
*/
public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null): PaginatorInterface
public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null): PaginatorInterface
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$this->skip(($page - 1) * $perPage)->take($perPage + 1);
return $this->paginator($this->get($columns), $perPage, $page, [
return $this->simplePaginator($this->get($columns), $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
/**
* @param int $perPage
* @param string[] $columns
* @param string $pageName
* @param null $page
*/
public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null): LengthAwarePaginatorInterface
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$total = $this->getCountForPagination();
$results = $total ? $this->forPage($page, $perPage)->get($columns) : collect();
return $this->paginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
@ -2489,18 +2508,6 @@ class Builder
});
}
/**
* Create a new simple paginator instance.
*/
protected function paginator(Collection $items, int $perPage, int $currentPage, array $options)
{
$container = ApplicationContext::getContainer();
if (! method_exists($container, 'make')) {
throw new \RuntimeException('The DI container does not support make() method.');
}
return $container->make(PaginatorInterface::class, compact('items', 'perPage', 'currentPage', 'options'));
}
/**
* Creates a subquery and parse it.
*
@ -2878,4 +2885,28 @@ class Builder
return ! $binding instanceof Expression;
}));
}
/**
* Create a new length-aware paginator instance.
*/
protected function paginator(Collection $items, int $total, int $perPage, int $currentPage, array $options): LengthAwarePaginatorInterface
{
$container = ApplicationContext::getContainer();
if (! method_exists($container, 'make')) {
throw new \RuntimeException('The DI container does not support make() method.');
}
return $container->make(LengthAwarePaginatorInterface::class, compact('items', 'total', 'perPage', 'currentPage', 'options'));
}
/**
* Create a new simple paginator instance.
*/
protected function simplePaginator(Collection $items, int $perPage, int $currentPage, array $options): PaginatorInterface
{
$container = ApplicationContext::getContainer();
if (! method_exists($container, 'make')) {
throw new \RuntimeException('The DI container does not support make() method.');
}
return $container->make(PaginatorInterface::class, compact('items', 'perPage', 'currentPage', 'options'));
}
}

View File

@ -17,21 +17,21 @@ use Hyperf\Utils\Fluent;
* @method ColumnDefinition after(string $column) Place the column "after" another column (MySQL)
* @method ColumnDefinition always() Used as a modifier for generatedAs() (PostgreSQL)
* @method ColumnDefinition autoIncrement() Set INTEGER columns as auto-increment (primary key)
* @method ColumnDefinition change() Change the column
* @method ColumnDefinition change() Change the column
* @method ColumnDefinition charset(string $charset) Specify a character set for the column (MySQL)
* @method ColumnDefinition collation(string $collation) Specify a collation for the column (MySQL/SQL Server)
* @method ColumnDefinition comment(string $comment) Add a comment to the column (MySQL)
* @method ColumnDefinition default(mixed $value) Specify a "default" value for the column
* @method ColumnDefinition default(mixed $value) Specify a "default" value for the column
* @method ColumnDefinition first() Place the column "first" in the table (MySQL)
* @method ColumnDefinition generatedAs(string $expression) Create a SQL compliant identity column (PostgreSQL)
* @method ColumnDefinition index() Add an index
* @method ColumnDefinition nullable(bool $value = true) Allow NULL values to be inserted into the column
* @method ColumnDefinition primary() Add a primary index
* @method ColumnDefinition spatialIndex() Add a spatial index
* @method ColumnDefinition index(string $name = null) Add an index
* @method ColumnDefinition nullable(bool $value = true) Allow NULL values to be inserted into the column
* @method ColumnDefinition primary() Add a primary index
* @method ColumnDefinition spatialIndex() Add a spatial index
* @method ColumnDefinition storedAs(string $expression) Create a stored generated column (MySQL)
* @method ColumnDefinition unique() Add a unique index
* @method ColumnDefinition unique(string $name = null) Add a unique index
* @method ColumnDefinition unsigned() Set the INTEGER column as UNSIGNED (MySQL)
* @method ColumnDefinition useCurrent() Set the TIMESTAMP column to use CURRENT_TIMESTAMP as default value
* @method ColumnDefinition useCurrent() Set the TIMESTAMP column to use CURRENT_TIMESTAMP as default value
* @method ColumnDefinition virtualAs(string $expression) Create a virtual generated column (MySQL)
*/
class ColumnDefinition extends Fluent

View File

@ -20,6 +20,7 @@ use Hyperf\Database\Query\Grammars\MySqlGrammar;
use Hyperf\Database\Query\Processors\MySqlProcessor;
use Hyperf\Database\Query\Processors\Processor;
use Hyperf\Di\Container;
use Hyperf\Paginator\LengthAwarePaginator;
use Hyperf\Paginator\Paginator;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Collection;
@ -2632,7 +2633,7 @@ class QueryBuilderTest extends TestCase
$this->assertTrue(true);
}
public function testPaginate()
public function testSimplePaginate()
{
$perPage = 16;
$columns = ['test'];
@ -2659,7 +2660,7 @@ class QueryBuilderTest extends TestCase
return Context::get('path');
});
$result = $builder->paginate($perPage, $columns, $pageName, $page);
$result = $builder->simplePaginate($perPage, $columns, $pageName, $page);
$this->assertEquals(new Paginator($results, $perPage, $page, [
'path' => $path,
@ -2667,7 +2668,44 @@ class QueryBuilderTest extends TestCase
]), $result);
}
public function testPaginateWithDefaultArguments()
public function testPaginate()
{
$perPage = 16;
$columns = ['test'];
$pageName = 'page-name';
$page = 1;
$total = 10;
$builder = $this->getMockQueryBuilder();
$path = 'http://foo.bar?page=3';
$results = collect([['test' => 'foo'], ['test' => 'bar']]);
$builder->shouldReceive('get')->once()->andReturn($results);
$builder->shouldReceive('getCountForPagination')->andReturn($total);
Context::set('path', $path);
if (! defined('BASE_PATH')) {
define('BASE_PATH', __DIR__);
}
$container = Mockery::mock(Container::class);
$container->shouldReceive('make')->once()->andReturnUsing(function ($interface, $args) {
return new LengthAwarePaginator($args['items'], $args['total'], $args['perPage'], $args['currentPage'], $args['options']);
});
ApplicationContext::setContainer($container);
Paginator::currentPathResolver(function () {
return Context::get('path');
});
$result = $builder->paginate($perPage, $columns, $pageName, $page);
$this->assertEquals(new LengthAwarePaginator($results, $total, $perPage, $page, [
'path' => $path,
'pageName' => $pageName,
]), $result);
}
public function testSimplePaginateWithDefaultArguments()
{
$perPage = 15;
$columns = ['*'];
@ -2700,7 +2738,7 @@ class QueryBuilderTest extends TestCase
return Context::get('path');
});
$result = $builder->paginate();
$result = $builder->simplePaginate();
$this->assertEquals(new Paginator($results, $perPage, $page, [
'path' => $path,
@ -2708,7 +2746,50 @@ class QueryBuilderTest extends TestCase
]), $result);
}
public function testPaginateWhenNoResults()
public function testPaginateWithDefaultArguments()
{
$perPage = 15;
$columns = ['*'];
$pageName = 'page';
$page = 1;
$total = 10;
$builder = $this->getMockQueryBuilder();
$path = 'http://foo.bar?page=3';
$results = collect([['test' => 'foo'], ['test' => 'bar']]);
$builder->shouldReceive('get')->once()->andReturn($results);
$builder->shouldReceive('getCountForPagination')->andReturn($total);
Context::set('path', $path);
Context::set('page', $page);
if (! defined('BASE_PATH')) {
define('BASE_PATH', __DIR__);
}
$container = Mockery::mock(Container::class);
$container->shouldReceive('make')->once()->andReturnUsing(function ($interface, $args) {
return new LengthAwarePaginator($args['items'], $args['total'], $args['perPage'], $args['currentPage'], $args['options']);
});
ApplicationContext::setContainer($container);
Paginator::currentPageResolver(function () {
return Context::get('page');
});
Paginator::currentPathResolver(function () {
return Context::get('path');
});
$result = $builder->paginate();
$this->assertEquals(new LengthAwarePaginator($results, $total, $perPage, $page, [
'path' => $path,
'pageName' => $pageName,
]), $result);
}
public function testSimplePaginateWhenNoResults()
{
$perPage = 15;
$pageName = 'page';
@ -2742,7 +2823,7 @@ class QueryBuilderTest extends TestCase
return Context::get('path');
});
$result = $builder->paginate();
$result = $builder->simplePaginate();
$this->assertEquals(new Paginator($results, $perPage, $page, [
'path' => $path,
@ -2750,6 +2831,50 @@ class QueryBuilderTest extends TestCase
]), $result);
}
public function testPaginateWhenNoResults()
{
$perPage = 15;
$pageName = 'page';
$page = 1;
$total = 10;
$builder = $this->getMockQueryBuilder();
$path = 'http://foo.bar?page=3';
$results = collect();
$builder->shouldReceive('get')->once()->andReturn($results);
$builder->shouldReceive('getCountForPagination')->andReturn($total);
Context::set('path', $path);
Context::set('page', $page);
if (! defined('BASE_PATH')) {
define('BASE_PATH', __DIR__);
}
$container = Mockery::mock(Container::class);
$container->shouldReceive('make')->once()->andReturnUsing(function ($interface, $args) {
/** @var Collection $items */
$items = $args['items'];
return new LengthAwarePaginator(collect($items), $args['total'], $args['perPage'], $args['currentPage'], $args['options']);
});
ApplicationContext::setContainer($container);
Paginator::currentPageResolver(function () {
return Context::get('page');
});
Paginator::currentPathResolver(function () {
return Context::get('path');
});
$result = $builder->paginate();
$this->assertEquals(new LengthAwarePaginator($results, $total, $perPage, $page, [
'path' => $path,
'pageName' => $pageName,
]), $result);
}
public function testWhereRowValues()
{
$builder = $this->getBuilder();

View File

@ -71,7 +71,8 @@ class ConnectionResolver implements ConnectionResolverInterface
Context::set($id, $connection);
} finally {
if (Coroutine::inCoroutine()) {
defer(function () use ($connection) {
defer(function () use ($connection, $id) {
Context::set($id, null);
$connection->release();
});
}

View File

@ -169,4 +169,18 @@ class ConnectionTest extends TestCase
$this->assertSame('mysql:host=192.168.1.1;dbname=hyperf', $pdo->dsn);
}]);
}
public function testDbConnectionUseInDefer()
{
$container = ContainerStub::mockReadWriteContainer();
parallel([function () use ($container) {
$resolver = $container->get(ConnectionResolverInterface::class);
defer(function () {
$this->assertFalse(Context::has('database.connection.default'));
});
$resolver->connection();
}]);
}
}

View File

@ -65,12 +65,12 @@ class PropertyHandlerVisitor extends NodeVisitorAbstract
if ($this->visitorMetadata->hasExtends) {
$constructor->stmts[] = $this->buildCallParentConstructorStatement();
}
$constructor->stmts[] = $this->buildStaticCallStatement();
$constructor->stmts[] = $this->buildMethodCallStatement();
$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);
$node->stmts = array_merge([$this->buildMethodCallStatement()], $node->stmts);
}
if ($node instanceof Node\Stmt\Class_ && ! $node->isAnonymous()) {
$node->stmts = array_merge([$this->buildProxyTraitUseStatement()], $node->stmts);
@ -115,9 +115,9 @@ class PropertyHandlerVisitor extends NodeVisitorAbstract
]);
}
protected function buildStaticCallStatement(): Node\Stmt\Expression
protected function buildMethodCallStatement(): Node\Stmt\Expression
{
return new Node\Stmt\Expression(new Node\Expr\StaticCall(new Name('self'), '__handlePropertyHandler', [
return new Node\Stmt\Expression(new Node\Expr\MethodCall(new Node\Expr\Variable('this'), '__handlePropertyHandler', [
new Node\Arg(new Node\Scalar\MagicConst\Class_()),
]));
}

View File

@ -63,7 +63,7 @@ class Foo
use \Hyperf\Di\Aop\PropertyHandlerTrait;
function __construct()
{
self::__handlePropertyHandler(__CLASS__);
$this->__handlePropertyHandler(__CLASS__);
}
}', $code);
}
@ -83,7 +83,7 @@ class Bar2 extends Bar
use \Hyperf\Di\Aop\PropertyHandlerTrait;
public function __construct(int $id)
{
self::__handlePropertyHandler(__CLASS__);
$this->__handlePropertyHandler(__CLASS__);
parent::__construct($id);
}
public static function build()
@ -112,7 +112,7 @@ class Bar5
{
public function __construct()
{
self::__handlePropertyHandler(__CLASS__);
$this->__handlePropertyHandler(__CLASS__);
$this->id = 9501;
}
};
@ -141,7 +141,7 @@ class Bar4
use \Hyperf\Di\Aop\PropertyHandlerTrait;
function __construct()
{
self::__handlePropertyHandler(__CLASS__);
$this->__handlePropertyHandler(__CLASS__);
}
public function toMethodString() : string
{
@ -185,7 +185,7 @@ class Bar3 extends Bar
if (method_exists(parent::class, \'__construct\')) {
parent::__construct(...func_get_args());
}
self::__handlePropertyHandler(__CLASS__);
$this->__handlePropertyHandler(__CLASS__);
}
public function getId() : int
{

View File

@ -46,7 +46,7 @@ abstract class AbstractRequestHandler
*/
public function __construct(array $middlewares, $coreHandler, ContainerInterface $container)
{
$this->middlewares = array_unique($middlewares);
$this->middlewares = array_values(array_unique($middlewares));
$this->coreHandler = $coreHandler;
$this->container = $container;
}

2
src/encryption/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
/tests export-ignore
/.github export-ignore

View File

@ -0,0 +1,25 @@
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Release
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false

22
src/encryption/LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Copyright (c) Hyperf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,36 @@
{
"name": "hyperf/encryption",
"type": "library",
"license": "MIT",
"keywords": [
"php",
"hyperf",
"encryption"
],
"description": "",
"autoload": {
"psr-4": {
"Hyperf\\Encryption\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\Encryption\\": "tests/"
}
},
"require": {
"php": ">=7.2",
"ext-json": "*",
"ext-openssl": "*",
"hyperf/contract": "~2.0.0",
"psr/container": "^1.0"
},
"config": {
"sort-packages": true
},
"extra": {
"hyperf": {
"config": "Hyperf\\Encryption\\ConfigProvider"
}
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
return [
'default' => [
'key' => 'Hyperf',
'cipher' => 'AES-128-CBC',
],
];

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Encryption;
use Hyperf\Encryption\Contract\EncrypterInterface;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => [
EncrypterInterface::class => EncrypterInvoker::class,
],
'publish' => [
[
'id' => 'config',
'description' => 'The config for encryption.',
'source' => __DIR__ . '/../publish/encryption.php',
'destination' => BASE_PATH . '/config/autoload/encryption.php',
],
],
];
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Encryption\Contract;
use RuntimeException;
interface EncrypterInterface
{
/**
* Encrypt the given value.
*
* @param mixed $value
* @throws RuntimeException
*/
public function encrypt($value, bool $serialize = true): string;
/**
* Decrypt the given value.
*
* @throws RuntimeException
* @return mixed
*/
public function decrypt(string $payload, bool $unserialize = true);
}

View File

@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Encryption;
use Hyperf\Encryption\Contract\EncrypterInterface;
use Hyperf\Encryption\Exception\DecryptException;
use Hyperf\Encryption\Exception\EncryptException;
use Hyperf\Encryption\Exception\InvalidArgumentException;
class Encrypter implements EncrypterInterface
{
/**
* The encryption key.
*
* @var string
*/
protected $key;
/**
* The algorithm used for encryption.
*
* @var string
*/
protected $cipher;
public function __construct(string $key, string $cipher = 'AES-128-CBC')
{
if (empty($key)) {
throw new InvalidArgumentException('Encrypter key Not Set');
}
if (! in_array(strtolower($cipher), openssl_get_cipher_methods())) {
throw new InvalidArgumentException('Encrypter ciphers Not supported');
}
$this->key = $key;
$this->cipher = $cipher;
}
/**
* Encrypt the given value.
*
* @param mixed $value
* @throws \Exception
*/
public function encrypt($value, bool $serialize = true): string
{
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
// First we will encrypt the value using OpenSSL. After this is encrypted we
// will proceed to calculating a MAC for the encrypted value so that this
// value can be verified later as not having been changed by the users.
$value = \openssl_encrypt(
$serialize ? serialize($value) : $value,
$this->cipher,
$this->key,
0,
$iv
);
if ($value === false) {
throw new EncryptException('Could not encrypt the data.');
}
// Once we get the encrypted value we'll go ahead and base64_encode the input
// vector and create the MAC for the encrypted value so we can then verify
// its authenticity. Then, we'll JSON the data into the "payload" array.
$mac = $this->hash($iv = base64_encode($iv), $value);
$json = json_encode(compact('iv', 'value', 'mac'), JSON_UNESCAPED_SLASHES);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new EncryptException('Could not encrypt the data.');
}
return base64_encode($json);
}
/**
* Encrypt a string without serialization.
*
* @param string $value
* @throws \Exception
* @return string
*/
public function encryptString($value)
{
return $this->encrypt($value, false);
}
/**
* Decrypt the given value.
*
* @return mixed
*/
public function decrypt(string $payload, bool $unserialize = true)
{
$payload = $this->getJsonPayload($payload);
$iv = base64_decode($payload['iv']);
// Here we will decrypt the value. If we are able to successfully decrypt it
// we will then unserialize it and return it out to the caller. If we are
// unable to decrypt this value we will throw out an exception message.
$decrypted = \openssl_decrypt(
$payload['value'],
$this->cipher,
$this->key,
0,
$iv
);
if ($decrypted === false) {
throw new DecryptException('Could not decrypt the data.');
}
return $unserialize ? unserialize($decrypted) : $decrypted;
}
/**
* Decrypt the given string without unserialization.
*
* @param string $payload
* @return string
*/
public function decryptString($payload)
{
return $this->decrypt($payload, false);
}
/**
* Get the encryption key.
*
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* Create a MAC for the given value.
*
* @param string $iv
* @param mixed $value
* @return string
*/
protected function hash($iv, $value)
{
return hash_hmac('sha256', $iv . $value, $this->key);
}
/**
* Get the JSON array from the given payload.
*
* @param string $payload
* @return array
*/
protected function getJsonPayload($payload)
{
$payload = json_decode(base64_decode($payload), true);
// If the payload is not valid JSON or does not have the proper keys set we will
// assume it is invalid and bail out of the routine since we will not be able
// to decrypt the given value. We'll also check the MAC for this encryption.
if (! $this->validPayload($payload)) {
throw new DecryptException('The payload is invalid.');
}
if (! $this->validMac($payload)) {
throw new DecryptException('The MAC is invalid.');
}
return $payload;
}
/**
* Verify that the encryption payload is valid.
*
* @param mixed $payload
* @return bool
*/
protected function validPayload($payload)
{
return is_array($payload) && isset($payload['iv'], $payload['value'], $payload['mac']) &&
strlen(base64_decode($payload['iv'], true)) === openssl_cipher_iv_length($this->cipher);
}
/**
* Determine if the MAC for the given payload is valid.
*
* @return bool
*/
protected function validMac(array $payload)
{
$calculated = $this->calculateMac($payload, $bytes = random_bytes(16));
return hash_equals(
hash_hmac('sha256', $payload['mac'], $bytes, true),
$calculated
);
}
/**
* Calculate the hash of the given payload.
*
* @param array $payload
* @param string $bytes
* @return string
*/
protected function calculateMac($payload, $bytes)
{
return hash_hmac(
'sha256',
$this->hash($payload['iv'], $payload['value']),
$bytes,
true
);
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Encryption;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Encryption\Contract\EncrypterInterface;
use Psr\Container\ContainerInterface;
class EncrypterFactory
{
/**
* @var ConfigInterface
*/
protected $config;
public function __construct(ContainerInterface $container)
{
$this->config = $container->get(ConfigInterface::class);
}
public function get(string $name): EncrypterInterface
{
$encryption = $this->config->get('encryption.' . $name, []);
$key = $encryption['key'] ?? '';
$cipher = $encryption['cipher'] ?? 'AES-128-CBC';
return new Encrypter($key, $cipher);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Encryption;
use Psr\Container\ContainerInterface;
class EncrypterInvoker
{
public function __invoke(ContainerInterface $container)
{
$factory = $container->get(EncrypterFactory::class);
return $factory->get('default');
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Encryption\Exception;
class DecryptException extends \RuntimeException
{
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Encryption\Exception;
class EncryptException extends \RuntimeException
{
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\Encryption\Exception;
class InvalidArgumentException extends \InvalidArgumentException
{
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\Encryption;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Encryption\Contract\EncrypterInterface;
use Hyperf\Encryption\EncrypterFactory;
use Hyperf\Encryption\EncrypterInvoker;
use Hyperf\Utils\ApplicationContext;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
/**
* @internal
* @coversNothing
*/
class EncrypterTest extends TestCase
{
protected function tearDown(): void
{
Mockery::close();
}
public function mockContainer(): ContainerInterface
{
$container = Mockery::mock(ContainerInterface::class);
ApplicationContext::setContainer($container);
$config = new Config([
'encryption' => [
'default' => [
'key' => '123456',
'cipher' => 'AES-256-CBC',
],
],
]);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config);
$container->shouldReceive('get')->with(EncrypterFactory::class)->andReturnUsing(function () use ($container) {
return new EncrypterFactory($container);
});
$container->shouldReceive('get')->with(EncrypterInterface::class)->andReturnUsing(function () use ($container) {
return (new EncrypterInvoker())($container);
});
return $container;
}
public function testEncodeString()
{
$input = 'Hello Word.';
$container = $this->mockContainer();
$encrypter = $container->get(EncrypterInterface::class);
$encrypt = $encrypter->encryptString($input);
$this->assertSame($input, $encrypter->decryptString($encrypt));
}
public function testEncodeObject()
{
$input = range(1, 3);
$container = $this->mockContainer();
$encrypter = $container->get(EncrypterInterface::class);
$encrypt = $encrypter->encrypt($input);
$this->assertSame($input, $encrypter->decrypt($encrypt));
}
}

View File

@ -180,8 +180,9 @@ class CoroutineHandler
protected function getResponse(RawResponse $raw, RequestInterface $request, array $options, float $transferTime)
{
$body = $raw->body;
if (isset($options['sink']) && is_string($options['sink'])) {
$body = $this->createSink($raw->body, $options['sink']);
$sink = $options['sink'] ?? null;
if (isset($sink) && (is_string($sink) || is_resource($sink))) {
$body = $this->createSink($body, $sink);
}
$response = new Psr7\Response(
@ -210,16 +211,16 @@ class CoroutineHandler
return Utils::streamFor($body);
}
protected function createSink(string $body, string $sink)
/**
* @param resource|string $stream
*/
protected function createSink(string $body, $stream)
{
if (! empty($options['stream'])) {
return $body;
if (is_string($stream)) {
$stream = fopen($stream, 'w+');
}
$stream = fopen($sink, 'w+');
if ($body !== '') {
fwrite($stream, $body);
fseek($stream, 0);
}
return $stream;

View File

@ -289,8 +289,30 @@ class CoroutineHandlerTest extends TestCase
@mkdir($dir, 0755, true);
$handler = new CoroutineHandlerStub();
$handler->createSink($body = uniqid(), $sink = $dir . uniqid());
$stream = $handler->createSink($body = uniqid(), $sink = $dir . uniqid());
$this->assertSame($body, file_get_contents($sink));
$this->assertSame('', stream_get_contents($stream));
$stream = $handler->createSink($body = uniqid(), $sink);
$this->assertSame($body, file_get_contents($sink));
$this->assertSame('', stream_get_contents($stream));
fseek($stream, 0);
$this->assertSame($body, stream_get_contents($stream));
}
public function testResourceSink()
{
$dir = BASE_PATH . '/runtime/guzzle/';
@mkdir($dir, 0755, true);
$sink = fopen($file = $dir . uniqid(), 'w+');
$handler = new CoroutineHandlerStub();
$stream = $handler->createSink($body1 = uniqid(), $sink);
$this->assertSame('', stream_get_contents($stream));
$stream = $handler->createSink($body2 = uniqid(), $sink);
$this->assertSame('', stream_get_contents($stream));
$this->assertSame($body1 . $body2, file_get_contents($file));
fseek($sink, 0);
$this->assertSame($body1 . $body2, stream_get_contents($stream));
}
public function testExpect100Continue()

View File

@ -26,7 +26,7 @@ class CoroutineHandlerStub extends CoroutineHandler
$this->statusCode = $statusCode;
}
public function createSink(string $body, string $sink)
public function createSink(string $body, $sink)
{
return parent::createSink($body, $sink);
}

View File

@ -14,7 +14,7 @@ namespace Hyperf\HttpMessage\Server\Request;
use Hyperf\HttpMessage\Exception\BadRequestHttpException;
use Hyperf\HttpMessage\Server\RequestParserInterface;
use Hyperf\Utils\Codec\Json;
use Hyperf\Utils\Exception\InvalidArgumentException;
use InvalidArgumentException;
class JsonParser implements RequestParserInterface
{

View File

@ -14,7 +14,7 @@ namespace Hyperf\HttpMessage\Server\Request;
use Hyperf\HttpMessage\Exception\BadRequestHttpException;
use Hyperf\HttpMessage\Server\RequestParserInterface;
use Hyperf\Utils\Codec\Xml;
use Hyperf\Utils\Exception\InvalidArgumentException;
use InvalidArgumentException;
class XmlParser implements RequestParserInterface
{

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://hyperf.wiki
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\HttpMessage;
use Hyperf\HttpMessage\Exception\BadRequestHttpException;
use Hyperf\HttpMessage\Server\Request\JsonParser;
use Hyperf\HttpMessage\Server\Request\XmlParser;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class RequestParserTest extends TestCase
{
public function testJsonParserFailed()
{
$this->expectException(BadRequestHttpException::class);
$parser = new JsonParser();
$parser->parse('{"hy"', '');
}
public function testXmlParserFailed()
{
$this->expectException(BadRequestHttpException::class);
$parser = new XmlParser();
$parser->parse('{"hy"', '');
}
}

View File

@ -122,7 +122,7 @@ class Server implements OnRequestInterface, MiddlewareInitializerInterface
if (! isset($psr7Response)) {
return;
}
if (! isset($psr7Request) || $psr7Request->getMethod() === 'HEAD') {
if (isset($psr7Request) && $psr7Request->getMethod() === 'HEAD') {
$this->responseEmitter->emit($psr7Response, $response, false);
} else {
$this->responseEmitter->emit($psr7Response, $response, true);

View File

@ -16,6 +16,7 @@ use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Nacos\Client;
use Hyperf\Process\AbstractProcess;
use Hyperf\Process\ProcessCollector;
use Hyperf\Process\ProcessManager;
use Psr\Container\ContainerInterface;
use Swoole\Coroutine\Server as CoServer;
use Swoole\Server;
@ -55,7 +56,7 @@ class FetchConfigProcess extends AbstractProcess
$cache = [];
$config = $this->container->get(ConfigInterface::class);
$client = $this->container->get(Client::class);
while (true) {
while (ProcessManager::isRunning()) {
$remoteConfig = $client->pull();
if ($remoteConfig != $cache) {
$pipeMessage = new PipeMessage($remoteConfig);

View File

@ -22,6 +22,7 @@ use Hyperf\Nacos\Constants;
use Hyperf\Nacos\Exception\RuntimeException;
use Hyperf\Nacos\Instance;
use Hyperf\Nacos\Service;
use Hyperf\Server\Event\CoroutineServerStart;
use Hyperf\Utils\Arr;
use Psr\Container\ContainerInterface;
@ -47,6 +48,7 @@ class MainWorkerStartListener implements ListenerInterface
{
return [
MainWorkerStart::class,
CoroutineServerStart::class,
];
}

View File

@ -17,6 +17,7 @@ use Hyperf\Framework\Event\OnShutdown;
use Hyperf\Nacos\Api\NacosInstance;
use Hyperf\Nacos\Contract\LoggerInterface;
use Hyperf\Nacos\Instance;
use Hyperf\Server\Event\CoroutineServerStop;
use Psr\Container\ContainerInterface;
class OnShutdownListener implements ListenerInterface
@ -26,6 +27,11 @@ class OnShutdownListener implements ListenerInterface
*/
protected $container;
/**
* @var bool
*/
private $processed = false;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
@ -35,11 +41,17 @@ class OnShutdownListener implements ListenerInterface
{
return [
OnShutdown::class,
CoroutineServerStop::class,
];
}
public function process(object $event)
{
if ($this->processed) {
return;
}
$this->processed = true;
$config = $this->container->get(ConfigInterface::class);
if (! $config->get('nacos.enable', true)) {
return;

View File

@ -17,6 +17,7 @@ use Hyperf\Nacos\Contract\LoggerInterface;
use Hyperf\Nacos\Instance;
use Hyperf\Nacos\Service;
use Hyperf\Process\AbstractProcess;
use Hyperf\Process\ProcessManager;
class InstanceBeatProcess extends AbstractProcess
{
@ -33,7 +34,7 @@ class InstanceBeatProcess extends AbstractProcess
$config = $this->container->get(ConfigInterface::class);
$logger = $this->container->get(LoggerInterface::class);
while (true) {
while (ProcessManager::isRunning()) {
sleep($config->get('nacos.client.beat_interval', 5));
$send = $nacosInstance->beat($service, $instance);
if ($send) {

View File

@ -14,6 +14,7 @@ namespace Hyperf\Nsq\Listener;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BeforeMainServerStart;
use Hyperf\Nsq\ConsumerManager;
use Hyperf\Server\Event\MainCoroutineServerStart;
use Psr\Container\ContainerInterface;
/**
@ -38,6 +39,7 @@ class BeforeMainServerStartListener implements ListenerInterface
{
return [
BeforeMainServerStart::class,
MainCoroutineServerStart::class,
];
}

View File

@ -20,7 +20,7 @@ return [
'concurrency' => 100,
'engine' => [
'elasticsearch' => [
'driver' => \Hyperf\Scout\Provider\ElasticsearchProvider::class,
'driver' => Hyperf\Scout\Provider\ElasticsearchProvider::class,
'index' => env('ELASTICSEARCH_INDEX', 'hyperf'),
'hosts' => [
env('ELASTICSEARCH_HOST', 'http://localhost'),

View File

@ -22,10 +22,17 @@ use Hyperf\Utils\Collection as BaseCollection;
class ElasticsearchEngine extends Engine
{
/**
* Index where the models will be saved.
* Elastic server version.
*
* @var string
*/
public static $version;
/**
* Index where the models will be saved.
*
* @var null|string
*/
protected $index;
/**
@ -37,13 +44,13 @@ class ElasticsearchEngine extends Engine
/**
* Create a new engine instance.
*
* @param $index
*/
public function __construct(Client $client, $index)
public function __construct(Client $client, ?string $index = null)
{
$this->elastic = $client;
$this->index = $index;
if ($index) {
$this->index = $this->initIndex($client, $index);
}
}
/**
@ -55,13 +62,19 @@ class ElasticsearchEngine extends Engine
{
$params['body'] = [];
$models->each(function ($model) use (&$params) {
$params['body'][] = [
'update' => [
if ($this->index) {
$update = [
'_id' => $model->getKey(),
'_index' => $this->index,
'_type' => $model->searchableAs(),
],
];
];
} else {
$update = [
'_id' => $model->getKey(),
'_index' => $model->searchableAs(),
];
}
$params['body'][] = ['update' => $update];
$params['body'][] = [
'doc' => $model->toSearchableArray(),
'doc_as_upsert' => true,
@ -79,13 +92,19 @@ class ElasticsearchEngine extends Engine
{
$params['body'] = [];
$models->each(function ($model) use (&$params) {
$params['body'][] = [
'delete' => [
if ($this->index) {
$delete = [
'_id' => $model->getKey(),
'_index' => $this->index,
'_type' => $model->searchableAs(),
],
];
];
} else {
$delete = [
'_id' => $model->getKey(),
'_index' => $model->searchableAs(),
];
}
$params['body'][] = ['delete' => $delete];
});
$this->elastic->bulk($params);
}
@ -171,6 +190,24 @@ class ElasticsearchEngine extends Engine
->unsearchable();
}
protected function initIndex(Client $client, string $index): ?string
{
if (! static::$version) {
try {
static::$version = $client->info()['version']['number'];
} catch (\Throwable $exception) {
static::$version = '0.0.0';
}
}
// When the version of elasticsearch is more than 7.0.0, it does not support type, so set `null` to `$index`.
if (version_compare(static::$version, '7.0.0', '<')) {
return $index;
}
return null;
}
/**
* Perform the given search on the engine.
*
@ -189,6 +226,10 @@ class ElasticsearchEngine extends Engine
],
],
];
if (! $this->index) {
unset($params['type']);
$params['index'] = $builder->index ?: $builder->model->searchableAs();
}
if ($sort = $this->sort($builder)) {
$params['body']['sort'] = $sort;
}

View File

@ -56,13 +56,9 @@ class ServerConfig implements Arrayable
->setCallbacks($config['callbacks'] ?? []);
}
public function __set($name, $value): self
public function __set($name, $value)
{
if (! $this->isAvailableProperty($name)) {
throw new \InvalidArgumentException(sprintf('Invalid property %s', $name));
}
$this->config[$name] = $value;
return $this;
$this->set($name, $value);
}
public function __get($name)
@ -81,7 +77,7 @@ class ServerConfig implements Arrayable
if (! $this->isAvailableProperty($propertyName)) {
throw new \InvalidArgumentException(sprintf('Invalid property %s', $propertyName));
}
return $prefix === 'set' ? $this->__set($propertyName, ...$arguments) : $this->__get($propertyName);
return $prefix === 'set' ? $this->set($propertyName, ...$arguments) : $this->__get($propertyName);
}
}
@ -96,6 +92,15 @@ class ServerConfig implements Arrayable
return $this->config;
}
protected function set($name, $value): self
{
if (! $this->isAvailableProperty($name)) {
throw new \InvalidArgumentException(sprintf('Invalid property %s', $name));
}
$this->config[$name] = $value;
return $this;
}
private function isAvailableProperty(string $name)
{
return in_array($name, [

View File

@ -22,10 +22,19 @@ class ConsulAgentFactory
{
return new Agent(function () use ($container) {
$config = $container->get(ConfigInterface::class);
return $container->get(ClientFactory::class)->create([
$token = $config->get('consul.token', '');
$options = [
'timeout' => 2,
'base_uri' => $config->get('consul.uri', Agent::DEFAULT_URI),
]);
];
if (! empty($token)) {
$options['headers'] = [
'X-Consul-Token' => $token,
];
}
return $container->get(ClientFactory::class)->create($options);
});
}
}

View File

@ -19,5 +19,6 @@ return [
'gc_maxlifetime' => 1200,
'session_name' => 'HYPERF_SESSION_ID',
'domain' => null,
'cookie_lifetime' => 5 * 60 * 60,
],
];

View File

@ -95,7 +95,8 @@ class SessionMiddleware implements MiddlewareInterface
if ($this->config->get('session.options.expire_on_close')) {
$expirationDate = 0;
} else {
$expirationDate = Carbon::now()->addMinutes(5 * 60)->getTimestamp();
$expireSeconds = $this->config->get('session.options.cookie_lifetime', 5 * 60 * 60);
$expirationDate = Carbon::now()->addSeconds($expireSeconds)->getTimestamp();
}
return $expirationDate;
}

View File

@ -64,6 +64,7 @@ class SessionMiddlewareTest extends TestCase
$config->shouldReceive('get')->with('session.options.expire_on_close')->andReturn(1);
$config->shouldReceive('get')->with('session.options.session_name', 'HYPERF_SESSION_ID')->andReturn('HYPERF_SESSION_ID');
$config->shouldReceive('get')->with('session.options.domain')->andReturn(null);
$config->shouldReceive('get')->with('session.options.cookie_lifetime', 5 * 60)->andReturn(5 * 60);
$sessionManager = new SessionManager($container, $config);
$middleware = new SessionMiddleware($sessionManager, $config);
@ -109,6 +110,7 @@ class SessionMiddlewareTest extends TestCase
$config->shouldReceive('get')->with('session.options.expire_on_close')->andReturn(0);
$config->shouldReceive('get')->with('session.options.session_name', 'HYPERF_SESSION_ID')->andReturn('HYPERF_SESSION_ID');
$config->shouldReceive('get')->with('session.options.domain')->andReturn(null);
$config->shouldReceive('get')->with('session.options.cookie_lifetime', 5 * 60 * 60)->andReturn(10 * 60 * 60);
$sessionManager = new SessionManager($container, $config);
$middleware = new SessionMiddleware($sessionManager, $config);
@ -119,7 +121,7 @@ class SessionMiddlewareTest extends TestCase
/** @var Cookie $cookie */
$cookie = $response->getCookies()['']['/'][$session->getName()];
$this->assertSame($time + (5 * 60 * 60), $cookie->getExpiresTime());
$this->assertSame($time + (10 * 60 * 60), $cookie->getExpiresTime());
}
public function testSessionOptionsDomain()
@ -135,6 +137,7 @@ class SessionMiddlewareTest extends TestCase
'path' => BASE_PATH . '/runtime/session',
'gc_maxlifetime' => 1200,
'session_name' => 'HYPERF_SESSION_ID',
'cookie_lifetime' => 5 * 60 * 60,
],
],
]);
@ -160,6 +163,7 @@ class SessionMiddlewareTest extends TestCase
'gc_maxlifetime' => 1200,
'session_name' => 'HYPERF_SESSION_ID',
'domain' => null,
'cookie_lifetime' => 5 * 60 * 60,
],
],
]);
@ -185,6 +189,7 @@ class SessionMiddlewareTest extends TestCase
'gc_maxlifetime' => 1200,
'session_name' => 'HYPERF_SESSION_ID',
'domain' => 'hyperf.wiki',
'cookie_lifetime' => 5 * 60 * 60,
],
],
]);
@ -235,6 +240,7 @@ class SessionMiddlewareTest extends TestCase
$config = Mockery::mock(ConfigInterface::class);
$config->shouldReceive('get')->with('session.options.expire_on_close')->andReturn(0);
$config->shouldReceive('get')->with('session.options.domain')->andReturn(null);
$config->shouldReceive('get')->with('session.options.cookie_lifetime', 5 * 60 * 60)->andReturn(5 * 60);
$middleware = new SessionMiddleware(Mockery::mock(SessionManager::class), $config);
$ref = new ReflectionClass($middleware);
$method = $ref->getMethod('addCookieToResponse');

View File

@ -91,11 +91,15 @@ class HttpClientAspect implements AroundInterface
);
$options['headers'] = array_replace($options['headers'] ?? [], $appendHeaders);
$proceedingJoinPoint->arguments['keys']['options'] = $options;
$result = $proceedingJoinPoint->process();
if ($result instanceof ResponseInterface) {
$span->setTag($this->spanTagManager->get('http_client', 'http.status_code'), $result->getStatusCode());
try {
$result = $proceedingJoinPoint->process();
if ($result instanceof ResponseInterface) {
$span->setTag($this->spanTagManager->get('http_client', 'http.status_code'), $result->getStatusCode());
}
} finally {
$span->finish();
}
$span->finish();
return $result;
}
}

View File

@ -90,13 +90,16 @@ class JsonRpcAspect implements AroundInterface
}
if ($proceedingJoinPoint->methodName === 'send') {
$result = $proceedingJoinPoint->process();
/** @var Span $span */
if ($span = CT::get('tracer.span.' . static::class)) {
if ($this->spanTagManager->has('rpc', 'status')) {
$span->setTag($this->spanTagManager->get('rpc', 'status'), isset($result['result']) ? 'OK' : 'Failed');
try {
$result = $proceedingJoinPoint->process();
} finally {
/** @var Span $span */
if ($span = CT::get('tracer.span.' . static::class)) {
if ($this->spanTagManager->has('rpc', 'status')) {
$span->setTag($this->spanTagManager->get('rpc', 'status'), isset($result['result']) ? 'OK' : 'Failed');
}
$span->finish();
}
$span->finish();
}
return $result;

View File

@ -58,8 +58,11 @@ class MethodAspect extends AbstractAspect
$key = $proceedingJoinPoint->className . '::' . $proceedingJoinPoint->methodName;
$span = $this->startSpan($key);
$result = $proceedingJoinPoint->process();
$span->finish();
try {
$result = $proceedingJoinPoint->process();
} finally {
$span->finish();
}
return $result;
}
}

View File

@ -74,9 +74,12 @@ class RedisAspect implements AroundInterface
$arguments = $proceedingJoinPoint->arguments['keys'];
$span = $this->startSpan('Redis' . '::' . $arguments['name']);
$span->setTag($this->spanTagManager->get('redis', 'arguments'), json_encode($arguments['arguments']));
$result = $proceedingJoinPoint->process();
$span->setTag($this->spanTagManager->get('redis', 'result'), json_encode($result));
$span->finish();
try {
$result = $proceedingJoinPoint->process();
$span->setTag($this->spanTagManager->get('redis', 'result'), json_encode($result));
} finally {
$span->finish();
}
return $result;
}
}

View File

@ -58,8 +58,11 @@ class TraceAnnotationAspect implements AroundInterface
}
$span = $this->startSpan($name);
$span->setTag($tag, $source);
$result = $proceedingJoinPoint->process();
$span->finish();
try {
$result = $proceedingJoinPoint->process();
} finally {
$span->finish();
}
return $result;
}
}

View File

@ -36,7 +36,12 @@ trait SpanStarter
/** @var ServerRequestInterface $request */
$request = Context::get(ServerRequestInterface::class);
if (! $request instanceof ServerRequestInterface) {
throw new \RuntimeException('ServerRequest object missing.');
// If the request object is absent, we are probably in a commandline context.
// Throwing an exception is unnecessary.
$root = $this->tracer->startSpan($name, $option);
$root->setTag(SPAN_KIND, $kind);
Context::set('tracer.root', $root);
return $root;
}
$carrier = array_map(function ($header) {
return $header[0];

View File

@ -13,7 +13,7 @@ namespace Hyperf\Utils\Codec;
use Hyperf\Utils\Contracts\Arrayable;
use Hyperf\Utils\Contracts\Jsonable;
use InvalidArgumentException;
use Hyperf\Utils\Exception\InvalidArgumentException;
class Json
{

View File

@ -13,6 +13,7 @@ namespace HyperfTest\Utils;
use HyperfTest\Utils\Exception\RetryException;
use PHPUnit\Framework\TestCase;
use Swoole\Coroutine\Channel;
/**
* @internal
@ -116,4 +117,34 @@ class FunctionTest extends TestCase
{
$this->assertSame(SWOOLE_HOOK_ALL, swoole_hook_flags());
}
public function testDefer()
{
$channel = new Channel(10);
parallel([function () use ($channel) {
defer(function () use ($channel) {
$channel->push(0);
});
defer(function () use ($channel) {
$channel->push(1);
defer(function () use ($channel) {
$channel->push(2);
});
defer(function () use ($channel) {
$channel->push(3);
});
});
defer(function () use ($channel) {
$channel->push(4);
});
$channel->push(5);
}]);
$this->assertSame(5, $channel->pop(0.001));
$this->assertSame(4, $channel->pop(0.001));
$this->assertSame(1, $channel->pop(0.001));
$this->assertSame(3, $channel->pop(0.001));
$this->assertSame(2, $channel->pop(0.001));
$this->assertSame(0, $channel->pop(0.001));
}
}

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace HyperfTest\Utils;
use Hyperf\Utils\Codec\Json;
use Hyperf\Utils\Exception\InvalidArgumentException;
use PHPUnit\Framework\TestCase;
/**
@ -55,4 +56,10 @@ class JsonTest extends TestCase
$this->assertSame([1, 2, 3], $result);
}
public function testJsonFailed()
{
$this->expectException(InvalidArgumentException::class);
Json::decode('{"hype');
}
}

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace HyperfTest\Utils;
use Hyperf\Utils\Codec\Xml;
use Hyperf\Utils\Exception\InvalidArgumentException;
use PHPUnit\Framework\TestCase;
/**
@ -51,4 +52,10 @@ class XmlTest extends TestCase
];
$this->assertSame(Xml::toXml(Xml::toArray($xml), null, 'xml'), Xml::toXml($data, null, 'xml'));
}
public function testXmlFailed()
{
$this->expectException(InvalidArgumentException::class);
Xml::toArray('{"hype');
}
}

View File

@ -9,12 +9,12 @@ declare(strict_types=1);
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
use Hyperf\View\Engine\BladeEngine;
use Hyperf\View\Mode;
use Hyperf\ViewEngine\HyperfViewEngine;
return [
'engine' => BladeEngine::class,
'mode' => Mode::TASK,
'engine' => HyperfViewEngine::class,
'mode' => Mode::SYNC,
'config' => [
'view_path' => BASE_PATH . '/storage/view/',
'cache_path' => BASE_PATH . '/runtime/view/',