├── LICENSE ├── README.md ├── composer.json └── src ├── Client.php ├── Connection ├── Connection.php ├── Greeting.php └── StreamConnection.php ├── Dsn.php ├── Error.php ├── Exception ├── ClientException.php ├── CommunicationFailed.php ├── ConnectionFailed.php ├── RequestDenied.php ├── RequestFailed.php └── UnexpectedResponse.php ├── Handler ├── DefaultHandler.php ├── Handler.php └── MiddlewareHandler.php ├── Keys.php ├── Middleware ├── AuthenticationMiddleware.php ├── CustomErrorMiddleware.php ├── FirewallMiddleware.php ├── LoggingMiddleware.php ├── Middleware.php └── RetryMiddleware.php ├── Packer ├── Extension │ ├── DecimalExtension.php │ ├── ErrorExtension.php │ └── UuidExtension.php ├── Packer.php ├── PacketLength.php └── PurePacker.php ├── PreparedStatement.php ├── Request ├── AuthenticateRequest.php ├── CallRequest.php ├── DeleteRequest.php ├── EvaluateRequest.php ├── ExecuteRequest.php ├── InsertRequest.php ├── PingRequest.php ├── PrepareRequest.php ├── ReplaceRequest.php ├── Request.php ├── SelectRequest.php ├── UpdateRequest.php └── UpsertRequest.php ├── RequestTypes.php ├── Response.php ├── Schema ├── Criteria.php ├── IteratorTypes.php ├── Operations.php └── Space.php ├── SqlQueryResult.php └── SqlUpdateResult.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2024 Eugene Leonovich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP client for Tarantool 2 | 3 | [![Quality Assurance](https://github.com/tarantool-php/client/workflows/QA/badge.svg)](https://github.com/tarantool-php/client/actions?query=workflow%3AQA) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/tarantool-php/client/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/tarantool-php/client/?branch=master) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/tarantool-php/client/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/tarantool-php/client/?branch=master) 6 | [![Telegram](https://img.shields.io/badge/Telegram-join%20chat-blue.svg)](https://t.me/tarantool_php) 7 | 8 | A pure PHP client for [Tarantool](https://www.tarantool.io/en/developers/) 1.7.1 or above. 9 | 10 | 11 | ## Features 12 | 13 | * Written in pure PHP, no extensions are required 14 | * Supports Unix domain sockets 15 | * Supports SQL protocol 16 | * Supports user-defined types (decimals and UUIDs are included) 17 | * Highly customizable 18 | * [Thoroughly tested](https://github.com/tarantool-php/client/actions?query=workflow%3AQA) 19 | * Being used in a number of projects, including [Queue](https://github.com/tarantool-php/queue), 20 | [Mapper](https://github.com/tarantool-php/mapper), [Web Admin](https://github.com/basis-company/tarantool-admin) 21 | and [others](https://github.com/tarantool-php). 22 | 23 | 24 | ## Table of contents 25 | 26 | * [Installation](#installation) 27 | * [Creating a client](#creating-a-client) 28 | * [Handlers](#handlers) 29 | * [Middleware](#middleware) 30 | * [Data manipulation](#data-manipulation) 31 | * [Binary protocol](#binary-protocol) 32 | * [SQL protocol](#sql-protocol) 33 | * [User-defined types](#user-defined-types) 34 | * [Tests](#tests) 35 | * [Benchmarks](#benchmarks) 36 | * [License](#license) 37 | 38 | 39 | ## Installation 40 | 41 | The recommended way to install the library is through [Composer](http://getcomposer.org): 42 | 43 | ```bash 44 | composer require tarantool/client 45 | ``` 46 | 47 | In order to use the [Decimal](https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-decimal-type) 48 | type that was added in Tarantool 2.3, you additionally need to install the [decimal](http://php-decimal.io/#installation) 49 | extension. Also, to improve performance when working with the [UUID](https://www.tarantool.io/en/doc/latest/dev_guide/internals/msgpack_extensions/#the-uuid-type) 50 | type, which is available since Tarantool 2.4, it is recommended to additionally install the [uuid](https://pecl.php.net/package/uuid) extension. 51 | 52 | 53 | ## Creating a client 54 | 55 | The easiest way to create a client is by using the default configuration: 56 | 57 | ```php 58 | use Tarantool\Client\Client; 59 | 60 | $client = Client::fromDefaults(); 61 | ``` 62 | 63 | The client will be configured to connect to `127.0.0.1` on port `3301` with the default stream connection options. 64 | Also, the best available msgpack package will be chosen automatically. A custom configuration can be accomplished 65 | by one of several methods listed. 66 | 67 | #### DSN string 68 | 69 | The client supports the following Data Source Name formats: 70 | 71 | ``` 72 | tcp://[[username[:password]@]host[:port][/?option1=value1&optionN=valueN] 73 | unix://[[username[:password]@]path[/?option1=value1&optionN=valueN] 74 | ``` 75 | 76 | Some examples: 77 | 78 | ```php 79 | use Tarantool\Client\Client; 80 | 81 | $client = Client::fromDsn('tcp://127.0.0.1'); 82 | $client = Client::fromDsn('tcp://[fe80::1]:3301'); 83 | $client = Client::fromDsn('tcp://user:pass@example.com:3301'); 84 | $client = Client::fromDsn('tcp://user@example.com/?connect_timeout=5.0&max_retries=3'); 85 | $client = Client::fromDsn('unix:///var/run/tarantool/my_instance.sock'); 86 | $client = Client::fromDsn('unix://user:pass@/var/run/tarantool/my_instance.sock?max_retries=3'); 87 | ``` 88 | 89 | If the username, password, path or options include special characters such as `@`, `:`, `/` or `%`, 90 | they must be encoded according to [RFC 3986](https://tools.ietf.org/html/rfc3986#section-2.1) 91 | (for example, with the [rawurlencode()](https://www.php.net/manual/en/function.rawurlencode.php) function). 92 | 93 | 94 | #### Array of options 95 | 96 | It is also possible to create the client from an array of configuration options: 97 | 98 | ```php 99 | use Tarantool\Client\Client; 100 | 101 | $client = Client::fromOptions([ 102 | 'uri' => 'tcp://127.0.0.1:3301', 103 | 'username' => '', 104 | 'password' => '', 105 | ... 106 | ); 107 | ``` 108 | 109 | The following options are available: 110 | 111 | Name | Type | Default | Description 112 | --- | :---: | :---: | --- 113 | *uri* | string | 'tcp://127.0.0.1:3301' | The connection uri that is used to create a `StreamConnection` object. 114 | *connect_timeout* | float | 5.0 | The number of seconds that the client waits for a connect to a Tarantool server before throwing a `ConnectionFailed` exception. 115 | *socket_timeout* | float | 5.0 | The number of seconds that the client waits for a respond from a Tarantool server before throwing a `CommunicationFailed` exception. 116 | *tcp_nodelay* | boolean | true | Whether the Nagle algorithm is disabled on a TCP connection. 117 | *persistent* | boolean | false | Whether to use a persistent connection. 118 | *username* | string | | The username for the user being authenticated. 119 | *password* | string | '' | The password for the user being authenticated. If the username is not set, this option will be ignored. 120 | *max_retries* | integer | 0 | The number of times the client retries unsuccessful request. If set to 0, the client does not try to resend the request after the initial unsuccessful attempt. 121 | 122 | 123 | #### Custom build 124 | 125 | For more deep customisation, you can build a client from the ground up: 126 | 127 | ```php 128 | use MessagePack\BufferUnpacker; 129 | use MessagePack\Packer; 130 | use Tarantool\Client\Client; 131 | use Tarantool\Client\Connection\StreamConnection; 132 | use Tarantool\Client\Handler\DefaultHandler; 133 | use Tarantool\Client\Handler\MiddlewareHandler; 134 | use Tarantool\Client\Middleware\AuthenticationMiddleware; 135 | use Tarantool\Client\Middleware\RetryMiddleware; 136 | use Tarantool\Client\Packer\PurePacker; 137 | 138 | $connection = StreamConnection::createTcp('tcp://127.0.0.1:3301', [ 139 | 'socket_timeout' => 5.0, 140 | 'connect_timeout' => 5.0, 141 | // ... 142 | ]); 143 | 144 | $pureMsgpackPacker = new Packer(); 145 | $pureMsgpackUnpacker = new BufferUnpacker(); 146 | $packer = new PurePacker($pureMsgpackPacker, $pureMsgpackUnpacker); 147 | 148 | $handler = new DefaultHandler($connection, $packer); 149 | $handler = MiddlewareHandler::append($handler, [ 150 | RetryMiddleware::exponential(3), 151 | new AuthenticationMiddleware('', ''), 152 | // ... 153 | ]); 154 | 155 | $client = new Client($handler); 156 | ``` 157 | 158 | 159 | ## Handlers 160 | 161 | A handler is a function which transforms a request into a response. Once you have created a handler object, 162 | you can make requests to Tarantool, for example: 163 | 164 | ```php 165 | use Tarantool\Client\Keys; 166 | use Tarantool\Client\Request\CallRequest; 167 | 168 | ... 169 | 170 | $request = new CallRequest('box.stat'); 171 | $response = $handler->handle($request); 172 | $data = $response->getBodyField(Keys::DATA); 173 | ``` 174 | 175 | The library ships with two handlers: 176 | 177 | * `DefaultHandler` is used for handling low-level communication with a Tarantool server 178 | * `MiddlewareHandler` is used as an extension point for an underlying handler via [middleware](#middleware) 179 | 180 | 181 | ## Middleware 182 | 183 | Middleware is the suggested way to extend the client with custom functionality. There are several middleware classes 184 | implemented to address the common use cases, like authentification, logging and [more](src/Middleware). 185 | The usage is straightforward: 186 | 187 | ```php 188 | use Tarantool\Client\Client; 189 | use Tarantool\Client\Middleware\AuthenticationMiddleware; 190 | 191 | $client = Client::fromDefaults()->withMiddleware( 192 | new AuthenticationMiddleware('', '') 193 | ); 194 | ``` 195 | 196 | You may also assign multiple middleware to the client (they will be executed in [FIFO](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)) order): 197 | 198 | ```php 199 | use Tarantool\Client\Client; 200 | use Tarantool\Client\Middleware\FirewallMiddleware; 201 | use Tarantool\Client\Middleware\LoggingMiddleware; 202 | use Tarantool\Client\Middleware\RetryMiddleware; 203 | 204 | ... 205 | 206 | $client = Client::fromDefaults()->withMiddleware( 207 | FirewallMiddleware::allowReadOnly(), 208 | RetryMiddleware::linear(), 209 | new LoggingMiddleware($logger) 210 | ); 211 | ``` 212 | 213 | Please be aware that the order in which you add the middleware does matter. The same middleware, 214 | placed in different order, can give very different or sometimes unexpected behavior. 215 | To illustrate, consider the following configurations: 216 | 217 | ```php 218 | $client1 = Client::fromDefaults()->withMiddleware( 219 | RetryMiddleware::linear(), 220 | new AuthenticationMiddleware('', '') 221 | ); 222 | 223 | $client2 = Client::fromDefaults()->withMiddleware( 224 | new AuthenticationMiddleware('', ''), 225 | RetryMiddleware::linear() 226 | ); 227 | 228 | $client3 = Client::fromOptions([ 229 | 'username' => '', 230 | 'password' => '', 231 | ])->withMiddleware(RetryMiddleware::linear()); 232 | ``` 233 | 234 | In this example, `$client1` will retry an unsuccessful operation and in case of connection 235 | problems may initiate reconnection with follow-up re-authentication. However, `$client2` 236 | and `$client3` will perform reconnection *without* doing any re-authentication. 237 | 238 | > *You may wonder why `$client3` behaves like `$client2` in this case. This is because 239 | > specifying some options (via array or DSN string) may implicitly register middleware. 240 | > Thus, the `username/password` options will be turned into `AuthenticationMiddleware` 241 | > under the hood, making the two configurations identical.* 242 | 243 | To make sure your middleware runs first, use the `withPrependedMiddleware()` method: 244 | 245 | ```php 246 | $client = $client->withPrependedMiddleware($myMiddleware); 247 | ``` 248 | 249 | 250 | ## Data manipulation 251 | 252 | ### Binary protocol 253 | 254 | The following are examples of binary protocol requests. For more detailed information and examples please see 255 | the [official documentation](https://www.tarantool.io/en/doc/latest/book/box/data_model/#operations). 256 | 257 |
258 | Select
259 | 260 | *Fixtures* 261 | 262 | ```lua 263 | local space = box.schema.space.create('example') 264 | space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}}) 265 | space:create_index('secondary', {type = 'tree', unique = false, parts = {2, 'str'}}) 266 | space:insert({1, 'foo'}) 267 | space:insert({2, 'bar'}) 268 | space:insert({3, 'bar'}) 269 | space:insert({4, 'bar'}) 270 | space:insert({5, 'baz'}) 271 | ``` 272 | 273 | *Code* 274 | 275 | ```php 276 | $space = $client->getSpace('example'); 277 | $result1 = $space->select(Criteria::key([1])); 278 | $result2 = $space->select(Criteria::index('secondary') 279 | ->andKey(['bar']) 280 | ->andLimit(2) 281 | ->andOffset(1) 282 | ); 283 | 284 | printf("Result 1: %s\n", json_encode($result1)); 285 | printf("Result 2: %s\n", json_encode($result2)); 286 | ``` 287 | 288 | *Output* 289 | 290 | ``` 291 | Result 1: [[1,"foo"]] 292 | Result 2: [[3,"bar"],[4,"bar"]] 293 | ``` 294 |
295 | 296 | 297 |
298 | Insert
299 | 300 | *Fixtures* 301 | 302 | ```lua 303 | local space = box.schema.space.create('example') 304 | space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}}) 305 | ``` 306 | 307 | *Code* 308 | 309 | ```php 310 | $space = $client->getSpace('example'); 311 | $result = $space->insert([1, 'foo', 'bar']); 312 | 313 | printf("Result: %s\n", json_encode($result)); 314 | ``` 315 | 316 | *Output* 317 | 318 | ``` 319 | Result: [[1,"foo","bar"]] 320 | ``` 321 | 322 | *Space data* 323 | 324 | ```lua 325 | tarantool> box.space.example:select() 326 | --- 327 | - - [1, 'foo', 'bar'] 328 | ... 329 | ``` 330 |
331 | 332 | 333 |
334 | Update
335 | 336 | *Fixtures* 337 | 338 | ```lua 339 | local space = box.schema.space.create('example') 340 | space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}}) 341 | space:format({ 342 | {name = 'id', type = 'unsigned'}, 343 | {name = 'num', type = 'unsigned'}, 344 | {name = 'name', type = 'string'} 345 | }) 346 | 347 | space:insert({1, 10, 'foo'}) 348 | space:insert({2, 20, 'bar'}) 349 | space:insert({3, 30, 'baz'}) 350 | ``` 351 | 352 | *Code* 353 | 354 | ```php 355 | $space = $client->getSpace('example'); 356 | $result = $space->update([2], Operations::add(1, 5)->andSet(2, 'BAR')); 357 | 358 | // Since Tarantool 2.3 you can refer to tuple fields by name: 359 | // $result = $space->update([2], Operations::add('num', 5)->andSet('name', 'BAR')); 360 | 361 | printf("Result: %s\n", json_encode($result)); 362 | ``` 363 | 364 | *Output* 365 | 366 | ``` 367 | Result: [[2,25,"BAR"]] 368 | ``` 369 | 370 | *Space data* 371 | 372 | ```lua 373 | tarantool> box.space.example:select() 374 | --- 375 | - - [1, 10, 'foo'] 376 | - [2, 25, 'BAR'] 377 | - [3, 30, 'baz'] 378 | ... 379 | ``` 380 |
381 | 382 | 383 |
384 | Upsert
385 | 386 | *Fixtures* 387 | 388 | ```lua 389 | local space = box.schema.space.create('example') 390 | space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}}) 391 | space:format({ 392 | {name = 'id', type = 'unsigned'}, 393 | {name = 'name1', type = 'string'}, 394 | {name = 'name2', type = 'string'} 395 | }) 396 | ``` 397 | 398 | *Code* 399 | 400 | ```php 401 | $space = $client->getSpace('example'); 402 | $space->upsert([1, 'foo', 'bar'], Operations::set(1, 'baz')); 403 | $space->upsert([1, 'foo', 'bar'], Operations::set(2, 'qux')); 404 | 405 | // Since Tarantool 2.3 you can refer to tuple fields by name: 406 | // $space->upsert([1, 'foo', 'bar'], Operations::set('name1', 'baz')); 407 | // $space->upsert([1, 'foo', 'bar'], Operations::set('name2'', 'qux')); 408 | ``` 409 | 410 | *Space data* 411 | 412 | ```lua 413 | tarantool> box.space.example:select() 414 | --- 415 | - - [1, 'foo', 'qux'] 416 | ... 417 | ``` 418 |
419 | 420 | 421 |
422 | Replace
423 | 424 | *Fixtures* 425 | 426 | ```lua 427 | local space = box.schema.space.create('example') 428 | space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}}) 429 | space:insert({1, 'foo'}) 430 | space:insert({2, 'bar'}) 431 | ``` 432 | 433 | *Code* 434 | 435 | ```php 436 | $space = $client->getSpace('example'); 437 | $result1 = $space->replace([2, 'BAR']); 438 | $result2 = $space->replace([3, 'BAZ']); 439 | 440 | printf("Result 1: %s\n", json_encode($result1)); 441 | printf("Result 2: %s\n", json_encode($result2)); 442 | ``` 443 | 444 | *Output* 445 | 446 | ``` 447 | Result 1: [[2,"BAR"]] 448 | Result 2: [[3,"BAZ"]] 449 | ``` 450 | 451 | *Space data* 452 | 453 | ```lua 454 | tarantool> box.space.example:select() 455 | --- 456 | - - [1, 'foo'] 457 | - [2, 'BAR'] 458 | - [3, 'BAZ'] 459 | ... 460 | ``` 461 |
462 | 463 | 464 |
465 | Delete
466 | 467 | *Fixtures* 468 | 469 | ```lua 470 | local space = box.schema.space.create('example') 471 | space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}}) 472 | space:create_index('secondary', {type = 'tree', parts = {2, 'str'}}) 473 | space:insert({1, 'foo'}) 474 | space:insert({2, 'bar'}) 475 | space:insert({3, 'baz'}) 476 | space:insert({4, 'qux'}) 477 | ``` 478 | 479 | *Code* 480 | 481 | ```php 482 | $space = $client->getSpace('example'); 483 | $result1 = $space->delete([2]); 484 | $result2 = $space->delete(['baz'], 'secondary'); 485 | 486 | printf("Result 1: %s\n", json_encode($result1)); 487 | printf("Result 2: %s\n", json_encode($result2)); 488 | ``` 489 | 490 | *Output* 491 | 492 | ``` 493 | Result 1: [[2,"bar"]] 494 | Result 2: [[3,"baz"]] 495 | ``` 496 | 497 | *Space data* 498 | 499 | ```lua 500 | tarantool> box.space.example:select() 501 | --- 502 | - - [1, 'foo'] 503 | - [4, 'qux'] 504 | ... 505 | ``` 506 |
507 | 508 | 509 |
510 | Call
511 | 512 | *Fixtures* 513 | 514 | ```lua 515 | function func_42() 516 | return 42 517 | end 518 | ``` 519 | 520 | *Code* 521 | 522 | ```php 523 | $result1 = $client->call('func_42'); 524 | $result2 = $client->call('math.min', 5, 3, 8); 525 | 526 | printf("Result 1: %s\n", json_encode($result1)); 527 | printf("Result 2: %s\n", json_encode($result2)); 528 | ``` 529 | 530 | *Output* 531 | 532 | ``` 533 | Result 1: [42] 534 | Result 2: [3] 535 | ``` 536 |
537 | 538 | 539 |
540 | Evaluate
541 | 542 | *Code* 543 | 544 | ```php 545 | $result1 = $client->evaluate('function func_42() return 42 end'); 546 | $result2 = $client->evaluate('return func_42()'); 547 | $result3 = $client->evaluate('return math.min(...)', 5, 3, 8); 548 | 549 | printf("Result 1: %s\n", json_encode($result1)); 550 | printf("Result 2: %s\n", json_encode($result2)); 551 | printf("Result 3: %s\n", json_encode($result3)); 552 | ``` 553 | 554 | *Output* 555 | 556 | ``` 557 | Result 1: [] 558 | Result 2: [42] 559 | Result 3: [3] 560 | ``` 561 |
562 | 563 | 564 | ### SQL protocol 565 | 566 | The following are examples of SQL protocol requests. For more detailed information and examples please see 567 | the [official documentation](https://www.tarantool.io/en/doc/latest/reference/reference_sql/sql/). 568 | *Note that SQL is supported only as of Tarantool 2.0.* 569 | 570 |
571 | Execute
572 | 573 | *Code* 574 | 575 | ```php 576 | $client->execute('CREATE TABLE users ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "email" VARCHAR(255))'); 577 | 578 | $result1 = $client->executeUpdate('CREATE UNIQUE INDEX email ON users ("email")'); 579 | 580 | $result2 = $client->executeUpdate(' 581 | INSERT INTO users VALUES (null, :email1), (null, :email2) 582 | ', 583 | [':email1' => 'foo@example.com'], 584 | [':email2' => 'bar@example.com'] 585 | ); 586 | 587 | $result3 = $client->executeQuery('SELECT * FROM users WHERE "email" = ?', 'foo@example.com'); 588 | $result4 = $client->executeQuery('SELECT * FROM users WHERE "id" IN (?, ?)', 1, 2); 589 | 590 | printf("Result 1: %s\n", json_encode([$result1->count(), $result1->getAutoincrementIds()])); 591 | printf("Result 2: %s\n", json_encode([$result2->count(), $result2->getAutoincrementIds()])); 592 | printf("Result 3: %s\n", json_encode([$result3->count(), $result3[0]])); 593 | printf("Result 4: %s\n", json_encode(iterator_to_array($result4))); 594 | ``` 595 | 596 | *Output* 597 | 598 | ``` 599 | Result 1: [1,[]] 600 | Result 2: [2,[1,2]] 601 | Result 3: [1,{"id":1,"email":"foo@example.com"}] 602 | Result 4: [{"id":1,"email":"foo@example.com"},{"id":2,"email":"bar@example.com"}] 603 | ``` 604 | 605 | If you need to execute a dynamic SQL statement whose type you don't know, you can use the generic method `execute()`. 606 | This method returns a Response object with the body containing either an array of result set rows or an array 607 | with information about the changed rows: 608 | 609 | ```php 610 | $response = $client->execute(''); 611 | $resultSet = $response->tryGetBodyField(Keys::DATA); 612 | 613 | if ($resultSet === null) { 614 | $sqlInfo = $response->getBodyField(Keys::SQL_INFO); 615 | $affectedCount = $sqlInfo[Keys::SQL_INFO_ROW_COUNT]; 616 | } 617 | ``` 618 |
619 | 620 | 621 |
622 | Prepare
623 | 624 | *Note that the `prepare` request is supported only as of Tarantool 2.3.2.* 625 | 626 | *Code* 627 | 628 | ```php 629 | $client->execute('CREATE TABLE users ("id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR(50))'); 630 | 631 | $stmt = $client->prepare('INSERT INTO users VALUES(null, ?)'); 632 | for ($i = 1; $i <= 100; ++$i) { 633 | $stmt->execute("name_$i"); 634 | // You can also use executeSelect() and executeUpdate(), e.g.: 635 | // $lastInsertIds = $stmt->executeUpdate("name_$i")->getAutoincrementIds(); 636 | } 637 | $stmt->close(); 638 | 639 | // Note the SEQSCAN keyword in the query. It is available as of Tarantool 2.11. 640 | // If you are using an older version of Tarantool, omit this keyword. 641 | $result = $client->executeQuery('SELECT COUNT("id") AS "cnt" FROM SEQSCAN users'); 642 | 643 | printf("Result: %s\n", json_encode($result[0])); 644 | ``` 645 | 646 | *Output* 647 | 648 | ``` 649 | Result: {"cnt":100} 650 | ``` 651 |
652 | 653 | 654 | ### User-defined types 655 | 656 | To store complex structures inside a tuple you may want to use objects: 657 | 658 | ```php 659 | $space->insert([42, Money::EUR(500)]); 660 | [[$id, $money]] = $space->select(Criteria::key([42])); 661 | ``` 662 | 663 | This can be achieved by extending the MessagePack type system with your own types. To do this, you need to write 664 | a MessagePack extension that converts your objects into MessagePack structures and back (for more details, read 665 | the msgpack.php's [README](https://github.com/rybakit/msgpack.php#custom-types)). Once you have implemented 666 | your extension, you should register it with the packer object: 667 | 668 | ```php 669 | $packer = PurePacker::fromExtensions(new MoneyExtension()); 670 | $client = new Client(new DefaultHandler($connection, $packer)); 671 | ``` 672 | 673 | > *A working example of using the user-defined types can be found in the [examples](examples/user_defined_type) folder.* 674 | 675 | 676 | ## Tests 677 | 678 | To run unit tests: 679 | 680 | ```bash 681 | vendor/bin/phpunit --testsuite unit 682 | ``` 683 | 684 | To run integration tests: 685 | 686 | ```bash 687 | vendor/bin/phpunit --testsuite integration 688 | ``` 689 | 690 | > *Make sure to start [client.lua](tests/Integration/client.lua) first.* 691 | 692 | To run all tests: 693 | 694 | ```bash 695 | vendor/bin/phpunit 696 | ``` 697 | 698 | If you already have Docker installed, you can run the tests in a docker container. 699 | First, create a container: 700 | 701 | ```bash 702 | ./dockerfile.sh | docker build -t client - 703 | ``` 704 | 705 | The command above will create a container named `client` with PHP 8.3 runtime. 706 | You may change the default runtime by defining the `PHP_IMAGE` environment variable: 707 | 708 | ```bash 709 | PHP_IMAGE='php:8.2-cli' ./dockerfile.sh | docker build -t client - 710 | ``` 711 | 712 | > *See a list of various images [here](https://hub.docker.com/_/php).* 713 | 714 | 715 | Then run a Tarantool instance (needed for integration tests): 716 | 717 | ```bash 718 | docker network create tarantool-php 719 | docker run -d --net=tarantool-php -p 3301:3301 --name=tarantool \ 720 | -v $(pwd)/tests/Integration/client.lua:/client.lua \ 721 | tarantool/tarantool:3 tarantool /client.lua 722 | ``` 723 | 724 | And then run both unit and integration tests: 725 | 726 | ```bash 727 | docker run --rm --net=tarantool-php -v $(pwd):/client -w /client client 728 | ``` 729 | 730 | 731 | ## Benchmarks 732 | 733 | The benchmarks can be found in the [dedicated repository](https://github.com/tarantool-php/benchmarks). 734 | 735 | 736 | ## License 737 | 738 | The library is released under the MIT License. See the bundled [LICENSE](LICENSE) file for details. 739 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tarantool/client", 3 | "description": "PHP client for Tarantool.", 4 | "keywords": ["tarantool", "client", "pure", "nosql"], 5 | "type": "library", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Eugene Leonovich", 10 | "email": "gen.work@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.2.5|^8", 15 | "rybakit/msgpack": "^0.9", 16 | "symfony/uid": "^5.1|^6|^7" 17 | }, 18 | "require-dev": { 19 | "ext-json": "*", 20 | "ext-sockets": "*", 21 | "friendsofphp/php-cs-fixer": "^2.19", 22 | "monolog/monolog": "^1.24|^2.0", 23 | "psr/log": "^1.1", 24 | "tarantool/phpunit-extras": "^0.2.0", 25 | "vimeo/psalm": "^3.9|^4" 26 | }, 27 | "suggest": { 28 | "ext-decimal": "For using decimals with Tarantool 2.3+", 29 | "ext-uuid": "For better performance when using UUIDs with Tarantool 2.4+", 30 | "psr/log": "For using LoggingMiddleware" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Tarantool\\Client\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Tarantool\\Client\\Tests\\": "tests/" 40 | } 41 | }, 42 | "config": { 43 | "preferred-install": { 44 | "*": "dist" 45 | }, 46 | "sort-packages": true 47 | }, 48 | "extra": { 49 | "branch-alias": { 50 | "dev-master": "0.10.x-dev" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client; 15 | 16 | use Tarantool\Client\Connection\StreamConnection; 17 | use Tarantool\Client\Exception\RequestFailed; 18 | use Tarantool\Client\Handler\DefaultHandler; 19 | use Tarantool\Client\Handler\Handler; 20 | use Tarantool\Client\Handler\MiddlewareHandler; 21 | use Tarantool\Client\Middleware\AuthenticationMiddleware; 22 | use Tarantool\Client\Middleware\Middleware; 23 | use Tarantool\Client\Middleware\RetryMiddleware; 24 | use Tarantool\Client\Packer\Packer; 25 | use Tarantool\Client\Packer\PurePacker; 26 | use Tarantool\Client\Request\CallRequest; 27 | use Tarantool\Client\Request\EvaluateRequest; 28 | use Tarantool\Client\Request\ExecuteRequest; 29 | use Tarantool\Client\Request\PingRequest; 30 | use Tarantool\Client\Request\PrepareRequest; 31 | use Tarantool\Client\Schema\Criteria; 32 | use Tarantool\Client\Schema\Space; 33 | 34 | final class Client 35 | { 36 | /** @var Handler */ 37 | private $handler; 38 | 39 | /** @var array */ 40 | private $spaces = []; 41 | 42 | public function __construct(Handler $handler) 43 | { 44 | $this->handler = $handler; 45 | } 46 | 47 | public static function fromDefaults() : self 48 | { 49 | return new self(new DefaultHandler( 50 | StreamConnection::createTcp(), 51 | PurePacker::fromAvailableExtensions() 52 | )); 53 | } 54 | 55 | public static function fromOptions(array $options, ?Packer $packer = null) : self 56 | { 57 | $connectionOptions = []; 58 | if (isset($options['connect_timeout'])) { 59 | $connectionOptions['connect_timeout'] = $options['connect_timeout']; 60 | } 61 | if (isset($options['socket_timeout'])) { 62 | $connectionOptions['socket_timeout'] = $options['socket_timeout']; 63 | } 64 | if (isset($options['tcp_nodelay'])) { 65 | $connectionOptions['tcp_nodelay'] = $options['tcp_nodelay']; 66 | } 67 | if (isset($options['persistent'])) { 68 | $connectionOptions['persistent'] = $options['persistent']; 69 | } 70 | 71 | $middleware = []; 72 | if (isset($options['max_retries']) && 0 !== $options['max_retries']) { 73 | $middleware[] = RetryMiddleware::linear($options['max_retries']); 74 | } 75 | if (isset($options['username'])) { 76 | $middleware[] = new AuthenticationMiddleware($options['username'], $options['password'] ?? ''); 77 | } 78 | 79 | $connection = isset($options['uri']) 80 | ? StreamConnection::create($options['uri'], $connectionOptions) 81 | : StreamConnection::createTcp(StreamConnection::DEFAULT_TCP_URI, $connectionOptions); 82 | 83 | $handler = new DefaultHandler($connection, $packer ?? PurePacker::fromAvailableExtensions()); 84 | 85 | return $middleware 86 | ? new self(MiddlewareHandler::append($handler, $middleware)) 87 | : new self($handler); 88 | } 89 | 90 | public static function fromDsn(string $dsn, ?Packer $packer = null) : self 91 | { 92 | $dsn = Dsn::parse($dsn); 93 | 94 | $connectionOptions = []; 95 | if (null !== $timeout = $dsn->getFloat('connect_timeout')) { 96 | $connectionOptions['connect_timeout'] = $timeout; 97 | } 98 | if (null !== $timeout = $dsn->getFloat('socket_timeout')) { 99 | $connectionOptions['socket_timeout'] = $timeout; 100 | } 101 | if (null !== $tcpNoDelay = $dsn->getBool('tcp_nodelay')) { 102 | $connectionOptions['tcp_nodelay'] = $tcpNoDelay; 103 | } 104 | if (null !== $persistent = $dsn->getBool('persistent')) { 105 | $connectionOptions['persistent'] = $persistent; 106 | } 107 | 108 | $middleware = []; 109 | if ($maxRetries = $dsn->getInt('max_retries')) { 110 | $middleware[] = RetryMiddleware::linear($maxRetries); 111 | } 112 | if ($username = $dsn->getUsername()) { 113 | $middleware[] = new AuthenticationMiddleware($username, $dsn->getPassword() ?? ''); 114 | } 115 | 116 | $connection = $dsn->isTcp() 117 | ? StreamConnection::createTcp($dsn->getConnectionUri(), $connectionOptions) 118 | : StreamConnection::createUds($dsn->getConnectionUri(), $connectionOptions); 119 | 120 | $handler = new DefaultHandler($connection, $packer ?? PurePacker::fromAvailableExtensions()); 121 | 122 | return $middleware 123 | ? new self(MiddlewareHandler::append($handler, $middleware)) 124 | : new self($handler); 125 | } 126 | 127 | public function withMiddleware(Middleware ...$middleware) : self 128 | { 129 | $new = clone $this; 130 | $new->handler = MiddlewareHandler::append($new->handler, $middleware); 131 | 132 | return $new; 133 | } 134 | 135 | public function withPrependedMiddleware(Middleware ...$middleware) : self 136 | { 137 | $new = clone $this; 138 | $new->handler = MiddlewareHandler::prepend($new->handler, $middleware); 139 | 140 | return $new; 141 | } 142 | 143 | public function getHandler() : Handler 144 | { 145 | return $this->handler; 146 | } 147 | 148 | public function getSpace(string $spaceName) : Space 149 | { 150 | if (isset($this->spaces[$spaceName])) { 151 | return $this->spaces[$spaceName]; 152 | } 153 | 154 | $spaceId = $this->getSpaceIdByName($spaceName); 155 | 156 | return $this->spaces[$spaceName] = $this->spaces[$spaceId] = new Space($this->handler, $spaceId); 157 | } 158 | 159 | public function getSpaceById(int $spaceId) : Space 160 | { 161 | if (isset($this->spaces[$spaceId])) { 162 | return $this->spaces[$spaceId]; 163 | } 164 | 165 | return $this->spaces[$spaceId] = new Space($this->handler, $spaceId); 166 | } 167 | 168 | public function ping() : void 169 | { 170 | $this->handler->handle(new PingRequest()); 171 | } 172 | 173 | /** 174 | * @param mixed ...$args 175 | */ 176 | public function call(string $funcName, ...$args) : array 177 | { 178 | return $this->handler->handle(new CallRequest($funcName, $args)) 179 | ->getBodyField(Keys::DATA); 180 | } 181 | 182 | /** 183 | * @param mixed ...$args 184 | */ 185 | public function evaluate(string $expr, ...$args) : array 186 | { 187 | return $this->handler->handle(new EvaluateRequest($expr, $args)) 188 | ->getBodyField(Keys::DATA); 189 | } 190 | 191 | /** 192 | * @param mixed ...$params 193 | */ 194 | public function execute(string $sql, ...$params) : Response 195 | { 196 | return $this->handler->handle(ExecuteRequest::fromSql($sql, $params)); 197 | } 198 | 199 | /** 200 | * @param mixed ...$params 201 | */ 202 | public function executeQuery(string $sql, ...$params) : SqlQueryResult 203 | { 204 | $response = $this->handler->handle(ExecuteRequest::fromSql($sql, $params)); 205 | 206 | return new SqlQueryResult( 207 | $response->getBodyField(Keys::DATA), 208 | $response->getBodyField(Keys::METADATA) 209 | ); 210 | } 211 | 212 | /** 213 | * @param mixed ...$params 214 | */ 215 | public function executeUpdate(string $sql, ...$params) : SqlUpdateResult 216 | { 217 | $response = $this->handler->handle(ExecuteRequest::fromSql($sql, $params)); 218 | 219 | return new SqlUpdateResult($response->getBodyField(Keys::SQL_INFO)); 220 | } 221 | 222 | public function prepare(string $sql) : PreparedStatement 223 | { 224 | $response = $this->handler->handle(PrepareRequest::fromSql($sql)); 225 | 226 | return new PreparedStatement( 227 | $this->handler, 228 | $response->getBodyField(Keys::STMT_ID), 229 | $response->getBodyField(Keys::BIND_COUNT), 230 | $response->getBodyField(Keys::BIND_METADATA), 231 | $response->tryGetBodyField(Keys::METADATA, []) 232 | ); 233 | } 234 | 235 | public function flushSpaces() : void 236 | { 237 | $this->spaces = []; 238 | } 239 | 240 | public function __clone() 241 | { 242 | $this->spaces = []; 243 | } 244 | 245 | private function getSpaceIdByName(string $spaceName) : int 246 | { 247 | $schema = $this->getSpaceById(Space::VSPACE_ID); 248 | $data = $schema->select(Criteria::key([$spaceName])->andIndex(Space::VSPACE_NAME_INDEX)); 249 | 250 | if ($data) { 251 | return $data[0][0]; 252 | } 253 | 254 | throw RequestFailed::unknownSpace($spaceName); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/Connection/Connection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Connection; 15 | 16 | use Tarantool\Client\Exception\CommunicationFailed; 17 | use Tarantool\Client\Exception\ConnectionFailed; 18 | 19 | interface Connection 20 | { 21 | /** 22 | * Opens a new connection or reuses an existing one. 23 | * 24 | * @throws ConnectionFailed 25 | * @throws CommunicationFailed 26 | */ 27 | public function open() : Greeting; 28 | 29 | /** 30 | * Closes an opened connection. 31 | */ 32 | public function close() : void; 33 | 34 | /** 35 | * Indicates whether a connection is closed. 36 | */ 37 | public function isClosed() : bool; 38 | 39 | /** 40 | * Sends a MessagePack request and gets a MessagePack response back. 41 | * 42 | * @throws CommunicationFailed 43 | */ 44 | public function send(string $data) : string; 45 | } 46 | -------------------------------------------------------------------------------- /src/Connection/Greeting.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Connection; 15 | 16 | use Tarantool\Client\Exception\UnexpectedResponse; 17 | 18 | final class Greeting 19 | { 20 | public const SIZE_BYTES = 128; 21 | 22 | /** @var string */ 23 | private $greeting; 24 | 25 | /** @var string|null */ 26 | private $salt; 27 | 28 | /** @var string|null */ 29 | private $serverVersion; 30 | 31 | /** @var bool */ 32 | private $unknown = false; 33 | 34 | /** 35 | * @param string $greeting 36 | */ 37 | private function __construct($greeting) 38 | { 39 | $this->greeting = $greeting; 40 | } 41 | 42 | public static function parse(string $greeting) : self 43 | { 44 | if (0 === \strpos($greeting, 'Tarantool')) { 45 | return new self($greeting); 46 | } 47 | 48 | throw new UnexpectedResponse('Unable to recognize Tarantool server'); 49 | } 50 | 51 | public static function unknown() : self 52 | { 53 | $self = new self(''); 54 | $self->unknown = true; 55 | 56 | return $self; 57 | } 58 | 59 | public function getSalt() : string 60 | { 61 | if (null !== $this->salt) { 62 | return $this->salt; 63 | } 64 | 65 | if ($this->unknown) { 66 | throw new \BadMethodCallException('Salt is unknown for persistent connections'); 67 | } 68 | 69 | if (false === $salt = \base64_decode(\substr($this->greeting, 64, 44), true)) { 70 | throw new UnexpectedResponse('Unable to decode salt'); 71 | } 72 | 73 | $salt = \substr($salt, 0, 20); 74 | 75 | if (isset($salt[19])) { 76 | return $this->salt = $salt; 77 | } 78 | 79 | throw new UnexpectedResponse('Salt is too short'); 80 | } 81 | 82 | public function getServerVersion() : string 83 | { 84 | if (null !== $this->serverVersion) { 85 | return $this->serverVersion; 86 | } 87 | 88 | if ($this->unknown) { 89 | throw new \BadMethodCallException('Server version is unknown for persistent connections'); 90 | } 91 | 92 | return $this->serverVersion = \substr($this->greeting, 10, \strspn($this->greeting, '0123456789.', 10)); 93 | } 94 | 95 | public function equals(?self $greeting) : bool 96 | { 97 | if (!$greeting || $greeting->unknown) { 98 | return $this->unknown; 99 | } 100 | 101 | if ($this->unknown) { 102 | return true; 103 | } 104 | 105 | return $greeting->getSalt() === $this->getSalt(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Connection/StreamConnection.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Connection; 15 | 16 | use Tarantool\Client\Exception\CommunicationFailed; 17 | use Tarantool\Client\Exception\ConnectionFailed; 18 | use Tarantool\Client\Packer\PacketLength; 19 | 20 | final class StreamConnection implements Connection 21 | { 22 | public const DEFAULT_TCP_URI = 'tcp://127.0.0.1:3301'; 23 | 24 | /** @var string */ 25 | private $uri; 26 | 27 | /** @var float */ 28 | private $connectTimeout; 29 | 30 | /** @var float */ 31 | private $socketTimeout; 32 | 33 | /** @var bool */ 34 | private $persistent; 35 | 36 | /** @var resource|null */ 37 | private $streamContext; 38 | 39 | /** @var resource|null */ 40 | private $stream; 41 | 42 | /** @var Greeting|null */ 43 | private $greeting; 44 | 45 | /** 46 | * @param string $uri 47 | */ 48 | private function __construct($uri, float $connectTimeout, float $socketTimeout, bool $persistent, bool $tcpNoDelay) 49 | { 50 | $this->uri = $uri; 51 | $this->connectTimeout = $connectTimeout; 52 | $this->socketTimeout = $socketTimeout; 53 | $this->persistent = $persistent; 54 | 55 | if ($tcpNoDelay) { 56 | $this->streamContext = \stream_context_create(['socket' => ['tcp_nodelay' => true]]); 57 | } 58 | } 59 | 60 | public static function createTcp(string $uri = self::DEFAULT_TCP_URI, array $options = []) : self 61 | { 62 | return new self($uri, 63 | $options['connect_timeout'] ?? 5.0, 64 | $options['socket_timeout'] ?? 5.0, 65 | $options['persistent'] ?? false, 66 | $options['tcp_nodelay'] ?? false 67 | ); 68 | } 69 | 70 | public static function createUds(string $uri, array $options = []) : self 71 | { 72 | return new self($uri, 73 | $options['connect_timeout'] ?? 5.0, 74 | $options['socket_timeout'] ?? 5.0, 75 | $options['persistent'] ?? false, 76 | false 77 | ); 78 | } 79 | 80 | public static function create(string $uri, array $options = []) : self 81 | { 82 | return 0 === \strpos($uri, 'unix://') 83 | ? self::createUds($uri, $options) 84 | : self::createTcp($uri, $options); 85 | } 86 | 87 | public function open() : Greeting 88 | { 89 | if ($this->greeting) { 90 | return $this->greeting; 91 | } 92 | 93 | $flags = $this->persistent 94 | ? \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_PERSISTENT 95 | : \STREAM_CLIENT_CONNECT; 96 | 97 | $stream = $this->streamContext ? @\stream_socket_client( 98 | $this->uri, 99 | $errorCode, 100 | $errorMessage, 101 | $this->connectTimeout, 102 | $flags, 103 | $this->streamContext 104 | ) : @\stream_socket_client( 105 | $this->uri, 106 | $errorCode, 107 | $errorMessage, 108 | $this->connectTimeout, 109 | $flags 110 | ); 111 | 112 | if (false === $stream) { 113 | throw ConnectionFailed::fromUriAndReason($this->uri, $errorMessage); 114 | } 115 | 116 | $socketTimeoutSeconds = (int) $this->socketTimeout; 117 | $socketTimeoutMicroSeconds = (int) (($this->socketTimeout - $socketTimeoutSeconds) * 1000000); 118 | \stream_set_timeout($stream, $socketTimeoutSeconds, $socketTimeoutMicroSeconds); 119 | 120 | $this->stream = $stream; 121 | 122 | if ($this->persistent && \ftell($stream)) { 123 | return $this->greeting = Greeting::unknown(); 124 | } 125 | 126 | $greeting = $this->read(Greeting::SIZE_BYTES, 'Error reading greeting'); 127 | 128 | return $this->greeting = Greeting::parse($greeting); 129 | } 130 | 131 | public function close() : void 132 | { 133 | if ($this->stream) { 134 | /** @psalm-suppress InvalidPropertyAssignmentValue */ 135 | \fclose($this->stream); 136 | } 137 | 138 | $this->stream = null; 139 | $this->greeting = null; 140 | } 141 | 142 | public function isClosed() : bool 143 | { 144 | return !$this->stream; 145 | } 146 | 147 | public function send(string $data) : string 148 | { 149 | if (!$this->stream) { 150 | throw new CommunicationFailed('Error writing request: connection closed'); 151 | } 152 | 153 | if (!\fwrite($this->stream, $data)) { 154 | throw CommunicationFailed::withLastPhpError('Error writing request'); 155 | } 156 | 157 | $length = $this->read(PacketLength::SIZE_BYTES, 'Error reading response length'); 158 | $length = PacketLength::unpack($length); 159 | 160 | return $this->read($length, 'Error reading response'); 161 | } 162 | 163 | private function read(int $length, string $errorMessage) : string 164 | { 165 | /** @psalm-suppress PossiblyNullArgument */ 166 | if ($data = \stream_get_contents($this->stream, $length)) { 167 | return $data; 168 | } 169 | 170 | /** @psalm-suppress PossiblyNullArgument */ 171 | $meta = \stream_get_meta_data($this->stream); 172 | if ($meta['timed_out']) { 173 | throw new CommunicationFailed('Read timed out'); 174 | } 175 | 176 | throw CommunicationFailed::withLastPhpError($errorMessage); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Dsn.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client; 15 | 16 | final class Dsn 17 | { 18 | /** @var string|null */ 19 | private $host; 20 | 21 | /** @var int|null */ 22 | private $port; 23 | 24 | /** @var string|null */ 25 | private $path; 26 | 27 | /** @var string */ 28 | private $connectionUri; 29 | 30 | /** @var string|null */ 31 | private $username; 32 | 33 | /** @var string|null */ 34 | private $password; 35 | 36 | /** @var bool */ 37 | private $isTcp = false; 38 | 39 | /** 40 | * @var array 41 | * @psalm-suppress PropertyNotSetInConstructor 42 | */ 43 | private $options; 44 | 45 | /** 46 | * @param string $connectionUri 47 | */ 48 | private function __construct($connectionUri) 49 | { 50 | $this->connectionUri = $connectionUri; 51 | } 52 | 53 | public static function parse(string $dsn) : self 54 | { 55 | if (0 === \strpos($dsn, 'unix://') && isset($dsn[7])) { 56 | return self::parseUds($dsn); 57 | } 58 | 59 | if (false === $parsed = \parse_url($dsn)) { 60 | self::throwParseError($dsn); 61 | } 62 | if (!isset($parsed['scheme'], $parsed['host']) || 'tcp' !== $parsed['scheme']) { 63 | self::throwParseError($dsn); 64 | } 65 | if (isset($parsed['path']) && '/' !== $parsed['path']) { 66 | self::throwParseError($dsn); 67 | } 68 | 69 | $self = new self('tcp://'.$parsed['host'].':'.($parsed['port'] ?? '3301')); 70 | $self->host = $parsed['host']; 71 | $self->port = $parsed['port'] ?? 3301; 72 | $self->isTcp = true; 73 | 74 | if (isset($parsed['user'])) { 75 | $self->username = \rawurldecode($parsed['user']); 76 | $self->password = isset($parsed['pass']) ? \rawurldecode($parsed['pass']) : ''; 77 | } 78 | 79 | if (isset($parsed['query'])) { 80 | \parse_str($parsed['query'], $self->options); 81 | } 82 | 83 | return $self; 84 | } 85 | 86 | private static function parseUds(string $dsn) : self 87 | { 88 | $parts = \explode('@', \substr($dsn, 7), 2); 89 | if (isset($parts[1])) { 90 | $parsed = \parse_url($parts[1]); 91 | $authority = \explode(':', $parts[0]); 92 | } else { 93 | $parsed = \parse_url($parts[0]); 94 | } 95 | 96 | if (false === $parsed) { 97 | self::throwParseError($dsn); 98 | } 99 | if (isset($parsed['host']) || !isset($parsed['path'])) { 100 | self::throwParseError($dsn); 101 | } 102 | 103 | $self = new self('unix://'.$parsed['path']); 104 | $self->path = \rawurldecode($parsed['path']); 105 | 106 | if (isset($authority)) { 107 | $self->username = \rawurldecode($authority[0]); 108 | $self->password = isset($authority[1]) ? \rawurldecode($authority[1]) : ''; 109 | } 110 | 111 | if (isset($parsed['query'])) { 112 | \parse_str($parsed['query'], $self->options); 113 | } 114 | 115 | return $self; 116 | } 117 | 118 | public function getConnectionUri() : string 119 | { 120 | return $this->connectionUri; 121 | } 122 | 123 | public function getHost() : ?string 124 | { 125 | return $this->host; 126 | } 127 | 128 | public function getPort() : ?int 129 | { 130 | return $this->port; 131 | } 132 | 133 | public function getPath() : ?string 134 | { 135 | return $this->path; 136 | } 137 | 138 | public function getUsername() : ?string 139 | { 140 | return $this->username; 141 | } 142 | 143 | public function getPassword() : ?string 144 | { 145 | return $this->password; 146 | } 147 | 148 | public function isTcp() : bool 149 | { 150 | return $this->isTcp; 151 | } 152 | 153 | public function getString(string $name, ?string $default = null) : ?string 154 | { 155 | return $this->options[$name] ?? $default; 156 | } 157 | 158 | public function getBool(string $name, ?bool $default = null) : ?bool 159 | { 160 | if (!isset($this->options[$name])) { 161 | return $default; 162 | } 163 | 164 | if (null === $value = \filter_var($this->options[$name], \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE)) { 165 | throw new \TypeError(\sprintf('DSN option "%s" must be of type bool', $name)); 166 | } 167 | 168 | return $value; 169 | } 170 | 171 | public function getInt(string $name, ?int $default = null) : ?int 172 | { 173 | if (!isset($this->options[$name])) { 174 | return $default; 175 | } 176 | 177 | if (false === $value = \filter_var($this->options[$name], \FILTER_VALIDATE_INT)) { 178 | throw new \TypeError(\sprintf('DSN option "%s" must be of type int', $name)); 179 | } 180 | 181 | return $value; 182 | } 183 | 184 | public function getFloat(string $name, ?float $default = null) : ?float 185 | { 186 | if (!isset($this->options[$name])) { 187 | return $default; 188 | } 189 | 190 | if (false === $value = \filter_var($this->options[$name], \FILTER_VALIDATE_FLOAT)) { 191 | throw new \TypeError(\sprintf('DSN option "%s" must be of type float', $name)); 192 | } 193 | 194 | return $value; 195 | } 196 | 197 | /** 198 | * @psalm-return never-returns 199 | */ 200 | private static function throwParseError(string $dsn) : void 201 | { 202 | throw new \InvalidArgumentException(\sprintf('Unable to parse DSN "%s"', $dsn)); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/Error.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client; 15 | 16 | final class Error 17 | { 18 | private $type; 19 | private $file; 20 | private $line; 21 | private $message; 22 | private $number; 23 | private $code; 24 | private $fields; 25 | private $previous; 26 | 27 | public function __construct( 28 | string $type, 29 | string $file, 30 | int $line, 31 | string $message, 32 | int $number, 33 | int $code, 34 | array $fields = [], 35 | ?self $previous = null 36 | ) { 37 | $this->type = $type; 38 | $this->file = $file; 39 | $this->line = $line; 40 | $this->message = $message; 41 | $this->number = $number; 42 | $this->code = $code; 43 | $this->fields = $fields; 44 | $this->previous = $previous; 45 | } 46 | 47 | public static function fromMap(array $errorMap) : self 48 | { 49 | if (empty($errorMap[Keys::ERROR_STACK])) { 50 | throw new \InvalidArgumentException('The error map should contain a non-empty error stack'); 51 | } 52 | 53 | $errorStack = $errorMap[Keys::ERROR_STACK]; 54 | 55 | $first = \count($errorStack) - 1; 56 | $error = new self( 57 | $errorStack[$first][Keys::ERROR_TYPE], 58 | $errorStack[$first][Keys::ERROR_FILE], 59 | $errorStack[$first][Keys::ERROR_LINE], 60 | $errorStack[$first][Keys::ERROR_MESSAGE], 61 | $errorStack[$first][Keys::ERROR_NUMBER], 62 | $errorStack[$first][Keys::ERROR_CODE], 63 | $errorStack[$first][Keys::ERROR_FIELDS] ?? [] 64 | ); 65 | 66 | if (0 === $first) { 67 | return $error; 68 | } 69 | 70 | for ($i = $first - 1; $i >= 0; --$i) { 71 | $error = new self( 72 | $errorStack[$i][Keys::ERROR_TYPE], 73 | $errorStack[$i][Keys::ERROR_FILE], 74 | $errorStack[$i][Keys::ERROR_LINE], 75 | $errorStack[$i][Keys::ERROR_MESSAGE], 76 | $errorStack[$i][Keys::ERROR_NUMBER], 77 | $errorStack[$i][Keys::ERROR_CODE], 78 | $errorStack[$i][Keys::ERROR_FIELDS] ?? [], 79 | $error 80 | ); 81 | } 82 | 83 | return $error; 84 | } 85 | 86 | public function toMap() : array 87 | { 88 | $error = $this; 89 | $errorStack = []; 90 | 91 | do { 92 | $errorStack[] = [ 93 | Keys::ERROR_TYPE => $error->type, 94 | Keys::ERROR_FILE => $error->file, 95 | Keys::ERROR_LINE => $error->line, 96 | Keys::ERROR_MESSAGE => $error->message, 97 | Keys::ERROR_NUMBER => $error->number, 98 | Keys::ERROR_CODE => $error->code, 99 | Keys::ERROR_FIELDS => $error->fields, 100 | ]; 101 | } while ($error = $error->getPrevious()); 102 | 103 | return [Keys::ERROR_STACK => $errorStack]; 104 | } 105 | 106 | public function getType() : string 107 | { 108 | return $this->type; 109 | } 110 | 111 | public function getFile() : string 112 | { 113 | return $this->file; 114 | } 115 | 116 | public function getLine() : int 117 | { 118 | return $this->line; 119 | } 120 | 121 | public function getMessage() : string 122 | { 123 | return $this->message; 124 | } 125 | 126 | public function getNumber() : int 127 | { 128 | return $this->number; 129 | } 130 | 131 | public function getCode() : int 132 | { 133 | return $this->code; 134 | } 135 | 136 | public function getFields() : array 137 | { 138 | return $this->fields; 139 | } 140 | 141 | /** 142 | * @return mixed 143 | */ 144 | public function getField(string $name) 145 | { 146 | if (\array_key_exists($name, $this->fields)) { 147 | return $this->fields[$name]; 148 | } 149 | 150 | throw new \OutOfRangeException(\sprintf('The field "%s" does not exist', $name)); 151 | } 152 | 153 | /** 154 | * @param mixed $default 155 | * @return mixed 156 | */ 157 | public function tryGetField(string $name, $default = null) 158 | { 159 | return \array_key_exists($name, $this->fields) ? $this->fields[$name] : $default; 160 | } 161 | 162 | public function getPrevious() : ?self 163 | { 164 | return $this->previous; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Exception/ClientException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Exception; 15 | 16 | interface ClientException extends \Throwable 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exception/CommunicationFailed.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Exception; 15 | 16 | final class CommunicationFailed extends \RuntimeException implements ClientException 17 | { 18 | public static function withLastPhpError(string $errorMessage) : self 19 | { 20 | $error = \error_get_last(); 21 | 22 | return new self($error ? \sprintf('%s: %s', $errorMessage, $error['message']) : $errorMessage); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exception/ConnectionFailed.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Exception; 15 | 16 | final class ConnectionFailed extends \RuntimeException implements ClientException 17 | { 18 | public static function fromUriAndReason(string $uri, string $reason) : self 19 | { 20 | throw new self(\sprintf('Failed to connect to %s: %s', $uri, $reason)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/RequestDenied.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Exception; 15 | 16 | use Tarantool\Client\Request\Request; 17 | 18 | final class RequestDenied extends \RuntimeException implements ClientException 19 | { 20 | public static function fromObject(Request $request) : self 21 | { 22 | return new self(\sprintf('Request "%s" is denied', \get_class($request))); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exception/RequestFailed.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Exception; 15 | 16 | use Tarantool\Client\Error; 17 | use Tarantool\Client\Keys; 18 | use Tarantool\Client\Response; 19 | 20 | final class RequestFailed extends \RuntimeException implements ClientException 21 | { 22 | /** @var Error|null */ 23 | private $error; 24 | 25 | public function getError() : ?Error 26 | { 27 | return $this->error; 28 | } 29 | 30 | public static function fromErrorResponse(Response $response) : self 31 | { 32 | $self = new self( 33 | $response->getBodyField(Keys::ERROR_24), 34 | $response->getCode() & (Response::TYPE_ERROR - 1) 35 | ); 36 | 37 | if ($error = $response->tryGetBodyField(Keys::ERROR)) { 38 | $self->error = Error::fromMap($error); 39 | } 40 | 41 | return $self; 42 | } 43 | 44 | public static function unknownSpace(string $spaceName) : self 45 | { 46 | return new self(\sprintf("Space '%s' does not exist", $spaceName)); 47 | } 48 | 49 | public static function unknownIndex(string $indexName, int $spaceId) : self 50 | { 51 | return new self(\sprintf("No index '%s' is defined in space #%d", $indexName, $spaceId)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Exception/UnexpectedResponse.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Exception; 15 | 16 | final class UnexpectedResponse extends \RuntimeException implements ClientException 17 | { 18 | public static function outOfSync(int $expectedSync, int $actualSync) : self 19 | { 20 | return new self("Unexpected response received: expected sync #$expectedSync, got #$actualSync"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Handler/DefaultHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Handler; 15 | 16 | use Tarantool\Client\Connection\Connection; 17 | use Tarantool\Client\Exception\RequestFailed; 18 | use Tarantool\Client\Exception\UnexpectedResponse; 19 | use Tarantool\Client\Packer\Packer; 20 | use Tarantool\Client\Request\Request; 21 | use Tarantool\Client\Response; 22 | 23 | final class DefaultHandler implements Handler 24 | { 25 | private $connection; 26 | private $packer; 27 | 28 | public function __construct(Connection $connection, Packer $packer) 29 | { 30 | $this->connection = $connection; 31 | $this->packer = $packer; 32 | } 33 | 34 | public function handle(Request $request) : Response 35 | { 36 | $packet = $this->packer->pack($request, $sync = \mt_rand()); 37 | $this->connection->open(); 38 | $packet = $this->connection->send($packet); 39 | 40 | $response = $this->packer->unpack($packet); 41 | 42 | if ($sync !== $response->getSync()) { 43 | $this->connection->close(); 44 | throw UnexpectedResponse::outOfSync($sync, $response->getSync()); 45 | } 46 | 47 | if ($response->isError()) { 48 | throw RequestFailed::fromErrorResponse($response); 49 | } 50 | 51 | return $response; 52 | } 53 | 54 | public function getConnection() : Connection 55 | { 56 | return $this->connection; 57 | } 58 | 59 | public function getPacker() : Packer 60 | { 61 | return $this->packer; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Handler/Handler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Handler; 15 | 16 | use Tarantool\Client\Connection\Connection; 17 | use Tarantool\Client\Exception\ClientException; 18 | use Tarantool\Client\Packer\Packer; 19 | use Tarantool\Client\Request\Request; 20 | use Tarantool\Client\Response; 21 | 22 | interface Handler 23 | { 24 | /** 25 | * @throws ClientException 26 | */ 27 | public function handle(Request $request) : Response; 28 | 29 | public function getConnection() : Connection; 30 | 31 | public function getPacker() : Packer; 32 | } 33 | -------------------------------------------------------------------------------- /src/Handler/MiddlewareHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Handler; 15 | 16 | use Tarantool\Client\Connection\Connection; 17 | use Tarantool\Client\Middleware\Middleware; 18 | use Tarantool\Client\Packer\Packer; 19 | use Tarantool\Client\Request\Request; 20 | use Tarantool\Client\Response; 21 | 22 | final class MiddlewareHandler implements Handler 23 | { 24 | /** @var Handler */ 25 | private $handler; 26 | 27 | /** @var Middleware[] */ 28 | private $middleware; 29 | 30 | /** @var int */ 31 | private $index = 0; 32 | 33 | /** 34 | * @param Handler $handler 35 | * @param Middleware[] $middleware 36 | */ 37 | private function __construct($handler, $middleware) 38 | { 39 | $this->handler = $handler; 40 | $this->middleware = $middleware; 41 | } 42 | 43 | /** 44 | * @param Middleware[] $middleware 45 | */ 46 | public static function append(Handler $handler, array $middleware) : Handler 47 | { 48 | if (!$handler instanceof self) { 49 | return new self($handler, $middleware); 50 | } 51 | 52 | $handler = clone $handler; 53 | $handler->middleware = \array_merge($handler->middleware, $middleware); 54 | 55 | return $handler; 56 | } 57 | 58 | /** 59 | * @param Middleware[] $middleware 60 | */ 61 | public static function prepend(Handler $handler, array $middleware) : Handler 62 | { 63 | if (!$handler instanceof self) { 64 | return new self($handler, $middleware); 65 | } 66 | 67 | $handler = clone $handler; 68 | $handler->middleware = \array_merge($middleware, $handler->middleware); 69 | 70 | return $handler; 71 | } 72 | 73 | public function handle(Request $request) : Response 74 | { 75 | if (!isset($this->middleware[$this->index])) { 76 | return $this->handler->handle($request); 77 | } 78 | 79 | $new = clone $this; 80 | ++$new->index; 81 | 82 | return $this->middleware[$this->index]->process($request, $new); 83 | } 84 | 85 | public function getConnection() : Connection 86 | { 87 | return $this->handler->getConnection(); 88 | } 89 | 90 | public function getPacker() : Packer 91 | { 92 | return $this->handler->getPacker(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Keys.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client; 15 | 16 | /** 17 | * @see https://www.tarantool.io/en/doc/latest/dev_guide/internals/box_protocol/ 18 | * @see https://github.com/tarantool/tarantool/blob/master/src/box/iproto_constants.h 19 | */ 20 | final class Keys 21 | { 22 | public const CODE = 0x00; 23 | public const SYNC = 0x01; 24 | public const SCHEMA_ID = 0x05; 25 | public const SPACE_ID = 0x10; 26 | public const INDEX_ID = 0x11; 27 | public const LIMIT = 0x12; 28 | public const OFFSET = 0x13; 29 | public const ITERATOR = 0x14; 30 | public const KEY = 0x20; 31 | public const TUPLE = 0x21; 32 | public const FUNCTION_NAME = 0x22; 33 | public const USER_NAME = 0x23; 34 | public const EXPR = 0x27; 35 | public const OPERATIONS = 0x28; 36 | public const DATA = 0x30; 37 | public const ERROR_24 = 0x31; 38 | public const METADATA = 0x32; 39 | public const BIND_METADATA = 0x33; 40 | public const BIND_COUNT = 0x34; 41 | public const SQL_TEXT = 0x40; 42 | public const SQL_BIND = 0x41; 43 | public const SQL_INFO = 0x42; 44 | public const STMT_ID = 0x43; 45 | public const ERROR = 0x52; 46 | 47 | // Sql info map keys 48 | // https://github.com/tarantool/tarantool/blob/master/src/box/execute.h 49 | public const SQL_INFO_ROW_COUNT = 0; 50 | public const SQL_INFO_AUTO_INCREMENT_IDS = 1; 51 | 52 | // Metadata map keys 53 | // https://github.com/tarantool/tarantool/blob/master/src/box/iproto_constants.h 54 | public const METADATA_FIELD_NAME = 0; 55 | public const METADATA_FIELD_TYPE = 1; 56 | public const METADATA_FIELD_COLL = 2; 57 | public const METADATA_FIELD_IS_NULLABLE = 3; 58 | public const METADATA_FIELD_IS_AUTOINCREMENT = 4; 59 | public const METADATA_FIELD_SPAN = 5; 60 | 61 | // Error map keys 62 | // https://github.com/tarantool/tarantool/blob/master/src/box/mp_error.cc 63 | public const ERROR_STACK = 0; 64 | public const ERROR_TYPE = 0; 65 | public const ERROR_FILE = 1; 66 | public const ERROR_LINE = 2; 67 | public const ERROR_MESSAGE = 3; 68 | public const ERROR_NUMBER = 4; 69 | public const ERROR_CODE = 5; 70 | public const ERROR_FIELDS = 6; 71 | 72 | private function __construct() 73 | { 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Middleware/AuthenticationMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Middleware; 15 | 16 | use Tarantool\Client\Connection\Greeting; 17 | use Tarantool\Client\Handler\Handler; 18 | use Tarantool\Client\Request\AuthenticateRequest; 19 | use Tarantool\Client\Request\Request; 20 | use Tarantool\Client\Response; 21 | 22 | final class AuthenticationMiddleware implements Middleware 23 | { 24 | private $username; 25 | private $password; 26 | 27 | /** @var Greeting|null */ 28 | private $greeting; 29 | 30 | public function __construct(string $username, string $password = '') 31 | { 32 | $this->username = $username; 33 | $this->password = $password; 34 | } 35 | 36 | public function process(Request $request, Handler $handler) : Response 37 | { 38 | $greeting = $handler->getConnection()->open(); 39 | 40 | if ($greeting->equals($this->greeting)) { 41 | return $handler->handle($request); 42 | } 43 | 44 | $handler->handle(new AuthenticateRequest( 45 | $greeting->getSalt(), 46 | $this->username, 47 | $this->password 48 | )); 49 | 50 | $this->greeting = $greeting; 51 | 52 | return $handler->handle($request); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Middleware/CustomErrorMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Middleware; 15 | 16 | use Tarantool\Client\Error; 17 | use Tarantool\Client\Exception\RequestFailed; 18 | use Tarantool\Client\Handler\Handler; 19 | use Tarantool\Client\Request\Request; 20 | use Tarantool\Client\Response; 21 | 22 | final class CustomErrorMiddleware implements Middleware 23 | { 24 | /** @var \Closure(Error, RequestFailed) : \Exception */ 25 | private $factory; 26 | 27 | /** 28 | * @param \Closure(Error, RequestFailed) : \Exception $factory 29 | */ 30 | private function __construct($factory) 31 | { 32 | $this->factory = $factory; 33 | } 34 | 35 | /** 36 | * Creates the middleware from a provided closure which receives 37 | * `Tarantool\Client\Error` and `Tarantool\Client\Exception\RequestFailed` 38 | * objects as arguments and should return a custom exception. 39 | * 40 | * Example: 41 | * 42 | * ```php 43 | * $middleware = CustomErrorMiddleware::fromFactory( 44 | * static function (Error $err, RequestFailed $ex) : \Exception { 45 | * return 'UserNotFound' === $err->tryGetField('custom_type') 46 | * ? new UserNotFound($err->getMessage(), $err->getCode()) 47 | * : $ex; 48 | * } 49 | * ); 50 | * ``` 51 | */ 52 | public static function fromFactory(\Closure $factory) : self 53 | { 54 | return new self( 55 | static function (Error $err, RequestFailed $ex) use ($factory) : \Exception { 56 | return $factory($err, $ex); 57 | } 58 | ); 59 | } 60 | 61 | /** 62 | * Creates the middleware from an array in which the keys are custom types 63 | * of box.error objects and the corresponding values are fully qualified names 64 | * of custom exception classes. 65 | * 66 | * Example: 67 | * 68 | * ```php 69 | * $middleware = CustomErrorMiddleware::fromMapping([ 70 | * 'UserNotFound' => UserNotFound::class, 71 | * 'MyCustomType' => MyCustomException::class, 72 | * ... 73 | * ]); 74 | * ``` 75 | * 76 | * @param array> $mapping 77 | */ 78 | public static function fromMapping(array $mapping) : self 79 | { 80 | return new self( 81 | static function (Error $err, RequestFailed $ex) use ($mapping) : \Exception { 82 | do { 83 | $customType = $err->tryGetField('custom_type'); 84 | if ($customType && isset($mapping[$customType])) { 85 | /** @psalm-suppress UnsafeInstantiation */ 86 | return new $mapping[$customType]($err->getMessage(), $err->getCode()); 87 | } 88 | } while ($err = $err->getPrevious()); 89 | 90 | return $ex; 91 | } 92 | ); 93 | } 94 | 95 | /** 96 | * Creates the middleware from the base namespace for the custom exception classes. 97 | * The exception class name then will be in the format of "\". 98 | * 99 | * Example: 100 | * 101 | * ```php 102 | * $middleware = CustomErrorMiddleware::fromNamespace('Foo\Bar'); 103 | * ``` 104 | */ 105 | public static function fromNamespace(string $namespace) : self 106 | { 107 | $namespace = \rtrim($namespace, '\\').'\\'; 108 | 109 | return new self( 110 | static function (Error $err, RequestFailed $ex) use ($namespace) : \Exception { 111 | if (!$customType = $err->tryGetField('custom_type')) { 112 | return $ex; 113 | } 114 | 115 | /** @var class-string<\Exception> $className */ 116 | $className = $namespace.$customType; 117 | 118 | /** @psalm-suppress UnsafeInstantiation */ 119 | return \class_exists($className) 120 | ? new $className($err->getMessage(), $err->getCode()) 121 | : $ex; 122 | } 123 | ); 124 | } 125 | 126 | public function process(Request $request, Handler $handler) : Response 127 | { 128 | try { 129 | return $handler->handle($request); 130 | } catch (RequestFailed $e) { 131 | if ($error = $e->getError()) { 132 | throw ($this->factory)($error, $e); 133 | } 134 | 135 | throw $e; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Middleware/FirewallMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Middleware; 15 | 16 | use Tarantool\Client\Exception\RequestDenied; 17 | use Tarantool\Client\Handler\Handler; 18 | use Tarantool\Client\Request\Request; 19 | use Tarantool\Client\RequestTypes; 20 | use Tarantool\Client\Response; 21 | 22 | final class FirewallMiddleware implements Middleware 23 | { 24 | /** @var array */ 25 | private $allowed; 26 | 27 | /** @var array */ 28 | private $denied; 29 | 30 | /** 31 | * @param array $allowed 32 | * @param array $denied 33 | */ 34 | private function __construct($allowed, $denied) 35 | { 36 | $this->allowed = $allowed ? \array_fill_keys($allowed, true) : []; 37 | $this->denied = $denied ? \array_fill_keys($denied, true) : []; 38 | } 39 | 40 | public static function allow(int $requestType, int ...$requestTypes) : self 41 | { 42 | return new self([-1 => $requestType] + $requestTypes, []); 43 | } 44 | 45 | public static function deny(int $requestType, int ...$requestTypes) : self 46 | { 47 | return new self([], [-1 => $requestType] + $requestTypes); 48 | } 49 | 50 | public static function allowReadOnly() : self 51 | { 52 | $self = new self([], []); 53 | $self->allowed = [ 54 | RequestTypes::AUTHENTICATE => true, 55 | RequestTypes::PING => true, 56 | RequestTypes::SELECT => true, 57 | ]; 58 | 59 | return $self; 60 | } 61 | 62 | public function andAllow(int $requestType, int ...$requestTypes) : self 63 | { 64 | $new = clone $this; 65 | $new->allowed += $requestTypes 66 | ? \array_fill_keys([-1 => $requestType] + $requestTypes, true) 67 | : [$requestType => true]; 68 | 69 | return $new; 70 | } 71 | 72 | public function andAllowOnly(int $requestType, int ...$requestTypes) : self 73 | { 74 | $new = clone $this; 75 | $new->allowed = $requestTypes 76 | ? \array_fill_keys([-1 => $requestType] + $requestTypes, true) 77 | : [$requestType => true]; 78 | 79 | return $new; 80 | } 81 | 82 | public function andDeny(int $requestType, int ...$requestTypes) : self 83 | { 84 | $new = clone $this; 85 | $new->denied += $requestTypes 86 | ? \array_fill_keys([-1 => $requestType] + $requestTypes, true) 87 | : [$requestType => true]; 88 | 89 | return $new; 90 | } 91 | 92 | public function andDenyOnly(int $requestType, int ...$requestTypes) : self 93 | { 94 | $new = clone $this; 95 | $new->denied = $requestTypes 96 | ? \array_fill_keys([-1 => $requestType] + $requestTypes, true) 97 | : [$requestType => true]; 98 | 99 | return $new; 100 | } 101 | 102 | public function process(Request $request, Handler $handler) : Response 103 | { 104 | $requestType = $request->getType(); 105 | 106 | if (isset($this->denied[$requestType])) { 107 | throw RequestDenied::fromObject($request); 108 | } 109 | 110 | if ([] !== $this->allowed && !isset($this->allowed[$requestType])) { 111 | throw RequestDenied::fromObject($request); 112 | } 113 | 114 | return $handler->handle($request); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Middleware/LoggingMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Middleware; 15 | 16 | use Psr\Log\LoggerInterface; 17 | use Tarantool\Client\Handler\Handler; 18 | use Tarantool\Client\Request\Request; 19 | use Tarantool\Client\RequestTypes; 20 | use Tarantool\Client\Response; 21 | 22 | final class LoggingMiddleware implements Middleware 23 | { 24 | private $logger; 25 | 26 | public function __construct(LoggerInterface $logger) 27 | { 28 | $this->logger = $logger; 29 | } 30 | 31 | public function process(Request $request, Handler $handler) : Response 32 | { 33 | $requestName = RequestTypes::getName($request->getType()); 34 | 35 | $this->logger->debug("Starting handling request \"$requestName\"", [ 36 | 'request' => $request, 37 | ]); 38 | 39 | $start = \microtime(true); 40 | try { 41 | $response = $handler->handle($request); 42 | } catch (\Throwable $e) { 43 | $this->logger->error("Request \"$requestName\" failed", [ 44 | 'request' => $request, 45 | 'exception' => $e, 46 | 'duration_ms' => \round((\microtime(true) - $start) * 1000), 47 | ]); 48 | 49 | throw $e; 50 | } 51 | 52 | $this->logger->debug("Finished handling request \"$requestName\"", [ 53 | 'request' => $request, 54 | 'response' => $response, 55 | 'duration_ms' => \round((\microtime(true) - $start) * 1000), 56 | ]); 57 | 58 | return $response; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Middleware/Middleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Middleware; 15 | 16 | use Tarantool\Client\Handler\Handler; 17 | use Tarantool\Client\Request\Request; 18 | use Tarantool\Client\Response; 19 | 20 | interface Middleware 21 | { 22 | public function process(Request $request, Handler $handler) : Response; 23 | } 24 | -------------------------------------------------------------------------------- /src/Middleware/RetryMiddleware.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Middleware; 15 | 16 | use Tarantool\Client\Exception\ClientException; 17 | use Tarantool\Client\Exception\CommunicationFailed; 18 | use Tarantool\Client\Exception\ConnectionFailed; 19 | use Tarantool\Client\Exception\UnexpectedResponse; 20 | use Tarantool\Client\Handler\Handler; 21 | use Tarantool\Client\Request\Request; 22 | use Tarantool\Client\Response; 23 | 24 | final class RetryMiddleware implements Middleware 25 | { 26 | private const DEFAULT_MAX_RETRIES = 2; 27 | private const MAX_RETRIES_LIMIT = 10; 28 | private const MAX_DELAY_MS = 60000; 29 | 30 | /** @var \Closure */ 31 | private $getDelayMs; 32 | 33 | /** 34 | * @param \Closure $getDelayMs 35 | */ 36 | private function __construct($getDelayMs) 37 | { 38 | $this->getDelayMs = $getDelayMs; 39 | } 40 | 41 | public static function constant(int $maxRetries = self::DEFAULT_MAX_RETRIES, int $intervalMs = 100) : self 42 | { 43 | return new self(static function (int $retries) use ($maxRetries, $intervalMs) { 44 | return $retries > $maxRetries ? null : $intervalMs; 45 | }); 46 | } 47 | 48 | public static function exponential(int $maxRetries = self::DEFAULT_MAX_RETRIES, int $baseMs = 10) : self 49 | { 50 | return new self(static function (int $retries) use ($maxRetries, $baseMs) { 51 | return $retries > $maxRetries ? null : $baseMs ** $retries; 52 | }); 53 | } 54 | 55 | public static function linear(int $maxRetries = self::DEFAULT_MAX_RETRIES, int $differenceMs = 100) : self 56 | { 57 | return new self(static function (int $retries) use ($maxRetries, $differenceMs) { 58 | return $retries > $maxRetries ? null : $differenceMs * $retries; 59 | }); 60 | } 61 | 62 | public static function custom(\Closure $getDelayMs) : self 63 | { 64 | return new self(static function (int $retries, \Throwable $e) use ($getDelayMs) : ?int { 65 | return $getDelayMs($retries, $e); 66 | }); 67 | } 68 | 69 | public function process(Request $request, Handler $handler) : Response 70 | { 71 | $retries = 0; 72 | 73 | do { 74 | try { 75 | return $handler->handle($request); 76 | } catch (UnexpectedResponse $e) { 77 | $handler->getConnection()->close(); 78 | break; 79 | } catch (ConnectionFailed | CommunicationFailed $e) { 80 | $handler->getConnection()->close(); 81 | goto retry; 82 | } catch (ClientException $e) { 83 | retry: 84 | if (self::MAX_RETRIES_LIMIT === $retries) { 85 | break; 86 | } 87 | if (null === $delayMs = ($this->getDelayMs)(++$retries, $e)) { 88 | break; 89 | } 90 | $delayMs = \min($delayMs, self::MAX_DELAY_MS) / 2; 91 | $delayMs += \mt_rand(0, $delayMs); 92 | \usleep($delayMs * 1000); 93 | } 94 | } while (true); 95 | 96 | throw $e; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Packer/Extension/DecimalExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Packer\Extension; 15 | 16 | use Decimal\Decimal; 17 | use MessagePack\BufferUnpacker; 18 | use MessagePack\Extension; 19 | use MessagePack\Packer; 20 | 21 | final class DecimalExtension implements Extension 22 | { 23 | private const TYPE = 1; 24 | private const PRECISION = 38; 25 | 26 | public function getType() : int 27 | { 28 | return self::TYPE; 29 | } 30 | 31 | public function pack(Packer $packer, $value) : ?string 32 | { 33 | if (!$value instanceof Decimal) { 34 | return null; 35 | } 36 | 37 | // @see https://github.com/php-decimal/ext-decimal/issues/22#issuecomment-512364914 38 | $data = $value->toFixed(self::PRECISION); 39 | 40 | if ('-' === $data[0]) { 41 | $nibble = 'd'; 42 | $data = \substr($data, 1); 43 | } else { 44 | $nibble = 'c'; 45 | } 46 | 47 | $pieces = \explode('.', $data, 2); 48 | $pieces[1] = \rtrim($pieces[1], '0'); 49 | 50 | $data = "{$pieces[0]}{$pieces[1]}{$nibble}"; 51 | if (0 !== \strlen($data) % 2) { 52 | $data = '0'.$data; 53 | } 54 | 55 | return $packer->packExt(self::TYPE, 56 | $packer->packInt('' === $pieces[1] ? 0 : \strlen($pieces[1])).\hex2bin($data) 57 | ); 58 | } 59 | 60 | /** 61 | * @return Decimal 62 | */ 63 | public function unpackExt(BufferUnpacker $unpacker, int $extLength) 64 | { 65 | /** 66 | * @psalm-suppress UndefinedDocblockClass (suppresses \GMP) 67 | * @var int $scale 68 | */ 69 | $scale = $unpacker->unpackInt(); 70 | $data = $unpacker->read($extLength - 1); 71 | $data = \bin2hex($data); 72 | 73 | $sign = 'd' === $data[-1] ? '-' : ''; 74 | $dec = \substr($data, 0, -1); 75 | 76 | if (0 !== $scale) { 77 | $length = \strlen($dec); 78 | $dec = ($length <= $scale) 79 | ? \substr_replace($dec, '0.'.\str_repeat('0', $scale - $length), -$scale, 0) 80 | : \substr_replace($dec, '.', -$scale, 0); 81 | } 82 | 83 | return new Decimal($sign.$dec, self::PRECISION); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Packer/Extension/ErrorExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Packer\Extension; 15 | 16 | use MessagePack\BufferUnpacker; 17 | use MessagePack\Extension; 18 | use MessagePack\Packer; 19 | use Tarantool\Client\Error; 20 | use Tarantool\Client\Keys; 21 | 22 | final class ErrorExtension implements Extension 23 | { 24 | private const TYPE = 3; 25 | 26 | public function getType() : int 27 | { 28 | return self::TYPE; 29 | } 30 | 31 | public function pack(Packer $packer, $value) : ?string 32 | { 33 | if (!$value instanceof Error) { 34 | return null; 35 | } 36 | 37 | [Keys::ERROR_STACK => $errorStack] = $value->toMap(); 38 | 39 | $packedError = $packer->packMapHeader(1); 40 | $packedError .= $packer->packInt(Keys::ERROR_STACK); 41 | $packedError .= $packer->packArrayHeader(\count($errorStack)); 42 | foreach ($errorStack as $error) { 43 | $packedError .= $packer->packMap($error); 44 | } 45 | 46 | return $packer->packExt(self::TYPE, $packedError); 47 | } 48 | 49 | /** 50 | * @return Error 51 | */ 52 | public function unpackExt(BufferUnpacker $unpacker, int $extLength) 53 | { 54 | return Error::fromMap($unpacker->unpackMap()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Packer/Extension/UuidExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Packer\Extension; 15 | 16 | use MessagePack\BufferUnpacker; 17 | use MessagePack\Extension; 18 | use MessagePack\Packer; 19 | use Symfony\Component\Uid\Uuid; 20 | 21 | final class UuidExtension implements Extension 22 | { 23 | private const TYPE = 2; 24 | 25 | public function getType() : int 26 | { 27 | return self::TYPE; 28 | } 29 | 30 | public function pack(Packer $packer, $value) : ?string 31 | { 32 | if (!$value instanceof Uuid) { 33 | return null; 34 | } 35 | 36 | return $packer->packExt(self::TYPE, $value->toBinary()); 37 | } 38 | 39 | /** 40 | * @return Uuid 41 | */ 42 | public function unpackExt(BufferUnpacker $unpacker, int $extLength) 43 | { 44 | return Uuid::fromString($unpacker->read($extLength)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Packer/Packer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Packer; 15 | 16 | use Tarantool\Client\Request\Request; 17 | use Tarantool\Client\Response; 18 | 19 | interface Packer 20 | { 21 | public function pack(Request $request, int $sync) : string; 22 | 23 | public function unpack(string $packet) : Response; 24 | } 25 | -------------------------------------------------------------------------------- /src/Packer/PacketLength.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Packer; 15 | 16 | final class PacketLength 17 | { 18 | public const SIZE_BYTES = 5; 19 | 20 | private function __construct() 21 | { 22 | } 23 | 24 | public static function pack(int $length) : string 25 | { 26 | return \pack('CN', 0xce, $length); 27 | } 28 | 29 | public static function unpack(string $data) : int 30 | { 31 | if (!isset($data[4]) || "\xce" !== $data[0]) { 32 | throw new \RuntimeException('Unable to unpack packet length'); 33 | } 34 | 35 | return \ord($data[1]) << 24 36 | | \ord($data[2]) << 16 37 | | \ord($data[3]) << 8 38 | | \ord($data[4]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Packer/PurePacker.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Packer; 15 | 16 | use MessagePack\BufferUnpacker; 17 | use MessagePack\Extension; 18 | use MessagePack\Packer; 19 | use MessagePack\PackOptions; 20 | use MessagePack\UnpackOptions; 21 | use Tarantool\Client\Keys; 22 | use Tarantool\Client\Packer\Extension\DecimalExtension; 23 | use Tarantool\Client\Packer\Extension\ErrorExtension; 24 | use Tarantool\Client\Packer\Extension\UuidExtension; 25 | use Tarantool\Client\Packer\Packer as ClientPacker; 26 | use Tarantool\Client\Request\Request; 27 | use Tarantool\Client\Response; 28 | 29 | final class PurePacker implements ClientPacker 30 | { 31 | /** @var Packer */ 32 | private $packer; 33 | 34 | /** @var BufferUnpacker */ 35 | private $unpacker; 36 | 37 | public function __construct(?Packer $packer = null, ?BufferUnpacker $unpacker = null) 38 | { 39 | $this->packer = $packer ?: new Packer(PackOptions::FORCE_STR); 40 | $this->unpacker = $unpacker ?: new BufferUnpacker('', \extension_loaded('decimal') ? UnpackOptions::BIGINT_AS_DEC : null); 41 | } 42 | 43 | public static function fromExtensions(Extension $extension, Extension ...$extensions) : self 44 | { 45 | $extensions = [-1 => $extension] + $extensions; 46 | 47 | return new self( 48 | new Packer(PackOptions::FORCE_STR, $extensions), 49 | new BufferUnpacker('', \extension_loaded('decimal') ? UnpackOptions::BIGINT_AS_DEC : null, $extensions) 50 | ); 51 | } 52 | 53 | public static function fromAvailableExtensions() : self 54 | { 55 | $extensions = [new UuidExtension(), new ErrorExtension()]; 56 | if (\extension_loaded('decimal')) { 57 | $extensions[] = new DecimalExtension(); 58 | 59 | return new self( 60 | new Packer(PackOptions::FORCE_STR, $extensions), 61 | new BufferUnpacker('', UnpackOptions::BIGINT_AS_DEC, $extensions) 62 | ); 63 | } 64 | 65 | return new self( 66 | new Packer(PackOptions::FORCE_STR, $extensions), 67 | new BufferUnpacker('', null, $extensions) 68 | ); 69 | } 70 | 71 | public function pack(Request $request, int $sync) : string 72 | { 73 | // Hot path optimization 74 | $packet = \pack('C*', 0x82, Keys::CODE, $request->getType(), Keys::SYNC). 75 | $this->packer->packInt($sync). 76 | $this->packer->packMap($request->getBody()); 77 | 78 | return PacketLength::pack(\strlen($packet)).$packet; 79 | } 80 | 81 | public function unpack(string $packet) : Response 82 | { 83 | $this->unpacker->reset($packet); 84 | 85 | return new Response( 86 | $this->unpacker->unpackMap(), 87 | $this->unpacker->unpackMap() 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/PreparedStatement.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client; 15 | 16 | use Tarantool\Client\Handler\Handler; 17 | use Tarantool\Client\Request\ExecuteRequest; 18 | use Tarantool\Client\Request\PrepareRequest; 19 | 20 | final class PreparedStatement 21 | { 22 | private $handler; 23 | private $id; 24 | private $bindCount; 25 | private $bindMetadata; 26 | private $metadata; 27 | 28 | public function __construct(Handler $handler, int $id, int $bindCount, array $bindMetadata, array $metadata) 29 | { 30 | $this->handler = $handler; 31 | $this->id = $id; 32 | $this->bindCount = $bindCount; 33 | $this->bindMetadata = $bindMetadata; 34 | $this->metadata = $metadata; 35 | } 36 | 37 | /** 38 | * @param mixed ...$params 39 | */ 40 | public function execute(...$params) : Response 41 | { 42 | return $this->handler->handle( 43 | ExecuteRequest::fromStatementId($this->id, $params) 44 | ); 45 | } 46 | 47 | /** 48 | * @param mixed ...$params 49 | */ 50 | public function executeQuery(...$params) : SqlQueryResult 51 | { 52 | $response = $this->handler->handle( 53 | ExecuteRequest::fromStatementId($this->id, $params) 54 | ); 55 | 56 | return new SqlQueryResult( 57 | $response->getBodyField(Keys::DATA), 58 | $response->getBodyField(Keys::METADATA) 59 | ); 60 | } 61 | 62 | /** 63 | * @param mixed ...$params 64 | */ 65 | public function executeUpdate(...$params) : SqlUpdateResult 66 | { 67 | $response = $this->handler->handle( 68 | ExecuteRequest::fromStatementId($this->id, $params) 69 | ); 70 | 71 | return new SqlUpdateResult($response->getBodyField(Keys::SQL_INFO)); 72 | } 73 | 74 | public function close() : void 75 | { 76 | $this->handler->handle(PrepareRequest::fromStatementId($this->id)); 77 | } 78 | 79 | public function getId() : int 80 | { 81 | return $this->id; 82 | } 83 | 84 | public function getBindCount() : int 85 | { 86 | return $this->bindCount; 87 | } 88 | 89 | public function getBindMetadata() : array 90 | { 91 | return $this->bindMetadata; 92 | } 93 | 94 | public function getMetadata() : array 95 | { 96 | return $this->metadata; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Request/AuthenticateRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class AuthenticateRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | public function __construct(string $salt, string $username, string $password = '') 25 | { 26 | $hash1 = \sha1($password, true); 27 | $hash2 = \sha1($hash1, true); 28 | 29 | $this->body = [ 30 | Keys::TUPLE => ['chap-sha1', $hash1 ^ \sha1($salt.$hash2, true)], 31 | Keys::USER_NAME => $username, 32 | ]; 33 | } 34 | 35 | public function getType() : int 36 | { 37 | return RequestTypes::AUTHENTICATE; 38 | } 39 | 40 | public function getBody() : array 41 | { 42 | return $this->body; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Request/CallRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class CallRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | public function __construct(string $funcName, array $args = []) 25 | { 26 | $this->body = [ 27 | Keys::FUNCTION_NAME => $funcName, 28 | Keys::TUPLE => $args, 29 | ]; 30 | } 31 | 32 | public function getType() : int 33 | { 34 | return RequestTypes::CALL; 35 | } 36 | 37 | public function getBody() : array 38 | { 39 | return $this->body; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Request/DeleteRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class DeleteRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | public function __construct(int $spaceId, int $indexId, array $key) 25 | { 26 | $this->body = [ 27 | Keys::SPACE_ID => $spaceId, 28 | Keys::INDEX_ID => $indexId, 29 | Keys::KEY => $key, 30 | ]; 31 | } 32 | 33 | public function getType() : int 34 | { 35 | return RequestTypes::DELETE; 36 | } 37 | 38 | public function getBody() : array 39 | { 40 | return $this->body; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Request/EvaluateRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class EvaluateRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | public function __construct(string $expr, array $args = []) 25 | { 26 | $this->body = [ 27 | Keys::EXPR => $expr, 28 | Keys::TUPLE => $args, 29 | ]; 30 | } 31 | 32 | public function getType() : int 33 | { 34 | return RequestTypes::EVALUATE; 35 | } 36 | 37 | public function getBody() : array 38 | { 39 | return $this->body; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Request/ExecuteRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class ExecuteRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | /** 25 | * @param non-empty-array $body 26 | */ 27 | private function __construct($body) 28 | { 29 | $this->body = $body; 30 | } 31 | 32 | public static function fromSql(string $sql, array $params = []) : self 33 | { 34 | return new self($params ? [ 35 | Keys::SQL_TEXT => $sql, 36 | Keys::SQL_BIND => $params, 37 | ] : [ 38 | Keys::SQL_TEXT => $sql, 39 | ]); 40 | } 41 | 42 | public static function fromStatementId(int $statementId, array $params = []) : self 43 | { 44 | return new self($params ? [ 45 | Keys::STMT_ID => $statementId, 46 | Keys::SQL_BIND => $params, 47 | ] : [ 48 | Keys::STMT_ID => $statementId, 49 | ]); 50 | } 51 | 52 | public function getType() : int 53 | { 54 | return RequestTypes::EXECUTE; 55 | } 56 | 57 | public function getBody() : array 58 | { 59 | return $this->body; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Request/InsertRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class InsertRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | public function __construct(int $spaceId, array $tuple) 25 | { 26 | $this->body = [ 27 | Keys::SPACE_ID => $spaceId, 28 | Keys::TUPLE => $tuple, 29 | ]; 30 | } 31 | 32 | public function getType() : int 33 | { 34 | return RequestTypes::INSERT; 35 | } 36 | 37 | public function getBody() : array 38 | { 39 | return $this->body; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Request/PingRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\RequestTypes; 17 | 18 | final class PingRequest implements Request 19 | { 20 | public function getType() : int 21 | { 22 | return RequestTypes::PING; 23 | } 24 | 25 | public function getBody() : array 26 | { 27 | return []; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Request/PrepareRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class PrepareRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | /** 25 | * @param non-empty-array $body 26 | */ 27 | private function __construct($body) 28 | { 29 | $this->body = $body; 30 | } 31 | 32 | public static function fromSql(string $sql) : self 33 | { 34 | return new self([Keys::SQL_TEXT => $sql]); 35 | } 36 | 37 | public static function fromStatementId(int $statementId) : self 38 | { 39 | return new self([Keys::STMT_ID => $statementId]); 40 | } 41 | 42 | public function getType() : int 43 | { 44 | return RequestTypes::PREPARE; 45 | } 46 | 47 | public function getBody() : array 48 | { 49 | return $this->body; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Request/ReplaceRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class ReplaceRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | public function __construct(int $spaceId, array $tuple) 25 | { 26 | $this->body = [ 27 | Keys::SPACE_ID => $spaceId, 28 | Keys::TUPLE => $tuple, 29 | ]; 30 | } 31 | 32 | public function getType() : int 33 | { 34 | return RequestTypes::REPLACE; 35 | } 36 | 37 | public function getBody() : array 38 | { 39 | return $this->body; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Request/Request.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | interface Request 17 | { 18 | public function getType() : int; 19 | 20 | public function getBody() : array; 21 | } 22 | -------------------------------------------------------------------------------- /src/Request/SelectRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class SelectRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | public function __construct(int $spaceId, int $indexId, array $key, int $offset, int $limit, int $iteratorType) 25 | { 26 | $this->body = [ 27 | Keys::KEY => $key, 28 | Keys::SPACE_ID => $spaceId, 29 | Keys::INDEX_ID => $indexId, 30 | Keys::LIMIT => $limit, 31 | Keys::OFFSET => $offset, 32 | Keys::ITERATOR => $iteratorType, 33 | ]; 34 | } 35 | 36 | public function getType() : int 37 | { 38 | return RequestTypes::SELECT; 39 | } 40 | 41 | public function getBody() : array 42 | { 43 | return $this->body; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Request/UpdateRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class UpdateRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | public function __construct(int $spaceId, int $indexId, array $key, array $operations) 25 | { 26 | $this->body = [ 27 | Keys::SPACE_ID => $spaceId, 28 | Keys::INDEX_ID => $indexId, 29 | Keys::KEY => $key, 30 | Keys::TUPLE => $operations, 31 | ]; 32 | } 33 | 34 | public function getType() : int 35 | { 36 | return RequestTypes::UPDATE; 37 | } 38 | 39 | public function getBody() : array 40 | { 41 | return $this->body; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Request/UpsertRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Request; 15 | 16 | use Tarantool\Client\Keys; 17 | use Tarantool\Client\RequestTypes; 18 | 19 | final class UpsertRequest implements Request 20 | { 21 | /** @var non-empty-array */ 22 | private $body; 23 | 24 | public function __construct(int $spaceId, array $tuple, array $operations) 25 | { 26 | $this->body = [ 27 | Keys::SPACE_ID => $spaceId, 28 | Keys::TUPLE => $tuple, 29 | Keys::OPERATIONS => $operations, 30 | ]; 31 | } 32 | 33 | public function getType() : int 34 | { 35 | return RequestTypes::UPSERT; 36 | } 37 | 38 | public function getBody() : array 39 | { 40 | return $this->body; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/RequestTypes.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client; 15 | 16 | final class RequestTypes 17 | { 18 | public const SELECT = 1; 19 | public const INSERT = 2; 20 | public const REPLACE = 3; 21 | public const UPDATE = 4; 22 | public const DELETE = 5; 23 | public const AUTHENTICATE = 7; 24 | public const EVALUATE = 8; 25 | public const UPSERT = 9; 26 | public const CALL = 10; 27 | public const EXECUTE = 11; 28 | public const PREPARE = 13; 29 | public const PING = 64; 30 | 31 | private const ALL = [ 32 | self::SELECT => 'select', 33 | self::INSERT => 'insert', 34 | self::REPLACE => 'replace', 35 | self::UPDATE => 'update', 36 | self::DELETE => 'delete', 37 | self::AUTHENTICATE => 'authenticate', 38 | self::EVALUATE => 'evaluate', 39 | self::UPSERT => 'upsert', 40 | self::CALL => 'call', 41 | self::EXECUTE => 'execute', 42 | self::PREPARE => 'prepare', 43 | self::PING => 'ping', 44 | ]; 45 | 46 | public static function getName(int $type) : string 47 | { 48 | if (isset(self::ALL[$type])) { 49 | return self::ALL[$type]; 50 | } 51 | 52 | throw new \InvalidArgumentException("Unknown request type #$type"); 53 | } 54 | 55 | private function __construct() 56 | { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client; 15 | 16 | final class Response 17 | { 18 | public const TYPE_ERROR = 0x8000; 19 | 20 | private $header; 21 | private $body; 22 | 23 | public function __construct(array $header, array $body) 24 | { 25 | $this->header = $header; 26 | $this->body = $body; 27 | } 28 | 29 | public function isError() : bool 30 | { 31 | $code = $this->header[Keys::CODE]; 32 | 33 | return $code >= self::TYPE_ERROR; 34 | } 35 | 36 | public function getCode() : int 37 | { 38 | return $this->header[Keys::CODE]; 39 | } 40 | 41 | public function getSync() : int 42 | { 43 | return $this->header[Keys::SYNC]; 44 | } 45 | 46 | public function getSchemaId() : int 47 | { 48 | return $this->header[Keys::SCHEMA_ID]; 49 | } 50 | 51 | /** 52 | * @return mixed 53 | */ 54 | public function getBodyField(int $key) 55 | { 56 | if (\array_key_exists($key, $this->body)) { 57 | return $this->body[$key]; 58 | } 59 | 60 | throw new \OutOfRangeException(\sprintf('The body key 0x%x does not exist', $key)); 61 | } 62 | 63 | /** 64 | * @param mixed $default 65 | * 66 | * @return mixed 67 | */ 68 | public function tryGetBodyField(int $key, $default = null) 69 | { 70 | return \array_key_exists($key, $this->body) ? $this->body[$key] : $default; 71 | } 72 | 73 | public function hasBodyField(int $key) : bool 74 | { 75 | return \array_key_exists($key, $this->body); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Schema/Criteria.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Schema; 15 | 16 | final class Criteria 17 | { 18 | /** @var int|string */ 19 | private $index = 0; 20 | 21 | /** @var array */ 22 | private $key = []; 23 | 24 | /** @var int */ 25 | private $limit = \PHP_INT_MAX & 0xffffffff; 26 | 27 | /** @var int */ 28 | private $offset = 0; 29 | 30 | /** @psalm-var null|IteratorTypes::* */ 31 | private $iteratorType; 32 | 33 | private function __construct() 34 | { 35 | } 36 | 37 | /** 38 | * @param int|string $index 39 | */ 40 | public static function index($index) : self 41 | { 42 | $self = new self(); 43 | $self->index = $index; 44 | 45 | return $self; 46 | } 47 | 48 | /** 49 | * @param int|string $index 50 | */ 51 | public function andIndex($index) : self 52 | { 53 | $new = clone $this; 54 | $new->index = $index; 55 | 56 | return $new; 57 | } 58 | 59 | /** 60 | * @return int|string 61 | */ 62 | public function getIndex() 63 | { 64 | return $this->index; 65 | } 66 | 67 | public static function key(array $key) : self 68 | { 69 | $self = new self(); 70 | $self->key = $key; 71 | 72 | return $self; 73 | } 74 | 75 | public function andKey(array $key) : self 76 | { 77 | $new = clone $this; 78 | $new->key = $key; 79 | 80 | return $new; 81 | } 82 | 83 | public function getKey() : array 84 | { 85 | return $this->key; 86 | } 87 | 88 | public static function limit(int $limit) : self 89 | { 90 | $self = new self(); 91 | $self->limit = $limit; 92 | 93 | return $self; 94 | } 95 | 96 | public function andLimit(int $limit) : self 97 | { 98 | $new = clone $this; 99 | $new->limit = $limit; 100 | 101 | return $new; 102 | } 103 | 104 | public function getLimit() : int 105 | { 106 | return $this->limit; 107 | } 108 | 109 | public static function offset(int $offset) : self 110 | { 111 | $self = new self(); 112 | $self->offset = $offset; 113 | 114 | return $self; 115 | } 116 | 117 | public function andOffset(int $offset) : self 118 | { 119 | $new = clone $this; 120 | $new->offset = $offset; 121 | 122 | return $new; 123 | } 124 | 125 | public function getOffset() : int 126 | { 127 | return $this->offset; 128 | } 129 | 130 | /** 131 | * @psalm-param IteratorTypes::* $iteratorType 132 | */ 133 | public static function iterator(int $iteratorType) : self 134 | { 135 | $self = new self(); 136 | $self->iteratorType = $iteratorType; 137 | 138 | return $self; 139 | } 140 | 141 | /** 142 | * @psalm-param IteratorTypes::* $iteratorType 143 | */ 144 | public function andIterator(int $iteratorType) : self 145 | { 146 | $new = clone $this; 147 | $new->iteratorType = $iteratorType; 148 | 149 | return $new; 150 | } 151 | 152 | public static function eqIterator() : self 153 | { 154 | return self::iterator(IteratorTypes::EQ); 155 | } 156 | 157 | public function andEqIterator() : self 158 | { 159 | return $this->andIterator(IteratorTypes::EQ); 160 | } 161 | 162 | public static function reqIterator() : self 163 | { 164 | return self::iterator(IteratorTypes::REQ); 165 | } 166 | 167 | public function andReqIterator() : self 168 | { 169 | return $this->andIterator(IteratorTypes::REQ); 170 | } 171 | 172 | public static function allIterator() : self 173 | { 174 | return self::iterator(IteratorTypes::ALL); 175 | } 176 | 177 | public function andAllIterator() : self 178 | { 179 | return $this->andIterator(IteratorTypes::ALL); 180 | } 181 | 182 | public static function ltIterator() : self 183 | { 184 | return self::iterator(IteratorTypes::LT); 185 | } 186 | 187 | public function andLtIterator() : self 188 | { 189 | return $this->andIterator(IteratorTypes::LT); 190 | } 191 | 192 | public static function leIterator() : self 193 | { 194 | return self::iterator(IteratorTypes::LE); 195 | } 196 | 197 | public function andLeIterator() : self 198 | { 199 | return $this->andIterator(IteratorTypes::LE); 200 | } 201 | 202 | public static function geIterator() : self 203 | { 204 | return self::iterator(IteratorTypes::GE); 205 | } 206 | 207 | public function andGeIterator() : self 208 | { 209 | return $this->andIterator(IteratorTypes::GE); 210 | } 211 | 212 | public static function gtIterator() : self 213 | { 214 | return self::iterator(IteratorTypes::GT); 215 | } 216 | 217 | public function andGtIterator() : self 218 | { 219 | return $this->andIterator(IteratorTypes::GT); 220 | } 221 | 222 | public static function bitsAllSetIterator() : self 223 | { 224 | return self::iterator(IteratorTypes::BITS_ALL_SET); 225 | } 226 | 227 | public function andBitsAllSetIterator() : self 228 | { 229 | return $this->andIterator(IteratorTypes::BITS_ALL_SET); 230 | } 231 | 232 | public static function bitsAnySetIterator() : self 233 | { 234 | return self::iterator(IteratorTypes::BITS_ANY_SET); 235 | } 236 | 237 | public function andBitsAnySetIterator() : self 238 | { 239 | return $this->andIterator(IteratorTypes::BITS_ANY_SET); 240 | } 241 | 242 | public static function bitsAllNotSetIterator() : self 243 | { 244 | return self::iterator(IteratorTypes::BITS_ALL_NOT_SET); 245 | } 246 | 247 | public function andBitsAllNotSetIterator() : self 248 | { 249 | return $this->andIterator(IteratorTypes::BITS_ALL_NOT_SET); 250 | } 251 | 252 | public static function overlapsIterator() : self 253 | { 254 | return self::iterator(IteratorTypes::OVERLAPS); 255 | } 256 | 257 | public function andOverlapsIterator() : self 258 | { 259 | return $this->andIterator(IteratorTypes::OVERLAPS); 260 | } 261 | 262 | public static function neighborIterator() : self 263 | { 264 | return self::iterator(IteratorTypes::NEIGHBOR); 265 | } 266 | 267 | public function andNeighborIterator() : self 268 | { 269 | return $this->andIterator(IteratorTypes::NEIGHBOR); 270 | } 271 | 272 | /** 273 | * @psalm-return IteratorTypes::* 274 | */ 275 | public function getIterator() : int 276 | { 277 | if (null !== $this->iteratorType) { 278 | return $this->iteratorType; 279 | } 280 | 281 | return $this->key ? IteratorTypes::EQ : IteratorTypes::ALL; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/Schema/IteratorTypes.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Schema; 15 | 16 | final class IteratorTypes 17 | { 18 | public const EQ = 0; 19 | public const REQ = 1; 20 | public const ALL = 2; 21 | public const LT = 3; 22 | public const LE = 4; 23 | public const GE = 5; 24 | public const GT = 6; 25 | public const BITS_ALL_SET = 7; 26 | public const BITS_ANY_SET = 8; 27 | public const BITS_ALL_NOT_SET = 9; 28 | public const OVERLAPS = 10; 29 | public const NEIGHBOR = 11; 30 | 31 | private function __construct() 32 | { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Schema/Operations.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Schema; 15 | 16 | final class Operations 17 | { 18 | /** @var non-empty-array */ 19 | private $operations; 20 | 21 | /** 22 | * @param non-empty-array $operation 23 | */ 24 | private function __construct($operation) 25 | { 26 | $this->operations = [$operation]; 27 | } 28 | 29 | /** 30 | * @param int|string $field 31 | */ 32 | public static function add($field, int $value) : self 33 | { 34 | return new self(['+', $field, $value]); 35 | } 36 | 37 | /** 38 | * @param int|string $field 39 | */ 40 | public function andAdd($field, int $value) : self 41 | { 42 | $new = clone $this; 43 | $new->operations[] = ['+', $field, $value]; 44 | 45 | return $new; 46 | } 47 | 48 | /** 49 | * @param int|string $field 50 | */ 51 | public static function subtract($field, int $value) : self 52 | { 53 | return new self(['-', $field, $value]); 54 | } 55 | 56 | /** 57 | * @param int|string $field 58 | */ 59 | public function andSubtract($field, int $value) : self 60 | { 61 | $new = clone $this; 62 | $new->operations[] = ['-', $field, $value]; 63 | 64 | return $new; 65 | } 66 | 67 | /** 68 | * @param int|string $field 69 | */ 70 | public static function bitwiseAnd($field, int $value) : self 71 | { 72 | return new self(['&', $field, $value]); 73 | } 74 | 75 | /** 76 | * @param int|string $field 77 | */ 78 | public function andBitwiseAnd($field, int $value) : self 79 | { 80 | $new = clone $this; 81 | $new->operations[] = ['&', $field, $value]; 82 | 83 | return $new; 84 | } 85 | 86 | /** 87 | * @param int|string $field 88 | */ 89 | public static function bitwiseOr($field, int $value) : self 90 | { 91 | return new self(['|', $field, $value]); 92 | } 93 | 94 | /** 95 | * @param int|string $field 96 | */ 97 | public function andBitwiseOr($field, int $value) : self 98 | { 99 | $new = clone $this; 100 | $new->operations[] = ['|', $field, $value]; 101 | 102 | return $new; 103 | } 104 | 105 | /** 106 | * @param int|string $field 107 | */ 108 | public static function bitwiseXor($field, int $value) : self 109 | { 110 | return new self(['^', $field, $value]); 111 | } 112 | 113 | /** 114 | * @param int|string $field 115 | */ 116 | public function andBitwiseXor($field, int $value) : self 117 | { 118 | $new = clone $this; 119 | $new->operations[] = ['^', $field, $value]; 120 | 121 | return $new; 122 | } 123 | 124 | /** 125 | * @param int|string $field 126 | */ 127 | public static function splice($field, int $offset, int $length, string $replacement) : self 128 | { 129 | return new self([':', $field, $offset, $length, $replacement]); 130 | } 131 | 132 | /** 133 | * @param int|string $field 134 | */ 135 | public function andSplice($field, int $offset, int $length, string $replacement) : self 136 | { 137 | $new = clone $this; 138 | $new->operations[] = [':', $field, $offset, $length, $replacement]; 139 | 140 | return $new; 141 | } 142 | 143 | /** 144 | * @param int|string $field 145 | */ 146 | public static function insert($field, int $value) : self 147 | { 148 | return new self(['!', $field, $value]); 149 | } 150 | 151 | /** 152 | * @param int|string $field 153 | */ 154 | public function andInsert($field, int $value) : self 155 | { 156 | $new = clone $this; 157 | $new->operations[] = ['!', $field, $value]; 158 | 159 | return $new; 160 | } 161 | 162 | /** 163 | * @param int|string $field 164 | */ 165 | public static function delete($field, int $value) : self 166 | { 167 | return new self(['#', $field, $value]); 168 | } 169 | 170 | /** 171 | * @param int|string $field 172 | */ 173 | public function andDelete($field, int $value) : self 174 | { 175 | $new = clone $this; 176 | $new->operations[] = ['#', $field, $value]; 177 | 178 | return $new; 179 | } 180 | 181 | /** 182 | * @param int|string $field 183 | * @param mixed $value 184 | */ 185 | public static function set($field, $value) : self 186 | { 187 | return new self(['=', $field, $value]); 188 | } 189 | 190 | /** 191 | * @param int|string $field 192 | * @param mixed $value 193 | */ 194 | public function andSet($field, $value) : self 195 | { 196 | $new = clone $this; 197 | $new->operations[] = ['=', $field, $value]; 198 | 199 | return $new; 200 | } 201 | 202 | public function toArray() : array 203 | { 204 | return $this->operations; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Schema/Space.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client\Schema; 15 | 16 | use Tarantool\Client\Exception\RequestFailed; 17 | use Tarantool\Client\Handler\Handler; 18 | use Tarantool\Client\Keys; 19 | use Tarantool\Client\Request\DeleteRequest; 20 | use Tarantool\Client\Request\InsertRequest; 21 | use Tarantool\Client\Request\ReplaceRequest; 22 | use Tarantool\Client\Request\SelectRequest; 23 | use Tarantool\Client\Request\UpdateRequest; 24 | use Tarantool\Client\Request\UpsertRequest; 25 | 26 | final class Space 27 | { 28 | public const VSPACE_ID = 281; 29 | public const VSPACE_NAME_INDEX = 2; 30 | public const VINDEX_ID = 289; 31 | public const VINDEX_NAME_INDEX = 2; 32 | 33 | private $handler; 34 | private $id; 35 | 36 | /** @var array */ 37 | private $indexes = []; 38 | 39 | public function __construct(Handler $handler, int $id) 40 | { 41 | $this->handler = $handler; 42 | $this->id = $id; 43 | } 44 | 45 | public function getId() : int 46 | { 47 | return $this->id; 48 | } 49 | 50 | public function select(Criteria $criteria) : array 51 | { 52 | $index = $criteria->getIndex(); 53 | 54 | if (\is_string($index)) { 55 | $index = $this->getIndexIdByName($index); 56 | } 57 | 58 | $request = new SelectRequest( 59 | $this->id, 60 | $index, 61 | $criteria->getKey(), 62 | $criteria->getOffset(), 63 | $criteria->getLimit(), 64 | $criteria->getIterator() 65 | ); 66 | 67 | return $this->handler->handle($request)->getBodyField(Keys::DATA); 68 | } 69 | 70 | /** 71 | * @psalm-param non-empty-array $tuple 72 | */ 73 | public function insert(array $tuple) : array 74 | { 75 | $request = new InsertRequest($this->id, $tuple); 76 | 77 | return $this->handler->handle($request)->getBodyField(Keys::DATA); 78 | } 79 | 80 | /** 81 | * @psalm-param non-empty-array $tuple 82 | */ 83 | public function replace(array $tuple) : array 84 | { 85 | $request = new ReplaceRequest($this->id, $tuple); 86 | 87 | return $this->handler->handle($request)->getBodyField(Keys::DATA); 88 | } 89 | 90 | /** 91 | * @psalm-param non-empty-array $key 92 | * @param int|string $index 93 | */ 94 | public function update(array $key, Operations $operations, $index = 0) : array 95 | { 96 | if (\is_string($index)) { 97 | $index = $this->getIndexIdByName($index); 98 | } 99 | 100 | $request = new UpdateRequest($this->id, $index, $key, $operations->toArray()); 101 | 102 | return $this->handler->handle($request)->getBodyField(Keys::DATA); 103 | } 104 | 105 | /** 106 | * @psalm-param non-empty-array $tuple 107 | */ 108 | public function upsert(array $tuple, Operations $operations) : void 109 | { 110 | $request = new UpsertRequest($this->id, $tuple, $operations->toArray()); 111 | 112 | $this->handler->handle($request); 113 | } 114 | 115 | /** 116 | * @psalm-param non-empty-array $key 117 | * @param int|string $index 118 | */ 119 | public function delete(array $key, $index = 0) : array 120 | { 121 | if (\is_string($index)) { 122 | $index = $this->getIndexIdByName($index); 123 | } 124 | 125 | $request = new DeleteRequest($this->id, $index, $key); 126 | 127 | return $this->handler->handle($request)->getBodyField(Keys::DATA); 128 | } 129 | 130 | public function flushIndexes() : void 131 | { 132 | $this->indexes = []; 133 | } 134 | 135 | private function getIndexIdByName(string $indexName) : int 136 | { 137 | if (isset($this->indexes[$indexName])) { 138 | return $this->indexes[$indexName]; 139 | } 140 | 141 | $schema = new self($this->handler, self::VINDEX_ID); 142 | $data = $schema->select(Criteria::key([$this->id, $indexName])->andIndex(self::VINDEX_NAME_INDEX)); 143 | 144 | if ($data) { 145 | return $this->indexes[$indexName] = $data[0][1]; 146 | } 147 | 148 | throw RequestFailed::unknownIndex($indexName, $this->id); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/SqlQueryResult.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client; 15 | 16 | final class SqlQueryResult implements \ArrayAccess, \Countable, \IteratorAggregate 17 | { 18 | /** @var array */ 19 | private $data; 20 | 21 | /** @var array> */ 22 | private $metadata; 23 | 24 | /** @var array */ 25 | private $keys; 26 | 27 | public function __construct(array $data, array $metadata) 28 | { 29 | $this->data = $data; 30 | $this->metadata = $metadata; 31 | $this->keys = $metadata ? \array_column($metadata, 0) : []; 32 | } 33 | 34 | public function getData() : array 35 | { 36 | return $this->data; 37 | } 38 | 39 | public function getMetadata() : array 40 | { 41 | return $this->metadata; 42 | } 43 | 44 | public function isEmpty() : bool 45 | { 46 | return !$this->data; 47 | } 48 | 49 | public function getFirst() : ?array 50 | { 51 | return $this->data ? \array_combine($this->keys, \reset($this->data)) : null; 52 | } 53 | 54 | public function getLast() : ?array 55 | { 56 | return $this->data ? \array_combine($this->keys, \end($this->data)) : null; 57 | } 58 | 59 | public function getIterator() : \Generator 60 | { 61 | foreach ($this->data as $item) { 62 | yield \array_combine($this->keys, $item); 63 | } 64 | } 65 | 66 | public function count() : int 67 | { 68 | return \count($this->data); 69 | } 70 | 71 | public function offsetExists($offset) : bool 72 | { 73 | return isset($this->data[$offset]); 74 | } 75 | 76 | public function offsetGet($offset) : array 77 | { 78 | if (!isset($this->data[$offset])) { 79 | throw new \OutOfBoundsException(\sprintf('The offset "%s" does not exist', $offset)); 80 | } 81 | 82 | return \array_combine($this->keys, $this->data[$offset]); 83 | } 84 | 85 | public function offsetSet($offset, $value) : void 86 | { 87 | throw new \BadMethodCallException(self::class.' object cannot be modified'); 88 | } 89 | 90 | public function offsetUnset($offset) : void 91 | { 92 | throw new \BadMethodCallException(self::class.' object cannot be modified'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/SqlUpdateResult.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | declare(strict_types=1); 13 | 14 | namespace Tarantool\Client; 15 | 16 | final class SqlUpdateResult implements \Countable 17 | { 18 | private $info; 19 | 20 | public function __construct(array $info) 21 | { 22 | $this->info = $info; 23 | } 24 | 25 | public function count() : int 26 | { 27 | return $this->info[Keys::SQL_INFO_ROW_COUNT]; 28 | } 29 | 30 | public function getAutoincrementIds() : array 31 | { 32 | return $this->info[Keys::SQL_INFO_AUTO_INCREMENT_IDS] ?? []; 33 | } 34 | } 35 | --------------------------------------------------------------------------------