tarantool/client

Tarantool 的 PHP 客户端。

资助包维护!
rybakit

v0.10.1 2024-06-20 22:53 UTC

README

Quality Assurance Scrutinizer Code Quality Code Coverage Telegram

纯 PHP 客户端,用于 Tarantool 1.7.1 或更高版本。

功能

  • 纯 PHP 编写,无需扩展
  • 支持 Unix 域套接字
  • 支持 SQL 协议
  • 支持用户自定义类型(包括十进制和 UUID)
  • 高度可定制
  • 经过彻底测试
  • 在多个项目中使用,包括 QueueMapperWeb Admin 以及 其他

目录

安装

推荐通过 Composer 安装库

composer require tarantool/client

为了使用 Tarantool 2.3 中添加的 Decimal 类型,您还需要安装 decimal 扩展。此外,为了提高与自 Tarantool 2.4 起可用的 UUID 类型一起工作时的性能,建议您还安装 uuid 扩展。

创建客户端

创建客户端的最简单方法是使用默认配置

use Tarantool\Client\Client;

$client = Client::fromDefaults();

客户端将被配置为使用默认流连接选项连接到 127.0.0.13301 端口。还将自动选择最佳可用的 msgpack 包。可以通过以下列出的几种方法之一实现自定义配置。

DSN 字符串

客户端支持以下数据源名称格式

tcp://[[username[:password]@]host[:port][/?option1=value1&optionN=valueN]
unix://[[username[:password]@]path[/?option1=value1&optionN=valueN]

一些示例

use Tarantool\Client\Client;

$client = Client::fromDsn('tcp://127.0.0.1');
$client = Client::fromDsn('tcp://[fe80::1]:3301');
$client = Client::fromDsn('tcp://user:pass@example.com:3301');
$client = Client::fromDsn('tcp://user@example.com/?connect_timeout=5.0&max_retries=3');
$client = Client::fromDsn('unix:///var/run/tarantool/my_instance.sock');
$client = Client::fromDsn('unix://user:pass@/var/run/tarantool/my_instance.sock?max_retries=3');

如果用户名、密码、路径或选项包含特殊字符,如 @:/%,则必须根据 RFC 3986(例如,使用 rawurlencode() 函数)进行编码。

选项数组

您还可以从配置选项数组创建客户端

use Tarantool\Client\Client;

