From d406c88dd8cc24e995fe1b6ff674a6c431300177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Dec 2021 11:16:12 +0100 Subject: [PATCH 01/32] Support PHP 8.1 --- .github/workflows/ci.yml | 1 + composer.json | 2 +- phpunit.xml.dist | 10 ++-------- phpunit.xml.legacy | 11 ++--------- src/Io/LazyConnection.php | 2 +- tests/ResultQueryTest.php | 2 +- 6 files changed, 8 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81387da..38ce90f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.1 - 8.0 - 7.4 - 7.3 diff --git a/composer.json b/composer.json index 9b8d694..f814098 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ "react/event-loop": "^1.2", "react/promise": "^2.7", "react/promise-stream": "^1.1", - "react/promise-timer": "^1.5", + "react/promise-timer": "^1.8", "react/socket": "^1.9" }, "require-dev": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3483838..eadec74 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,17 +2,11 @@ + convertDeprecationsToExceptions="true"> ./tests/ diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index d8dc954..cc2a130 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -2,16 +2,9 @@ + colors="true"> ./tests/ diff --git a/src/Io/LazyConnection.php b/src/Io/LazyConnection.php index 3246a93..3fc58cd 100644 --- a/src/Io/LazyConnection.php +++ b/src/Io/LazyConnection.php @@ -34,7 +34,7 @@ class LazyConnection extends EventEmitter implements ConnectionInterface public function __construct(Factory $factory, $uri, LoopInterface $loop) { $args = []; - \parse_str(\parse_url($uri, \PHP_URL_QUERY), $args); + \parse_str((string) \parse_url($uri, \PHP_URL_QUERY), $args); if (isset($args['idle'])) { $this->idlePeriod = (float)$args['idle']; } diff --git a/tests/ResultQueryTest.php b/tests/ResultQueryTest.php index 3e2953a..2569082 100644 --- a/tests/ResultQueryTest.php +++ b/tests/ResultQueryTest.php @@ -68,7 +68,7 @@ public function testSelectStaticValueWillBeReturnedAsIs($value) */ public function testSelectStaticValueWillBeReturnedAsIsWithNoBackslashEscapesSqlMode($value) { - if (strpos($value, '\\') !== false) { + 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'); } From 7b4428da4d625787a0ce1420b497dadcd70ff7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 14 Dec 2021 12:25:28 +0100 Subject: [PATCH 02/32] Prepare v0.5.6 release --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ README.md | 6 +++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b647644..8c45264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # Changelog +## 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. diff --git a/README.md b/README.md index a15fee6..5189a23 100644 --- a/README.md +++ b/README.md @@ -497,13 +497,13 @@ See also the [`close()`](#close) method. ## Install -The recommended way to install this library is [through Composer](https://getcomposer.org). +The recommended way to install this library is [through Composer](https://getcomposer.org/). [New to Composer?](https://getcomposer.org/doc/00-intro.md) This will install the latest supported version: ```bash -$ composer require react/mysql:^0.5.5 +$ composer require react/mysql:^0.5.6 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -516,7 +516,7 @@ It's *highly recommended to use the latest supported PHP version* for this proje ## Tests To run the test suite, you first need to clone this repo and then install all -dependencies [through Composer](https://getcomposer.org): +dependencies [through Composer](https://getcomposer.org/): ```bash $ composer install From e39dd3a3cf51aa29ead6a3e6b4693bf5f29a15bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 22 Jun 2022 09:17:25 +0200 Subject: [PATCH 03/32] Forward compatibility with upcoming Promise v3 --- .github/workflows/ci.yml | 3 +++ composer.json | 18 ++++++++++------ src/Io/Connection.php | 4 ++-- src/Io/LazyConnection.php | 2 +- tests/Io/LazyConnectionTest.php | 38 ++++++++++++++++----------------- tests/ResultQueryTest.php | 2 +- 6 files changed, 38 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38ce90f..09cd260 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,8 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - 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 mysql:5 - run: bash tests/wait-for-mysql.sh @@ -39,6 +41,7 @@ jobs: name: PHPUnit (HHVM) runs-on: ubuntu-18.04 continue-on-error: true + if: false # temporarily skipped until https://github.com/azjezz/setup-hhvm/issues/3 is addressed steps: - uses: actions/checkout@v2 - uses: azjezz/setup-hhvm@v1 diff --git a/composer.json b/composer.json index f814098..9491f1b 100644 --- a/composer.json +++ b/composer.json @@ -7,13 +7,13 @@ "php": ">=5.4.0", "evenement/evenement": "^3.0 || ^2.1 || ^1.1", "react/event-loop": "^1.2", - "react/promise": "^2.7", - "react/promise-stream": "^1.1", - "react/promise-timer": "^1.8", - "react/socket": "^1.9" + "react/promise": "^3@dev || ^2.7", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.9", + "react/socket": "dev-promise-3 as 1.12.0" }, "require-dev": { - "clue/block-react": "^1.2", + "clue/block-react": "^1.5", "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" }, "autoload": { @@ -25,5 +25,11 @@ "psr-4": { "React\\Tests\\MySQL\\": "tests" } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/WyriHaximus-labs/socket" + } + ] } diff --git a/src/Io/Connection.php b/src/Io/Connection.php index 20810d2..4b0f927 100644 --- a/src/Io/Connection.php +++ b/src/Io/Connection.php @@ -128,7 +128,7 @@ public function ping() $reject($reason); }) ->on('success', function () use ($resolve) { - $resolve(); + $resolve(null); }); }); } @@ -144,7 +144,7 @@ public function quit() $this->state = self::STATE_CLOSED; $this->emit('end', [$this]); $this->emit('close', [$this]); - $resolve(); + $resolve(null); }); $this->state = self::STATE_CLOSEING; }); diff --git a/src/Io/LazyConnection.php b/src/Io/LazyConnection.php index 3fc58cd..b7dcd84 100644 --- a/src/Io/LazyConnection.php +++ b/src/Io/LazyConnection.php @@ -181,7 +181,7 @@ public function quit() // not already connecting => no need to connect, simply close virtual connection if ($this->connecting === null) { $this->close(); - return \React\Promise\resolve(); + return \React\Promise\resolve(null); } return $this->connecting()->then(function (ConnectionInterface $connection) { diff --git a/tests/Io/LazyConnectionTest.php b/tests/Io/LazyConnectionTest.php index ff31592..fbc1ac7 100644 --- a/tests/Io/LazyConnectionTest.php +++ b/tests/Io/LazyConnectionTest.php @@ -31,7 +31,7 @@ public function testPingWillNotCloseConnectionWhenPendingConnectionFails() public function testPingWillNotCloseConnectionWhenUnderlyingConnectionCloses() { $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); @@ -48,7 +48,7 @@ public function testPingWillNotCloseConnectionWhenUnderlyingConnectionCloses() public function testPingWillCancelTimerWithoutClosingConnectionWhenUnderlyingConnectionCloses() { $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); @@ -69,7 +69,7 @@ public function testPingWillCancelTimerWithoutClosingConnectionWhenUnderlyingCon public function testPingWillNotForwardErrorFromUnderlyingConnection() { $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); @@ -87,8 +87,8 @@ public function testPingWillNotForwardErrorFromUnderlyingConnection() public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection() { $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); - $base->expects($this->once())->method('quit')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $base->expects($this->once())->method('quit')->willReturn(\React\Promise\resolve(null)); $base->expects($this->never())->method('close'); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); @@ -115,8 +115,8 @@ public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection() public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuitFails() { $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); - $base->expects($this->once())->method('quit')->willReturn(\React\Promise\reject()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $base->expects($this->once())->method('quit')->willReturn(\React\Promise\reject(new \RuntimeException())); $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); @@ -143,7 +143,7 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuit public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatingSecondConnection() { $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $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'); @@ -289,7 +289,7 @@ public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromU public function testQueryAfterPingWillCancelTimerAgainWhenPingFromUnderlyingConnectionResolved() { $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); @@ -480,7 +480,7 @@ public function testPingWillTryToCreateNewUnderlyingConnectionAfterPreviousPingF public function testPingWillResolveAndStartTimerWhenPingFromUnderlyingConnectionResolves() { $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); @@ -570,7 +570,7 @@ public function testQuitAfterPingReturnsPendingPromiseWhenConnectionIsPending() public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved() { $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $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\Factory')->disableOriginalConstructor()->getMock(); @@ -585,8 +585,8 @@ public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved() public function testQuitAfterPingResolvesAndEmitsCloseWhenUnderlyingConnectionQuits() { $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); - $base->expects($this->once())->method('quit')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); + $base->expects($this->once())->method('quit')->willReturn(\React\Promise\resolve(null)); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); @@ -606,7 +606,7 @@ public function testQuitAfterPingRejectsAndEmitsCloseWhenUnderlyingConnectionFai { $error = new \RuntimeException(); $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('quit')->willReturn(\React\Promise\reject($error)); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); @@ -651,7 +651,7 @@ public function testCloseAfterPingCancelsPendingConnection() public function testCloseTwiceAfterPingWillCloseUnderlyingConnectionWhenResolved() { $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); @@ -685,7 +685,7 @@ public function testCloseAfterPingDoesNotEmitConnectionErrorFromAbortedConnectio public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectionResolves() { $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); @@ -704,7 +704,7 @@ public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectio public function testCloseAfterPingHasResolvedWillCloseUnderlyingConnectionWithoutTryingToCancelConnection() { $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('close')->willReturnCallback(function () use ($base) { $base->emit('close'); }); @@ -723,7 +723,7 @@ public function testCloseAfterPingHasResolvedWillCloseUnderlyingConnectionWithou public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuitIsStillPending() { $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $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'); @@ -740,7 +740,7 @@ public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuit public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionWhenQuitIsStillPending() { $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); - $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve()); + $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'); diff --git a/tests/ResultQueryTest.php b/tests/ResultQueryTest.php index 2569082..80e6892 100644 --- a/tests/ResultQueryTest.php +++ b/tests/ResultQueryTest.php @@ -415,7 +415,7 @@ public function testSimpleSelectFromLazyConnectionWithoutDatabaseNameReturnsSame $connection->query('select * from test.book')->then(function (QueryResult $command) { $this->assertCount(2, $command->resultRows); - })->done(); + }); $connection->quit(); Loop::run(); From 98690d955cc53b83d651f306edd442fec4c4f943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 22 Jun 2022 11:57:44 +0200 Subject: [PATCH 04/32] Update to stable reactphp/socket v1.12.0 --- .github/workflows/ci.yml | 3 --- composer.json | 12 +++--------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09cd260..38ce90f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,6 @@ jobs: with: php-version: ${{ matrix.php }} coverage: xdebug - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - 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 mysql:5 - run: bash tests/wait-for-mysql.sh @@ -41,7 +39,6 @@ jobs: name: PHPUnit (HHVM) runs-on: ubuntu-18.04 continue-on-error: true - if: false # temporarily skipped until https://github.com/azjezz/setup-hhvm/issues/3 is addressed steps: - uses: actions/checkout@v2 - uses: azjezz/setup-hhvm@v1 diff --git a/composer.json b/composer.json index 9491f1b..f3a9485 100644 --- a/composer.json +++ b/composer.json @@ -7,10 +7,10 @@ "php": ">=5.4.0", "evenement/evenement": "^3.0 || ^2.1 || ^1.1", "react/event-loop": "^1.2", - "react/promise": "^3@dev || ^2.7", + "react/promise": "^3 || ^2.7", "react/promise-stream": "^1.4", "react/promise-timer": "^1.9", - "react/socket": "dev-promise-3 as 1.12.0" + "react/socket": "^1.12" }, "require-dev": { "clue/block-react": "^1.5", @@ -25,11 +25,5 @@ "psr-4": { "React\\Tests\\MySQL\\": "tests" } - }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/WyriHaximus-labs/socket" - } - ] + } } From 7ce30f4eb3399546efc9bc73beca86cba0ac0ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 18 Aug 2022 18:56:19 +0200 Subject: [PATCH 05/32] Fix parsing ERR after result set --- src/Io/Parser.php | 5 ++- tests/Io/ParserTest.php | 74 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/Io/Parser.php b/src/Io/Parser.php index 0085a84..05996d7 100644 --- a/src/Io/Parser.php +++ b/src/Io/Parser.php @@ -127,7 +127,7 @@ public function parse($data) $len = $this->buffer->length(); if ($len < $this->pctSize) { - $this->debug('Buffer not enouth, return'); + $this->debug('Waiting for complete packet with ' . $len . '/' . $this->pctSize . ' bytes'); return; } @@ -277,6 +277,9 @@ private function onResultRow($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) { diff --git a/tests/Io/ParserTest.php b/tests/Io/ParserTest.php index b96c9e9..118e291 100644 --- a/tests/Io/ParserTest.php +++ b/tests/Io/ParserTest.php @@ -3,11 +3,12 @@ namespace React\Tests\MySQL\Io; use React\MySQL\Commands\QueryCommand; +use React\MySQL\Exception; use React\MySQL\Io\Executor; use React\MySQL\Io\Parser; +use React\Stream\CompositeStream; use React\Stream\ThroughStream; use React\Tests\MySQL\BaseTestCase; -use React\MySQL\Exception; class ParserTest extends BaseTestCase { @@ -56,7 +57,7 @@ public function testUnexpectedErrorWithoutCurrentCommandWillBeIgnored() $stream->write("\x17\0\0\0" . "\xFF" . "\x10\x04" . "Too many connections"); } - public function testSendingErrorFrameDuringHandshakeShouldEmitErrorOnFollowingCommand() + public function testReceivingErrorFrameDuringHandshakeShouldEmitErrorOnFollowingCommand() { $stream = new ThroughStream(); @@ -81,7 +82,74 @@ public function testSendingErrorFrameDuringHandshakeShouldEmitErrorOnFollowingCo $this->assertEquals('Too many connections', $error->getMessage()); } - public function testSendingIncompleteErrorFrameDuringHandshakeShouldNotEmitError() + 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("\x1E\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 testReceivingIncompleteErrorFrameDuringHandshakeShouldNotEmitError() { $stream = new ThroughStream(); From cbcb6edb71b9dc623a2780c5cd8aff3d3068bb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 25 Aug 2022 18:24:47 +0200 Subject: [PATCH 06/32] Fix legacy HHVM build by downgrading Composer --- .github/workflows/ci.yml | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38ce90f..70a84a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,7 @@ jobs: - uses: azjezz/setup-hhvm@v1 with: version: lts-3.30 + - run: composer self-update --2.2 # downgrade Composer for HHVM - run: hhvm $(which composer) require phpunit/phpunit:^5 --dev --no-interaction - 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 diff --git a/README.md b/README.md index 5189a23..2f6bd26 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MySQL -[![CI status](https://github.com/friends-of-reactphp/mysql/workflows/CI/badge.svg)](https://github.com/friends-of-reactphp/mysql/actions) +[![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/). From 8a05b9e667ffaf0b48e8e8558d2bccf115bc82cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 21 Aug 2022 14:04:22 +0200 Subject: [PATCH 07/32] Make parser more robust by splitting on individual package boundaries --- src/Io/Buffer.php | 55 ++++++++++++------- src/Io/Parser.php | 114 +++++++++++++++++++++------------------- tests/Io/BufferTest.php | 42 +++++++++------ tests/Io/ParserTest.php | 2 +- 4 files changed, 124 insertions(+), 89 deletions(-) diff --git a/src/Io/Buffer.php b/src/Io/Buffer.php index e012128..d8d797c 100644 --- a/src/Io/Buffer.php +++ b/src/Io/Buffer.php @@ -61,6 +61,43 @@ public function read($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 * @@ -79,24 +116,6 @@ public function skip($len) $this->bufferPos += $len; } - /** - * Clears all consumed data from the buffer - * - * This class keeps consumed data in memory for performance reasons and only - * advances the internal buffer position until this method is called. - * - * @return void - */ - public function trim() - { - if (!isset($this->buffer[$this->bufferPos])) { - $this->buffer = ''; - } else { - $this->buffer = \substr($this->buffer, $this->bufferPos); - } - $this->bufferPos = 0; - } - /** * returns the buffer length measures in number of bytes * diff --git a/src/Io/Parser.php b/src/Io/Parser.php index 05996d7..94dbad4 100644 --- a/src/Io/Parser.php +++ b/src/Io/Parser.php @@ -131,16 +131,22 @@ public function parse($data) return; } + + $packet = $this->buffer->readBuffer($this->pctSize); $this->state = self::STATE_STANDBY; - //$this->stream->bufferSize = 4; + + 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 = $this->buffer->readInt1(); + $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 = $this->buffer->readInt2(); - $exception = new Exception($this->buffer->read($this->pctSize - $len + $this->buffer->length()), $code); + $code = $packet->readInt2(); + $exception = new Exception($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 @@ -155,32 +161,32 @@ public function parse($data) $this->debug(sprintf("Protocal Version: %d", $this->protocalVersion)); $options = &$this->connectOptions; - $options['serverVersion'] = $this->buffer->readStringNull(); - $options['threadId'] = $this->buffer->readInt4(); - $this->scramble = $this->buffer->read(8); // 1st part - $this->buffer->skip(1); // filler - $options['ServerCaps'] = $this->buffer->readInt2(); // 1st part - $options['serverLang'] = $this->buffer->readInt1(); - $options['serverStatus'] = $this->buffer->readInt2(); - $options['ServerCaps'] += $this->buffer->readInt2() << 16; // 2nd part - $this->buffer->skip(11); // plugin length, 6 + 4 filler - $this->scramble .= $this->buffer->read(12); // 2nd part - $this->buffer->skip(1); + $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->buffer->readStringNull(); // skip authentication plugin name + $packet->readStringNull(); // skip authentication plugin name } // init completed, continue with sending AuthenticateCommand $this->nextRequest(true); } else { - $fieldCount = $this->buffer->readInt1(); + $fieldCount = $packet->readInt1(); if ($fieldCount === 0xFF) { // error packet - $code = $this->buffer->readInt2(); - $this->buffer->skip(6); // skip SQL state - $exception = new Exception($this->buffer->read($this->pctSize - $len + $this->buffer->length()), $code); + $code = $packet->readInt2(); + $packet->skip(6); // skip SQL state + $exception = new Exception($packet->read($packet->length()), $code); $this->debug(sprintf("Error Packet:%d %s\n", $code, $exception->getMessage())); $this->onError($exception); @@ -193,19 +199,19 @@ public function parse($data) $this->phase = self::PHASE_HANDSHAKED; } - $this->affectedRows = $this->buffer->readIntLen(); - $this->insertId = $this->buffer->readIntLen(); - $this->serverStatus = $this->buffer->readInt2(); - $this->warningCount = $this->buffer->readInt2(); + $this->affectedRows = $packet->readIntLen(); + $this->insertId = $packet->readIntLen(); + $this->serverStatus = $packet->readInt2(); + $this->warningCount = $packet->readInt2(); - $this->message = $this->buffer->read($this->pctSize - $len + $this->buffer->length()); + $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) { // EOF Packet - $this->buffer->skip(4); // warn, status + $packet->skip(4); // warn, status if ($this->rsState === self::RS_STATE_ROW) { // finalize this result set (all rows completed) $this->debug('Result set done'); @@ -217,54 +223,52 @@ public function parse($data) $this->debug('Result set next part'); ++$this->rsState; } - } elseif ($fieldCount === 0x00 && $this->pctSize === 1) { - // Empty data packet during result set => row with only empty strings - $this->debug('Result set empty row data'); - - $row = []; - foreach ($this->resultFields as $field) { - $row[$field['name']] = ''; - } - $this->onResultRow($row); } else { // Data packet - $this->buffer->prepend($this->buffer->buildInt1($fieldCount)); + $packet->prepend($packet->buildInt1($fieldCount)); if ($this->rsState === self::RS_STATE_HEADER) { - $this->debug('Result set header packet'); - $this->buffer->readIntLen(); // extra + $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) { - $this->debug('Result set field packet'); $field = [ - 'catalog' => $this->buffer->readStringLen(), - 'db' => $this->buffer->readStringLen(), - 'table' => $this->buffer->readStringLen(), - 'org_table' => $this->buffer->readStringLen(), - 'name' => $this->buffer->readStringLen(), - 'org_name' => $this->buffer->readStringLen() + 'catalog' => $packet->readStringLen(), + 'db' => $packet->readStringLen(), + 'table' => $packet->readStringLen(), + 'org_table' => $packet->readStringLen(), + 'name' => $packet->readStringLen(), + 'org_name' => $packet->readStringLen() ]; - $this->buffer->skip(1); // 0xC0 - $field['charset'] = $this->buffer->readInt2(); - $field['length'] = $this->buffer->readInt4(); - $field['type'] = $this->buffer->readInt1(); - $field['flags'] = $this->buffer->readInt2(); - $field['decimals'] = $this->buffer->readInt1(); - $this->buffer->skip(2); // unused + $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) { - $this->debug('Result set row data'); $row = []; foreach ($this->resultFields as $field) { - $row[$field['name']] = $this->buffer->readStringLen(); + $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); } } } - $this->buffer->trim(); + // finished parsing packet, continue with next packet + assert($packet->length() === 0); goto packet; } diff --git a/tests/Io/BufferTest.php b/tests/Io/BufferTest.php index 8569e60..5d3ac7a 100644 --- a/tests/Io/BufferTest.php +++ b/tests/Io/BufferTest.php @@ -36,41 +36,53 @@ public function testReadAfterSkipOne() $this->assertSame('i', $buffer->read(1)); } - public function testSkipZeroThrows() + public function testReadBufferEmptyIsNoop() { $buffer = new Buffer(); - $buffer->append('hi'); + $new = $buffer->readBuffer(0); - $this->setExpectedException('LogicException'); - $buffer->skip(0); + $this->assertSame(0, $buffer->length()); + $this->assertSame(0, $new->length()); } - public function testSkipBeyondLimitThrows() + public function testReadBufferReturnsBufferWithOriginalLengthAndClearsOriginalBuffer() { $buffer = new Buffer(); + $buffer->append('foo'); - $buffer->append('hi'); + $new = $buffer->readBuffer($buffer->length()); - $this->setExpectedException('LogicException'); - $buffer->skip(3); + $this->assertSame(0, $buffer->length()); + $this->assertSame(3, $new->length()); } - public function testTrimEmptyIsNoop() + public function testReadBufferBeyondLimitThrows() { $buffer = new Buffer(); - $buffer->trim(); - $this->assertSame(0, $buffer->length()); + $this->setExpectedException('UnderflowException'); + $buffer->readBuffer(3); + } + + public function testSkipZeroThrows() + { + $buffer = new Buffer(); + + $buffer->append('hi'); + + $this->setExpectedException('LogicException'); + $buffer->skip(0); } - public function testTrimDoesNotChangeLength() + public function testSkipBeyondLimitThrows() { $buffer = new Buffer(); - $buffer->append('a'); - $buffer->trim(); - $this->assertSame(1, $buffer->length()); + $buffer->append('hi'); + + $this->setExpectedException('LogicException'); + $buffer->skip(3); } public function testParseInt1() diff --git a/tests/Io/ParserTest.php b/tests/Io/ParserTest.php index 118e291..4ee984c 100644 --- a/tests/Io/ParserTest.php +++ b/tests/Io/ParserTest.php @@ -132,7 +132,7 @@ public function testReceivingErrorFrameForQueryAfterResultSetHeadersShouldEmitEr $stream->write("\x33\0\0\0" . "\x0a" . "mysql\0" . str_repeat("\0", 44)); $stream->write("\x01\0\0\1" . "\x01"); - $stream->write("\x1E\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("\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"); From 3bf10acaa60e8ae9aa145a0e9c39a54b05537b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 22 Aug 2022 09:09:00 +0200 Subject: [PATCH 08/32] Handle parser errors by emitting error and closing connection --- src/Io/Buffer.php | 12 +++--- src/Io/Parser.php | 90 +++++++++++++++++++++++++++++------------ tests/Io/BufferTest.php | 8 ++-- tests/Io/ParserTest.php | 66 ++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 36 deletions(-) diff --git a/src/Io/Buffer.php b/src/Io/Buffer.php index d8d797c..4eac8c4 100644 --- a/src/Io/Buffer.php +++ b/src/Io/Buffer.php @@ -37,7 +37,7 @@ public function prepend($str) * * @param int $len length in bytes, must be positive or zero * @return string - * @throws \LogicException + * @throws \UnderflowException */ public function read($len) { @@ -53,7 +53,7 @@ public function read($len) // ensure buffer size contains $len bytes by checking target buffer position if ($len < 0 || !isset($this->buffer[$this->bufferPos + $len - 1])) { - throw new \LogicException('Not enough data in buffer to read ' . $len . ' bytes'); + throw new \UnderflowException('Not enough data in buffer to read ' . $len . ' bytes'); } $buffer = \substr($this->buffer, $this->bufferPos, $len); $this->bufferPos += $len; @@ -106,12 +106,12 @@ public function readBuffer($len) * * @param int $len length in bytes, must be positve and non-zero * @return void - * @throws \LogicException + * @throws \UnderflowException */ public function skip($len) { if ($len < 1 || !isset($this->buffer[$this->bufferPos + $len - 1])) { - throw new \LogicException('Not enough data in buffer'); + throw new \UnderflowException('Not enough data in buffer'); } $this->bufferPos += $len; } @@ -220,13 +220,13 @@ public function readStringLen() * Reads string until NULL character * * @return string - * @throws \LogicException + * @throws \UnderflowException */ public function readStringNull() { $pos = \strpos($this->buffer, "\0", $this->bufferPos); if ($pos === false) { - throw new \LogicException('Missing NULL character'); + throw new \UnderflowException('Missing NULL character'); } $ret = $this->read($pos - $this->bufferPos); diff --git a/src/Io/Parser.php b/src/Io/Parser.php index 94dbad4..7c07b4a 100644 --- a/src/Io/Parser.php +++ b/src/Io/Parser.php @@ -5,7 +5,7 @@ use React\MySQL\Commands\AuthenticateCommand; use React\MySQL\Commands\QueryCommand; use React\MySQL\Commands\QuitCommand; -use React\MySQL\Exception; +use React\MySQL\Exception as MysqlException; use React\Stream\DuplexStreamInterface; /** @@ -25,6 +25,13 @@ class Parser const STATE_STANDBY = 0; const STATE_BODY = 1; + /** + * The packet header always consists of 4 bytes, 3 bytes packet length + 1 byte sequence number + * + * @var integer + */ + const PACKET_SIZE_HEADER = 4; + /** * Keeps a reference to the command that is currently being processed. * @@ -63,7 +70,20 @@ class Parser protected $serverStatus; protected $rsState = 0; - protected $pctSize = 0; + + /** + * Packet size expected in number of bytes + * + * Depending on `self::$state`, the Parser excepts either a packet header + * (always 4 bytes) or the packet contents (n bytes determined by prior + * packet header). + * + * @var int + * @see self::$state + * @see self::PACKET_SIZE_HEADER + */ + private $pctSize = self::PACKET_SIZE_HEADER; + protected $resultFields = []; protected $insertId; @@ -97,7 +117,7 @@ public function __construct(DuplexStreamInterface $stream, Executor $executor) public function start() { - $this->stream->on('data', [$this, 'parse']); + $this->stream->on('data', [$this, 'handleData']); $this->stream->on('close', [$this, 'onClose']); } @@ -110,31 +130,53 @@ public function debug($message) } } - public function parse($data) + /** @var string $data */ + public function handleData($data) { $this->buffer->append($data); -packet: - if ($this->state === self::STATE_STANDBY) { - if ($this->buffer->length() < 4) { + + 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; } - $this->pctSize = $this->buffer->readInt3(); - //printf("packet size:%d\n", $this->pctSize); - $this->state = self::STATE_BODY; - $this->seq = $this->buffer->readInt1() + 1; - } + $packet = $this->buffer->readBuffer($this->pctSize); + $this->state = self::STATE_STANDBY; + $this->pctSize = self::PACKET_SIZE_HEADER; - $len = $this->buffer->length(); - if ($len < $this->pctSize) { - $this->debug('Waiting for complete packet with ' . $len . '/' . $this->pctSize . ' bytes'); + 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; + } - 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; + } } + } - $packet = $this->buffer->readBuffer($this->pctSize); - $this->state = self::STATE_STANDBY; - + /** @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 } @@ -146,7 +188,7 @@ public function parse($data) $this->phase = self::PHASE_AUTH_ERR; $code = $packet->readInt2(); - $exception = new Exception($packet->read($packet->length()), $code); + $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 @@ -186,7 +228,7 @@ public function parse($data) // error packet $code = $packet->readInt2(); $packet->skip(6); // skip SQL state - $exception = new Exception($packet->read($packet->length()), $code); + $exception = new MysqlException($packet->read($packet->length()), $code); $this->debug(sprintf("Error Packet:%d %s\n", $code, $exception->getMessage())); $this->onError($exception); @@ -266,10 +308,6 @@ public function parse($data) } } } - - // finished parsing packet, continue with next packet - assert($packet->length() === 0); - goto packet; } private function onResultRow($row) @@ -279,7 +317,7 @@ private function onResultRow($row) $command->emit('result', [$row]); } - private function onError(Exception $error) + private function onError(\Exception $error) { $this->rsState = self::RS_STATE_HEADER; $this->resultFields = []; diff --git a/tests/Io/BufferTest.php b/tests/Io/BufferTest.php index 5d3ac7a..81c9993 100644 --- a/tests/Io/BufferTest.php +++ b/tests/Io/BufferTest.php @@ -22,7 +22,7 @@ public function testReadBeyondLimitThrows() $buffer->append('hi'); - $this->setExpectedException('LogicException'); + $this->setExpectedException('UnderflowException'); $buffer->read(3); } @@ -71,7 +71,7 @@ public function testSkipZeroThrows() $buffer->append('hi'); - $this->setExpectedException('LogicException'); + $this->setExpectedException('UnderflowException'); $buffer->skip(0); } @@ -81,7 +81,7 @@ public function testSkipBeyondLimitThrows() $buffer->append('hi'); - $this->setExpectedException('LogicException'); + $this->setExpectedException('UnderflowException'); $buffer->skip(3); } @@ -214,7 +214,7 @@ public function testParseStringNullCharacterThrowsIfNullNotFound() $buffer = new Buffer(); $buffer->append("hello"); - $this->setExpectedException('LogicException'); + $this->setExpectedException('UnderflowException'); $buffer->readStringNull(); } } diff --git a/tests/Io/ParserTest.php b/tests/Io/ParserTest.php index 4ee984c..8d56e52 100644 --- a/tests/Io/ParserTest.php +++ b/tests/Io/ParserTest.php @@ -149,6 +149,72 @@ public function testReceivingErrorFrameForQueryAfterResultSetHeadersShouldEmitEr $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(); From a9a73c9be87a244b1ea67a07a28a72bf5c7101fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 29 Aug 2022 21:18:32 +0200 Subject: [PATCH 09/32] Test on PHP 8.2 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70a84a0..6aa1354 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.2 - 8.1 - 8.0 - 7.4 From 583e1e1bbdd6e7a1526a557b781a5987e425cb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 31 Aug 2022 13:51:42 +0200 Subject: [PATCH 10/32] Mark passwords and URIs as `#[\SensitiveParameter]` (PHP 8.2+) --- src/Commands/AuthenticateCommand.php | 9 +++++++-- src/Factory.php | 12 ++++++++---- src/Io/LazyConnection.php | 8 ++++++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Commands/AuthenticateCommand.php b/src/Commands/AuthenticateCommand.php index a7edfe8..9283c4c 100644 --- a/src/Commands/AuthenticateCommand.php +++ b/src/Commands/AuthenticateCommand.php @@ -51,8 +51,13 @@ class AuthenticateCommand extends AbstractCommand * @param string $charset * @throws \InvalidArgumentException for invalid/unknown charset name */ - public function __construct($user, $passwd, $dbname, $charset) - { + public function __construct( + $user, + #[\SensitiveParameter] + $passwd, + $dbname, + $charset + ) { if (!isset(self::$charsetMap[$charset])) { throw new \InvalidArgumentException('Unsupported charset selected'); } diff --git a/src/Factory.php b/src/Factory.php index 351ffb6..1ccee7e 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -156,8 +156,10 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn * @param string $uri * @return PromiseInterface Promise */ - public function createConnection($uri) - { + public function createConnection( + #[\SensitiveParameter] + $uri + ) { if (strpos($uri, '://') === false) { $uri = 'mysql://' . $uri; } @@ -374,8 +376,10 @@ public function createConnection($uri) * @param string $uri * @return ConnectionInterface */ - public function createLazyConnection($uri) - { + public function createLazyConnection( + #[\SensitiveParameter] + $uri + ) { return new LazyConnection($this, $uri, $this->loop); } } diff --git a/src/Io/LazyConnection.php b/src/Io/LazyConnection.php index 3fc58cd..b493456 100644 --- a/src/Io/LazyConnection.php +++ b/src/Io/LazyConnection.php @@ -31,8 +31,12 @@ class LazyConnection extends EventEmitter implements ConnectionInterface private $idleTimer; private $pending = 0; - public function __construct(Factory $factory, $uri, LoopInterface $loop) - { + public function __construct( + Factory $factory, + #[\SensitiveParameter] + $uri, + LoopInterface $loop + ) { $args = []; \parse_str((string) \parse_url($uri, \PHP_URL_QUERY), $args); if (isset($args['idle'])) { From c36b92f1ede95f49036b6ba1805736f80fa4d4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 15 Sep 2022 15:40:17 +0200 Subject: [PATCH 11/32] Prepare v0.5.7 release --- CHANGELOG.md | 17 +++++++++++++++++ README.md | 18 +++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c45264..7264bbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 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`). diff --git a/README.md b/README.md index 2f6bd26..a439aba 100644 --- a/README.md +++ b/README.md @@ -503,7 +503,7 @@ The recommended way to install this library is [through Composer](https://getcom This will install the latest supported version: ```bash -$ composer require react/mysql:^0.5.6 +composer require react/mysql:^0.5.7 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -519,7 +519,7 @@ 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 +composer install ``` The test suite contains a number of functional integration tests that send @@ -530,18 +530,18 @@ 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 +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 \ +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 ``` @@ -549,7 +549,7 @@ $ docker run -it --rm --net=host \ To run the test suite, go to the project root and run: ```bash -$ vendor/bin/phpunit +vendor/bin/phpunit ``` ## License From 65ba13f3ef2863b8e0136465eca9a4747821b0b3 Mon Sep 17 00:00:00 2001 From: Fabian Meyer Date: Sat, 1 Oct 2022 01:55:11 +0200 Subject: [PATCH 12/32] Use reactphp/async instead of clue/reactphp-block --- composer.json | 2 +- tests/BaseTestCase.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index f3a9485..009c34c 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "react/socket": "^1.12" }, "require-dev": { - "clue/block-react": "^1.5", + "react/async": "^4 || ^3 || ^2", "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" }, "autoload": { diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index cbf72eb..be67f93 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -37,7 +37,7 @@ protected function createConnection(LoopInterface $loop) $factory = new Factory($loop); $promise = $factory->createConnection($this->getConnectionString()); - return \Clue\React\Block\await($promise, $loop, 10.0); + return \React\Async\await(\React\Promise\Timer\timeout($promise, 10.0)); } protected function getDataTable() From 157fd9478dc9af233ee8d64c0a7be3584de45fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 16 Oct 2022 17:06:59 +0200 Subject: [PATCH 13/32] Hello `0.6.x` development branch --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a439aba..6b1c147 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,15 @@ Async MySQL database client for [ReactPHP](https://reactphp.org/). +> **Development version:** This branch contains the code for the upcoming 0.6 release. +> For the code of the current stable 0.5 release, check out the +> [`0.5.x` branch](https://github.com/friends-of-reactphp/mysql/tree/0.5.x). +> +> The upcoming 0.6 release will be the way forward for this package. +> However, we will still actively support 0.5.x 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. @@ -500,10 +509,11 @@ See also the [`close()`](#close) method. The recommended way to install this library is [through Composer](https://getcomposer.org/). [New to Composer?](https://getcomposer.org/doc/00-intro.md) -This will install the latest supported version: +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.5.7 +composer require react/mysql:^0.6@dev ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From a093d87df088d86b01a8e676239950cd31d39d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 18 Oct 2022 09:26:51 +0200 Subject: [PATCH 14/32] Change default charset encoding to `utf8mb4` for full UTF-8 support --- README.md | 18 ++++++++---------- src/Factory.php | 20 +++++++++----------- tests/ResultQueryTest.php | 2 +- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 6b1c147..6fe2343 100644 --- a/README.md +++ b/README.md @@ -175,11 +175,10 @@ authentication. You can explicitly pass a custom timeout value in seconds $factory->createConnection('localhost?timeout=0.5'); ``` -By default, the connection uses the `utf8` charset encoding. Note that -MySQL's `utf8` encoding (also known as `utf8mb3`) predates what is now -known as UTF-8 and for historical reasons doesn't support emojis and -other characters. If you want full UTF-8 support, you can pass the -charset encoding like this: +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'); @@ -291,11 +290,10 @@ timeout) like this: $factory->createLazyConnection('localhost?idle=0.1'); ``` -By default, the connection uses the `utf8` charset encoding. Note that -MySQL's `utf8` encoding (also known as `utf8mb3`) predates what is now -known as UTF-8 and for historical reasons doesn't support emojis and -other characters. If you want full UTF-8 support, you can pass the -charset encoding like this: +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->createLazyConnection('localhost?charset=utf8mb4'); diff --git a/src/Factory.php b/src/Factory.php index 1ccee7e..dea8c6d 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -143,11 +143,10 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn * $factory->createConnection('localhost?timeout=0.5'); * ``` * - * By default, the connection uses the `utf8` charset encoding. Note that - * MySQL's `utf8` encoding (also known as `utf8mb3`) predates what is now - * known as UTF-8 and for historical reasons doesn't support emojis and - * other characters. If you want full UTF-8 support, you can pass the - * charset encoding like this: + * 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'); @@ -183,7 +182,7 @@ public function createConnection( 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'] : 'utf8' + isset($args['charset']) ? $args['charset'] : 'utf8mb4' ); } catch (\InvalidArgumentException $e) { return \React\Promise\reject($e); @@ -363,11 +362,10 @@ public function createConnection( * $factory->createLazyConnection('localhost?idle=0.1'); * ``` * - * By default, the connection uses the `utf8` charset encoding. Note that - * MySQL's `utf8` encoding (also known as `utf8mb3`) predates what is now - * known as UTF-8 and for historical reasons doesn't support emojis and - * other characters. If you want full UTF-8 support, you can pass the - * charset encoding like this: + * 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->createLazyConnection('localhost?charset=utf8mb4'); diff --git a/tests/ResultQueryTest.php b/tests/ResultQueryTest.php index 80e6892..2375a52 100644 --- a/tests/ResultQueryTest.php +++ b/tests/ResultQueryTest.php @@ -361,7 +361,7 @@ public function testSelectCharsetDefaultsToUtf8() $connection->query('SELECT @@character_set_client')->then(function (QueryResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); - $this->assertSame('utf8', reset($command->resultRows[0])); + $this->assertSame('utf8mb4', reset($command->resultRows[0])); }); $connection->quit(); From 9404e9991e5f19217ce5027db5e03863bfc7adb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 28 Jan 2023 16:56:18 +0100 Subject: [PATCH 15/32] Update test suite and report failed assertions --- .github/workflows/ci.yml | 23 ++++++++++++----------- composer.json | 4 ++-- phpunit.xml.dist | 10 ++++++++-- phpunit.xml.legacy | 8 +++++++- src/Io/Connection.php | 2 +- src/Io/Parser.php | 4 ++-- src/Io/QueryStream.php | 2 -- 7 files changed, 32 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6aa1354..99f82ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: jobs: PHPUnit: name: PHPUnit (PHP ${{ matrix.php }}) - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 strategy: matrix: php: @@ -23,30 +23,31 @@ jobs: - 5.5 - 5.4 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: xdebug + 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 mysql:5 - run: bash tests/wait-for-mysql.sh - - run: MYSQL_USER=test MYSQL_PASSWORD=test vendor/bin/phpunit --coverage-text + - run: vendor/bin/phpunit --coverage-text if: ${{ matrix.php >= 7.3 }} - - run: MYSQL_USER=test MYSQL_PASSWORD=test vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy + - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy if: ${{ matrix.php < 7.3 }} PHPUnit-hhvm: name: PHPUnit (HHVM) - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 continue-on-error: true steps: - - uses: actions/checkout@v2 - - uses: azjezz/setup-hhvm@v1 + - uses: actions/checkout@v3 + - 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: - version: lts-3.30 - - run: composer self-update --2.2 # downgrade Composer for HHVM - - run: hhvm $(which composer) require phpunit/phpunit:^5 --dev --no-interaction + 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: MYSQL_USER=test MYSQL_PASSWORD=test hhvm vendor/bin/phpunit + - run: docker run -i --rm --workdir=/data -v "$(pwd):/data" --net=host hhvm/hhvm:3.30-lts-latest hhvm vendor/bin/phpunit diff --git a/composer.json b/composer.json index 009c34c..8ab2633 100644 --- a/composer.json +++ b/composer.json @@ -13,8 +13,8 @@ "react/socket": "^1.12" }, "require-dev": { - "react/async": "^4 || ^3 || ^2", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.35", + "react/async": "^4 || ^3 || ^2" }, "autoload": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index eadec74..23ebd3b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,8 @@ - + + + + + + + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index cc2a130..711b2cf 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -1,6 +1,6 @@ - + + + + + + + diff --git a/src/Io/Connection.php b/src/Io/Connection.php index 4b0f927..c04d565 100644 --- a/src/Io/Connection.php +++ b/src/Io/Connection.php @@ -81,7 +81,7 @@ public function query($sql, array $params = []) }); $command->on('end', function () use ($command, $deferred, &$rows) { $result = new QueryResult(); - $result->resultFields = $command->resultFields; + $result->resultFields = $command->fields; $result->resultRows = $rows; $result->warningCount = $command->warningCount; diff --git a/src/Io/Parser.php b/src/Io/Parser.php index 7c07b4a..2d6613e 100644 --- a/src/Io/Parser.php +++ b/src/Io/Parser.php @@ -337,7 +337,8 @@ protected function onResultDone() $command = $this->currCommand; $this->currCommand = null; - $command->resultFields = $this->resultFields; + assert($command instanceof QueryCommand); + $command->fields = $this->resultFields; $command->emit('end'); $this->rsState = self::RS_STATE_HEADER; @@ -353,7 +354,6 @@ protected function onSuccess() $command->affectedRows = $this->affectedRows; $command->insertId = $this->insertId; $command->warningCount = $this->warningCount; - $command->message = $this->message; } $command->emit('success'); } diff --git a/src/Io/QueryStream.php b/src/Io/QueryStream.php index 2940a44..1b95734 100644 --- a/src/Io/QueryStream.php +++ b/src/Io/QueryStream.php @@ -15,7 +15,6 @@ */ class QueryStream extends EventEmitter implements ReadableStreamInterface { - private $query; private $connection; private $started = false; private $closed = false; @@ -23,7 +22,6 @@ class QueryStream extends EventEmitter implements ReadableStreamInterface public function __construct(QueryCommand $command, ConnectionInterface $connection) { - $this->command = $command; $this->connection = $connection; // forward result set rows until result set end From 9c342f3ed127eca946f13856d4f4b467f6d5813c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 10 Jul 2023 11:02:59 +0200 Subject: [PATCH 16/32] Update close handler to avoid unhandled promise rejections --- composer.json | 2 +- src/Factory.php | 2 ++ src/Io/LazyConnection.php | 2 ++ tests/FactoryTest.php | 27 +++++++++++++++------------ tests/Io/LazyConnectionTest.php | 10 ++++++++-- tests/ResultQueryTest.php | 18 +++++++++++------- 6 files changed, 39 insertions(+), 22 deletions(-) diff --git a/composer.json b/composer.json index 8ab2633..afed294 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "evenement/evenement": "^3.0 || ^2.1 || ^1.1", "react/event-loop": "^1.2", "react/promise": "^3 || ^2.7", - "react/promise-stream": "^1.4", + "react/promise-stream": "^1.6", "react/promise-timer": "^1.9", "react/socket": "^1.12" }, diff --git a/src/Factory.php b/src/Factory.php index dea8c6d..f43f70b 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -202,6 +202,8 @@ public function createConnection( // 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(); }); diff --git a/src/Io/LazyConnection.php b/src/Io/LazyConnection.php index b6b3b04..49cb9f9 100644 --- a/src/Io/LazyConnection.php +++ b/src/Io/LazyConnection.php @@ -220,6 +220,8 @@ public function close() if ($this->connecting !== null) { $this->connecting->then(function (ConnectionInterface $connection) { $connection->close(); + }, function () { + // ignore to avoid reporting unhandled rejection }); if ($this->connecting !== null) { $this->connecting->cancel(); diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php index bee52ee..22235d7 100644 --- a/tests/FactoryTest.php +++ b/tests/FactoryTest.php @@ -279,7 +279,7 @@ public function testConnectWithValidAuthWillRunUntilQuit() $connection->quit()->then(function () { echo 'closed.'; }); - }, 'printf')->then(null, 'printf'); + }); Loop::run(); } @@ -296,7 +296,7 @@ public function testConnectWithValidAuthAndWithoutDbNameWillRunUntilQuit() $connection->quit()->then(function () { echo 'closed.'; }); - }, 'printf')->then(null, 'printf'); + }); Loop::run(); } @@ -313,7 +313,7 @@ public function testConnectWithValidAuthWillIgnoreNegativeTimeoutAndRunUntilQuit $connection->quit()->then(function () { echo 'closed.'; }); - }, 'printf')->then(null, 'printf'); + }); Loop::run(); } @@ -333,8 +333,7 @@ public function testConnectWithValidAuthCanPingAndThenQuit() echo 'closed.'; }); }); - - }, 'printf')->then(null, 'printf'); + }); Loop::run(); } @@ -354,14 +353,14 @@ public function testConnectWithValidAuthCanQueuePingAndQuit() $connection->quit()->then(function () { echo 'closed.'; }); - }, 'printf')->then(null, 'printf'); + }); Loop::run(); } public function testConnectWithValidAuthQuitOnlyOnce() { - $this->expectOutputString('connected.closed.'); + $this->expectOutputString('connected.rejected.closed.'); $factory = new Factory(); @@ -372,9 +371,11 @@ public function testConnectWithValidAuthQuitOnlyOnce() echo 'closed.'; }); $connection->quit()->then(function () { - echo 'closed.'; + echo 'never reached.'; + }, function () { + echo 'rejected.'; }); - }, 'printf')->then(null, 'printf'); + }); Loop::run(); } @@ -397,7 +398,7 @@ public function testConnectWithValidAuthCanCloseOnlyOnce() $connection->close(); $connection->close(); - }, 'printf')->then(null, 'printf'); + }); Loop::run(); } @@ -425,7 +426,7 @@ public function testConnectWithValidAuthCanCloseAndAbortPing() echo 'aborted queued (' . $e->getMessage() . ').'; }); $connection->close(); - }, 'printf')->then(null, 'printf'); + }); Loop::run(); } @@ -626,7 +627,7 @@ public function testConnectLazyWithInvalidAuthWillRejectPingButWillNotEmitErrorO public function testConnectLazyWithValidAuthWillPingBeforeQuitButNotAfter() { - $this->expectOutputString('ping.closed.'); + $this->expectOutputString('rejected.ping.closed.'); $factory = new Factory(); @@ -643,6 +644,8 @@ public function testConnectLazyWithValidAuthWillPingBeforeQuitButNotAfter() $connection->ping()->then(function () { echo 'never reached'; + }, function () { + echo 'rejected.'; }); Loop::run(); diff --git a/tests/Io/LazyConnectionTest.php b/tests/Io/LazyConnectionTest.php index fbc1ac7..fc06ea9 100644 --- a/tests/Io/LazyConnectionTest.php +++ b/tests/Io/LazyConnectionTest.php @@ -24,10 +24,13 @@ public function testPingWillNotCloseConnectionWhenPendingConnectionFails() $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableNever()); - $connection->ping(); + $promise = $connection->ping(); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection $deferred->reject(new \RuntimeException()); } + public function testPingWillNotCloseConnectionWhenUnderlyingConnectionCloses() { $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); @@ -678,7 +681,10 @@ public function testCloseAfterPingDoesNotEmitConnectionErrorFromAbortedConnectio $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); - $connection->ping(); + $promise = $connection->ping(); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $connection->close(); } diff --git a/tests/ResultQueryTest.php b/tests/ResultQueryTest.php index 2375a52..f0f4eb4 100644 --- a/tests/ResultQueryTest.php +++ b/tests/ResultQueryTest.php @@ -17,8 +17,6 @@ public function testSelectStaticText() $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame('foo', reset($command->resultRows[0])); - - $this->assertInstanceOf('React\MySQL\Connection', $conn); }); $connection->quit(); @@ -57,7 +55,7 @@ public function testSelectStaticValueWillBeReturnedAsIs($value) $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame($expected, reset($command->resultRows[0])); - })->then(null, 'printf'); + }); $connection->quit(); Loop::run(); @@ -82,7 +80,7 @@ public function testSelectStaticValueWillBeReturnedAsIsWithNoBackslashEscapesSql $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertSame($expected, reset($command->resultRows[0])); - })->then(null, 'printf'); + }); $connection->quit(); Loop::run(); @@ -138,7 +136,7 @@ public function testSelectLongStaticTextHasTypeStringWithValidLength() $connection->query('SELECT ?', [$value])->then(function (QueryResult $command) use ($length) { $this->assertCount(1, $command->resultFields); - $this->assertEquals($length * 3, $command->resultFields[0]['length']); + $this->assertEquals($length * 4, $command->resultFields[0]['length']); $this->assertSame(Constants::FIELD_TYPE_VAR_STRING, $command->resultFields[0]['type']); }); @@ -430,7 +428,7 @@ public function testInvalidSelectShouldFail() $connection->query('select * from invalid_table')->then( $this->expectCallableNever(), - function (\Exception $error) { + function (\Exception $error) use ($db) { $this->assertEquals("Table '$db.invalid_table' doesn't exist", $error->getMessage()); } ); @@ -446,7 +444,13 @@ public function testInvalidMultiStatementsShouldFailToPreventSqlInjections() $connection->query('select 1;select 2;')->then( $this->expectCallableNever(), function (\Exception $error) { - $this->assertContains("You have an error in your SQL syntax", $error->getMessage()); + 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()); + } } ); From 74b9d3df6e15c0fe9354d199b0c96f5d558252ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 3 Nov 2023 12:46:50 +0100 Subject: [PATCH 17/32] Run tests on PHP 8.3 and update test suite --- .github/workflows/ci.yml | 5 +++-- composer.json | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99f82ba..c169b47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.3 - 8.2 - 8.1 - 8.0 @@ -23,7 +24,7 @@ jobs: - 5.5 - 5.4 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -42,7 +43,7 @@ jobs: runs-on: ubuntu-22.04 continue-on-error: true steps: - - uses: actions/checkout@v3 + - 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 diff --git a/composer.json b/composer.json index afed294..7f00983 100644 --- a/composer.json +++ b/composer.json @@ -13,17 +13,17 @@ "react/socket": "^1.12" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.35", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", "react/async": "^4 || ^3 || ^2" }, "autoload": { "psr-4": { - "React\\MySQL\\": "src" + "React\\MySQL\\": "src/" } }, "autoload-dev": { "psr-4": { - "React\\Tests\\MySQL\\": "tests" + "React\\Tests\\MySQL\\": "tests/" } } } From 3c2ecb56c47bd177f7f9f4153b763637bbddba52 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Mon, 6 Nov 2023 09:02:03 +0100 Subject: [PATCH 18/32] Improve CI workflow to await database --- tests/wait-for-mysql.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wait-for-mysql.sh b/tests/wait-for-mysql.sh index 00a39e3..262a818 100644 --- a/tests/wait-for-mysql.sh +++ b/tests/wait-for-mysql.sh @@ -3,6 +3,6 @@ CONTAINER="mysql" USERNAME="test" PASSWORD="test" -while ! docker exec $CONTAINER mysql --user=$USERNAME --password=$PASSWORD -e "SELECT 1" >/dev/null 2>&1; do +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 From b30a49b92395e1b2f612fdbf156d39c3e728915d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 6 Nov 2023 17:54:06 +0100 Subject: [PATCH 19/32] Reduce default idle time to 1ms --- README.md | 32 ++++++++++++++++++-------------- examples/01-query.php | 2 -- examples/02-query-stream.php | 2 -- src/Factory.php | 26 ++++++++++++++------------ src/Io/LazyConnection.php | 2 +- tests/FactoryTest.php | 2 +- tests/Io/LazyConnectionTest.php | 2 +- 7 files changed, 35 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 6fe2343..396ec96 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ It is written in pure PHP and does not require any extensions. This example runs a simple `SELECT` query and dumps all the records from a `book` table: ```php +createLazyConnection('user:pass@localhost/bookstore'); @@ -54,8 +58,6 @@ $connection->query('SELECT * FROM book')->then( echo 'Error: ' . $error->getMessage() . PHP_EOL; } ); - -$connection->quit(); ``` See also the [examples](examples). @@ -202,9 +204,11 @@ This method immediately returns a "virtual" connection implementing the 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. +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 @@ -277,17 +281,17 @@ in seconds (or use a negative number to not apply a timeout) like this: $factory->createLazyConnection('localhost?timeout=0.5'); ``` -By default, this method will keep "idle" connection open for 60s and will -then end the underlying connection. The next request after an "idle" -connection ended will automatically create a new underlying connection. -This ensure 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: +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 -$factory->createLazyConnection('localhost?idle=0.1'); +$factory->createLazyConnection('localhost?idle=10.0'); ``` By default, the connection provides full UTF-8 support (using the diff --git a/examples/01-query.php b/examples/01-query.php index 269b066..776e1f5 100644 --- a/examples/01-query.php +++ b/examples/01-query.php @@ -29,5 +29,3 @@ // the query was not executed successfully echo 'Error: ' . $error->getMessage() . PHP_EOL; }); - -$connection->quit(); diff --git a/examples/02-query-stream.php b/examples/02-query-stream.php index 1bc3744..c4e69b7 100644 --- a/examples/02-query-stream.php +++ b/examples/02-query-stream.php @@ -24,5 +24,3 @@ $stream->on('close', function () { echo 'CLOSED' . PHP_EOL; }); - -$connection->quit(); diff --git a/src/Factory.php b/src/Factory.php index f43f70b..7fb89d0 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -276,9 +276,11 @@ public function createConnection( * 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. + * 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 @@ -351,17 +353,17 @@ public function createConnection( * $factory->createLazyConnection('localhost?timeout=0.5'); * ``` * - * By default, this method will keep "idle" connection open for 60s and will - * then end the underlying connection. The next request after an "idle" - * connection ended will automatically create a new underlying connection. - * This ensure 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: + * 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 - * $factory->createLazyConnection('localhost?idle=0.1'); + * $factory->createLazyConnection('localhost?idle=10.0'); * ``` * * By default, the connection provides full UTF-8 support (using the diff --git a/src/Io/LazyConnection.php b/src/Io/LazyConnection.php index 49cb9f9..d825dbd 100644 --- a/src/Io/LazyConnection.php +++ b/src/Io/LazyConnection.php @@ -27,7 +27,7 @@ class LazyConnection extends EventEmitter implements ConnectionInterface private $disconnecting; private $loop; - private $idlePeriod = 60.0; + private $idlePeriod = 0.001; private $idleTimer; private $pending = 0; diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php index 22235d7..2e73bed 100644 --- a/tests/FactoryTest.php +++ b/tests/FactoryTest.php @@ -602,7 +602,7 @@ public function testConnectLazyWithValidAuthWillRunUntilIdleTimerAfterPingEvenWi { $factory = new Factory(); - $uri = $this->getConnectionString() . '?idle=0'; + $uri = $this->getConnectionString(); $connection = $factory->createLazyConnection($uri); $connection->ping(); diff --git a/tests/Io/LazyConnectionTest.php b/tests/Io/LazyConnectionTest.php index fc06ea9..89e75e8 100644 --- a/tests/Io/LazyConnectionTest.php +++ b/tests/Io/LazyConnectionTest.php @@ -218,7 +218,7 @@ public function testQueryWillResolveAndStartTimerWithDefaultIntervalWhenQueryFro $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with(60.0, $this->anything()); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); $connection = new LazyConnection($factory, '', $loop); From cb5c9b4773ac88dfc8944add9955ae06936097c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 8 Nov 2023 12:17:55 +0100 Subject: [PATCH 20/32] Use Promise v3 template types --- README.md | 8 ++++---- src/ConnectionInterface.php | 9 ++++++--- src/Factory.php | 3 ++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 396ec96..80d27a4 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ $factory = new React\MySQL\Factory(null, $connector); #### createConnection() -The `createConnection(string $url): PromiseInterface` method can be used to +The `createConnection(string $url): PromiseInterface` method can be used to create a new [`ConnectionInterface`](#connectioninterface). It helps with establishing a TCP/IP connection to your MySQL database @@ -311,7 +311,7 @@ and sending your database queries. #### query() -The `query(string $query, array $params = []): PromiseInterface` method can be used to +The `query(string $query, array $params = []): PromiseInterface` method can be used to perform an async query. This method returns a promise that will resolve with a `QueryResult` on @@ -424,7 +424,7 @@ suited for exposing multiple possible results. #### ping() -The `ping(): PromiseInterface` method can be used to +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 @@ -443,7 +443,7 @@ $connection->ping()->then(function () { #### quit() -The `quit(): PromiseInterface` method can be used to +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 diff --git a/src/ConnectionInterface.php b/src/ConnectionInterface.php index c07ac22..db94b47 100644 --- a/src/ConnectionInterface.php +++ b/src/ConnectionInterface.php @@ -98,7 +98,8 @@ interface ConnectionInterface extends EventEmitterInterface * * @param string $sql SQL statement * @param array $params Parameters which should be bound to query - * @return PromiseInterface Returns a Promise + * @return PromiseInterface + * Resolves with a `QueryResult` on success or rejects with an `Exception` on error. */ public function query($sql, array $params = []); @@ -180,7 +181,8 @@ public function queryStream($sql, $params = []); * }); * ``` * - * @return PromiseInterface Returns a Promise + * @return PromiseInterface + * Resolves with a `void` value on success or rejects with an `Exception` on error. */ public function ping(); @@ -198,7 +200,8 @@ public function ping(); * $connection->quit(); * ``` * - * @return PromiseInterface Returns a Promise + * @return PromiseInterface + * Resolves with a `void` value on success or rejects with an `Exception` on error. */ public function quit(); diff --git a/src/Factory.php b/src/Factory.php index 7fb89d0..9d55800 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -153,7 +153,8 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn * ``` * * @param string $uri - * @return PromiseInterface Promise + * @return PromiseInterface + * Resolves with a `ConnectionInterface` on success or rejects with an `Exception` on error. */ public function createConnection( #[\SensitiveParameter] From 69629c9faba7a655676c7027ca8c391cfaac4be9 Mon Sep 17 00:00:00 2001 From: Yada Clintjens Date: Wed, 8 Nov 2023 15:38:33 +0100 Subject: [PATCH 21/32] Fix typos in documentation --- CHANGELOG.md | 6 +++--- README.md | 6 +++--- examples/12-slow-stream.php | 2 +- src/ConnectionInterface.php | 2 +- src/Factory.php | 4 ++-- src/Io/Buffer.php | 2 +- src/Io/Connection.php | 6 +++--- src/Io/Parser.php | 6 +++--- src/Io/Query.php | 6 +++--- tests/ResultQueryTest.php | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7264bbb..123bfcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -130,7 +130,7 @@ using the new lazy connections as detailed below. 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 oustanding commands and will ensure that all + 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 @@ -176,7 +176,7 @@ have to take care of when updating from an older version. $connection = new Connection($loop, $options); $connection->connect(function (?Exception $error, $connection) { if ($error) { - // an error occured while trying to connect or authorize client + // an error occurred while trying to connect or authorize client } else { // client connection established (and authenticated) } @@ -189,7 +189,7 @@ have to take care of when updating from an older version. // client connection established (and authenticated) }, function (Exception $e) { - // an error occured while trying to connect or authorize client + // an error occurred while trying to connect or authorize client } ); ``` diff --git a/README.md b/README.md index 396ec96..1b9fc71 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ $factory->createConnection($url)->then( // client connection established (and authenticated) }, function (Exception $e) { - // an error occured while trying to connect or authorize client + // an error occurred while trying to connect or authorize client } ); ``` @@ -213,7 +213,7 @@ 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 oustanding commands and will ensure that all +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 @@ -463,7 +463,7 @@ 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 oustanding commands. +connection and reject all outstanding commands. ```php $connection->close(); diff --git a/examples/12-slow-stream.php b/examples/12-slow-stream.php index 8d5faa4..bb1af49 100644 --- a/examples/12-slow-stream.php +++ b/examples/12-slow-stream.php @@ -18,7 +18,7 @@ $factory->createConnection($uri)->then(function (ConnectionInterface $connection) use ($query) { // The protocol parser reads rather large chunked from the underlying connection // and as such can yield multiple (dozens to hundreds) rows from a single data - // chunk. We try to artifically limit the stream chunk size here to try to + // 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 diff --git a/src/ConnectionInterface.php b/src/ConnectionInterface.php index c07ac22..b7f4d58 100644 --- a/src/ConnectionInterface.php +++ b/src/ConnectionInterface.php @@ -206,7 +206,7 @@ public function quit(); * Force-close the connection. * * Unlike the `quit()` method, this method will immediately force-close the - * connection and reject all oustanding commands. + * connection and reject all outstanding commands. * * ```php * $connection->close(); diff --git a/src/Factory.php b/src/Factory.php index 7fb89d0..bba61f0 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -77,7 +77,7 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn * // client connection established (and authenticated) * }, * function (Exception $e) { - * // an error occured while trying to connect or authorize client + * // an error occurred while trying to connect or authorize client * } * ); * ``` @@ -285,7 +285,7 @@ public function createConnection( * 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 oustanding commands and will ensure that all + * 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 diff --git a/src/Io/Buffer.php b/src/Io/Buffer.php index 4eac8c4..36bd87e 100644 --- a/src/Io/Buffer.php +++ b/src/Io/Buffer.php @@ -104,7 +104,7 @@ public function readBuffer($len) * 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 positve and non-zero + * @param int $len length in bytes, must be positive and non-zero * @return void * @throws \UnderflowException */ diff --git a/src/Io/Connection.php b/src/Io/Connection.php index c04d565..73313ba 100644 --- a/src/Io/Connection.php +++ b/src/Io/Connection.php @@ -21,7 +21,7 @@ class Connection extends EventEmitter implements ConnectionInterface { const STATE_AUTHENTICATED = 5; - const STATE_CLOSEING = 6; + const STATE_CLOSING = 6; const STATE_CLOSED = 7; /** @@ -146,7 +146,7 @@ public function quit() $this->emit('close', [$this]); $resolve(null); }); - $this->state = self::STATE_CLOSEING; + $this->state = self::STATE_CLOSING; }); } @@ -199,7 +199,7 @@ public function handleConnectionError($err) */ public function handleConnectionClosed() { - if ($this->state < self::STATE_CLOSEING) { + if ($this->state < self::STATE_CLOSING) { $this->emit('error', [new \RuntimeException( 'Connection closed by peer (ECONNRESET)', \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104 diff --git a/src/Io/Parser.php b/src/Io/Parser.php index 2d6613e..0e8643f 100644 --- a/src/Io/Parser.php +++ b/src/Io/Parser.php @@ -89,7 +89,7 @@ class Parser protected $insertId; protected $affectedRows; - public $protocalVersion = 0; + public $protocolVersion = 0; private $buffer; @@ -199,8 +199,8 @@ private function parsePacket(Buffer $packet) } $this->phase = self::PHASE_GOT_INIT; - $this->protocalVersion = $response; - $this->debug(sprintf("Protocal Version: %d", $this->protocalVersion)); + $this->protocolVersion = $response; + $this->debug(sprintf("Protocol Version: %d", $this->protocolVersion)); $options = &$this->connectOptions; $options['serverVersion'] = $packet->readStringNull(); diff --git a/src/Io/Query.php b/src/Io/Query.php index 71ab90d..76a4687 100644 --- a/src/Io/Query.php +++ b/src/Io/Query.php @@ -47,7 +47,7 @@ public function __construct($sql) } /** - * Binding params for the query, mutiple arguments support. + * Binding params for the query, multiple arguments support. * * @param mixed $param * @return self @@ -69,7 +69,7 @@ public function bindParamsFromArray(array $params) } /** - * Binding params for the query, mutiple arguments support. + * Binding params for the query, multiple arguments support. * * @param mixed $param * @return self @@ -116,7 +116,7 @@ protected function resolveValueForSql($value) $value = 'NULL'; break; default: - throw new \InvalidArgumentException(sprintf('Not supportted value type of %s.', $type)); + throw new \InvalidArgumentException(sprintf('Not supported value type of %s.', $type)); break; } diff --git a/tests/ResultQueryTest.php b/tests/ResultQueryTest.php index f0f4eb4..e38fe71 100644 --- a/tests/ResultQueryTest.php +++ b/tests/ResultQueryTest.php @@ -366,7 +366,7 @@ public function testSelectCharsetDefaultsToUtf8() Loop::run(); } - public function testSelectWithExplcitCharsetReturnsCharset() + public function testSelectWithExplicitCharsetReturnsCharset() { $factory = new Factory(); From 914ff50ebc15934a3847a90f032cccaac215146f Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Fri, 10 Nov 2023 13:08:50 +0100 Subject: [PATCH 22/32] Prepare v0.6.0 release --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ README.md | 15 +++------------ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 123bfcb..651429b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # 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. diff --git a/README.md b/README.md index 97cc083..c1f938a 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,6 @@ Async MySQL database client for [ReactPHP](https://reactphp.org/). -> **Development version:** This branch contains the code for the upcoming 0.6 release. -> For the code of the current stable 0.5 release, check out the -> [`0.5.x` branch](https://github.com/friends-of-reactphp/mysql/tree/0.5.x). -> -> The upcoming 0.6 release will be the way forward for this package. -> However, we will still actively support 0.5.x 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. @@ -511,11 +502,11 @@ See also the [`close()`](#close) method. 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: +This project follows [SemVer](https://semver.org/). +This will install the latest supported version: ```bash -composer require react/mysql:^0.6@dev +composer require react/mysql:^0.6 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 32e2faa9a8bd3309d97bfe54044b1ddf8de40b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 12 Nov 2023 18:19:57 +0100 Subject: [PATCH 23/32] Hello `0.7.x` development branch --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c1f938a..8b0e027 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,15 @@ 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. @@ -502,11 +511,11 @@ See also the [`close()`](#close) method. The recommended way to install this library is [through Composer](https://getcomposer.org/). [New to Composer?](https://getcomposer.org/doc/00-intro.md) -This project follows [SemVer](https://semver.org/). -This will install the latest supported version: +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.6 +composer require react/mysql:^0.7@dev ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From 9a3976b18620f07ae1a003b943c297451603cc40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 10 Nov 2023 22:55:07 +0100 Subject: [PATCH 24/32] Simplify API, add new `MysqlClient` and remove `Factory` --- README.md | 195 ++------ examples/01-query.php | 8 +- examples/02-query-stream.php | 7 +- examples/11-interactive.php | 126 +++-- examples/12-slow-stream.php | 72 +-- src/{ => Io}/Factory.php | 139 +----- .../LazyConnection.php => MysqlClient.php} | 22 +- tests/BaseTestCase.php | 2 +- tests/{ => Io}/FactoryTest.php | 95 +--- ...ConnectionTest.php => MysqlClientTest.php} | 472 ++++++++++++++---- tests/NoResultQueryTest.php | 80 +++ tests/ResultQueryTest.php | 24 +- 12 files changed, 620 insertions(+), 622 deletions(-) rename src/{ => Io}/Factory.php (61%) rename src/{Io/LazyConnection.php => MysqlClient.php} (94%) rename tests/{ => Io}/FactoryTest.php (90%) rename tests/{Io/LazyConnectionTest.php => MysqlClientTest.php} (64%) diff --git a/README.md b/README.md index 8b0e027..7c77dec 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,8 @@ It is written in pure PHP and does not require any extensions. * [Quickstart example](#quickstart-example) * [Usage](#usage) - * [Factory](#factory) - * [createConnection()](#createconnection) - * [createLazyConnection()](#createlazyconnection) + * [MysqlClient](#mysqlclient) + * [__construct()](#__construct) * [ConnectionInterface](#connectioninterface) * [query()](#query) * [queryStream()](#querystream) @@ -45,11 +44,10 @@ This example runs a simple `SELECT` query and dumps all the records from a `book require __DIR__ . '/vendor/autoload.php'; -$factory = new React\MySQL\Factory(); -$connection = $factory->createLazyConnection('user:pass@localhost/bookstore'); +$mysql = new React\MySQL\MysqlClient('user:pass@localhost/bookstore'); -$connection->query('SELECT * FROM book')->then( - function (QueryResult $command) { +$mysql->query('SELECT * FROM book')->then( + function (React\MySQL\QueryResult $command) { print_r($command->resultFields); print_r($command->resultRows); echo count($command->resultRows) . ' row(s) in set' . PHP_EOL; @@ -64,137 +62,13 @@ See also the [examples](examples). ## Usage -### Factory +### MysqlClient -The `Factory` is responsible for creating your [`ConnectionInterface`](#connectioninterface) instance. +The `MysqlClient` is responsible for exchanging messages with your MySQL server +and keeps track of pending queries. ```php -$factory = new React\MySQL\Factory(); -``` - -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. - -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 - ) -]); - -$factory = new React\MySQL\Factory(null, $connector); -``` - -#### createConnection() - -The `createConnection(string $url): PromiseInterface` method can be used to -create a new [`ConnectionInterface`](#connectioninterface). - -It helps with establishing a TCP/IP connection to your MySQL database -and issuing the initial authentication handshake. - -```php -$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 - } -); -``` - -The method returns a [Promise](https://github.com/reactphp/promise) that -will resolve with a [`ConnectionInterface`](#connectioninterface) -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'); -``` - -#### createLazyConnection() - -Creates a new connection. - -It helps with establishing a TCP/IP connection to your MySQL database -and issuing the initial authentication handshake. - -```php -$connection = $factory->createLazyConnection($url); +$connection = new React\MySQL\MysqlClient($uri); $connection->query(…); ``` @@ -215,9 +89,6 @@ 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. If the underlying database connection fails, it will reject all outstanding commands and will return to the initial "idle" state. This @@ -234,15 +105,16 @@ 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. -Depending on your particular use case, you may prefer this method or the -underlying `createConnection()` which resolves with a promise. For many -simple use cases it may be easier to create a lazy connection. +#### __construct() + +The `new MysqlClient(string $uri, ConnectorInterface $connector = null, LoopInterface $loop = null)` constructor can be used to +create a new `MysqlClient` instance. -The `$url` parameter must contain the database host, optional +The `$uri` parameter must contain the database host, optional authentication, port and database to connect to: ```php -$factory->createLazyConnection('user:secret@localhost:3306/database'); +$mysql = new React\MySQL\MysqlClient('user:secret@localhost:3306/database'); ``` Note that both the username and password must be URL-encoded (percent-encoded) @@ -252,7 +124,7 @@ if they contain special characters: $user = 'he:llo'; $pass = 'p@ss'; -$connection = $factory->createLazyConnection( +$mysql = new React\MySQL\MysqlClient( rawurlencode($user) . ':' . rawurlencode($pass) . '@localhost:3306/db' ); ``` @@ -260,7 +132,7 @@ $connection = $factory->createLazyConnection( You can omit the port if you're connecting to default port `3306`: ```php -$factory->createLazyConnection('user:secret@localhost/database'); +$mysql = new React\MySQL\MysqlClient('user:secret@localhost/database'); ``` If you do not include authentication and/or database, then this method @@ -269,7 +141,7 @@ 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->createLazyConnection('localhost'); +$mysql = new React\MySQL\MysqlClient('localhost'); ``` This method respects PHP's `default_socket_timeout` setting (default 60s) @@ -278,7 +150,7 @@ 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->createLazyConnection('localhost?timeout=0.5'); +$mysql = new React\MySQL\MysqlClient('localhost?timeout=0.5'); ``` By default, idle connections will be held open for 1ms (0.001s) when not @@ -291,7 +163,7 @@ pass a custom idle timeout value in seconds (or use a negative number to not apply a timeout) like this: ```php -$factory->createLazyConnection('localhost?idle=10.0'); +$mysql = new React\MySQL\MysqlClient('localhost?idle=10.0'); ``` By default, the connection provides full UTF-8 support (using the @@ -300,9 +172,34 @@ applications nowadays, but for legacy reasons you can change this to use a different ASCII-compatible charset encoding like this: ```php -$factory->createLazyConnection('localhost?charset=utf8mb4'); +$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. + ### ConnectionInterface The `ConnectionInterface` represents a connection that is responsible for diff --git a/examples/01-query.php b/examples/01-query.php index 776e1f5..849bdb5 100644 --- a/examples/01-query.php +++ b/examples/01-query.php @@ -3,16 +3,12 @@ // $ php examples/01-query.php // $ MYSQL_URI=test:test@localhost/test php examples/01-query.php "SELECT * FROM book" -use React\MySQL\Factory; -use React\MySQL\QueryResult; - require __DIR__ . '/../vendor/autoload.php'; -$factory = new Factory(); -$connection = $factory->createLazyConnection(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); +$mysql = new React\MySQL\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); $query = isset($argv[1]) ? $argv[1] : 'select * from book'; -$connection->query($query)->then(function (QueryResult $command) { +$mysql->query($query)->then(function (React\MySQL\QueryResult $command) { if (isset($command->resultRows)) { // this is a response to a SELECT etc. with some rows (0+) print_r($command->resultFields); diff --git a/examples/02-query-stream.php b/examples/02-query-stream.php index c4e69b7..1562603 100644 --- a/examples/02-query-stream.php +++ b/examples/02-query-stream.php @@ -3,15 +3,12 @@ // $ php examples/02-query-stream.php "SHOW VARIABLES" // $ MYSQL_URI=test:test@localhost/test php examples/02-query-stream.php "SELECT * FROM book" -use React\MySQL\Factory; - require __DIR__ . '/../vendor/autoload.php'; -$factory = new Factory(); -$connection = $factory->createLazyConnection(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); +$mysql = new React\MySQL\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); $query = isset($argv[1]) ? $argv[1] : 'select * from book'; -$stream = $connection->queryStream($query); +$stream = $mysql->queryStream($query); $stream->on('data', function ($row) { echo json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; diff --git a/examples/11-interactive.php b/examples/11-interactive.php index 10ee6ea..2e92f4c 100644 --- a/examples/11-interactive.php +++ b/examples/11-interactive.php @@ -3,87 +3,73 @@ // $ php examples/11-interactive.php // $ MYSQL_URI=test:test@localhost/test php examples/11-interactive.php -use React\MySQL\ConnectionInterface; -use React\MySQL\QueryResult; -use React\MySQL\Factory; -use React\Stream\ReadableResourceStream; - require __DIR__ . '/../vendor/autoload.php'; -$factory = new Factory(); -$uri = getenv('MYSQL_URI') ?: 'test:test@localhost/test'; +$mysql = new React\MySQL\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); // open a STDIN stream to read keyboard input (not supported on Windows) -$stdin = new ReadableResourceStream(STDIN); -$stdin->pause(); - -//create a mysql connection for executing queries -$factory->createConnection($uri)->then(function (ConnectionInterface $connection) use ($stdin) { - echo 'Connection success.' . PHP_EOL; - $stdin->resume(); +$stdin = new React\Stream\ReadableResourceStream(STDIN); - $stdin->on('data', function ($line) use ($connection) { - $query = trim($line); +$stdin->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; - $connection->quit(); - return; - } + 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); - $connection->query($query)->then(function (QueryResult $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); - } + $time = microtime(true); + $mysql->query($query)->then(function (React\MySQL\QueryResult $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( - 'Query OK, %d row%s affected (%.03f sec)%s', - $command->affectedRows, - $command->affectedRows === 1 ? '' : 's', - microtime(true) - $time, - 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); } - }, 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 ($connection) { - $connection->quit(); + 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 STDIN (stop reading) when connection closes - $connection->on('close', function () use ($stdin) { - $stdin->close(); - echo 'Disconnected.' . PHP_EOL; - }); -}, function (Exception $e) use ($stdin) { - echo 'Connection error: ' . $e->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; diff --git a/examples/12-slow-stream.php b/examples/12-slow-stream.php index bb1af49..b61c6f8 100644 --- a/examples/12-slow-stream.php +++ b/examples/12-slow-stream.php @@ -4,19 +4,21 @@ // $ MYSQL_URI=test:test@localhost/test php examples/12-slow-stream.php "SELECT * FROM book" use React\EventLoop\Loop; -use React\MySQL\ConnectionInterface; -use React\MySQL\Factory; require __DIR__ . '/../vendor/autoload.php'; -$factory = new Factory(); -$uri = getenv('MYSQL_URI') ?: 'test:test@localhost/test'; +$mysql = new React\MySQL\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); $query = isset($argv[1]) ? $argv[1] : 'select * from book'; +$stream = $mysql->queryStream($query); -//create a mysql connection for executing query -$factory->createConnection($uri)->then(function (ConnectionInterface $connection) use ($query) { - // The protocol parser reads rather large chunked from the underlying connection +$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. @@ -28,11 +30,13 @@ $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'); @@ -41,38 +45,34 @@ } catch (Exception $e) { echo 'Warning: Unable to reduce buffer size: ' . $e->getMessage() . PHP_EOL; } +}); - $stream = $connection->queryStream($query); - - $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; +$throttle = null; +$stream->on('data', function ($row) use (&$throttle, $stream) { + echo json_encode($row, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; - if ($throttle) { - Loop::cancelTimer($throttle); + // 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(); + } +}); - $connection->quit(); -}, function (Exception $e) { +$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(); diff --git a/src/Factory.php b/src/Io/Factory.php similarity index 61% rename from src/Factory.php rename to src/Io/Factory.php index 25911e7..9aa27d1 100644 --- a/src/Factory.php +++ b/src/Io/Factory.php @@ -1,21 +1,22 @@ createLazyConnection($url); - * - * $connection->query(…); - * ``` - * - * This method immediately returns a "virtual" connection implementing the - * [`ConnectionInterface`](#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. 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. - * 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. - * - * 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 "virtual" 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. - * - * Depending on your particular use case, you may prefer this method or the - * underlying `createConnection()` which resolves with a promise. For many - * simple use cases it may be easier to create a lazy connection. - * - * The `$url` parameter must contain the database host, optional - * authentication, port and database to connect to: - * - * ```php - * $factory->createLazyConnection('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'; - * - * $connection = $factory->createLazyConnection( - * rawurlencode($user) . ':' . rawurlencode($pass) . '@localhost:3306/db' - * ); - * ``` - * - * You can omit the port if you're connecting to default port `3306`: - * - * ```php - * $factory->createLazyConnection('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->createLazyConnection('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 - * $factory->createLazyConnection('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 - * $factory->createLazyConnection('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 - * $factory->createLazyConnection('localhost?charset=utf8mb4'); - * ``` - * - * @param string $uri - * @return ConnectionInterface - */ - public function createLazyConnection( - #[\SensitiveParameter] - $uri - ) { - return new LazyConnection($this, $uri, $this->loop); - } } diff --git a/src/Io/LazyConnection.php b/src/MysqlClient.php similarity index 94% rename from src/Io/LazyConnection.php rename to src/MysqlClient.php index d825dbd..a88892c 100644 --- a/src/Io/LazyConnection.php +++ b/src/MysqlClient.php @@ -1,19 +1,17 @@ idlePeriod = (float)$args['idle']; } - $this->factory = $factory; + $this->factory = new Factory($loop, $connector); $this->uri = $uri; - $this->loop = $loop; + $this->loop = $loop ?: Loop::get(); } private function connecting() diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index be67f93..ecd19b8 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -5,7 +5,7 @@ use PHPUnit\Framework\TestCase; use React\EventLoop\LoopInterface; use React\MySQL\ConnectionInterface; -use React\MySQL\Factory; +use React\MySQL\Io\Factory; class BaseTestCase extends TestCase { diff --git a/tests/FactoryTest.php b/tests/Io/FactoryTest.php similarity index 90% rename from tests/FactoryTest.php rename to tests/Io/FactoryTest.php index 2e73bed..bbedaa3 100644 --- a/tests/FactoryTest.php +++ b/tests/Io/FactoryTest.php @@ -1,12 +1,13 @@ expectOutputString('closed.'); - - $factory = new Factory(); - - $uri = 'mysql://random:pass@host'; - $connection = $factory->createLazyConnection($uri); - - $connection->quit()->then(function () { - echo 'closed.'; - }); - } - - public function testConnectLazyWithValidAuthWillRunUntilQuitAfterPing() - { - $this->expectOutputString('closed.'); - - $factory = new Factory(); - - $uri = $this->getConnectionString(); - $connection = $factory->createLazyConnection($uri); - - $connection->ping(); - - $connection->quit()->then(function () { - echo 'closed.'; - }); - - Loop::run(); - } - - /** - * @doesNotPerformAssertions - */ - public function testConnectLazyWithValidAuthWillRunUntilIdleTimerAfterPingEvenWithoutQuit() - { - $factory = new Factory(); - - $uri = $this->getConnectionString(); - $connection = $factory->createLazyConnection($uri); - - $connection->ping(); - - Loop::run(); - } - - public function testConnectLazyWithInvalidAuthWillRejectPingButWillNotEmitErrorOrClose() - { - $factory = new Factory(); - - $uri = $this->getConnectionString(['passwd' => 'invalidpass']); - $connection = $factory->createLazyConnection($uri); - - $connection->on('error', $this->expectCallableNever()); - $connection->on('close', $this->expectCallableNever()); - - $connection->ping()->then(null, $this->expectCallableOnce()); - - Loop::run(); - } - - public function testConnectLazyWithValidAuthWillPingBeforeQuitButNotAfter() - { - $this->expectOutputString('rejected.ping.closed.'); - - $factory = new Factory(); - - $uri = $this->getConnectionString(); - $connection = $factory->createLazyConnection($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(); - } } diff --git a/tests/Io/LazyConnectionTest.php b/tests/MysqlClientTest.php similarity index 64% rename from tests/Io/LazyConnectionTest.php rename to tests/MysqlClientTest.php index 89e75e8..df7d021 100644 --- a/tests/Io/LazyConnectionTest.php +++ b/tests/MysqlClientTest.php @@ -2,24 +2,84 @@ namespace React\Tests\MySQL\Io; -use React\MySQL\Io\LazyConnection; +use React\MySQL\Io\Connection; +use React\MySQL\MysqlClient; +use React\MySQL\QueryResult; use React\Promise\Deferred; use React\Promise\Promise; use React\Promise\PromiseInterface; -use React\Tests\MySQL\BaseTestCase; use React\Stream\ReadableStreamInterface; use React\Stream\ThroughStream; -use React\MySQL\QueryResult; +use React\Tests\MySQL\BaseTestCase; -class LazyConnectionTest extends BaseTestCase +class MysqlClientTest extends BaseTestCase { + public function testConstructWithoutConnectorAndLoopAssignsConnectorAndLoopAutomatically() + { + $mysql = new MysqlClient('localhost'); + + $ref = new \ReflectionProperty($mysql, 'factory'); + $ref->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($mysql, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($mysql); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + + $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($mysql, 'loop'); + $ref->setAccessible(true); + + $this->assertSame($loop, $ref->getValue($mysql)); + + $ref = new \ReflectionProperty($factory, 'loop'); + $ref->setAccessible(true); + + $this->assertSame($loop, $ref->getValue($factory)); + } + public function testPingWillNotCloseConnectionWhenPendingConnectionFails() { $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableNever()); @@ -33,27 +93,34 @@ public function testPingWillNotCloseConnectionWhenPendingConnectionFails() public function testPingWillNotCloseConnectionWhenUnderlyingConnectionCloses() { - $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); + $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\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', 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(); - $base->close(); + + assert($base instanceof Connection); + $base->emit('close'); } public function testPingWillCancelTimerWithoutClosingConnectionWhenUnderlyingConnectionCloses() { - $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); + $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\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); @@ -61,23 +128,34 @@ public function testPingWillCancelTimerWithoutClosingConnectionWhenUnderlyingCon $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('close', $this->expectCallableNever()); $connection->ping(); - $base->close(); + + assert($base instanceof Connection); + $base->emit('close'); } public function testPingWillNotForwardErrorFromUnderlyingConnection() { - $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); + $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\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableNever()); @@ -89,12 +167,12 @@ public function testPingWillNotForwardErrorFromUnderlyingConnection() public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection() { - $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); + $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->once())->method('quit')->willReturn(\React\Promise\resolve(null)); $base->expects($this->never())->method('close'); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timeout = null; @@ -105,7 +183,11 @@ public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection() return true; }))->willReturn($timer); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('close', $this->expectCallableNever()); @@ -117,12 +199,12 @@ public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection() public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuitFails() { - $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); + $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->once())->method('quit')->willReturn(\React\Promise\reject(new \RuntimeException())); $base->expects($this->once())->method('close'); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timeout = null; @@ -133,7 +215,11 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuit return true; }))->willReturn($timer); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('close', $this->expectCallableNever()); @@ -145,12 +231,12 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuit public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatingSecondConnection() { - $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); + $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->once())->method('quit')->willReturn(new Promise(function () { })); $base->expects($this->once())->method('close'); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\resolve($base), new Promise(function () { }) @@ -164,7 +250,11 @@ public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatin return true; }))->willReturn($timer); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('close', $this->expectCallableNever()); @@ -180,13 +270,17 @@ public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatin public function testQueryReturnsPendingPromiseAndWillNotStartTimerWhenConnectionIsPending() { $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->never())->method('addTimer'); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $ret = $connection->query('SELECT 1'); @@ -199,10 +293,15 @@ public function testQueryWillQueryUnderlyingConnectionWhenResolved() $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->query('SELECT 1'); } @@ -214,13 +313,17 @@ public function testQueryWillResolveAndStartTimerWithDefaultIntervalWhenQueryFro $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', 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()); @@ -233,13 +336,17 @@ public function testQueryWillResolveAndStartTimerWithIntervalFromIdleParameterWh $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->once())->method('addTimer')->with(2.5, $this->anything()); - $connection = new LazyConnection($factory, 'mysql://localhost?idle=2.5', $loop); + $connection = new MysqlClient('mysql://localhost?idle=2.5', 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()); @@ -252,13 +359,17 @@ public function testQueryWillResolveWithoutStartingTimerWhenQueryFromUnderlyingC $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->never())->method('addTimer'); - $connection = new LazyConnection($factory, 'mysql://localhost?idle=-1', $loop); + $connection = new MysqlClient('mysql://localhost?idle=-1', 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()); @@ -273,13 +384,17 @@ public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromU $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn($deferred->promise()); $base->expects($this->once())->method('ping')->willReturn(new Promise(function () { })); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->never())->method('addTimer'); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $ret = $connection->query('SELECT 1'); $connection->ping(); @@ -295,7 +410,7 @@ public function testQueryAfterPingWillCancelTimerAgainWhenPingFromUnderlyingConn $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); @@ -303,7 +418,11 @@ public function testQueryAfterPingWillCancelTimerAgainWhenPingFromUnderlyingConn $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->ping(); $connection->query('SELECT 1'); @@ -316,13 +435,17 @@ public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectio $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\reject($error)); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->once())->method('addTimer'); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', 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)); @@ -331,13 +454,17 @@ public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectio public function testQueryWillRejectWithoutStartingTimerWhenUnderlyingConnectionRejects() { $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->never())->method('addTimer'); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', 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()); @@ -348,10 +475,15 @@ public function testQueryWillRejectWithoutStartingTimerWhenUnderlyingConnectionR public function testQueryStreamReturnsReadableStreamWhenConnectionIsPending() { $promise = new Promise(function () { }); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $ret = $connection->queryStream('SELECT 1'); @@ -365,13 +497,17 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutSt $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn($stream); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->never())->method('addTimer'); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $ret = $connection->queryStream('SELECT 1'); @@ -387,13 +523,17 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionAndStartT $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn($stream); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->once())->method('addTimer'); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $ret = $connection->queryStream('SELECT 1'); @@ -409,10 +549,15 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionAndStartT public function testQueryStreamWillCloseStreamWithErrorWhenUnderlyingConnectionRejects() { $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $ret = $connection->queryStream('SELECT 1'); @@ -427,10 +572,15 @@ public function testQueryStreamWillCloseStreamWithErrorWhenUnderlyingConnectionR public function testPingReturnsPendingPromiseWhenConnectionIsPending() { $promise = new Promise(function () { }); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $ret = $connection->ping(); @@ -443,10 +593,15 @@ public function testPingWillPingUnderlyingConnectionWhenResolved() $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('ping')->willReturn(new Promise(function () { })); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->ping(); } @@ -456,10 +611,15 @@ public function testPingTwiceWillBothRejectWithSameErrorWhenUnderlyingConnection $error = new \RuntimeException(); $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', 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)); @@ -471,10 +631,15 @@ public function testPingWillTryToCreateNewUnderlyingConnectionAfterPreviousPingF { $error = new \RuntimeException(); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', 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)); @@ -485,13 +650,17 @@ public function testPingWillResolveAndStartTimerWhenPingFromUnderlyingConnection $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->once())->method('addTimer'); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $ret = $connection->ping(); $ret->then($this->expectCallableOnce(), $this->expectCallableNever()); @@ -504,13 +673,17 @@ public function testPingWillRejectAndStartTimerWhenPingFromUnderlyingConnectionR $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\reject($error)); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->once())->method('addTimer'); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $ret = $connection->ping(); $ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); @@ -520,20 +693,24 @@ public function testPingWillRejectAndNotStartIdleTimerWhenPingFromUnderlyingConn { $error = new \RuntimeException(); - $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); + $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\Factory')->disableOriginalConstructor()->getMock(); + $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(); $loop->expects($this->never())->method('addTimer'); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $ret = $connection->ping(); $ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); @@ -541,10 +718,15 @@ public function testPingWillRejectAndNotStartIdleTimerWhenPingFromUnderlyingConn public function testQuitResolvesAndEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() { - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); @@ -558,10 +740,15 @@ public function testQuitResolvesAndEmitsCloseImmediatelyWhenConnectionIsNotAlrea public function testQuitAfterPingReturnsPendingPromiseWhenConnectionIsPending() { $promise = new Promise(function () { }); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->ping(); $ret = $connection->quit(); @@ -576,10 +763,15 @@ public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved() $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\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->ping(); $connection->quit(); @@ -591,10 +783,15 @@ public function testQuitAfterPingResolvesAndEmitsCloseWhenUnderlyingConnectionQu $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('quit')->willReturn(\React\Promise\resolve(null)); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('close', $this->expectCallableOnce()); @@ -612,10 +809,15 @@ public function testQuitAfterPingRejectsAndEmitsCloseWhenUnderlyingConnectionFai $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('quit')->willReturn(\React\Promise\reject($error)); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('close', $this->expectCallableOnce()); @@ -628,10 +830,15 @@ public function testQuitAfterPingRejectsAndEmitsCloseWhenUnderlyingConnectionFai public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() { - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); @@ -642,10 +849,15 @@ public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending( public function testCloseAfterPingCancelsPendingConnection() { $deferred = new Deferred($this->expectCallableOnce()); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->ping(); $connection->close(); @@ -657,10 +869,15 @@ public function testCloseTwiceAfterPingWillCloseUnderlyingConnectionWhenResolved $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('close'); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->ping(); $connection->close(); @@ -673,10 +890,15 @@ public function testCloseAfterPingDoesNotEmitConnectionErrorFromAbortedConnectio throw new \RuntimeException(); }); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); @@ -693,7 +915,7 @@ public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectio $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); @@ -701,7 +923,11 @@ public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectio $loop->expects($this->once())->method('addTimer')->willReturn($timer); $loop->expects($this->once())->method('cancelTimer')->with($timer); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->ping()->then($this->expectCallableOnce(), $this->expectCallableNever()); $connection->close(); @@ -709,18 +935,22 @@ public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectio public function testCloseAfterPingHasResolvedWillCloseUnderlyingConnectionWithoutTryingToCancelConnection() { - $base = $this->getMockBuilder('React\MySQL\Io\LazyConnection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); + $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')->willReturnCallback(function () use ($base) { $base->emit('close'); }); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->ping(); $connection->close(); @@ -733,10 +963,15 @@ public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuit $base->expects($this->once())->method('quit')->willReturn(new Promise(function () { })); $base->expects($this->once())->method('close'); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->ping(); $connection->quit(); @@ -750,7 +985,7 @@ public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionW $base->expects($this->once())->method('quit')->willReturn(new Promise(function () { })); $base->expects($this->once())->method('close'); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timeout = null; @@ -761,7 +996,11 @@ public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionW return true; }))->willReturn($timer); - $connection = new LazyConnection($factory, '', $loop); + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->ping(); @@ -774,10 +1013,15 @@ public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionW public function testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnectionIsPending() { $promise = new Promise(function () { }); - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $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 LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); @@ -789,10 +1033,15 @@ public function testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnectionIsPendin public function testQueryReturnsRejectedPromiseAfterConnectionIsClosed() { - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->close(); $ret = $connection->query('SELECT 1'); @@ -803,10 +1052,15 @@ public function testQueryReturnsRejectedPromiseAfterConnectionIsClosed() public function testQueryStreamThrowsAfterConnectionIsClosed() { - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->close(); @@ -816,10 +1070,15 @@ public function testQueryStreamThrowsAfterConnectionIsClosed() public function testPingReturnsRejectedPromiseAfterConnectionIsClosed() { - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->close(); $ret = $connection->ping(); @@ -830,10 +1089,15 @@ public function testPingReturnsRejectedPromiseAfterConnectionIsClosed() public function testQuitReturnsRejectedPromiseAfterConnectionIsClosed() { - $factory = $this->getMockBuilder('React\MySQL\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new LazyConnection($factory, '', $loop); + + $connection = new MysqlClient('', null, $loop); + + $ref = new \ReflectionProperty($connection, 'factory'); + $ref->setAccessible(true); + $ref->setValue($connection, $factory); $connection->close(); $ret = $connection->quit(); diff --git a/tests/NoResultQueryTest.php b/tests/NoResultQueryTest.php index 1f48380..75c8f98 100644 --- a/tests/NoResultQueryTest.php +++ b/tests/NoResultQueryTest.php @@ -3,6 +3,7 @@ namespace React\Tests\MySQL; use React\EventLoop\Loop; +use React\MySQL\MysqlClient; use React\MySQL\QueryResult; class NoResultQueryTest extends BaseTestCase @@ -102,4 +103,83 @@ public function testPingMultipleWillBeExecutedInSameOrderTheyAreEnqueuedFromHand 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(); + } + + /** + * @doesNotPerformAssertions + */ + public function testPingWithValidAuthWillRunUntilIdleTimerAfterPingEvenWithoutQuit() + { + $uri = $this->getConnectionString(); + $connection = new MysqlClient($uri); + + $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(); + } } diff --git a/tests/ResultQueryTest.php b/tests/ResultQueryTest.php index e38fe71..768a9d5 100644 --- a/tests/ResultQueryTest.php +++ b/tests/ResultQueryTest.php @@ -4,8 +4,8 @@ use React\EventLoop\Loop; use React\MySQL\Io\Constants; +use React\MySQL\MysqlClient; use React\MySQL\QueryResult; -use React\MySQL\Factory; class ResultQueryTest extends BaseTestCase { @@ -368,10 +368,8 @@ public function testSelectCharsetDefaultsToUtf8() public function testSelectWithExplicitCharsetReturnsCharset() { - $factory = new Factory(); - $uri = $this->getConnectionString() . '?charset=latin1'; - $connection = $factory->createLazyConnection($uri); + $connection = new MysqlClient($uri); $connection->query('SELECT @@character_set_client')->then(function (QueryResult $command) { $this->assertCount(1, $command->resultRows); @@ -404,12 +402,10 @@ public function testSimpleSelect() /** * @depends testSimpleSelect */ - public function testSimpleSelectFromLazyConnectionWithoutDatabaseNameReturnsSameData() + public function testSimpleSelectFromMysqlClientWithoutDatabaseNameReturnsSameData() { - $factory = new Factory(); - $uri = $this->getConnectionString(['dbname' => '']); - $connection = $factory->createLazyConnection($uri); + $connection = new MysqlClient($uri); $connection->query('select * from test.book')->then(function (QueryResult $command) { $this->assertCount(2, $command->resultRows); @@ -560,12 +556,10 @@ public function testQueryStreamExplicitCloseEmitsCloseEventWithoutData() Loop::run(); } - public function testQueryStreamFromLazyConnectionEmitsSingleRow() + public function testQueryStreamFromMysqlClientEmitsSingleRow() { - $factory = new Factory(); - $uri = $this->getConnectionString(); - $connection = $factory->createLazyConnection($uri); + $connection = new MysqlClient($uri); $stream = $connection->queryStream('SELECT 1'); @@ -577,12 +571,10 @@ public function testQueryStreamFromLazyConnectionEmitsSingleRow() Loop::run(); } - public function testQueryStreamFromLazyConnectionWillErrorWhenConnectionIsClosed() + public function testQueryStreamFromMysqlClientWillErrorWhenConnectionIsClosed() { - $factory = new Factory(); - $uri = $this->getConnectionString(); - $connection = $factory->createLazyConnection($uri); + $connection = new MysqlClient($uri); $stream = $connection->queryStream('SELECT 1'); From c4dc4153e3d7945f9c094bb03adb6070bffb7d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 14 Nov 2023 15:26:12 +0100 Subject: [PATCH 25/32] Simplify API, remove `ConnectionInterface` --- README.md | 71 +++++------- src/ConnectionInterface.php | 225 ------------------------------------ src/Io/Connection.php | 5 +- src/Io/Factory.php | 10 +- src/MysqlClient.php | 224 +++++++++++++++++++++++++++++++++-- tests/BaseTestCase.php | 4 +- tests/Io/FactoryTest.php | 18 +-- tests/MysqlClientTest.php | 38 +++--- 8 files changed, 282 insertions(+), 313 deletions(-) delete mode 100644 src/ConnectionInterface.php diff --git a/README.md b/README.md index 7c77dec..90a6a7b 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,13 @@ It is written in pure PHP and does not require any extensions. * [Usage](#usage) * [MysqlClient](#mysqlclient) * [__construct()](#__construct) - * [ConnectionInterface](#connectioninterface) * [query()](#query) * [queryStream()](#querystream) * [ping()](#ping) * [quit()](#quit) * [close()](#close) - * [Events](#events) + * [error event](#error-event) + * [close event](#close-event) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -68,21 +68,21 @@ The `MysqlClient` is responsible for exchanging messages with your MySQL server and keeps track of pending queries. ```php -$connection = new React\MySQL\MysqlClient($uri); +$mysql = new React\MySQL\MysqlClient($uri); -$connection->query(…); +$mysql->query(…); ``` -This method immediately returns a "virtual" connection implementing the -[`ConnectionInterface`](#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. 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. +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 @@ -100,7 +100,7 @@ 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 "virtual" connection will be soft-closed +`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. @@ -200,12 +200,6 @@ here in order to use the [default loop](https://github.com/reactphp/event-loop#l This value SHOULD NOT be given unless you're sure you want to explicitly use a given event loop instance. -### ConnectionInterface - -The `ConnectionInterface` represents a connection that is responsible for -communicating with your MySQL server instance, managing the connection state -and sending your database queries. - #### query() The `query(string $query, array $params = []): PromiseInterface` method can be used to @@ -218,8 +212,8 @@ and outstanding queries will be put into a queue to be executed once the previous queries are completed. ```php -$connection->query('CREATE TABLE test ...'); -$connection->query('INSERT INTO test (id) VALUES (1)'); +$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` @@ -231,7 +225,7 @@ unknown or known to be too large to fit into memory, you should use the [`queryStream()`](#querystream) method instead. ```php -$connection->query($query)->then(function (QueryResult $command) { +$mysql->query($query)->then(function (QueryResult $command) { if (isset($command->resultRows)) { // this is a response to a SELECT etc. with some rows (0+) print_r($command->resultFields); @@ -254,7 +248,7 @@ You can optionally pass an array of `$params` that will be bound to the query like this: ```php -$connection->query('SELECT * FROM user WHERE id > ?', [$id]); +$mysql->query('SELECT * FROM user WHERE id > ?', [$id]); ``` The given `$sql` parameter MUST contain a single statement. Support @@ -275,7 +269,7 @@ 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 = $connection->queryStream('SELECT * FROM user'); +$stream = $mysql->queryStream('SELECT * FROM user'); $stream->on('data', function ($row) { echo $row['name'] . PHP_EOL; }); @@ -288,7 +282,7 @@ You can optionally pass an array of `$params` that will be bound to the query like this: ```php -$stream = $connection->queryStream('SELECT * FROM user WHERE id > ?', [$id]); +$stream = $mysql->queryStream('SELECT * FROM user WHERE id > ?', [$id]); ``` This method is specifically designed for queries that return a result set @@ -303,7 +297,7 @@ rows to a [`WritableStreamInterface`](https://github.com/reactphp/stream#writabl like this: ```php -$connection->queryStream('SELECT * FROM user')->pipe($formatter)->pipe($logger); +$mysql->queryStream('SELECT * FROM user')->pipe($formatter)->pipe($logger); ``` Note that as per the underlying stream definition, calling `pause()` and @@ -331,7 +325,7 @@ and outstanding command will be put into a queue to be executed once the previous queries are completed. ```php -$connection->ping()->then(function () { +$mysql->ping()->then(function () { echo 'OK' . PHP_EOL; }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; @@ -350,8 +344,8 @@ and outstanding commands will be put into a queue to be executed once the previous commands are completed. ```php -$connection->query('CREATE TABLE test ...'); -$connection->quit(); +$mysql->query('CREATE TABLE test ...'); +$mysql->quit(); ``` #### close() @@ -363,26 +357,21 @@ Unlike the `quit()` method, this method will immediately force-close the connection and reject all outstanding commands. ```php -$connection->close(); +$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. -#### Events - -Besides defining a few methods, this interface also implements the -`EventEmitterInterface` which allows you to react to certain events: - -##### error event +#### 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 -$connection->on('error', function (Exception $e) { +$mysql->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -391,12 +380,12 @@ 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 +#### close event The `close` event will be emitted once the connection closes (terminates). ```php -$connection->on('close', function () { +$mysql->on('close', function () { echo 'Connection closed' . PHP_EOL; }); ``` diff --git a/src/ConnectionInterface.php b/src/ConnectionInterface.php deleted file mode 100644 index e379dbc..0000000 --- a/src/ConnectionInterface.php +++ /dev/null @@ -1,225 +0,0 @@ -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 - * $connection->on('close', function () { - * echo 'Connection closed' . PHP_EOL; - * }); - * ``` - * - * See also the [`close()`](#close) method. - */ -interface ConnectionInterface extends EventEmitterInterface -{ - /** - * Performs an async query. - * - * This method returns a promise that will resolve with a `QueryResult` 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 - * $connection->query('CREATE TABLE test ...'); - * $connection->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 - * $connection->query($query)->then(function (QueryResult $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 - * $connection->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 array $params Parameters which should be bound to query - * @return PromiseInterface - * Resolves with a `QueryResult` on success or rejects with an `Exception` on error. - */ - public function query($sql, array $params = []); - - /** - * 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 = $connection->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 = $connection->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 - * $connection->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 array $params Parameters which should be bound to query - * @return ReadableStreamInterface - */ - public function queryStream($sql, $params = []); - - /** - * 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 - * $connection->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(); - - /** - * 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 - * $connection->query('CREATE TABLE test ...'); - * $connection->quit(); - * ``` - * - * @return PromiseInterface - * Resolves with a `void` value on success or rejects with an `Exception` on error. - */ - public function quit(); - - /** - * Force-close the connection. - * - * Unlike the `quit()` method, this method will immediately force-close the - * connection and reject all outstanding commands. - * - * ```php - * $connection->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(); -} diff --git a/src/Io/Connection.php b/src/Io/Connection.php index 73313ba..fc71c6c 100644 --- a/src/Io/Connection.php +++ b/src/Io/Connection.php @@ -7,7 +7,6 @@ use React\MySQL\Commands\PingCommand; use React\MySQL\Commands\QueryCommand; use React\MySQL\Commands\QuitCommand; -use React\MySQL\ConnectionInterface; use React\MySQL\Exception; use React\MySQL\QueryResult; use React\Promise\Deferred; @@ -16,9 +15,9 @@ /** * @internal - * @see ConnectionInterface + * @see \React\MySQL\MysqlClient */ -class Connection extends EventEmitter implements ConnectionInterface +class Connection extends EventEmitter { const STATE_AUTHENTICATED = 5; const STATE_CLOSING = 6; diff --git a/src/Io/Factory.php b/src/Io/Factory.php index 9aa27d1..f50eb5a 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -26,7 +26,7 @@ class Factory private $connector; /** - * The `Factory` is responsible for creating your [`ConnectionInterface`](#connectioninterface) instance. + * The `Factory` is responsible for creating an internal `Connection` instance. * * ```php * $factory = new React\MySQL\Io\Factory(); @@ -74,7 +74,7 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn * * ```php * $factory->createConnection($url)->then( - * function (ConnectionInterface $connection) { + * function (Connection $connection) { * // client connection established (and authenticated) * }, * function (Exception $e) { @@ -84,7 +84,7 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn * ``` * * The method returns a [Promise](https://github.com/reactphp/promise) that - * will resolve with a [`ConnectionInterface`](#connectioninterface) + * 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. * @@ -154,8 +154,8 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn * ``` * * @param string $uri - * @return PromiseInterface - * Resolves with a `ConnectionInterface` on success or rejects with an `Exception` on error. + * @return PromiseInterface + * Resolves with a `Connection` on success or rejects with an `Exception` on error. */ public function createConnection( #[\SensitiveParameter] diff --git a/src/MysqlClient.php b/src/MysqlClient.php index a88892c..c96d300 100644 --- a/src/MysqlClient.php +++ b/src/MysqlClient.php @@ -5,13 +5,48 @@ use Evenement\EventEmitter; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; +use React\MySQL\Io\Connection; use React\MySQL\Io\Factory; +use React\Stream\ReadableStreamInterface; use React\Socket\ConnectorInterface; /** + * This class represents a connection that is responsible for communicating + * with your MySQL server instance, managing the connection state and sending + * your database queries. + * + * Besides defining a few methods, this class also implements the + * `EventEmitterInterface` which allows you to react to certain events: + * + * 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. + * * @final */ -class MysqlClient extends EventEmitter implements ConnectionInterface +class MysqlClient extends EventEmitter { private $factory; private $uri; @@ -20,7 +55,7 @@ class MysqlClient extends EventEmitter implements ConnectionInterface private $busy = false; /** - * @var ConnectionInterface|null + * @var Connection|null */ private $disconnecting; @@ -59,7 +94,7 @@ private function connecting() } $this->connecting = $connecting = $this->factory->createConnection($this->uri); - $this->connecting->then(function (ConnectionInterface $connection) { + $this->connecting->then(function (Connection $connection) { // connection completed => remember only until closed $connection->on('close', function () { $this->connecting = null; @@ -93,7 +128,7 @@ private function idle() if ($this->pending < 1 && $this->idlePeriod >= 0 && $this->connecting !== null) { $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () { - $this->connecting->then(function (ConnectionInterface $connection) { + $this->connecting->then(function (Connection $connection) { $this->disconnecting = $connection; $connection->quit()->then( function () { @@ -113,13 +148,72 @@ function () use ($connection) { } } + /** + * Performs an async query. + * + * This method returns a promise that will resolve with a `QueryResult` 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 (QueryResult $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 array $params Parameters which should be bound to query + * @return PromiseInterface + * Resolves with a `QueryResult` on success or rejects with an `Exception` on error. + */ public function query($sql, array $params = []) { if ($this->closed) { return \React\Promise\reject(new Exception('Connection closed')); } - return $this->connecting()->then(function (ConnectionInterface $connection) use ($sql, $params) { + return $this->connecting()->then(function (Connection $connection) use ($sql, $params) { $this->awake(); return $connection->query($sql, $params)->then( function (QueryResult $result) { @@ -134,6 +228,65 @@ function (\Exception $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 array $params Parameters which should be bound to query + * @return ReadableStreamInterface + */ public function queryStream($sql, $params = []) { if ($this->closed) { @@ -141,7 +294,7 @@ public function queryStream($sql, $params = []) } return \React\Promise\Stream\unwrapReadable( - $this->connecting()->then(function (ConnectionInterface $connection) use ($sql, $params) { + $this->connecting()->then(function (Connection $connection) use ($sql, $params) { $stream = $connection->queryStream($sql, $params); $this->awake(); @@ -154,13 +307,33 @@ public function queryStream($sql, $params = []) ); } + /** + * 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) { return \React\Promise\reject(new Exception('Connection closed')); } - return $this->connecting()->then(function (ConnectionInterface $connection) { + return $this->connecting()->then(function (Connection $connection) { $this->awake(); return $connection->ping()->then( function () { @@ -174,6 +347,23 @@ function (\Exception $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(); + * ``` + * + * @return PromiseInterface + * Resolves with a `void` value on success or rejects with an `Exception` on error. + */ public function quit() { if ($this->closed) { @@ -186,7 +376,7 @@ public function quit() return \React\Promise\resolve(null); } - return $this->connecting()->then(function (ConnectionInterface $connection) { + return $this->connecting()->then(function (Connection $connection) { $this->awake(); return $connection->quit()->then( function () { @@ -200,6 +390,22 @@ function (\Exception $e) { }); } + /** + * 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) { @@ -216,7 +422,7 @@ public function close() // either close active connection or cancel pending connection attempt if ($this->connecting !== null) { - $this->connecting->then(function (ConnectionInterface $connection) { + $this->connecting->then(function (Connection $connection) { $connection->close(); }, function () { // ignore to avoid reporting unhandled rejection diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index ecd19b8..d584c7b 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -4,7 +4,7 @@ use PHPUnit\Framework\TestCase; use React\EventLoop\LoopInterface; -use React\MySQL\ConnectionInterface; +use React\MySQL\Io\Connection; use React\MySQL\Io\Factory; class BaseTestCase extends TestCase @@ -30,7 +30,7 @@ protected function getConnectionString($params = []) /** * @param LoopInterface $loop - * @return ConnectionInterface + * @return Connection */ protected function createConnection(LoopInterface $loop) { diff --git a/tests/Io/FactoryTest.php b/tests/Io/FactoryTest.php index bbedaa3..8757592 100644 --- a/tests/Io/FactoryTest.php +++ b/tests/Io/FactoryTest.php @@ -3,7 +3,7 @@ namespace React\Tests\MySQL\Io; use React\EventLoop\Loop; -use React\MySQL\ConnectionInterface; +use React\MySQL\Io\Connection; use React\MySQL\Io\Factory; use React\Promise\Promise; use React\Socket\SocketServer; @@ -275,7 +275,7 @@ public function testConnectWithValidAuthWillRunUntilQuit() $factory = new Factory(); $uri = $this->getConnectionString(); - $factory->createConnection($uri)->then(function (ConnectionInterface $connection) { + $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->quit()->then(function () { echo 'closed.'; @@ -292,7 +292,7 @@ public function testConnectWithValidAuthAndWithoutDbNameWillRunUntilQuit() $factory = new Factory(); $uri = $this->getConnectionString(['dbname' => '']); - $factory->createConnection($uri)->then(function (ConnectionInterface $connection) { + $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->quit()->then(function () { echo 'closed.'; @@ -309,7 +309,7 @@ public function testConnectWithValidAuthWillIgnoreNegativeTimeoutAndRunUntilQuit $factory = new Factory(); $uri = $this->getConnectionString() . '?timeout=-1'; - $factory->createConnection($uri)->then(function (ConnectionInterface $connection) { + $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->quit()->then(function () { echo 'closed.'; @@ -326,7 +326,7 @@ public function testConnectWithValidAuthCanPingAndThenQuit() $factory = new Factory(); $uri = $this->getConnectionString(); - $factory->createConnection($uri)->then(function (ConnectionInterface $connection) { + $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->ping()->then(function () use ($connection) { echo 'ping.'; @@ -346,7 +346,7 @@ public function testConnectWithValidAuthCanQueuePingAndQuit() $factory = new Factory(); $uri = $this->getConnectionString(); - $factory->createConnection($uri)->then(function (ConnectionInterface $connection) { + $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->ping()->then(function () { echo 'ping.'; @@ -366,7 +366,7 @@ public function testConnectWithValidAuthQuitOnlyOnce() $factory = new Factory(); $uri = $this->getConnectionString(); - $factory->createConnection($uri)->then(function (ConnectionInterface $connection) { + $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->quit()->then(function () { echo 'closed.'; @@ -388,7 +388,7 @@ public function testConnectWithValidAuthCanCloseOnlyOnce() $factory = new Factory(); $uri = $this->getConnectionString(); - $factory->createConnection($uri)->then(function (ConnectionInterface $connection) { + $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->on('close', function () { echo 'closed.'; @@ -411,7 +411,7 @@ public function testConnectWithValidAuthCanCloseAndAbortPing() $factory = new Factory(); $uri = $this->getConnectionString(); - $factory->createConnection($uri)->then(function (ConnectionInterface $connection) { + $factory->createConnection($uri)->then(function (Connection $connection) { echo 'connected.'; $connection->on('close', function () { echo 'closed.'; diff --git a/tests/MysqlClientTest.php b/tests/MysqlClientTest.php index df7d021..c3923ea 100644 --- a/tests/MysqlClientTest.php +++ b/tests/MysqlClientTest.php @@ -290,7 +290,7 @@ public function testQueryReturnsPendingPromiseAndWillNotStartTimerWhenConnection public function testQueryWillQueryUnderlyingConnectionWhenResolved() { - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); @@ -310,7 +310,7 @@ public function testQueryWillResolveAndStartTimerWithDefaultIntervalWhenQueryFro { $result = new QueryResult(); - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); @@ -333,7 +333,7 @@ public function testQueryWillResolveAndStartTimerWithIntervalFromIdleParameterWh { $result = new QueryResult(); - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); @@ -356,7 +356,7 @@ public function testQueryWillResolveWithoutStartingTimerWhenQueryFromUnderlyingC { $result = new QueryResult(); - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); @@ -380,7 +380,7 @@ public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromU $result = new QueryResult(); $deferred = new Deferred(); - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn($deferred->promise()); $base->expects($this->once())->method('ping')->willReturn(new Promise(function () { })); @@ -406,7 +406,7 @@ public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromU public function testQueryAfterPingWillCancelTimerAgainWhenPingFromUnderlyingConnectionResolved() { - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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('query')->with('SELECT 1')->willReturn(new Promise(function () { })); @@ -432,7 +432,7 @@ public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectio { $error = new \RuntimeException(); - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\reject($error)); $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); @@ -494,7 +494,7 @@ public function testQueryStreamReturnsReadableStreamWhenConnectionIsPending() public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutStartingTimerWhenResolved() { $stream = new ThroughStream(); - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn($stream); $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); @@ -520,7 +520,7 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutSt public function testQueryStreamWillReturnStreamFromUnderlyingConnectionAndStartTimerWhenResolvedAndClosed() { $stream = new ThroughStream(); - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn($stream); $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); @@ -590,7 +590,7 @@ public function testPingReturnsPendingPromiseWhenConnectionIsPending() public function testPingWillPingUnderlyingConnectionWhenResolved() { - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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(); @@ -647,7 +647,7 @@ public function testPingWillTryToCreateNewUnderlyingConnectionAfterPreviousPingF public function testPingWillResolveAndStartTimerWhenPingFromUnderlyingConnectionResolves() { - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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(); @@ -670,7 +670,7 @@ public function testPingWillRejectAndStartTimerWhenPingFromUnderlyingConnectionR { $error = new \RuntimeException(); - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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(); @@ -759,7 +759,7 @@ public function testQuitAfterPingReturnsPendingPromiseWhenConnectionIsPending() public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved() { - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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 () { })); @@ -779,7 +779,7 @@ public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved() public function testQuitAfterPingResolvesAndEmitsCloseWhenUnderlyingConnectionQuits() { - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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(\React\Promise\resolve(null)); @@ -805,7 +805,7 @@ public function testQuitAfterPingResolvesAndEmitsCloseWhenUnderlyingConnectionQu public function testQuitAfterPingRejectsAndEmitsCloseWhenUnderlyingConnectionFailsToQuit() { $error = new \RuntimeException(); - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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(\React\Promise\reject($error)); @@ -865,7 +865,7 @@ public function testCloseAfterPingCancelsPendingConnection() public function testCloseTwiceAfterPingWillCloseUnderlyingConnectionWhenResolved() { - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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'); @@ -912,7 +912,7 @@ public function testCloseAfterPingDoesNotEmitConnectionErrorFromAbortedConnectio public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectionResolves() { - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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(); @@ -958,7 +958,7 @@ public function testCloseAfterPingHasResolvedWillCloseUnderlyingConnectionWithou public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuitIsStillPending() { - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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'); @@ -980,7 +980,7 @@ public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuit public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionWhenQuitIsStillPending() { - $base = $this->getMockBuilder('React\MySQL\ConnectionInterface')->getMock(); + $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'); From 1cd32d46c1cd78aeb500e397e55b114970dde055 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 21 Nov 2023 17:24:57 +0100 Subject: [PATCH 26/32] Rename `QueryResult` to `MysqlResult` --- README.md | 8 ++-- examples/01-query.php | 2 +- examples/11-interactive.php | 2 +- src/Io/Connection.php | 6 +-- src/MysqlClient.php | 10 ++--- src/{QueryResult.php => MysqlResult.php} | 2 +- tests/MysqlClientTest.php | 10 ++--- tests/NoResultQueryTest.php | 10 ++--- tests/ResultQueryTest.php | 48 ++++++++++++------------ 9 files changed, 49 insertions(+), 49 deletions(-) rename src/{QueryResult.php => MysqlResult.php} (96%) diff --git a/README.md b/README.md index 90a6a7b..c64598a 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ require __DIR__ . '/vendor/autoload.php'; $mysql = new React\MySQL\MysqlClient('user:pass@localhost/bookstore'); $mysql->query('SELECT * FROM book')->then( - function (React\MySQL\QueryResult $command) { + function (React\MySQL\MysqlResult $command) { print_r($command->resultFields); print_r($command->resultRows); echo count($command->resultRows) . ' row(s) in set' . PHP_EOL; @@ -202,10 +202,10 @@ given event loop instance. #### query() -The `query(string $query, array $params = []): PromiseInterface` method can be used to +The `query(string $query, array $params = []): PromiseInterface` method can be used to perform an async query. -This method returns a promise that will resolve with a `QueryResult` on +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 @@ -225,7 +225,7 @@ 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 (QueryResult $command) { +$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); diff --git a/examples/01-query.php b/examples/01-query.php index 849bdb5..fbf66be 100644 --- a/examples/01-query.php +++ b/examples/01-query.php @@ -8,7 +8,7 @@ $mysql = new React\MySQL\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); $query = isset($argv[1]) ? $argv[1] : 'select * from book'; -$mysql->query($query)->then(function (React\MySQL\QueryResult $command) { +$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); diff --git a/examples/11-interactive.php b/examples/11-interactive.php index 2e92f4c..fca5203 100644 --- a/examples/11-interactive.php +++ b/examples/11-interactive.php @@ -25,7 +25,7 @@ } $time = microtime(true); - $mysql->query($query)->then(function (React\MySQL\QueryResult $command) use ($time) { + $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; diff --git a/src/Io/Connection.php b/src/Io/Connection.php index fc71c6c..d579163 100644 --- a/src/Io/Connection.php +++ b/src/Io/Connection.php @@ -8,7 +8,7 @@ use React\MySQL\Commands\QueryCommand; use React\MySQL\Commands\QuitCommand; use React\MySQL\Exception; -use React\MySQL\QueryResult; +use React\MySQL\MysqlResult; use React\Promise\Deferred; use React\Promise\Promise; use React\Socket\ConnectionInterface as SocketConnectionInterface; @@ -79,7 +79,7 @@ public function query($sql, array $params = []) $rows[] = $row; }); $command->on('end', function () use ($command, $deferred, &$rows) { - $result = new QueryResult(); + $result = new MysqlResult(); $result->resultFields = $command->fields; $result->resultRows = $rows; $result->warningCount = $command->warningCount; @@ -94,7 +94,7 @@ public function query($sql, array $params = []) $deferred->reject($error); }); $command->on('success', function () use ($command, $deferred) { - $result = new QueryResult(); + $result = new MysqlResult(); $result->affectedRows = $command->affectedRows; $result->insertId = $command->insertId; $result->warningCount = $command->warningCount; diff --git a/src/MysqlClient.php b/src/MysqlClient.php index c96d300..8ea23e4 100644 --- a/src/MysqlClient.php +++ b/src/MysqlClient.php @@ -151,7 +151,7 @@ function () use ($connection) { /** * Performs an async query. * - * This method returns a promise that will resolve with a `QueryResult` on + * 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 @@ -171,7 +171,7 @@ function () use ($connection) { * [`queryStream()`](#querystream) method instead. * * ```php - * $mysql->query($query)->then(function (QueryResult $command) { + * $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); @@ -204,8 +204,8 @@ function () use ($connection) { * * @param string $sql SQL statement * @param array $params Parameters which should be bound to query - * @return PromiseInterface - * Resolves with a `QueryResult` on success or rejects with an `Exception` on error. + * @return PromiseInterface + * Resolves with a `MysqlResult` on success or rejects with an `Exception` on error. */ public function query($sql, array $params = []) { @@ -216,7 +216,7 @@ public function query($sql, array $params = []) return $this->connecting()->then(function (Connection $connection) use ($sql, $params) { $this->awake(); return $connection->query($sql, $params)->then( - function (QueryResult $result) { + function (MysqlResult $result) { $this->idle(); return $result; }, diff --git a/src/QueryResult.php b/src/MysqlResult.php similarity index 96% rename from src/QueryResult.php rename to src/MysqlResult.php index 7197bb5..86c886c 100644 --- a/src/QueryResult.php +++ b/src/MysqlResult.php @@ -2,7 +2,7 @@ namespace React\MySQL; -class QueryResult +class MysqlResult { /** * last inserted ID (if any) diff --git a/tests/MysqlClientTest.php b/tests/MysqlClientTest.php index c3923ea..d16eade 100644 --- a/tests/MysqlClientTest.php +++ b/tests/MysqlClientTest.php @@ -4,7 +4,7 @@ use React\MySQL\Io\Connection; use React\MySQL\MysqlClient; -use React\MySQL\QueryResult; +use React\MySQL\MysqlResult; use React\Promise\Deferred; use React\Promise\Promise; use React\Promise\PromiseInterface; @@ -308,7 +308,7 @@ public function testQueryWillQueryUnderlyingConnectionWhenResolved() public function testQueryWillResolveAndStartTimerWithDefaultIntervalWhenQueryFromUnderlyingConnectionResolves() { - $result = new QueryResult(); + $result = new MysqlResult(); $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); @@ -331,7 +331,7 @@ public function testQueryWillResolveAndStartTimerWithDefaultIntervalWhenQueryFro public function testQueryWillResolveAndStartTimerWithIntervalFromIdleParameterWhenQueryFromUnderlyingConnectionResolves() { - $result = new QueryResult(); + $result = new MysqlResult(); $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); @@ -354,7 +354,7 @@ public function testQueryWillResolveAndStartTimerWithIntervalFromIdleParameterWh public function testQueryWillResolveWithoutStartingTimerWhenQueryFromUnderlyingConnectionResolvesAndIdleParameterIsNegative() { - $result = new QueryResult(); + $result = new MysqlResult(); $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); @@ -377,7 +377,7 @@ public function testQueryWillResolveWithoutStartingTimerWhenQueryFromUnderlyingC public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromUnderlyingConnectionResolvesBecausePingIsStillPending() { - $result = new QueryResult(); + $result = new MysqlResult(); $deferred = new Deferred(); $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); diff --git a/tests/NoResultQueryTest.php b/tests/NoResultQueryTest.php index 75c8f98..e129c5c 100644 --- a/tests/NoResultQueryTest.php +++ b/tests/NoResultQueryTest.php @@ -4,7 +4,7 @@ use React\EventLoop\Loop; use React\MySQL\MysqlClient; -use React\MySQL\QueryResult; +use React\MySQL\MysqlResult; class NoResultQueryTest extends BaseTestCase { @@ -27,7 +27,7 @@ public function testUpdateSimpleNonExistentReportsNoAffectedRows() { $connection = $this->createConnection(Loop::get()); - $connection->query('update book set created=999 where id=999')->then(function (QueryResult $command) { + $connection->query('update book set created=999 where id=999')->then(function (MysqlResult $command) { $this->assertEquals(0, $command->affectedRows); }); @@ -39,7 +39,7 @@ public function testInsertSimpleReportsFirstInsertId() { $connection = $this->createConnection(Loop::get()); - $connection->query("insert into book (`name`) values ('foo')")->then(function (QueryResult $command) { + $connection->query("insert into book (`name`) values ('foo')")->then(function (MysqlResult $command) { $this->assertEquals(1, $command->affectedRows); $this->assertEquals(1, $command->insertId); }); @@ -53,7 +53,7 @@ public function testUpdateSimpleReportsAffectedRow() $connection = $this->createConnection(Loop::get()); $connection->query("insert into book (`name`) values ('foo')"); - $connection->query('update book set created=999 where id=1')->then(function (QueryResult $command) { + $connection->query('update book set created=999 where id=1')->then(function (MysqlResult $command) { $this->assertEquals(1, $command->affectedRows); }); @@ -75,7 +75,7 @@ public function testCreateTableAgainWillAddWarning() PRIMARY KEY (`id`) )'; - $connection->query($sql)->then(function (QueryResult $command) { + $connection->query($sql)->then(function (MysqlResult $command) { $this->assertEquals(1, $command->warningCount); }); diff --git a/tests/ResultQueryTest.php b/tests/ResultQueryTest.php index 768a9d5..8d4c3be 100644 --- a/tests/ResultQueryTest.php +++ b/tests/ResultQueryTest.php @@ -5,7 +5,7 @@ use React\EventLoop\Loop; use React\MySQL\Io\Constants; use React\MySQL\MysqlClient; -use React\MySQL\QueryResult; +use React\MySQL\MysqlResult; class ResultQueryTest extends BaseTestCase { @@ -13,7 +13,7 @@ public function testSelectStaticText() { $connection = $this->createConnection(Loop::get()); - $connection->query('select \'foo\'')->then(function (QueryResult $command) { + $connection->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])); @@ -51,7 +51,7 @@ public function testSelectStaticValueWillBeReturnedAsIs($value) $expected = $value; - $connection->query('select ?', [$value])->then(function (QueryResult $command) use ($expected) { + $connection->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])); @@ -76,7 +76,7 @@ public function testSelectStaticValueWillBeReturnedAsIsWithNoBackslashEscapesSql $expected = $value; $connection->query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); - $connection->query('select ?', [$value])->then(function (QueryResult $command) use ($expected) { + $connection->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])); @@ -103,7 +103,7 @@ public function testSelectStaticValueWillBeConvertedToString($value, $expected) { $connection = $this->createConnection(Loop::get()); - $connection->query('select ?', [$value])->then(function (QueryResult $command) use ($expected) { + $connection->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])); @@ -117,7 +117,7 @@ public function testSelectStaticTextWithQuestionMark() { $connection = $this->createConnection(Loop::get()); - $connection->query('select \'hello?\'')->then(function (QueryResult $command) { + $connection->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])); @@ -134,7 +134,7 @@ public function testSelectLongStaticTextHasTypeStringWithValidLength() $length = 40000; $value = str_repeat('.', $length); - $connection->query('SELECT ?', [$value])->then(function (QueryResult $command) use ($length) { + $connection->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']); @@ -148,7 +148,7 @@ public function testSelectStaticTextWithEmptyLabel() { $connection = $this->createConnection(Loop::get()); - $connection->query('select \'foo\' as ``')->then(function (QueryResult $command) { + $connection->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])); @@ -166,7 +166,7 @@ public function testSelectStaticNullHasTypeNull() { $connection = $this->createConnection(Loop::get()); - $connection->query('select null')->then(function (QueryResult $command) { + $connection->query('select null')->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); $this->assertNull(reset($command->resultRows[0])); @@ -183,7 +183,7 @@ public function testSelectStaticTextTwoRows() { $connection = $this->createConnection(Loop::get()); - $connection->query('select "foo" UNION select "bar"')->then(function (QueryResult $command) { + $connection->query('select "foo" UNION select "bar"')->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); @@ -199,7 +199,7 @@ public function testSelectStaticTextTwoRowsWithNullHasTypeString() { $connection = $this->createConnection(Loop::get()); - $connection->query('select "foo" UNION select null')->then(function (QueryResult $command) { + $connection->query('select "foo" UNION select null')->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); @@ -218,7 +218,7 @@ public function testSelectStaticIntegerTwoRowsWithNullHasTypeLongButReturnsIntAs { $connection = $this->createConnection(Loop::get()); - $connection->query('select 0 UNION select null')->then(function (QueryResult $command) { + $connection->query('select 0 UNION select null')->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); @@ -237,7 +237,7 @@ public function testSelectStaticTextTwoRowsWithIntegerHasTypeString() { $connection = $this->createConnection(Loop::get()); - $connection->query('select "foo" UNION select 1')->then(function (QueryResult $command) { + $connection->query('select "foo" UNION select 1')->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); @@ -256,7 +256,7 @@ public function testSelectStaticTextTwoRowsWithEmptyRow() { $connection = $this->createConnection(Loop::get()); - $connection->query('select "foo" UNION select ""')->then(function (QueryResult $command) { + $connection->query('select "foo" UNION select ""')->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); @@ -272,7 +272,7 @@ public function testSelectStaticTextNoRows() { $connection = $this->createConnection(Loop::get()); - $connection->query('select "foo" LIMIT 0')->then(function (QueryResult $command) { + $connection->query('select "foo" LIMIT 0')->then(function (MysqlResult $command) { $this->assertCount(0, $command->resultRows); $this->assertCount(1, $command->resultFields); @@ -287,7 +287,7 @@ public function testSelectStaticTextTwoColumns() { $connection = $this->createConnection(Loop::get()); - $connection->query('select "foo","bar"')->then(function (QueryResult $command) { + $connection->query('select "foo","bar"')->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(2, $command->resultRows[0]); @@ -303,7 +303,7 @@ public function testSelectStaticTextTwoColumnsWithOneEmptyColumn() { $connection = $this->createConnection(Loop::get()); - $connection->query('select "foo",""')->then(function (QueryResult $command) { + $connection->query('select "foo",""')->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(2, $command->resultRows[0]); @@ -319,7 +319,7 @@ public function testSelectStaticTextTwoColumnsWithBothEmpty() { $connection = $this->createConnection(Loop::get()); - $connection->query('select \'\' as `first`, \'\' as `second`')->then(function (QueryResult $command) { + $connection->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])); @@ -337,7 +337,7 @@ public function testSelectStaticTextTwoColumnsWithSameNameOverwritesValue() { $connection = $this->createConnection(Loop::get()); - $connection->query('select "foo" as `col`,"bar" as `col`')->then(function (QueryResult $command) { + $connection->query('select "foo" as `col`,"bar" as `col`')->then(function (MysqlResult $command) { $this->assertCount(1, $command->resultRows); $this->assertCount(1, $command->resultRows[0]); @@ -356,7 +356,7 @@ public function testSelectCharsetDefaultsToUtf8() { $connection = $this->createConnection(Loop::get()); - $connection->query('SELECT @@character_set_client')->then(function (QueryResult $command) { + $connection->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])); @@ -371,7 +371,7 @@ public function testSelectWithExplicitCharsetReturnsCharset() $uri = $this->getConnectionString() . '?charset=latin1'; $connection = new MysqlClient($uri); - $connection->query('SELECT @@character_set_client')->then(function (QueryResult $command) { + $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])); @@ -391,7 +391,7 @@ public function testSimpleSelect() $connection->query("insert into book (`name`) values ('foo')"); $connection->query("insert into book (`name`) values ('bar')"); - $connection->query('select * from book')->then(function (QueryResult $command) { + $connection->query('select * from book')->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); }); @@ -407,7 +407,7 @@ public function testSimpleSelectFromMysqlClientWithoutDatabaseNameReturnsSameDat $uri = $this->getConnectionString(['dbname' => '']); $connection = new MysqlClient($uri); - $connection->query('select * from test.book')->then(function (QueryResult $command) { + $connection->query('select * from test.book')->then(function (MysqlResult $command) { $this->assertCount(2, $command->resultRows); }); @@ -459,7 +459,7 @@ public function testSelectAfterDelay() $connection = $this->createConnection(Loop::get()); Loop::addTimer(0.1, function () use ($connection) { - $connection->query('select 1+1')->then(function (QueryResult $command) { + $connection->query('select 1+1')->then(function (MysqlResult $command) { $this->assertEquals([['1+1' => 2]], $command->resultRows); }); $connection->quit(); From e9429fb81d61994544727f1a2f8f89df45baa838 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Fri, 24 Nov 2023 09:02:42 +0100 Subject: [PATCH 27/32] Rename `MySQL` namespace to `Mysql` --- README.md | 24 ++-- composer.json | 4 +- examples/01-query.php | 4 +- examples/02-query-stream.php | 2 +- examples/11-interactive.php | 4 +- examples/12-slow-stream.php | 4 +- src/Commands/AbstractCommand.php | 2 +- src/Commands/AuthenticateCommand.php | 8 +- src/Commands/CommandInterface.php | 2 +- src/Commands/PingCommand.php | 2 +- src/Commands/QueryCommand.php | 4 +- src/Commands/QuitCommand.php | 2 +- src/Exception.php | 2 +- src/Io/Buffer.php | 2 +- src/Io/Connection.php | 16 +-- src/Io/Constants.php | 2 +- src/Io/Executor.php | 2 +- src/Io/Factory.php | 12 +- src/Io/Parser.php | 12 +- src/Io/Query.php | 4 +- src/Io/QueryStream.php | 4 +- src/MysqlClient.php | 8 +- src/MysqlResult.php | 2 +- tests/BaseTestCase.php | 6 +- tests/Commands/AuthenticateCommandTest.php | 4 +- tests/Io/BufferTest.php | 6 +- tests/Io/ConnectionTest.php | 20 +-- tests/Io/FactoryTest.php | 8 +- tests/Io/ParserTest.php | 12 +- tests/Io/QueryStreamTest.php | 10 +- tests/Io/QueryTest.php | 4 +- tests/MysqlClientTest.php | 156 ++++++++++----------- tests/NoResultQueryTest.php | 6 +- tests/ResultQueryTest.php | 8 +- 34 files changed, 185 insertions(+), 183 deletions(-) diff --git a/README.md b/README.md index c64598a..f296c40 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,10 @@ This example runs a simple `SELECT` query and dumps all the records from a `book require __DIR__ . '/vendor/autoload.php'; -$mysql = new React\MySQL\MysqlClient('user:pass@localhost/bookstore'); +$mysql = new React\Mysql\MysqlClient('user:pass@localhost/bookstore'); $mysql->query('SELECT * FROM book')->then( - function (React\MySQL\MysqlResult $command) { + function (React\Mysql\MysqlResult $command) { print_r($command->resultFields); print_r($command->resultRows); echo count($command->resultRows) . ' row(s) in set' . PHP_EOL; @@ -68,7 +68,7 @@ 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 = new React\Mysql\MysqlClient($uri); $mysql->query(…); ``` @@ -114,7 +114,7 @@ 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'); +$mysql = new React\Mysql\MysqlClient('user:secret@localhost:3306/database'); ``` Note that both the username and password must be URL-encoded (percent-encoded) @@ -124,7 +124,7 @@ if they contain special characters: $user = 'he:llo'; $pass = 'p@ss'; -$mysql = new React\MySQL\MysqlClient( +$mysql = new React\Mysql\MysqlClient( rawurlencode($user) . ':' . rawurlencode($pass) . '@localhost:3306/db' ); ``` @@ -132,7 +132,7 @@ $mysql = new React\MySQL\MysqlClient( You can omit the port if you're connecting to default port `3306`: ```php -$mysql = new React\MySQL\MysqlClient('user:secret@localhost/database'); +$mysql = new React\Mysql\MysqlClient('user:secret@localhost/database'); ``` If you do not include authentication and/or database, then this method @@ -141,7 +141,7 @@ 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'); +$mysql = new React\Mysql\MysqlClient('localhost'); ``` This method respects PHP's `default_socket_timeout` setting (default 60s) @@ -150,7 +150,7 @@ 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'); +$mysql = new React\Mysql\MysqlClient('localhost?timeout=0.5'); ``` By default, idle connections will be held open for 1ms (0.001s) when not @@ -163,7 +163,7 @@ 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'); +$mysql = new React\Mysql\MysqlClient('localhost?idle=10.0'); ``` By default, the connection provides full UTF-8 support (using the @@ -172,7 +172,7 @@ 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'); +$mysql = new React\Mysql\MysqlClient('localhost?charset=utf8mb4'); ``` If you need custom connector settings (DNS resolution, TLS parameters, timeouts, @@ -191,7 +191,7 @@ $connector = new React\Socket\Connector([ ) ]); -$mysql = new React\MySQL\MysqlClient('user:secret@localhost:3306/database', $connector); +$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 @@ -225,7 +225,7 @@ 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) { +$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); diff --git a/composer.json b/composer.json index 7f00983..5b7824c 100644 --- a/composer.json +++ b/composer.json @@ -18,12 +18,12 @@ }, "autoload": { "psr-4": { - "React\\MySQL\\": "src/" + "React\\Mysql\\": "src/" } }, "autoload-dev": { "psr-4": { - "React\\Tests\\MySQL\\": "tests/" + "React\\Tests\\Mysql\\": "tests/" } } } diff --git a/examples/01-query.php b/examples/01-query.php index fbf66be..ef40c4b 100644 --- a/examples/01-query.php +++ b/examples/01-query.php @@ -5,10 +5,10 @@ require __DIR__ . '/../vendor/autoload.php'; -$mysql = new React\MySQL\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); +$mysql = new React\Mysql\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); $query = isset($argv[1]) ? $argv[1] : 'select * from book'; -$mysql->query($query)->then(function (React\MySQL\MysqlResult $command) { +$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); diff --git a/examples/02-query-stream.php b/examples/02-query-stream.php index 1562603..b96ed74 100644 --- a/examples/02-query-stream.php +++ b/examples/02-query-stream.php @@ -5,7 +5,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$mysql = new React\MySQL\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); +$mysql = new React\Mysql\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); $query = isset($argv[1]) ? $argv[1] : 'select * from book'; $stream = $mysql->queryStream($query); diff --git a/examples/11-interactive.php b/examples/11-interactive.php index fca5203..dfca10c 100644 --- a/examples/11-interactive.php +++ b/examples/11-interactive.php @@ -5,7 +5,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$mysql = new React\MySQL\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); +$mysql = new React\Mysql\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); // open a STDIN stream to read keyboard input (not supported on Windows) $stdin = new React\Stream\ReadableResourceStream(STDIN); @@ -25,7 +25,7 @@ } $time = microtime(true); - $mysql->query($query)->then(function (React\MySQL\MysqlResult $command) use ($time) { + $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; diff --git a/examples/12-slow-stream.php b/examples/12-slow-stream.php index b61c6f8..bf46e9a 100644 --- a/examples/12-slow-stream.php +++ b/examples/12-slow-stream.php @@ -7,7 +7,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$mysql = new React\MySQL\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); +$mysql = new React\Mysql\MysqlClient(getenv('MYSQL_URI') ?: 'test:test@localhost/test'); $query = isset($argv[1]) ? $argv[1] : 'select * from book'; $stream = $mysql->queryStream($query); @@ -17,7 +17,7 @@ $promise = $ref->getValue($mysql); assert($promise instanceof React\Promise\PromiseInterface); -$promise->then(function (React\MySQL\Io\Connection $connection) { +$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 diff --git a/src/Commands/AbstractCommand.php b/src/Commands/AbstractCommand.php index 6f9aaa0..882576b 100644 --- a/src/Commands/AbstractCommand.php +++ b/src/Commands/AbstractCommand.php @@ -1,6 +1,6 @@ * @see self::$charsetNumber - * @see \React\MySQL\Io\Query::$escapeChars + * @see \React\Mysql\Io\Query::$escapeChars */ private static $charsetMap = [ 'latin1' => 8, diff --git a/src/Commands/CommandInterface.php b/src/Commands/CommandInterface.php index 32a82de..310279c 100644 --- a/src/Commands/CommandInterface.php +++ b/src/Commands/CommandInterface.php @@ -1,6 +1,6 @@ - * @see \React\MySQL\Commands\AuthenticateCommand::$charsetMap + * @see \React\Mysql\Commands\AuthenticateCommand::$charsetMap */ private $escapeChars = [ //"\x00" => "\\0", diff --git a/src/Io/QueryStream.php b/src/Io/QueryStream.php index 1b95734..dceb90a 100644 --- a/src/Io/QueryStream.php +++ b/src/Io/QueryStream.php @@ -1,9 +1,9 @@ query($query)->then(function (React\MySQL\MysqlResult $command) { + * $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); diff --git a/src/MysqlResult.php b/src/MysqlResult.php index 86c886c..e0da9af 100644 --- a/src/MysqlResult.php +++ b/src/MysqlResult.php @@ -1,6 +1,6 @@ getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $conn = new Connection($stream, $executor); @@ -20,7 +20,7 @@ public function testQuitWillEnqueueOneCommand() public function testQueryAfterQuitRejectsImmediately() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $conn = new Connection($stream, $executor); @@ -43,7 +43,7 @@ public function testQueryAfterQuitRejectsImmediately() public function testQueryAfterCloseRejectsImmediately() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->never())->method('enqueue'); $conn = new Connection($stream, $executor); @@ -66,7 +66,7 @@ public function testQueryAfterCloseRejectsImmediately() public function testQueryStreamAfterQuitThrows() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $conn = new Connection($stream, $executor); @@ -83,7 +83,7 @@ public function testQueryStreamAfterQuitThrows() public function testPingAfterQuitRejectsImmediately() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $conn = new Connection($stream, $executor); @@ -106,7 +106,7 @@ public function testPingAfterQuitRejectsImmediately() public function testQuitAfterQuitRejectsImmediately() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); $conn = new Connection($stream, $executor); @@ -137,7 +137,7 @@ public function testCloseStreamEmitsErrorEvent() return true; })) ); - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->never())->method('enqueue'); $conn = new Connection($stream, $executor); diff --git a/tests/Io/FactoryTest.php b/tests/Io/FactoryTest.php index 8757592..6675abd 100644 --- a/tests/Io/FactoryTest.php +++ b/tests/Io/FactoryTest.php @@ -1,13 +1,13 @@ on('error', $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); $stream->on('close', $this->expectCallableOnce()); - $command->emit('error', [new RuntimeException()]); + $command->emit('error', [new \RuntimeException()]); } public function testPauseForwardsToConnectionAfterResultStarted() diff --git a/tests/Io/QueryTest.php b/tests/Io/QueryTest.php index 3a5831f..420c650 100644 --- a/tests/Io/QueryTest.php +++ b/tests/Io/QueryTest.php @@ -1,9 +1,9 @@ getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -93,10 +93,10 @@ public function testPingWillNotCloseConnectionWhenPendingConnectionFails() public function testPingWillNotCloseConnectionWhenUnderlyingConnectionCloses() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); + $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 = $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(); @@ -117,10 +117,10 @@ public function testPingWillNotCloseConnectionWhenUnderlyingConnectionCloses() public function testPingWillCancelTimerWithoutClosingConnectionWhenUnderlyingConnectionCloses() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); + $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 = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); @@ -144,10 +144,10 @@ public function testPingWillCancelTimerWithoutClosingConnectionWhenUnderlyingCon public function testPingWillNotForwardErrorFromUnderlyingConnection() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->setMethods(['ping'])->disableOriginalConstructor()->getMock(); + $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 = $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(); @@ -167,12 +167,12 @@ public function testPingWillNotForwardErrorFromUnderlyingConnection() public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); + $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->once())->method('quit')->willReturn(\React\Promise\resolve(null)); $base->expects($this->never())->method('close'); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timeout = null; @@ -199,12 +199,12 @@ public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection() public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuitFails() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); + $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->once())->method('quit')->willReturn(\React\Promise\reject(new \RuntimeException())); $base->expects($this->once())->method('close'); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timeout = null; @@ -231,12 +231,12 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuit public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatingSecondConnection() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); + $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->once())->method('quit')->willReturn(new Promise(function () { })); $base->expects($this->once())->method('close'); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->exactly(2))->method('createConnection')->willReturnOnConsecutiveCalls( \React\Promise\resolve($base), new Promise(function () { }) @@ -270,7 +270,7 @@ public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatin public function testQueryReturnsPendingPromiseAndWillNotStartTimerWhenConnectionIsPending() { $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -290,10 +290,10 @@ public function testQueryReturnsPendingPromiseAndWillNotStartTimerWhenConnection public function testQueryWillQueryUnderlyingConnectionWhenResolved() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -310,10 +310,10 @@ public function testQueryWillResolveAndStartTimerWithDefaultIntervalWhenQueryFro { $result = new MysqlResult(); - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -333,10 +333,10 @@ public function testQueryWillResolveAndStartTimerWithIntervalFromIdleParameterWh { $result = new MysqlResult(); - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -356,10 +356,10 @@ public function testQueryWillResolveWithoutStartingTimerWhenQueryFromUnderlyingC { $result = new MysqlResult(); - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\resolve($result)); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -380,11 +380,11 @@ public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromU $result = new MysqlResult(); $deferred = new Deferred(); - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('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 = $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(); @@ -406,11 +406,11 @@ public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromU public function testQueryAfterPingWillCancelTimerAgainWhenPingFromUnderlyingConnectionResolved() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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('query')->with('SELECT 1')->willReturn(new Promise(function () { })); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); @@ -432,10 +432,10 @@ public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectio { $error = new \RuntimeException(); - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(\React\Promise\reject($error)); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -454,7 +454,7 @@ public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectio public function testQueryWillRejectWithoutStartingTimerWhenUnderlyingConnectionRejects() { $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -475,7 +475,7 @@ public function testQueryWillRejectWithoutStartingTimerWhenUnderlyingConnectionR public function testQueryStreamReturnsReadableStreamWhenConnectionIsPending() { $promise = new Promise(function () { }); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($promise); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -494,10 +494,10 @@ public function testQueryStreamReturnsReadableStreamWhenConnectionIsPending() public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutStartingTimerWhenResolved() { $stream = new ThroughStream(); - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn($stream); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -520,10 +520,10 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutSt public function testQueryStreamWillReturnStreamFromUnderlyingConnectionAndStartTimerWhenResolvedAndClosed() { $stream = new ThroughStream(); - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('queryStream')->with('SELECT 1')->willReturn($stream); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -549,7 +549,7 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionAndStartT public function testQueryStreamWillCloseStreamWithErrorWhenUnderlyingConnectionRejects() { $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -572,7 +572,7 @@ public function testQueryStreamWillCloseStreamWithErrorWhenUnderlyingConnectionR public function testPingReturnsPendingPromiseWhenConnectionIsPending() { $promise = new Promise(function () { }); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($promise); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -590,10 +590,10 @@ public function testPingReturnsPendingPromiseWhenConnectionIsPending() public function testPingWillPingUnderlyingConnectionWhenResolved() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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 = $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(); @@ -611,7 +611,7 @@ public function testPingTwiceWillBothRejectWithSameErrorWhenUnderlyingConnection $error = new \RuntimeException(); $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -631,7 +631,7 @@ public function testPingWillTryToCreateNewUnderlyingConnectionAfterPreviousPingF { $error = new \RuntimeException(); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -647,10 +647,10 @@ public function testPingWillTryToCreateNewUnderlyingConnectionAfterPreviousPingF public function testPingWillResolveAndStartTimerWhenPingFromUnderlyingConnectionResolves() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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 = $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(); @@ -670,10 +670,10 @@ public function testPingWillRejectAndStartTimerWhenPingFromUnderlyingConnectionR { $error = new \RuntimeException(); - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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 = $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(); @@ -693,14 +693,14 @@ public function testPingWillRejectAndNotStartIdleTimerWhenPingFromUnderlyingConn { $error = new \RuntimeException(); - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); + $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 = $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(); @@ -718,7 +718,7 @@ public function testPingWillRejectAndNotStartIdleTimerWhenPingFromUnderlyingConn public function testQuitResolvesAndEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() { - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -740,7 +740,7 @@ public function testQuitResolvesAndEmitsCloseImmediatelyWhenConnectionIsNotAlrea public function testQuitAfterPingReturnsPendingPromiseWhenConnectionIsPending() { $promise = new Promise(function () { }); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($promise); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -759,11 +759,11 @@ public function testQuitAfterPingReturnsPendingPromiseWhenConnectionIsPending() public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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 = $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(); @@ -779,11 +779,11 @@ public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved() public function testQuitAfterPingResolvesAndEmitsCloseWhenUnderlyingConnectionQuits() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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(\React\Promise\resolve(null)); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -805,11 +805,11 @@ public function testQuitAfterPingResolvesAndEmitsCloseWhenUnderlyingConnectionQu public function testQuitAfterPingRejectsAndEmitsCloseWhenUnderlyingConnectionFailsToQuit() { $error = new \RuntimeException(); - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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(\React\Promise\reject($error)); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -830,7 +830,7 @@ public function testQuitAfterPingRejectsAndEmitsCloseWhenUnderlyingConnectionFai public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() { - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -849,7 +849,7 @@ public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending( public function testCloseAfterPingCancelsPendingConnection() { $deferred = new Deferred($this->expectCallableOnce()); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -865,11 +865,11 @@ public function testCloseAfterPingCancelsPendingConnection() public function testCloseTwiceAfterPingWillCloseUnderlyingConnectionWhenResolved() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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 = $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(); @@ -890,7 +890,7 @@ public function testCloseAfterPingDoesNotEmitConnectionErrorFromAbortedConnectio throw new \RuntimeException(); }); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($promise); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -912,10 +912,10 @@ public function testCloseAfterPingDoesNotEmitConnectionErrorFromAbortedConnectio public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectionResolves() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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 = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); @@ -935,13 +935,13 @@ public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectio public function testCloseAfterPingHasResolvedWillCloseUnderlyingConnectionWithoutTryingToCancelConnection() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->setMethods(['ping', 'close'])->disableOriginalConstructor()->getMock(); + $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')->willReturnCallback(function () use ($base) { $base->emit('close'); }); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -958,12 +958,12 @@ public function testCloseAfterPingHasResolvedWillCloseUnderlyingConnectionWithou public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuitIsStillPending() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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 = $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(); @@ -980,12 +980,12 @@ public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuit public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionWhenQuitIsStillPending() { - $base = $this->getMockBuilder('React\MySQL\Io\Connection')->disableOriginalConstructor()->getMock(); + $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 = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $timeout = null; @@ -1013,7 +1013,7 @@ public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionW public function testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnectionIsPending() { $promise = new Promise(function () { }); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->once())->method('createConnection')->willReturn($promise); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -1033,7 +1033,7 @@ public function testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnectionIsPendin public function testQueryReturnsRejectedPromiseAfterConnectionIsClosed() { - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -1052,7 +1052,7 @@ public function testQueryReturnsRejectedPromiseAfterConnectionIsClosed() public function testQueryStreamThrowsAfterConnectionIsClosed() { - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -1064,13 +1064,13 @@ public function testQueryStreamThrowsAfterConnectionIsClosed() $connection->close(); - $this->setExpectedException('React\MySQL\Exception'); + $this->setExpectedException('React\Mysql\Exception'); $connection->queryStream('SELECT 1'); } public function testPingReturnsRejectedPromiseAfterConnectionIsClosed() { - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); @@ -1089,7 +1089,7 @@ public function testPingReturnsRejectedPromiseAfterConnectionIsClosed() public function testQuitReturnsRejectedPromiseAfterConnectionIsClosed() { - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); $factory->expects($this->never())->method('createConnection'); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); diff --git a/tests/NoResultQueryTest.php b/tests/NoResultQueryTest.php index e129c5c..0789ed8 100644 --- a/tests/NoResultQueryTest.php +++ b/tests/NoResultQueryTest.php @@ -1,10 +1,10 @@ Date: Fri, 24 Nov 2023 15:25:31 +0100 Subject: [PATCH 28/32] Consistently emit close event after quit even if server rejects --- README.md | 5 +++ src/Io/Connection.php | 19 +++++---- src/MysqlClient.php | 43 +++++++++++++------- tests/Io/ConnectionTest.php | 66 ++++++++++++++++++++++++++++++ tests/MysqlClientTest.php | 81 ++++++++++++++++++++++++++++--------- tests/NoResultQueryTest.php | 27 +++++++++++-- 6 files changed, 194 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index f296c40..d81fb86 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,11 @@ $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 diff --git a/src/Io/Connection.php b/src/Io/Connection.php index 4381ca2..8fc2007 100644 --- a/src/Io/Connection.php +++ b/src/Io/Connection.php @@ -135,17 +135,16 @@ public function ping() public function quit() { return new Promise(function ($resolve, $reject) { - $this->_doCommand(new QuitCommand()) - ->on('error', function ($reason) use ($reject) { - $reject($reason); - }) - ->on('success', function () use ($resolve) { - $this->state = self::STATE_CLOSED; - $this->emit('end', [$this]); - $this->emit('close', [$this]); - $resolve(null); - }); + $command = $this->_doCommand(new QuitCommand()); $this->state = self::STATE_CLOSING; + $command->on('success', function () use ($resolve) { + $resolve(null); + $this->close(); + }); + $command->on('error', function ($reason) use ($reject) { + $reject($reason); + $this->close(); + }); }); } diff --git a/src/MysqlClient.php b/src/MysqlClient.php index e087c15..01ac492 100644 --- a/src/MysqlClient.php +++ b/src/MysqlClient.php @@ -7,8 +7,9 @@ use React\EventLoop\LoopInterface; use React\Mysql\Io\Connection; use React\Mysql\Io\Factory; -use React\Stream\ReadableStreamInterface; +use React\Promise\Promise; use React\Socket\ConnectorInterface; +use React\Stream\ReadableStreamInterface; /** * This class represents a connection that is responsible for communicating @@ -135,9 +136,8 @@ function () { // successfully disconnected => remove reference $this->disconnecting = null; }, - function () use ($connection) { - // soft-close failed => force-close connection - $connection->close(); + function () { + // soft-close failed but will close anyway => remove reference $this->disconnecting = null; } ); @@ -361,6 +361,11 @@ function (\Exception $e) { * $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. */ @@ -376,17 +381,25 @@ public function quit() return \React\Promise\resolve(null); } - return $this->connecting()->then(function (Connection $connection) { - $this->awake(); - return $connection->quit()->then( - function () { - $this->close(); - }, - function (\Exception $e) { - $this->close(); - throw $e; - } - ); + return new Promise(function (callable $resolve, callable $reject) { + $this->connecting()->then(function (Connection $connection) use ($resolve, $reject) { + $this->awake(); + // 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(); + }); }); } diff --git a/tests/Io/ConnectionTest.php b/tests/Io/ConnectionTest.php index b73d556..f0cb934 100644 --- a/tests/Io/ConnectionTest.php +++ b/tests/Io/ConnectionTest.php @@ -17,6 +17,72 @@ public function testQuitWillEnqueueOneCommand() $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; + }); + + $connection = new Connection($stream, $executor); + + $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; + }); + + $connection = new Connection($stream, $executor); + + $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 testQueryAfterQuitRejectsImmediately() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); diff --git a/tests/MysqlClientTest.php b/tests/MysqlClientTest.php index 8cc9a8f..5adf222 100644 --- a/tests/MysqlClientTest.php +++ b/tests/MysqlClientTest.php @@ -1,6 +1,6 @@ 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->once())->method('quit')->willReturn(\React\Promise\reject(new \RuntimeException())); - $base->expects($this->once())->method('close'); + $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)); @@ -227,6 +226,15 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWhenQuit $this->assertNotNull($timeout); $timeout(); + + assert($base instanceof Connection); + $base->emit('close'); + + $ref = new \ReflectionProperty($connection, 'connecting'); + $ref->setAccessible(true); + $connecting = $ref->getValue($connection); + + $this->assertNull($connecting); } public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatingSecondConnection() @@ -757,6 +765,32 @@ public function testQuitAfterPingReturnsPendingPromiseWhenConnectionIsPending() $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('', 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(); @@ -777,11 +811,12 @@ public function testQuitAfterPingWillQuitUnderlyingConnectionWhenResolved() $connection->quit(); } - public function testQuitAfterPingResolvesAndEmitsCloseWhenUnderlyingConnectionQuits() + 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(\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)); @@ -793,21 +828,25 @@ public function testQuitAfterPingResolvesAndEmitsCloseWhenUnderlyingConnectionQu $ref->setAccessible(true); $ref->setValue($connection, $factory); - $connection->on('close', $this->expectCallableOnce()); - $connection->ping(); - $ret = $connection->quit(); - $this->assertTrue($ret instanceof PromiseInterface); - $ret->then($this->expectCallableOnce(), $this->expectCallableNever()); + $this->expectOutputString('quit.close.'); + $connection->on('close', function () { + echo 'close.'; + }); + $connection->quit()->then(function () { + echo 'quit.'; + }); + + $deferred->resolve(null); } - public function testQuitAfterPingRejectsAndEmitsCloseWhenUnderlyingConnectionFailsToQuit() + public function testQuitAfterPingRejectsAndThenEmitsCloseWhenUnderlyingConnectionFailsToQuit() { - $error = new \RuntimeException(); + $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(\React\Promise\reject($error)); + $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)); @@ -819,13 +858,17 @@ public function testQuitAfterPingRejectsAndEmitsCloseWhenUnderlyingConnectionFai $ref->setAccessible(true); $ref->setValue($connection, $factory); - $connection->on('close', $this->expectCallableOnce()); - $connection->ping(); - $ret = $connection->quit(); - $this->assertTrue($ret instanceof PromiseInterface); - $ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); + $this->expectOutputString('reject.close.'); + $connection->on('close', function () { + echo 'close.'; + }); + $connection->quit()->then(null, function () { + echo 'reject.'; + }); + + $deferred->reject(new \RuntimeException()); } public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() diff --git a/tests/NoResultQueryTest.php b/tests/NoResultQueryTest.php index 0789ed8..efccb7d 100644 --- a/tests/NoResultQueryTest.php +++ b/tests/NoResultQueryTest.php @@ -133,14 +133,35 @@ public function testPingWithValidAuthWillRunUntilQuitAfterPing() Loop::run(); } - /** - * @doesNotPerformAssertions - */ + 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(); From 7c3bd22d5b9b625053d697819f8f99e36b6366a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 20 Nov 2023 12:48:40 +0100 Subject: [PATCH 29/32] Refactor to differentiate "connecting" and "connected" state --- src/MysqlClient.php | 83 ++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/src/MysqlClient.php b/src/MysqlClient.php index 01ac492..12358c1 100644 --- a/src/MysqlClient.php +++ b/src/MysqlClient.php @@ -51,13 +51,16 @@ class MysqlClient extends EventEmitter { private $factory; private $uri; - private $connecting; private $closed = false; private $busy = false; - /** - * @var Connection|null - */ + /** @var PromiseInterface|null */ + private $connecting; + + /** @var ?Connection */ + private $connection; + + /** @var ?Connection */ private $disconnecting; private $loop; @@ -82,8 +85,15 @@ public function __construct( $this->loop = $loop ?: Loop::get(); } - private function connecting() + /** + * @return PromiseInterface + */ + private function getConnection() { + if ($this->connection !== null && $this->disconnecting === null) { + return \React\Promise\resolve($this->connection); + } + if ($this->connecting !== null) { return $this->connecting; } @@ -96,9 +106,12 @@ private function connecting() $this->connecting = $connecting = $this->factory->createConnection($this->uri); $this->connecting->then(function (Connection $connection) { + $this->connection = $connection; + $this->connecting = null; + // connection completed => remember only until closed $connection->on('close', function () { - $this->connecting = null; + $this->connection = null; if ($this->idleTimer !== null) { $this->loop->cancelTimer($this->idleTimer); @@ -127,23 +140,20 @@ private function idle() { --$this->pending; - if ($this->pending < 1 && $this->idlePeriod >= 0 && $this->connecting !== null) { + if ($this->pending < 1 && $this->idlePeriod >= 0 && $this->connection !== null) { $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () { - $this->connecting->then(function (Connection $connection) { - $this->disconnecting = $connection; - $connection->quit()->then( - function () { - // successfully disconnected => remove reference - $this->disconnecting = null; - }, - function () { - // soft-close failed but will close anyway => remove reference - $this->disconnecting = null; - } - ); - }); - $this->connecting = null; $this->idleTimer = null; + $this->disconnecting = $this->connection; + $this->connection->quit()->then( + function () { + // successfully disconnected => remove reference + $this->disconnecting = null; + }, + function () { + // soft-close failed but will close anyway => remove reference + $this->disconnecting = null; + } + ); }); } } @@ -213,7 +223,7 @@ public function query($sql, array $params = []) return \React\Promise\reject(new Exception('Connection closed')); } - return $this->connecting()->then(function (Connection $connection) use ($sql, $params) { + return $this->getConnection()->then(function (Connection $connection) use ($sql, $params) { $this->awake(); return $connection->query($sql, $params)->then( function (MysqlResult $result) { @@ -294,7 +304,7 @@ public function queryStream($sql, $params = []) } return \React\Promise\Stream\unwrapReadable( - $this->connecting()->then(function (Connection $connection) use ($sql, $params) { + $this->getConnection()->then(function (Connection $connection) use ($sql, $params) { $stream = $connection->queryStream($sql, $params); $this->awake(); @@ -333,7 +343,7 @@ public function ping() return \React\Promise\reject(new Exception('Connection closed')); } - return $this->connecting()->then(function (Connection $connection) { + return $this->getConnection()->then(function (Connection $connection) { $this->awake(); return $connection->ping()->then( function () { @@ -376,13 +386,13 @@ public function quit() } // not already connecting => no need to connect, simply close virtual connection - if ($this->connecting === null) { + if ($this->connection === null && $this->connecting === null) { $this->close(); return \React\Promise\resolve(null); } return new Promise(function (callable $resolve, callable $reject) { - $this->connecting()->then(function (Connection $connection) use ($resolve, $reject) { + $this->getConnection()->then(function (Connection $connection) use ($resolve, $reject) { $this->awake(); // soft-close connection and emit close event afterwards both on success or on error $connection->quit()->then( @@ -428,22 +438,17 @@ public function close() $this->closed = true; // force-close connection if still waiting for previous disconnection + // either close active connection or cancel pending connection attempt + // below branches are exclusive, there can only be a single connection if ($this->disconnecting !== null) { $this->disconnecting->close(); $this->disconnecting = null; - } - - // either close active connection or cancel pending connection attempt - if ($this->connecting !== null) { - $this->connecting->then(function (Connection $connection) { - $connection->close(); - }, function () { - // ignore to avoid reporting unhandled rejection - }); - if ($this->connecting !== null) { - $this->connecting->cancel(); - $this->connecting = null; - } + } elseif ($this->connection !== null) { + $this->connection->close(); + $this->connection = null; + } elseif ($this->connecting !== null) { + $this->connecting->cancel(); + $this->connecting = null; } if ($this->idleTimer !== null) { From df9ac961a59d49dd708a1139baf59d42ef4626e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 24 Nov 2023 13:12:37 +0100 Subject: [PATCH 30/32] Refactor to move idle connection handling to `Connection` --- src/Io/Connection.php | 91 ++++++- src/Io/Factory.php | 5 +- src/MysqlClient.php | 117 ++------- tests/Io/ConnectionTest.php | 502 +++++++++++++++++++++++++++++++++++- tests/MysqlClientTest.php | 278 ++++---------------- 5 files changed, 644 insertions(+), 349 deletions(-) diff --git a/src/Io/Connection.php b/src/Io/Connection.php index 8fc2007..749f456 100644 --- a/src/Io/Connection.php +++ b/src/Io/Connection.php @@ -3,6 +3,7 @@ namespace React\Mysql\Io; use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; use React\Mysql\Commands\CommandInterface; use React\Mysql\Commands\PingCommand; use React\Mysql\Commands\QueryCommand; @@ -29,26 +30,46 @@ class Connection extends EventEmitter private $executor; /** - * @var integer + * @var int one of the state constants (may change, but should be used readonly from outside) + * @see self::STATE_* */ - private $state = self::STATE_AUTHENTICATED; + public $state = self::STATE_AUTHENTICATED; /** * @var SocketConnectionInterface */ private $stream; + /** @var LoopInterface */ + private $loop; + + /** @var float */ + private $idlePeriod = 0.001; + + /** @var ?\React\EventLoop\TimerInterface */ + private $idleTimer; + + /** @var int */ + private $pending = 0; + /** * Connection constructor. * * @param SocketConnectionInterface $stream * @param Executor $executor + * @param LoopInterface $loop + * @param ?float $idlePeriod */ - public function __construct(SocketConnectionInterface $stream, Executor $executor) + public function __construct(SocketConnectionInterface $stream, Executor $executor, LoopInterface $loop, $idlePeriod) { $this->stream = $stream; $this->executor = $executor; + $this->loop = $loop; + if ($idlePeriod !== null) { + $this->idlePeriod = $idlePeriod; + } + $stream->on('error', [$this, 'handleConnectionError']); $stream->on('close', [$this, 'handleConnectionClosed']); } @@ -71,6 +92,7 @@ public function query($sql, array $params = []) return \React\Promise\reject($e); } + $this->awake(); $deferred = new Deferred(); // store all result set rows until result set end @@ -86,11 +108,13 @@ public function query($sql, array $params = []) $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) { @@ -99,6 +123,7 @@ public function query($sql, array $params = []) $result->insertId = $command->insertId; $result->warningCount = $command->warningCount; + $this->idle(); $deferred->resolve($result); }); @@ -115,20 +140,30 @@ public function queryStream($sql, $params = []) $command = new QueryCommand(); $command->setQuery($query); $this->_doCommand($command); + $this->awake(); + + $stream = new QueryStream($command, $this->stream); + $stream->on('close', function () { + $this->idle(); + }); - return new QueryStream($command, $this->stream); + return $stream; } public function ping() { return new Promise(function ($resolve, $reject) { - $this->_doCommand(new PingCommand()) - ->on('error', function ($reason) use ($reject) { - $reject($reason); - }) - ->on('success', function () use ($resolve) { - $resolve(null); - }); + $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); + }); }); } @@ -137,6 +172,10 @@ 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(); @@ -158,6 +197,11 @@ public function close() $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(); @@ -223,4 +267,29 @@ protected function _doCommand(CommandInterface $command) 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 + }); + }); + } + } } diff --git a/src/Io/Factory.php b/src/Io/Factory.php index 60bf3b2..5233907 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -210,11 +210,12 @@ public function createConnection( $connecting->cancel(); }); - $connecting->then(function (SocketConnectionInterface $stream) use ($authCommand, $deferred, $uri) { + $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); + $connection = new Connection($stream, $executor, $this->loop, $idlePeriod); $command = $executor->enqueue($authCommand); $parser->start(); diff --git a/src/MysqlClient.php b/src/MysqlClient.php index 12358c1..b5c0a9c 100644 --- a/src/MysqlClient.php +++ b/src/MysqlClient.php @@ -3,7 +3,6 @@ namespace React\Mysql; use Evenement\EventEmitter; -use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Mysql\Io\Connection; use React\Mysql\Io\Factory; @@ -52,7 +51,6 @@ class MysqlClient extends EventEmitter private $factory; private $uri; private $closed = false; - private $busy = false; /** @var PromiseInterface|null */ private $connecting; @@ -60,13 +58,14 @@ class MysqlClient extends EventEmitter /** @var ?Connection */ private $connection; - /** @var ?Connection */ - private $disconnecting; - - private $loop; - private $idlePeriod = 0.001; - private $idleTimer; - private $pending = 0; + /** + * set to true only between calling `quit()` and the connection closing in response + * + * @var bool + * @see self::quit() + * @see self::$closed + */ + private $quitting = false; public function __construct( #[\SensitiveParameter] @@ -74,15 +73,8 @@ public function __construct( ConnectorInterface $connector = null, LoopInterface $loop = null ) { - $args = []; - \parse_str((string) \parse_url($uri, \PHP_URL_QUERY), $args); - if (isset($args['idle'])) { - $this->idlePeriod = (float)$args['idle']; - } - $this->factory = new Factory($loop, $connector); $this->uri = $uri; - $this->loop = $loop ?: Loop::get(); } /** @@ -90,7 +82,8 @@ public function __construct( */ private function getConnection() { - if ($this->connection !== null && $this->disconnecting === null) { + // happy path: reuse existing connection unless it is already closing after an idle timeout + if ($this->connection !== null && ($this->quitting || $this->connection->state !== Connection::STATE_CLOSING)) { return \React\Promise\resolve($this->connection); } @@ -99,11 +92,12 @@ private function getConnection() } // force-close connection if still waiting for previous disconnection - if ($this->disconnecting !== null) { - $this->disconnecting->close(); - $this->disconnecting = null; + if ($this->connection !== null) { + assert($this->connection->state === Connection::STATE_CLOSING); + $this->connection->close(); } + // create new connection if not already connected or connecting $this->connecting = $connecting = $this->factory->createConnection($this->uri); $this->connecting->then(function (Connection $connection) { $this->connection = $connection; @@ -112,11 +106,6 @@ private function getConnection() // connection completed => remember only until closed $connection->on('close', function () { $this->connection = null; - - if ($this->idleTimer !== null) { - $this->loop->cancelTimer($this->idleTimer); - $this->idleTimer = null; - } }); }, function () { // connection failed => discard connection attempt @@ -126,38 +115,6 @@ private function getConnection() return $connecting; } - 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->connection !== null) { - $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () { - $this->idleTimer = null; - $this->disconnecting = $this->connection; - $this->connection->quit()->then( - function () { - // successfully disconnected => remove reference - $this->disconnecting = null; - }, - function () { - // soft-close failed but will close anyway => remove reference - $this->disconnecting = null; - } - ); - }); - } - } - /** * Performs an async query. * @@ -224,17 +181,7 @@ public function query($sql, array $params = []) } return $this->getConnection()->then(function (Connection $connection) use ($sql, $params) { - $this->awake(); - return $connection->query($sql, $params)->then( - function (MysqlResult $result) { - $this->idle(); - return $result; - }, - function (\Exception $e) { - $this->idle(); - throw $e; - } - ); + return $connection->query($sql, $params); }); } @@ -305,14 +252,7 @@ public function queryStream($sql, $params = []) return \React\Promise\Stream\unwrapReadable( $this->getConnection()->then(function (Connection $connection) use ($sql, $params) { - $stream = $connection->queryStream($sql, $params); - - $this->awake(); - $stream->on('close', function () { - $this->idle(); - }); - - return $stream; + return $connection->queryStream($sql, $params); }) ); } @@ -344,16 +284,7 @@ public function ping() } return $this->getConnection()->then(function (Connection $connection) { - $this->awake(); - return $connection->ping()->then( - function () { - $this->idle(); - }, - function (\Exception $e) { - $this->idle(); - throw $e; - } - ); + return $connection->ping(); }); } @@ -391,9 +322,9 @@ public function quit() 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) { - $this->awake(); // soft-close connection and emit close event afterwards both on success or on error $connection->quit()->then( function () use ($resolve){ @@ -436,14 +367,11 @@ public function close() } $this->closed = true; + $this->quitting = false; - // force-close connection if still waiting for previous disconnection // either close active connection or cancel pending connection attempt // below branches are exclusive, there can only be a single connection - if ($this->disconnecting !== null) { - $this->disconnecting->close(); - $this->disconnecting = null; - } elseif ($this->connection !== null) { + if ($this->connection !== null) { $this->connection->close(); $this->connection = null; } elseif ($this->connecting !== null) { @@ -451,11 +379,6 @@ public function close() $this->connecting = null; } - if ($this->idleTimer !== null) { - $this->loop->cancelTimer($this->idleTimer); - $this->idleTimer = null; - } - $this->emit('close'); $this->removeAllListeners(); } diff --git a/tests/Io/ConnectionTest.php b/tests/Io/ConnectionTest.php index f0cb934..394545f 100644 --- a/tests/Io/ConnectionTest.php +++ b/tests/Io/ConnectionTest.php @@ -7,13 +7,429 @@ class ConnectionTest extends BaseTestCase { + 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); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $conn = new Connection($stream, $executor, $loop, null); + $conn->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; + }); + + $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, $loop, null); + + $this->assertNull($currentCommand); + + $promise = $connection->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; + }); + + $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, $loop, null); + + $this->assertNull($currentCommand); + + $promise = $connection->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; + }); + + $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, $loop, 1.0); + + $this->assertNull($currentCommand); + + $promise = $connection->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; + }); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connection = new Connection($stream, $executor, $loop, -1); + + $this->assertNull($currentCommand); + + $promise = $connection->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; + }); + + $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, $loop, null); + + $this->assertNull($currentCommand); + + $promise = $connection->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; + }); + + $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, $loop, null); + + $connection->on('close', $this->expectCallableOnce()); + + $this->assertNull($currentCommand); + + $connection->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; + }); + + $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, $loop, null); + + $connection->on('close', $this->expectCallableOnce()); + + $this->assertNull($currentCommand); + + $connection->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; + }); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connection = new Connection($stream, $executor, $loop, null); + + $this->assertNull($currentCommand); + + $connection->query('SELECT 1'); + $connection->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; + }); + + $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, $loop, null); + + $this->assertNull($currentCommand); + + $connection->query('SELECT 1'); + + $this->assertNotNull($currentCommand); + $currentCommand->emit('success'); + + $connection->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); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $conn = new Connection($stream, $executor, $loop, null); + $conn->queryStream('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; + }); + + $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, $loop, null); + + $this->assertNull($currentCommand); + + $stream = $connection->queryStream('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; + }); + + $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, $loop, null); + + $this->assertNull($currentCommand); + + $stream = $connection->queryStream('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); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $conn = new Connection($stream, $executor, $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; + }); + + $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, $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; + }); + + $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, $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); - $conn = new Connection($stream, $executor); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $conn = new Connection($stream, $executor, $loop, null); $conn->quit(); } @@ -22,12 +438,15 @@ public function testQuitWillResolveBeforeEmittingCloseEventWhenQuitCommandEmitsS $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $pingCommand = null; - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$pingCommand) { return $pingCommand = $command; }); - $connection = new Connection($stream, $executor); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connection = new Connection($stream, $executor, $loop, null); $events = ''; $connection->on('close', function () use (&$events) { @@ -55,12 +474,15 @@ public function testQuitWillRejectBeforeEmittingCloseEventWhenQuitCommandEmitsEr $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $pingCommand = null; - $executor = $this->getMockBuilder('React\MySQL\Io\Executor')->setMethods(['enqueue'])->getMock(); + $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnCallback(function ($command) use (&$pingCommand) { return $pingCommand = $command; }); - $connection = new Connection($stream, $executor); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connection = new Connection($stream, $executor, $loop, null); $events = ''; $connection->on('close', function () use (&$events) { @@ -83,13 +505,61 @@ public function testQuitWillRejectBeforeEmittingCloseEventWhenQuitCommandEmitsEr $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); + + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->never())->method('addTimer'); + + $connection = new Connection($stream, $executor, $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; + }); + + $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, $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); - $conn = new Connection($stream, $executor); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $loop, null); $conn->quit(); $promise = $conn->query('SELECT 1'); @@ -112,7 +582,9 @@ public function testQueryAfterCloseRejectsImmediately() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->never())->method('enqueue'); - $conn = new Connection($stream, $executor); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $loop, null); $conn->close(); $promise = $conn->query('SELECT 1'); @@ -135,7 +607,9 @@ public function testQueryStreamAfterQuitThrows() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); - $conn = new Connection($stream, $executor); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $loop, null); $conn->quit(); try { @@ -152,7 +626,9 @@ public function testPingAfterQuitRejectsImmediately() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); - $conn = new Connection($stream, $executor); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $loop, null); $conn->quit(); $promise = $conn->ping(); @@ -175,7 +651,9 @@ public function testQuitAfterQuitRejectsImmediately() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->once())->method('enqueue')->willReturnArgument(0); - $conn = new Connection($stream, $executor); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $loop, null); $conn->quit(); $promise = $conn->quit(); @@ -206,7 +684,9 @@ public function testCloseStreamEmitsErrorEvent() $executor = $this->getMockBuilder('React\Mysql\Io\Executor')->setMethods(['enqueue'])->getMock(); $executor->expects($this->never())->method('enqueue'); - $conn = new Connection($stream, $executor); + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $conn = new Connection($stream, $executor, $loop, null); $conn->on('error', $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), diff --git a/tests/MysqlClientTest.php b/tests/MysqlClientTest.php index 5adf222..2576902 100644 --- a/tests/MysqlClientTest.php +++ b/tests/MysqlClientTest.php @@ -27,12 +27,6 @@ public function testConstructWithoutConnectorAndLoopAssignsConnectorAndLoopAutom $this->assertInstanceOf('React\Socket\ConnectorInterface', $connector); - $ref = new \ReflectionProperty($mysql, 'loop'); - $ref->setAccessible(true); - $loop = $ref->getValue($mysql); - - $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); - $ref = new \ReflectionProperty($factory, 'loop'); $ref->setAccessible(true); $loop = $ref->getValue($factory); @@ -56,11 +50,6 @@ public function testConstructWithConnectorAndLoopAssignsGivenConnectorAndLoop() $this->assertSame($connector, $ref->getValue($factory)); - $ref = new \ReflectionProperty($mysql, 'loop'); - $ref->setAccessible(true); - - $this->assertSame($loop, $ref->getValue($mysql)); - $ref = new \ReflectionProperty($factory, 'loop'); $ref->setAccessible(true); @@ -90,7 +79,7 @@ public function testPingWillNotCloseConnectionWhenPendingConnectionFails() $deferred->reject(new \RuntimeException()); } - public function testPingWillNotCloseConnectionWhenUnderlyingConnectionCloses() + 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)); @@ -114,34 +103,7 @@ public function testPingWillNotCloseConnectionWhenUnderlyingConnectionCloses() $base->emit('close'); } - public function testPingWillCancelTimerWithoutClosingConnectionWhenUnderlyingConnectionCloses() - { - $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)); - - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->willReturn($timer); - $loop->expects($this->once())->method('cancelTimer')->with($timer); - - $connection = new MysqlClient('', null, $loop); - - $ref = new \ReflectionProperty($connection, 'factory'); - $ref->setAccessible(true); - $ref->setValue($connection, $factory); - - $connection->on('close', $this->expectCallableNever()); - - $connection->ping(); - - assert($base instanceof Connection); - $base->emit('close'); - } - - public function testPingWillNotForwardErrorFromUnderlyingConnection() + 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)); @@ -161,87 +123,15 @@ public function testPingWillNotForwardErrorFromUnderlyingConnection() $connection->ping(); - $base->emit('error', [new \RuntimeException()]); - } - - public function testPingFollowedByIdleTimerWillQuitUnderlyingConnection() - { - $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->once())->method('quit')->willReturn(\React\Promise\resolve(null)); - $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)); - - $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { - $timeout = $cb; - return true; - }))->willReturn($timer); - - $connection = new MysqlClient('', null, $loop); - - $ref = new \ReflectionProperty($connection, 'factory'); - $ref->setAccessible(true); - $ref->setValue($connection, $factory); - - $connection->on('close', $this->expectCallableNever()); - - $connection->ping(); - - $this->assertNotNull($timeout); - $timeout(); - } - - public function testPingFollowedByIdleTimerWillNotHaveToCloseUnderlyingConnectionWhenQuitFailsBecauseUnderlyingConnectionEmitsCloseAutomatically() - { - $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->once())->method('quit')->willReturn(\React\Promise\reject(new \RuntimeException())); - $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)); - - $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { - $timeout = $cb; - return true; - }))->willReturn($timer); - - $connection = new MysqlClient('', null, $loop); - - $ref = new \ReflectionProperty($connection, 'factory'); - $ref->setAccessible(true); - $ref->setValue($connection, $factory); - - $connection->on('close', $this->expectCallableNever()); - - $connection->ping(); - - $this->assertNotNull($timeout); - $timeout(); - assert($base instanceof Connection); - $base->emit('close'); - - $ref = new \ReflectionProperty($connection, 'connecting'); - $ref->setAccessible(true); - $connecting = $ref->getValue($connection); - - $this->assertNull($connecting); + $base->emit('error', [new \RuntimeException()]); } - public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatingSecondConnection() + 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->once())->method('quit')->willReturn(new Promise(function () { })); + $base->expects($this->never())->method('quit'); $base->expects($this->once())->method('close'); $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); @@ -250,13 +140,7 @@ public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatin new Promise(function () { }) ); - $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { - $timeout = $cb; - return true; - }))->willReturn($timer); $connection = new MysqlClient('', null, $loop); @@ -268,21 +152,19 @@ public function testPingAfterIdleTimerWillCloseUnderlyingConnectionBeforeCreatin $connection->ping(); - $this->assertNotNull($timeout); - $timeout(); + // emulate triggering idle timer by setting connection state to closing + $base->state = Connection::STATE_CLOSING; $connection->ping(); } - - public function testQueryReturnsPendingPromiseAndWillNotStartTimerWhenConnectionIsPending() + public function testQueryReturnsPendingPromiseWhenConnectionIsPending() { $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(); - $loop->expects($this->never())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -314,7 +196,7 @@ public function testQueryWillQueryUnderlyingConnectionWhenResolved() $connection->query('SELECT 1'); } - public function testQueryWillResolveAndStartTimerWithDefaultIntervalWhenQueryFromUnderlyingConnectionResolves() + public function testQueryWillResolveWhenQueryFromUnderlyingConnectionResolves() { $result = new MysqlResult(); @@ -325,7 +207,6 @@ public function testQueryWillResolveAndStartTimerWithDefaultIntervalWhenQueryFro $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); $connection = new MysqlClient('', null, $loop); @@ -337,53 +218,7 @@ public function testQueryWillResolveAndStartTimerWithDefaultIntervalWhenQueryFro $ret->then($this->expectCallableOnceWith($result), $this->expectCallableNever()); } - public function testQueryWillResolveAndStartTimerWithIntervalFromIdleParameterWhenQueryFromUnderlyingConnectionResolves() - { - $result = new MysqlResult(); - - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('query')->with('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(); - $loop->expects($this->once())->method('addTimer')->with(2.5, $this->anything()); - - $connection = new MysqlClient('mysql://localhost?idle=2.5', 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 testQueryWillResolveWithoutStartingTimerWhenQueryFromUnderlyingConnectionResolvesAndIdleParameterIsNegative() - { - $result = new MysqlResult(); - - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('query')->with('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(); - $loop->expects($this->never())->method('addTimer'); - - $connection = new MysqlClient('mysql://localhost?idle=-1', 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 testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromUnderlyingConnectionResolvesBecausePingIsStillPending() + public function testPingAfterQueryWillPassPingToConnectionWhenQueryResolves() { $result = new MysqlResult(); $deferred = new Deferred(); @@ -396,7 +231,6 @@ public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromU $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->never())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -412,31 +246,7 @@ public function testQueryBeforePingWillResolveWithoutStartingTimerWhenQueryFromU $ret->then($this->expectCallableOnceWith($result), $this->expectCallableNever()); } - public function testQueryAfterPingWillCancelTimerAgainWhenPingFromUnderlyingConnectionResolved() - { - $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('query')->with('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($base)); - - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); - $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->willReturn($timer); - $loop->expects($this->once())->method('cancelTimer')->with($timer); - - $connection = new MysqlClient('', null, $loop); - - $ref = new \ReflectionProperty($connection, 'factory'); - $ref->setAccessible(true); - $ref->setValue($connection, $factory); - - $connection->ping(); - $connection->query('SELECT 1'); - } - - public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectionRejects() + public function testQueryWillRejectWhenQueryFromUnderlyingConnectionRejects() { $error = new \RuntimeException(); @@ -447,7 +257,6 @@ public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectio $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -459,14 +268,13 @@ public function testQueryWillRejectAndStartTimerWhenQueryFromUnderlyingConnectio $ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); } - public function testQueryWillRejectWithoutStartingTimerWhenUnderlyingConnectionRejects() + 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(); - $loop->expects($this->never())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -499,7 +307,7 @@ public function testQueryStreamReturnsReadableStreamWhenConnectionIsPending() $this->assertTrue($ret->isReadable()); } - public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutStartingTimerWhenResolved() + public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWhenResolved() { $stream = new ThroughStream(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); @@ -509,7 +317,6 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutSt $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->never())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -525,7 +332,7 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWithoutSt $this->assertTrue($ret->isReadable()); } - public function testQueryStreamWillReturnStreamFromUnderlyingConnectionAndStartTimerWhenResolvedAndClosed() + public function testQueryStreamWillReturnStreamFromUnderlyingConnectionWhenResolvedAndClosed() { $stream = new ThroughStream(); $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); @@ -535,7 +342,6 @@ public function testQueryStreamWillReturnStreamFromUnderlyingConnectionAndStartT $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -653,7 +459,7 @@ public function testPingWillTryToCreateNewUnderlyingConnectionAfterPreviousPingF $connection->ping()->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); } - public function testPingWillResolveAndStartTimerWhenPingFromUnderlyingConnectionResolves() + public function testPingWillResolveWhenPingFromUnderlyingConnectionResolves() { $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); $base->expects($this->once())->method('ping')->willReturn(\React\Promise\resolve(null)); @@ -662,7 +468,6 @@ public function testPingWillResolveAndStartTimerWhenPingFromUnderlyingConnection $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -674,7 +479,7 @@ public function testPingWillResolveAndStartTimerWhenPingFromUnderlyingConnection $ret->then($this->expectCallableOnce(), $this->expectCallableNever()); } - public function testPingWillRejectAndStartTimerWhenPingFromUnderlyingConnectionRejects() + public function testPingWillRejectWhenPingFromUnderlyingConnectionRejects() { $error = new \RuntimeException(); @@ -685,7 +490,6 @@ public function testPingWillRejectAndStartTimerWhenPingFromUnderlyingConnectionR $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -697,7 +501,7 @@ public function testPingWillRejectAndStartTimerWhenPingFromUnderlyingConnectionR $ret->then($this->expectCallableNever(), $this->expectCallableOnceWith($error)); } - public function testPingWillRejectAndNotStartIdleTimerWhenPingFromUnderlyingConnectionRejectsBecauseConnectionIsDead() + public function testPingWillRejectWhenPingFromUnderlyingConnectionEmitsCloseEventAndRejects() { $error = new \RuntimeException(); @@ -712,7 +516,6 @@ public function testPingWillRejectAndNotStartIdleTimerWhenPingFromUnderlyingConn $factory->expects($this->once())->method('createConnection')->willReturn(\React\Promise\resolve($base)); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->never())->method('addTimer'); $connection = new MysqlClient('', null, $loop); @@ -871,6 +674,33 @@ public function testQuitAfterPingRejectsAndThenEmitsCloseWhenUnderlyingConnectio $deferred->reject(new \RuntimeException()); } + public function testPingAfterQuitWillPassPingCommandToConnectionWhenItIsStillQuitting() + { + $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); + $connection->expects($this->exactly(2))->method('ping')->willReturn(\React\Promise\resolve(null)); + $connection->expects($this->once())->method('quit')->willReturn(new Promise(function () { })); + $connection->expects($this->never())->method('close'); + + $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('', 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(); + } + public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() { $factory = $this->getMockBuilder('React\Mysql\Io\Factory')->disableOriginalConstructor()->getMock(); @@ -953,18 +783,16 @@ public function testCloseAfterPingDoesNotEmitConnectionErrorFromAbortedConnectio $connection->close(); } - public function testCloseAfterPingWillCancelTimerWhenPingFromUnderlyingConnectionResolves() + 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)); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->willReturn($timer); - $loop->expects($this->once())->method('cancelTimer')->with($timer); $connection = new MysqlClient('', null, $loop); @@ -1021,23 +849,17 @@ public function testCloseAfterQuitAfterPingWillCloseUnderlyingConnectionWhenQuit $connection->close(); } - public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionWhenQuitIsStillPending() + 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->once())->method('quit')->willReturn(new Promise(function () { })); + $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)); - $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { - $timeout = $cb; - return true; - }))->willReturn($timer); $connection = new MysqlClient('', null, $loop); @@ -1047,8 +869,8 @@ public function testCloseAfterPingAfterIdleTimeoutWillCloseUnderlyingConnectionW $connection->ping(); - $this->assertNotNull($timeout); - $timeout(); + // emulate triggering idle timer by setting connection state to closing + $base->state = Connection::STATE_CLOSING; $connection->close(); } From 836ca2d75c6d2eb7a4d97b63397f61623d213d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 28 Nov 2023 19:40:48 +0100 Subject: [PATCH 31/32] Refactor to move command queuing logic to `MysqlClient` --- src/Io/Connection.php | 18 +- src/Io/Factory.php | 2 +- src/Io/Parser.php | 11 + src/MysqlClient.php | 150 +++-- tests/Io/ConnectionTest.php | 144 ++++- tests/MysqlClientTest.php | 1043 ++++++++++++++++++++++++++++++++++- 6 files changed, 1269 insertions(+), 99 deletions(-) diff --git a/src/Io/Connection.php b/src/Io/Connection.php index 749f456..74be321 100644 --- a/src/Io/Connection.php +++ b/src/Io/Connection.php @@ -40,6 +40,9 @@ class Connection extends EventEmitter */ private $stream; + /** @var Parser */ + private $parser; + /** @var LoopInterface */ private $loop; @@ -57,13 +60,15 @@ class Connection extends EventEmitter * * @param SocketConnectionInterface $stream * @param Executor $executor + * @param Parser $parser * @param LoopInterface $loop * @param ?float $idlePeriod */ - public function __construct(SocketConnectionInterface $stream, Executor $executor, LoopInterface $loop, $idlePeriod) + public function __construct(SocketConnectionInterface $stream, Executor $executor, Parser $parser, LoopInterface $loop, $idlePeriod) { $this->stream = $stream; $this->executor = $executor; + $this->parser = $parser; $this->loop = $loop; if ($idlePeriod !== null) { @@ -74,6 +79,17 @@ public function __construct(SocketConnectionInterface $stream, Executor $executo $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(); + } + /** * {@inheritdoc} */ diff --git a/src/Io/Factory.php b/src/Io/Factory.php index 5233907..0300415 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -215,7 +215,7 @@ public function createConnection( $executor = new Executor(); $parser = new Parser($stream, $executor); - $connection = new Connection($stream, $executor, $this->loop, $idlePeriod); + $connection = new Connection($stream, $executor, $parser, $this->loop, $idlePeriod); $command = $executor->enqueue($authCommand); $parser->start(); diff --git a/src/Io/Parser.php b/src/Io/Parser.php index f65ca5e..c3006e9 100644 --- a/src/Io/Parser.php +++ b/src/Io/Parser.php @@ -115,6 +115,17 @@ public function __construct(DuplexStreamInterface $stream, Executor $executor) }); } + /** + * 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']); diff --git a/src/MysqlClient.php b/src/MysqlClient.php index b5c0a9c..a7d8aa8 100644 --- a/src/MysqlClient.php +++ b/src/MysqlClient.php @@ -6,6 +6,7 @@ use React\EventLoop\LoopInterface; use React\Mysql\Io\Connection; use React\Mysql\Io\Factory; +use React\Promise\Deferred; use React\Promise\Promise; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface; @@ -58,6 +59,13 @@ class MysqlClient extends EventEmitter /** @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 * @@ -77,44 +85,6 @@ public function __construct( $this->uri = $uri; } - /** - * @return PromiseInterface - */ - private function getConnection() - { - // happy path: reuse existing connection unless it is already closing after an idle timeout - if ($this->connection !== null && ($this->quitting || $this->connection->state !== Connection::STATE_CLOSING)) { - return \React\Promise\resolve($this->connection); - } - - if ($this->connecting !== null) { - return $this->connecting; - } - - // force-close connection if still waiting for previous disconnection - if ($this->connection !== null) { - assert($this->connection->state === Connection::STATE_CLOSING); - $this->connection->close(); - } - - // create new connection if not already connected or connecting - $this->connecting = $connecting = $this->factory->createConnection($this->uri); - $this->connecting->then(function (Connection $connection) { - $this->connection = $connection; - $this->connecting = null; - - // connection completed => remember only until closed - $connection->on('close', function () { - $this->connection = null; - }); - }, function () { - // connection failed => discard connection attempt - $this->connecting = null; - }); - - return $connecting; - } - /** * Performs an async query. * @@ -176,12 +146,18 @@ private function getConnection() */ public function query($sql, array $params = []) { - if ($this->closed) { + if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } return $this->getConnection()->then(function (Connection $connection) use ($sql, $params) { - return $connection->query($sql, $params); + return $connection->query($sql, $params)->then(function (MysqlResult $result) use ($connection) { + $this->handleConnectionReady($connection); + return $result; + }, function (\Exception $e) use ($connection) { + $this->handleConnectionReady($connection); + throw $e; + }); }); } @@ -246,13 +222,22 @@ public function query($sql, array $params = []) */ public function queryStream($sql, $params = []) { - if ($this->closed) { + if ($this->closed || $this->quitting) { throw new Exception('Connection closed'); } return \React\Promise\Stream\unwrapReadable( $this->getConnection()->then(function (Connection $connection) use ($sql, $params) { - return $connection->queryStream($sql, $params); + $stream = $connection->queryStream($sql, $params); + + $stream->on('end', function () use ($connection) { + $this->handleConnectionReady($connection); + }); + $stream->on('error', function () use ($connection) { + $this->handleConnectionReady($connection); + }); + + return $stream; }) ); } @@ -279,12 +264,17 @@ public function queryStream($sql, $params = []) */ public function ping() { - if ($this->closed) { + if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } return $this->getConnection()->then(function (Connection $connection) { - return $connection->ping(); + return $connection->ping()->then(function () use ($connection) { + $this->handleConnectionReady($connection); + }, function (\Exception $e) use ($connection) { + $this->handleConnectionReady($connection); + throw $e; + }); }); } @@ -312,7 +302,7 @@ public function ping() */ public function quit() { - if ($this->closed) { + if ($this->closed || $this->quitting) { return \React\Promise\reject(new Exception('Connection closed')); } @@ -379,7 +369,77 @@ public function close() $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); + } } diff --git a/tests/Io/ConnectionTest.php b/tests/Io/ConnectionTest.php index 394545f..5a0a5ff 100644 --- a/tests/Io/ConnectionTest.php +++ b/tests/Io/ConnectionTest.php @@ -7,6 +7,42 @@ class ConnectionTest extends BaseTestCase { + public function testIsBusyReturnsTrueWhenParserIsBusy() + { + $stream = $this->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('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(); @@ -15,10 +51,12 @@ public function testQueryWillEnqueueOneCommand() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->query('SELECT 1'); } @@ -32,12 +70,14 @@ public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryComm 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -59,12 +99,14 @@ public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenQueryComm 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -86,12 +128,14 @@ public function testQueryWillReturnResolvedPromiseAndStartIdleTimerWhenIdlePerio 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, $loop, 1.0); + $connection = new Connection($stream, $executor, $parser, $loop, 1.0); $this->assertNull($currentCommand); @@ -113,10 +157,12 @@ public function testQueryWillReturnResolvedPromiseAndNotStartIdleTimerWhenIdlePe 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, $loop, -1); + $connection = new Connection($stream, $executor, $parser, $loop, -1); $this->assertNull($currentCommand); @@ -138,12 +184,14 @@ public function testQueryWillReturnRejectedPromiseAndStartIdleTimerWhenQueryComm 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -166,6 +214,8 @@ public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitC 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(); @@ -174,7 +224,7 @@ public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitC return true; }))->willReturn($timer); - $connection = new Connection($stream, $executor, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $connection->on('close', $this->expectCallableOnce()); @@ -203,6 +253,8 @@ public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitC 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(); @@ -211,7 +263,7 @@ public function testQueryFollowedByIdleTimerWillQuitUnderlyingConnectionAndEmitC return true; }))->willReturn($timer); - $connection = new Connection($stream, $executor, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $connection->on('close', $this->expectCallableOnce()); @@ -239,10 +291,12 @@ public function testQueryTwiceWillEnqueueSecondQueryWithoutStartingIdleTimerWhen 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -263,12 +317,14 @@ public function testQueryTwiceAfterIdleTimerWasStartedWillCancelIdleTimerAndEnqu 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -288,10 +344,12 @@ public function testQueryStreamWillEnqueueOneCommand() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->queryStream('SELECT 1'); } @@ -305,12 +363,14 @@ public function testQueryStreamWillReturnStreamThatWillEmitEndEventAndStartIdleT 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -333,12 +393,14 @@ public function testQueryStreamWillReturnStreamThatWillEmitErrorEventAndStartIdl 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -359,10 +421,12 @@ public function testPingWillEnqueueOneCommand() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->ping(); } @@ -376,12 +440,14 @@ public function testPingWillReturnResolvedPromiseAndStartIdleTimerWhenPingComman 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -403,12 +469,14 @@ public function testPingWillReturnRejectedPromiseAndStartIdleTimerWhenPingComman 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -426,10 +494,12 @@ public function testQuitWillEnqueueOneCommand() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); } @@ -443,10 +513,12 @@ public function testQuitWillResolveBeforeEmittingCloseEventWhenQuitCommandEmitsS 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $events = ''; $connection->on('close', function () use (&$events) { @@ -479,10 +551,12 @@ public function testQuitWillRejectBeforeEmittingCloseEventWhenQuitCommandEmitsEr 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $events = ''; $connection->on('close', function () use (&$events) { @@ -512,10 +586,12 @@ public function testCloseWillEmitCloseEvent() $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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $connection->on('close', $this->expectCallableOnce()); @@ -532,12 +608,14 @@ public function testCloseAfterIdleTimerWasStartedWillCancelIdleTimerAndEmitClose 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, $loop, null); + $connection = new Connection($stream, $executor, $parser, $loop, null); $this->assertNull($currentCommand); @@ -557,9 +635,11 @@ public function testQueryAfterQuitRejectsImmediately() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->query('SELECT 1'); @@ -582,9 +662,11 @@ public function testQueryAfterCloseRejectsImmediately() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->close(); $promise = $conn->query('SELECT 1'); @@ -607,9 +689,11 @@ public function testQueryStreamAfterQuitThrows() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); try { @@ -626,9 +710,11 @@ public function testPingAfterQuitRejectsImmediately() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->ping(); @@ -651,9 +737,11 @@ public function testQuitAfterQuitRejectsImmediately() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->quit(); $promise = $conn->quit(); @@ -684,9 +772,11 @@ public function testCloseStreamEmitsErrorEvent() $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, $loop, null); + $conn = new Connection($stream, $executor, $parser, $loop, null); $conn->on('error', $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), diff --git a/tests/MysqlClientTest.php b/tests/MysqlClientTest.php index 2576902..2cedb2b 100644 --- a/tests/MysqlClientTest.php +++ b/tests/MysqlClientTest.php @@ -158,42 +158,1036 @@ public function testPingAfterConnectionIsInClosingStateDueToIdleTimerWillCloseCo $connection->ping(); } - public function testQueryReturnsPendingPromiseWhenConnectionIsPending() + 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('', 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('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('', 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('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('', 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('', 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('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('', 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('', 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('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('', 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( + ['SELECT 1'], + ['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('', 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( + ['SELECT 1'], + ['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('', 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('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('', 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('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('', 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('', 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('', 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('', 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( + ['SELECT 1'], + ['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('', 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('', 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('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('', 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('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('', 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( + ['SELECT 1'], + ['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('', 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( + ['SELECT 1'], + ['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('', 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('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('', 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( + ['SELECT 1'], + ['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('', 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('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('', 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('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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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(); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $ret = $connection->query('SELECT 1'); + $promise1 = $mysql->ping(); + $promise2 = $mysql->ping(); - $this->assertTrue($ret instanceof PromiseInterface); - $ret->then($this->expectCallableNever(), $this->expectCallableNever()); + $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 testQueryWillQueryUnderlyingConnectionWhenResolved() + public function testPingTwiceWillCallSecondPingOnConnectionAfterFirstPingRejectsWhenBothQueriesAreGivenBeforeCreateConnectionResolves() { - $base = $this->getMockBuilder('React\Mysql\Io\Connection')->disableOriginalConstructor()->getMock(); - $base->expects($this->once())->method('query')->with('SELECT 1')->willReturn(new Promise(function () { })); + $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(\React\Promise\resolve($base)); + $factory->expects($this->once())->method('createConnection')->willReturn($deferred->promise()); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $connection = new MysqlClient('', null, $loop); + $mysql = new MysqlClient('', null, $loop); - $ref = new \ReflectionProperty($connection, 'factory'); + $ref = new \ReflectionProperty($mysql, 'factory'); $ref->setAccessible(true); - $ref->setValue($connection, $factory); + $ref->setValue($mysql, $factory); - $connection->query('SELECT 1'); + $promise1 = $mysql->ping(); + + $promise2 = $mysql->ping(); + + $deferred->resolve($connection); + + $promise1->then(null, $this->expectCallableOnce()); + $promise2->then($this->expectCallableNever(), $this->expectCallableNever()); } public function testQueryWillResolveWhenQueryFromUnderlyingConnectionResolves() @@ -571,7 +1565,7 @@ public function testQuitAfterPingReturnsPendingPromiseWhenConnectionIsPending() public function testQuitAfterPingRejectsAndThenEmitsCloseWhenFactoryFailsToCreateUnderlyingConnection() { $deferred = new Deferred(); - $factory = $this->getMockBuilder('React\MySQL\Io\Factory')->disableOriginalConstructor()->getMock(); + $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(); @@ -674,12 +1668,13 @@ public function testQuitAfterPingRejectsAndThenEmitsCloseWhenUnderlyingConnectio $deferred->reject(new \RuntimeException()); } - public function testPingAfterQuitWillPassPingCommandToConnectionWhenItIsStillQuitting() + public function testPingAfterQuitWillNotPassPingCommandToConnection() { - $connection = $this->getMockBuilder('React\Mysql\Io\Connection')->setMethods(['ping', 'quit', 'close'])->disableOriginalConstructor()->getMock(); - $connection->expects($this->exactly(2))->method('ping')->willReturn(\React\Promise\resolve(null)); + $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)); @@ -698,7 +1693,7 @@ public function testPingAfterQuitWillPassPingCommandToConnectionWhenItIsStillQui $mysql->quit(); - $mysql->ping(); + $mysql->ping()->then(null, $this->expectCallableOnce()); } public function testCloseEmitsCloseImmediatelyWhenConnectionIsNotAlreadyPending() @@ -732,7 +1727,7 @@ public function testCloseAfterPingCancelsPendingConnection() $ref->setAccessible(true); $ref->setValue($connection, $factory); - $connection->ping(); + $connection->ping()->then(null, $this->expectCallableOnce()); $connection->close(); } @@ -808,9 +1803,7 @@ public function testCloseAfterPingHasResolvedWillCloseUnderlyingConnectionWithou { $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')->willReturnCallback(function () use ($base) { - $base->emit('close'); - }); + $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)); @@ -891,7 +1884,7 @@ public function testCloseTwiceAfterPingEmitsCloseEventOnceWhenConnectionIsPendin $connection->on('error', $this->expectCallableNever()); $connection->on('close', $this->expectCallableOnce()); - $connection->ping(); + $connection->ping()->then(null, $this->expectCallableOnce()); $connection->close(); $connection->close(); } From 7a5a9e6fe94490802698b242e12eb22d8cbd7762 Mon Sep 17 00:00:00 2001 From: Bastian Waidelich Date: Wed, 20 Mar 2024 12:08:34 +0100 Subject: [PATCH 32/32] Add missing namespace import in `MysqlClient` Adds a missing `use` statement for the `PromiseInterface` type to `MysqlClient`. Without that change, using the API leads to type warnings: ```php function someMethod(): PromiseInterface { return $this->mysql->query(...); } // Return value is expected to be '\React\Promise\PromiseInterface', '\React\Mysql\PromiseInterface' returned --- src/MysqlClient.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MysqlClient.php b/src/MysqlClient.php index a7d8aa8..2879ac2 100644 --- a/src/MysqlClient.php +++ b/src/MysqlClient.php @@ -8,6 +8,7 @@ use React\Mysql\Io\Factory; use React\Promise\Deferred; use React\Promise\Promise; +use React\Promise\PromiseInterface; use React\Socket\ConnectorInterface; use React\Stream\ReadableStreamInterface;