tarantool/client
Pure PHP client for Tarantool 1.7.1+, supporting Unix sockets, SQL, custom types like decimals and UUIDs, middleware, and full customization with thorough testing.
Check your runtime first — this release is a hard break:
rybakit/msgpack >= 0.11.2 (pulled in automatically, but pin it if you constrain it yourself).Installation:
composer require tarantool/client:^0.11
For Decimal (Tarantool >= 2.3) and UUID support, install the PHP extensions:
pecl install decimal uuid
First connection:
use Tarantool\Client\Client;
$client = Client::fromDsn('tcp://127.0.0.1:3301');
Or via defaults:
$client = Client::fromDefaults(); // 127.0.0.1:3301
First query (binary protocol):
$space = $client->getSpace('users');
$result = $space->select(\Tarantool\Client\Criteria::key([1]));
print_r($result); // [[1, "user_data"]]
First SQL query (if enabled server-side):
print_r($client->execute("SELECT * FROM users WHERE id = 1"));
Tarantool\Client\Client (entry point), Tarantool\Client\Schema\Space, Tarantool\Client\Criteria, Tarantool\Client\Operations.new-ing it in controllers:
// AppServiceProvider::register()
$this->app->singleton(Client::class, fn () => Client::fromOptions(config('services.tarantool')));
$client = Client::fromOptions([
'uri' => 'tcp://127.0.0.1:3301',
'persistent' => true,
]);
RetryMiddleware rather than assuming the socket is alive.Middleware is the normal extension seam: authentication, retries, logging.
$client = Client::fromDefaults()
->withMiddleware(
\Tarantool\Client\Middleware\RetryMiddleware::exponential(3), // outer
new \Tarantool\Client\Middleware\AuthenticationMiddleware('user', 'pass') // inner
);
Logging:
$client->withMiddleware(new \Tarantool\Client\Middleware\LoggingMiddleware(
new \Monolog\Logger('tarantool')
));
$space->insert([1, 'data']);
$user = $space->select(\Tarantool\Client\Criteria::key([1]))[0];
$space->update([1], \Tarantool\Client\Operations::set(1, 'updated_data'));
$space->delete([1]);
Indexed / bounded queries:
$space->select(
\Tarantool\Client\Criteria::index('secondary')->andKey(['value'])->andLimit(10)
);
$result = $client->execute("SELECT * FROM users WHERE id = ?", [1]);
$client->beginTransaction();
try {
$client->execute("INSERT INTO users VALUES (?, ?)", [2, 'new_user']);
$client->commit();
} catch (\Throwable $e) {
$client->rollback();
throw $e;
}
$space->insert([
1,
\Tarantool\Client\Decimal::fromString('123.45'),
\Ramsey\Uuid\Uuid::uuid4(),
]);
With typed APIs in v0.11, pass the real objects/values — no stringly-typed shortcuts.
try {
$client->execute("SELECT * FROM nonexistent");
} catch (\Tarantool\Client\Exception\CommunicationFailed $e) {
// socket/connection level
} catch (\Tarantool\Client\Exception\RequestFailed $e) {
// server rejected the request
} catch (\TypeError $e) {
// NEW in 0.11: argument failed a native type check
}
Catching \TypeError explicitly is worth it during the upgrade — it distinguishes "my code is wrong" from "the server said no".
PHP 8.2 is a hard floor. Composer will refuse to install on 8.1 and below. If you support multiple PHP versions in CI, split the matrix or drop the old legs; otherwise composer update fails at resolution time, not runtime.
Native types are now enforced at runtime — coercion is gone. Previously loose arguments (numeric strings for keys, null for non-nullable params, ints where floats/strings were declared) were silently accepted. Now they throw TypeError.
(int) $request->input('id'), (string) $value.declare(strict_types=1); to new files so behaviour matches between dev and prod.Custom implementations must be re-signed. If you wrote your own Middleware, Packer, or similar, v0.11's typed interfaces mean a missing parameter type, missing return type, or widened type is a fatal error at class load, not a soft deprecation. Update signatures to match the interface exactly before upgrading.
Middleware order still matters. Retry must be outside authentication so a retried request re-authenticates:
// correct
->withMiddleware(RetryMiddleware::exponential(3), new AuthenticationMiddleware($u, $p))
Persistent connections cost you on cold-start-heavy local dev but pay off in long-running apps. Common compromise:
'persistent' => ! app()->isLocal(),
With Octane/queues, always pair persistence with retry logic.
SQL vs binary protocol. SQL is off by default on the server (box.cfg{sql_enabled = true}). Binary protocol is faster for bulk work but requires you to know the schema. Don't reach for SQL "because it's familiar" in hot paths.
Decimal/UUID extensions. Without decimal/uuid you'll get UnsupportedTypeException. Fall back to strings if you can't install extensions on the host.
Timeouts. Defaults (connect_timeout 5s, socket_timeout 5s) are short for cross-AZ/cloud traffic:
'connect_timeout' => 10.0,
'socket_timeout' => 30.0,
Large payloads. Tuples above ~1MB can hit PHP/msgpack limits. Split the work or move it into a Lua function and call it.
No client-side batching primitive. Each request is a round trip; for bulk writes use a stored procedure (call) that loops server-side, or chunk your loop and measure. Don't assume a multi()-style API exists.
Logging middleware — raw request/response visibility:
$client->withMiddleware(new \Tarantool\Client\Middleware\LoggingMiddleware(
new \Monolog\Logger('tarantool', [
'handlers' => [new \Monolog\Handler\StreamHandler('php://stderr')],
])
));
Verify the connection is alive inside middleware:
class ConnectionGuard implements \Tarantool\Client\Middleware
{
public function handle(\Tarantool\Client\Request $request, callable $next): \Tarantool\Client\Response
{
if (! $request->getConnection()->isConnected()) {
throw new \RuntimeException('Connection lost');
}
return $next($request);
}
}
Schema mismatches are quiet. Sending a string to a number field fails without much ceremony. Use named-field operations (Operations::set('field_name', $value), Tarantool >= 2.3) to make intent explicit.
Bound the retries. Cap attempts and delay to avoid hammering a dead node:
\Tarantool\Client\Middleware\RetryMiddleware::exponential(3, 100); // attempts, max delay ms
Upgrade checklist after 0.11: bump PHP in CI/Docker/Sail → composer require tarantool/client:^0.11 → run Psalm/PHPStan → grep for custom Middleware/Packer implementations → re-run integration tests against a real Tarantool instance (unit tests won't catch type/signature breakage in the wire protocol).
Custom middleware — request/response transformation, rate limiting, circuit breaking:
class PingShortcut implements \Tarantool\Client\Middleware
{
public function handle(\Tarantool\Client\Request $request, callable $next): \Tarantool\Client\Response
{
if ($request instanceof \Tarantool\Client\Request\CallRequest
&& $request->getFunction() === 'admin.ping') {
return new \Tarantool\Client\Response\SuccessResponse([['pong']]);
}
return $next($request);
}
}
Custom Packer — swap serialization (e.g. Protobuf). Implement the typed \Tarantool\Client\Packer interface exactly, then inject via a handler:
$client = new \Tarantool\Client\Client(
new \Tarantool\Client\Handler\DefaultHandler($connection, $packer)
);
Custom DSN handling — extend \Tarantool\Client\DsnParser for non-standard URI schemes and drive it from config/services.php.
Testing hooks — v0.11 moved the test tooling to PHPUnit 10 attributes and tarantool/phpunit-extras 0.3. If you extend the package's test helpers, drop the old annotation-style/compatibility helper usages in favour of attributes.
Criteria for hot paths; keep SQL for ad-hoc/reporting.call rather than N round trips.Space lookups if you resolve the same spaces repeatedly in a request.How can I help you explore Laravel packages today?