$client = Client::fromOptions([
    'uri' => 'tcp://127.0.0.1:3301',
    'username' => '<username>',
    'password' => '<password>',
    ...
);

以下选项可用

自定义构建

为了进行更深入的定制,您可以从头开始构建客户端

use MessagePack\BufferUnpacker;
use MessagePack\Packer;
use Tarantool\Client\Client;
use Tarantool\Client\Connection\StreamConnection;
use Tarantool\Client\Handler\DefaultHandler;
use Tarantool\Client\Handler\MiddlewareHandler;
use Tarantool\Client\Middleware\AuthenticationMiddleware;
use Tarantool\Client\Middleware\RetryMiddleware;
use Tarantool\Client\Packer\PurePacker;

$connection = StreamConnection::createTcp('tcp://127.0.0.1:3301', [
    'socket_timeout' => 5.0,
    'connect_timeout' => 5.0,
    // ...
]);

$pureMsgpackPacker = new Packer();
$pureMsgpackUnpacker = new BufferUnpacker();
$packer = new PurePacker($pureMsgpackPacker, $pureMsgpackUnpacker);

$handler = new DefaultHandler($connection, $packer);
$handler = MiddlewareHandler::append($handler, [
    RetryMiddleware::exponential(3),
    new AuthenticationMiddleware('<username>', '<password>'),
    // ...
]);

$client = new Client($handler);

处理器

处理器是一个将请求转换为响应的函数。一旦您创建了处理器对象,您就可以向 Tarantool 发出请求,例如

use Tarantool\Client\Keys;
use Tarantool\Client\Request\CallRequest;

...

$request = new CallRequest('box.stat');
$response = $handler->handle($request);
$data = $response->getBodyField(Keys::DATA);

库附带两个处理器

  • DefaultHandler 用于处理与 Tarantool 服务器的基本通信
  • MiddlewareHandler 通过 中间件 作为底层处理器的扩展点

中间件

中间件是向客户端扩展自定义功能的首选方式。已经实现了几个中间件类来解决常见的用例,如身份验证、日志记录等更多。使用方法简单明了。

use Tarantool\Client\Client;
use Tarantool\Client\Middleware\AuthenticationMiddleware;

$client = Client::fromDefaults()->withMiddleware(
    new AuthenticationMiddleware('<username>', '<password>')
);

您还可以将多个中间件分配给客户端(它们将按FIFO顺序执行)。

use Tarantool\Client\Client;
use Tarantool\Client\Middleware\FirewallMiddleware;
use Tarantool\Client\Middleware\LoggingMiddleware;
use Tarantool\Client\Middleware\RetryMiddleware;

...

$client = Client::fromDefaults()->withMiddleware(
    FirewallMiddleware::allowReadOnly(),
    RetryMiddleware::linear(),
    new LoggingMiddleware($logger)
);

请注意,您添加中间件的顺序很重要。同一中间件放置在不同的顺序可能会产生非常不同的或有时意想不到的行为。为了说明这一点,考虑以下配置

$client1 = Client::fromDefaults()->withMiddleware(
    RetryMiddleware::linear(),
    new AuthenticationMiddleware('<username>', '<password>') 
);

$client2 = Client::fromDefaults()->withMiddleware(
    new AuthenticationMiddleware('<username>', '<password>'), 
    RetryMiddleware::linear()
);

$client3 = Client::fromOptions([
    'username' => '<username>',
    'password' => '<password>',
])->withMiddleware(RetryMiddleware::linear());

在这个例子中,$client1将重试失败的操作,并在连接问题的情况下可能启动重新连接,并随后进行重新身份验证。然而,$client2$client3将执行重新连接而不进行任何重新身份验证。

您可能会想知道为什么$client3在这种情况下表现得像$client2。这是因为指定一些选项(通过数组或DSN字符串)可能隐式注册中间件。因此,username/password选项将在底层转换为AuthenticationMiddleware,使得两个配置相同。

要确保您的中间件先运行,请使用withPrependedMiddleware()方法

$client = $client->withPrependedMiddleware($myMiddleware);

数据操作

二进制协议

以下是一些二进制协议请求的示例。有关更详细的信息和示例,请参阅官方文档

选择

固定装置

local space = box.schema.space.create('example')
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}})
space:create_index('secondary', {type = 'tree', unique = false, parts = {2, 'str'}})
space:insert({1, 'foo'})
space:insert({2, 'bar'})
space:insert({3, 'bar'})
space:insert({4, 'bar'})
space:insert({5, 'baz'})

代码

$space = $client->getSpace('example');
$result1 = $space->select(Criteria::key([1]));
$result2 = $space->select(Criteria::index('secondary')
    ->andKey(['bar'])
    ->andLimit(2)
    ->andOffset(1)
);

printf("Result 1: %s\n", json_encode($result1));
printf("Result 2: %s\n", json_encode($result2));

输出

Result 1: [[1,"foo"]]
Result 2: [[3,"bar"],[4,"bar"]]
插入

固定装置

local space = box.schema.space.create('example')
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}})

代码

$space = $client->getSpace('example');
$result = $space->insert([1, 'foo', 'bar']);

printf("Result: %s\n", json_encode($result));

输出

Result: [[1,"foo","bar"]]

空间数据

tarantool> box.space.example:select()
---
- - [1, 'foo', 'bar']
...
更新

固定装置

local space = box.schema.space.create('example')
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}})
space:format({
    {name = 'id', type = 'unsigned'}, 
    {name = 'num', type = 'unsigned'}, 
    {name = 'name', type = 'string'}
})

space:insert({1, 10, 'foo'})
space:insert({2, 20, 'bar'})
space:insert({3, 30, 'baz'})

代码

$space = $client->getSpace('example');
$result = $space->update([2], Operations::add(1, 5)->andSet(2, 'BAR'));

