Merge remote-tracking branch 'upstream/master' into 1.1-graphql

This commit is contained in:
李铭昕 2019-11-11 09:57:33 +08:00
commit d68987c44d
55 changed files with 2062 additions and 59 deletions

View File

@ -38,6 +38,6 @@ before_script:
- composer config -g process-timeout 900 && composer update
script:
- composer analyse src/di src/json-rpc src/tracer src/metric src/redis src/nats
- composer analyse src/di src/json-rpc src/tracer src/metric src/redis src/nats src/db
- composer test -- --exclude-group NonCoroutine
- vendor/bin/phpunit --group NonCoroutine

View File

@ -1,9 +1,18 @@
# v1.1.6 - TBD
## Added
- [#827](https://github.com/hyperf/hyperf/pull/827) Added a simple db component.
- [#905](https://github.com/hyperf/hyperf/pull/905) Added twig template engine for view.
- [#913](https://github.com/hyperf/hyperf/pull/913) Added `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`.
## Fixed
- [#897](https://github.com/hyperf/hyperf/pull/897) Fixed `pool` for `Hyperf\Nats\Annotation\Consumer` does not works.
- [#897](https://github.com/hyperf/hyperf/pull/897) Fixed connection pool of `Hyperf\Nats\Annotation\Consumer` does not works as expected.
- [#903](https://github.com/hyperf/hyperf/pull/903) Fixed execute `init-proxy` command can not stop when `hyperf/rpc-client` component exists.
- [#904](https://github.com/hyperf/hyperf/pull/904) Fixed the hooked I/O request does not works in the listener that listening `Hyperf\Framework\Event\BeforeMainServerStart` event.
- [#906](https://github.com/hyperf/hyperf/pull/906) Fixed `port` property of URI of `Hyperf\HttpMessage\Server\Request`.
.
# v1.1.5 - 2019-11-07
## Added

View File

@ -8,23 +8,23 @@ English | [中文](./README-CN.md)
# Introduction
Hyperf is a high-performance, highly flexible PHP CLI framework based on `Swoole 4.4+`. It has a built-in coroutine server with a large number of commonly used components. It provides ultra-high and better performance than the traditional PHP-FPM-based framework and also maintains extremely flexible scalability at the same time. Standard components are implemented in the latest PSR standards, and a powerful dependency injection design ensures that most components or classes within the framework are replaceable.
Hyperf is an extremely performant and flexible PHP CLI framework based on `Swoole 4.4+`, powered by the state-of-the-art coroutine server and a large number of battle-tested components. Aside from the decisive benchmark outmatching against PHP-FPM frameworks, Hyperf also distinct itself by its focus on flexibility and composability. Hyperf ships with an AOP-enabling dependency injector to ensure components and classes are pluggable and meta programmable. All of its core components strictly follow the PSR standards and thus can be used in other frameworks.
In addition to providing common coroutine clients such as `MySQL coroutine client` and `Redis coroutine client`, the Hyperf component libraries are also prepared for the coroutine version of `Eloquent ORM`, `WebSocket server and client`, `JSON RPC server and client`, `gRPC server and client`, `Zipkin/Jaeger (OpenTracing) client`, `Guzzle HTTP client`, `Elasticsearch client`, `Consul client`, `ETCD client`, `AMQP component`, `Apollo configuration center`, `Aliyun ACM`, `ETCD configuration center`, `Token bucket algorithm-based limiter`, `Universal connection pool`, `Circuit breaker`, `Swagger`, `Swoole Tracker`, `View engine (Blake/Smarty)`, `Snowflake` etc. Therefore, the trouble of implementing the corresponding coroutine version client by yourself can be avoided. Hyperf also provides convenient functions such as `Dependency injection`, `Annotation`, `AOP (aspect-oriented programming)`, `Middleware`, `Custom Processes`, `Event Manager`, `Simply Redis MQ`, ` RabbitMQ`, `Automatic model cache`, `Simple Cache`, `Seconds level crontab`, `Translation`, `Validation` to meet a wide range of technical and business scenarios.
Hyperf's architecture is built upon the combination of `Coroutine`, `Dependency injection`, `Events`, `Annotation`, `AOP (aspect-oriented programming)`. Core components provided by Hyperf can be used out of the box in coroutine context. The set includes but not limited to: `MySQL coroutine client`, `Redis coroutine client`, `WebSocket server and client`, `JSON RPC server and client`, `gRPC server and client`, `Zipkin/Jaeger (OpenTracing) client`, `Guzzle HTTP client`, `Elasticsearch client`, `Consul client`, `ETCD client`, `AMQP component`, `Apollo configuration center`, `Aliyun ACM`, `ETCD configuration center`, `Token bucket algorithm-based limiter`, `Universal connection pool`, `Circuit breaker`, `Swagger`, `Swoole Tracker`, `Snowflake`, `Simply Redis MQ`, ` RabbitMQ`, `Seconds level crontab`, `Custom Processes`, etc. Be assured Hyperf is still a PHP framework. You will also find familiar packages such as `Middleware`, `Event Manager`, `Coroutine optimized Eloquent ORM` (And Model Cache!), `Translation`, `Validation`, `View engine (Blake/Smarty)` and more at your command.
# Original intention
# Origin
Although many new PHP frameworks have appeared, we still haven't seen a comprehensive framework, which introduces an elegant design and ultra-high performance, suitable for PHP microservices and as an evangelist of PHP microservices. For the original intention of Hyperf and its team members, we will continue to invest in it, and you are welcome to join us to participate in open source development.
Many new PHP frameworks had emerged over the years, yet we were still waiting for one that unites the ultra-performance and elegant design, as well as paving the way for PHP microservice. Hence Hyperf was born to be the pioneer. With this vision in mind, we will continue to invest in it, and you are welcome to join us to participate in open source development.
# Design concept
# Design Goals
`Hyperspeed + Flexibility = Hyperf`, from the framework name we have been used `hyperfspeed (ultra-high performance)` and `flexibility` as the gene of Hyperf.
`Hyperspeed + Flexibility = Hyperf`. The equation hidden in the name exhibits Hyperf's genetic ambition.
For ultra-high performance, Hyperf based on the Swoole coroutine, it providered an amazing performance, Hyperf team also makes a lots of code optimizations on the framework design to ensure ultra-high performance.
Hyperspeed: Leveraging Swoole coroutine, Hyperf is capable of handling massive traffic. Hyperf team made many optimizations throughout the framework to eliminate every obstacle between the end-user and the roaring engine.
For flexibility, based on the powerful dependency injection component of Hyperf, all components are based on [PSR](https://www.php-fig.org/psr) and the contracts that defined by Hyperf, so that most of the components or classes within the framework are replaceable and re-useable.
Flexibility: Hyperf believes its Dependency Injection component is best in class. With the help of Hyperf DI, components and class are all pluggable and meta programmable. On the other hand, All Hyperf components are mean to be shared interchangeably with the world, as they all follow [PSR](https://www.php-fig.org/psr) or open contracts.
Based on the above characteristics, Hyperf has a lots of possibilities, such as implementing Web servers, gateway servers, distributed middleware software, microservices architecture, game servers, and Internet of Things (IoT).
Thanks to these metrits, Hyperf has enabled untapped potential in many areas, such as implementing Web servers, gateway servers, distributed middleware software, microservices architecture, game servers, and Internet of Things (IoT).
# Documentation

View File

@ -46,18 +46,20 @@
},
"require-dev": {
"doctrine/common": "@stable",
"domnikl/statsd": "^3.0.1",
"friendsofphp/php-cs-fixer": "^2.14",
"influxdb/influxdb-php": "^1.15.0",
"jonahgeorge/jaeger-client-php": "^0.4.4",
"malukenho/docheader": "^0.1.6",
"mockery/mockery": "^1.0",
"php-di/php-di": "^6.0",
"phpstan/phpstan": "^0.11.15",
"phpunit/phpunit": "^7.0.0",
"smarty/smarty": "^3.1",
"swoft/swoole-ide-helper": "dev-master",
"symfony/property-access": "^4.3",
"symfony/serializer": "^4.3",
"influxdb/influxdb-php": "^1.15.0",
"domnikl/statsd": "^3.0.1"
"twig/twig": "^2.12"
},
"replace": {
"hyperf/amqp": "self.version",
@ -133,6 +135,7 @@
"Hyperf\\Consul\\": "src/consul/src/",
"Hyperf\\Contract\\": "src/contract/src/",
"Hyperf\\Crontab\\": "src/crontab/src/",
"Hyperf\\DB\\": "src/db/src/",
"Hyperf\\Database\\": "src/database/src/",
"Hyperf\\DbConnection\\": "src/db-connection/src/",
"Hyperf\\Devtool\\": "src/devtool/src/",
@ -200,6 +203,7 @@
"HyperfTest\\Constants\\": "src/constants/tests/",
"HyperfTest\\Consul\\": "src/consul/tests/",
"HyperfTest\\Crontab\\": "src/crontab/tests/",
"HyperfTest\\DB\\": "src/db/tests/",
"HyperfTest\\Database\\": "src/database/tests/",
"HyperfTest\\DbConnection\\": "src/db-connection/tests/",
"HyperfTest\\Di\\": "src/di/tests/",
@ -235,6 +239,7 @@
"HyperfTest\\Translation\\": "src/translation/tests/",
"HyperfTest\\Utils\\": "src/utils/tests/",
"HyperfTest\\Validation\\": "src/validation/tests/",
"HyperfTest\\View\\": "src/view/tests/",
"HyperfTest\\WebSocketClient\\": "src/websocket-client/tests/"
}
},
@ -256,6 +261,7 @@
"Hyperf\\Constants\\ConfigProvider",
"Hyperf\\Consul\\ConfigProvider",
"Hyperf\\Crontab\\ConfigProvider",
"Hyperf\\DB\\ConfigProvider",
"Hyperf\\DbConnection\\ConfigProvider",
"Hyperf\\Devtool\\ConfigProvider",
"Hyperf\\Di\\ConfigProvider",

69
doc/zh/db/db.md Normal file
View File

@ -0,0 +1,69 @@
# 极简的DB组件
[hyperf/database](https://github.com/hyperf/database) 功能十分强大,但也不可否认效率上确实些许不足。这里提供一个极简的 `DB` 组件,支持 `PDO``Swoole Mysql`
> 压测对比 database 1800qpsdb 6800qps。
## 组件配置
默认配置 `autoload/db.php` 如下,数据库支持多库配置,默认为 `default`
| 配置项 | 类型 | 默认值 | 备注 |
|:--------------------:|:------:|:------------------:|:--------------------------------:|
| driver | string | 无 | 数据库引擎 支持 `pdo``mysql` |
| host | string | `localhost` | 数据库地址 |
| port | int | 3306 | 数据库地址 |
| database | string | 无 | 数据库默认DB |
| username | string | 无 | 数据库用户名 |
| password | string | null | 数据库密码 |
| charset | string | utf8 | 数据库编码 |
| collation | string | utf8_unicode_ci | 数据库编码 |
| fetch_mode | int | `PDO::FETCH_ASSOC` | PDO查询结果集类型 |
| pool.min_connections | int | 1 | 连接池内最少连接数 |
| pool.max_connections | int | 10 | 连接池内最大连接数 |
| pool.connect_timeout | float | 10.0 | 连接等待超时时间 |
| pool.wait_timeout | float | 3.0 | 超时时间 |
| pool.heartbeat | int | -1 | 心跳 |
| pool.max_idle_time | float | 60.0 | 最大闲置时间 |
| options | array | | PDO 配置 |
## 组件支持的方法
具体接口可以查看 `Hyperf\DB\ConnectionInterface`
| 方法名 | 返回值类型 | 备注 |
|:----------------:|:----------:|:--------------------------------------:|
| beginTransaction | void | 开启事务 支持事务嵌套 |
| commit | void | 提交事务 支持事务嵌套 |
| rollBack | void | 回滚事务 支持事务嵌套 |
| insert | int | 插入数据返回主键ID非自增主键返回 0 |
| execute | int | 执行SQL返回受影响的行数 |
| query | array | 查询SQL |
| fetch | array | object|查询SQL返回首行数据 |
## 使用
### 使用DB实例
```php
<?php
use Hyperf\Utils\ApplicationContext;
use Hyperf\DB\DB;
$db = ApplicationContext::getContainer()->get(DB::class);
$res = $db->query('SELECT * FROM `user` WHERE gender = ?;',[1]);
```
### 使用静态方法
```php
<?php
use Hyperf\DB\DB;
$res = DB::query('SELECT * FROM `user` WHERE gender = ?;',[1]);
```

View File

@ -105,3 +105,43 @@ class IndexController extends Controller
```
在上面这个例子,我们先假设 `FooException` 是存在的一个异常,以及假设已经完成了该处理器的配置,那么当业务抛出一个没有被捕获处理的异常时,就会根据配置的顺序依次传递,整一个处理流程可以理解为一个管道,若前一个异常处理器调用 `$this->stopPropagation()` 则不再往后传递,若最后一个配置的异常处理器仍不对该异常进行捕获处理,那么就会交由 Hyperf 的默认异常处理器处理了。
## Error 监听器
框架提供了 `error_reporting()` 错误级别的监听器 `Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler`
### 配置
`config/autoload/listeners.php` 中添加监听器
```php
<?php
return [
\Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler::class
];
```
则当出现类似以下的代码时会抛出 `\ErrorException` 异常
```php
<?php
try {
$a = [];
var_dump($a[1]);
} catch (\Throwable $throwable) {
var_dump(get_class($throwable), $throwable->getMessage());
}
// string(14) "ErrorException"
// string(19) "Undefined offset: 1"
```
如果不配置监听器则如下,且不会抛出异常。
```
PHP Notice: Undefined offset: 1 in IndexController.php on line 24
Notice: Undefined offset: 1 in IndexController.php on line 24
NULL
```

View File

@ -49,6 +49,7 @@
* [模型事件](zh/db/event.md)
* [模型缓存](zh/db/model-cache.md)
* [数据库迁移](zh/db/migration.md)
* [极简的DB组件](zh/db/db.md)
* 微服务

View File

@ -201,3 +201,202 @@ class UserTest extends HttpTestCase
```
composer test -- --filter=testUserDaoFirst
```
## 测试替身
Gerard Meszaros 在 Meszaros2007 中介绍了测试替身的概念:
有时候对被测系统(SUT)进行测试是很困难的,因为它依赖于其他无法在测试环境中使用的组件。这有可能是因为这些组件不可用,它们不会返回测试所需要的结果,或者执行它们会有不良副作用。在其他情况下,我们的测试策略要求对被测系统的内部行为有更多控制或更多可见性。
如果在编写测试时无法使用(或选择不使用)实际的依赖组件(DOC),可以用测试替身来代替。测试替身不需要和真正的依赖组件有完全一样的的行为方式;他只需要提供和真正的组件同样的 API 即可,这样被测系统就会以为它是真正的组件!
下面展示分别通过构造函数注入依赖、通过inject注释注入依赖的测试替身
### 构造函数注入依赖的测试替身
```
<?php
namespace App\Logic;
use App\Api\DemoApi;
class DemoLogic
{
/**
* @var DemoApi $demoApi
*/
private $demoApi;
public function __construct(DemoApi $demoApi)
{
$this->demoApi = $demoApi;
}
public function test()
{
$result = $this->demoApi->test();
return $result;
}
}
```
```
<?php
namespace App\Api;
class DemoApi
{
public function test()
{
return [
'status' => 1
];
}
}
```
```
<?php
namespace HyperfTest\Cases;
use App\Api\DemoApi;
use App\Logic\DemoLogic;
use Hyperf\Di\Container;
use HyperfTest\HttpTestCase;
use Mockery;
class DemoLogicTest extends HttpTestCase
{
public function tearDown()
{
Mockery::close();
}
public function testIndex()
{
$res = $this->getContainer()->get(DemoLogic::class)->test();
$this->assertEquals(1, $res['status']);
}
/**
* @return Container
*/
protected function getContainer()
{
$container = Mockery::mock(Container::class);
$apiStub = $this->createMock(DemoApi::class);
$apiStub->method('test')->willReturn([
'status' => 1,
]);
$container->shouldReceive('get')->with(DemoLogic::class)->andReturn(new DemoLogic($apiStub));
return $container;
}
}
```
### 通过inject注释注入依赖的测试替身
```
<?php
namespace App\Logic;
use App\Api\DemoApi;
use Hyperf\Di\Annotation\Inject;
class DemoLogic
{
/**
* @var DemoApi $demoApi
* @Inject()
*/
private $demoApi;
public function test()
{
$result = $this->demoApi->test();
return $result;
}
}
```
```
<?php
namespace App\Api;
class DemoApi
{
public function test()
{
return [
'status' => 1
];
}
}
```
```
<?php
namespace HyperfTest\Cases;
use App\Api\DemoApi;
use App\Logic\DemoLogic;
use Hyperf\Di\Container;
use Hyperf\Utils\ApplicationContext;
use HyperfTest\HttpTestCase;
use Mockery;
class DemoLogicTest extends HttpTestCase
{
public function tearDown()
{
Mockery::close();
}
public function testIndex()
{
$this->getContainer();
$res = $this->getContainer()->get(DemoLogic::class)->test();
$this->assertEquals(11, $res['status']);
}
/**
* @return Container
*/
protected function getContainer()
{
$container = ApplicationContext::getContainer();
$apiStub = $this->createMock(DemoApi::class);
$apiStub->method('test')->willReturn([
'status' => 11
]);
$container->getDefinitionSource()->addDefinition(DemoApi::class, function () use ($apiStub) {
return $apiStub;
});
return $container;
}
}
```

View File

@ -64,7 +64,7 @@ return [
## 视图渲染引擎
官方目前支持 `Blade` `Smarty`种模板,默认安装 [hyperf/view](https://github.com/hyperf/view) 时不会自动安装任何模板引擎,需要您根据自身需求,自行安装对应的模板引擎,使用前必须安装任一模板引擎。
官方目前支持 `Blade` `Smarty` 和 `Twig`种模板,默认安装 [hyperf/view](https://github.com/hyperf/view) 时不会自动安装任何模板引擎,需要您根据自身需求,自行安装对应的模板引擎,使用前必须安装任一模板引擎。
### 安装 Blade 引擎
@ -78,6 +78,12 @@ composer require duncan3dc/blade
composer require smarty/smarty
```
### 安装 Twig 引擎
```bash
composer require twig/twig
```
### 接入其他模板
假设我们想要接入一个虚拟的模板引擎名为 `TemplateEngine`,那么我们需要在任意地方创建对应的 `TemplateEngine` 类,并实现 `Hyperf\View\Engine\EngineInterface` 接口。

View File

@ -17,6 +17,7 @@
<directory suffix="Test.php">./src/constants/tests</directory>
<directory suffix="Test.php">./src/consul/tests</directory>
<directory suffix="Test.php">./src/database/tests</directory>
<directory suffix="Test.php">./src/db/tests</directory>
<directory suffix="Test.php">./src/db-connection/tests</directory>
<directory suffix="Test.php">./src/di/tests</directory>
<directory suffix="Test.php">./src/dispatcher/tests</directory>

View File

@ -14,6 +14,7 @@ return [
'default' => [
'driver' => env('DB_DRIVER', 'mysql'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),

1
src/db/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
/tests export-ignore

21
src/db/LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
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.

49
src/db/composer.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "hyperf/db",
"type": "library",
"license": "MIT",
"keywords": [
"php",
"hyperf"
],
"description": "",
"autoload": {
"psr-4": {
"Hyperf\\DB\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\DB\\": "tests/"
}
},
"require": {
"php": ">=7.2",
"ext-swoole": ">=4.4",
"hyperf/config": "~1.1.0",
"hyperf/contract": "~1.1.0",
"hyperf/pool": "~1.1.0",
"hyperf/utils": "~1.1.0",
"psr/container": "^1.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.14",
"phpstan/phpstan": "^0.10.5",
"hyperf/testing": "1.1.*",
"mockery/mockery": "^1.0",
"swoft/swoole-ide-helper": "dev-master"
},
"config": {
"sort-packages": true
},
"scripts": {
"test": "co-phpunit -c phpunit.xml --colors=always",
"analyze": "phpstan analyse --memory-limit 300M -l 0 ./src",
"cs-fix": "php-cs-fixer fix $1"
},
"extra": {
"hyperf": {
"config": "Hyperf\\DB\\ConfigProvider"
}
}
}

40
src/db/publish/db.php Normal file
View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
return [
'default' => [
'driver' => 'pdo',
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'fetch_mode' => PDO::FETCH_ASSOC,
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60),
],
'options' => [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
];

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Pool\Connection;
use Hyperf\Pool\Exception\ConnectionException;
abstract class AbstractConnection extends Connection implements ConnectionInterface
{
use DetectsLostConnections;
use ManagesTransactions;
/**
* @var array
*/
protected $config = [];
public function getConfig(): array
{
return $this->config;
}
public function release(): void
{
if ($this->transactionLevel() > 0) {
$this->rollBack(0);
if ($this->container->has(StdoutLoggerInterface::class)) {
$logger = $this->container->get(StdoutLoggerInterface::class);
$logger->error('Maybe you\'ve forgotten to commit or rollback the MySQL transaction.');
}
}
$this->pool->release($this);
}
public function getActiveConnection()
{
if ($this->check()) {
return $this;
}
if (! $this->reconnect()) {
throw new ConnectionException('Connection reconnect failed.');
}
return $this;
}
public function retry(\Throwable $throwable, $name, $arguments)
{
if ($this->causedByLostConnection($throwable)) {
try {
$this->reconnect();
return $this->{$name}(...$arguments);
} catch (\Throwable $throwable) {
if ($this->container->has(StdoutLoggerInterface::class)) {
$logger = $this->container->get(StdoutLoggerInterface::class);
$logger->error('Connection execute retry failed. message = ' . $throwable->getMessage());
}
}
}
throw $throwable;
}
}

View File

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

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
interface ConnectionInterface
{
/**
* Start a new database transaction.
*/
public function beginTransaction(): void;
/**
* Commit the active database transaction.
*/
public function commit(): void;
/**
* Rollback the active database transaction.
*/
public function rollBack(?int $toLevel = null): void;
/**
* Run an insert statement against the database.
*
* @return int last insert id
*/
public function insert(string $query, array $bindings = []): int;
/**
* Run an execute statement against the database.
*
* @return int affected rows
*/
public function execute(string $query, array $bindings = []): int;
/**
* Execute an SQL statement and return the number of affected rows.
*
* @return int affected rows
*/
public function exec(string $sql): int;
/**
* Run a select statement against the database.
*/
public function query(string $query, array $bindings = []): array;
/**
* Run a select statement and return a single result.
*/
public function fetch(string $query, array $bindings = []);
public function call(string $method, array $argument = []);
}

118
src/db/src/DB.php Normal file
View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\DB\Pool\PoolFactory;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Context;
use Throwable;
/**
* @method beginTransaction()
* @method commit()
* @method rollback()
* @method insert(string $query, array $bindings = [])
* @method execute(string $query, array $bindings = [])
* @method query(string $query, array $bindings = [])
* @method fetch(string $query, array $bindings = [])
*/
class DB
{
/**
* @var PoolFactory
*/
protected $factory;
/**
* @var string
*/
protected $poolName;
public function __construct(PoolFactory $factory, string $poolName = 'default')
{
$this->factory = $factory;
$this->poolName = $poolName;
}
public function __call($name, $arguments)
{
$hasContextConnection = Context::has($this->getContextKey());
$connection = $this->getConnection($hasContextConnection);
try {
$connection = $connection->getConnection();
$result = $connection->{$name}(...$arguments);
} catch (Throwable $exception) {
$result = $connection->retry($exception, $name, $arguments);
} finally {
if (! $hasContextConnection) {
if ($this->shouldUseSameConnection($name)) {
// Should storage the connection to coroutine context, then use defer() to release the connection.
Context::set($this->getContextKey(), $connection);
defer(function () use ($connection) {
$connection->release();
});
} else {
// Release the connection after command executed.
$connection->release();
}
}
}
return $result;
}
public static function __callStatic($name, $arguments)
{
$container = ApplicationContext::getContainer();
$db = $container->get(static::class);
return $db->{$name}(...$arguments);
}
/**
* Define the commands that needs same connection to execute.
* When these commands executed, the connection will storage to coroutine context.
*/
protected function shouldUseSameConnection(string $methodName): bool
{
return in_array($methodName, [
'beginTransaction',
'commit',
'rollBack',
]);
}
/**
* Get a connection from coroutine context, or from mysql connectio pool.
*/
protected function getConnection(bool $hasContextConnection): AbstractConnection
{
$connection = null;
if ($hasContextConnection) {
$connection = Context::get($this->getContextKey());
}
if (! $connection instanceof AbstractConnection) {
$pool = $this->factory->getPool($this->poolName);
$connection = $pool->get();
}
return $connection;
}
/**
* The key to identify the connection object in coroutine context.
*/
private function getContextKey(): string
{
return sprintf('db.connection.%s', $this->poolName);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\Utils\Str;
use Throwable;
trait DetectsLostConnections
{
/**
* Determine if the given exception was caused by a lost connection.
*/
protected function causedByLostConnection(Throwable $e): bool
{
$message = $e->getMessage();
return Str::contains($message, [
'server has gone away',
'no connection to the server',
'Lost connection',
'is dead or not enabled',
'Error while sending',
'decryption failed or bad record mac',
'server closed the connection unexpectedly',
'SSL connection has been closed unexpectedly',
'Error writing data to the connection',
'Resource deadlock avoided',
'Transaction() on null',
'child connection forced to terminate due to client_idle_limit',
'query_wait_timeout',
'reset by peer',
'Physical connection is not usable',
'TCP Provider: Error code 0x68',
'Name or service not known',
'ORA-03114',
'Packets out of order. Expected',
]);
}
}

View File

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

View File

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

View File

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

19
src/db/src/Frequency.php Normal file
View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\Pool\Frequency as DefaultFrequency;
class Frequency extends DefaultFrequency
{
}

View File

@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Throwable;
trait ManagesTransactions
{
/**
* The number of active transactions.
*
* @var int
*/
protected $transactions = 0;
/**
* Start a new database transaction.
* @throws Throwable
*/
public function beginTransaction(): void
{
$this->createTransaction();
++$this->transactions;
}
/**
* Commit the active database transaction.
*/
public function commit(): void
{
if ($this->transactions == 1) {
$this->call('commit');
}
$this->transactions = max(0, $this->transactions - 1);
}
/**
* Rollback the active database transaction.
*
* @throws Throwable
*/
public function rollBack(?int $toLevel = null): void
{
// We allow developers to rollback to a certain transaction level. We will verify
// that this given transaction level is valid before attempting to rollback to
// that level. If it's not we will just return out and not attempt anything.
$toLevel = is_null($toLevel)
? $this->transactions - 1
: $toLevel;
if ($toLevel < 0 || $toLevel >= $this->transactions) {
return;
}
// Next, we will actually perform this rollback within this database and fire the
// rollback event. We will also set the current transaction level to the given
// level that was passed into this method so it will be right from here out.
try {
$this->performRollBack($toLevel);
} catch (Throwable $e) {
$this->handleRollBackException($e);
}
$this->transactions = $toLevel;
}
/**
* Get the number of active transactions.
*/
public function transactionLevel(): int
{
return $this->transactions;
}
/**
* Create a transaction within the database.
*/
protected function createTransaction(): void
{
if ($this->transactions == 0) {
try {
$this->call('beginTransaction');
} catch (Throwable $e) {
$this->handleBeginTransactionException($e);
}
} elseif ($this->transactions >= 1) {
$this->createSavepoint();
}
}
/**
* Create a save point within the database.
*/
protected function createSavepoint()
{
$this->exec(
$this->compileSavepoint('trans' . ($this->transactions + 1))
);
}
/**
* Handle an exception from a transaction beginning.
*
* @throws Throwable
*/
protected function handleBeginTransactionException(Throwable $e)
{
if ($this->causedByLostConnection($e)) {
$this->reconnect();
$this->call('beginTransaction');
} else {
throw $e;
}
}
/**
* Perform a rollback within the database.
*/
protected function performRollBack(int $toLevel)
{
if ($toLevel == 0) {
$this->call('rollBack');
} else {
$this->exec(
$this->compileSavepointRollBack('trans' . ($toLevel + 1))
);
}
}
/**
* Handle an exception from a rollback.
*
* @throws Throwable
*/
protected function handleRollBackException(Throwable $e)
{
if ($this->causedByLostConnection($e)) {
$this->transactions = 0;
}
throw $e;
}
/**
* Compile the SQL statement to define a savepoint.
*/
protected function compileSavepoint(string $name): string
{
return 'SAVEPOINT ' . $name;
}
/**
* Compile the SQL statement to execute a savepoint rollback.
*/
protected function compileSavepointRollBack(string $name): string
{
return 'ROLLBACK TO SAVEPOINT ' . $name;
}
}

View File

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\DB\Exception\RuntimeException;
use Hyperf\Pool\Pool;
use Psr\Container\ContainerInterface;
use Swoole\Coroutine\MySQL;
use Swoole\Coroutine\MySQL\Statement;
class MySQLConnection extends AbstractConnection
{
/**
* @var MySQL
*/
protected $connection;
/**
* @var array
*/
protected $config = [
'driver' => 'pdo',
'host' => 'localhost',
'port' => 3306,
'database' => 'hyperf',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => 60.0,
],
];
public function __construct(ContainerInterface $container, Pool $pool, array $config)
{
parent::__construct($container, $pool);
$this->config = array_replace_recursive($this->config, $config);
$this->reconnect();
}
public function __call($name, $arguments)
{
return $this->connection->{$name}(...$arguments);
}
/**
* Reconnect the connection.
*/
public function reconnect(): bool
{
$connection = new MySQL();
$connection->connect([
'host' => $this->config['host'],
'port' => $this->config['port'],
'user' => $this->config['username'],
'password' => $this->config['password'],
'database' => $this->config['database'],
'timeout' => $this->config['pool']['connect_timeout'],
'charset' => $this->config['charset'],
'fetch_mode' => true,
]);
$this->connection = $connection;
$this->lastUseTime = microtime(true);
return true;
}
/**
* Close the connection.
*/
public function close(): bool
{
unset($this->connection);
return true;
}
public function insert(string $query, array $bindings = []): int
{
$statement = $this->prepare($query);
$statement->execute($bindings);
return $statement->insert_id;
}
public function execute(string $query, array $bindings = []): int
{
$statement = $this->prepare($query);
$statement->execute($bindings);
return $statement->affected_rows;
}
public function exec(string $sql): int
{
$res = $this->connection->query($sql);
if ($res === false) {
throw new RuntimeException($this->connection->error);
}
return $this->connection->affected_rows;
}
public function query(string $query, array $bindings = []): array
{
// For select statements, we'll simply execute the query and return an array
// of the database result set. Each element in the array will be a single
// row from the database table, and will either be an array or objects.
$statement = $this->prepare($query);
$statement->execute($bindings);
return $statement->fetchAll();
}
public function fetch(string $query, array $bindings = [])
{
$records = $this->query($query, $bindings);
return array_shift($records);
}
public function call(string $method, array $argument = [])
{
$timeout = $this->config['pool']['wait_timeout'];
switch ($method) {
case 'beginTransaction':
return $this->connection->begin($timeout);
case 'rollBack':
return $this->connection->rollback($timeout);
case 'commit':
return $this->connection->commit($timeout);
}
return $this->connection->{$method}(...$argument);
}
protected function prepare(string $query): Statement
{
$statement = $this->connection->prepare($query);
if ($statement === false) {
throw new RuntimeException($this->connection->error);
}
return $statement;
}
}

View File

@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB;
use Hyperf\Pool\Exception\ConnectionException;
use Hyperf\Pool\Pool;
use PDO;
use PDOStatement;
use Psr\Container\ContainerInterface;
class PDOConnection extends AbstractConnection
{
/**
* @var PDO
*/
protected $connection;
/**
* @var array
*/
protected $config = [
'driver' => 'pdo',
'host' => 'localhost',
'port' => 3306,
'database' => 'hyperf',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'fetch_mode' => PDO::FETCH_ASSOC,
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
'connect_timeout' => 10.0,
'wait_timeout' => 3.0,
'heartbeat' => -1,
'max_idle_time' => 60.0,
],
'options' => [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
],
];
/**
* Current mysql database.
* @var null|int
*/
protected $database;
public function __construct(ContainerInterface $container, Pool $pool, array $config)
{
parent::__construct($container, $pool);
$this->config = array_replace_recursive($this->config, $config);
$this->reconnect();
}
public function __call($name, $arguments)
{
return $this->connection->{$name}(...$arguments);
}
/**
* Reconnect the connection.
*/
public function reconnect(): bool
{
$host = $this->config['host'];
$dbName = $this->config['database'];
$username = $this->config['username'];
$password = $this->config['password'];
$dsn = "mysql:host={$host};dbname={$dbName}";
try {
$pdo = new \PDO($dsn, $username, $password, $this->config['options']);
} catch (\Throwable $e) {
throw new ConnectionException('Connection reconnect failed.:' . $e->getMessage());
}
$this->connection = $pdo;
$this->lastUseTime = microtime(true);
return true;
}
/**
* Close the connection.
*/
public function close(): bool
{
unset($this->connection);
return true;
}
public function query(string $query, array $bindings = []): array
{
// For select statements, we'll simply execute the query and return an array
// of the database result set. Each element in the array will be a single
// row from the database table, and will either be an array or objects.
$statement = $this->connection->prepare($query);
$this->bindValues($statement, $bindings);
$statement->execute();
$fetchModel = $this->config['fetch_mode'];
return $statement->fetchAll($fetchModel);
}
public function fetch(string $query, array $bindings = [])
{
$records = $this->query($query, $bindings);
return array_shift($records);
}
public function execute(string $query, array $bindings = []): int
{
$statement = $this->connection->prepare($query);
$this->bindValues($statement, $bindings);
$statement->execute();
return $statement->rowCount();
}
public function exec(string $sql): int
{
return $this->connection->exec($sql);
}
public function insert(string $query, array $bindings = []): int
{
$statement = $this->connection->prepare($query);
$this->bindValues($statement, $bindings);
$statement->execute();
return (int) $this->connection->lastInsertId();
}
public function call(string $method, array $argument = [])
{
return $this->connection->{$method}(...$argument);
}
/**
* Bind values to their parameters in the given statement.
*/
protected function bindValues(PDOStatement $statement, array $bindings): void
{
foreach ($bindings as $key => $value) {
$statement->bindValue(
is_string($key) ? $key : $key + 1,
$value,
is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR
);
}
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Pool;
use Hyperf\Contract\ConnectionInterface;
use Hyperf\DB\MySQLConnection;
class MySQLPool extends Pool
{
protected function createConnection(): ConnectionInterface
{
return new MySQLConnection($this->container, $this, $this->config);
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Pool;
use Hyperf\Contract\ConnectionInterface;
use Hyperf\DB\PDOConnection;
class PDOPool extends Pool
{
protected function createConnection(): ConnectionInterface
{
return new PDOConnection($this->container, $this, $this->config);
}
}

58
src/db/src/Pool/Pool.php Normal file
View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Pool;
use Hyperf\Contract\ConfigInterface;
use Hyperf\DB\Frequency;
use Hyperf\Pool\Pool as HyperfPool;
use Hyperf\Utils\Arr;
use Psr\Container\ContainerInterface;
abstract class Pool extends HyperfPool
{
/**
* @var string
*/
protected $name;
/**
* @var array
*/
protected $config;
public function __construct(ContainerInterface $container, string $name)
{
$config = $container->get(ConfigInterface::class);
$key = sprintf('db.%s', $name);
if (! $config->has($key)) {
throw new \InvalidArgumentException(sprintf('config[%s] is not exist!', $key));
}
$this->name = $name;
$this->config = $config->get($key);
$options = Arr::get($this->config, 'pool', []);
$this->frequency = make(Frequency::class);
parent::__construct($container, $options);
}
public function getName(): string
{
return $this->name;
}
public function getConfig(): array
{
return $this->config;
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\DB\Pool;
use Hyperf\Contract\ConfigInterface;
use Hyperf\DB\Exception\DriverNotFoundException;
use Psr\Container\ContainerInterface;
class PoolFactory
{
/**
* @var Pool[]
*/
protected $pools = [];
/**
* @var ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getPool(string $name)
{
if (isset($this->pools[$name])) {
return $this->pools[$name];
}
$config = $this->container->get(ConfigInterface::class);
$driver = $config->get(sprintf('db.%s.driver', $name), 'pdo');
$class = $this->getPoolName($driver);
return $this->pools[$name] = make($class, [$this->container, $name]);
}
protected function getPoolName(string $driver)
{
switch (strtolower($driver)) {
case 'mysql':
return MySQLPool::class;
case 'pdo':
return PDOPool::class;
}
throw new DriverNotFoundException(sprintf('Driver %s is not found.', $driver));
}
}

View File

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\DB\Cases;
use Hyperf\Config\Config;
use Hyperf\Contract\ConfigInterface;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\DB\DB;
use Hyperf\DB\Frequency;
use Hyperf\DB\Pool\MySQLPool;
use Hyperf\DB\Pool\PDOPool;
use Hyperf\DB\Pool\PoolFactory;
use Hyperf\Di\Container;
use Hyperf\Pool\Channel;
use Hyperf\Pool\PoolOption;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Utils\Context;
use Mockery;
use PHPUnit\Framework\TestCase;
/**
* Class AbstractTestCase.
*/
abstract class AbstractTestCase extends TestCase
{
protected $driver = 'pdo';
protected function tearDown()
{
Mockery::close();
Context::set('db.connection.default', null);
}
protected function getContainer($options = [])
{
$container = Mockery::mock(Container::class);
$container->shouldReceive('get')->with(ConfigInterface::class)->andReturn(new Config([
'db' => [
'default' => [
'driver' => $this->driver,
'password' => '',
'database' => 'hyperf',
'pool' => [
'max_connections' => 20,
],
'options' => $options,
],
],
]));
$container->shouldReceive('make')->with(PDOPool::class, Mockery::any())->andReturnUsing(function ($_, $args) {
return new PDOPool(...array_values($args));
});
$container->shouldReceive('make')->with(MySQLPool::class, Mockery::any())->andReturnUsing(function ($_, $args) {
return new MySQLPool(...array_values($args));
});
$container->shouldReceive('make')->with(Frequency::class, Mockery::any())->andReturn(new Frequency());
$container->shouldReceive('make')->with(PoolOption::class, Mockery::any())->andReturnUsing(function ($_, $args) {
return new PoolOption(...array_values($args));
});
$container->shouldReceive('make')->with(Channel::class, Mockery::any())->andReturnUsing(function ($_, $args) {
return new Channel(...array_values($args));
});
$container->shouldReceive('get')->with(PoolFactory::class)->andReturn($factory = new PoolFactory($container));
$container->shouldReceive('get')->with(DB::class)->andReturn(new DB($factory, 'default'));
$container->shouldReceive('has')->with(StdoutLoggerInterface::class)->andReturn(false);
ApplicationContext::setContainer($container);
return $container;
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\DB\Cases;
/**
* @internal
* @coversNothing
*/
class MySQLDriverTest extends PDODriverTest
{
protected $driver = 'mysql';
public function testFetch()
{
parent::testFetch();
}
public function testQuery()
{
parent::testQuery();
}
public function testInsertAndExecute()
{
parent::testInsertAndExecute();
}
public function testTransaction()
{
parent::testTransaction();
}
public function testConfig()
{
parent::testConfig();
}
public function testMultiTransaction()
{
parent::testMultiTransaction();
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\DB\Cases;
use Hyperf\DB\DB;
use Hyperf\DB\Pool\PoolFactory;
/**
* @internal
* @coversNothing
*/
class PDODriverTest extends AbstractTestCase
{
public function testFetch()
{
$db = $this->getContainer()->get(DB::class);
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [2]);
$this->assertSame('Hyperflex', $res['name']);
}
public function testQuery()
{
$db = $this->getContainer()->get(DB::class);
$res = $db->query('SELECT * FROM `user` WHERE id = ?;', [2]);
$this->assertSame('Hyperflex', $res[0]['name']);
}
public function testInsertAndExecute()
{
$db = $this->getContainer()->get(DB::class);
$id = $db->insert('INSERT INTO `user` (`name`, `gender`) VALUES (?,?);', [$name = uniqid(), $gender = rand(0, 2)]);
$this->assertTrue($id > 0);
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id]);
$this->assertSame($name, $res['name']);
$this->assertSame($gender, $res['gender']);
$res = $db->execute('UPDATE `user` SET `name` = ? WHERE id = ?', [$name = uniqid(), $id]);
$this->assertTrue($res > 0);
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id]);
$this->assertSame($name, $res['name']);
}
public function testTransaction()
{
$db = $this->getContainer()->get(DB::class);
$db->beginTransaction();
$id = $db->insert('INSERT INTO `user` (`name`, `gender`) VALUES (?,?);', [$name = uniqid(), $gender = rand(0, 2)]);
$this->assertTrue($id > 0);
$db->commit();
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id]);
$this->assertSame($name, $res['name']);
$this->assertSame($gender, $res['gender']);
$db->beginTransaction();
$id = $db->insert('INSERT INTO `user` (`name`, `gender`) VALUES (?,?);', [$name = uniqid(), $gender = rand(0, 2)]);
$this->assertTrue($id > 0);
$db->rollBack();
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id]);
$this->assertNull($res);
}
public function testConfig()
{
$factory = $this->getContainer()->get(PoolFactory::class);
$pool = $factory->getPool('default');
$this->assertSame('hyperf', $pool->getConfig()['database']);
$this->assertSame([], $pool->getConfig()['options']);
$connection = $pool->get();
$this->assertSame(6, count($connection->getConfig()['pool']));
$this->assertSame(20, $connection->getConfig()['pool']['max_connections']);
}
public function testMultiTransaction()
{
$db = $this->getContainer()->get(DB::class);
$db->beginTransaction();
$id = $db->insert('INSERT INTO `user` (`name`, `gender`) VALUES (?,?);', [$name = 'trans' . uniqid(), $gender = rand(0, 2)]);
$this->assertTrue($id > 0);
$db->beginTransaction();
$id2 = $db->insert('INSERT INTO `user` (`name`, `gender`) VALUES (?,?);', ['rollback' . uniqid(), rand(0, 2)]);
$this->assertTrue($id2 > 0);
$db->rollBack();
$db->commit();
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id2]);
$this->assertNull($res);
$res = $db->fetch('SELECT * FROM `user` WHERE id = ?;', [$id]);
$this->assertNotNull($res);
}
public function testStaticCall()
{
$this->getContainer();
$res = DB::fetch('SELECT * FROM `user` WHERE id = ?;', [1]);
$this->assertSame('Hyperf', $res['name']);
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
require_once dirname(dirname(__FILE__)) . '/vendor/autoload.php';

View File

@ -17,6 +17,7 @@ use Hyperf\Config\ProviderConfig;
use Hyperf\Di\Annotation\Scanner;
use Hyperf\Di\Container;
use Psr\Container\ContainerInterface;
use Swoole\Timer;
use Symfony\Component\Console\Exception\LogicException;
class InitProxyCommand extends Command
@ -51,6 +52,8 @@ class InitProxyCommand extends Command
$this->createAopProxies();
Timer::clearAll();
$this->output->writeln('<info>Proxy class create success.</info>');
}

View File

@ -54,7 +54,6 @@ class Container implements ContainerInterface
/**
* Container constructor.
* @param Definition\DefinitionSourceInterface $definitionSource
*/
public function __construct(Definition\DefinitionSourceInterface $definitionSource)
{

View File

@ -22,7 +22,6 @@ interface MethodDefinitionCollectorInterface
/**
* Retrieve the metadata for the return value of the method.
* @return ReflectionType
*/
public function getReturnType(string $class, string $method): ReflectionType;
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\ExceptionHandler\Listener;
use ErrorException;
use Hyperf\Event\Contract\ListenerInterface;
use Hyperf\Framework\Event\BootApplication;
class ErrorExceptionHandler implements ListenerInterface
{
public function listen(): array
{
return [
BootApplication::class,
];
}
public function process(object $event)
{
set_error_handler(function ($level, $message, $file = '', $line = 0, $context = []) {
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
});
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\ExceptionHandler;
use Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class ErrorExceptionHandlerTest extends TestCase
{
public function testHandleError()
{
$listener = new ErrorExceptionHandler();
$listener->process((object) []);
$this->expectException(\ErrorException::class);
$this->expectExceptionMessage('Undefined offset: 1');
try {
$array = [];
$array[1];
} finally {
restore_error_handler();
}
}
}

View File

@ -192,7 +192,6 @@ trait MessageTrait
}
/**
* @param array $headers
* @return static
*/
public function withHeaders(array $headers)
@ -341,7 +340,6 @@ trait MessageTrait
}
/**
* @param array $headers
* @return static
*/
private function setHeaders(array $headers)

View File

@ -274,7 +274,6 @@ class CookieJar implements CookieJarInterface
*
* @see https://tools.ietf.org/html/rfc6265#section-5.1.4
*
* @param RequestInterface $request
* @return string
*/
private function getCookiePathFromRequest(RequestInterface $request)
@ -299,8 +298,6 @@ class CookieJar implements CookieJarInterface
/**
* If a cookie already exists and the server asks to set it again with a
* null value, the cookie must be deleted.
*
* @param SetCookie $cookie
*/
private function removeCookieIfEmpty(SetCookie $cookie)
{

View File

@ -66,7 +66,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
/**
* Load a swoole request, and transfer to a swoft request object.
*
* @param \Swoole\Http\Request $swooleRequest
* @return \Hyperf\HttpMessage\Server\Request
*/
public static function loadFromSwooleRequest(\Swoole\Http\Request $swooleRequest)
@ -103,7 +102,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
/**
* Return an instance with the specified server params.
*
* @param array $serverParams
* @return static
*/
public function withServerParams(array $serverParams)
@ -118,8 +116,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
* Retrieves cookies sent by the client to the server.
* The data MUST be compatible with the structure of the $_COOKIE
* superglobal.
*
* @return array
*/
public function getCookieParams(): array
{
@ -154,8 +150,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
* params. If you need to ensure you are only getting the original
* values, you may need to parse the query string from `getUri()->getQuery()`
* or from the `QUERY_STRING` server param.
*
* @return array
*/
public function getQueryParams(): array
{
@ -416,8 +410,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
/**
* Get the full URL for the request.
*
* @return string
*/
public function fullUrl(): string
{
@ -451,16 +443,12 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
return $this->hasHeader('X-Requested-With') == 'XMLHttpRequest';
}
/**
* @return \Swoole\Http\Request
*/
public function getSwooleRequest(): \Swoole\Http\Request
{
return $this->swooleRequest;
}
/**
* @param \Swoole\Http\Request $swooleRequest
* @return $this
*/
public function setSwooleRequest(\Swoole\Http\Request $swooleRequest)
@ -532,7 +520,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
* Loops through all nested files and returns a normalized array of
* UploadedFileInterface instances.
*
* @param array $files
* @return UploadedFileInterface[]
*/
private static function normalizeNestedFileSpec(array $files = [])
@ -555,7 +542,6 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
/**
* Get a Uri populated with values from $swooleRequest->server.
* @param \Swoole\Http\Request $swooleRequest
* @throws \InvalidArgumentException
* @return \Psr\Http\Message\UriInterface
*/
@ -579,11 +565,10 @@ class Request extends \Hyperf\HttpMessage\Base\Request implements ServerRequestI
} elseif (isset($server['server_addr'])) {
$uri = $uri->withHost($server['server_addr']);
} elseif (isset($header['host'])) {
$hasPort = true;
if (\strpos($header['host'], ':')) {
$hasPort = true;
[$host, $port] = explode(':', $header['host'], 2);
if ($port !== '80') {
if ($port != $uri->getDefaultPort()) {
$uri = $uri->withPort($port);
}
} else {

View File

@ -29,9 +29,6 @@ class Response extends \Hyperf\HttpMessage\Base\Response implements Sendable
*/
protected $cookies = [];
/**
* @param null|\Swoole\Http\Response $response
*/
public function __construct(\Swoole\Http\Response $response = null)
{
$this->swooleResponse = $response;

View File

@ -28,8 +28,6 @@ class SwooleStream implements StreamInterface
/**
* SwooleStream constructor.
*
* @param string $contents
*/
public function __construct(string $contents = '')
{

View File

@ -66,13 +66,6 @@ class UploadedFile extends \SplFileInfo implements UploadedFileInterface
*/
private $mimeType;
/**
* @param string $tmpFile
* @param null|int $size
* @param int $errorStatus
* @param null|string $clientFilename
* @param null|string $clientMediaType
*/
public function __construct(
string $tmpFile,
?int $size,
@ -254,9 +247,6 @@ class UploadedFile extends \SplFileInfo implements UploadedFileInterface
return $this->clientMediaType;
}
/**
* @return array
*/
public function toArray(): array
{
return [

View File

@ -545,8 +545,6 @@ class Uri implements UriInterface
* Whether the URI has the default port of the current scheme.
* `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used
* independently of the implementation.
*
* @return bool
*/
public function isDefaultPort(): bool
{
@ -708,7 +706,6 @@ class Uri implements UriInterface
}
/**
* @param array $match
* @return string
*/
private function rawurlencodeMatchZero(array $match)

View File

@ -12,11 +12,14 @@ declare(strict_types=1);
namespace HyperfTest\HttpMessage;
use Hyperf\HttpMessage\Server\Request;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Utils\Codec\Json;
use HyperfTest\HttpMessage\Stub\Server\RequestStub;
use Mockery;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;
use Swoole\Http\Request as SwooleRequest;
/**
* @internal
@ -60,4 +63,24 @@ class ServerRequestTest extends TestCase
$request->shouldReceive('getBody')->andReturn(new SwooleStream(json_encode($json)));
$this->assertSame($json, RequestStub::normalizeParsedBody($data, $request));
}
public function testGetUriFromGlobals()
{
$swooleRequest = Mockery::mock(SwooleRequest::class);
$data = ['name' => 'Hyperf'];
$swooleRequest->shouldReceive('rawContent')->andReturn(Json::encode($data));
$swooleRequest->server = ['server_port' => 9501];
$request = Request::loadFromSwooleRequest($swooleRequest);
$uri = $request->getUri();
$this->assertSame(9501, $uri->getPort());
$swooleRequest = Mockery::mock(SwooleRequest::class);
$data = ['name' => 'Hyperf'];
$swooleRequest->shouldReceive('rawContent')->andReturn(Json::encode($data));
$swooleRequest->header = ['host' => '127.0.0.1'];
$swooleRequest->server = ['server_port' => 9501];
$request = Request::loadFromSwooleRequest($swooleRequest);
$uri = $request->getUri();
$this->assertSame(null, $uri->getPort());
}
}

View File

@ -39,8 +39,6 @@ class StartServer extends Command
protected function execute(InputInterface $input, OutputInterface $output)
{
Runtime::enableCoroutine(true, swoole_hook_flags());
$this->checkEnvironment($output);
$serverFactory = $this->container->get(ServerFactory::class)
@ -53,6 +51,9 @@ class StartServer extends Command
}
$serverFactory->configure($serverConfig);
Runtime::enableCoroutine(true, swoole_hook_flags());
$serverFactory->start();
}

View File

@ -26,6 +26,7 @@
"suggest": {
"hyperf/task": "Required to use task as a view render mode.",
"smarty/smarty": "Required to use smarty as a view render engine.",
"twig/twig": "Required to use twig as a view render engine.",
"duncan3dc/blade": "Required to use blade as a view render engine."
},
"autoload": {
@ -35,6 +36,7 @@
},
"autoload-dev": {
"psr-4": {
"HyperfTest\\View\\": "tests/"
}
},
"config": {

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace Hyperf\View\Engine;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
class TwigEngine implements EngineInterface
{
public function render($template, $data, $config): string
{
$loader = new FilesystemLoader($config['view_path']);
$twig = new Environment($loader, ['cache' => $config['cache_path']]);
return $twig->render($template, $data);
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\View;
use Hyperf\View\Engine\SmartyEngine;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class SmartyTest extends TestCase
{
public function testRender()
{
$config = [
'view_path' => __DIR__ . '/tpl',
'cache_path' => __DIR__ . '/runtime',
];
$engine = new SmartyEngine();
$res = $engine->render('index.tpl', ['name' => 'Hyperf'], $config);
$this->assertEquals('<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hyperf</title>
</head>
<body>
Hello, Hyperf. You are using smarty template now.
</body>
</html>', $res);
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/
namespace HyperfTest\View;
use Hyperf\View\Engine\TwigEngine;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
class TwigTest extends TestCase
{
public function testRender()
{
$config = [
'view_path' => __DIR__ . '/tpl',
'cache_path' => __DIR__ . '/runtime',
];
$engine = new TwigEngine();
$res = $engine->render('index.twig', ['name' => 'Hyperf'], $config);
$this->assertEquals('<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hyperf</title>
</head>
<body>
Hello, Hyperf. You are using smarty template now.
</body>
</html>', $res);
}
}

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hyperf</title>
</head>
<body>
Hello, {$name}. You are using smarty template now.
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hyperf</title>
</head>
<body>
Hello, {{ name }}. You are using smarty template now.
</body>
</html>