Repository: friends-of-reactphp/mysql Branch: 0.7.x Commit: 6ceb2358c590 Files: 42 Total size: 332.4 KB Directory structure: gitextract_ldg5u_7o/ ├── .gitattributes ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── examples/ │ ├── 01-query.php │ ├── 02-query-stream.php │ ├── 11-interactive.php │ └── 12-slow-stream.php ├── phpunit.xml.dist ├── phpunit.xml.legacy ├── src/ │ ├── Commands/ │ │ ├── AbstractCommand.php │ │ ├── AuthenticateCommand.php │ │ ├── CommandInterface.php │ │ ├── PingCommand.php │ │ ├── QueryCommand.php │ │ └── QuitCommand.php │ ├── Exception.php │ ├── Io/ │ │ ├── Buffer.php │ │ ├── Connection.php │ │ ├── Constants.php │ │ ├── Executor.php │ │ ├── Factory.php │ │ ├── Parser.php │ │ ├── Query.php │ │ └── QueryStream.php │ ├── MysqlClient.php │ └── MysqlResult.php └── tests/ ├── BaseTestCase.php ├── Commands/ │ └── AuthenticateCommandTest.php ├── Io/ │ ├── BufferTest.php │ ├── ConnectionTest.php │ ├── FactoryTest.php │ ├── ParserTest.php │ ├── QueryStreamTest.php │ └── QueryTest.php ├── MysqlClientTest.php ├── NoResultQueryTest.php ├── ResultQueryTest.php └── wait-for-mysql.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ /.gitattributes export-ignore /.github/ export-ignore /.gitignore export-ignore /examples/ export-ignore /phpunit.xml.dist export-ignore /phpunit.xml.legacy export-ignore /tests/ export-ignore ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: pull_request: jobs: PHPUnit: name: PHPUnit (PHP ${{ matrix.php }} + ${{ matrix.rdbms }}) runs-on: ubuntu-24.04 strategy: matrix: rdbms: - mysql:5 php: - 8.4 - 8.3 - 8.2 - 8.1 - 8.0 - 7.4 - 7.3 - 7.2 - 7.1 - 7.0 - 5.6 - 5.5 - 5.4 include: - php: 8.4 rdbms: mysql:9 - php: 8.4 rdbms: mysql:8 - php: 8.4 rdbms: mariadb:10 steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: ${{ matrix.php < 8.0 && 'xdebug' || 'pcov' }} ini-file: development - run: composer install - run: docker run -d --name mysql --net=host -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test ${{ matrix.rdbms }} - run: bash tests/wait-for-mysql.sh - run: vendor/bin/phpunit --coverage-text ${{ matrix.php < 7.3 && '-c phpunit.xml.legacy' || '' }} PHPUnit-hhvm: name: PHPUnit (HHVM) runs-on: ubuntu-24.04 continue-on-error: true steps: - uses: actions/checkout@v4 - run: cp "$(which composer)" composer.phar && ./composer.phar self-update --2.2 # downgrade Composer for HHVM - name: Run hhvm composer.phar install uses: docker://hhvm/hhvm:3.30-lts-latest with: args: hhvm composer.phar install - run: docker run -d --name mysql --net=host -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test -e MYSQL_USER=test -e MYSQL_PASSWORD=test mysql:5 - run: bash tests/wait-for-mysql.sh - run: docker run -i --rm --workdir=/data -v "$(pwd):/data" --net=host hhvm/hhvm:3.30-lts-latest hhvm vendor/bin/phpunit ================================================ FILE: .gitignore ================================================ /composer.lock /vendor/ ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 0.6.0 (2023-11-10) * Feature: Improve Promise v3 support and use template types. (#183 and #178 by @clue) * Feature: Full PHP 8.3 compatibility. (#180 by @clue) * Feature / BC break: Update default charset encoding to `utf8mb4` for full UTF-8 support. (#165 by @clue) This feature updates the MySQL client to use `utf8mb4` as the default charset encoding for full UTF-8 support instead of the legacy `utf8mb3` charset encoding. For legacy reasons you can still change this to use a different ASCII-compatible charset encoding like this: ```php $factory->createConnection('localhost?charset=utf8mb4'); ``` * Feature: Reduce default idle time to 1ms. (#182 by @clue) The idle time defines the time the client is willing to keep the underlying connection alive before automatically closing it. The default idle time was previously 60s and can be configured for more specific requirements like this: ```php $factory->createConnection('localhost?idle=10.0'); ``` * Minor documentation improvements. (#184 by @yadaiio) * Improve test suite, update to use reactphp/async and report failed assertions. (#164 and #170 by @clue, #163 by @dinooo13 and #181 by @SimonFrings) ## 0.5.7 (2022-09-15) * Feature: Full support for PHP 8.2. (#161 by @clue) * Feature: Mark passwords and URIs as `#[\SensitiveParameter]` (PHP 8.2+). (#162 by @clue) * Feature: Forward compatibility with upcoming Promise v3. (#157 by @clue) * Feature / Fix: Improve protocol parser, emit parser errors and close invalid connections. (#158 and #159 by @clue) * Improve test suite, fix legacy HHVM build by downgrading Composer. (#160 by @clue) ## 0.5.6 (2021-12-14) * Feature: Support optional `charset` parameter for full UTF-8 support (`utf8mb4`). (#135 by @clue) ```php $db = $factory->createLazyConnection('localhost?charset=utf8mb4'); ``` * Feature: Improve error reporting, include MySQL URI and socket error codes in all connection errors. (#141 by @clue and #138 by @SimonFrings) For most common use cases this means that simply reporting the `Exception` message should give the most relevant details for any connection issues: ```php $db->query($sql)->then(function (React\MySQL\QueryResult $result) { // … }, function (Exception $e) { echo 'Error:' . $e->getMessage() . PHP_EOL; }); ``` * Feature: Full support for PHP 8.1 release. (#150 by @clue) * Feature: Provide limited support for `NO_BACKSLASH_ESCAPES` SQL mode. (#139 by @clue) * Update project dependencies, simplify socket usage, and improve documentation. (#136 and #137 by @SimonFrings) * Improve test suite and add `.gitattributes` to exclude dev files from exports. Run tests on PHPUnit 9 and PHP 8 and clean up test suite. (#142 and #143 by @SimonFrings) ## 0.5.5 (2021-07-19) * Feature: Simplify usage by supporting new default loop. (#134 by @clue) ```php // old (still supported) $factory = new React\MySQL\Factory($loop); // new (using default loop) $factory = new React\MySQL\Factory(); ``` * Improve test setup, use GitHub actions for continuous integration (CI) and fix minor typo. (#132 by @SimonFrings and #129 by @mmoreram) ## 0.5.4 (2019-05-21) * Fix: Do not start idle timer when lazy connection is already closed. (#110 by @clue) * Fix: Fix explicit `close()` on lazy connection when connection is active. (#109 by @clue) ## 0.5.3 (2019-04-03) * Fix: Ignore unsolicited server error when not executing any commands. (#102 by @clue) * Fix: Fix decoding URL-encoded special characters in credentials from database connection URI. (#98 and #101 by @clue) ## 0.5.2 (2019-02-05) * Fix: Fix `ConnectionInterface` return type hint in `Factory`. (#93 by @clue) * Minor documentation typo fix and improve test suite to test against PHP 7.3, add forward compatibility with PHPUnit 7 and use legacy PHPUnit 5 on HHVM. (#92 and #94 by @clue) ## 0.5.1 (2019-01-12) * Fix: Fix "bad handshake" error when connecting without database name. (#91 by @clue) ## 0.5.0 (2018-11-28) A major feature release with a significant API improvement! This update does not involve any BC breaks, but we figured the new API provides significant features that warrant a major version bump. Existing code will continue to work without changes, but you're highly recommended to consider using the new lazy connections as detailed below. * Feature: Add new `createLazyConnection()` method to only connect on demand and implement "idle" timeout to close underlying connection when unused. (#87 and #88 by @clue) ```php // new $connection = $factory->createLazyConnection($url); $connection->query(…); ``` This method immediately returns a "virtual" connection implementing the [`ConnectionInterface`](README.md#connectioninterface) that can be used to interface with your MySQL database. Internally, it lazily creates the underlying database connection only on demand once the first request is invoked on this instance and will queue all outstanding requests until the underlying connection is ready. Additionally, it will only keep this underlying connection in an "idle" state for 60s by default and will automatically end the underlying connection when it is no longer needed. From a consumer side this means that you can start sending queries to the database right away while the underlying connection may still be outstanding. Because creating this underlying connection may take some time, it will enqueue all outstanding commands and will ensure that all commands will be executed in correct order once the connection is ready. In other words, this "virtual" connection behaves just like a "real" connection as described in the `ConnectionInterface` and frees you from having to deal with its async resolution. * Feature: Support connection timeouts. (#86 by @clue) ## 0.4.1 (2018-10-18) * Feature: Support cancellation of pending connection attempts. (#84 by @clue) * Feature: Add `warningCount` to `QueryResult`. (#82 by @legionth) * Feature: Add exception message for invalid MySQL URI. (#80 by @CharlotteDunois) * Fix: Fix parsing error message during handshake (Too many connections). (#83 by @clue) ## 0.4.0 (2018-09-21) A major feature release with a significant documentation overhaul and long overdue API cleanup! This update involves a number of BC breaks due to various changes to make the API more consistent with the ReactPHP ecosystem. In particular, this now uses promises consistently as return values instead of accepting callback functions and this now offers an additional streaming API for processing very large result sets efficiently. We realize that the changes listed below may seem a bit overwhelming, but we've tried to be very clear about any possible BC breaks. See below for changes you have to take care of when updating from an older version. * Feature / BC break: Add Factory to simplify connecting and keeping connection state, mark `Connection` class as internal and remove `connect()` method. (#64 by @clue) ```php // old $connection = new Connection($loop, $options); $connection->connect(function (?Exception $error, $connection) { if ($error) { // an error occurred while trying to connect or authorize client } else { // client connection established (and authenticated) } }); // new $factory = new Factory($loop); $factory->createConnection($url)->then( function (ConnectionInterface $connection) { // client connection established (and authenticated) }, function (Exception $e) { // an error occurred while trying to connect or authorize client } ); ``` * Feature / BC break: Use promises for `query()` method and resolve with `QueryResult` on success and and mark all commands as internal and move its base to Commands namespace. (#61 and #62 by @clue) ```php // old $connection->query('CREATE TABLE test'); $connection->query('DELETE FROM user WHERE id < ?', $id); $connection->query('SELECT * FROM user', function (QueryCommand $command) { if ($command->hasError()) { echo 'Error: ' . $command->getError()->getMessage() . PHP_EOL; } elseif (isset($command->resultRows)) { var_dump($command->resultRows); } }); // new $connection->query('CREATE TABLE test'); $connection->query('DELETE FROM user WHERE id < ?', [$id]); $connection->query('SELECT * FROM user')->then(function (QueryResult $result) { var_dump($result->resultRows); }, function (Exception $error) { echo 'Error: ' . $error->getMessage() . PHP_EOL; }); ``` * Feature / BC break: Add new `queryStream()` method to stream result set rows and remove undocumented "results" event. (#57 and #77 by @clue) ```php $stream = $connection->queryStream('SELECT * FROM users'); $stream->on('data', function ($row) { var_dump($row); }); $stream->on('end', function () { echo 'DONE' . PHP_EOL; }); ``` * Feature / BC break: Rename `close()` to `quit()`, use promises for `quit()` method and add new `close()` method to force-close the connection. (#65 and #76 by @clue) ```php // old: soft-close/quit $connection->close(function () { echo 'closed'; }); // new: soft-close/quit $connection->quit()->then(function () { echo 'closed'; }); // new: force-close $connection->close(); ``` * Feature / BC break: Use promises for `ping()` method and resolve with void value on success. (#63 and #66 by @clue) ```php // old $connection->ping(function ($error, $connection) { if ($error) { echo 'Error: ' . $error->getMessage() . PHP_EOL; } else { echo 'OK' . PHP_EOL; } }); // new $connection->ping(function () { echo 'OK' . PHP_EOL; }, function (Exception $error) { echo 'Error: ' . $error->getMessage() . PHP_EOL; }); ``` * Feature / BC break: Define events on ConnectionInterface (#78 by @clue) * BC break: Remove unneeded `ConnectionInterface` methods `getState()`, `getOptions()`, `setOptions()` and `getServerOptions()`, `selectDb()` and `listFields()` dummy. (#60 and #68 by @clue) * BC break: Mark all protocol logic classes as internal and move to new Io namespace. (#53 and #62 by @clue) * Fix: Fix executing queued commands in the order they are enqueued (#75 by @clue) * Fix: Fix reading all incoming response packets until end (#59 by @clue) * [maintenance] Internal refactoring to simplify connection and authentication logic (#69 by @clue) * [maintenance] Internal refactoring to remove unneeded references from Commands (#67 by @clue) * [maintenance] Internal refactoring to remove unneeded EventEmitter implementation and circular references (#56 by @clue) * [maintenance] Refactor internal parsing logic to separate Buffer class, remove dead code and improve performance (#54 by @clue) ## 0.3.3 (2018-06-18) * Fix: Reject pending commands if connection is closed (#52 by @clue) * Fix: Do not support multiple statements for security and API reasons (#51 by @clue) * Fix: Fix reading empty rows containing only empty string columns (#46 by @clue) * Fix: Report correct field length for fields longer than 16k chars (#42 by @clue) * Add quickstart example and interactive CLI example (#45 by @clue) ## 0.3.2 (2018-04-04) * Fix: Fix parameter binding if query contains question marks (#40 by @clue) * Improve test suite by simplifying test structure, improve test isolation and remove dbunit (#39 by @clue) ## 0.3.1 (2018-03-26) * Feature: Forward compatibility with upcoming ReactPHP components (#37 by @clue) * Fix: Consistent `connect()` behavior for all connection states (#36 by @clue) * Fix: Report connection error to `connect()` callback (#35 by @clue) ## 0.3.0 (2018-03-13) * This is now a community project managed by @friends-of-reactphp. Thanks to @bixuehujin for releasing this project under MIT license and handing over! (#12 and #33 by @bixuehujin and @clue) * Feature / BC break: Update react/socket to v0.8.0 (#21 by @Fneufneu) * Feature: Support passing custom connector and load system default DNS config by default (#24 by @flow-control and #30 by @clue) * Feature: Add `ConnectionInterface` with documentation (#26 by @freedemster) * Fix: Last query param is lost if no callback is given (#22 by @Fneufneu) * Fix: Fix memory increase (memory leak due to keeping incoming receive buffer) (#17 by @sukui) * Improve test suite by adding test instructions and adding Travis CI (#34 by @clue and #25 by @freedemster) * Improve documentation (#8 by @ovr and #10 by @RafaelKa) ## 0.2.0 (2014-10-15) * Now compatible with ReactPHP v0.4 ## 0.1.0 (2014-02-18) * First tagged release (ReactPHP v0.3) ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Jin Hu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # MySQL [![CI status](https://github.com/friends-of-reactphp/mysql/actions/workflows/ci.yml/badge.svg)](https://github.com/friends-of-reactphp/mysql/actions) Async MySQL database client for [ReactPHP](https://reactphp.org/). > **Development version:** This branch contains the code for the upcoming > version 0.7 release. For the code of the current stable version 0.6 release, check > out the [`0.6.x` branch](https://github.com/friends-of-reactphp/mysql/tree/0.6.x). > > The upcoming version 0.7 release will be the way forward for this package. > However, we will still actively support version 0.6 for those not yet on the > latest version. > See also [installation instructions](#install) for more details. This is a MySQL database driver for [ReactPHP](https://reactphp.org/). It implements the MySQL protocol and allows you to access your existing MySQL database. It is written in pure PHP and does not require any extensions. **Table of contents** * [Quickstart example](#quickstart-example) * [Usage](#usage) * [MysqlClient](#mysqlclient) * [__construct()](#__construct) * [query()](#query) * [queryStream()](#querystream) * [ping()](#ping) * [quit()](#quit) * [close()](#close) * [error event](#error-event) * [close event](#close-event) * [Install](#install) * [Tests](#tests) * [License](#license) ## Quickstart example This example runs a simple `SELECT` query and dumps all the records from a `book` table: ```php query('SELECT * FROM book')->then( function (React\Mysql\MysqlResult $command) { print_r($command->resultFields); print_r($command->resultRows); echo count($command->resultRows) . ' row(s) in set' . PHP_EOL; }, function (Exception $error) { echo 'Error: ' . $error->getMessage() . PHP_EOL; } ); ``` See also the [examples](examples). ## Usage ### MysqlClient The `MysqlClient` is responsible for exchanging messages with your MySQL server and keeps track of pending queries. ```php $mysql = new React\Mysql\MysqlClient($uri); $mysql->query(…); ``` This class represents a connection that is responsible for communicating with your MySQL server instance, managing the connection state and sending your database queries. Internally, it creates the underlying database connection only on demand once the first request is invoked on this instance and will queue all outstanding requests until the underlying connection is ready. This underlying connection will be reused for all requests until it is closed. By default, idle connections will be held open for 1ms (0.001s) when not used. The next request will either reuse the existing connection or will automatically create a new underlying connection if this idle time is expired. From a consumer side this means that you can start sending queries to the database right away while the underlying connection may still be outstanding. Because creating this underlying connection may take some time, it will enqueue all outstanding commands and will ensure that all commands will be executed in correct order once the connection is ready. If the underlying database connection fails, it will reject all outstanding commands and will return to the initial "idle" state. This means that you can keep sending additional commands at a later time which will again try to open a new underlying connection. Note that this may require special care if you're using transactions that are kept open for longer than the idle period. Note that creating the underlying connection will be deferred until the first request is invoked. Accordingly, any eventual connection issues will be detected once this instance is first used. You can use the `quit()` method to ensure that the connection will be soft-closed and no further commands can be enqueued. Similarly, calling `quit()` on this instance when not currently connected will succeed immediately and will not have to wait for an actual underlying connection. #### __construct() The `new MysqlClient(string $uri, ?ConnectorInterface $connector = null, ?LoopInterface $loop = null)` constructor can be used to create a new `MysqlClient` instance. The `$uri` parameter must contain the database host, optional authentication, port and database to connect to: ```php $mysql = new React\Mysql\MysqlClient('user:secret@localhost:3306/database'); ``` Note that both the username and password must be URL-encoded (percent-encoded) if they contain special characters: ```php $user = 'he:llo'; $pass = 'p@ss'; $mysql = new React\Mysql\MysqlClient( rawurlencode($user) . ':' . rawurlencode($pass) . '@localhost:3306/db' ); ``` You can omit the port if you're connecting to default port `3306`: ```php $mysql = new React\Mysql\MysqlClient('user:secret@localhost/database'); ``` If you do not include authentication and/or database, then this method will default to trying to connect as user `root` with an empty password and no database selected. This may be useful when initially setting up a database, but likely to yield an authentication error in a production system: ```php $mysql = new React\Mysql\MysqlClient('localhost'); ``` This method respects PHP's `default_socket_timeout` setting (default 60s) as a timeout for establishing the underlying connection and waiting for successful authentication. You can explicitly pass a custom timeout value in seconds (or use a negative number to not apply a timeout) like this: ```php $mysql = new React\Mysql\MysqlClient('localhost?timeout=0.5'); ``` By default, idle connections will be held open for 1ms (0.001s) when not used. The next request will either reuse the existing connection or will automatically create a new underlying connection if this idle time is expired. This ensures you always get a "fresh" connection and as such should not be confused with a "keepalive" or "heartbeat" mechanism, as this will not actively try to probe the connection. You can explicitly pass a custom idle timeout value in seconds (or use a negative number to not apply a timeout) like this: ```php $mysql = new React\Mysql\MysqlClient('localhost?idle=10.0'); ``` By default, the connection provides full UTF-8 support (using the `utf8mb4` charset encoding). This should usually not be changed for most applications nowadays, but for legacy reasons you can change this to use a different ASCII-compatible charset encoding like this: ```php $mysql = new React\Mysql\MysqlClient('localhost?charset=utf8mb4'); ``` If you need custom connector settings (DNS resolution, TLS parameters, timeouts, proxy servers etc.), you can explicitly pass a custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): ```php $connector = new React\Socket\Connector([ 'dns' => '127.0.0.1', 'tcp' => [ 'bindto' => '192.168.10.1:0' ], 'tls' => [ 'verify_peer' => false, 'verify_peer_name' => false ) ]); $mysql = new React\Mysql\MysqlClient('user:secret@localhost:3306/database', $connector); ``` This class takes an optional `LoopInterface|null $loop` parameter that can be used to pass the event loop instance to use for this object. You can use a `null` value here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). This value SHOULD NOT be given unless you're sure you want to explicitly use a given event loop instance. #### query() The `query(string $query, list $params = []): PromiseInterface` method can be used to perform an async query. This method returns a promise that will resolve with a `MysqlResult` on success or will reject with an `Exception` on error. The MySQL protocol is inherently sequential, so that all queries will be performed in order and outstanding queries will be put into a queue to be executed once the previous queries are completed. ```php $mysql->query('CREATE TABLE test ...'); $mysql->query('INSERT INTO test (id) VALUES (1)'); ``` If this SQL statement returns a result set (such as from a `SELECT` statement), this method will buffer everything in memory until the result set is completed and will then resolve the resulting promise. This is the preferred method if you know your result set to not exceed a few dozens or hundreds of rows. If the size of your result set is either unknown or known to be too large to fit into memory, you should use the [`queryStream()`](#querystream) method instead. ```php $mysql->query($query)->then(function (React\Mysql\MysqlResult $command) { if (isset($command->resultRows)) { // this is a response to a SELECT etc. with some rows (0+) print_r($command->resultFields); print_r($command->resultRows); echo count($command->resultRows) . ' row(s) in set' . PHP_EOL; } else { // this is an OK message in response to an UPDATE etc. if ($command->insertId !== 0) { var_dump('last insert ID', $command->insertId); } echo 'Query OK, ' . $command->affectedRows . ' row(s) affected' . PHP_EOL; } }, function (Exception $error) { // the query was not executed successfully echo 'Error: ' . $error->getMessage() . PHP_EOL; }); ``` You can optionally pass an array of `$params` that will be bound to the query like this: ```php $mysql->query('SELECT * FROM user WHERE id > ?', [$id]); ``` The given `$sql` parameter MUST contain a single statement. Support for multiple statements is disabled for security reasons because it could allow for possible SQL injection attacks and this API is not suited for exposing multiple possible results. #### queryStream() The `queryStream(string $sql, list $params = []): ReadableStreamInterface` method can be used to perform an async query and stream the rows of the result set. This method returns a readable stream that will emit each row of the result set as a `data` event. It will only buffer data to complete a single row in memory and will not store the whole result set. This allows you to process result sets of unlimited size that would not otherwise fit into memory. If you know your result set to not exceed a few dozens or hundreds of rows, you may want to use the [`query()`](#query) method instead. ```php $stream = $mysql->queryStream('SELECT * FROM user'); $stream->on('data', function ($row) { echo $row['name'] . PHP_EOL; }); $stream->on('end', function () { echo 'Completed.'; }); ``` You can optionally pass an array of `$params` that will be bound to the query like this: ```php $stream = $mysql->queryStream('SELECT * FROM user WHERE id > ?', [$id]); ``` This method is specifically designed for queries that return a result set (such as from a `SELECT` or `EXPLAIN` statement). Queries that do not return a result set (such as a `UPDATE` or `INSERT` statement) will not emit any `data` events. See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) for more details about how readable streams can be used in ReactPHP. For example, you can also use its `pipe()` method to forward the result set rows to a [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface) like this: ```php $mysql->queryStream('SELECT * FROM user')->pipe($formatter)->pipe($logger); ``` Note that as per the underlying stream definition, calling `pause()` and `resume()` on this stream is advisory-only, i.e. the stream MAY continue emitting some data until the underlying network buffer is drained. Also notice that the server side limits how long a connection is allowed to be in a state that has outgoing data. Special care should be taken to ensure the stream is resumed in time. This implies that using `pipe()` with a slow destination stream may cause the connection to abort after a while. The given `$sql` parameter MUST contain a single statement. Support for multiple statements is disabled for security reasons because it could allow for possible SQL injection attacks and this API is not suited for exposing multiple possible results. #### ping() The `ping(): PromiseInterface` method can be used to check that the connection is alive. This method returns a promise that will resolve (with a void value) on success or will reject with an `Exception` on error. The MySQL protocol is inherently sequential, so that all commands will be performed in order and outstanding command will be put into a queue to be executed once the previous queries are completed. ```php $mysql->ping()->then(function () { echo 'OK' . PHP_EOL; }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` #### quit() The `quit(): PromiseInterface` method can be used to quit (soft-close) the connection. This method returns a promise that will resolve (with a void value) on success or will reject with an `Exception` on error. The MySQL protocol is inherently sequential, so that all commands will be performed in order and outstanding commands will be put into a queue to be executed once the previous commands are completed. ```php $mysql->query('CREATE TABLE test ...'); $mysql->quit(); ``` This method will gracefully close the connection to the MySQL database server once all outstanding commands are completed. See also [`close()`](#close) if you want to force-close the connection without waiting for any commands to complete instead. #### close() The `close(): void` method can be used to force-close the connection. Unlike the `quit()` method, this method will immediately force-close the connection and reject all outstanding commands. ```php $mysql->close(); ``` Forcefully closing the connection will yield a warning in the server logs and should generally only be used as a last resort. See also [`quit()`](#quit) as a safe alternative. #### error event The `error` event will be emitted once a fatal error occurs, such as when the connection is lost or is invalid. The event receives a single `Exception` argument for the error instance. ```php $mysql->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` This event will only be triggered for fatal errors and will be followed by closing the connection. It is not to be confused with "soft" errors caused by invalid SQL queries. #### close event The `close` event will be emitted once the connection closes (terminates). ```php $mysql->on('close', function () { echo 'Connection closed' . PHP_EOL; }); ``` See also the [`close()`](#close) method. ## Install The recommended way to install this library is [through Composer](https://getcomposer.org/). [New to Composer?](https://getcomposer.org/doc/00-intro.md) Once released, this project will follow [SemVer](https://semver.org/). At the moment, this will install the latest development version: ```bash composer require react/mysql:^0.7@dev ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP extensions and supports running on legacy PHP 5.4 through current PHP 8+ and HHVM. It's *highly recommended to use the latest supported PHP version* for this project. This project supports connecting to a variety of MySQL database versions and compatible projects using the MySQL protocol. The `caching_sha2_password` authentication plugin (default in MySQL 8+) requires PHP 7.1+ and `ext-openssl` to be installed, while the older `mysql_native_password` authentication plugin (default in MySQL 5.7) is supported across all supported PHP versions. ## Tests To run the test suite, you first need to clone this repo and then install all dependencies [through Composer](https://getcomposer.org/): ```bash composer install ``` The test suite contains a number of functional integration tests that send actual test SQL queries against your local database and thus rely on a local MySQL test database with appropriate write access. The test suite creates and modifies a test table in this database, so make sure to not use a production database! You can change your test database credentials by passing these ENV variables: ```bash export DB_HOST=localhost export DB_PORT=3306 export DB_USER=test export DB_PASSWD=test export DB_DBNAME=test ``` For example, to create an empty test database, you can also use a temporary [`mysql` Docker image](https://hub.docker.com/_/mysql/) like this: ```bash docker run -it --rm --net=host \ -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_DATABASE=test \ -e MYSQL_USER=test -e MYSQL_PASSWORD=test mysql:5 ``` To run the test suite, go to the project root and run: ```bash vendor/bin/phpunit ``` ## License MIT, see [LICENSE file](LICENSE). This is a community project now managed by [@friends-of-reactphp](https://github.com/friends-of-reactphp). The original implementation was created by [@bixuehujin](https://github.com/bixuehujin) starting in 2013 and has been migrated to [@friends-of-reactphp](https://github.com/friends-of-reactphp) in 2018 to help with maintenance and upcoming feature development. The original implementation was made possible thanks to the following projects: * [phpdaemon](https://github.com/kakserpom/phpdaemon): the MySQL protocol implementation is based on code of this project (with permission). * [node-mysql](https://github.com/felixge/node-mysql): the API design is inspired by this project. ================================================ FILE: composer.json ================================================ { "name": "react/mysql", "description": "Async MySQL database client for ReactPHP.", "keywords": ["mysql", "database", "async", "reactphp"], "license": "MIT", "require": { "php": ">=5.4.0", "evenement/evenement": "^3.0 || ^2.1 || ^1.1", "react/event-loop": "^1.2", "react/promise": "^3.2 || ^2.7", "react/promise-stream": "^1.6", "react/promise-timer": "^1.11", "react/socket": "^1.16" }, "require-dev": { "phpunit/phpunit": "^9.6 || ^8.5 || ^5.7 || ^4.8.36", "react/async": "^4.3 || ^3 || ^2" }, "autoload": { "psr-4": { "React\\Mysql\\": "src/" } }, "autoload-dev": { "psr-4": { "React\\Tests\\Mysql\\": "tests/" } } } ================================================ FILE: examples/01-query.php ================================================ query($query)->then(function (React\Mysql\MysqlResult $command) { if (isset($command->resultRows)) { // this is a response to a SELECT etc. with some rows (0+) print_r($command->resultFields); print_r($command->resultRows); echo count($command->resultRows) . ' row(s) in set' . PHP_EOL; } else { // this is an OK message in response to an UPDATE etc. if ($command->insertId !== 0) { var_dump('last insert ID', $command->insertId); } echo 'Query OK, ' . $command->affectedRows . ' row(s) affected' . PHP_EOL; } }, function (Exception $error) { // the query was not executed successfully echo 'Error: ' . $error->getMessage() . PHP_EOL; }); ================================================ FILE: examples/02-query-stream.php ================================================ queryStream($query); $stream->on('data', function ($row) { echo json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; }); $stream->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $stream->on('close', function () { echo 'CLOSED' . PHP_EOL; }); ================================================ FILE: examples/11-interactive.php ================================================ on('data', function ($line) use ($mysql) { $query = trim($line); if ($query === '') { // skip empty commands return; } if ($query === 'exit') { // exit command should close the connection echo 'bye.' . PHP_EOL; $mysql->quit(); return; } $time = microtime(true); $mysql->query($query)->then(function (React\Mysql\MysqlResult $command) use ($time) { if (isset($command->resultRows)) { // this is a response to a SELECT etc. with some rows (0+) echo implode("\t", array_column($command->resultFields, 'name')) . PHP_EOL; foreach ($command->resultRows as $row) { echo implode("\t", $row) . PHP_EOL; } printf( '%d row%s in set (%.03f sec)%s', count($command->resultRows), count($command->resultRows) === 1 ? '' : 's', microtime(true) - $time, PHP_EOL ); } else { // this is an OK message in response to an UPDATE etc. // the insertId will only be set if this is if ($command->insertId !== 0) { var_dump('last insert ID', $command->insertId); } printf( 'Query OK, %d row%s affected (%.03f sec)%s', $command->affectedRows, $command->affectedRows === 1 ? '' : 's', microtime(true) - $time, PHP_EOL ); } }, function (Exception $error) { // the query was not executed successfully echo 'Error: ' . $error->getMessage() . PHP_EOL; }); }); // close connection when STDIN closes (EOF or CTRL+D) $stdin->on('close', function () use ($mysql) { $mysql->quit(); }); // close STDIN (stop reading) when connection closes $mysql->on('close', function () use ($stdin) { $stdin->close(); echo 'Disconnected.' . PHP_EOL; }); echo '# Entering interactive mode ready, hit CTRL-D to quit' . PHP_EOL; ================================================ FILE: examples/12-slow-stream.php ================================================ queryStream($query); $ref = new ReflectionProperty($mysql, 'connecting'); $ref->setAccessible(true); $promise = $ref->getValue($mysql); assert($promise instanceof React\Promise\PromiseInterface); $promise->then(function (React\Mysql\Io\Connection $connection) { // The protocol parser reads rather large chunks from the underlying connection // and as such can yield multiple (dozens to hundreds) rows from a single data // chunk. We try to artificially limit the stream chunk size here to try to // only ever read a single row so we can demonstrate throttling this stream. // It goes without saying this is only a hack! Real world applications rarely // have the need to limit the chunk size. As an alternative, consider using // a stream decorator that rate-limits and buffers the resulting flow. try { // accept private "stream" (instanceof React\Socket\ConnectionInterface) $ref = new ReflectionProperty($connection, 'stream'); $ref->setAccessible(true); $conn = $ref->getValue($connection); assert($conn instanceof React\Socket\ConnectionInterface); // access private "input" (instanceof React\Stream\DuplexStreamInterface) $ref = new ReflectionProperty($conn, 'input'); $ref->setAccessible(true); $stream = $ref->getValue($conn); assert($stream instanceof React\Stream\DuplexStreamInterface); // reduce private bufferSize to just a few bytes to slow things down $ref = new ReflectionProperty($stream, 'bufferSize'); $ref->setAccessible(true); $ref->setValue($stream, 8); } catch (Exception $e) { echo 'Warning: Unable to reduce buffer size: ' . $e->getMessage() . PHP_EOL; } }); $throttle = null; $stream->on('data', function ($row) use (&$throttle, $stream) { echo json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; // simple throttle mechanism: explicitly pause the result stream and // resume it again after some time. if ($throttle === null) { $throttle = Loop::addTimer(1.0, function () use ($stream, &$throttle) { $throttle = null; $stream->resume(); }); $stream->pause(); } }); $stream->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); $stream->on('close', function () use (&$throttle) { echo 'CLOSED' . PHP_EOL; if ($throttle) { Loop::cancelTimer($throttle); $throttle = null; } }); $mysql->quit(); ================================================ FILE: phpunit.xml.dist ================================================ ./tests/ ./src/ ================================================ FILE: phpunit.xml.legacy ================================================ ./tests/ ./src/ ================================================ FILE: src/Commands/AbstractCommand.php ================================================ * @see self::$charsetNumber * @see \React\Mysql\Io\Query::$escapeChars */ private static $charsetMap = [ 'latin1' => 8, 'latin2' => 9, 'ascii' => 11, 'latin5' => 30, 'utf8' => 33, 'latin7' => 41, 'utf8mb4' => 45, 'binary' => 63 ]; /** * @param string $user * @param string $passwd * @param string $dbname * @param string $charset * @throws \InvalidArgumentException for invalid/unknown charset name */ public function __construct( $user, #[\SensitiveParameter] $passwd, $dbname, $charset ) { if (!isset(self::$charsetMap[$charset])) { throw new \InvalidArgumentException('Unsupported charset selected'); } $this->user = $user; $this->passwd = $passwd; $this->dbname = $dbname; $this->charsetNumber = self::$charsetMap[$charset]; } public function getId() { return 0; } /** * @param string $scramble * @param ?string $authPlugin * @param Buffer $buffer * @return string * @throws \UnexpectedValueException for unsupported authentication plugin */ public function authenticatePacket($scramble, $authPlugin, Buffer $buffer) { $clientFlags = Constants::CLIENT_LONG_PASSWORD | Constants::CLIENT_LONG_FLAG | Constants::CLIENT_LOCAL_FILES | Constants::CLIENT_PROTOCOL_41 | Constants::CLIENT_INTERACTIVE | Constants::CLIENT_TRANSACTIONS | Constants::CLIENT_SECURE_CONNECTION | Constants::CLIENT_CONNECT_WITH_DB; if ($authPlugin !== null) { $clientFlags |= Constants::CLIENT_PLUGIN_AUTH; } return pack('VVc', $clientFlags, $this->maxPacketSize, $this->charsetNumber) . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" . $this->user . "\x00" . $buffer->buildStringLen($this->authResponse($scramble, $authPlugin)) . $this->dbname . "\x00" . ($authPlugin !== null ? $authPlugin . "\0" : ''); } /** * @param string $scramble * @param ?string $authPlugin * @return string * @throws \UnexpectedValueException for unsupported authentication plugin */ public function authResponse($scramble, $authPlugin) { if ($authPlugin === null || $authPlugin === 'mysql_native_password') { return $this->authMysqlNativePassword($scramble); } elseif ($authPlugin === 'caching_sha2_password') { return $this->authCachingSha2Password($scramble); } else { throw new \UnexpectedValueException('Unknown authentication plugin "' . addslashes($authPlugin) . '" requested by server'); } } /** * @param string $scramble * @return string */ private function authMysqlNativePassword($scramble) { if ($this->passwd === '') { return ''; } return \sha1($scramble . \sha1($hash1 = \sha1($this->passwd, true), true), true) ^ $hash1; } /** * @param string $scramble * @return string * @throws \BadFunctionCallException if SHA256 hash algorithm is not available if ext-hash is missing, only possible in PHP < 7.4 */ private function authCachingSha2Password($scramble) { if ($this->passwd === '') { return ''; } if (\PHP_VERSION_ID < 70100 || !\function_exists('hash')) { throw new \UnexpectedValueException('Requires PHP 7.1+ with ext-hash for authentication plugin "caching_sha2_password" requested by server'); } \assert(\in_array('sha256', \hash_algos(), true)); return ($hash1 = \hash('sha256', $this->passwd, true)) ^ \hash('sha256', \hash('sha256', $hash1, true) . $scramble, true); } /** * @param string $scramble * @param string $pubkey * @return string * @throws \UnexpectedValueException if encryption fails (e.g. missing ext-openssl or invalid public key) */ public function authSha256($scramble, $pubkey) { if (!\function_exists('openssl_public_encrypt')) { throw new \UnexpectedValueException('Requires ext-openssl for authentication plugin "caching_sha2_password" requested by server'); } $ret = @\openssl_public_encrypt( $this->passwd . "\x00" ^ \str_pad($scramble, \strlen($this->passwd) + 1, $scramble), $auth, $pubkey, \OPENSSL_PKCS1_OAEP_PADDING ); // unlikely: openssl_public_encrypt() may return false if the public key sent by the server is invalid if ($ret === false) { throw new \UnexpectedValueException('Failed to encrypt password with public key'); } return $auth; } } ================================================ FILE: src/Commands/CommandInterface.php ================================================ query; } public function setQuery($query) { if ($query instanceof Query) { $this->query = $query; } elseif (is_string($query)) { $this->query = new Query($query); } else { throw new \InvalidArgumentException('Invalid argument type of query specified.'); } } public function getSql() { $query = $this->query; if ($query instanceof Query) { return $query->getSql(); } return $query; } } ================================================ FILE: src/Commands/QuitCommand.php ================================================ buffer .= $str; } /** * prepends some data to start of buffer and resets buffer position to start * * @param string $str * @return void */ public function prepend($str) { $this->buffer = $str . \substr($this->buffer, $this->bufferPos); $this->bufferPos = 0; } /** * Reads binary string data with given byte length from buffer * * @param int $len length in bytes, must be positive or zero * @return string * @throws \UnderflowException */ public function read($len) { // happy path to return empty string for zero length string if ($len === 0) { return ''; } // happy path for single byte strings without using substrings if ($len === 1 && isset($this->buffer[$this->bufferPos])) { return $this->buffer[$this->bufferPos++]; } // ensure buffer size contains $len bytes by checking target buffer position if ($len < 0 || !isset($this->buffer[$this->bufferPos + $len - 1])) { throw new \UnderflowException('Not enough data in buffer to read ' . $len . ' bytes'); } $buffer = \substr($this->buffer, $this->bufferPos, $len); $this->bufferPos += $len; return $buffer; } /** * Reads data with given byte length from buffer into a new buffer * * This class keeps consumed data in memory for performance reasons and only * advances the internal buffer position by default. Reading data into a new * buffer will clear the data from the original buffer to free memory. * * @param int $len length in bytes, must be positive or zero * @return self * @throws \UnderflowException */ public function readBuffer($len) { // happy path to return empty buffer without any memory access for zero length string if ($len === 0) { return new self(); } // ensure buffer size contains $len bytes by checking target buffer position if ($len < 0 || !isset($this->buffer[$this->bufferPos + $len - 1])) { throw new \UnderflowException('Not enough data in buffer to read ' . $len . ' bytes'); } $buffer = new self(); $buffer->buffer = $this->read($len); if (!isset($this->buffer[$this->bufferPos])) { $this->buffer = ''; } else { $this->buffer = \substr($this->buffer, $this->bufferPos); } $this->bufferPos = 0; return $buffer; } /** * Skips binary string data with given byte length from buffer * * This method can be used instead of `read()` if you do not care about the * bytes that will be skipped. * * @param int $len length in bytes, must be positive and non-zero * @return void * @throws \UnderflowException */ public function skip($len) { if ($len < 1 || !isset($this->buffer[$this->bufferPos + $len - 1])) { throw new \UnderflowException('Not enough data in buffer'); } $this->bufferPos += $len; } /** * returns the buffer length measures in number of bytes * * @return int */ public function length() { return \strlen($this->buffer) - $this->bufferPos; } /** * @return int 1 byte / 8 bit integer (0 to 255) */ public function readInt1() { return \ord($this->read(1)); } /** * @return int 2 byte / 16 bit integer (0 to 64 K / 0xFFFF) */ public function readInt2() { $v = \unpack('v', $this->read(2)); return $v[1]; } /** * @return int 3 byte / 24 bit integer (0 to 16 M / 0xFFFFFF) */ public function readInt3() { $v = \unpack('V', $this->read(3) . "\0"); return $v[1]; } /** * @return int 4 byte / 32 bit integer (0 to 4 G / 0xFFFFFFFF) */ public function readInt4() { $v = \unpack('V', $this->read(4)); return $v[1]; } /** * @return int 8 byte / 64 bit integer (0 to 2^64-1) * @codeCoverageIgnore */ public function readInt8() { // PHP < 5.6.3 does not support packing 64 bit ints, so use manual bit shifting if (\PHP_VERSION_ID < 50603) { $v = \unpack('V*', $this->read(8)); return $v[1] + ($v[2] << 32); } $v = \unpack('P', $this->read(8)); return $v[1]; } /** * Parses length-encoded binary integer * * @return int|null decoded integer 0 to 2^64 or null for special null int */ public function readIntLen() { $f = $this->readInt1(); if ($f <= 250) { return $f; } if ($f === 251) { return null; } if ($f === 252) { return $this->readInt2(); } if ($f === 253) { return $this->readInt3(); } return $this->readInt8(); } /** * Parses length-encoded binary string * * @return string|null decoded string or null if length indicates null */ public function readStringLen() { $l = $this->readIntLen(); if ($l === null) { return $l; } return $this->read($l); } /** * Reads string until NULL character * * @return string * @throws \UnderflowException */ public function readStringNull() { $pos = \strpos($this->buffer, "\0", $this->bufferPos); if ($pos === false) { throw new \UnderflowException('Missing NULL character'); } $ret = $this->read($pos - $this->bufferPos); ++$this->bufferPos; return $ret; } /** * @param int $int * @return string */ public function buildInt1($int) { return \chr($int); } /** * @param int $int * @return string */ public function buildInt2($int) { return \pack('v', $int); } /** * @param int $int * @return string */ public function buildInt3($int) { return \substr(\pack('V', $int), 0, 3); } /** * @param int $int * @return string * @codeCoverageIgnore */ public function buildInt8($int) { // PHP < 5.6.3 does not support packing 64 bit ints, so use manual bit shifting if (\PHP_VERSION_ID < 50603) { return \pack('VV', $int, $int >> 32); } return \pack('P', $int); } /** * Builds length-encoded binary string * * @param string|null $s * @return string Resulting binary string */ public function buildStringLen($s) { if ($s === NULL) { // \xFB (251) return "\xFB"; } $l = \strlen($s); if ($l <= 250) { // this is the only path that is currently used in fact. return $this->buildInt1($l) . $s; } if ($l <= 0xFFFF) { // max 2^16: \xFC (252) return "\xFC" . $this->buildInt2($l) . $s; } if ($l <= 0xFFFFFF) { // max 2^24: \xFD (253) return "\xFD" . $this->buildInt3($l) . $s; } // max 2^64: \xFE (254) return "\xFE" . $this->buildInt8($l) . $s; } } ================================================ FILE: src/Io/Connection.php ================================================ stream = $stream; $this->executor = $executor; $this->parser = $parser; $this->loop = $loop; if ($idlePeriod !== null) { $this->idlePeriod = $idlePeriod; } $stream->on('error', [$this, 'handleConnectionError']); $stream->on('close', [$this, 'handleConnectionClosed']); } /** * busy executing some command such as query or ping * * @return bool * @throws void */ public function isBusy() { return $this->parser->isBusy() || !$this->executor->isIdle(); } public function query(Query $query) { $command = new QueryCommand(); $command->setQuery($query); try { $this->_doCommand($command); } catch (\Exception $e) { return \React\Promise\reject($e); } $this->awake(); $deferred = new Deferred(); // store all result set rows until result set end $rows = []; $command->on('result', function ($row) use (&$rows) { $rows[] = $row; }); $command->on('end', function () use ($command, $deferred, &$rows) { $result = new MysqlResult(); $result->resultFields = $command->fields; $result->resultRows = $rows; $result->warningCount = $command->warningCount; $rows = []; $this->idle(); $deferred->resolve($result); }); // resolve / reject status reply (response without result set) $command->on('error', function ($error) use ($deferred) { $this->idle(); $deferred->reject($error); }); $command->on('success', function () use ($command, $deferred) { $result = new MysqlResult(); $result->affectedRows = $command->affectedRows; $result->insertId = $command->insertId; $result->warningCount = $command->warningCount; $this->idle(); $deferred->resolve($result); }); return $deferred->promise(); } public function queryStream(Query $query) { $command = new QueryCommand(); $command->setQuery($query); $this->_doCommand($command); $this->awake(); $stream = new QueryStream($command, $this->stream); $stream->on('close', function () { $this->idle(); }); return $stream; } public function ping() { return new Promise(function ($resolve, $reject) { $command = $this->_doCommand(new PingCommand()); $this->awake(); $command->on('success', function () use ($resolve) { $this->idle(); $resolve(null); }); $command->on('error', function ($reason) use ($reject) { $this->idle(); $reject($reason); }); }); } public function quit() { return new Promise(function ($resolve, $reject) { $command = $this->_doCommand(new QuitCommand()); $this->state = self::STATE_CLOSING; // mark connection as "awake" until it is closed, so never "idle" $this->awake(); $command->on('success', function () use ($resolve) { $resolve(null); $this->close(); }); $command->on('error', function ($reason) use ($reject) { $reject($reason); $this->close(); }); }); } public function close() { if ($this->state === self::STATE_CLOSED) { return; } $this->state = self::STATE_CLOSED; $remoteClosed = $this->stream->isReadable() === false && $this->stream->isWritable() === false; $this->stream->close(); if ($this->idleTimer !== null) { $this->loop->cancelTimer($this->idleTimer); $this->idleTimer = null; } // reject all pending commands if connection is closed while (!$this->executor->isIdle()) { $command = $this->executor->dequeue(); assert($command instanceof CommandInterface); if ($remoteClosed) { $command->emit('error', [new \RuntimeException( 'Connection closed by peer (ECONNRESET)', \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104 )]); } else { $command->emit('error', [new \RuntimeException( 'Connection closing (ECONNABORTED)', \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 )]); } } $this->emit('close'); $this->removeAllListeners(); } /** * @param Exception $err Error from socket. * * @return void * @internal */ public function handleConnectionError($err) { $this->emit('error', [$err, $this]); } /** * @return void * @internal */ public function handleConnectionClosed() { if ($this->state < self::STATE_CLOSING) { $this->emit('error', [new \RuntimeException( 'Connection closed by peer (ECONNRESET)', \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104 )]); } $this->close(); } /** * @param CommandInterface $command The command which should be executed. * @return CommandInterface * @throws Exception Can't send command */ protected function _doCommand(CommandInterface $command) { if ($this->state !== self::STATE_AUTHENTICATED) { throw new \RuntimeException( 'Connection ' . ($this->state === self::STATE_CLOSED ? 'closed' : 'closing'). ' (ENOTCONN)', \defined('SOCKET_ENOTCONN') ? \SOCKET_ENOTCONN : 107 ); } return $this->executor->enqueue($command); } private function awake() { ++$this->pending; if ($this->idleTimer !== null) { $this->loop->cancelTimer($this->idleTimer); $this->idleTimer = null; } } private function idle() { --$this->pending; if ($this->pending < 1 && $this->idlePeriod >= 0 && $this->state === self::STATE_AUTHENTICATED) { $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () { // soft-close connection and emit close event afterwards both on success or on error $this->idleTimer = null; $this->quit()->then(null, function () { // ignore to avoid reporting unhandled rejection }); }); } } } ================================================ FILE: src/Io/Constants.php ================================================ queue = new \SplQueue(); } public function isIdle() { return $this->queue->isEmpty(); } public function enqueue($command) { $this->queue->enqueue($command); $this->emit('new'); return $command; } public function dequeue() { return $this->queue->dequeue(); } } ================================================ FILE: src/Io/Factory.php ================================================ '127.0.0.1', * 'tcp' => [ * 'bindto' => '192.168.10.1:0' * ], * 'tls' => [ * 'verify_peer' => false, * 'verify_peer_name' => false * ] * ]); * * $factory = new React\Mysql\Factory(null, $connector); * ``` * * @param ?LoopInterface $loop * @param ?ConnectorInterface $connector */ public function __construct($loop = null, $connector = null) { // manual type check to support legacy PHP < 7.1 assert($loop === null || $loop instanceof LoopInterface); assert($connector === null || $connector instanceof ConnectorInterface); $this->loop = $loop ?: Loop::get(); $this->connector = $connector ?: new Connector([], $this->loop); } /** * Creates a new connection. * * It helps with establishing a TCP/IP connection to your MySQL database * and issuing the initial authentication handshake. * * ```php * $factory->createConnection($url)->then( * function (Connection $connection) { * // client connection established (and authenticated) * }, * function (Exception $e) { * // an error occurred while trying to connect or authorize client * } * ); * ``` * * The method returns a [Promise](https://github.com/reactphp/promise) that * will resolve with an internal `Connection` * instance on success or will reject with an `Exception` if the URL is * invalid or the connection or authentication fails. * * The returned Promise is implemented in such a way that it can be * cancelled when it is still pending. Cancelling a pending promise will * reject its value with an Exception and will cancel the underlying TCP/IP * connection attempt and/or MySQL authentication. * * ```php * $promise = $factory->createConnection($url); * * Loop::addTimer(3.0, function () use ($promise) { * $promise->cancel(); * }); * ``` * * The `$url` parameter must contain the database host, optional * authentication, port and database to connect to: * * ```php * $factory->createConnection('user:secret@localhost:3306/database'); * ``` * * Note that both the username and password must be URL-encoded (percent-encoded) * if they contain special characters: * * ```php * $user = 'he:llo'; * $pass = 'p@ss'; * * $promise = $factory->createConnection( * rawurlencode($user) . ':' . rawurlencode($pass) . '@localhost:3306/db' * ); * ``` * * You can omit the port if you're connecting to default port `3306`: * * ```php * $factory->createConnection('user:secret@localhost/database'); * ``` * * If you do not include authentication and/or database, then this method * will default to trying to connect as user `root` with an empty password * and no database selected. This may be useful when initially setting up a * database, but likely to yield an authentication error in a production system: * * ```php * $factory->createConnection('localhost'); * ``` * * This method respects PHP's `default_socket_timeout` setting (default 60s) * as a timeout for establishing the connection and waiting for successful * authentication. You can explicitly pass a custom timeout value in seconds * (or use a negative number to not apply a timeout) like this: * * ```php * $factory->createConnection('localhost?timeout=0.5'); * ``` * * By default, the connection provides full UTF-8 support (using the * `utf8mb4` charset encoding). This should usually not be changed for most * applications nowadays, but for legacy reasons you can change this to use * a different ASCII-compatible charset encoding like this: * * ```php * $factory->createConnection('localhost?charset=utf8mb4'); * ``` * * @param string $uri * @return PromiseInterface * Resolves with a `Connection` on success or rejects with an `Exception` on error. */ public function createConnection( #[\SensitiveParameter] $uri ) { $parts = parse_url($uri); $uri = preg_replace('#:[^:/]*@#', ':***@', $uri); assert(is_array($parts) && isset($parts['scheme'], $parts['host'])); assert($parts['scheme'] === 'mysql'); $args = []; if (isset($parts['query'])) { parse_str($parts['query'], $args); } /** @throws void already validated in MysqlClient ctor */ $authCommand = new AuthenticateCommand( isset($parts['user']) ? rawurldecode($parts['user']) : 'root', isset($parts['pass']) ? rawurldecode($parts['pass']) : '', isset($parts['path']) ? rawurldecode(ltrim($parts['path'], '/')) : '', isset($args['charset']) ? $args['charset'] : 'utf8mb4' ); $connecting = $this->connector->connect( $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 3306) ); $deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) { // connection cancelled, start with rejecting attempt, then clean up $reject(new \RuntimeException( 'Connection to ' . $uri . ' cancelled (ECONNABORTED)', \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 )); // either close successful connection or cancel pending connection attempt $connecting->then(function (SocketConnectionInterface $connection) { $connection->close(); }, function () { // ignore to avoid reporting unhandled rejection }); $connecting->cancel(); }); $idlePeriod = isset($args['idle']) ? (float) $args['idle'] : null; $connecting->then(function (SocketConnectionInterface $stream) use ($authCommand, $deferred, $uri, $idlePeriod) { $executor = new Executor(); $parser = new Parser($stream, $executor); $connection = new Connection($stream, $executor, $parser, $this->loop, $idlePeriod); $command = $executor->enqueue($authCommand); $parser->start(); $command->on('success', function () use ($deferred, $connection) { $deferred->resolve($connection); }); $command->on('error', function (\Exception $error) use ($deferred, $stream, $uri) { $const = ''; $errno = $error->getCode(); if ($error instanceof Exception) { $const = ' (EACCES)'; $errno = \defined('SOCKET_EACCES') ? \SOCKET_EACCES : 13; } $deferred->reject(new \RuntimeException( 'Connection to ' . $uri . ' failed during authentication: ' . $error->getMessage() . $const, $errno, $error )); $stream->close(); }); }, function (\Exception $error) use ($deferred, $uri) { $deferred->reject(new \RuntimeException( 'Connection to ' . $uri . ' failed: ' . $error->getMessage(), $error->getCode(), $error )); }); // use timeout from explicit ?timeout=x parameter or default to PHP's default_socket_timeout (60) $timeout = (float) isset($args['timeout']) ? $args['timeout'] : ini_get("default_socket_timeout"); if ($timeout < 0) { return $deferred->promise(); } return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) { if ($e instanceof TimeoutException) { throw new \RuntimeException( 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)', \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110 ); } throw $e; }); } } ================================================ FILE: src/Io/Parser.php ================================================ stream = $stream; $this->executor = $executor; $this->buffer = new Buffer(); $executor->on('new', function () { $this->nextRequest(); }); } /** * busy executing some command such as query or ping * * @return bool * @throws void */ public function isBusy() { return $this->currCommand !== null; } public function start() { $this->stream->on('data', [$this, 'handleData']); $this->stream->on('close', [$this, 'onClose']); } public function debug($message) { if ($this->debug) { $bt = \debug_backtrace(); $caller = \array_shift($bt); printf("[DEBUG] <%s:%d> %s\n", $caller['class'], $caller['line'], $message); } } /** @var string $data */ public function handleData($data) { $this->buffer->append($data); if ($this->debug) { $this->debug('Received ' . strlen($data) . ' byte(s), buffer now has ' . ($len = $this->buffer->length()) . ' byte(s): ' . wordwrap(bin2hex($b = $this->buffer->read($len)), 2, ' ', true)); $this->buffer->append($b); // @codeCoverageIgnore } while ($this->buffer->length() >= $this->pctSize) { if ($this->state === self::STATE_STANDBY) { $this->pctSize = $this->buffer->readInt3(); //printf("packet size:%d\n", $this->pctSize); $this->state = self::STATE_BODY; $this->seq = $this->buffer->readInt1() + 1; } $len = $this->buffer->length(); if ($len < $this->pctSize) { $this->debug('Waiting for complete packet with ' . $len . '/' . $this->pctSize . ' bytes'); return; } $packet = $this->buffer->readBuffer($this->pctSize); $this->state = self::STATE_STANDBY; $this->pctSize = self::PACKET_SIZE_HEADER; try { $this->parsePacket($packet); } catch (\UnderflowException $e) { $this->onError(new \UnexpectedValueException('Unexpected protocol error, received malformed packet: ' . $e->getMessage(), 0, $e)); $this->stream->close(); return; } if ($packet->length() !== 0) { $this->onError(new \UnexpectedValueException('Unexpected protocol error, received malformed packet with ' . $packet->length() . ' unknown byte(s)')); $this->stream->close(); return; } } } /** @return void */ private function parsePacket(Buffer $packet) { if ($this->debug) { $this->debug('Parse packet#' . $this->seq . ' with ' . ($len = $packet->length()) . ' bytes: ' . wordwrap(bin2hex($b = $packet->read($len)), 2, ' ', true)); $packet->append($b); // @codeCoverageIgnore } if ($this->phase === 0) { $response = $packet->readInt1(); if ($response === 0xFF) { // error packet before handshake means we did not exchange capabilities and error does not include SQL state $this->phase = self::PHASE_AUTH_ERR; $code = $packet->readInt2(); $exception = new MysqlException($packet->read($packet->length()), $code); $this->debug(sprintf("Error Packet:%d %s\n", $code, $exception->getMessage())); // error during init phase also means we're not currently executing any command // simply reject the first outstanding command in the queue (AuthenticateCommand) $this->currCommand = $this->executor->dequeue(); $this->onError($exception); return; } $this->phase = self::PHASE_GOT_INIT; $this->protocolVersion = $response; $this->debug(sprintf("Protocol Version: %d", $this->protocolVersion)); $options = &$this->connectOptions; $options['serverVersion'] = $packet->readStringNull(); $options['threadId'] = $packet->readInt4(); $this->scramble = $packet->read(8); // 1st part $packet->skip(1); // filler $options['ServerCaps'] = $packet->readInt2(); // 1st part $options['serverLang'] = $packet->readInt1(); $options['serverStatus'] = $packet->readInt2(); $options['ServerCaps'] += $packet->readInt2() << 16; // 2nd part $packet->skip(11); // plugin length, 6 + 4 filler $this->scramble .= $packet->read(12); // 2nd part $packet->skip(1); if ($this->connectOptions['ServerCaps'] & Constants::CLIENT_PLUGIN_AUTH) { $this->authPlugin = $packet->readStringNull(); $this->debug('Authentication plugin: ' . $this->authPlugin); } // init completed, continue with sending AuthenticateCommand $this->nextRequest(true); } else { $fieldCount = $packet->readInt1(); if ($fieldCount === 0xFF) { // error packet $code = $packet->readInt2(); $packet->skip(6); // skip SQL state $exception = new MysqlException($packet->read($packet->length()), $code); $this->debug(sprintf("Error Packet:%d %s\n", $code, $exception->getMessage())); $this->onError($exception); $this->nextRequest(); } elseif ($fieldCount === 0x00 && $this->rsState !== self::RS_STATE_ROW) { // Empty OK Packet terminates a query without a result set (UPDATE, INSERT etc.) $this->debug('Ok Packet'); if ($this->phase === self::PHASE_AUTH_SENT) { $this->phase = self::PHASE_HANDSHAKED; } $this->affectedRows = $packet->readIntLen(); $this->insertId = $packet->readIntLen(); $this->serverStatus = $packet->readInt2(); $this->warningCount = $packet->readInt2(); $this->message = $packet->read($packet->length()); $this->debug(sprintf("AffectedRows: %d, InsertId: %d, WarningCount:%d", $this->affectedRows, $this->insertId, $this->warningCount)); $this->onSuccess(); $this->nextRequest(); } elseif ($fieldCount === 0xFE && $this->phase !== self::PHASE_AUTH_SENT) { // EOF Packet $packet->skip(4); // warn, status if ($this->rsState === self::RS_STATE_ROW) { // finalize this result set (all rows completed) $this->debug('Result set done'); $this->onResultDone(); $this->nextRequest(); } else { // move to next part of result set (header->field->row) $this->debug('Result set next part'); ++$this->rsState; } } elseif ($fieldCount === 0xFE && $this->phase === self::PHASE_AUTH_SENT) { // Protocol::AuthSwitchRequest packet // https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_switch_request.html $this->authPlugin = $packet->readStringNull(); $this->scramble = $packet->read($packet->length() - 1); $packet->skip(1); // 0x00 $this->debug('Switched to authentication plugin: ' . $this->authPlugin); try { assert($this->currCommand instanceof AuthenticateCommand); $this->sendPacket($this->currCommand->authResponse($this->scramble, $this->authPlugin)); //$this->sendPacket($this->currCommand->authenticatePacket($this->scramble, $this->authPlugin, $this->buffer)); } catch (\UnexpectedValueException $e) { $this->onError($e); $this->stream->close(); } } elseif ($fieldCount === 0x01 && $this->phase === self::PHASE_AUTH_SENT && $this->authPlugin === 'caching_sha2_password') { // Protocol::AuthMoreData packet // https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_packets_protocol_auth_more_data.html $status = $packet->readInt1(); if ($status === 0x03 && $packet->length() === 0) { // ignore fast auth success here, will be followed by OK packet $this->debug('Fast auth success'); } elseif ($status === 0x04 && $packet->length() === 0) { // fast auth failure means we need to request the certificate to send the encrypted password $this->debug('Fast auth failure, request certificate'); $this->sendPacket("\x02"); } else { // extra auth containing certificate data $this->debug('Extra auth certificate received, send encrypted password'); $packet->prepend($packet->buildInt1($status)); try { assert($this->currCommand instanceof AuthenticateCommand); $this->sendPacket($this->currCommand->authSha256($this->scramble, $packet->read($packet->length()))); } catch (\UnexpectedValueException $e) { $this->onError($e); $this->stream->close(); } } } else { // Data packet $packet->prepend($packet->buildInt1($fieldCount)); if ($this->rsState === self::RS_STATE_HEADER) { $columns = $packet->readIntLen(); // extra $this->debug('Result set with ' . $columns . ' column(s)'); $this->rsState = self::RS_STATE_FIELD; } elseif ($this->rsState === self::RS_STATE_FIELD) { $field = [ 'catalog' => $packet->readStringLen(), 'db' => $packet->readStringLen(), 'table' => $packet->readStringLen(), 'org_table' => $packet->readStringLen(), 'name' => $packet->readStringLen(), 'org_name' => $packet->readStringLen() ]; $packet->skip(1); // 0xC0 $field['charset'] = $packet->readInt2(); $field['length'] = $packet->readInt4(); $field['type'] = $packet->readInt1(); $field['flags'] = $packet->readInt2(); $field['decimals'] = $packet->readInt1(); $packet->skip(2); // unused if ($this->debug) { $this->debug('Result set column: ' . json_encode($field, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE)); // @codeCoverageIgnore } $this->resultFields[] = $field; } elseif ($this->rsState === self::RS_STATE_ROW) { $row = []; foreach ($this->resultFields as $field) { $row[$field['name']] = $packet->readStringLen(); } if ($this->debug) { $this->debug('Result set row: ' . json_encode($row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE)); // @codeCoverageIgnore } $this->onResultRow($row); } } } } private function onResultRow($row) { // $this->debug('row data: ' . json_encode($row)); $command = $this->currCommand; $command->emit('result', [$row]); } private function onError(\Exception $error) { $this->rsState = self::RS_STATE_HEADER; $this->resultFields = []; // reject current command with error if we're currently executing any commands // ignore unsolicited server error in case we're not executing any commands (connection will be dropped) if ($this->currCommand !== null) { $command = $this->currCommand; $this->currCommand = null; $command->emit('error', [$error]); } } protected function onResultDone() { $command = $this->currCommand; $this->currCommand = null; assert($command instanceof QueryCommand); $command->fields = $this->resultFields; $command->emit('end'); $this->rsState = self::RS_STATE_HEADER; $this->resultFields = []; } protected function onSuccess() { $command = $this->currCommand; $this->currCommand = null; if ($command instanceof QueryCommand) { $command->affectedRows = $this->affectedRows; $command->insertId = $this->insertId; $command->warningCount = $this->warningCount; } $command->emit('success'); } public function onClose() { if ($this->currCommand !== null) { $command = $this->currCommand; $this->currCommand = null; if ($command instanceof QuitCommand) { $command->emit('success'); } else { $command->emit('error', [new \RuntimeException( 'Connection closing (ECONNABORTED)', \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 )]); } } } public function sendPacket($packet) { return $this->stream->write($this->buffer->buildInt3(\strlen($packet)) . $this->buffer->buildInt1($this->seq++) . $packet); } protected function nextRequest($isHandshake = false) { if (!$isHandshake && $this->phase != self::PHASE_HANDSHAKED) { return false; } if ($this->currCommand === null && !$this->executor->isIdle()) { $command = $this->executor->dequeue(); $this->currCommand = $command; if ($command instanceof AuthenticateCommand) { $this->phase = self::PHASE_AUTH_SENT; try { $this->sendPacket($command->authenticatePacket($this->scramble, $this->authPlugin, $this->buffer)); } catch (\UnexpectedValueException $e) { $this->onError($e); $this->stream->close(); } } else { $this->seq = 0; $this->sendPacket($this->buffer->buildInt1($command->getId()) . $command->getSql()); } } return true; } } ================================================ FILE: src/Io/Query.php ================================================ * @see \React\Mysql\Commands\AuthenticateCommand::$charsetMap */ private $escapeChars = [ //"\x00" => "\\0", //"\r" => "\\r", //"\n" => "\\n", //"\t" => "\\t", //"\b" => "\\b", //"\x1a" => "\\Z", "'" => "''", //'"' => '\"', "\\" => "\\\\", //"%" => "\\%", //"_" => "\\_", ]; /** * @param string $sql * @param list $params * @throws \InvalidArgumentException if given $params are invalid */ public function __construct($sql, array $params = []) { foreach ($params as $param) { if (!\is_scalar($param) && $param !== null) { throw new \InvalidArgumentException('Query param must be of type string|int|float|bool|null, ' . (\is_object($param) ? \get_class($param) : \gettype($param)) . ' given'); } } $this->sql = $sql; $this->builtSql = $params ? null : $sql; $this->params = $params; } public function escape($str) { return strtr($str, $this->escapeChars); } /** * @param mixed $value * @return string */ protected function resolveValueForSql($value) { $type = gettype($value); switch ($type) { case 'boolean': $value = (int) $value; break; case 'double': case 'integer': break; case 'string': $value = "'" . $this->escape($value) . "'"; break; case 'NULL': $value = 'NULL'; break; } return $value; } protected function buildSql() { $sql = $this->sql; $offset = strpos($sql, '?'); foreach ($this->params as $param) { $replacement = $this->resolveValueForSql($param); $sql = substr_replace($sql, $replacement, $offset, 1); $offset = strpos($sql, '?', $offset + strlen($replacement)); } if ($offset !== false) { throw new \LogicException('Params not enough to build sql'); } return $sql; /* $names = []; $inName = false; $currName = ''; $currIdx = 0; $sql = $this->sql; $len = strlen($sql); $i = 0; do { $c = $sql[$i]; if ($c === '?') { $names[$i] = $c; } elseif ($c === ':') { $currName .= $c; $currIdx = $i; $inName = true; } elseif ($c === ' ') { $inName = false; if ($currName) { $names[$currIdx] = $currName; $currName = ''; } } else { if ($inName) { $currName .= $c; } } } while (++ $i < $len); if ($inName) { $names[$currIdx] = $currName; } $namedMarks = $unnamedMarks = []; foreach ($this->params as $arg) { if (is_array($arg)) { $namedMarks += $arg; } else { $unnamedMarks[] = $arg; } } $offset = 0; foreach ($names as $idx => $value) { if ($value === '?') { $replacement = array_shift($unnamedMarks); } else { $replacement = $namedMarks[$value]; } list($arg, $len) = $this->getEscapedStringAndLen($replacement); $sql = substr_replace($sql, $arg, $idx + $offset, strlen($value)); $offset += $len - strlen($value); } return $sql; */ } /** * Get the constructed and escaped sql string. * * @return string */ public function getSql() { if ($this->builtSql === null) { $this->builtSql = $this->buildSql(); } return $this->builtSql; } } ================================================ FILE: src/Io/QueryStream.php ================================================ connection = $connection; // forward result set rows until result set end $command->on('result', function ($row) { if (!$this->started && $this->paused) { $this->connection->pause(); } $this->started = true; $this->emit('data', [$row]); }); $command->on('end', function () { $this->emit('end'); $this->close(); }); // status reply (response without result set) ends stream without data $command->on('success', function () { $this->emit('end'); $this->close(); }); $command->on('error', function ($err) { $this->emit('error', [$err]); $this->close(); }); } public function isReadable() { return !$this->closed; } public function pause() { $this->paused = true; if ($this->started && !$this->closed) { $this->connection->pause(); } } public function resume() { $this->paused = false; if ($this->started && !$this->closed) { $this->connection->resume(); } } public function close() { if ($this->closed) { return; } $this->closed = true; if ($this->started && $this->paused) { $this->connection->resume(); } $this->emit('close'); $this->removeAllListeners(); } public function pipe(WritableStreamInterface $dest, array $options = []) { return Util::pipe($this, $dest, $options); } } ================================================ FILE: src/MysqlClient.php ================================================ on('error', function (Exception $e) { * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * * This event will only be triggered for fatal errors and will be followed * by closing the connection. It is not to be confused with "soft" errors * caused by invalid SQL queries. * * close event: * The `close` event will be emitted once the connection closes (terminates). * * ```php * $mysql->on('close', function () { * echo 'Connection closed' . PHP_EOL; * }); * ``` * * See also the [`close()`](#close) method. * * @final */ class MysqlClient extends EventEmitter { private $factory; private $uri; private $closed = false; /** @var PromiseInterface|null */ private $connecting; /** @var ?Connection */ private $connection; /** * array of outstanding connection requests to send next commands once a connection becomes ready * * @var array> */ private $pending = []; /** * set to true only between calling `quit()` and the connection closing in response * * @var bool * @see self::quit() * @see self::$closed */ private $quitting = false; /** * @param string $uri * @param ?ConnectorInterface $connector * @param ?LoopInterface $loop * @throws \InvalidArgumentException if $uri is not a valid MySQL URI */ public function __construct( #[\SensitiveParameter] $uri, $connector = null, $loop = null ) { if (strpos($uri, '://') === false) { $uri = 'mysql://' . $uri; } $parts = parse_url($uri); if ($parts === false || !isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'mysql') { $uri = preg_replace('#:[^:/]*@#', ':***@', $uri); throw new \InvalidArgumentException( 'Invalid MySQL URI "' . $uri . '" (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); } if (isset($parts['query'])) { $query = []; parse_str($parts['query'], $query); // validate charset if given if (isset($query['charset'])) { new AuthenticateCommand('', '', '', $query['charset']); } } if ($connector !== null && !$connector instanceof ConnectorInterface) { // manual type check to support legacy PHP < 7.1 throw new \InvalidArgumentException('Argument #2 ($connector) expected null|React\Socket\ConnectorInterface'); } if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1 throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface'); } $this->factory = new Factory($loop, $connector); $this->uri = $uri; } /** * Performs an async query. * * This method returns a promise that will resolve with a `MysqlResult` on * success or will reject with an `Exception` on error. The MySQL protocol * is inherently sequential, so that all queries will be performed in order * and outstanding queries will be put into a queue to be executed once the * previous queries are completed. * * ```php * $mysql->query('CREATE TABLE test ...'); * $mysql->query('INSERT INTO test (id) VALUES (1)'); * ``` * * If this SQL statement returns a result set (such as from a `SELECT` * statement), this method will buffer everything in memory until the result * set is completed and will then resolve the resulting promise. This is * the preferred method if you know your result set to not exceed a few * dozens or hundreds of rows. If the size of your result set is either * unknown or known to be too large to fit into memory, you should use the * [`queryStream()`](#querystream) method instead. * * ```php * $mysql->query($query)->then(function (React\Mysql\MysqlResult $command) { * if (isset($command->resultRows)) { * // this is a response to a SELECT etc. with some rows (0+) * print_r($command->resultFields); * print_r($command->resultRows); * echo count($command->resultRows) . ' row(s) in set' . PHP_EOL; * } else { * // this is an OK message in response to an UPDATE etc. * if ($command->insertId !== 0) { * var_dump('last insert ID', $command->insertId); * } * echo 'Query OK, ' . $command->affectedRows . ' row(s) affected' . PHP_EOL; * } * }, function (Exception $error) { * // the query was not executed successfully * echo 'Error: ' . $error->getMessage() . PHP_EOL; * }); * ``` * * You can optionally pass an array of `$params` that will be bound to the * query like this: * * ```php * $mysql->query('SELECT * FROM user WHERE id > ?', [$id]); * ``` * * The given `$sql` parameter MUST contain a single statement. Support * for multiple statements is disabled for security reasons because it * could allow for possible SQL injection attacks and this API is not * suited for exposing multiple possible results. * * @param string $sql SQL statement * @param list $params Parameters which should be bound to query * @return PromiseInterface * Resolves with a `MysqlResult` on success or rejects with an `Exception` on error. * @throws \InvalidArgumentException if given $params are invalid */ public function query($sql, array $params = []) { $query = new Query($sql, $params); if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } return $this->getConnection()->then(function (Connection $connection) use ($query) { return $connection->query($query)->then(function (MysqlResult $result) use ($connection) { $this->handleConnectionReady($connection); return $result; }, function (\Exception $e) use ($connection) { $this->handleConnectionReady($connection); throw $e; }); }); } /** * Performs an async query and streams the rows of the result set. * * This method returns a readable stream that will emit each row of the * result set as a `data` event. It will only buffer data to complete a * single row in memory and will not store the whole result set. This allows * you to process result sets of unlimited size that would not otherwise fit * into memory. If you know your result set to not exceed a few dozens or * hundreds of rows, you may want to use the [`query()`](#query) method instead. * * ```php * $stream = $mysql->queryStream('SELECT * FROM user'); * $stream->on('data', function ($row) { * echo $row['name'] . PHP_EOL; * }); * $stream->on('end', function () { * echo 'Completed.'; * }); * ``` * * You can optionally pass an array of `$params` that will be bound to the * query like this: * * ```php * $stream = $mysql->queryStream('SELECT * FROM user WHERE id > ?', [$id]); * ``` * * This method is specifically designed for queries that return a result set * (such as from a `SELECT` or `EXPLAIN` statement). Queries that do not * return a result set (such as a `UPDATE` or `INSERT` statement) will not * emit any `data` events. * * See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) * for more details about how readable streams can be used in ReactPHP. For * example, you can also use its `pipe()` method to forward the result set * rows to a [`WritableStreamInterface`](https://github.com/reactphp/stream#writablestreaminterface) * like this: * * ```php * $mysql->queryStream('SELECT * FROM user')->pipe($formatter)->pipe($logger); * ``` * * Note that as per the underlying stream definition, calling `pause()` and * `resume()` on this stream is advisory-only, i.e. the stream MAY continue * emitting some data until the underlying network buffer is drained. Also * notice that the server side limits how long a connection is allowed to be * in a state that has outgoing data. Special care should be taken to ensure * the stream is resumed in time. This implies that using `pipe()` with a * slow destination stream may cause the connection to abort after a while. * * The given `$sql` parameter MUST contain a single statement. Support * for multiple statements is disabled for security reasons because it * could allow for possible SQL injection attacks and this API is not * suited for exposing multiple possible results. * * @param string $sql SQL statement * @param list $params Parameters which should be bound to query * @return ReadableStreamInterface * @throws \InvalidArgumentException if given $params are invalid * @throws Exception if connection is already closed/closing */ public function queryStream($sql, array $params = []) { $query = new Query($sql, $params); if ($this->closed || $this->quitting) { throw new Exception('Connection closed'); } return \React\Promise\Stream\unwrapReadable( $this->getConnection()->then(function (Connection $connection) use ($query) { $stream = $connection->queryStream($query); $stream->on('end', function () use ($connection) { $this->handleConnectionReady($connection); }); $stream->on('error', function () use ($connection) { $this->handleConnectionReady($connection); }); return $stream; }) ); } /** * Checks that the connection is alive. * * This method returns a promise that will resolve (with a void value) on * success or will reject with an `Exception` on error. The MySQL protocol * is inherently sequential, so that all commands will be performed in order * and outstanding command will be put into a queue to be executed once the * previous queries are completed. * * ```php * $mysql->ping()->then(function () { * echo 'OK' . PHP_EOL; * }, function (Exception $e) { * echo 'Error: ' . $e->getMessage() . PHP_EOL; * }); * ``` * * @return PromiseInterface * Resolves with a `void` value on success or rejects with an `Exception` on error. */ public function ping() { if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } return $this->getConnection()->then(function (Connection $connection) { return $connection->ping()->then(function () use ($connection) { $this->handleConnectionReady($connection); }, function (\Exception $e) use ($connection) { $this->handleConnectionReady($connection); throw $e; }); }); } /** * Quits (soft-close) the connection. * * This method returns a promise that will resolve (with a void value) on * success or will reject with an `Exception` on error. The MySQL protocol * is inherently sequential, so that all commands will be performed in order * and outstanding commands will be put into a queue to be executed once the * previous commands are completed. * * ```php * $mysql->query('CREATE TABLE test ...'); * $mysql->quit(); * ``` * * This method will gracefully close the connection to the MySQL database * server once all outstanding commands are completed. See also * [`close()`](#close) if you want to force-close the connection without * waiting for any commands to complete instead. * * @return PromiseInterface * Resolves with a `void` value on success or rejects with an `Exception` on error. */ public function quit() { if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } // not already connecting => no need to connect, simply close virtual connection if ($this->connection === null && $this->connecting === null) { $this->close(); return \React\Promise\resolve(null); } $this->quitting = true; return new Promise(function (callable $resolve, callable $reject) { $this->getConnection()->then(function (Connection $connection) use ($resolve, $reject) { // soft-close connection and emit close event afterwards both on success or on error $connection->quit()->then( function () use ($resolve){ $resolve(null); $this->close(); }, function (\Exception $e) use ($reject) { $reject($e); $this->close(); } ); }, function (\Exception $e) use ($reject) { // emit close event afterwards when no connection can be established $reject($e); $this->close(); }); }); } /** * Force-close the connection. * * Unlike the `quit()` method, this method will immediately force-close the * connection and reject all outstanding commands. * * ```php * $mysql->close(); * ``` * * Forcefully closing the connection will yield a warning in the server logs * and should generally only be used as a last resort. See also * [`quit()`](#quit) as a safe alternative. * * @return void */ public function close() { if ($this->closed) { return; } $this->closed = true; $this->quitting = false; // either close active connection or cancel pending connection attempt // below branches are exclusive, there can only be a single connection if ($this->connection !== null) { $this->connection->close(); $this->connection = null; } elseif ($this->connecting !== null) { $this->connecting->cancel(); $this->connecting = null; } // clear all outstanding commands foreach ($this->pending as $deferred) { $deferred->reject(new \RuntimeException('Connection closed')); } $this->pending = []; $this->emit('close'); $this->removeAllListeners(); } /** * @return PromiseInterface */ private function getConnection() { $deferred = new Deferred(); // force-close connection if still waiting for previous disconnection due to idle timer if ($this->connection !== null && $this->connection->state === Connection::STATE_CLOSING) { $this->connection->close(); $this->connection = null; } // happy path: reuse existing connection unless it is currently busy executing another command if ($this->connection !== null && !$this->connection->isBusy()) { $deferred->resolve($this->connection); return $deferred->promise(); } // queue pending connection request until connection becomes ready $this->pending[] = $deferred; // create new connection if not already connected or connecting if ($this->connection === null && $this->connecting === null) { $this->connecting = $this->factory->createConnection($this->uri); $this->connecting->then(function (Connection $connection) { // connection completed => remember only until closed $this->connecting = null; $this->connection = $connection; $connection->on('close', function () { $this->connection = null; }); // handle first command from queue when connection is ready $this->handleConnectionReady($connection); }, function (\Exception $e) { // connection failed => discard connection attempt $this->connecting = null; foreach ($this->pending as $key => $deferred) { $deferred->reject($e); unset($this->pending[$key]); } }); } return $deferred->promise(); } private function handleConnectionReady(Connection $connection) { $deferred = \reset($this->pending); if ($deferred === false) { // nothing to do if there are no outstanding connection requests return; } assert($deferred instanceof Deferred); unset($this->pending[\key($this->pending)]); $deferred->resolve($connection); } } ================================================ FILE: src/MysqlResult.php ================================================ getenv('DB_HOST'), 'port' => (int)getenv('DB_PORT'), 'dbname' => getenv('DB_DBNAME'), 'user' => getenv('DB_USER'), 'passwd' => getenv('DB_PASSWD'), ] + ($debug ? ['debug' => true] : []); } protected function getConnectionString($params = []) { $parts = $params + $this->getConnectionOptions(); return 'mysql://' . rawurlencode($parts['user']) . ':' . rawurlencode($parts['passwd']) . '@' . $parts['host'] . ':' . $parts['port'] . '/' . rawurlencode($parts['dbname']); } /** * @param LoopInterface $loop * @return Connection */ protected function createConnection(LoopInterface $loop) { $factory = new Factory($loop); $promise = $factory->createConnection($this->getConnectionString()); return \React\Async\await(\React\Promise\Timer\timeout($promise, 10.0)); } protected function getDataTable() { return <<getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock(); $mock->expects($this->once())->method('__invoke'); return $mock; } protected function expectCallableOnceWith($value) { $mock = $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock(); $mock->expects($this->once())->method('__invoke')->with($value); return $mock; } protected function expectCallableNever() { $mock = $this->getMockBuilder('stdClass')->setMethods(['__invoke'])->getMock(); $mock->expects($this->never())->method('__invoke'); return $mock; } public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) { if (method_exists($this, 'expectException')) { // PHPUnit 5.2+ $this->expectException($exception); if ($exceptionMessage !== '') { $this->expectExceptionMessage($exceptionMessage); } if ($exceptionCode !== null) { $this->expectExceptionCode($exceptionCode); } } else { // legacy PHPUnit 4 - PHPUnit 5.1 parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); } } } ================================================ FILE: tests/Commands/AuthenticateCommandTest.php ================================================ expectException('InvalidArgumentException'); } else { // legacy PHPUnit < 5.2 $this->setExpectedException('InvalidArgumentException'); } new AuthenticateCommand('Alice', 'secret', '', 'utf16'); } public function testAuthenticatePacketWithEmptyPassword() { $command = new AuthenticateCommand('root', '', 'test', 'utf8mb4'); $data = $command->authenticatePacket('scramble', null, new Buffer()); $this->assertEquals("\x8d\xa6\0\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\0" . "test\0", $data); } public function testAuthenticatePacketWithMysqlNativePasswordAuthPluginAndEmptyPassword() { $command = new AuthenticateCommand('root', '', 'test', 'utf8mb4'); $data = $command->authenticatePacket('scramble', 'mysql_native_password', new Buffer()); $this->assertEquals("\x8d\xa6\x08\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\0" . "test\0" . "mysql_native_password\0", $data); } public function testAuthenticatePacketWithCachingSha2PasswordAuthPluginAndEmptyPassword() { $command = new AuthenticateCommand('root', '', 'test', 'utf8mb4'); $data = $command->authenticatePacket('scramble', 'caching_sha2_password', new Buffer()); $this->assertEquals("\x8d\xa6\x08\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\0" . "test\0" . "caching_sha2_password\0", $data); } public function testAuthenticatePacketWithSecretPassword() { $command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4'); $data = $command->authenticatePacket('scramble', null, new Buffer()); $this->assertEquals("\x8d\xa6\0\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\x14\x18\xd8\x8d\x74\x77\x2c\x27\x22\x89\xe1\xcd\xbc\x4b\x5f\x77\x08\x18\x3c\x6e\xba" . "test\0", $data); } /** * @requires PHP 7.1 * @requires function hash */ public function testAuthenticatePacketWithCachingSha2PasswordWithSecretPasswordHashed() { $command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4'); $data = $command->authenticatePacket('scramble', 'caching_sha2_password', new Buffer()); $this->assertEquals("\x8d\xa6\x08\0\0\0\0\x01\x2d" . str_repeat("\0", 23) . "root\0" . "\x20\x7a\x62\x89\x95\x53\xed\xdd\xa4\x11\x2d\x28\x9a\x02\x72\x12\xbb\x4c\xdd\xfd\xd3\x08\xfe\xc3\x6a\x85\xf1\xe9\x4a\xdb\xcf\x8b\xf3" . "test\0" . "caching_sha2_password\0", $data); } public function testAuthenticatePacketWithUnknownAuthPluginThrows() { $command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4'); if (method_exists($this, 'expectException')) { $this->expectException('UnexpectedValueException'); $this->expectExceptionMessage('Unknown authentication plugin "mysql_old_password" requested by server'); } else { // legacy PHPUnit < 5.2 $this->setExpectedException('UnexpectedValueException', 'Unknown authentication plugin "mysql_old_password" requested by server'); } $command->authenticatePacket('scramble', 'mysql_old_password', new Buffer()); } /** * @requires function openssl_public_encrypt */ public function testAuthSha256WithValidPublicKeyReturnsScrambledDataThatCanBeDecryptedByReceiverWithPrivateKey() { $command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4'); $key = openssl_pkey_new(); $encrypted = $command->authSha256('scramble', openssl_pkey_get_details($key)['key']); $decrypted = ''; $ok = openssl_private_decrypt($encrypted, $decrypted, $key, OPENSSL_PKCS1_OAEP_PADDING); $this->assertTrue($ok); $this->assertEquals("secret\0", $decrypted ^ "scramble"); } /** * @requires function openssl_public_encrypt */ public function testAuthSha256WithPasswordLongerThanScrambleLengthReturnsScrambledDataThatCanBeDecryptedByReceiverWithPrivateKey() { $command = new AuthenticateCommand('root', '012345678901234567890123456789', 'test', 'utf8mb4'); $key = openssl_pkey_new(); $encrypted = $command->authSha256('scramble', openssl_pkey_get_details($key)['key']); $decrypted = ''; $ok = openssl_private_decrypt($encrypted, $decrypted, $key, OPENSSL_PKCS1_OAEP_PADDING); $this->assertTrue($ok); $this->assertEquals("012345678901234567890123456789\0", $decrypted ^ "scramblescramblescramblescramblescramble"); } /** * @requires function openssl_public_encrypt */ public function testAuthSha256WithInvalidPublicKeyThrows() { $command = new AuthenticateCommand('root', 'secret', 'test', 'utf8mb4'); if (method_exists($this, 'expectException')) { $this->expectException('UnexpectedValueException'); $this->expectExceptionMessage('Failed to encrypt password with public key'); } else { // legacy PHPUnit < 5.2 $this->setExpectedException('UnexpectedValueException', 'Failed to encrypt password with public key'); } $command->authSha256('scramble', 'invalid pubkey'); } } ================================================ FILE: tests/Io/BufferTest.php ================================================ append('hello'); $this->assertSame('hello', $buffer->read(5)); } public function testReadBeyondLimitThrows() { $buffer = new Buffer(); $buffer->append('hi'); $this->setExpectedException('UnderflowException'); $buffer->read(3); } public function testReadAfterSkipOne() { $buffer = new Buffer(); $buffer->append('hi'); $buffer->skip(1); $this->assertSame('i', $buffer->read(1)); } public function testReadBufferEmptyIsNoop() { $buffer = new Buffer(); $new = $buffer->readBuffer(0); $this->assertSame(0, $buffer->length()); $this->assertSame(0, $new->length()); } public function testReadBufferReturnsBufferWithOriginalLengthAndClearsOriginalBuffer() { $buffer = new Buffer(); $buffer->append('foo'); $new = $buffer->readBuffer($buffer->length()); $this->assertSame(0, $buffer->length()); $this->assertSame(3, $new->length()); } public function testReadBufferBeyondLimitThrows() { $buffer = new Buffer(); $this->setExpectedException('UnderflowException'); $buffer->readBuffer(3); } public function testSkipZeroThrows() { $buffer = new Buffer(); $buffer->append('hi'); $this->setExpectedException('UnderflowException'); $buffer->skip(0); } public function testSkipBeyondLimitThrows() { $buffer = new Buffer(); $buffer->append('hi'); $this->setExpectedException('UnderflowException'); $buffer->skip(3); } public function testParseInt1() { $buffer = new Buffer(); $buffer->append($buffer->buildInt1(0) . $buffer->buildInt1(255)); $this->assertSame(0, $buffer->readInt1()); $this->assertSame(255, $buffer->readInt1()); } public function testParseInt2() { $buffer = new Buffer(); $buffer->append($buffer->buildInt2(0) . $buffer->buildInt2(65535)); $this->assertSame(0, $buffer->readInt2()); $this->assertSame(65535, $buffer->readInt2()); } public function testParseInt3() { $buffer = new Buffer(); $buffer->append($buffer->buildInt3(0) . $buffer->buildInt3(0xFFFFFF)); $this->assertSame(0, $buffer->readInt3()); $this->assertSame(0xFFFFFF, $buffer->readInt3()); } public function testParseInt8() { $buffer = new Buffer(); $buffer->append($buffer->buildInt8(0) . $buffer->buildInt8(PHP_INT_MAX)); $this->assertSame(0, $buffer->readInt8()); $this->assertSame(PHP_INT_MAX, $buffer->readInt8()); } public function testParseIntLen() { $buffer = new Buffer(); $buffer->append("\x0A" . "\xFC" . "\x00\x04"); $this->assertSame(10, $buffer->readIntLen()); $this->assertSame(1024, $buffer->readIntLen()); } public function testParseStringEmpty() { $buffer = new Buffer(); $data = $buffer->buildStringLen(''); $this->assertEquals("\x00", $data); $buffer->append($data); $this->assertSame('', $buffer->readStringLen()); } public function testParseStringShort() { $buffer = new Buffer(); $data = $buffer->buildStringLen('hello'); $this->assertEquals("\x05" . "hello", $data); $buffer->append($data); $this->assertSame('hello', $buffer->readStringLen()); } public function testParseStringKilo() { $buffer = new Buffer(); $buffer->append($buffer->buildStringLen(str_repeat('.', 1024))); $this->assertSame(1024, strlen($buffer->readStringLen())); } public function testParseStringMega() { $buffer = new Buffer(); $buffer->append($buffer->buildStringLen(str_repeat('.', 1000000))); $this->assertSame(1000000, strlen($buffer->readStringLen())); } /** * Test encoding/parsing string larger than 16 MiB. This should not happen * in practice as the protocol parser is currently limited to a packet * size of 16 MiB. */ public function testParseStringExcessive() { $buffer = new Buffer(); $buffer->append($buffer->buildStringLen(str_repeat('.', 17000000))); $this->assertSame(17000000, strlen($buffer->readStringLen())); } public function testParseStringNullLength() { $buffer = new Buffer(); $data = $buffer->buildStringLen(null); $this->assertEquals("\xFB", $data); $buffer->append($data); $this->assertNull($buffer->readStringLen()); } public function testParseStringNullCharacterTwice() { $buffer = new Buffer(); $buffer->append("hello" . "\x00" . "world" . "\x00"); $this->assertEquals('hello', $buffer->readStringNull()); $this->assertEquals('world', $buffer->readStringNull()); } public function testParseStringNullCharacterThrowsIfNullNotFound() { $buffer = new Buffer(); $buffer->append("hello"); $this->setExpectedException('UnderflowException'); $buffer->readStringNull(); } } ================================================ FILE: tests/Io/ConnectionTest.php ================================================ getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue', 'isIdle'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $executor->expects($this->never())->method('isIdle'); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $parser->expects($this->once())->method('isBusy')->willReturn(true); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new Connection($stream, $executor, $parser, $loop, null); $connection->query(new Query('SELECT 1')); $this->assertTrue($connection->isBusy()); } public function testIsBusyReturnsFalseWhenParserIsNotBusyAndExecutorIsIdle() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->getMock(); $executor->expects($this->once())->method('isIdle')->willReturn(true); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertFalse($connection->isBusy()); } public function testQueryWillEnqueueOneCommand() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream->expects($this->never())->method('close'); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->query(new Query('SELECT 1')); } public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryCommandEmitsSuccess() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); $promise = $connection->query(new Query('SELECT 1')); $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult'))); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); } public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryCommandEmitsEnd() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); $promise = $connection->query(new Query('SELECT 1')); $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult'))); $this->assertNotNull($currentCommand); $currentCommand->emit('end'); } public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenIdlePeriodIsGivenAndQueryCommandEmitsSuccess() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(1.0, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $connection = new Connection($stream, $executor, $parser, $loop, 1.0); $this->assertNull($currentCommand); $promise = $connection->query(new Query('SELECT 1')); $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult'))); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); } public function testQueryWillReturnResolvedPromiseAndNotStartIdleTimerWhenIdlePeriodIsNegativeAndQueryCommandEmitsSuccess() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $connection = new Connection($stream, $executor, $parser, $loop, -1); $this->assertNull($currentCommand); $promise = $connection->query(new Query('SELECT 1')); $promise->then($this->expectCallableOnceWith($this->isInstanceOf('React\Mysql\MysqlResult'))); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); } public function testQueryWillReturnRejectedPromiseAndStartIdleTimerWhenQueryCommandEmitsError() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); $promise = $connection->query(new Query('SELECT 1')); $promise->then(null, $this->expectCallableOnce()); $this->assertNotNull($currentCommand); $currentCommand->emit('error', [new \RuntimeException()]); } public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitCloseEventWhenQuitCommandEmitsSuccess() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream->expects($this->once())->method('close'); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timeout = null; $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->callback(function ($cb) use (&$timeout) { $timeout = $cb; return true; }))->willReturn($timer); $connection = new Connection($stream, $executor, $parser, $loop, null); $connection->on('close', $this->expectCallableOnce()); $this->assertNull($currentCommand); $connection->query(new Query('SELECT 1')); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); $this->assertNotNull($timeout); $timeout(); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); } public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitCloseEventWhenQuitCommandEmitsError() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream->expects($this->once())->method('close'); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timeout = null; $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->callback(function ($cb) use (&$timeout) { $timeout = $cb; return true; }))->willReturn($timer); $connection = new Connection($stream, $executor, $parser, $loop, null); $connection->on('close', $this->expectCallableOnce()); $this->assertNull($currentCommand); $connection->query(new Query('SELECT 1')); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); $this->assertNotNull($timeout); $timeout(); $this->assertNotNull($currentCommand); $currentCommand->emit('error', [new \RuntimeException()]); } public function testQueryTwiceWillEnqueueSecondQueryWithoutStartingIdleTimerWhenFirstQueryCommandEmitsSuccess() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); $connection->query(new Query('SELECT 1')); $connection->query(new Query('SELECT 2')); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); } public function testQueryTwiceAfterIdleTimerWasStartedWillCancelIdleTimerAndEnqueueSecondCommand() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->any())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); $connection->query(new Query('SELECT 1')); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); $connection->query(new Query('SELECT 2')); } public function testQueryStreamWillEnqueueOneCommand() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream->expects($this->never())->method('close'); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->queryStream(new Query('SELECT 1')); } public function testQueryStreamWillReturnStreamThatWillEmitEndEventAndStartIdleTimerWhenQueryCommandEmitsSuccess() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); $stream = $connection->queryStream(new Query('SELECT 1')); $stream->on('end', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); } public function testQueryStreamWillReturnStreamThatWillEmitErrorEventAndStartIdleTimerWhenQueryCommandEmitsError() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); $stream = $connection->queryStream(new Query('SELECT 1')); $stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); $stream->on('close', $this->expectCallableOnce()); $this->assertNotNull($currentCommand); $currentCommand->emit('error', [new \RuntimeException()]); } public function testPingWillEnqueueOneCommand() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream->expects($this->never())->method('close'); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->ping(); } public function testPingWillReturnResolvedPromiseAndStartIdleTimerWhenPingCommandEmitsSuccess() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); $promise = $connection->ping(); $promise->then($this->expectCallableOnce()); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); } public function testPingWillReturnRejectedPromiseAndStartIdleTimerWhenPingCommandEmitsError() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); $loop->expects($this->never())->method('cancelTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); $promise = $connection->ping(); $promise->then(null, $this->expectCallableOnce()); $this->assertNotNull($currentCommand); $currentCommand->emit('error', [new \RuntimeException()]); } public function testQuitWillEnqueueOneCommand() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); } public function testQuitWillResolveBeforeEmittingCloseEventWhenQuitCommandEmitsSuccess() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $pingCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$pingCommand) { return $pingCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $events = ''; $connection->on('close', function () use (&$events) { $events .= 'closed.'; }); $this->assertEquals('', $events); $promise = $connection->quit(); $promise->then(function () use (&$events) { $events .= 'fulfilled.'; }); $this->assertEquals('', $events); $this->assertNotNull($pingCommand); $pingCommand->emit('success'); $this->assertEquals('fulfilled.closed.', $events); } public function testQuitWillRejectBeforeEmittingCloseEventWhenQuitCommandEmitsError() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $pingCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$pingCommand) { return $pingCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $events = ''; $connection->on('close', function () use (&$events) { $events .= 'closed.'; }); $this->assertEquals('', $events); $promise = $connection->quit(); $promise->then(null, function () use (&$events) { $events .= 'rejected.'; }); $this->assertEquals('', $events); $this->assertNotNull($pingCommand); $pingCommand->emit('error', [new \RuntimeException()]); $this->assertEquals('rejected.closed.', $events); } public function testCloseWillEmitCloseEvent() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->getMock(); $executor->expects($this->once())->method('isIdle')->willReturn(true); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addTimer'); $connection = new Connection($stream, $executor, $parser, $loop, null); $connection->on('close', $this->expectCallableOnce()); $connection->close(); } public function testCloseAfterIdleTimerWasStartedWillCancelIdleTimerAndEmitCloseEvent() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $currentCommand = null; $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$currentCommand) { return $currentCommand = $command; }); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); $connection->ping(); $this->assertNotNull($currentCommand); $currentCommand->emit('success'); $connection->on('close', $this->expectCallableOnce()); $connection->close(); } public function testQueryAfterQuitRejectsImmediately() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->query(new Query('SELECT 1')); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection closing (ENOTCONN)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107); }) ) )); } public function testQueryAfterCloseRejectsImmediately() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->never())->method('enqueue'); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->close(); $promise = $conn->query(new Query('SELECT 1')); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection closed (ENOTCONN)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107); }) ) )); } public function testQueryStreamAfterQuitThrows() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); try { $conn->queryStream(new Query('SELECT 1')); } catch (\RuntimeException $e) { $this->assertEquals('Connection closing (ENOTCONN)', $e->getMessage()); $this->assertEquals(defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107, $e->getCode()); } } public function testPingAfterQuitRejectsImmediately() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->ping(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection closing (ENOTCONN)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107); }) ) )); } public function testQuitAfterQuitRejectsImmediately() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->quit(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection closing (ENOTCONN)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107); }) ) )); } public function testCloseStreamEmitsErrorEvent() { $closeHandler = null; $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream->expects($this->exactly(2))->method('on')->withConsecutive( array('error', $this->anything()), array('close', $this->callback(function ($arg) use (&$closeHandler) { $closeHandler = $arg; return true; })) ); $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->never())->method('enqueue'); $parser = $this->getMockBuilder('React\Mysql\Io\Parser')->disableOriginalConstructor()->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->on('error', $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection closed by peer (ECONNRESET)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104); }) ) )); $this->assertNotNull($closeHandler); $closeHandler(); } } ================================================ FILE: tests/Io/FactoryTest.php ================================================ setAccessible(true); $loop = $ref->getValue($factory); $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); } public function testConnectWillUseHostAndDefaultPort() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $pending = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn($pending); $factory = new Factory($loop, $connector); $factory->createConnection('mysql://127.0.0.1'); } public function testConnectWillUseGivenScheme() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $pending = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn($pending); $factory = new Factory($loop, $connector); $factory->createConnection('mysql://127.0.0.1'); } public function testConnectWillUseGivenHostAndGivenPort() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $pending = $this->getMockBuilder('React\Promise\PromiseInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('127.0.0.1:1234')->willReturn($pending); $factory = new Factory($loop, $connector); $factory->createConnection('mysql://127.0.0.1:1234'); } public function testConnectWillUseGivenUserInfoAsDatabaseCredentialsAfterUrldecoding() { $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['write'])->getMock(); $connection->expects($this->once())->method('write')->with($this->stringContains("user!\0")); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn(\React\Promise\resolve($connection)); $factory = new Factory($loop, $connector); $promise = $factory->createConnection('mysql://user%21@127.0.0.1'); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); $connection->emit('data', ["\x33\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 44)]); } public function testConnectWillUseGivenPathAsDatabaseNameAfterUrldecoding() { $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(['write'])->getMock(); $connection->expects($this->once())->method('write')->with($this->stringContains("test database\0")); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->with('127.0.0.1:3306')->willReturn(\React\Promise\resolve($connection)); $factory = new Factory($loop, $connector); $promise = $factory->createConnection('mysql://127.0.0.1/test%20database'); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); $connection->emit('data', ["\x33\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 44)]); } public function testConnectWithInvalidHostRejectsWithConnectionError() { $factory = new Factory(); $uri = $this->getConnectionString(['host' => 'example.invalid']); $promise = $factory->createConnection($uri); $promise->then(null, $this->expectCallableOnce()); Loop::run(); } public function testConnectWithInvalidPassRejectsWithAuthenticationError() { $factory = new Factory(); $uri = $this->getConnectionString(['passwd' => 'invalidpass']); $promise = $factory->createConnection($uri); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { return !!preg_match("/^Connection to mysql:\/\/[^ ]* failed during authentication: Access denied for user '.*?'@'.*?' \(using password: YES\) \(EACCES\)$/", $e->getMessage()); }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13); }), $this->callback(function (\RuntimeException $e) { return !!preg_match("/^Access denied for user '.*?'@'.*?' \(using password: YES\)$/", $e->getPrevious()->getMessage()); }) ) )); Loop::run(); } public function testConnectWillRejectWhenServerClosesConnection() { $factory = new Factory(); $socket = new SocketServer('127.0.0.1:0', []); $socket->on('connection', function ($connection) use ($socket) { $socket->close(); $connection->close(); }); $parts = parse_url($socket->getAddress()); $uri = $this->getConnectionString(['host' => $parts['host'], 'port' => $parts['port']]); $promise = $factory->createConnection($uri); $uri = preg_replace('/:[^:]*@/', ':***@', $uri); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) use ($uri) { return $e->getMessage() === 'Connection to ' . $uri . ' failed during authentication: Connection closed by peer (ECONNRESET)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104); }) ) )); Loop::run(); } public function testConnectWillRejectOnExplicitTimeoutDespiteValidAuth() { $factory = new Factory(); $uri = 'mysql://' . $this->getConnectionString() . '?timeout=0'; $promise = $factory->createConnection($uri); $uri = preg_replace('/:[^:]*@/', ':***@', $uri); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) use ($uri) { return $e->getMessage() === 'Connection to ' . $uri . ' timed out after 0 seconds (ETIMEDOUT)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110); }) ) )); Loop::run(); } public function testConnectWillRejectOnDefaultTimeoutFromIniDespiteValidAuth() { $factory = new Factory(); $uri = $this->getConnectionString(); $old = ini_get('default_socket_timeout'); ini_set('default_socket_timeout', '0'); $promise = $factory->createConnection($uri); ini_set('default_socket_timeout', $old); $uri = preg_replace('/:[^:]*@/', ':***@', $uri); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) use ($uri) { return $e->getMessage() === 'Connection to ' . $uri . ' timed out after 0 seconds (ETIMEDOUT)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110); }) ) )); Loop::run(); } public function testConnectWithValidAuthWillRunUntilQuit() { $this->expectOutputString('connected.closed.'); $factory = new Factory(); $uri = $this->getConnectionString(); $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->quit()->then(function () { echo 'closed.'; }); }); Loop::run(); } public function testConnectWithValidAuthAndWithoutDbNameWillRunUntilQuit() { $this->expectOutputString('connected.closed.'); $factory = new Factory(); $uri = $this->getConnectionString(['dbname' => '']); $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->quit()->then(function () { echo 'closed.'; }); }); Loop::run(); } public function testConnectWithValidAuthWillIgnoreNegativeTimeoutAndRunUntilQuit() { $this->expectOutputString('connected.closed.'); $factory = new Factory(); $uri = $this->getConnectionString() . '?timeout=-1'; $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->quit()->then(function () { echo 'closed.'; }); }); Loop::run(); } public function testConnectWithValidAuthCanPingAndThenQuit() { $this->expectOutputString('connected.ping.closed.'); $factory = new Factory(); $uri = $this->getConnectionString(); $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->ping()->then(function () use ($connection) { echo 'ping.'; $connection->quit()->then(function () { echo 'closed.'; }); }); }); Loop::run(); } public function testConnectWithValidAuthCanQueuePingAndQuit() { $this->expectOutputString('connected.ping.closed.'); $factory = new Factory(); $uri = $this->getConnectionString(); $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->ping()->then(function () { echo 'ping.'; }); $connection->quit()->then(function () { echo 'closed.'; }); }); Loop::run(); } public function testConnectWithValidAuthQuitOnlyOnce() { $this->expectOutputString('connected.rejected.closed.'); $factory = new Factory(); $uri = $this->getConnectionString(); $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->quit()->then(function () { echo 'closed.'; }); $connection->quit()->then(function () { echo 'never reached.'; }, function () { echo 'rejected.'; }); }); Loop::run(); } public function testConnectWithValidAuthCanCloseOnlyOnce() { $this->expectOutputString('connected.closed.'); $factory = new Factory(); $uri = $this->getConnectionString(); $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->on('close', function () { echo 'closed.'; }); $connection->on('error', function () { echo 'error?'; }); $connection->close(); $connection->close(); }); Loop::run(); } public function testConnectWithValidAuthCanCloseAndAbortPing() { $this->expectOutputString('connected.aborted pending (Connection closing (ECONNABORTED)).aborted queued (Connection closing (ECONNABORTED)).closed.'); $factory = new Factory(); $uri = $this->getConnectionString(); $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->on('close', function () { echo 'closed.'; }); $connection->on('error', function () { echo 'error?'; }); $connection->ping()->then(null, function ($e) { echo 'aborted pending (' . $e->getMessage() .').'; }); $connection->ping()->then(null, function ($e) { echo 'aborted queued (' . $e->getMessage() . ').'; }); $connection->close(); }); Loop::run(); } public function testlConnectWillRejectWhenUnderlyingConnectorRejects() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(\React\Promise\reject(new \RuntimeException('Failed', 123))); $factory = new Factory($loop, $connector); $promise = $factory->createConnection('mysql://user:secret@127.0.0.1'); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection to mysql://user:***@127.0.0.1 failed: Failed'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === 123; }) ) )); } public function provideUris() { return [ [ 'mysql://localhost', 'mysql://localhost' ], [ 'mysql://user:pass@localhost', 'mysql://user:***@localhost' ], [ 'mysql://user:@localhost', 'mysql://user:***@localhost' ], [ 'mysql://user@localhost', 'mysql://user@localhost' ] ]; } /** * @dataProvider provideUris * @param string $uri * @param string $safe */ public function testCancelConnectWillCancelPendingConnection($uri, $safe) { $pending = new Promise(function () { }, $this->expectCallableOnce()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn($pending); $factory = new Factory($loop, $connector); $promise = $factory->createConnection($uri); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) use ($safe) { return $e->getMessage() === 'Connection to ' . $safe . ' cancelled (ECONNABORTED)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103); }) ) )); } public function testCancelConnectWillCancelPendingConnectionWithRuntimeException() { $pending = new Promise(function () { }, function () { throw new \UnexpectedValueException('ignored'); }); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn($pending); $factory = new Factory($loop, $connector); $promise = $factory->createConnection('mysql://127.0.0.1'); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection to mysql://127.0.0.1 cancelled (ECONNABORTED)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103); }) ) )); } public function testCancelConnectDuringAuthenticationWillCloseConnection() { $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('close'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $connector->expects($this->once())->method('connect')->willReturn(\React\Promise\resolve($connection)); $factory = new Factory($loop, $connector); $promise = $factory->createConnection('mysql://127.0.0.1'); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection to mysql://127.0.0.1 cancelled (ECONNABORTED)'; }), $this->callback(function (\RuntimeException $e) { return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103); }) ) )); } } ================================================ FILE: tests/Io/ParserTest.php ================================================ start(); $command = new QueryCommand(); $command->on('error', $this->expectCallableOnce()); $error = null; $command->on('error', function ($e) use (&$error) { $error = $e; }); // hack to inject command as current command $ref = new \ReflectionProperty($parser, 'currCommand'); $ref->setAccessible(true); $ref->setValue($parser, $command); $stream->close(); $this->assertInstanceOf('RuntimeException', $error); assert($error instanceof \RuntimeException); $this->assertEquals('Connection closing (ECONNABORTED)', $error->getMessage()); $this->assertEquals(defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103, $error->getCode()); } public function testParseValidAuthPluginWillSendAuthResponse() { $stream = new ThroughStream(); $outgoing = new ThroughStream(); $outgoing->on('data', $this->expectCallableOnceWith("\x08\0\0\x01" . "response")); $command = $this->getMockBuilder('React\Mysql\Commands\AuthenticateCommand')->disableOriginalConstructor()->getMock(); $command->expects($this->once())->method('authenticatePacket')->with($this->anything(), 'caching_sha2_password')->willReturn('response'); $executor = new Executor(); $executor->enqueue($command); $parser = new Parser(new CompositeStream($stream, $outgoing), $executor); $parser->start(); $stream->write("\x49\0\0\0\x0a\x38\x2e\x34\x2e\x35\0\x5e\0\0\0\x08\x0c\x41\x44\x12\x5e\x69\x59\0\xff\xff\xff\x02\0\xff\xdf\x15\0\0\0\0\0\0\0\0\0\0\x3c\x2c\x5e\x54\x06\x04\x01\x61\x01\x20\x79\x1b\0\x63\x61\x63\x68\x69\x6e\x67\x5f\x73\x68\x61\x32\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\0"); $ref = new \ReflectionProperty($parser, 'authPlugin'); $ref->setAccessible(true); $this->assertEquals('caching_sha2_password', $ref->getValue($parser)); } public function testUnexpectedAuthPluginShouldEmitErrorOnAuthenticateCommandAndCloseStream() { $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); $command = new AuthenticateCommand('root', '', 'test', 'utf8mb4'); $command->on('error', $this->expectCallableOnceWith(new \UnexpectedValueException('Unknown authentication plugin "sha256_password" requested by server'))); $executor = new Executor(); $executor->enqueue($command); $parser = new Parser($stream, $executor); $parser->start(); $stream->write("\x43\0\0\0\x0a\x38\x2e\x34\x2e\x35\0\x5e\0\0\0\x08\x0c\x41\x44\x12\x5e\x69\x59\0\xff\xff\xff\x02\0\xff\xdf\x15\0\0\0\0\0\0\0\0\0\0\x3c\x2c\x5e\x54\x06\x04\x01\x61\x01\x20\x79\x1b\0\x73\x68\x61\x32\x35\x36\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\0"); } public function testParseAuthSwitchRequestWillSendAuthSwitchResponsePacket() { $stream = new ThroughStream(); $stream->on('close', $this->expectCallableNever()); $outgoing = new ThroughStream(); $outgoing->on('data', $this->expectCallableOnceWith("\x09\0\0\x01" . "encrypted")); $executor = new Executor(); $command = $this->getMockBuilder('React\Mysql\Commands\AuthenticateCommand')->disableOriginalConstructor()->getMock(); $command->expects($this->once())->method('authResponse')->with('scramble', 'caching_sha2_password')->willReturn('encrypted'); $parser = new Parser(new CompositeStream($stream, $outgoing), $executor); $parser->start(); $ref = new \ReflectionProperty($parser, 'phase'); $ref->setAccessible(true); $ref->setValue($parser, Parser::PHASE_AUTH_SENT); $ref = new \ReflectionProperty($parser, 'currCommand'); $ref->setAccessible(true); $ref->setValue($parser, $command); $stream->write("\x20\0\0\0" . "\xfe" . "caching_sha2_password" . "\0" . "scramble" . "\0"); } public function testParseAuthSwitchRequestWithUnexpectedAuthPluginWillEmitErrorAndCloseConnection() { $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); $outgoing = new ThroughStream(); $outgoing->on('data', $this->expectCallableNever()); $command = new AuthenticateCommand('root', '', 'test', 'utf8mb4'); $command->on('error', $this->expectCallableOnceWith(new \UnexpectedValueException('Unknown authentication plugin "sha256_password" requested by server'))); $executor = new Executor(); $parser = new Parser(new CompositeStream($stream, $outgoing), $executor); $parser->start(); $ref = new \ReflectionProperty($parser, 'phase'); $ref->setAccessible(true); $ref->setValue($parser, Parser::PHASE_AUTH_SENT); $ref = new \ReflectionProperty($parser, 'currCommand'); $ref->setAccessible(true); $ref->setValue($parser, $command); $stream->write("\x19\0\0\0" . "\xfe" . "sha256_password" . "\0" . "scramble" . "\0"); } public function testParseAuthMoreDataWithFastAuthSuccessWillPrintDebugLogAndWaitForOkPacketWithoutSendingPacket() { $stream = new ThroughStream(); $stream->on('close', $this->expectCallableNever()); $outgoing = new ThroughStream(); $outgoing->on('data', $this->expectCallableNever()); $executor = new Executor(); $parser = new Parser(new CompositeStream($stream, $outgoing), $executor); $parser->start(); $ref = new \ReflectionProperty($parser, 'debug'); $ref->setAccessible(true); $ref->setValue($parser, true); $ref = new \ReflectionProperty($parser, 'phase'); $ref->setAccessible(true); $ref->setValue($parser, Parser::PHASE_AUTH_SENT); $ref = new \ReflectionProperty($parser, 'authPlugin'); $ref->setAccessible(true); $ref->setValue($parser, 'caching_sha2_password'); $this->expectOutputRegex('/Fast auth success\n$/'); $stream->write("\x02\0\0\0" . "\x01\x03"); } public function testParseAuthMoreDataWithFastAuthFailureWillSendCertificateRequest() { $stream = new ThroughStream(); $stream->on('close', $this->expectCallableNever()); $outgoing = new ThroughStream(); $outgoing->on('data', $this->expectCallableOnceWith("\x01\0\0\x01" . "\x02")); $executor = new Executor(); $parser = new Parser(new CompositeStream($stream, $outgoing), $executor); $parser->start(); $ref = new \ReflectionProperty($parser, 'phase'); $ref->setAccessible(true); $ref->setValue($parser, Parser::PHASE_AUTH_SENT); $ref = new \ReflectionProperty($parser, 'authPlugin'); $ref->setAccessible(true); $ref->setValue($parser, 'caching_sha2_password'); $stream->write("\x02\0\0\0" . "\x01\x04"); } public function testParseAuthMoreDataWithCertificateWillSendEncryptedPassword() { $stream = new ThroughStream(); $stream->on('close', $this->expectCallableNever()); $outgoing = new ThroughStream(); $outgoing->on('data', $this->expectCallableOnceWith("\x09\0\0\x01" . "encrypted")); $command = $this->getMockBuilder('React\Mysql\Commands\AuthenticateCommand')->disableOriginalConstructor()->getMock(); $command->expects($this->once())->method('authSha256')->with('', '---')->willReturn('encrypted'); $executor = new Executor(); $parser = new Parser(new CompositeStream($stream, $outgoing), $executor); $parser->start(); $ref = new \ReflectionProperty($parser, 'phase'); $ref->setAccessible(true); $ref->setValue($parser, Parser::PHASE_AUTH_SENT); $ref = new \ReflectionProperty($parser, 'authPlugin'); $ref->setAccessible(true); $ref->setValue($parser, 'caching_sha2_password'); $ref = new \ReflectionProperty($parser, 'currCommand'); $ref->setAccessible(true); $ref->setValue($parser, $command); $stream->write("\x04\0\0\0" . "\x01---"); } public function testParseAuthMoreDataWithCertificateWillEmitErrorAndCloseConnectionWhenEncryptingPasswordThrows() { $stream = new ThroughStream(); $stream->on('close', $this->expectCallableOnce()); $outgoing = new ThroughStream(); $outgoing->on('data', $this->expectCallableNever()); $command = $this->getMockBuilder('React\Mysql\Commands\AuthenticateCommand')->disableOriginalConstructor()->getMock(); $command->expects($this->once())->method('authSha256')->with('', '---')->willThrowException(new \UnexpectedValueException('Error')); $command->expects($this->once())->method('emit')->with('error', [new \UnexpectedValueException('Error')]); $executor = new Executor(); $parser = new Parser(new CompositeStream($stream, $outgoing), $executor); $parser->start(); $ref = new \ReflectionProperty($parser, 'phase'); $ref->setAccessible(true); $ref->setValue($parser, Parser::PHASE_AUTH_SENT); $ref = new \ReflectionProperty($parser, 'authPlugin'); $ref->setAccessible(true); $ref->setValue($parser, 'caching_sha2_password'); $ref = new \ReflectionProperty($parser, 'currCommand'); $ref->setAccessible(true); $ref->setValue($parser, $command); $stream->write("\x04\0\0\0" . "\x01---"); } public function testUnexpectedErrorWithoutCurrentCommandWillBeIgnored() { $stream = new ThroughStream(); $executor = new Executor(); $parser = new Parser($stream, $executor); $parser->start(); $stream->on('close', $this->expectCallableNever()); $stream->write("\x33\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 44)); $stream->write("\x17\0\0\0" . "\xFF" . "\x10\x04" . "Too many connections"); } public function testReceivingErrorFrameDuringHandshakeShouldEmitErrorOnFollowingCommand() { $stream = new ThroughStream(); $command = new QueryCommand(); $command->on('error', $this->expectCallableOnce()); $error = null; $command->on('error', function ($e) use (&$error) { $error = $e; }); $executor = new Executor(); $executor->enqueue($command); $parser = new Parser($stream, $executor); $parser->start(); $stream->write("\x17\0\0\0" . "\xFF" . "\x10\x04" . "Too many connections"); $this->assertTrue($error instanceof Exception); $this->assertEquals(1040, $error->getCode()); $this->assertEquals('Too many connections', $error->getMessage()); } public function testReceivingErrorFrameForQueryShouldEmitError() { $stream = new ThroughStream(); $command = new QueryCommand(); $command->on('error', $this->expectCallableOnce()); $error = null; $command->on('error', function ($e) use (&$error) { $error = $e; }); $executor = new Executor(); $executor->enqueue($command); $parser = new Parser($stream, $executor); $parser->start(); $stream->on('close', $this->expectCallableNever()); $stream->write("\x33\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 44)); $stream->write("\x1E\0\0\1" . "\xFF" . "\x46\x04" . "#abcde" . "Unknown thread id: 42"); $this->assertTrue($error instanceof Exception); $this->assertEquals(1094, $error->getCode()); $this->assertEquals('Unknown thread id: 42', $error->getMessage()); } public function testReceivingErrorFrameForQueryAfterResultSetHeadersShouldEmitError() { $stream = new ThroughStream(); $command = new QueryCommand(); $command->on('error', $this->expectCallableOnce()); $error = null; $command->on('error', function ($e) use (&$error) { $error = $e; }); $executor = new Executor(); $executor->enqueue($command); $parser = new Parser(new CompositeStream($stream, new ThroughStream()), $executor); $parser->start(); $stream->on('close', $this->expectCallableNever()); $stream->write("\x33\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 44)); $stream->write("\x01\0\0\1" . "\x01"); $stream->write("\x1F\0\0\2" . "\x03" . "def" . "\0" . "\0" . "\0" . "\x09" . "sleep(10)" . "\0" . "\xC0" . "\x3F\0" . "\1\0\0\0" . "\3" . "\x81\0". "\0" . "\0\0"); $stream->write("\x05\0\0\3" . "\xFE" . "\0\0\2\0"); $stream->write("\x28\0\0\4" . "\xFF" . "\x25\x05" . "#abcde" . "Query execution was interrupted"); $this->assertTrue($error instanceof Exception); $this->assertEquals(1317, $error->getCode()); $this->assertEquals('Query execution was interrupted', $error->getMessage()); $ref = new \ReflectionProperty($parser, 'rsState'); $ref->setAccessible(true); $this->assertEquals(0, $ref->getValue($parser)); $ref = new \ReflectionProperty($parser, 'resultFields'); $ref->setAccessible(true); $this->assertEquals([], $ref->getValue($parser)); } public function testReceivingInvalidPacketWithMissingDataShouldEmitErrorAndCloseConnection() { $stream = new ThroughStream(); $command = new QueryCommand(); $command->on('error', $this->expectCallableOnce()); $error = null; $command->on('error', function ($e) use (&$error) { $error = $e; }); $executor = new Executor(); $executor->enqueue($command); $parser = new Parser(new CompositeStream($stream, new ThroughStream()), $executor); $parser->start(); // hack to inject command as current command $ref = new \ReflectionProperty($parser, 'currCommand'); $ref->setAccessible(true); $ref->setValue($parser, $command); $stream->on('close', $this->expectCallableOnce()); $stream->write("\x32\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 43)); $this->assertTrue($error instanceof \UnexpectedValueException); $this->assertEquals('Unexpected protocol error, received malformed packet: Not enough data in buffer', $error->getMessage()); $this->assertEquals(0, $error->getCode()); $this->assertInstanceOf('UnderflowException', $error->getPrevious()); } public function testReceivingInvalidPacketWithExcessiveDataShouldEmitErrorAndCloseConnection() { $stream = new ThroughStream(); $command = new QueryCommand(); $command->on('error', $this->expectCallableOnce()); $error = null; $command->on('error', function ($e) use (&$error) { $error = $e; }); $executor = new Executor(); $executor->enqueue($command); $parser = new Parser(new CompositeStream($stream, new ThroughStream()), $executor); $parser->start(); // hack to inject command as current command $ref = new \ReflectionProperty($parser, 'currCommand'); $ref->setAccessible(true); $ref->setValue($parser, $command); $stream->on('close', $this->expectCallableOnce()); $stream->write("\x34\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 45)); $this->assertTrue($error instanceof \UnexpectedValueException); $this->assertEquals('Unexpected protocol error, received malformed packet with 1 unknown byte(s)', $error->getMessage()); $this->assertEquals(0, $error->getCode()); $this->assertNull($error->getPrevious()); } public function testReceivingIncompleteErrorFrameDuringHandshakeShouldNotEmitError() { $stream = new ThroughStream(); $command = new QueryCommand(); $command->on('error', $this->expectCallableNever()); $executor = new Executor(); $executor->enqueue($command); $parser = new Parser($stream, $executor); $parser->start(); $stream->write("\xFF\0\0\0" . "\xFF" . "\x12\x34" . "Some incomplete error message..."); } } ================================================ FILE: tests/Io/QueryStreamTest.php ================================================ getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream = new QueryStream($command, $connection); $stream->on('data', $this->expectCallableOnceWith(['key' => 'value'])); $command->emit('result', [['key' => 'value']]); } public function testDataEventWillNotBeForwardedFromCommandResultAfterClosingStream() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream = new QueryStream($command, $connection); $stream->on('data', $this->expectCallableNever()); $stream->close(); $command->emit('result', [['key' => 'value']]); } public function testEndEventWillBeForwardedFromCommandResult() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream = new QueryStream($command, $connection); $stream->on('end', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); $command->emit('end'); } public function testSuccessEventWillBeForwardedFromCommandResultAsEndWithoutData() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream = new QueryStream($command, $connection); $stream->on('data', $this->expectCallableNever()); $stream->on('end', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); $command->emit('success'); } public function testErrorEventWillBeForwardedFromCommandResult() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream = new QueryStream($command, $connection); $stream->on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); $stream->on('close', $this->expectCallableOnce()); $command->emit('error', [new \RuntimeException()]); } public function testPauseForwardsToConnectionAfterResultStarted() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('pause'); $stream = new QueryStream($command, $connection); $command->emit('result', [[]]); $stream->pause(); } public function testPauseForwardsToConnectionWhenResultStarted() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('pause'); $stream = new QueryStream($command, $connection); $stream->pause(); $command->emit('result', [[]]); } public function testPauseDoesNotForwardToConnectionWhenResultIsNotStarted() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->never())->method('pause'); $stream = new QueryStream($command, $connection); $stream->pause(); } public function testPauseDoesNotForwardToConnectionAfterClosing() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->never())->method('pause'); $stream = new QueryStream($command, $connection); $stream->close(); $stream->pause(); } public function testResumeForwardsToConnectionAfterResultStarted() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('resume'); $stream = new QueryStream($command, $connection); $command->emit('result', [[]]); $stream->resume(); } public function testResumeDoesNotForwardToConnectionAfterClosing() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->never())->method('resume'); $stream = new QueryStream($command, $connection); $stream->close(); $stream->resume(); } public function testPipeReturnsDestStream() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream = new QueryStream($command, $connection); $dest = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); $ret = $stream->pipe($dest); $this->assertSame($dest, $ret); } public function testCloseTwiceEmitsCloseEventOnce() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream = new QueryStream($command, $connection); $stream->on('close', $this->expectCallableOnce()); $stream->close(); $stream->close(); } public function testCloseForwardsResumeToConnectionIfPreviouslyPaused() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->once())->method('resume'); $stream = new QueryStream($command, $connection); $command->emit('result', [[]]); $stream->pause(); $stream->close(); } public function testCloseDoesNotResumeConnectionIfNotPreviouslyPaused() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->never())->method('resume'); $stream = new QueryStream($command, $connection); $stream->close(); } public function testCloseDoesNotResumeConnectionIfPreviouslyPausedWhenResultIsNotActive() { $command = new QueryCommand(); $connection = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $connection->expects($this->never())->method('resume'); $stream = new QueryStream($command, $connection); $stream->pause(); $stream->close(); } } ================================================ FILE: tests/Io/QueryTest.php ================================================ setExpectedException('InvalidArgumentException', 'Query param must be of type string|int|float|bool|null, resource given'); new Query('SELECT ?', [tmpfile()]); } public function testBindParams() { $query = new Query('select * from test where id = ? and name = ?', [100, 'test']); $this->assertEquals("select * from test where id = 100 and name = 'test'", $query->getSql()); $query = new Query('select * from test where id in (?,?) and name = ?', [1, 2, 'test']); $this->assertEquals("select * from test where id in (1,2) and name = 'test'", $query->getSql()); /* $query = new Query('select * from test where id = :id and name = :name', [':id' => 100, ':name' => 'test']); $this->assertEquals("select * from test where id = 100 and name = 'test'", $query->getSql()); $query = new Query('select * from test where id = :id and name = ?', ['test', ':id' => 100]); $this->assertEquals("select * from test where id = 100 and name = 'test'", $query->getSql()); */ } public function testGetSqlReturnsQuestionMarkReplacedWhenBound() { $query = new Query('select ?', ['hello']); $this->assertEquals("select 'hello'", $query->getSql()); } public function testGetSqlReturnsQuestionMarkReplacedWithNullValueWhenBound() { $query = new Query('select ?', [null]); $this->assertEquals("select NULL", $query->getSql()); } public function testGetSqlReturnsQuestionMarkReplacedFromBoundWhenBound() { $query = new Query('select CONCAT(?, ?)', ['hello??', 'world??']); $this->assertEquals("select CONCAT('hello??', 'world??')", $query->getSql()); } public function testGetSqlReturnsQuestionMarksAsIsWhenNotBound() { $query = new Query('select "hello?"'); $this->assertEquals("select \"hello?\"", $query->getSql()); } public function testEscapeChars() { $query = new Query(''); $this->assertEquals('\\\\', $query->escape('\\')); $this->assertEquals("''", $query->escape("'")); $this->assertEquals("foo\0bar", $query->escape("foo" . chr(0) . "bar")); $this->assertEquals("n%3A", $query->escape("n%3A")); $this->assertEquals('§ä¨ì¥H¤U¤º®e\\\\§ä¨ì¥H¤U¤º®e', $query->escape('§ä¨ì¥H¤U¤º®e\\§ä¨ì¥H¤U¤º®e')); } } ================================================ FILE: tests/MysqlClientTest.php ================================================ setAccessible(true); $factory = $ref->getValue($mysql); $ref = new \ReflectionProperty($factory, 'connector'); $ref->setAccessible(true); $connector = $ref->getValue($factory); $this->assertInstanceOf('React\Socket\ConnectorInterface', $connector); $ref = new \ReflectionProperty($factory, 'loop'); $ref->setAccessible(true); $loop = $ref->getValue($factory); $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); } public function testConstructWithConnectorAndLoopAssignsGivenConnectorAndLoop() { $connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', $connector, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $factory = $ref->getValue($mysql); $ref = new \ReflectionProperty($factory, 'connector'); $ref->setAccessible(true); $this->assertSame($connector, $ref->getValue($factory)); $ref = new \ReflectionProperty($factory, 'loop'); $ref->setAccessible(true); $this->assertSame($loop, $ref->getValue($factory)); } public static function provideInvalidUris() { return [ [ '', 'mysql://' ], [ 'localhost:100000', 'mysql://localhost:100000' ], [ 'tcp://localhost', 'tcp://localhost' ], [ 'mysql://', 'mysql://' ], [ 'mysql+unix://', 'mysql+unix://' ], [ 'user@localhost:100000', 'mysql://user@localhost:100000' ], [ ':pass@localhost:100000', 'mysql://:***@localhost:100000' ], [ 'user:@localhost:100000', 'mysql://user:***@localhost:100000' ], [ 'user:pass@localhost:100000', 'mysql://user:***@localhost:100000' ], [ 'user@', 'mysql://user@' ], [ 'user:pass@', 'mysql://user:***@' ] ]; } /** @dataProvider provideInvalidUris */ public function testContructorThrowsExceptionForInvalidUri($uri, $message) { $this->setExpectedException( 'InvalidArgumentException', 'Invalid MySQL URI "' . $message . '" (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 ); new MysqlClient($uri); } public function testContructorThrowsExceptionForInvalidCharset() { $this->setExpectedException('InvalidArgumentException', 'Unsupported charset selected'); new MysqlClient('localhost?charset=unknown'); } public function testContructorThrowsExceptionForInvalidConnector() { $this->setExpectedException('InvalidArgumentException', 'Argument #2 ($connector) expected null|React\Socket\ConnectorInterface'); new MysqlClient('localhost', 'connector'); } public function testContructorThrowsExceptionForInvalidLoop() { $this->setExpectedException('InvalidArgumentException', 'Argument #3 ($loop) expected null|React\EventLoop\LoopInterface'); new MysqlClient('localhost', null, 'loop'); } public function testPingWillNotCloseConnectionWhenPendingConnectionFails() { $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableNever()); $promise = $connection->ping(); $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection $deferred->reject(new \RuntimeException()); } public function testConnectionCloseEventAfterPingWillNotEmitCloseEvent() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableNever()); $connection->ping(); assert($base instanceof Connection); $base->emit('close'); } public function testConnectionErrorEventAfterPingWillNotEmitErrorEvent() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableNever()); $connection->ping(); assert($base instanceof Connection); $base->emit('error', [new \RuntimeException()]); } public function testPingAfterConnectionIsInClosingStateDueToIdleTimerWillCloseConnectionBeforeCreatingSecondConnection() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->never())->method('quit'); $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\resolve($base), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->on('close', $this->expectCallableNever()); $connection->ping(); // emulate triggering idle timer by setting connection state to closing $base->state = Connection::STATE_CLOSING; $connection->ping(); } public function testQueryWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionIsPending() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(new Promise(function () { })); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise = $mysql->query('SELECT 1'); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionResolvesAndQueryOnConnectionIsPending() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('query')->with(new Query('SELECT 1'))->willReturn(new Promise(function () { })); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise = $mysql->query('SELECT 1'); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryWillReturnResolvedPromiseWhenQueryOnConnectionResolves() { $result = new MysqlResult(); $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('query')->with(new Query('SELECT 1'))->willReturn(\React\Promise\resolve($result)); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise = $mysql->query('SELECT 1'); $promise->then($this->expectCallableOnceWith($result)); } public function testQueryWillReturnRejectedPromiseWhenCreateConnectionRejects() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\reject(new \RuntimeException())); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise = $mysql->query('SELECT 1'); $promise->then(null, $this->expectCallableOnce()); } public function testQueryWillReturnRejectedPromiseWhenQueryOnConnectionRejectsAfterCreateConnectionResolves() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('query')->with(new Query('SELECT 1'))->willReturn(\React\Promise\reject(new \RuntimeException())); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise = $mysql->query('SELECT 1'); $promise->then(null, $this->expectCallableOnce()); } public function testQueryTwiceWillCreateSingleConnectionAndReturnPendingPromiseWhenCreateConnectionIsPending() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(new Promise(function () { })); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->query('SELECT 1'); $promise = $mysql->query('SELECT 2'); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryTwiceWillCallQueryOnConnectionOnlyOnceWhenQueryIsStillPending() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('query')->with(new Query('SELECT 1'))->willReturn(new Promise(function () { })); $connection->expects($this->once())->method('isBusy')->willReturn(true); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->query('SELECT 1'); $promise = $mysql->query('SELECT 2'); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryTwiceWillReuseConnectionForSecondQueryWhenFirstQueryIsAlreadyResolved() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->exactly(2))->method('query')->withConsecutive( [new Query('SELECT 1')], [new Query('SELECT 2')] )->willReturnOnConsecutiveCalls( \React\Promise\resolve(new MysqlResult()), new Promise(function () { }) ); $connection->expects($this->once())->method('isBusy')->willReturn(false); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->query('SELECT 1'); $promise = $mysql->query('SELECT 2'); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryTwiceWillCallSecondQueryOnConnectionAfterFirstQueryResolvesWhenBothQueriesAreGivenBeforeCreateConnectionResolves() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->exactly(2))->method('query')->withConsecutive( [new Query('SELECT 1')], [new Query('SELECT 2')] )->willReturnOnConsecutiveCalls( \React\Promise\resolve(new MysqlResult()), new Promise(function () { }) ); $connection->expects($this->never())->method('isBusy'); $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->query('SELECT 1'); $promise = $mysql->query('SELECT 2'); $deferred->resolve($connection); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryTwiceWillCreateNewConnectionForSecondQueryWhenFirstConnectionIsClosedAfterFirstQueryIsResolved() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['query', 'isBusy'])->getMock(); $connection->expects($this->once())->method('query')->with(new Query('SELECT 1'))->willReturn(\React\Promise\resolve(new MysqlResult())); $connection->expects($this->never())->method('isBusy'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\resolve($connection), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->query('SELECT 1'); assert($connection instanceof Connection); $connection->emit('close'); $promise = $mysql->query('SELECT 2'); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryTwiceWillCloseFirstConnectionAndCreateNewConnectionForSecondQueryWhenFirstConnectionIsInClosingStateDueToIdleTimerAfterFirstQueryIsResolved() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['query', 'isBusy', 'close'])->getMock(); $connection->expects($this->once())->method('query')->with(new Query('SELECT 1'))->willReturn(\React\Promise\resolve(new MysqlResult())); $connection->expects($this->once())->method('close'); $connection->expects($this->never())->method('isBusy'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\resolve($connection), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $mysql->on('close', $this->expectCallableNever()); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->query('SELECT 1'); // emulate triggering idle timer by setting connection state to closing $connection->state = Connection::STATE_CLOSING; $promise = $mysql->query('SELECT 2'); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryTwiceWillRejectFirstQueryWhenCreateConnectionRejectsAndWillCreateNewConnectionForSecondQuery() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\reject(new \RuntimeException()), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise1 = $mysql->query('SELECT 1'); $promise2 = $mysql->query('SELECT 2'); $promise1->then(null, $this->expectCallableOnce()); $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryTwiceWillRejectBothQueriesWhenBothQueriesAreGivenBeforeCreateConnectionRejects() { $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise1 = $mysql->query('SELECT 1'); $promise2 = $mysql->query('SELECT 2'); $deferred->reject(new \RuntimeException()); $promise1->then(null, $this->expectCallableOnce()); $promise2->then(null, $this->expectCallableOnce()); } public function testQueryTriceWillRejectFirstTwoQueriesAndKeepThirdPendingWhenTwoQueriesAreGivenBeforeCreateConnectionRejects() { $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise1 = $mysql->query('SELECT 1'); $promise2 = $mysql->query('SELECT 2'); $promise3 = $promise1->then(null, function () use ($mysql) { return $mysql->query('SELECT 3'); }); $deferred->reject(new \RuntimeException()); $promise1->then(null, $this->expectCallableOnce()); $promise2->then(null, $this->expectCallableOnce()); $promise3->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryTwiceWillCallSecondQueryOnConnectionAfterFirstQueryRejectsWhenBothQueriesAreGivenBeforeCreateConnectionResolves() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->exactly(2))->method('query')->withConsecutive( [new Query('SELECT 1')], [new Query('SELECT 2')] )->willReturnOnConsecutiveCalls( \React\Promise\reject(new \RuntimeException()), new Promise(function () { }) ); $connection->expects($this->never())->method('isBusy'); $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise1 = $mysql->query('SELECT 1'); $promise2 = $mysql->query('SELECT 2'); $deferred->resolve($connection); $promise1->then(null, $this->expectCallableOnce()); $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryStreamWillCreateNewConnectionAndReturnReadableStreamWhenConnectionIsPending() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(new Promise(function () { })); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $stream = $mysql->queryStream('SELECT 1'); $this->assertTrue($stream->isReadable()); } public function testQueryStreamWillCreateNewConnectionAndReturnReadableStreamWhenConnectionResolvesAndQueryStreamOnConnectionReturnsReadableStream() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('queryStream')->with(new Query('SELECT 1'))->willReturn(new ThroughStream()); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $stream = $mysql->queryStream('SELECT 1'); $this->assertTrue($stream->isReadable()); } public function testQueryStreamTwiceWillCallQueryStreamOnConnectionOnlyOnceWhenQueryStreamIsStillReadable() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('queryStream')->with(new Query('SELECT 1'))->willReturn(new ThroughStream()); $connection->expects($this->once())->method('isBusy')->willReturn(true); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->queryStream('SELECT 1'); $stream = $mysql->queryStream('SELECT 2'); $this->assertTrue($stream->isReadable()); } public function testQueryStreamTwiceWillReuseConnectionForSecondQueryStreamWhenFirstQueryStreamEnds() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->exactly(2))->method('queryStream')->withConsecutive( [new Query('SELECT 1')], [new Query('SELECT 2')] )->willReturnOnConsecutiveCalls( $base = new ThroughStream(), new ThroughStream() ); $connection->expects($this->once())->method('isBusy')->willReturn(false); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->queryStream('SELECT 1'); $base->end(); $stream = $mysql->queryStream('SELECT 2'); $this->assertTrue($stream->isReadable()); } public function testQueryStreamTwiceWillReuseConnectionForSecondQueryStreamWhenFirstQueryStreamEmitsError() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->exactly(2))->method('queryStream')->withConsecutive( [new Query('SELECT 1')], [new Query('SELECT 2')] )->willReturnOnConsecutiveCalls( $base = new ThroughStream(), new ThroughStream() ); $connection->expects($this->once())->method('isBusy')->willReturn(true); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $stream1 = $mysql->queryStream('SELECT 1'); $stream2 = $mysql->queryStream('SELECT 2'); $this->assertTrue($stream1->isReadable()); $this->assertTrue($stream2->isReadable()); $base->emit('error', [new \RuntimeException()]); $this->assertFalse($stream1->isReadable()); $this->assertTrue($stream2->isReadable()); } public function testQueryStreamTwiceWillWaitForFirstQueryStreamToEndBeforeStartingSecondQueryStreamWhenFirstQueryStreamIsExplicitlyClosed() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('queryStream')->with(new Query('SELECT 1'))->willReturn(new ThroughStream()); $connection->expects($this->once())->method('isBusy')->willReturn(true); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $stream1 = $mysql->queryStream('SELECT 1'); $stream2 = $mysql->queryStream('SELECT 2'); $this->assertTrue($stream1->isReadable()); $this->assertTrue($stream2->isReadable()); $stream1->close(); $this->assertFalse($stream1->isReadable()); $this->assertTrue($stream2->isReadable()); } public function testQueryStreamTwiceWillCallSecondQueryStreamOnConnectionAfterFirstQueryStreamIsClosedWhenBothQueriesAreGivenBeforeCreateConnectionResolves() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->exactly(2))->method('queryStream')->withConsecutive( [new Query('SELECT 1')], [new Query('SELECT 2')] )->willReturnOnConsecutiveCalls( $base = new ThroughStream(), new ThroughStream() ); $connection->expects($this->never())->method('isBusy'); $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->queryStream('SELECT 1'); $stream = $mysql->queryStream('SELECT 2'); $deferred->resolve($connection); $base->end(); $this->assertTrue($stream->isReadable()); } public function testQueryStreamTwiceWillCreateNewConnectionForSecondQueryStreamWhenFirstConnectionIsClosedAfterFirstQueryStreamIsClosed() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['queryStream', 'isBusy'])->getMock(); $connection->expects($this->once())->method('queryStream')->with(new Query('SELECT 1'))->willReturn($base = new ThroughStream()); $connection->expects($this->never())->method('isBusy'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\resolve($connection), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->queryStream('SELECT 1'); $base->end(); assert($connection instanceof Connection); $connection->emit('close'); $stream = $mysql->queryStream('SELECT 2'); $this->assertTrue($stream->isReadable()); } public function testQueryStreamTwiceWillCloseFirstConnectionAndCreateNewConnectionForSecondQueryStreamWhenFirstConnectionIsInClosingStateDueToIdleTimerAfterFirstQueryStreamIsClosed() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['queryStream', 'isBusy', 'close'])->getMock(); $connection->expects($this->once())->method('queryStream')->with(new Query('SELECT 1'))->willReturn($base = new ThroughStream()); $connection->expects($this->once())->method('close'); $connection->expects($this->never())->method('isBusy'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\resolve($connection), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $mysql->on('close', $this->expectCallableNever()); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->queryStream('SELECT 1'); $base->end(); // emulate triggering idle timer by setting connection state to closing $connection->state = Connection::STATE_CLOSING; $stream = $mysql->queryStream('SELECT 2'); $this->assertTrue($stream->isReadable()); } public function testQueryStreamTwiceWillEmitErrorOnFirstQueryStreamWhenCreateConnectionRejectsAndWillCreateNewConnectionForSecondQueryStream() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\reject(new \RuntimeException()), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $stream1 = $mysql->queryStream('SELECT 1'); $this->assertFalse($stream1->isReadable()); $stream2 = $mysql->queryStream('SELECT 2'); $this->assertTrue($stream2->isReadable()); } public function testQueryStreamTwiceWillEmitErrorOnBothQueriesWhenBothQueriesAreGivenBeforeCreateConnectionRejects() { $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $stream1 = $mysql->queryStream('SELECT 1'); $stream2 = $mysql->queryStream('SELECT 2'); $stream1->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); $stream1->on('close', $this->expectCallableOnce()); $stream2->on('error', $this->expectCallableOnceWith($this->isInstanceOf('Exception'))); $stream2->on('close', $this->expectCallableOnce()); $deferred->reject(new \RuntimeException()); $this->assertFalse($stream1->isReadable()); $this->assertFalse($stream2->isReadable()); } public function testPingWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionIsPending() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(new Promise(function () { })); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise = $mysql->ping(); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingWillCreateNewConnectionAndReturnPendingPromiseWhenConnectionResolvesAndPingOnConnectionIsPending() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('ping')->willReturn(new Promise(function () { })); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise = $mysql->ping(); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingWillReturnResolvedPromiseWhenPingOnConnectionResolves() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise = $mysql->ping(); $promise->then($this->expectCallableOnce()); } public function testPingWillReturnRejectedPromiseWhenCreateConnectionRejects() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\reject(new \RuntimeException())); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise = $mysql->ping(); $promise->then(null, $this->expectCallableOnce()); } public function testPingWillReturnRejectedPromiseWhenPingOnConnectionRejectsAfterCreateConnectionResolves() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('ping')->willReturn(\React\Promise\reject(new \RuntimeException())); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise = $mysql->ping(); $promise->then(null, $this->expectCallableOnce()); } public function testPingTwiceWillCreateSingleConnectionAndReturnPendingPromiseWhenCreateConnectionIsPending() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(new Promise(function () { })); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->ping(); $promise = $mysql->ping(); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingTwiceWillCallPingOnConnectionOnlyOnceWhenPingIsStillPending() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('ping')->willReturn(new Promise(function () { })); $connection->expects($this->once())->method('isBusy')->willReturn(true); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->ping(); $promise = $mysql->ping(); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingTwiceWillReuseConnectionForSecondPingWhenFirstPingIsAlreadyResolved() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->exactly(2))->method('ping')->willReturnOnConsecutiveCalls( \React\Promise\resolve(null), new Promise(function () { }) ); $connection->expects($this->once())->method('isBusy')->willReturn(false); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->ping(); $promise = $mysql->ping(); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingTwiceWillCallSecondPingOnConnectionAfterFirstPingResolvesWhenBothQueriesAreGivenBeforeCreateConnectionResolves() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->exactly(2))->method('ping')->willReturnOnConsecutiveCalls( \React\Promise\resolve(new MysqlResult()), new Promise(function () { }) ); $connection->expects($this->never())->method('isBusy'); $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->ping(); $promise = $mysql->ping(); $deferred->resolve($connection); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingTwiceWillCreateNewConnectionForSecondPingWhenFirstConnectionIsClosedAfterFirstPingIsResolved() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['ping', 'isBusy'])->getMock(); $connection->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $connection->expects($this->never())->method('isBusy'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\resolve($connection), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->ping(); assert($connection instanceof Connection); $connection->emit('close'); $promise = $mysql->ping(); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingTwiceWillCloseFirstConnectionAndCreateNewConnectionForSecondPingWhenFirstConnectionIsInClosingStateDueToIdleTimerAfterFirstPingIsResolved() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->setMethods(['ping', 'isBusy', 'close'])->getMock(); $connection->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $connection->expects($this->once())->method('close'); $connection->expects($this->never())->method('isBusy'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\resolve($connection), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $mysql->on('close', $this->expectCallableNever()); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->ping(); // emulate triggering idle timer by setting connection state to closing $connection->state = Connection::STATE_CLOSING; $promise = $mysql->ping(); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingTwiceWillRejectFirstPingWhenCreateConnectionRejectsAndWillCreateNewConnectionForSecondPing() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\reject(new \RuntimeException()), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise1 = $mysql->ping(); $promise2 = $mysql->ping(); $promise1->then(null, $this->expectCallableOnce()); $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingTwiceWillRejectBothQueriesWhenBothQueriesAreGivenBeforeCreateConnectionRejects() { $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise1 = $mysql->ping(); $promise2 = $mysql->ping(); $deferred->reject(new \RuntimeException()); $promise1->then(null, $this->expectCallableOnce()); $promise2->then(null, $this->expectCallableOnce()); } public function testPingTriceWillRejectFirstTwoQueriesAndKeepThirdPendingWhenTwoQueriesAreGivenBeforeCreateConnectionRejects() { $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) ); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise1 = $mysql->ping(); $promise2 = $mysql->ping(); $promise3 = $promise1->then(null, function () use ($mysql) { return $mysql->ping(); }); $deferred->reject(new \RuntimeException()); $promise1->then(null, $this->expectCallableOnce()); $promise2->then(null, $this->expectCallableOnce()); $promise3->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingTwiceWillCallSecondPingOnConnectionAfterFirstPingRejectsWhenBothQueriesAreGivenBeforeCreateConnectionResolves() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $connection->expects($this->exactly(2))->method('ping')->willReturnOnConsecutiveCalls( \React\Promise\reject(new \RuntimeException()), new Promise(function () { }) ); $connection->expects($this->never())->method('isBusy'); $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $promise1 = $mysql->ping(); $promise2 = $mysql->ping(); $deferred->resolve($connection); $promise1->then(null, $this->expectCallableOnce()); $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryWillResolveWhenQueryFromUnderlyingConnectionResolves() { $result = new MysqlResult(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with(new Query('SELECT 1'))->willReturn(\React\Promise\resolve($result)); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->query('SELECT 1'); $ret->then($this->expectCallableOnceWith($result), $this->expectCallableNever()); } public function testPingAfterQueryWillPassPingToConnectionWhenQueryResolves() { $result = new MysqlResult(); $deferred = new Deferred(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with(new Query('SELECT 1'))->willReturn($deferred->promise()); $base->expects($this->once())->method('ping')->willReturn(new Promise(function () { })); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->query('SELECT 1'); $connection->ping(); $deferred->resolve($result); $ret->then($this->expectCallableOnceWith($result), $this->expectCallableNever()); } public function testQueryWillRejectWhenQueryFromUnderlyingConnectionRejects() { $error = new \RuntimeException(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with(new Query('SELECT 1'))->willReturn(\React\Promise\reject($error)); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->query('SELECT 1'); $ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); } public function testQueryWillRejectWhenUnderlyingConnectionRejects() { $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->query('SELECT 1'); $ret->then($this->expectCallableNever(), $this->expectCallableOnce()); $deferred->reject(new \RuntimeException()); } public function testQueryStreamReturnsReadableStreamWhenConnectionIsPending() { $promise = new Promise(function () { }); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($promise); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->queryStream('SELECT 1'); $this->assertTrue($ret instanceof ReadableStreamInterface); $this->assertTrue($ret->isReadable()); } public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWhenResolved() { $stream = new ThroughStream(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('queryStream')->with(new Query('SELECT 1'))->willReturn($stream); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->queryStream('SELECT 1'); $ret->on('data', $this->expectCallableOnceWith('hello')); $stream->write('hello'); $this->assertTrue($ret->isReadable()); } public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWhenResolvedAndClosed() { $stream = new ThroughStream(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('queryStream')->with(new Query('SELECT 1'))->willReturn($stream); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->queryStream('SELECT 1'); $ret->on('data', $this->expectCallableOnceWith('hello')); $stream->write('hello'); $ret->on('close', $this->expectCallableOnce()); $stream->close(); $this->assertFalse($ret->isReadable()); } public function testQueryStreamWillCloseStreamWithErrorWhenUnderlyingConnectionRejects() { $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->queryStream('SELECT 1'); $ret->on('error', $this->expectCallableOnce()); $ret->on('close', $this->expectCallableOnce()); $deferred->reject(new \RuntimeException()); $this->assertFalse($ret->isReadable()); } public function testPingReturnsPendingPromiseWhenConnectionIsPending() { $promise = new Promise(function () { }); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($promise); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->ping(); $this->assertTrue($ret instanceof PromiseInterface); $ret->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testPingWillPingUnderlyingConnectionWhenResolved() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(new Promise(function () { })); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping(); } public function testPingTwiceWillBothRejectWithSameErrorWhenUnderlyingConnectionRejects() { $error = new \RuntimeException(); $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping()->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); $connection->ping()->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); $deferred->reject($error); } public function testPingWillTryToCreateNewUnderlyingConnectionAfterPreviousPingFailedToCreateUnderlyingConnection() { $error = new \RuntimeException(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturn(\React\Promise\reject($error)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping()->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); $connection->ping()->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); } public function testPingWillResolveWhenPingFromUnderlyingConnectionResolves() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->ping(); $ret->then($this->expectCallableOnce(), $this->expectCallableNever()); } public function testPingWillRejectWhenPingFromUnderlyingConnectionRejects() { $error = new \RuntimeException(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\reject($error)); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->ping(); $ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); } public function testPingWillRejectWhenPingFromUnderlyingConnectionEmitsCloseEventAndRejects() { $error = new \RuntimeException(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturnCallback(function () use ($base, $error) { $base->emit('close'); return \React\Promise\reject($error); }); $base->expects($this->never())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $ret = $connection->ping(); $ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); } public function testQuitResolvesAndEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); $ret = $connection->quit(); $this->assertTrue($ret instanceof PromiseInterface); $ret->then($this->expectCallableOnce(), $this->expectCallableNever()); } public function testQuitAfterPingReturnsPendingPromiseWhenConnectionIsPending() { $promise = new Promise(function () { }); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($promise); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping(); $ret = $connection->quit(); $this->assertTrue($ret instanceof PromiseInterface); $ret->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQuitAfterPingRejectsAndThenEmitsCloseWhenFactoryFailsToCreateUnderlyingConnection() { $deferred = new Deferred(); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping()->then(null, $this->expectCallableOnce()); $this->expectOutputString('reject.close.'); $connection->on('close', function () { echo 'close.'; }); $connection->quit()->then(null, function () { echo 'reject.'; }); $deferred->reject(new \RuntimeException()); } public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('quit')->willReturn(new Promise(function () { })); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping(); $connection->quit(); } public function testQuitAfterPingResolvesAndThenEmitsCloseWhenUnderlyingConnectionQuits() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $deferred = new Deferred(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('quit')->willReturn($deferred->promise()); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping(); $this->expectOutputString('quit.close.'); $connection->on('close', function () { echo 'close.'; }); $connection->quit()->then(function () { echo 'quit.'; }); $deferred->resolve(null); } public function testQuitAfterPingRejectsAndThenEmitsCloseWhenUnderlyingConnectionFailsToQuit() { $deferred = new Deferred(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('quit')->willReturn($deferred->promise()); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping(); $this->expectOutputString('reject.close.'); $connection->on('close', function () { echo 'close.'; }); $connection->quit()->then(null, function () { echo 'reject.'; }); $deferred->reject(new \RuntimeException()); } public function testPingAfterQuitWillNotPassPingCommandToConnection() { $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'quit', 'close', 'isBusy'])->disableOriginalConstructor()->getMock(); $connection->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $connection->expects($this->once())->method('quit')->willReturn(new Promise(function () { })); $connection->expects($this->never())->method('close'); $connection->expects($this->once())->method('isBusy')->willReturn(false); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($connection)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $mysql = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); $ref->setValue($mysql, $factory); $mysql->on('close', $this->expectCallableNever()); $mysql->ping(); $mysql->quit(); $mysql->ping()->then(null, $this->expectCallableOnce()); } public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); $connection->close(); } public function testCloseAfterPingCancelsPendingConnection() { $deferred = new Deferred($this->expectCallableOnce()); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping()->then(null, $this->expectCallableOnce()); $connection->close(); } public function testCloseTwiceAfterPingWillCloseUnderlyingConnectionWhenResolved() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping(); $connection->close(); $connection->close(); } public function testCloseAfterPingDoesNotEmitConnectionErrorFromAbortedConnection() { $promise = new Promise(function () { }, function () { throw new \RuntimeException(); }); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($promise); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); $promise = $connection->ping(); $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection $connection->close(); } public function testCloseAfterPingWillCloseUnderlyingConnection() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping()->then($this->expectCallableOnce(), $this->expectCallableNever()); $connection->close(); } public function testCloseAfterPingHasResolvedWillCloseUnderlyingConnectionWithoutTryingToCancelConnection() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping(); $connection->close(); } public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuitIsStillPending() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('quit')->willReturn(new Promise(function () { })); $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping(); $connection->quit(); $connection->close(); } public function testCloseAfterConnectionIsInClosingStateDueToIdleTimerWillCloseUnderlyingConnection() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->never())->method('quit'); $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->ping(); // emulate triggering idle timer by setting connection state to closing $base->state = Connection::STATE_CLOSING; $connection->close(); } public function testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnectionIsPending() { $promise = new Promise(function () { }); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($promise); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); $connection->ping()->then(null, $this->expectCallableOnce()); $connection->close(); $connection->close(); } public function testQueryReturnsRejectedPromiseAfterConnectionIsClosed() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->close(); $ret = $connection->query('SELECT 1'); $this->assertTrue($ret instanceof PromiseInterface); $ret->then($this->expectCallableNever(), $this->expectCallableOnce()); } public function testQueryThrowsForInvalidQueryParamsWithoutCreatingNewConnection() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $this->setExpectedException('InvalidArgumentException', 'Query param must be of type string|int|float|bool|null, array given'); $connection->query('SELECT ?', [[]]); } public function testQueryThrowsForInvalidQueryParamsWhenConnectionIsAlreadyClosed() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->close(); $this->setExpectedException('InvalidArgumentException', 'Query param must be of type string|int|float|bool|null, array given'); $connection->query('SELECT ?', [[]]); } public function testQueryStreamThrowsAfterConnectionIsClosed() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->close(); $this->setExpectedException('React\Mysql\Exception'); $connection->queryStream('SELECT 1'); } public function testQueryStreamThrowsForInvalidQueryParamsWithoutCreatingNewConnection() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $this->setExpectedException('InvalidArgumentException', 'Query param must be of type string|int|float|bool|null, stdClass given'); $connection->queryStream('SELECT ?', [new \stdClass()]); } public function testQueryStreamThrowsForInvalidQueryParamsWhenConnectionIsAlreadyClosed() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->close(); $this->setExpectedException('InvalidArgumentException', 'Query param must be of type string|int|float|bool|null, stdClass given'); $connection->queryStream('SELECT ?', [new \stdClass()]); } public function testPingReturnsRejectedPromiseAfterConnectionIsClosed() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->close(); $ret = $connection->ping(); $this->assertTrue($ret instanceof PromiseInterface); $ret->then($this->expectCallableNever(), $this->expectCallableOnce()); } public function testQuitReturnsRejectedPromiseAfterConnectionIsClosed() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $connection = new MysqlClient('localhost', null, $loop); $ref = new \ReflectionProperty($connection, 'factory'); $ref->setAccessible(true); $ref->setValue($connection, $factory); $connection->close(); $ret = $connection->quit(); $this->assertTrue($ret instanceof PromiseInterface); $ret->then($this->expectCallableNever(), $this->expectCallableOnce()); } } ================================================ FILE: tests/NoResultQueryTest.php ================================================ createConnection(Loop::get()); // re-create test "book" table $connection->query(new Query('DROP TABLE IF EXISTS book')); $connection->query(new Query($this->getDataTable())); $connection->quit(); Loop::run(); } public function testUpdateSimpleNonExistentReportsNoAffectedRows() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('update book set created=999 where id=999'))->then(function (MysqlResult $command) { $this->assertEquals(0, $command->affectedRows); }); $connection->quit(); Loop::run(); } public function testInsertSimpleReportsFirstInsertId() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query("insert into book (`name`) values ('foo')"))->then(function (MysqlResult $command) { $this->assertEquals(1, $command->affectedRows); $this->assertEquals(1, $command->insertId); }); $connection->quit(); Loop::run(); } public function testUpdateSimpleReportsAffectedRow() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query("insert into book (`name`) values ('foo')")); $connection->query(new Query('update book set created=999 where id=1'))->then(function (MysqlResult $command) { $this->assertEquals(1, $command->affectedRows); }); $connection->quit(); Loop::run(); } public function testCreateTableAgainWillAddWarning() { $connection = $this->createConnection(Loop::get()); $sql = ' CREATE TABLE IF NOT EXISTS `book` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `name` VARCHAR(255) NOT NULL, `isbn` VARCHAR(255) NULL, `author` VARCHAR(255) NULL, `created` INT(11) NULL, PRIMARY KEY (`id`) )'; $connection->query(new Query($sql))->then(function (MysqlResult $command) { // 3 warnings on MySQL 8+, 1 warning on legacy MySQL 5 $this->assertGreaterThanOrEqual(1, $command->warningCount); }); $connection->quit(); Loop::run(); } public function testPingMultipleWillBeExecutedInSameOrderTheyAreEnqueuedFromHandlers() { $this->expectOutputString('123'); $connection = $this->createConnection(Loop::get()); $connection->ping()->then(function () use ($connection) { echo '1'; $connection->ping()->then(function () use ($connection) { echo '3'; $connection->quit(); }); }); $connection->ping()->then(function () { echo '2'; }); Loop::run(); } public function testQuitWithAnyAuthWillQuitWithoutRunning() { $this->expectOutputString('closed.'); $uri = 'mysql://random:pass@host'; $connection = new MysqlClient($uri); $connection->quit()->then(function () { echo 'closed.'; }); } public function testPingWithValidAuthWillRunUntilQuitAfterPing() { $this->expectOutputString('closed.'); $uri = $this->getConnectionString(); $connection = new MysqlClient($uri); $connection->ping(); $connection->quit()->then(function () { echo 'closed.'; }); Loop::run(); } public function testPingAndQuitWillFulfillPingBeforeQuitBeforeCloseEvent() { $this->expectOutputString('ping.quit.close.'); $uri = $this->getConnectionString(); $connection = new MysqlClient($uri); $connection->on('close', function () { echo 'close.'; }); $connection->ping()->then(function () { echo 'ping.'; }); $connection->quit()->then(function () { echo 'quit.'; }); Loop::run(); } public function testPingWithValidAuthWillRunUntilIdleTimerAfterPingEvenWithoutQuit() { $uri = $this->getConnectionString(); $connection = new MysqlClient($uri); $connection->on('close', $this->expectCallableNever()); $connection->ping(); Loop::run(); } public function testPingWithInvalidAuthWillRejectPingButWillNotEmitErrorOrClose() { $uri = $this->getConnectionString(['passwd' => 'invalidpass']); $connection = new MysqlClient($uri); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableNever()); $connection->ping()->then(null, $this->expectCallableOnce()); Loop::run(); } public function testPingWithValidAuthWillPingBeforeQuitButNotAfter() { $this->expectOutputString('rejected.ping.closed.'); $uri = $this->getConnectionString(); $connection = new MysqlClient($uri); $connection->ping()->then(function () { echo 'ping.'; }); $connection->quit()->then(function () { echo 'closed.'; }); $connection->ping()->then(function () { echo 'never reached'; }, function () { echo 'rejected.'; }); Loop::run(); } } ================================================ FILE: tests/ResultQueryTest.php ================================================ createConnection(Loop::get()); $connection->query(new Query('select \'foo\''))->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('foo', reset($command->resultRows[0])); }); $connection->quit(); Loop::run(); } public function provideValuesThatWillBeReturnedAsIs() { return array_map(function ($e) { return [$e]; }, [ 'foo', 'hello?', 'FööBär', 'pile of 💩', 'Dave\'s Diner', 'Robert "Bobby"', "first\r\nsecond", 'C:\\\\Users\\', '<>&--\'";', "\0\1\2\3\4\5\6\7\10\xff", implode('', range("\x00", "\x2F")) . implode('', range("\x7f", "\xFF")), '', null ]); } /** * @dataProvider provideValuesThatWillBeReturnedAsIs */ public function testSelectStaticValueWillBeReturnedAsIs($value) { $connection = $this->createConnection(Loop::get()); $expected = $value; $connection->query(new Query('select ?', [$value]))->then(function (MysqlResult $command) use ($expected) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame($expected, reset($command->resultRows[0])); }); $connection->quit(); Loop::run(); } /** * @dataProvider provideValuesThatWillBeReturnedAsIs */ public function testSelectStaticValueWillBeReturnedAsIsWithNoBackslashEscapesSqlMode($value) { if ($value !== null && strpos($value, '\\') !== false) { // TODO: strings such as '%\\' work as-is when string contains percent?! $this->markTestIncomplete('Escaping backslash not supported when using NO_BACKSLASH_ESCAPES SQL mode'); } $connection = $this->createConnection(Loop::get()); $expected = $value; $connection->query(new Query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"')); $connection->query(new Query('select ?', [$value]))->then(function (MysqlResult $command) use ($expected) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame($expected, reset($command->resultRows[0])); }); $connection->quit(); Loop::run(); } public function provideValuesThatWillBeConvertedToString() { return [ [1, '1'], [1.5, '1.5'], [true, '1'], [false, '0'] ]; } /** * @dataProvider provideValuesThatWillBeConvertedToString */ public function testSelectStaticValueWillBeConvertedToString($value, $expected) { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select ?', [$value]))->then(function (MysqlResult $command) use ($expected) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame($expected, reset($command->resultRows[0])); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextWithQuestionMark() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select \'hello?\''))->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertEquals('hello?', reset($command->resultRows[0])); }); $connection->quit(); Loop::run(); } public function testSelectLongStaticTextHasTypeStringWithValidLength() { $connection = $this->createConnection(Loop::get()); $length = 40000; $value = str_repeat('.', $length); $connection->query(new Query('SELECT ?', [$value]))->then(function (MysqlResult $command) use ($length) { $this->assertCount(1, $command->resultFields); $this->assertEquals($length * 4, $command->resultFields[0]['length']); $this->assertSame(Constants::FIELD_TYPE_VAR_STRING, $command->resultFields[0]['type']); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextWithEmptyLabel() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select \'foo\' as ``'))->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('foo', reset($command->resultRows[0])); $this->assertSame('', key($command->resultRows[0])); $this->assertCount(1, $command->resultFields); $this->assertSame('', $command->resultFields[0]['name']); }); $connection->quit(); Loop::run(); } public function testSelectStaticNullHasTypeNull() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select null'))->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertNull(reset($command->resultRows[0])); $this->assertCount(1, $command->resultFields); $this->assertSame(Constants::FIELD_TYPE_NULL, $command->resultFields[0]['type']); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextTwoRows() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select "foo" UNION select "bar"'))->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('foo', reset($command->resultRows[0])); $this->assertSame('bar', reset($command->resultRows[1])); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextTwoRowsWithNullHasTypeString() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select "foo" UNION select null'))->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('foo', reset($command->resultRows[0])); $this->assertNull(reset($command->resultRows[1])); $this->assertCount(1, $command->resultFields); $this->assertSame(Constants::FIELD_TYPE_VAR_STRING, $command->resultFields[0]['type']); }); $connection->quit(); Loop::run(); } public function testSelectStaticIntegerTwoRowsWithNullHasTypeLongButReturnsIntAsString() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select 0 UNION select null'))->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('0', reset($command->resultRows[0])); $this->assertNull(reset($command->resultRows[1])); $this->assertCount(1, $command->resultFields); $this->assertSame(Constants::FIELD_TYPE_LONGLONG, $command->resultFields[0]['type']); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextTwoRowsWithIntegerHasTypeString() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select "foo" UNION select 1'))->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('foo', reset($command->resultRows[0])); $this->assertSame('1', reset($command->resultRows[1])); $this->assertCount(1, $command->resultFields); $this->assertSame(Constants::FIELD_TYPE_VAR_STRING, $command->resultFields[0]['type']); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextTwoRowsWithEmptyRow() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select "foo" UNION select ""'))->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('foo', reset($command->resultRows[0])); $this->assertSame('', reset($command->resultRows[1])); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextNoRows() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select "foo" LIMIT 0'))->then(function (MysqlResult $command) { $this->assertCount(0, $command->resultRows); $this->assertCount(1, $command->resultFields); $this->assertSame('foo', $command->resultFields[0]['name']); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextTwoColumns() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select "foo","bar"'))->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(2, $command->resultRows[0]); $this->assertSame('foo', reset($command->resultRows[0])); $this->assertSame('bar', next($command->resultRows[0])); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextTwoColumnsWithOneEmptyColumn() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select "foo",""'))->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(2, $command->resultRows[0]); $this->assertSame('foo', reset($command->resultRows[0])); $this->assertSame('', next($command->resultRows[0])); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextTwoColumnsWithBothEmpty() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select \'\' as `first`, \'\' as `second`'))->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(2, $command->resultRows[0]); $this->assertSame(['', ''], array_values($command->resultRows[0])); $this->assertCount(2, $command->resultFields); $this->assertSame(Constants::FIELD_TYPE_VAR_STRING, $command->resultFields[0]['type']); $this->assertSame(Constants::FIELD_TYPE_VAR_STRING, $command->resultFields[1]['type']); }); $connection->quit(); Loop::run(); } public function testSelectStaticTextTwoColumnsWithSameNameOverwritesValue() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select "foo" as `col`,"bar" as `col`'))->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('bar', reset($command->resultRows[0])); $this->assertCount(2, $command->resultFields); $this->assertSame('col', $command->resultFields[0]['name']); $this->assertSame('col', $command->resultFields[1]['name']); }); $connection->quit(); Loop::run(); } public function testSelectCharsetDefaultsToUtf8() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('SELECT @@character_set_client'))->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('utf8mb4', reset($command->resultRows[0])); }); $connection->quit(); Loop::run(); } public function testSelectWithExplicitCharsetReturnsCharset() { $uri = $this->getConnectionString() . '?charset=latin1'; $connection = new MysqlClient($uri); $connection->query('SELECT @@character_set_client')->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('latin1', reset($command->resultRows[0])); }); $connection->quit(); Loop::run(); } public function testSimpleSelect() { $connection = $this->createConnection(Loop::get()); // re-create test "book" table $connection->query(new Query('DROP TABLE IF EXISTS book')); $connection->query(new Query($this->getDataTable())); $connection->query(new Query("insert into book (`name`) values ('foo')")); $connection->query(new Query("insert into book (`name`) values ('bar')")); $connection->query(new Query('select * from book'))->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); }); $connection->quit(); Loop::run(); } /** * @depends testSimpleSelect */ public function testSimpleSelectFromMysqlClientWithoutDatabaseNameReturnsSameData() { $uri = $this->getConnectionString(['dbname' => '']); $connection = new MysqlClient($uri); $connection->query('select * from test.book')->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); }); $connection->quit(); Loop::run(); } public function testInvalidSelectShouldFail() { $connection = $this->createConnection(Loop::get()); $options = $this->getConnectionOptions(); $db = $options['dbname']; $connection->query(new Query('select * from invalid_table'))->then( $this->expectCallableNever(), function (\Exception $error) use ($db) { $this->assertEquals("Table '$db.invalid_table' doesn't exist", $error->getMessage()); } ); $connection->quit(); Loop::run(); } public function testInvalidMultiStatementsShouldFailToPreventSqlInjections() { $connection = $this->createConnection(Loop::get()); $connection->query(new Query('select 1;select 2;'))->then( $this->expectCallableNever(), function (\Exception $error) { if (method_exists($this, 'assertStringContainsString')) { // PHPUnit 9+ $this->assertStringContainsString("You have an error in your SQL syntax", $error->getMessage()); } else { // legacy PHPUnit < 9 $this->assertContains("You have an error in your SQL syntax", $error->getMessage()); } } ); $connection->quit(); Loop::run(); } public function testSelectAfterDelay() { $connection = $this->createConnection(Loop::get()); Loop::addTimer(0.1, function () use ($connection) { $connection->query(new Query('select 1+1'))->then(function (MysqlResult $command) { $this->assertEquals([['1+1' => 2]], $command->resultRows); }); $connection->quit(); }); $timeout = Loop::addTimer(1, function () { Loop::stop(); $this->fail('Test timeout'); }); $connection->on('close', function () use ($timeout) { Loop::cancelTimer($timeout); }); Loop::run(); } public function testQueryStreamStaticEmptyEmitsSingleRow() { $connection = $this->createConnection(Loop::get()); $stream = $connection->queryStream(new Query('SELECT 1')); $stream->on('data', $this->expectCallableOnceWith(['1' => '1'])); $stream->on('end', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); $connection->quit(); Loop::run(); } public function testQueryStreamBoundVariableEmitsSingleRow() { $connection = $this->createConnection(Loop::get()); $stream = $connection->queryStream(new Query('SELECT ? as value', ['test'])); $stream->on('data', $this->expectCallableOnceWith(['value' => 'test'])); $stream->on('end', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); $connection->quit(); Loop::run(); } public function testQueryStreamZeroRowsEmitsEndWithoutData() { $connection = $this->createConnection(Loop::get()); $stream = $connection->queryStream(new Query('SELECT 1 LIMIT 0')); $stream->on('data', $this->expectCallableNever()); $stream->on('end', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); $connection->quit(); Loop::run(); } public function testQueryStreamInvalidStatementEmitsError() { $connection = $this->createConnection(Loop::get()); $stream = $connection->queryStream(new Query('SELECT')); $stream->on('data', $this->expectCallableNever()); $stream->on('end', $this->expectCallableNever()); $stream->on('error', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); $connection->quit(); Loop::run(); } public function testQueryStreamDropStatementEmitsEndWithoutData() { $connection = $this->createConnection(Loop::get()); $stream = $connection->queryStream(new Query('DROP TABLE IF exists helloworldtest1')); $stream->on('data', $this->expectCallableNever()); $stream->on('end', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); $connection->quit(); Loop::run(); } public function testQueryStreamExplicitCloseEmitsCloseEventWithoutData() { $connection = $this->createConnection(Loop::get()); $stream = $connection->queryStream(new Query('SELECT 1')); $stream->on('data', $this->expectCallableNever()); $stream->on('end', $this->expectCallableNever()); $stream->on('close', $this->expectCallableOnce()); $stream->close(); $connection->quit(); Loop::run(); } public function testQueryStreamFromMysqlClientEmitsSingleRow() { $uri = $this->getConnectionString(); $connection = new MysqlClient($uri); $stream = $connection->queryStream('SELECT 1'); $stream->on('data', $this->expectCallableOnceWith([1 => '1'])); $stream->on('end', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); $connection->quit(); Loop::run(); } public function testQueryStreamFromMysqlClientWillErrorWhenConnectionIsClosed() { $uri = $this->getConnectionString(); $connection = new MysqlClient($uri); $stream = $connection->queryStream('SELECT 1'); $stream->on('data', $this->expectCallableNever()); $stream->on('error', $this->expectCallableOnce()); $stream->on('close', $this->expectCallableOnce()); $connection->close(); } } ================================================ FILE: tests/wait-for-mysql.sh ================================================ #!/bin/sh CONTAINER="mysql" USERNAME="test" PASSWORD="test" while ! docker exec $CONTAINER mysql --host=127.0.0.1 --port=3306 --user=$USERNAME --password=$PASSWORD -e "SELECT 1" >/dev/null 2>&1; do sleep 1 done