// Since Tarantool 2.3 you can refer to tuple fields by name:
// $result = $space->update([2], Operations::add('num', 5)->andSet('name', 'BAR'));

printf("Result: %s\n", json_encode($result));

输出

Result: [[2,25,"BAR"]]

空间数据

tarantool> box.space.example:select()
---
- - [1, 10, 'foo']
  - [2, 25, 'BAR']
  - [3, 30, 'baz']
...
更新或插入

固定装置

local space = box.schema.space.create('example')
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}}) 
space:format({
    {name = 'id', type = 'unsigned'}, 
    {name = 'name1', type = 'string'}, 
    {name = 'name2', type = 'string'}
})

代码

$space = $client->getSpace('example');
$space->upsert([1, 'foo', 'bar'], Operations::set(1, 'baz'));
$space->upsert([1, 'foo', 'bar'], Operations::set(2, 'qux'));

// Since Tarantool 2.3 you can refer to tuple fields by name:
// $space->upsert([1, 'foo', 'bar'], Operations::set('name1', 'baz'));
// $space->upsert([1, 'foo', 'bar'], Operations::set('name2'', 'qux'));

空间数据

tarantool> box.space.example:select()
---
- - [1, 'foo', 'qux']
...
替换

固定装置

local space = box.schema.space.create('example')
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}})
space:insert({1, 'foo'})
space:insert({2, 'bar'})

代码

$space = $client->getSpace('example');
$result1 = $space->replace([2, 'BAR']);
$result2 = $space->replace([3, 'BAZ']);

printf("Result 1: %s\n", json_encode($result1));
printf("Result 2: %s\n", json_encode($result2));

输出

Result 1: [[2,"BAR"]]
Result 2: [[3,"BAZ"]]

空间数据

tarantool> box.space.example:select()
---
- - [1, 'foo']
  - [2, 'BAR']
  - [3, 'BAZ']
...
删除

固定装置

local space = box.schema.space.create('example')
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}})
space:create_index('secondary', {type = 'tree', parts = {2, 'str'}})
space:insert({1, 'foo'})
space:insert({2, 'bar'})
space:insert({3, 'baz'})
space:insert({4, 'qux'})

代码

$space = $client->getSpace('example');
$result1 = $space->delete([2]);
$result2 = $space->delete(['baz'], 'secondary');

printf("Result 1: %s\n", json_encode($result1));
printf("Result 2: %s\n", json_encode($result2));

输出

Result 1: [[2,"bar"]]
Result 2: [[3,"baz"]]

空间数据

tarantool> box.space.example:select()
---
- - [1, 'foo']
  - [4, 'qux']
...
调用

固定装置

function func_42()
    return 42
end

代码

$result1 = $client->call('func_42');
$result2 = $client->call('math.min', 5, 3, 8);

printf("Result 1: %s\n", json_encode($result1));
printf("Result 2: %s\n", json_encode($result2));

输出

Result 1: [42]
Result 2: [3]
评估

代码

$result1 = $client->evaluate('function func_42() return 42 end');
$result2 = $client->evaluate('return func_42()');
$result3 = $client->evaluate('return math.min(...)', 5, 3, 8);

printf("Result 1: %s\n", json_encode($result1));
printf("Result 2: %s\n", json_encode($result2));
printf("Result 3: %s\n", json_encode($result3));

输出

Result 1: []
Result 2: [42]
Result 3: [3]

SQL 协议

以下是一些SQL协议请求的示例。有关更详细的信息和示例,请参阅官方文档注意,SQL仅从Tarantool 2.0开始支持。

执行

代码

$client->execute('CREATE TABLE users ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "email" VARCHAR(255))');

$result1 = $client->executeUpdate('CREATE UNIQUE INDEX email ON users ("email")');

$result2 = $client->executeUpdate('
    INSERT INTO users VALUES (null, :email1), (null, :email2)
',
    [':email1' => 'foo@example.com'],
    [':email2' => 'bar@example.com']
);

