Merge pull request #827 from PandaLIU-1111/master

添加一个db的组件,databases配置文件添加一个port配置
This commit is contained in:
李铭昕 2019-11-09 16:20:39 +08:00 committed by GitHub
commit 87c3ecc986
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1551 additions and 1 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,5 +1,9 @@
# v1.1.6 - TBD
## Added
- [#827](https://github.com/hyperf/hyperf/pull/827) Added a simple db component.
## Fixed
- [#897](https://github.com/hyperf/hyperf/pull/897) Fixed `pool` for `Hyperf\Nats\Annotation\Consumer` does not works.

View File

@ -133,6 +133,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 +201,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/",
@ -256,6 +258,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``SwooleMysql`
> 压测对比 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

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

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';