$result3 = $client->executeQuery('SELECT * FROM users WHERE "email" = ?', 'foo@example.com');
$result4 = $client->executeQuery('SELECT * FROM users WHERE "id" IN (?, ?)', 1, 2);

printf("Result 1: %s\n", json_encode([$result1->count(), $result1->getAutoincrementIds()]));
printf("Result 2: %s\n", json_encode([$result2->count(), $result2->getAutoincrementIds()]));
printf("Result 3: %s\n", json_encode([$result3->count(), $result3[0]]));
printf("Result 4: %s\n", json_encode(iterator_to_array($result4)));

输出

Result 1: [1,[]]
Result 2: [2,[1,2]]
Result 3: [1,{"id":1,"email":"foo@example.com"}]
Result 4: [{"id":1,"email":"foo@example.com"},{"id":2,"email":"bar@example.com"}]

如果您需要执行一个您不知道类型的动态SQL语句,您可以使用通用的execute()方法。此方法返回一个包含结果集行数组的Response对象或包含关于更改行信息的数组

$response = $client->execute('<any-type-of-sql-statement>');
$resultSet = $response->tryGetBodyField(Keys::DATA);

if ($resultSet === null) {
    $sqlInfo = $response->getBodyField(Keys::SQL_INFO);
    $affectedCount = $sqlInfo[Keys::SQL_INFO_ROW_COUNT];
} 
准备

请注意,prepare请求仅从Tarantool 2.3.2开始支持。

代码

$client->execute('CREATE TABLE users ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR(50))');

$stmt = $client->prepare('INSERT INTO users VALUES(null, ?)');
for ($i = 1; $i <= 100; ++$i) {
    $stmt->execute("name_$i");
    // You can also use executeSelect() and executeUpdate(), e.g.:
    // $lastInsertIds = $stmt->executeUpdate("name_$i")->getAutoincrementIds();
}
$stmt->close();

// Note the SEQSCAN keyword in the query. It is available as of Tarantool 2.11.
// If you are using an older version of Tarantool, omit this keyword.
$result = $client->executeQuery('SELECT COUNT("id") AS "cnt" FROM SEQSCAN users');

printf("Result: %s\n", json_encode($result[0]));

输出

Result: {"cnt":100}

用户自定义类型

要在元组中存储复杂结构,您可能需要使用对象

$space->insert([42, Money::EUR(500)]);
[[$id, $money]] = $space->select(Criteria::key([42]));

这可以通过扩展MessagePack类型系统并添加您自己的类型来实现。为此,您需要编写一个MessagePack扩展,该扩展将您的对象转换为MessagePack结构并将其转换回(有关详细信息,请参阅msgpack.php的README)。一旦您实现了您的扩展,您应该将其注册到packer对象

$packer = PurePacker::fromExtensions(new MoneyExtension());
$client = new Client(new DefaultHandler($connection, $packer));

用户定义类型的有效示例可以在示例文件夹中找到。

测试

运行单元测试

vendor/bin/phpunit --testsuite unit

运行集成测试

vendor/bin/phpunit --testsuite integration

确保首先启动client.lua

运行所有测试

vendor/bin/phpunit

如果您已经安装了Docker,您可以在Docker容器中运行测试。首先,创建一个容器

./dockerfile.sh | docker build -t client -

上述命令将创建一个名为client的容器,具有PHP 8.3运行时。您可以通过定义PHP_IMAGE环境变量来更改默认运行时

PHP_IMAGE='php:8.2-cli' ./dockerfile.sh | docker build -t client -

在此处查看各种镜像列表

然后运行Tarantool实例(需要集成测试)

docker network create tarantool-php
docker run -d --net=tarantool-php -p 3301:3301 --name=tarantool \
    -v $(pwd)/tests/Integration/client.lua:/client.lua \
    tarantool/tarantool:3 tarantool /client.lua

然后运行单元和集成测试

docker run --rm --net=tarantool-php -v $(pwd):/client -w /client client

基准测试

基准测试可以在专用存储库中找到。

许可证

该库采用MIT许可证发布。有关详细信息,请参阅附带LICENSE文件。