From 2703f2a627f1d79d15520b3362a5f5b61868becc Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 12 May 2020 16:16:06 +0200 Subject: [PATCH 01/65] Clean up test suite --- phpunit.xml.dist | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 75fec35..5395aa6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,11 +1,6 @@ - + ./tests/ From 79bd84e7931e7a36064b3b7e3055ac421b99d9e1 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 12 May 2020 16:16:49 +0200 Subject: [PATCH 02/65] Add .gitattributes to exclude dev files from exports --- .gitattributes | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0925d33 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/examples/ export-ignore +/phpunit.xml.dist export-ignore +/tests/ export-ignore From 987ca7db32fbbfbad5663a25ef84e6a801e0308d Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Thu, 6 Aug 2020 15:25:38 +0200 Subject: [PATCH 03/65] Run tests on PHPUnit 9 --- .travis.yml | 8 ++------ composer.json | 8 ++++---- tests/FactoryLazyClientTest.php | 5 ++++- tests/FactoryStreamingClientTest.php | 5 ++++- tests/FunctionalTest.php | 5 ++++- tests/LazyClientTest.php | 5 ++++- tests/StreamingClientTest.php | 5 ++++- tests/TestCase.php | 21 ++++++++++----------- 8 files changed, 36 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index 435bb14..cc9e05b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: php # lock distro so new future defaults will not break the build dist: trusty -matrix: +jobs: include: - php: 5.3 dist: precise @@ -16,21 +16,17 @@ matrix: - php: 7.3 - php: 7.4 - php: hhvm-3.18 - install: - - composer require phpunit/phpunit:^5 --dev --no-interaction # requires legacy phpunit allow_failures: - php: hhvm-3.18 services: - redis-server -sudo: false - env: - REDIS_URI=localhost install: - - composer install --no-interaction + - composer install script: - vendor/bin/phpunit --coverage-text diff --git a/composer.json b/composer.json index a2346c4..d5a7138 100644 --- a/composer.json +++ b/composer.json @@ -19,14 +19,14 @@ "react/promise-timer": "^1.5", "react/socket": "^1.1" }, + "require-dev": { + "clue/block-react": "^1.1", + "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" + }, "autoload": { "psr-4": { "Clue\\React\\Redis\\": "src/" } }, "autoload-dev": { "psr-4": { "Clue\\Tests\\React\\Redis\\": "tests/" } - }, - "require-dev": { - "clue/block-react": "^1.1", - "phpunit/phpunit": "^7.0 || ^6.0 || ^5.0 || ^4.8.35" } } diff --git a/tests/FactoryLazyClientTest.php b/tests/FactoryLazyClientTest.php index bd63c68..c0a6430 100644 --- a/tests/FactoryLazyClientTest.php +++ b/tests/FactoryLazyClientTest.php @@ -11,7 +11,10 @@ class FactoryLazyClientTest extends TestCase private $connector; private $factory; - public function setUp() + /** + * @before + */ + public function setUpFactory() { $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php index 1fc3ce8..afe63db 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/FactoryStreamingClientTest.php @@ -12,7 +12,10 @@ class FactoryStreamingClientTest extends TestCase private $connector; private $factory; - public function setUp() + /** + * @before + */ + public function setUpFactory() { $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index b8b722b..0f5549d 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -16,7 +16,10 @@ class FunctionalTest extends TestCase private $factory; private $uri; - public function setUp() + /** + * @before + */ + public function setUpFactory() { $this->uri = getenv('REDIS_URI'); if ($this->uri === false) { diff --git a/tests/LazyClientTest.php b/tests/LazyClientTest.php index 2038d38..0df029a 100644 --- a/tests/LazyClientTest.php +++ b/tests/LazyClientTest.php @@ -21,7 +21,10 @@ class LazyClientTest extends TestCase private $loop; private $client; - public function setUp() + /** + * @before + */ + public function setUpClient() { $this->factory = $this->getMockBuilder('Clue\React\Redis\Factory')->disableOriginalConstructor()->getMock(); $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); diff --git a/tests/StreamingClientTest.php b/tests/StreamingClientTest.php index d9d437b..67c5c17 100644 --- a/tests/StreamingClientTest.php +++ b/tests/StreamingClientTest.php @@ -18,7 +18,10 @@ class StreamingClientTest extends TestCase private $serializer; private $client; - public function setUp() + /** + * @before + */ + public function setUpClient() { $this->stream = $this->getMockBuilder('React\Stream\DuplexStreamInterface')->getMock(); $this->parser = $this->getMockBuilder('Clue\Redis\Protocol\Parser\ParserInterface')->getMock(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 81aeda4..013aa7a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,9 +9,7 @@ class TestCase extends BaseTestCase protected function expectCallableOnce() { $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke'); + $mock->expects($this->once())->method('__invoke'); return $mock; } @@ -19,10 +17,7 @@ protected function expectCallableOnce() protected function expectCallableOnceWith($argument) { $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke') - ->with($argument); + $mock->expects($this->once())->method('__invoke')->with($argument); return $mock; } @@ -30,16 +25,20 @@ protected function expectCallableOnceWith($argument) protected function expectCallableNever() { $mock = $this->createCallableMock(); - $mock - ->expects($this->never()) - ->method('__invoke'); + $mock->expects($this->never())->method('__invoke'); return $mock; } protected function createCallableMock() { - return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + // PHPUnit 9+ + return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); + } else { + // legacy PHPUnit 4 - PHPUnit 8 + return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + } } protected function expectPromiseResolve($promise) From 68458a7a7ddd784995e66d81ae0561ae175a1ce4 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Thu, 27 Aug 2020 15:04:51 +0200 Subject: [PATCH 04/65] Update PHPUnit configuration schema for PHPUnit 9.3 --- .gitattributes | 1 + .travis.yml | 3 ++- composer.json | 2 +- phpunit.xml.dist | 17 +++++++++++------ phpunit.xml.legacy | 18 ++++++++++++++++++ 5 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 phpunit.xml.legacy diff --git a/.gitattributes b/.gitattributes index 0925d33..eccc763 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,4 +3,5 @@ /.travis.yml export-ignore /examples/ export-ignore /phpunit.xml.dist export-ignore +/phpunit.xml.legacy export-ignore /tests/ export-ignore diff --git a/.travis.yml b/.travis.yml index cc9e05b..e8bf15b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,4 +29,5 @@ install: - composer install script: - - vendor/bin/phpunit --coverage-text + - if [[ "$TRAVIS_PHP_VERSION" > "7.2" ]]; then vendor/bin/phpunit --coverage-text; fi + - if [[ "$TRAVIS_PHP_VERSION" < "7.3" ]]; then vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy; fi diff --git a/composer.json b/composer.json index d5a7138..ebcf44b 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ }, "require-dev": { "clue/block-react": "^1.1", - "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" }, "autoload": { "psr-4": { "Clue\\React\\Redis\\": "src/" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5395aa6..e19a12c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,14 +1,19 @@ - + + ./tests/ - - + + ./src/ - - - \ No newline at end of file + + + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy new file mode 100644 index 0000000..8d93c4f --- /dev/null +++ b/phpunit.xml.legacy @@ -0,0 +1,18 @@ + + + + + + + ./tests/ + + + + + ./src/ + + + From 838e284b68c5b048cacdc4771ff534da98dab6d3 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Fri, 28 Aug 2020 13:57:49 +0200 Subject: [PATCH 05/65] Fix Race Condition in tests and prepare PHPUnit 10 --- tests/FactoryStreamingClientTest.php | 19 +++++++++++--- tests/FunctionalTest.php | 9 ++++--- tests/LazyClientTest.php | 39 ++++++++++++++++++---------- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php index afe63db..38caa53 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/FactoryStreamingClientTest.php @@ -136,7 +136,7 @@ public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo() public function testWillResolveWhenAuthCommandReceivesOkResponseIfRedisUriContainsUserInfo() { - $stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write'))->getMock(); + $stream = $this->createCallableMockWithOriginalConstructorDisabled(array('write')); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); @@ -149,7 +149,7 @@ public function testWillResolveWhenAuthCommandReceivesOkResponseIfRedisUriContai public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorResponseIfRedisUriContainsUserInfo() { - $stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock(); + $stream = $this->createCallableMockWithOriginalConstructorDisabled(array('write', 'close')); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); $stream->expects($this->once())->method('close'); @@ -182,7 +182,7 @@ public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter public function testWillResolveWhenSelectCommandReceivesOkResponseIfRedisUriContainsPath() { - $stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write'))->getMock(); + $stream = $this->createCallableMockWithOriginalConstructorDisabled(array('write')); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$3\r\n123\r\n"); $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); @@ -195,7 +195,7 @@ public function testWillResolveWhenSelectCommandReceivesOkResponseIfRedisUriCont public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErrorResponseIfRedisUriContainsPath() { - $stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock(); + $stream = $this->createCallableMockWithOriginalConstructorDisabled(array('write', 'close')); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$3\r\n123\r\n"); $stream->expects($this->once())->method('close'); @@ -337,4 +337,15 @@ public function testCreateClientWithoutTimeoutParameterWillStartTimerWithDefault $this->factory->createClient('redis://127.0.0.1:2'); ini_set('default_socket_timeout', $old); } + + public function createCallableMockWithOriginalConstructorDisabled($array) + { + if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + // PHPUnit 9+ + return $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->onlyMethods($array)->getMock(); + } else { + // legacy PHPUnit 4 - PHPUnit 8 + return $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods($array)->getMock(); + } + } } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 0f5549d..7e70b71 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -149,10 +149,11 @@ public function testPubSub() $deferred = new Deferred(); $consumer->on('message', $this->expectCallableOnce()); $consumer->on('message', array($deferred, 'resolve')); - $consumer->subscribe($channel)->then($this->expectCallableOnce()); - - // producer sends a single message - $producer->publish($channel, 'hello world')->then($this->expectCallableOnceWith(1)); + $once = $this->expectCallableOnceWith(1); + $consumer->subscribe($channel)->then(function() use ($producer, $channel, $once){ + // producer sends a single message + $producer->publish($channel, 'hello world')->then($once); + })->then($this->expectCallableOnce()); // expect "message" event to take no longer than 0.1s Block\await($deferred->promise(), $this->loop, 0.1); diff --git a/tests/LazyClientTest.php b/tests/LazyClientTest.php index 0df029a..b7bb5d7 100644 --- a/tests/LazyClientTest.php +++ b/tests/LazyClientTest.php @@ -156,7 +156,7 @@ public function testPingAfterPreviousFactoryRejectsUnderlyingClientWillCreateNew public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewUnderlyingConnection() { - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $this->factory->expects($this->exactly(2))->method('createClient')->willReturnOnConsecutiveCalls( @@ -183,7 +183,7 @@ public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection( public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() { $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) @@ -201,7 +201,7 @@ public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStartsAfterFirstResolves() { $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) @@ -220,7 +220,7 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent() { - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call', 'close'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call', 'close')); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $client->expects($this->once())->method('close')->willReturn(\React\Promise\resolve()); @@ -298,7 +298,7 @@ public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlready public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() { $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call', 'close'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call', 'close')); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->once())->method('close'); @@ -316,7 +316,7 @@ public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() public function testCloseAfterPingRejectsWillEmitClose() { $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call', 'close'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call', 'close')); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->once())->method('close')->willReturnCallback(function () use ($client) { $client->emit('close'); @@ -358,7 +358,7 @@ public function testEndAfterPingWillEndUnderlyingClient() public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose() { - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call', 'end'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call', 'end')); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->once())->method('end'); @@ -378,7 +378,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() { $error = new \RuntimeException(); - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $deferred = new Deferred(); @@ -393,7 +393,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() { - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $deferred = new Deferred(); @@ -409,7 +409,7 @@ public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnectionEmitsCloseAfterPingIsAlreadyResolved() { $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); @@ -428,7 +428,7 @@ public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnect public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubChannel() { - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $deferred = new Deferred(); @@ -443,7 +443,7 @@ public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubCh public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClosesWhileUsingPubSubChannel() { - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->exactly(6))->method('__call')->willReturn(\React\Promise\resolve()); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); @@ -474,7 +474,7 @@ public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClo public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAndNotStartIdleTimerWithIdleDueToSubscription() { $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->once())->method('__call')->with('subscribe')->willReturn($deferred->promise()); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); @@ -492,7 +492,7 @@ public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientReso { $deferredSubscribe = new Deferred(); $deferredUnsubscribe = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods(array('__call'))->getMock(); + $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls($deferredSubscribe->promise(), $deferredUnsubscribe->promise()); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); @@ -509,4 +509,15 @@ public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientReso $deferredUnsubscribe->resolve(array('unsubscribe', 'foo', 0)); $promise->then($this->expectCallableOnceWith(array('unsubscribe', 'foo', 0))); } + + public function createCallableMockWithOriginalConstructorDisabled($array) + { + if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + // PHPUnit 9+ + return $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->onlyMethods($array)->getMock(); + } else { + // legacy PHPUnit 4 - PHPUnit 8 + return $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods($array)->getMock(); + } + } } From caabd1364c409b61aff945255a355cd48a522f1b Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Thu, 24 Sep 2020 17:29:12 +0200 Subject: [PATCH 06/65] Use ConnectionInterface intsead of partial mocks --- tests/FactoryStreamingClientTest.php | 63 +++++++++----- tests/LazyClientTest.php | 119 ++++++++++++++++++++------- 2 files changed, 135 insertions(+), 47 deletions(-) diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php index 38caa53..2c577a1 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/FactoryStreamingClientTest.php @@ -136,27 +136,45 @@ public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo() public function testWillResolveWhenAuthCommandReceivesOkResponseIfRedisUriContainsUserInfo() { - $stream = $this->createCallableMockWithOriginalConstructorDisabled(array('write')); + $dataHandler = null; + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); + $stream->expects($this->exactly(2))->method('on')->withConsecutive( + array('data', $this->callback(function ($arg) use (&$dataHandler) { + $dataHandler = $arg; + return true; + })), + array('close', $this->anything()) + ); $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); $promise = $this->factory->createClient('redis://:world@localhost'); - $stream->emit('data', array("+OK\r\n")); + $this->assertTrue(is_callable($dataHandler)); + $dataHandler("+OK\r\n"); $promise->then($this->expectCallableOnceWith($this->isInstanceOf('Clue\React\Redis\Client'))); } public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorResponseIfRedisUriContainsUserInfo() { - $stream = $this->createCallableMockWithOriginalConstructorDisabled(array('write', 'close')); + $dataHandler = null; + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); $stream->expects($this->once())->method('close'); + $stream->expects($this->exactly(2))->method('on')->withConsecutive( + array('data', $this->callback(function ($arg) use (&$dataHandler) { + $dataHandler = $arg; + return true; + })), + array('close', $this->anything()) + ); $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); $promise = $this->factory->createClient('redis://:world@localhost'); - $stream->emit('data', array("-ERR invalid password\r\n")); + $this->assertTrue(is_callable($dataHandler)); + $dataHandler("-ERR invalid password\r\n"); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -182,27 +200,45 @@ public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter public function testWillResolveWhenSelectCommandReceivesOkResponseIfRedisUriContainsPath() { - $stream = $this->createCallableMockWithOriginalConstructorDisabled(array('write')); + $dataHandler = null; + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$3\r\n123\r\n"); + $stream->expects($this->exactly(2))->method('on')->withConsecutive( + array('data', $this->callback(function ($arg) use (&$dataHandler) { + $dataHandler = $arg; + return true; + })), + array('close', $this->anything()) + ); $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); $promise = $this->factory->createClient('redis://localhost/123'); - $stream->emit('data', array("+OK\r\n")); + $this->assertTrue(is_callable($dataHandler)); + $dataHandler("+OK\r\n"); $promise->then($this->expectCallableOnceWith($this->isInstanceOf('Clue\React\Redis\Client'))); } public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErrorResponseIfRedisUriContainsPath() { - $stream = $this->createCallableMockWithOriginalConstructorDisabled(array('write', 'close')); + $dataHandler = null; + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$3\r\n123\r\n"); $stream->expects($this->once())->method('close'); + $stream->expects($this->exactly(2))->method('on')->withConsecutive( + array('data', $this->callback(function ($arg) use (&$dataHandler) { + $dataHandler = $arg; + return true; + })), + array('close', $this->anything()) + ); $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); $promise = $this->factory->createClient('redis://localhost/123'); - $stream->emit('data', array("-ERR DB index is out of range\r\n")); + $this->assertTrue(is_callable($dataHandler)); + $dataHandler("-ERR DB index is out of range\r\n"); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -337,15 +373,4 @@ public function testCreateClientWithoutTimeoutParameterWillStartTimerWithDefault $this->factory->createClient('redis://127.0.0.1:2'); ini_set('default_socket_timeout', $old); } - - public function createCallableMockWithOriginalConstructorDisabled($array) - { - if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { - // PHPUnit 9+ - return $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->onlyMethods($array)->getMock(); - } else { - // legacy PHPUnit 4 - PHPUnit 8 - return $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods($array)->getMock(); - } - } } diff --git a/tests/LazyClientTest.php b/tests/LazyClientTest.php index b7bb5d7..bf3ceb6 100644 --- a/tests/LazyClientTest.php +++ b/tests/LazyClientTest.php @@ -156,8 +156,15 @@ public function testPingAfterPreviousFactoryRejectsUnderlyingClientWillCreateNew public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewUnderlyingConnection() { - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); + $closeHandler = null; + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); + $client->expects($this->any())->method('on')->withConsecutive( + array('close', $this->callback(function ($arg) use (&$closeHandler) { + $closeHandler = $arg; + return true; + })) + ); $this->factory->expects($this->exactly(2))->method('createClient')->willReturnOnConsecutiveCalls( \React\Promise\resolve($client), @@ -165,7 +172,8 @@ public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewU ); $this->client->ping(); - $client->emit('close'); + $this->assertTrue(is_callable($closeHandler)); + $closeHandler(); $this->client->ping(); } @@ -183,7 +191,7 @@ public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection( public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() { $deferred = new Deferred(); - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) @@ -201,7 +209,7 @@ public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStartsAfterFirstResolves() { $deferred = new Deferred(); - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) @@ -220,7 +228,7 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent() { - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call', 'close')); + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $client->expects($this->once())->method('close')->willReturn(\React\Promise\resolve()); @@ -298,7 +306,7 @@ public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlready public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() { $deferred = new Deferred(); - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call', 'close')); + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->once())->method('close'); @@ -316,7 +324,7 @@ public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() public function testCloseAfterPingRejectsWillEmitClose() { $deferred = new Deferred(); - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call', 'close')); + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->once())->method('close')->willReturnCallback(function () use ($client) { $client->emit('close'); @@ -358,9 +366,15 @@ public function testEndAfterPingWillEndUnderlyingClient() public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose() { - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call', 'end')); + $closeHandler = null; + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->once())->method('end'); + $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$closeHandler) { + if ($event === 'close') { + $closeHandler = $callback; + } + }); $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); @@ -371,14 +385,15 @@ public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose() $this->client->on('close', $this->expectCallableOnce()); $this->client->end(); - $client->emit('close'); + $this->assertTrue(is_callable($closeHandler)); + $closeHandler(); } public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() { $error = new \RuntimeException(); - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $deferred = new Deferred(); @@ -393,7 +408,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() { - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $deferred = new Deferred(); @@ -408,9 +423,16 @@ public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnectionEmitsCloseAfterPingIsAlreadyResolved() { + $closeHandler = null; + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $deferred = new Deferred(); - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); + $client->expects($this->any())->method('on')->withConsecutive( + array('close', $this->callback(function ($arg) use (&$closeHandler) { + $closeHandler = $arg; + return true; + })) + ); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); @@ -423,13 +445,20 @@ public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnect $this->client->ping(); $deferred->resolve(); - $client->emit('close'); + $this->assertTrue(is_callable($closeHandler)); + $closeHandler(); } public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubChannel() { - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); + $messageHandler = null; + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); + $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$messageHandler) { + if ($event === 'message') { + $messageHandler = $callback; + } + }); $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); @@ -438,51 +467,73 @@ public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubCh $deferred->resolve($client); $this->client->on('message', $this->expectCallableOnce()); - $client->emit('message', array('foo', 'bar')); + $this->assertTrue(is_callable($messageHandler)); + $messageHandler('foo', 'bar'); } public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClosesWhileUsingPubSubChannel() { - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); + $allHandler = null; + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->exactly(6))->method('__call')->willReturn(\React\Promise\resolve()); + $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$allHandler) { + if (!isset($allHandler[$event])) { + $allHandler[$event] = $callback; + } + }); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); $this->client->subscribe('foo'); - $client->emit('subscribe', array('foo', 1)); + $this->assertTrue(is_callable($allHandler['subscribe'])); + $allHandler['subscribe']('foo', 1); $this->client->subscribe('bar'); - $client->emit('subscribe', array('bar', 2)); + $this->assertTrue(is_callable($allHandler['subscribe'])); + $allHandler['subscribe']('bar', 2); $this->client->unsubscribe('bar'); - $client->emit('unsubscribe', array('bar', 1)); + $this->assertTrue(is_callable($allHandler['unsubscribe'])); + $allHandler['unsubscribe']('bar', 1); $this->client->psubscribe('foo*'); - $client->emit('psubscribe', array('foo*', 1)); + $this->assertTrue(is_callable($allHandler['psubscribe'])); + $allHandler['psubscribe']('foo*', 1); $this->client->psubscribe('bar*'); - $client->emit('psubscribe', array('bar*', 2)); + $this->assertTrue(is_callable($allHandler['psubscribe'])); + $allHandler['psubscribe']('bar*', 2); $this->client->punsubscribe('bar*'); - $client->emit('punsubscribe', array('bar*', 1)); + $this->assertTrue(is_callable($allHandler['punsubscribe'])); + $allHandler['punsubscribe']('bar*', 1); $this->client->on('unsubscribe', $this->expectCallableOnce()); $this->client->on('punsubscribe', $this->expectCallableOnce()); - $client->emit('close'); + + $this->assertTrue(is_callable($allHandler['close'])); + $allHandler['close'](); } public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAndNotStartIdleTimerWithIdleDueToSubscription() { + $subscribeHandler = null; $deferred = new Deferred(); - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->with('subscribe')->willReturn($deferred->promise()); + $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$subscribeHandler) { + if ($event === 'subscribe' && $subscribeHandler === null) { + $subscribeHandler = $callback; + } + }); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); $this->loop->expects($this->never())->method('addTimer'); $promise = $this->client->subscribe('foo'); - $client->emit('subscribe', array('foo', 1)); + $this->assertTrue(is_callable($subscribeHandler)); + $subscribeHandler('foo', 1); $deferred->resolve(array('subscribe', 'foo', 1)); $promise->then($this->expectCallableOnceWith(array('subscribe', 'foo', 1))); @@ -490,22 +541,34 @@ public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAnd public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientResolvesUnsubscribeAndStartIdleTimerWhenSubscriptionStopped() { + $subscribeHandler = null; + $unsubscribeHandler = null; $deferredSubscribe = new Deferred(); $deferredUnsubscribe = new Deferred(); - $client = $this->createCallableMockWithOriginalConstructorDisabled(array('__call')); + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls($deferredSubscribe->promise(), $deferredUnsubscribe->promise()); + $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$subscribeHandler, &$unsubscribeHandler) { + if ($event === 'subscribe' && $subscribeHandler === null) { + $subscribeHandler = $callback; + } + if ($event === 'unsubscribe' && $unsubscribeHandler === null) { + $unsubscribeHandler = $callback; + } + }); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); $this->loop->expects($this->once())->method('addTimer'); $promise = $this->client->subscribe('foo'); - $client->emit('subscribe', array('foo', 1)); + $this->assertTrue(is_callable($subscribeHandler)); + $subscribeHandler('foo', 1); $deferredSubscribe->resolve(array('subscribe', 'foo', 1)); $promise->then($this->expectCallableOnceWith(array('subscribe', 'foo', 1))); $promise = $this->client->unsubscribe('foo'); - $client->emit('unsubscribe', array('foo', 0)); + $this->assertTrue(is_callable($unsubscribeHandler)); + $unsubscribeHandler('foo', 0); $deferredUnsubscribe->resolve(array('unsubscribe', 'foo', 0)); $promise->then($this->expectCallableOnceWith(array('unsubscribe', 'foo', 0))); } From 6859bf79defe8d83e57f3803adc007a3061ff0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 15 Sep 2020 18:51:53 +0200 Subject: [PATCH 07/65] Fix dangling timer when lazy connection closes with pending commands This is a somewhat hidden and rare race condition. The faulty idle timer would be started when the underlying Redis connection closes while the lazy connection still has pending commands, such as a blocking BLPOP operation. The timer would trigger some timer after the connection has been closed and would try to close the connection again, thus referencing undefined variables and causing a hard error in this case. The idle timer should not be started when the connection is already closed. --- src/LazyClient.php | 2 +- tests/LazyClientTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/LazyClient.php b/src/LazyClient.php index f901546..bfb2fef 100644 --- a/src/LazyClient.php +++ b/src/LazyClient.php @@ -201,7 +201,7 @@ public function idle() { --$this->pending; - if ($this->pending < 1 && $this->idlePeriod >= 0 && !$this->subscribed && !$this->psubscribed) { + if ($this->pending < 1 && $this->idlePeriod >= 0 && !$this->subscribed && !$this->psubscribed && $this->promise !== null) { $idleTimer =& $this->idleTimer; $promise =& $this->promise; $idleTimer = $this->loop->addTimer($this->idlePeriod, function () use (&$idleTimer, &$promise) { diff --git a/tests/LazyClientTest.php b/tests/LazyClientTest.php index bf3ceb6..1faf779 100644 --- a/tests/LazyClientTest.php +++ b/tests/LazyClientTest.php @@ -573,6 +573,33 @@ public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientReso $promise->then($this->expectCallableOnceWith(array('unsubscribe', 'foo', 0))); } + public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResponse() + { + $closeHandler = null; + $deferred = new Deferred(); + $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client->expects($this->once())->method('__call')->with('blpop')->willReturn($deferred->promise()); + $client->expects($this->any())->method('on')->withConsecutive( + array('close', $this->callback(function ($arg) use (&$closeHandler) { + $closeHandler = $arg; + return true; + })) + ); + + $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); + + $this->loop->expects($this->never())->method('addTimer'); + + $promise = $this->client->blpop('list'); + + $this->assertTrue(is_callable($closeHandler)); + $closeHandler(); + + $deferred->reject($e = new \RuntimeException()); + + $promise->then(null, $this->expectCallableOnceWith($e)); + } + public function createCallableMockWithOriginalConstructorDisabled($array) { if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { From a3630f14cc0852d9b0d4bf8f5732479389215792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 25 Sep 2020 13:10:39 +0200 Subject: [PATCH 08/65] Prepare v2.4.0 release --- CHANGELOG.md | 9 +++++++++ README.md | 13 ++++++++++++- composer.json | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac21950..e53491f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2.4.0 (2020-09-25) + +* Fix: Fix dangling timer when lazy connection closes with pending commands. + (#105 by @clue) + +* Improve test suite and add `.gitattributes` to exclude dev files from exports. + Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix. + (#96 and #97 by @clue and #99, #101 and #104 by @SimonFrings) + ## 2.3.0 (2019-03-11) * Feature: Add new `createLazyClient()` method to connect only on demand and diff --git a/README.md b/README.md index c4a417c..98a3a1d 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ It enables you to set and query its data or use its PubSub topics to react to in **Table of Contents** +* [Support us](#support-us) * [Quickstart example](#quickstart-example) * [Usage](#usage) * [Factory](#factory) @@ -40,6 +41,16 @@ It enables you to set and query its data or use its PubSub topics to react to in * [Tests](#tests) * [License](#license) +## Support us + +We invest a lot of time developing, maintaining and updating our awesome +open-source projects. You can help us sustain this high-quality of our work by +[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get +numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue) +for details. + +Let's take these projects to the next level together! 🚀 + ## Quickstart example Once [installed](#install), you can use the following code to connect to your @@ -538,7 +549,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require clue/redis-react:^2.3 +$ composer require clue/redis-react:^2.4 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. diff --git a/composer.json b/composer.json index ebcf44b..3a12889 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "authors": [ { "name": "Christian Lück", - "email": "christian@lueck.tv" + "email": "christian@clue.engineering" } ], "require": { From d540d842397b03cbacc2fbdcbb006f84263eaa44 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Fri, 14 May 2021 14:45:00 +0200 Subject: [PATCH 09/65] Use GitHub actions for continuous integration (CI) Bye bye Travis CI, you've served us well. --- .gitattributes | 2 +- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++ .gitignore | 2 +- .travis.yml | 33 ---------------------------- README.md | 4 +++- 5 files changed, 52 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.gitattributes b/.gitattributes index eccc763..da20d18 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,6 @@ /.gitattributes export-ignore +/.github/workflows/ export-ignore /.gitignore export-ignore -/.travis.yml export-ignore /examples/ export-ignore /phpunit.xml.dist export-ignore /phpunit.xml.legacy export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f9a22c3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + pull_request: + +jobs: + PHPUnit: + name: PHPUnit (PHP ${{ matrix.php }}) + runs-on: ubuntu-20.04 + strategy: + matrix: + php: + - 7.4 + - 7.3 + - 7.2 + - 7.1 + - 7.0 + - 5.6 + - 5.5 + - 5.4 + - 5.3 + steps: + - uses: actions/checkout@v2 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + - run: composer install + - run: docker run --net=host -d redis + - run: REDIS_URI=localhost:6379 vendor/bin/phpunit --coverage-text + if: ${{ matrix.php >= 7.3 }} + - run: REDIS_URI=localhost:6379 vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy + if: ${{ matrix.php < 7.3 }} + + PHPUnit-hhvm: + name: PHPUnit (HHVM) + runs-on: ubuntu-18.04 + continue-on-error: true + steps: + - uses: actions/checkout@v2 + - uses: azjezz/setup-hhvm@v1 + with: + version: lts-3.30 + - run: hhvm $(which composer) install + - run: docker run --net=host -d redis + - run: REDIS_URI=localhost:6379 hhvm vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index de4a392..c8153b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/vendor /composer.lock +/vendor/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e8bf15b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -language: php - -# lock distro so new future defaults will not break the build -dist: trusty - -jobs: - include: - - php: 5.3 - dist: precise - - php: 5.4 - - php: 5.5 - - php: 5.6 - - php: 7.0 - - php: 7.1 - - php: 7.2 - - php: 7.3 - - php: 7.4 - - php: hhvm-3.18 - allow_failures: - - php: hhvm-3.18 - -services: - - redis-server - -env: - - REDIS_URI=localhost - -install: - - composer install - -script: - - if [[ "$TRAVIS_PHP_VERSION" > "7.2" ]]; then vendor/bin/phpunit --coverage-text; fi - - if [[ "$TRAVIS_PHP_VERSION" < "7.3" ]]; then vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy; fi diff --git a/README.md b/README.md index 98a3a1d..ab7a048 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# clue/reactphp-redis [![Build Status](https://travis-ci.org/clue/reactphp-redis.svg?branch=master)](https://travis-ci.org/clue/reactphp-redis) +# clue/reactphp-redis + +[![CI status](https://github.com/clue/reactphp-redis/workflows/CI/badge.svg)](https://github.com/clue/reactphp-redis/actions) Async [Redis](https://redis.io/) client implementation, built on top of [ReactPHP](https://reactphp.org/). From aa181df649bad5498ab4ebeaab6cdd086e45d1f0 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Fri, 14 May 2021 15:30:15 +0200 Subject: [PATCH 10/65] Update README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ab7a048..a49b3a1 100644 --- a/README.md +++ b/README.md @@ -579,7 +579,14 @@ $ php vendor/bin/phpunit The test suite contains both unit tests and functional integration tests. The functional tests require access to a running Redis server instance and will be skipped by default. -If you want to also run the functional tests, you need to supply *your* login + +If you don't have access to a running Redis server, you can also use a temporary `Redis` Docker image: + +```bash +$ docker run --net=host redis +``` + +To now run the functional tests, you need to supply *your* login details in an environment variable like this: ```bash From 69a7615faf945d8e7efc1e5283d93eeb4689fbef Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Fri, 14 May 2021 15:30:52 +0200 Subject: [PATCH 11/65] Support PHP 8 --- .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 f9a22c3..7a9676b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: strategy: matrix: php: + - 8.0 - 7.4 - 7.3 - 7.2 diff --git a/README.md b/README.md index a49b3a1..ed8d64e 100644 --- a/README.md +++ b/README.md @@ -557,7 +557,7 @@ $ composer require clue/redis-react:^2.4 See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP -extensions and supports running on legacy PHP 5.3 through current PHP 7+ and +extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. It's *highly recommended to use PHP 7+* for this project. From fabbe7bee5d9bf3700f43603dadcc5ab0d3284bd Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 12 Jun 2021 11:35:02 +0200 Subject: [PATCH 12/65] Use full namespace so the example is runnable --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ed8d64e..10bb815 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ local Redis server and send some requests: ```php $loop = React\EventLoop\Factory::create(); -$factory = new Factory($loop); +$factory = new Clue\React\Redis\Factory($loop); $client = $factory->createLazyClient('localhost'); $client->set('greeting', 'Hello world'); @@ -92,7 +92,7 @@ It also registers everything with the main [`EventLoop`](https://github.com/reac ```php $loop = \React\EventLoop\Factory::create(); -$factory = new Factory($loop); +$factory = new \Clue\React\Redis\Factory($loop); ``` If you need custom DNS, proxy or TLS settings, you can explicitly pass a From d2079d8538b3281759c73131e6b73aafc2a33e0f Mon Sep 17 00:00:00 2001 From: Paul Date: Wed, 30 Jun 2021 16:18:52 +0200 Subject: [PATCH 13/65] Add badge to show number of project installations --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 10bb815..5f81659 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # clue/reactphp-redis [![CI status](https://github.com/clue/reactphp-redis/workflows/CI/badge.svg)](https://github.com/clue/reactphp-redis/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/clue/redis-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/redis-react) Async [Redis](https://redis.io/) client implementation, built on top of [ReactPHP](https://reactphp.org/). From bd94553d4c1e3d7e1c16f39d443f6533f4d9c42b Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Wed, 28 Jul 2021 13:15:27 +0200 Subject: [PATCH 14/65] Simplify usage by supporting new default loop --- README.md | 23 +++++++++++----------- composer.json | 4 ++-- examples/cli.php | 16 +++++++-------- examples/incr.php | 5 +---- examples/publish.php | 5 +---- examples/subscribe.php | 14 ++++++-------- src/Factory.php | 29 +++++++++++++--------------- tests/FactoryLazyClientTest.php | 11 +++++++++++ tests/FactoryStreamingClientTest.php | 11 +++++++++++ 9 files changed, 64 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 5f81659..0cc11c8 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,7 @@ Once [installed](#install), you can use the following code to connect to your local Redis server and send some requests: ```php -$loop = React\EventLoop\Factory::create(); -$factory = new Clue\React\Redis\Factory($loop); +$factory = new Clue\React\Redis\Factory(); $client = $factory->createLazyClient('localhost'); $client->set('greeting', 'Hello world'); @@ -78,8 +77,6 @@ $client->incr('invocation')->then(function ($n) { // end connection once all pending requests have been resolved $client->end(); - -$loop->run(); ``` See also the [examples](examples). @@ -89,18 +86,22 @@ See also the [examples](examples). ### Factory The `Factory` is responsible for creating your [`Client`](#client) instance. -It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage). ```php -$loop = \React\EventLoop\Factory::create(); -$factory = new \Clue\React\Redis\Factory($loop); +$factory = new Clue\React\Redis\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 DNS, proxy or TLS settings, you can explicitly pass a custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): ```php -$connector = new \React\Socket\Connector($loop, array( +$connector = new React\Socket\Connector(null, array( 'dns' => '127.0.0.1', 'tcp' => array( 'bindto' => '192.168.10.1:0' @@ -111,7 +112,7 @@ $connector = new \React\Socket\Connector($loop, array( ) )); -$factory = new Factory($loop, $connector); +$factory = new Clue\React\Redis\Factory(null, $connector); ``` #### createClient() @@ -146,7 +147,7 @@ connection attempt and/or Redis authentication. ```php $promise = $factory->createClient($redisUri); -$loop->addTimer(3.0, function () use ($promise) { +Loop::addTimer(3.0, function () use ($promise) { $promise->cancel(); }); ``` @@ -466,7 +467,7 @@ respectively: ```php $client->subscribe('user'); -$loop->addTimer(60.0, function () use ($client) { +Loop::addTimer(60.0, function () use ($client) { $client->unsubscribe('user'); }); ``` diff --git a/composer.json b/composer.json index 3a12889..8f4dde6 100644 --- a/composer.json +++ b/composer.json @@ -14,10 +14,10 @@ "php": ">=5.3", "clue/redis-protocol": "0.3.*", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "react/event-loop": "^1.0 || ^0.5", + "react/event-loop": "^1.2", "react/promise": "^2.0 || ^1.1", "react/promise-timer": "^1.5", - "react/socket": "^1.1" + "react/socket": "^1.8" }, "require-dev": { "clue/block-react": "^1.1", diff --git a/examples/cli.php b/examples/cli.php index f4dd537..8f2baed 100644 --- a/examples/cli.php +++ b/examples/cli.php @@ -2,23 +2,23 @@ use Clue\React\Redis\Client; use Clue\React\Redis\Factory; +use React\EventLoop\Loop; use React\Promise\PromiseInterface; require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$factory = new Factory($loop); +$factory = new Factory(); echo '# connecting to redis...' . PHP_EOL; -$factory->createClient('localhost')->then(function (Client $client) use ($loop) { +$factory->createClient('localhost')->then(function (Client $client) { echo '# connected! Entering interactive mode, hit CTRL-D to quit' . PHP_EOL; - $loop->addReadStream(STDIN, function () use ($client, $loop) { + Loop::addReadStream(STDIN, function () use ($client) { $line = fgets(STDIN); if ($line === false || $line === '') { echo '# CTRL-D -> Ending connection...' . PHP_EOL; - $loop->removeReadStream(STDIN); + Loop::removeReadStream(STDIN); return $client->end(); } @@ -43,10 +43,10 @@ }); }); - $client->on('close', function() use ($loop) { + $client->on('close', function() { echo '## DISCONNECTED' . PHP_EOL; - $loop->removeReadStream(STDIN); + Loop::removeReadStream(STDIN); }); }, function (Exception $error) { echo 'CONNECTION ERROR: ' . $error->getMessage() . PHP_EOL; @@ -55,5 +55,3 @@ } exit(1); }); - -$loop->run(); diff --git a/examples/incr.php b/examples/incr.php index 0eaaa32..8eb34e9 100644 --- a/examples/incr.php +++ b/examples/incr.php @@ -4,8 +4,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$factory = new Factory($loop); +$factory = new Factory(); $client = $factory->createLazyClient('localhost'); $client->incr('test'); @@ -21,5 +20,3 @@ }); $client->end(); - -$loop->run(); diff --git a/examples/publish.php b/examples/publish.php index 4da3c17..8f371e0 100644 --- a/examples/publish.php +++ b/examples/publish.php @@ -4,8 +4,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$loop = React\EventLoop\Factory::create(); -$factory = new Factory($loop); +$factory = new Factory(); $channel = isset($argv[1]) ? $argv[1] : 'channel'; $message = isset($argv[2]) ? $argv[2] : 'message'; @@ -22,5 +21,3 @@ }); $client->end(); - -$loop->run(); diff --git a/examples/subscribe.php b/examples/subscribe.php index 3dedae8..bb22a67 100644 --- a/examples/subscribe.php +++ b/examples/subscribe.php @@ -1,11 +1,11 @@ on('unsubscribe', function ($channel) use ($client, $loop) { +$client->on('unsubscribe', function ($channel) use ($client) { echo 'Unsubscribed from ' . $channel . PHP_EOL; - $loop->addPeriodicTimer(2.0, function ($timer) use ($client, $channel, $loop){ - $client->subscribe($channel)->then(function () use ($timer, $loop) { + Loop::addPeriodicTimer(2.0, function ($timer) use ($client, $channel){ + $client->subscribe($channel)->then(function () use ($timer) { echo 'Now subscribed again' . PHP_EOL; - $loop->cancelTimer($timer); + Loop::cancelTimer($timer); }, function (Exception $e) { echo 'Unable to subscribe again: ' . $e->getMessage() . PHP_EOL; }); }); }); - -$loop->run(); diff --git a/src/Factory.php b/src/Factory.php index ce95c41..aec03da 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -3,6 +3,7 @@ namespace Clue\React\Redis; use Clue\Redis\Protocol\Factory as ProtocolFactory; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Promise\Deferred; use React\Promise\Timer\TimeoutException; @@ -13,29 +14,25 @@ class Factory { + /** @var LoopInterface */ private $loop; + + /** @var ConnectorInterface */ private $connector; + + /** @var ProtocolFactory */ private $protocol; /** - * @param LoopInterface $loop - * @param ConnectorInterface|null $connector [optional] Connector to use. - * Should be `null` in order to use default Connector. - * @param ProtocolFactory|null $protocol + * @param ?LoopInterface $loop + * @param ?ConnectorInterface $connector + * @param ?ProtocolFactory $protocol */ - public function __construct(LoopInterface $loop, ConnectorInterface $connector = null, ProtocolFactory $protocol = null) + public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null, ProtocolFactory $protocol = null) { - if ($connector === null) { - $connector = new Connector($loop); - } - - if ($protocol === null) { - $protocol = new ProtocolFactory(); - } - - $this->loop = $loop; - $this->connector = $connector; - $this->protocol = $protocol; + $this->loop = $loop ?: Loop::get(); + $this->connector = $connector ?: new Connector($this->loop); + $this->protocol = $protocol ?: new ProtocolFactory(); } /** diff --git a/tests/FactoryLazyClientTest.php b/tests/FactoryLazyClientTest.php index c0a6430..33a216c 100644 --- a/tests/FactoryLazyClientTest.php +++ b/tests/FactoryLazyClientTest.php @@ -21,6 +21,17 @@ public function setUpFactory() $this->factory = new Factory($this->loop, $this->connector); } + public function testConstructWithoutLoopAssignsLoopAutomatically() + { + $factory = new Factory(); + + $ref = new \ReflectionProperty($factory, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($factory); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + public function testWillConnectWithDefaultPort() { $this->connector->expects($this->never())->method('connect')->with('redis.example.com:6379')->willReturn(Promise\reject(new \RuntimeException())); diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php index 2c577a1..bb43f66 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/FactoryStreamingClientTest.php @@ -22,6 +22,17 @@ public function setUpFactory() $this->factory = new Factory($this->loop, $this->connector); } + public function testConstructWithoutLoopAssignsLoopAutomatically() + { + $factory = new Factory(); + + $ref = new \ReflectionProperty($factory, 'loop'); + $ref->setAccessible(true); + $loop = $ref->getValue($factory); + + $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + } + /** * @doesNotPerformAssertions */ From 7fc60547bcd7cc0e5b2649ef848c81f2b95be58f Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Thu, 5 Aug 2021 10:47:08 +0200 Subject: [PATCH 15/65] Simplify usage by supporting new Socket API --- README.md | 7 ++++--- composer.json | 2 +- src/Factory.php | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0cc11c8..07fb1a3 100644 --- a/README.md +++ b/README.md @@ -97,11 +97,12 @@ 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. -If you need custom DNS, proxy or TLS settings, you can explicitly pass a -custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): +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(null, array( +$connector = new React\Socket\Connector(array( 'dns' => '127.0.0.1', 'tcp' => array( 'bindto' => '192.168.10.1:0' diff --git a/composer.json b/composer.json index 8f4dde6..3f8c7af 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "react/event-loop": "^1.2", "react/promise": "^2.0 || ^1.1", "react/promise-timer": "^1.5", - "react/socket": "^1.8" + "react/socket": "^1.9" }, "require-dev": { "clue/block-react": "^1.1", diff --git a/src/Factory.php b/src/Factory.php index aec03da..90706d3 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -31,7 +31,7 @@ class Factory public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null, ProtocolFactory $protocol = null) { $this->loop = $loop ?: Loop::get(); - $this->connector = $connector ?: new Connector($this->loop); + $this->connector = $connector ?: new Connector(array(), $this->loop); $this->protocol = $protocol ?: new ProtocolFactory(); } From 0c6a9b3a7663ec96177c8d50376f1a6dbff8eb13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 13 Apr 2021 21:13:03 +0200 Subject: [PATCH 16/65] Refactor to simplify parsing Redis URI --- README.md | 4 +- src/Factory.php | 116 ++++++++++----------------- tests/FactoryStreamingClientTest.php | 9 +++ 3 files changed, 52 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 07fb1a3..101a5fd 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ $factory = new Clue\React\Redis\Factory(null, $connector); #### createClient() -The `createClient(string $redisUri): PromiseInterface` method can be used to +The `createClient(string $uri): PromiseInterface` method can be used to create a new [`Client`](#client). It helps with establishing a plain TCP/IP or secure TLS connection to Redis @@ -215,7 +215,7 @@ $factory->createClient('localhost?timeout=0.5'); #### createLazyClient() -The `createLazyClient(string $redisUri): Client` method can be used to +The `createLazyClient(string $uri): Client` method can be used to create a new [`Client`](#client). It helps with establishing a plain TCP/IP or secure TLS connection to Redis diff --git a/src/Factory.php b/src/Factory.php index 90706d3..9235fff 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -10,7 +10,6 @@ use React\Socket\ConnectionInterface; use React\Socket\Connector; use React\Socket\ConnectorInterface; -use InvalidArgumentException; class Factory { @@ -38,18 +37,39 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn /** * Create Redis client connected to address of given redis instance * - * @param string $target Redis server URI to connect to - * @return \React\Promise\PromiseInterface resolves with Client or rejects with \Exception + * @param string $uri Redis server URI to connect to + * @return \React\Promise\PromiseInterface Promise that will + * be fulfilled with `Client` on success or rejects with `\Exception` on error. */ - public function createClient($target) + public function createClient($uri) { - try { - $parts = $this->parseUrl($target); - } catch (InvalidArgumentException $e) { - return \React\Promise\reject($e); + // support `redis+unix://` scheme for Unix domain socket (UDS) paths + if (preg_match('/^(redis\+unix:\/\/(?:[^:]*:[^@]*@)?)(.+?)?$/', $uri, $match)) { + $parts = parse_url($match[1] . 'localhost/' . $match[2]); + } else { + if (strpos($uri, '://') === false) { + $uri = 'redis://' . $uri; + } + + $parts = parse_url($uri); + } + + if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss', 'redis+unix'))) { + return \React\Promise\reject(new \InvalidArgumentException('Given URL can not be parsed')); } - $connecting = $this->connector->connect($parts['authority']); + $args = array(); + parse_str(isset($parts['query']) ? $parts['query'] : '', $args); + + $authority = $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 6379); + if ($parts['scheme'] === 'rediss') { + $authority = 'tls://' . $authority; + } elseif ($parts['scheme'] === 'redis+unix') { + $authority = 'unix://' . substr($parts['path'], 1); + unset($parts['path']); + } + $connecting = $this->connector->connect($authority); + $deferred = new Deferred(function ($_, $reject) use ($connecting) { // connection cancelled, start with rejecting attempt, then clean up $reject(new \RuntimeException('Connection to Redis server cancelled')); @@ -72,9 +92,12 @@ public function createClient($target) ); }); - if (isset($parts['auth'])) { - $promise = $promise->then(function (StreamingClient $client) use ($parts) { - return $client->auth($parts['auth'])->then( + // use `?password=secret` query or `user:secret@host` password form URL + $pass = isset($args['password']) ? $args['password'] : (isset($parts['pass']) ? rawurldecode($parts['pass']) : null); + if (isset($args['password']) || isset($parts['pass'])) { + $pass = isset($args['password']) ? $args['password'] : rawurldecode($parts['pass']); + $promise = $promise->then(function (StreamingClient $client) use ($pass) { + return $client->auth($pass)->then( function () use ($client) { return $client; }, @@ -91,9 +114,11 @@ function ($error) use ($client) { }); } - if (isset($parts['db'])) { - $promise = $promise->then(function (StreamingClient $client) use ($parts) { - return $client->select($parts['db'])->then( + // use `?db=1` query or `/1` path (skip first slash) + if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) { + $db = isset($args['db']) ? $args['db'] : substr($parts['path'], 1); + $promise = $promise->then(function (StreamingClient $client) use ($db) { + return $client->select($db)->then( function () use ($client) { return $client; }, @@ -113,7 +138,7 @@ function ($error) use ($client) { $promise->then(array($deferred, 'resolve'), array($deferred, 'reject')); // use timeout from explicit ?timeout=x parameter or default to PHP's default_socket_timeout (60) - $timeout = isset($parts['timeout']) ? $parts['timeout'] : (int) ini_get("default_socket_timeout"); + $timeout = isset($args['timeout']) ? (float) $args['timeout'] : (int) ini_get("default_socket_timeout"); if ($timeout < 0) { return $deferred->promise(); } @@ -138,63 +163,4 @@ public function createLazyClient($target) { return new LazyClient($target, $this, $this->loop); } - - /** - * @param string $target - * @return array with keys authority, auth and db - * @throws InvalidArgumentException - */ - private function parseUrl($target) - { - $ret = array(); - // support `redis+unix://` scheme for Unix domain socket (UDS) paths - if (preg_match('/^redis\+unix:\/\/([^:]*:[^@]*@)?(.+?)(\?.*)?$/', $target, $match)) { - $ret['authority'] = 'unix://' . $match[2]; - $target = 'redis://' . (isset($match[1]) ? $match[1] : '') . 'localhost' . (isset($match[3]) ? $match[3] : ''); - } - - if (strpos($target, '://') === false) { - $target = 'redis://' . $target; - } - - $parts = parse_url($target); - if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss'))) { - throw new InvalidArgumentException('Given URL can not be parsed'); - } - - if (isset($parts['pass'])) { - $ret['auth'] = rawurldecode($parts['pass']); - } - - if (isset($parts['path']) && $parts['path'] !== '') { - // skip first slash - $ret['db'] = substr($parts['path'], 1); - } - - if (!isset($ret['authority'])) { - $ret['authority'] = - ($parts['scheme'] === 'rediss' ? 'tls://' : '') . - $parts['host'] . ':' . - (isset($parts['port']) ? $parts['port'] : 6379); - } - - if (isset($parts['query'])) { - $args = array(); - parse_str($parts['query'], $args); - - if (isset($args['password'])) { - $ret['auth'] = $args['password']; - } - - if (isset($args['db'])) { - $ret['db'] = $args['db']; - } - - if (isset($args['timeout'])) { - $ret['timeout'] = (float) $args['timeout']; - } - } - - return $ret; - } } diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php index bb43f66..1d5f9cf 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/FactoryStreamingClientTest.php @@ -136,6 +136,15 @@ public function testWillWriteAuthCommandIfRedisUnixUriContainsPasswordQueryParam $this->factory->createClient('redis+unix:///tmp/redis.sock?password=world'); } + public function testWillNotWriteAnyCommandIfRedisUnixUriContainsNoPasswordOrDb() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream->expects($this->never())->method('write'); + + $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->factory->createClient('redis+unix:///tmp/redis.sock'); + } + public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); From 2da3c60c90630b9860fe95e14ad8245c0d9cec50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 14 Apr 2021 22:52:45 +0200 Subject: [PATCH 17/65] Improve error reporting, include Redis URI in all connection errors --- README.md | 8 +- examples/cli.php | 14 ++-- examples/incr.php | 10 +-- examples/publish.php | 8 +- examples/subscribe.php | 5 +- src/Factory.php | 31 +++---- tests/FactoryStreamingClientTest.php | 117 ++++++++++++++++++++++----- 7 files changed, 139 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 101a5fd..b53c2ff 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,8 @@ local Redis server and send some requests: ```php $factory = new Clue\React\Redis\Factory(); +$client = $factory->createLazyClient('localhost:6379'); -$client = $factory->createLazyClient('localhost'); $client->set('greeting', 'Hello world'); $client->append('greeting', '!'); @@ -125,7 +125,7 @@ It helps with establishing a plain TCP/IP or secure TLS connection to Redis and optionally authenticating (AUTH) and selecting the right database (SELECT). ```php -$factory->createClient('redis://localhost:6379')->then( +$factory->createClient('localhost:6379')->then( function (Client $client) { // client connected (and authenticated) }, @@ -146,7 +146,7 @@ reject its value with an Exception and will cancel the underlying TCP/IP connection attempt and/or Redis authentication. ```php -$promise = $factory->createClient($redisUri); +$promise = $factory->createClient($uri); Loop::addTimer(3.0, function () use ($promise) { $promise->cancel(); @@ -222,7 +222,7 @@ It helps with establishing a plain TCP/IP or secure TLS connection to Redis and optionally authenticating (AUTH) and selecting the right database (SELECT). ```php -$client = $factory->createLazyClient('redis://localhost:6379'); +$client = $factory->createLazyClient('localhost:6379'); $client->incr('hello'); $client->end(); diff --git a/examples/cli.php b/examples/cli.php index 8f2baed..8b7fa03 100644 --- a/examples/cli.php +++ b/examples/cli.php @@ -1,5 +1,8 @@ createClient('localhost')->then(function (Client $client) { +$factory->createClient(getenv('REDIS_URI') ?: 'localhost:6379')->then(function (Client $client) { echo '# connected! Entering interactive mode, hit CTRL-D to quit' . PHP_EOL; Loop::addReadStream(STDIN, function () use ($client) { @@ -38,7 +41,7 @@ $promise->then(function ($data) { echo '# reply: ' . json_encode($data) . PHP_EOL; - }, function ($e) { + }, function (Exception $e) { echo '# error reply: ' . $e->getMessage() . PHP_EOL; }); }); @@ -48,10 +51,7 @@ Loop::removeReadStream(STDIN); }); -}, function (Exception $error) { - echo 'CONNECTION ERROR: ' . $error->getMessage() . PHP_EOL; - if ($error->getPrevious()) { - echo $error->getPrevious()->getMessage() . PHP_EOL; - } +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; exit(1); }); diff --git a/examples/incr.php b/examples/incr.php index 8eb34e9..36a24d2 100644 --- a/examples/incr.php +++ b/examples/incr.php @@ -1,22 +1,22 @@ createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); -$client = $factory->createLazyClient('localhost'); $client->incr('test'); $client->get('test')->then(function ($result) { var_dump($result); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; - if ($e->getPrevious()) { - echo $e->getPrevious()->getMessage() . PHP_EOL; - } exit(1); }); -$client->end(); +//$client->end(); diff --git a/examples/publish.php b/examples/publish.php index 8f371e0..5353301 100644 --- a/examples/publish.php +++ b/examples/publish.php @@ -1,22 +1,22 @@ createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); $channel = isset($argv[1]) ? $argv[1] : 'channel'; $message = isset($argv[2]) ? $argv[2] : 'message'; -$client = $factory->createLazyClient('localhost'); $client->publish($channel, $message)->then(function ($received) { echo 'Successfully published. Received by ' . $received . PHP_EOL; }, function (Exception $e) { echo 'Unable to publish: ' . $e->getMessage() . PHP_EOL; - if ($e->getPrevious()) { - echo $e->getPrevious()->getMessage() . PHP_EOL; - } exit(1); }); diff --git a/examples/subscribe.php b/examples/subscribe.php index bb22a67..4e8c6fe 100644 --- a/examples/subscribe.php +++ b/examples/subscribe.php @@ -1,15 +1,18 @@ createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); $channel = isset($argv[1]) ? $argv[1] : 'channel'; -$client = $factory->createLazyClient('localhost'); $client->subscribe($channel)->then(function () { echo 'Now subscribed to channel ' . PHP_EOL; }, function (Exception $e) use ($client) { diff --git a/src/Factory.php b/src/Factory.php index 9235fff..683e1ca 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -54,8 +54,9 @@ public function createClient($uri) $parts = parse_url($uri); } + $uri = preg_replace(array('/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'), '$1***$2', $uri); if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss', 'redis+unix'))) { - return \React\Promise\reject(new \InvalidArgumentException('Given URL can not be parsed')); + return \React\Promise\reject(new \InvalidArgumentException('Invalid Redis URI given')); } $args = array(); @@ -70,9 +71,9 @@ public function createClient($uri) } $connecting = $this->connector->connect($authority); - $deferred = new Deferred(function ($_, $reject) use ($connecting) { + $deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) { // connection cancelled, start with rejecting attempt, then clean up - $reject(new \RuntimeException('Connection to Redis server cancelled')); + $reject(new \RuntimeException('Connection to ' . $uri . ' cancelled')); // either close successful connection or cancel pending connection attempt $connecting->then(function (ConnectionInterface $connection) { @@ -84,9 +85,9 @@ public function createClient($uri) $protocol = $this->protocol; $promise = $connecting->then(function (ConnectionInterface $stream) use ($protocol) { return new StreamingClient($stream, $protocol->createResponseParser(), $protocol->createSerializer()); - }, function (\Exception $e) { + }, function (\Exception $e) use ($uri) { throw new \RuntimeException( - 'Connection to Redis server failed because underlying transport connection failed', + 'Connection to ' . $uri . ' failed: ' . $e->getMessage(), 0, $e ); @@ -96,18 +97,18 @@ public function createClient($uri) $pass = isset($args['password']) ? $args['password'] : (isset($parts['pass']) ? rawurldecode($parts['pass']) : null); if (isset($args['password']) || isset($parts['pass'])) { $pass = isset($args['password']) ? $args['password'] : rawurldecode($parts['pass']); - $promise = $promise->then(function (StreamingClient $client) use ($pass) { + $promise = $promise->then(function (StreamingClient $client) use ($pass, $uri) { return $client->auth($pass)->then( function () use ($client) { return $client; }, - function ($error) use ($client) { + function (\Exception $e) use ($client, $uri) { $client->close(); throw new \RuntimeException( - 'Connection to Redis server failed because AUTH command failed', + 'Connection to ' . $uri . ' failed during AUTH command: ' . $e->getMessage(), 0, - $error + $e ); } ); @@ -117,18 +118,18 @@ function ($error) use ($client) { // use `?db=1` query or `/1` path (skip first slash) if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) { $db = isset($args['db']) ? $args['db'] : substr($parts['path'], 1); - $promise = $promise->then(function (StreamingClient $client) use ($db) { + $promise = $promise->then(function (StreamingClient $client) use ($db, $uri) { return $client->select($db)->then( function () use ($client) { return $client; }, - function ($error) use ($client) { + function (\Exception $e) use ($client, $uri) { $client->close(); throw new \RuntimeException( - 'Connection to Redis server failed because SELECT command failed', + 'Connection to ' . $uri . ' failed during SELECT command: ' . $e->getMessage(), 0, - $error + $e ); } ); @@ -143,10 +144,10 @@ function ($error) use ($client) { return $deferred->promise(); } - return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) { + return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) { if ($e instanceof TimeoutException) { throw new \RuntimeException( - 'Connection to Redis server timed out after ' . $e->getTimeout() . ' seconds' + 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds' ); } throw $e; diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php index 1d5f9cf..4bd2ca1 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/FactoryStreamingClientTest.php @@ -199,10 +199,10 @@ public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorR $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), - $this->callback(function (\Exception $e) { - return $e->getMessage() === 'Connection to Redis server failed because AUTH command failed'; + $this->callback(function (\RuntimeException $e) { + return $e->getMessage() === 'Connection to redis://:***@localhost failed during AUTH command: ERR invalid password'; }), - $this->callback(function (\Exception $e) { + $this->callback(function (\RuntimeException $e) { return $e->getPrevious()->getMessage() === 'ERR invalid password'; }) ) @@ -263,10 +263,10 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErro $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), - $this->callback(function (\Exception $e) { - return $e->getMessage() === 'Connection to Redis server failed because SELECT command failed'; + $this->callback(function (\RuntimeException $e) { + return $e->getMessage() === 'Connection to redis://localhost/123 failed during SELECT command: ERR DB index is out of range'; }), - $this->callback(function (\Exception $e) { + $this->callback(function (\RuntimeException $e) { return $e->getPrevious()->getMessage() === 'ERR DB index is out of range'; }) ) @@ -275,14 +275,17 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErro public function testWillRejectIfConnectorRejects() { - $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn(Promise\reject(new \RuntimeException())); + $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn(Promise\reject(new \RuntimeException('Foo', 42))); $promise = $this->factory->createClient('redis://127.0.0.1:2'); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), - $this->callback(function (\Exception $e) { - return $e->getMessage() === 'Connection to Redis server failed because underlying transport connection failed'; + $this->callback(function (\RuntimeException $e) { + return $e->getMessage() === 'Connection to redis://127.0.0.1:2 failed: Foo'; + }), + $this->callback(function (\RuntimeException $e) { + return $e->getPrevious()->getMessage() === 'Foo'; }) ) )); @@ -292,7 +295,14 @@ public function testWillRejectIfTargetIsInvalid() { $promise = $this->factory->createClient('http://invalid target'); - $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('InvalidArgumentException'))); + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('InvalidArgumentException'), + $this->callback(function (\InvalidArgumentException $e) { + return $e->getMessage() === 'Invalid Redis URI given'; + }) + ) + )); } public function testCancelWillRejectPromise() @@ -306,19 +316,90 @@ public function testCancelWillRejectPromise() $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); } - public function testCancelWillCancelConnectorWhenConnectionIsPending() + public function provideUris() + { + return array( + array( + 'localhost', + 'redis://localhost' + ), + array( + 'redis://localhost', + 'redis://localhost' + ), + array( + 'redis://localhost:6379', + 'redis://localhost:6379' + ), + array( + 'redis://localhost/0', + 'redis://localhost/0' + ), + array( + 'redis://user@localhost', + 'redis://user@localhost' + ), + array( + 'redis://:secret@localhost', + 'redis://:***@localhost' + ), + array( + 'redis://user:secret@localhost', + 'redis://user:***@localhost' + ), + array( + 'redis://:@localhost', + 'redis://:***@localhost' + ), + array( + 'redis://localhost?password=secret', + 'redis://localhost?password=***' + ), + array( + 'redis://localhost/0?password=secret', + 'redis://localhost/0?password=***' + ), + array( + 'redis://localhost?password=', + 'redis://localhost?password=***' + ), + array( + 'redis://localhost?foo=1&password=secret&bar=2', + 'redis://localhost?foo=1&password=***&bar=2' + ), + array( + 'rediss://localhost', + 'rediss://localhost' + ), + array( + 'redis+unix://:secret@/tmp/redis.sock', + 'redis+unix://:***@/tmp/redis.sock' + ), + array( + 'redis+unix:///tmp/redis.sock?password=secret', + 'redis+unix:///tmp/redis.sock?password=***' + ) + ); + } + + /** + * @dataProvider provideUris + * @param string $uri + * @param string $safe + */ + public function testCancelWillRejectWithUriInMessageAndCancelConnectorWhenConnectionIsPending($uri, $safe) { $deferred = new Deferred($this->expectCallableOnce()); - $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn($deferred->promise()); + $this->connector->expects($this->once())->method('connect')->willReturn($deferred->promise()); - $promise = $this->factory->createClient('redis://127.0.0.1:2'); + $promise = $this->factory->createClient($uri); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), - $this->callback(function (\Exception $e) { - return $e->getMessage() === 'Connection to Redis server cancelled'; + $this->callback(function (\RuntimeException $e) use ($safe) { + return $e->getMessage() === 'Connection to ' . $safe . ' cancelled'; }) ) )); @@ -338,8 +419,8 @@ public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect() $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('RuntimeException'), - $this->callback(function (\Exception $e) { - return $e->getMessage() === 'Connection to Redis server cancelled'; + $this->callback(function (\RuntimeException $e) { + return $e->getMessage() === 'Connection to redis://127.0.0.1:2/123 cancelled'; }) ) )); @@ -365,7 +446,7 @@ public function testCreateClientWithTimeoutParameterWillStartTimerAndRejectOnExp $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\Exception $e) { - return $e->getMessage() === 'Connection to Redis server timed out after 0 seconds'; + return $e->getMessage() === 'Connection to redis://127.0.0.1:2?timeout=0 timed out after 0 seconds'; }) ) )); From e57ccc234f9be5db1d5048864a82238651fb1036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 16 Apr 2021 20:24:04 +0200 Subject: [PATCH 18/65] Use socket error codes (errnos) for connection rejections --- src/Factory.php | 23 ++-- src/LazyClient.php | 5 +- src/StreamingClient.php | 52 +++++---- tests/FactoryStreamingClientTest.php | 33 ++++-- tests/LazyClientTest.php | 12 ++- tests/StreamingClientTest.php | 152 ++++++++++++++++++++++++--- 6 files changed, 225 insertions(+), 52 deletions(-) diff --git a/src/Factory.php b/src/Factory.php index 683e1ca..3563552 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -56,7 +56,10 @@ public function createClient($uri) $uri = preg_replace(array('/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'), '$1***$2', $uri); if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss', 'redis+unix'))) { - return \React\Promise\reject(new \InvalidArgumentException('Invalid Redis URI given')); + return \React\Promise\reject(new \InvalidArgumentException( + 'Invalid Redis URI given (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } $args = array(); @@ -73,7 +76,10 @@ public function createClient($uri) $deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) { // connection cancelled, start with rejecting attempt, then clean up - $reject(new \RuntimeException('Connection to ' . $uri . ' cancelled')); + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' cancelled (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + )); // either close successful connection or cancel pending connection attempt $connecting->then(function (ConnectionInterface $connection) { @@ -88,7 +94,7 @@ public function createClient($uri) }, function (\Exception $e) use ($uri) { throw new \RuntimeException( 'Connection to ' . $uri . ' failed: ' . $e->getMessage(), - 0, + $e->getCode(), $e ); }); @@ -106,8 +112,8 @@ function (\Exception $e) use ($client, $uri) { $client->close(); throw new \RuntimeException( - 'Connection to ' . $uri . ' failed during AUTH command: ' . $e->getMessage(), - 0, + 'Connection to ' . $uri . ' failed during AUTH command: ' . $e->getMessage() . ' (EACCES)', + defined('SOCKET_EACCES') ? SOCKET_EACCES : 13, $e ); } @@ -127,8 +133,8 @@ function (\Exception $e) use ($client, $uri) { $client->close(); throw new \RuntimeException( - 'Connection to ' . $uri . ' failed during SELECT command: ' . $e->getMessage(), - 0, + 'Connection to ' . $uri . ' failed during SELECT command: ' . $e->getMessage() . ' (ENOENT)', + defined('SOCKET_ENOENT') ? SOCKET_ENOENT : 2, $e ); } @@ -147,7 +153,8 @@ function (\Exception $e) use ($client, $uri) { return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) { if ($e instanceof TimeoutException) { throw new \RuntimeException( - 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds' + 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)', + defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110 ); } throw $e; diff --git a/src/LazyClient.php b/src/LazyClient.php index bfb2fef..c542f6b 100644 --- a/src/LazyClient.php +++ b/src/LazyClient.php @@ -115,7 +115,10 @@ private function client() public function __call($name, $args) { if ($this->closed) { - return \React\Promise\reject(new \RuntimeException('Connection closed')); + return \React\Promise\reject(new \RuntimeException( + 'Connection closed (ENOTCONN)', + defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107 + )); } $that = $this; diff --git a/src/StreamingClient.php b/src/StreamingClient.php index 0d5f9bb..c9fa1fe 100644 --- a/src/StreamingClient.php +++ b/src/StreamingClient.php @@ -2,18 +2,15 @@ namespace Clue\React\Redis; -use Evenement\EventEmitter; -use Clue\Redis\Protocol\Parser\ParserInterface; -use Clue\Redis\Protocol\Parser\ParserException; -use Clue\Redis\Protocol\Serializer\SerializerInterface; use Clue\Redis\Protocol\Factory as ProtocolFactory; -use UnderflowException; -use RuntimeException; -use InvalidArgumentException; -use React\Promise\Deferred; use Clue\Redis\Protocol\Model\ErrorReply; use Clue\Redis\Protocol\Model\ModelInterface; use Clue\Redis\Protocol\Model\MultiBulkReply; +use Clue\Redis\Protocol\Parser\ParserException; +use Clue\Redis\Protocol\Parser\ParserInterface; +use Clue\Redis\Protocol\Serializer\SerializerInterface; +use Evenement\EventEmitter; +use React\Promise\Deferred; use React\Stream\DuplexStreamInterface; /** @@ -47,9 +44,12 @@ public function __construct(DuplexStreamInterface $stream, ParserInterface $pars $stream->on('data', function($chunk) use ($parser, $that) { try { $models = $parser->pushIncoming($chunk); - } - catch (ParserException $error) { - $that->emit('error', array($error)); + } catch (ParserException $error) { + $that->emit('error', array(new \UnexpectedValueException( + 'Invalid data received: ' . $error->getMessage() . ' (EBADMSG)', + defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG : 77, + $error + ))); $that->close(); return; } @@ -57,8 +57,7 @@ public function __construct(DuplexStreamInterface $stream, ParserInterface $pars foreach ($models as $data) { try { $that->handleMessage($data); - } - catch (UnderflowException $error) { + } catch (\UnderflowException $error) { $that->emit('error', array($error)); $that->close(); return; @@ -84,11 +83,20 @@ public function __call($name, $args) static $pubsubs = array('subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe'); if ($this->ending) { - $request->reject(new RuntimeException('Connection closed')); + $request->reject(new \RuntimeException( + 'Connection ' . ($this->closed ? 'closed' : 'closing'). ' (ENOTCONN)', + defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107 + )); } elseif (count($args) !== 1 && in_array($name, $pubsubs)) { - $request->reject(new InvalidArgumentException('PubSub commands limited to single argument')); + $request->reject(new \InvalidArgumentException( + 'PubSub commands limited to single argument (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + )); } elseif ($name === 'monitor') { - $request->reject(new \BadMethodCallException('MONITOR command explicitly not supported')); + $request->reject(new \BadMethodCallException( + 'MONITOR command explicitly not supported (ENOTSUP)', + defined('SOCKET_ENOTSUP') ? SOCKET_ENOTSUP : (defined('SOCKET_EOPNOTSUPP') ? SOCKET_EOPNOTSUPP : 95) + )); } else { $this->stream->write($this->serializer->getRequestMessage($name, $args)); $this->requests []= $request; @@ -131,7 +139,10 @@ public function handleMessage(ModelInterface $message) } if (!$this->requests) { - throw new UnderflowException('Unexpected reply received, no matching request found'); + throw new \UnderflowException( + 'Unexpected reply received, no matching request found (ENOMSG)', + defined('SOCKET_ENOMSG') ? SOCKET_ENOMSG : 42 + ); } $request = array_shift($this->requests); @@ -173,8 +184,11 @@ public function close() // reject all remaining requests in the queue while($this->requests) { $request = array_shift($this->requests); - /* @var $request Request */ - $request->reject(new RuntimeException('Connection closing')); + /* @var $request Deferred */ + $request->reject(new \RuntimeException( + 'Connection closing (ENOTCONN)', + defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107 + )); } } } diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php index 4bd2ca1..6c6ab9d 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/FactoryStreamingClientTest.php @@ -200,7 +200,10 @@ public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorR $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { - return $e->getMessage() === 'Connection to redis://:***@localhost failed during AUTH command: ERR invalid password'; + return $e->getMessage() === 'Connection to redis://:***@localhost failed during AUTH command: ERR invalid password (EACCES)'; + }), + $this->callback(function (\RuntimeException $e) { + return $e->getCode() === (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13); }), $this->callback(function (\RuntimeException $e) { return $e->getPrevious()->getMessage() === 'ERR invalid password'; @@ -264,7 +267,10 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErro $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { - return $e->getMessage() === 'Connection to redis://localhost/123 failed during SELECT command: ERR DB index is out of range'; + return $e->getMessage() === 'Connection to redis://localhost/123 failed during SELECT command: ERR DB index is out of range (ENOENT)'; + }), + $this->callback(function (\RuntimeException $e) { + return $e->getCode() === (defined('SOCKET_ENOENT') ? SOCKET_ENOENT : 2); }), $this->callback(function (\RuntimeException $e) { return $e->getPrevious()->getMessage() === 'ERR DB index is out of range'; @@ -284,6 +290,9 @@ public function testWillRejectIfConnectorRejects() $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection to redis://127.0.0.1:2 failed: Foo'; }), + $this->callback(function (\RuntimeException $e) { + return $e->getCode() === 42; + }), $this->callback(function (\RuntimeException $e) { return $e->getPrevious()->getMessage() === 'Foo'; }) @@ -299,7 +308,10 @@ public function testWillRejectIfTargetIsInvalid() $this->logicalAnd( $this->isInstanceOf('InvalidArgumentException'), $this->callback(function (\InvalidArgumentException $e) { - return $e->getMessage() === 'Invalid Redis URI given'; + return $e->getMessage() === 'Invalid Redis URI given (EINVAL)'; + }), + $this->callback(function (\InvalidArgumentException $e) { + return $e->getCode() === (defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22); }) ) )); @@ -399,7 +411,10 @@ public function testCancelWillRejectWithUriInMessageAndCancelConnectorWhenConnec $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) use ($safe) { - return $e->getMessage() === 'Connection to ' . $safe . ' cancelled'; + return $e->getMessage() === 'Connection to ' . $safe . ' cancelled (ECONNABORTED)'; + }), + $this->callback(function (\RuntimeException $e) { + return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103); }) ) )); @@ -420,7 +435,10 @@ public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect() $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { - return $e->getMessage() === 'Connection to redis://127.0.0.1:2/123 cancelled'; + return $e->getMessage() === 'Connection to redis://127.0.0.1:2/123 cancelled (ECONNABORTED)'; + }), + $this->callback(function (\RuntimeException $e) { + return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103); }) ) )); @@ -446,7 +464,10 @@ public function testCreateClientWithTimeoutParameterWillStartTimerAndRejectOnExp $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\Exception $e) { - return $e->getMessage() === 'Connection to redis://127.0.0.1:2?timeout=0 timed out after 0 seconds'; + return $e->getMessage() === 'Connection to redis://127.0.0.1:2?timeout=0 timed out after 0 seconds (ETIMEDOUT)'; + }), + $this->callback(function (\Exception $e) { + return $e->getCode() === (defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110); }) ) )); diff --git a/tests/LazyClientTest.php b/tests/LazyClientTest.php index 1faf779..5510ec9 100644 --- a/tests/LazyClientTest.php +++ b/tests/LazyClientTest.php @@ -185,7 +185,17 @@ public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection( $this->client->close(); $promise = $this->client->ping(); - $promise->then(null, $this->expectCallableOnce()); + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('RuntimeException'), + $this->callback(function (\RuntimeException $e) { + return $e->getMessage() === 'Connection closed (ENOTCONN)'; + }), + $this->callback(function (\RuntimeException $e) { + return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107); + }) + ) + )); } public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() diff --git a/tests/StreamingClientTest.php b/tests/StreamingClientTest.php index 67c5c17..ed51069 100644 --- a/tests/StreamingClientTest.php +++ b/tests/StreamingClientTest.php @@ -2,13 +2,13 @@ namespace Clue\Tests\React\Redis; -use Clue\React\Redis\StreamingClient; -use Clue\Redis\Protocol\Parser\ParserException; -use Clue\Redis\Protocol\Model\IntegerReply; use Clue\Redis\Protocol\Model\BulkReply; use Clue\Redis\Protocol\Model\ErrorReply; +use Clue\Redis\Protocol\Model\IntegerReply; use Clue\Redis\Protocol\Model\MultiBulkReply; +use Clue\Redis\Protocol\Parser\ParserException; use Clue\React\Redis\Client; +use Clue\React\Redis\StreamingClient; use React\Stream\ThroughStream; class StreamingClientTest extends TestCase @@ -30,6 +30,28 @@ public function setUpClient() $this->client = new StreamingClient($this->stream, $this->parser, $this->serializer); } + public function testConstructWithoutParserAssignsParserAutomatically() + { + $this->client = new StreamingClient($this->stream, null, $this->serializer); + + $ref = new \ReflectionProperty($this->client, 'parser'); + $ref->setAccessible(true); + $parser = $ref->getValue($this->client); + + $this->assertInstanceOf('Clue\Redis\Protocol\Parser\ParserInterface', $parser); + } + + public function testConstructWithoutParserAndSerializerAssignsParserAndSerializerAutomatically() + { + $this->client = new StreamingClient($this->stream, $this->parser); + + $ref = new \ReflectionProperty($this->client, 'serializer'); + $ref->setAccessible(true); + $serializer = $ref->getValue($this->client); + + $this->assertInstanceOf('Clue\Redis\Protocol\Serializer\SerializerInterface', $serializer); + } + public function testSending() { $this->serializer->expects($this->once())->method('getRequestMessage')->with($this->equalTo('ping'))->will($this->returnValue('message')); @@ -60,21 +82,43 @@ public function testReceiveParseErrorEmitsErrorEvent() $this->stream = new ThroughStream(); $this->client = new StreamingClient($this->stream, $this->parser, $this->serializer); - $this->client->on('error', $this->expectCallableOnce()); + $this->client->on('error', $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('UnexpectedValueException'), + $this->callback(function (\UnexpectedValueException $e) { + return $e->getMessage() === 'Invalid data received: Foo (EBADMSG)'; + }), + $this->callback(function (\UnexpectedValueException $e) { + return $e->getCode() === (defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG : 77); + }) + ) + )); $this->client->on('close', $this->expectCallableOnce()); - $this->parser->expects($this->once())->method('pushIncoming')->with($this->equalTo('message'))->will($this->throwException(new ParserException())); + $this->parser->expects($this->once())->method('pushIncoming')->with('message')->willThrowException(new ParserException('Foo')); $this->stream->emit('data', array('message')); } - public function testReceiveThrowMessageEmitsErrorEvent() + public function testReceiveUnexpectedReplyEmitsErrorEvent() { $this->stream = new ThroughStream(); $this->client = new StreamingClient($this->stream, $this->parser, $this->serializer); $this->client->on('error', $this->expectCallableOnce()); - - $this->parser->expects($this->once())->method('pushIncoming')->with($this->equalTo('message'))->will($this->returnValue(array(new IntegerReply(2)))); + $this->client->on('error', $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('UnderflowException'), + $this->callback(function (\UnderflowException $e) { + return $e->getMessage() === 'Unexpected reply received, no matching request found (ENOMSG)'; + }), + $this->callback(function (\UnderflowException $e) { + return $e->getCode() === (defined('SOCKET_ENOMSG') ? SOCKET_ENOMSG : 42); + }) + ) + )); + + + $this->parser->expects($this->once())->method('pushIncoming')->with('message')->willReturn(array(new IntegerReply(2))); $this->stream->emit('data', array('message')); } @@ -102,10 +146,19 @@ public function testMonitorCommandIsNotSupported() { $promise = $this->client->monitor(); - $this->expectPromiseReject($promise); + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('BadMethodCallException'), + $this->callback(function (\BadMethodCallException $e) { + return $e->getMessage() === 'MONITOR command explicitly not supported (ENOTSUP)'; + }), + $this->callback(function (\BadMethodCallException $e) { + return $e->getCode() === (defined('SOCKET_ENOTSUP') ? SOCKET_ENOTSUP : (defined('SOCKET_EOPNOTSUPP') ? SOCKET_EOPNOTSUPP : 95)); + }) + ) + )); } - public function testErrorReply() { $promise = $this->client->invalid(); @@ -113,7 +166,6 @@ public function testErrorReply() $err = new ErrorReply("ERR unknown command 'invalid'"); $this->client->handleMessage($err); - $this->expectPromiseReject($promise); $promise->then(null, $this->expectCallableOnceWith($err)); } @@ -122,7 +174,36 @@ public function testClosingClientRejectsAllRemainingRequests() $promise = $this->client->ping(); $this->client->close(); - $this->expectPromiseReject($promise); + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('RuntimeException'), + $this->callback(function (\RuntimeException $e) { + return $e->getMessage() === 'Connection closing (ENOTCONN)'; + }), + $this->callback(function (\RuntimeException $e) { + return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107); + }) + ) + )); + } + + public function testEndingClientRejectsAllNewRequests() + { + $this->client->ping(); + $this->client->end(); + $promise = $this->client->ping(); + + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('RuntimeException'), + $this->callback(function (\RuntimeException $e) { + return $e->getMessage() === 'Connection closing (ENOTCONN)'; + }), + $this->callback(function (\RuntimeException $e) { + return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107); + }) + ) + )); } public function testClosedClientRejectsAllNewRequests() @@ -130,7 +211,17 @@ public function testClosedClientRejectsAllNewRequests() $this->client->close(); $promise = $this->client->ping(); - $this->expectPromiseReject($promise); + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('RuntimeException'), + $this->callback(function (\RuntimeException $e) { + return $e->getMessage() === 'Connection closed (ENOTCONN)'; + }), + $this->callback(function (\RuntimeException $e) { + return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107); + }) + ) + )); } public function testEndingNonBusyClosesClient() @@ -212,10 +303,37 @@ public function testPubsubMessage(Client $client) $client->handleMessage(new MultiBulkReply(array(new BulkReply('message'), new BulkReply('test'), new BulkReply('payload')))); } - public function testPubsubSubscribeSingleOnly() + public function testSubscribeWithMultipleArgumentsRejects() + { + $promise = $this->client->subscribe('a', 'b'); + + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('InvalidArgumentException'), + $this->callback(function (\InvalidArgumentException $e) { + return $e->getMessage() === 'PubSub commands limited to single argument (EINVAL)'; + }), + $this->callback(function (\InvalidArgumentException $e) { + return $e->getCode() === (defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22); + }) + ) + )); + } + + public function testUnsubscribeWithoutArgumentsRejects() { - $this->expectPromiseReject($this->client->subscribe('a', 'b')); - $this->expectPromiseReject($this->client->unsubscribe('a', 'b')); - $this->expectPromiseReject($this->client->unsubscribe()); + $promise = $this->client->unsubscribe(); + + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('InvalidArgumentException'), + $this->callback(function (\InvalidArgumentException $e) { + return $e->getMessage() === 'PubSub commands limited to single argument (EINVAL)'; + }), + $this->callback(function (\InvalidArgumentException $e) { + return $e->getCode() === (defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22); + }) + ) + )); } } From af0cce329143f5ef15ea12b4611686ac3bfc0c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 17 Apr 2021 12:38:02 +0200 Subject: [PATCH 19/65] Improve socket error codes by preserving upstreaming errnos --- src/Factory.php | 25 +++++- src/StreamingClient.php | 23 ++++-- tests/FactoryStreamingClientTest.php | 112 +++++++++++++++++++++++++++ tests/StreamingClientTest.php | 26 ++++++- 4 files changed, 173 insertions(+), 13 deletions(-) diff --git a/src/Factory.php b/src/Factory.php index 3563552..171523c 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -111,9 +111,16 @@ function () use ($client) { function (\Exception $e) use ($client, $uri) { $client->close(); + $const = ''; + $errno = $e->getCode(); + if ($errno === 0) { + $const = ' (EACCES)'; + $errno = $e->getCode() ?: (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13); + } + throw new \RuntimeException( - 'Connection to ' . $uri . ' failed during AUTH command: ' . $e->getMessage() . ' (EACCES)', - defined('SOCKET_EACCES') ? SOCKET_EACCES : 13, + 'Connection to ' . $uri . ' failed during AUTH command: ' . $e->getMessage() . $const, + $errno, $e ); } @@ -132,9 +139,19 @@ function () use ($client) { function (\Exception $e) use ($client, $uri) { $client->close(); + $const = ''; + $errno = $e->getCode(); + if ($errno === 0 && strpos($e->getMessage(), 'NOAUTH ') === 0) { + $const = ' (EACCES)'; + $errno = defined('SOCKET_EACCES') ? SOCKET_EACCES : 13; + } elseif ($errno === 0) { + $const = ' (ENOENT)'; + $errno = defined('SOCKET_ENOENT') ? SOCKET_ENOENT : 2; + } + throw new \RuntimeException( - 'Connection to ' . $uri . ' failed during SELECT command: ' . $e->getMessage() . ' (ENOENT)', - defined('SOCKET_ENOENT') ? SOCKET_ENOENT : 2, + 'Connection to ' . $uri . ' failed during SELECT command: ' . $e->getMessage() . $const, + $errno, $e ); } diff --git a/src/StreamingClient.php b/src/StreamingClient.php index c9fa1fe..8afd84d 100644 --- a/src/StreamingClient.php +++ b/src/StreamingClient.php @@ -146,7 +146,7 @@ public function handleMessage(ModelInterface $message) } $request = array_shift($this->requests); - /* @var $request Deferred */ + assert($request instanceof Deferred); if ($message instanceof ErrorReply) { $request->reject($message); @@ -177,18 +177,27 @@ public function close() $this->ending = true; $this->closed = true; + $remoteClosed = $this->stream->isReadable() === false && $this->stream->isWritable() === false; $this->stream->close(); $this->emit('close'); // reject all remaining requests in the queue - while($this->requests) { + while ($this->requests) { $request = array_shift($this->requests); - /* @var $request Deferred */ - $request->reject(new \RuntimeException( - 'Connection closing (ENOTCONN)', - defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107 - )); + assert($request instanceof Deferred); + + if ($remoteClosed) { + $request->reject(new \RuntimeException( + 'Connection closed by peer (ECONNRESET)', + defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104 + )); + } else { + $request->reject(new \RuntimeException( + 'Connection closing (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + )); + } } } } diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php index 6c6ab9d..882af76 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/FactoryStreamingClientTest.php @@ -212,6 +212,44 @@ public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorR )); } + public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWaitingForAuthCommand() + { + $closeHandler = null; + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); + $stream->expects($this->once())->method('close'); + $stream->expects($this->exactly(2))->method('on')->withConsecutive( + array('data', $this->anything()), + array('close', $this->callback(function ($arg) use (&$closeHandler) { + $closeHandler = $arg; + return true; + })) + ); + + $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $promise = $this->factory->createClient('redis://:world@localhost'); + + $this->assertTrue(is_callable($closeHandler)); + $stream->expects($this->once())->method('isReadable')->willReturn(false); + $stream->expects($this->once())->method('isWritable')->willReturn(false); + call_user_func($closeHandler); + + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('RuntimeException'), + $this->callback(function (\Exception $e) { + return $e->getMessage() === 'Connection to redis://:***@localhost failed during AUTH command: Connection closed by peer (ECONNRESET)'; + }), + $this->callback(function (\Exception $e) { + return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104); + }), + $this->callback(function (\Exception $e) { + return $e->getPrevious()->getMessage() === 'Connection closed by peer (ECONNRESET)'; + }) + ) + )); + } + public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter() { $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); @@ -279,6 +317,80 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErro )); } + public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesAuthErrorResponseIfRedisUriContainsPath() + { + $dataHandler = null; + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$3\r\n123\r\n"); + $stream->expects($this->once())->method('close'); + $stream->expects($this->exactly(2))->method('on')->withConsecutive( + array('data', $this->callback(function ($arg) use (&$dataHandler) { + $dataHandler = $arg; + return true; + })), + array('close', $this->anything()) + ); + + $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $promise = $this->factory->createClient('redis://localhost/123'); + + $this->assertTrue(is_callable($dataHandler)); + $dataHandler("-NOAUTH Authentication required.\r\n"); + + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('RuntimeException'), + $this->callback(function (\Exception $e) { + return $e->getMessage() === 'Connection to redis://localhost/123 failed during SELECT command: NOAUTH Authentication required. (EACCES)'; + }), + $this->callback(function (\Exception $e) { + return $e->getCode() === (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13); + }), + $this->callback(function (\Exception $e) { + return $e->getPrevious()->getMessage() === 'NOAUTH Authentication required.'; + }) + ) + )); + } + + public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWaitingForSelectCommand() + { + $closeHandler = null; + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$3\r\n123\r\n"); + $stream->expects($this->once())->method('close'); + $stream->expects($this->exactly(2))->method('on')->withConsecutive( + array('data', $this->anything()), + array('close', $this->callback(function ($arg) use (&$closeHandler) { + $closeHandler = $arg; + return true; + })) + ); + + $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $promise = $this->factory->createClient('redis://localhost/123'); + + $this->assertTrue(is_callable($closeHandler)); + $stream->expects($this->once())->method('isReadable')->willReturn(false); + $stream->expects($this->once())->method('isWritable')->willReturn(false); + call_user_func($closeHandler); + + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('RuntimeException'), + $this->callback(function (\Exception $e) { + return $e->getMessage() === 'Connection to redis://localhost/123 failed during SELECT command: Connection closed by peer (ECONNRESET)'; + }), + $this->callback(function (\Exception $e) { + return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104); + }), + $this->callback(function (\Exception $e) { + return $e->getPrevious()->getMessage() === 'Connection closed by peer (ECONNRESET)'; + }) + ) + )); + } + public function testWillRejectIfConnectorRejects() { $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn(Promise\reject(new \RuntimeException('Foo', 42))); diff --git a/tests/StreamingClientTest.php b/tests/StreamingClientTest.php index ed51069..af54942 100644 --- a/tests/StreamingClientTest.php +++ b/tests/StreamingClientTest.php @@ -178,10 +178,32 @@ public function testClosingClientRejectsAllRemainingRequests() $this->logicalAnd( $this->isInstanceOf('RuntimeException'), $this->callback(function (\RuntimeException $e) { - return $e->getMessage() === 'Connection closing (ENOTCONN)'; + return $e->getMessage() === 'Connection closing (ECONNABORTED)'; }), $this->callback(function (\RuntimeException $e) { - return $e->getCode() === (defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107); + return $e->getCode() === (defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103); + }) + ) + )); + } + + public function testClosingStreamRejectsAllRemainingRequests() + { + $this->stream = new ThroughStream(); + $this->parser->expects($this->once())->method('pushIncoming')->willReturn(array()); + $this->client = new StreamingClient($this->stream, $this->parser, $this->serializer); + + $promise = $this->client->ping(); + $this->stream->close(); + + $promise->then(null, $this->expectCallableOnceWith( + $this->logicalAnd( + $this->isInstanceOf('RuntimeException'), + $this->callback(function (\RuntimeException $e) { + return $e->getMessage() === 'Connection closed by peer (ECONNRESET)'; + }), + $this->callback(function (\RuntimeException $e) { + return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104); }) ) )); From cc0f4aca1e5af4751c312fdd6200d892e0211ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 18 Apr 2021 10:37:58 +0200 Subject: [PATCH 20/65] Improve documentation structure --- README.md | 331 +++++++++++++++++++++++++++--------------------------- 1 file changed, 167 insertions(+), 164 deletions(-) diff --git a/README.md b/README.md index b53c2ff..c808f47 100644 --- a/README.md +++ b/README.md @@ -29,17 +29,18 @@ It enables you to set and query its data or use its PubSub topics to react to in * [Support us](#support-us) * [Quickstart example](#quickstart-example) * [Usage](#usage) - * [Factory](#factory) - * [createClient()](#createclient) - * [createLazyClient()](#createlazyclient) - * [Client](#client) * [Commands](#commands) * [Promises](#promises) * [PubSub](#pubsub) - * [close()](#close) - * [end()](#end) - * [error event](#error-event) - * [close event](#close-event) +* [API](#api) + * [Factory](#factory) + * [createClient()](#createclient) + * [createLazyClient()](#createlazyclient) + * [Client](#client) + * [end()](#end) + * [close()](#close) + * [error event](#error-event) + * [close event](#close-event) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -83,6 +84,161 @@ See also the [examples](examples). ## Usage +### Commands + +All [Redis commands](https://redis.io/commands) are automatically available as public methods like this: + +```php +$client->get($key); +$client->set($key, $value); +$client->exists($key); +$client->expire($key, $seconds); +$client->mget($key1, $key2, $key3); + +$client->multi(); +$client->exec(); + +$client->publish($channel, $payload); +$client->subscribe($channel); + +$client->ping(); +$client->select($database); + +// many more… +``` + +Listing all available commands is out of scope here, please refer to the [Redis command reference](https://redis.io/commands). +All [Redis commands](https://redis.io/commands) are automatically available as public methods via the magic `__call()` method. + +Each of these commands supports async operation and either *resolves* with +its *results* or *rejects* with an `Exception`. +Please see the following section about [promises](#promises) for more details. + +### Promises + +Sending commands is async (non-blocking), so you can actually send multiple commands in parallel. +Redis will respond to each command request with a response message, pending commands will be pipelined automatically. + +Sending commands uses a [Promise](https://github.com/reactphp/promise)-based interface that makes it easy to react to when a command is *fulfilled* +(i.e. either successfully resolved or rejected with an error): + +```php +$client->set('hello', 'world'); +$client->get('hello')->then(function ($response) { + // response received for GET command + echo 'hello ' . $response; +}); +``` + +### PubSub + +This library is commonly used to efficiently transport messages using Redis' +[Pub/Sub](https://redis.io/topics/pubsub) (Publish/Subscribe) channels. For +instance, this can be used to distribute single messages to a larger number +of subscribers (think horizontal scaling for chat-like applications) or as an +efficient message transport in distributed systems (microservice architecture). + +The [`PUBLISH` command](https://redis.io/commands/publish) can be used to +send a message to all clients currently subscribed to a given channel: + +```php +$channel = 'user'; +$message = json_encode(array('id' => 10)); +$client->publish($channel, $message); +``` + +The [`SUBSCRIBE` command](https://redis.io/commands/subscribe) can be used to +subscribe to a channel and then receive incoming PubSub `message` events: + +```php +$channel = 'user'; +$client->subscribe($channel); + +$client->on('message', function ($channel, $payload) { + // pubsub message received on given $channel + var_dump($channel, json_decode($payload)); +}); +``` + +Likewise, you can use the same client connection to subscribe to multiple +channels by simply executing this command multiple times: + +```php +$client->subscribe('user.register'); +$client->subscribe('user.join'); +$client->subscribe('user.leave'); +``` + +Similarly, the [`PSUBSCRIBE` command](https://redis.io/commands/psubscribe) can +be used to subscribe to all channels matching a given pattern and then receive +all incoming PubSub messages with the `pmessage` event: + + +```php +$pattern = 'user.*'; +$client->psubscribe($pattern); + +$client->on('pmessage', function ($pattern, $channel, $payload) { + // pubsub message received matching given $pattern + var_dump($channel, json_decode($payload)); +}); +``` + +Once you're in a subscribed state, Redis no longer allows executing any other +commands on the same client connection. This is commonly worked around by simply +creating a second client connection and dedicating one client connection solely +for PubSub subscriptions and the other for all other commands. + +The [`UNSUBSCRIBE` command](https://redis.io/commands/unsubscribe) and +[`PUNSUBSCRIBE` command](https://redis.io/commands/punsubscribe) can be used to +unsubscribe from active subscriptions if you're no longer interested in +receiving any further events for the given channel and pattern subscriptions +respectively: + +```php +$client->subscribe('user'); + +Loop::addTimer(60.0, function () use ($client) { + $client->unsubscribe('user'); +}); +``` + +Likewise, once you've unsubscribed the last channel and pattern, the client +connection is no longer in a subscribed state and you can issue any other +command over this client connection again. + +Each of the above methods follows normal request-response semantics and return +a [`Promise`](#promises) to await successful subscriptions. Note that while +Redis allows a variable number of arguments for each of these commands, this +library is currently limited to single arguments for each of these methods in +order to match exactly one response to each command request. As an alternative, +the methods can simply be invoked multiple times with one argument each. + +Additionally, can listen for the following PubSub events to get notifications +about subscribed/unsubscribed channels and patterns: + +```php +$client->on('subscribe', function ($channel, $total) { + // subscribed to given $channel +}); +$client->on('psubscribe', function ($pattern, $total) { + // subscribed to matching given $pattern +}); +$client->on('unsubscribe', function ($channel, $total) { + // unsubscribed from given $channel +}); +$client->on('punsubscribe', function ($pattern, $total) { + // unsubscribed from matching given $pattern +}); +``` + +When using the [`createLazyClient()`](#createlazyclient) method, the `unsubscribe` +and `punsubscribe` events will be invoked automatically when the underlying +connection is lost. This gives you control over re-subscribing to the channels +and patterns as appropriate. + +## API + ### Factory The `Factory` is responsible for creating your [`Client`](#client) instance. @@ -354,169 +510,16 @@ and keeps track of pending commands. Besides defining a few methods, this interface also implements the `EventEmitterInterface` which allows you to react to certain events as documented below. -#### Commands - -All [Redis commands](https://redis.io/commands) are automatically available as public methods like this: - -```php -$client->get($key); -$client->set($key, $value); -$client->exists($key); -$client->expire($key, $seconds); -$client->mget($key1, $key2, $key3); - -$client->multi(); -$client->exec(); - -$client->publish($channel, $payload); -$client->subscribe($channel); - -$client->ping(); -$client->select($database); - -// many more… -``` - -Listing all available commands is out of scope here, please refer to the [Redis command reference](https://redis.io/commands). -All [Redis commands](https://redis.io/commands) are automatically available as public methods via the magic `__call()` method. - -Each of these commands supports async operation and either *resolves* with -its *results* or *rejects* with an `Exception`. -Please see the following section about [promises](#promises) for more details. - -#### Promises - -Sending commands is async (non-blocking), so you can actually send multiple commands in parallel. -Redis will respond to each command request with a response message, pending commands will be pipelined automatically. - -Sending commands uses a [Promise](https://github.com/reactphp/promise)-based interface that makes it easy to react to when a command is *fulfilled* -(i.e. either successfully resolved or rejected with an error): - -```php -$client->set('hello', 'world'); -$client->get('hello')->then(function ($response) { - // response received for GET command - echo 'hello ' . $response; -}); -``` - -#### PubSub - -This library is commonly used to efficiently transport messages using Redis' -[Pub/Sub](https://redis.io/topics/pubsub) (Publish/Subscribe) channels. For -instance, this can be used to distribute single messages to a larger number -of subscribers (think horizontal scaling for chat-like applications) or as an -efficient message transport in distributed systems (microservice architecture). - -The [`PUBLISH` command](https://redis.io/commands/publish) can be used to -send a message to all clients currently subscribed to a given channel: - -```php -$channel = 'user'; -$message = json_encode(array('id' => 10)); -$client->publish($channel, $message); -``` - -The [`SUBSCRIBE` command](https://redis.io/commands/subscribe) can be used to -subscribe to a channel and then receive incoming PubSub `message` events: - -```php -$channel = 'user'; -$client->subscribe($channel); - -$client->on('message', function ($channel, $payload) { - // pubsub message received on given $channel - var_dump($channel, json_decode($payload)); -}); -``` - -Likewise, you can use the same client connection to subscribe to multiple -channels by simply executing this command multiple times: - -```php -$client->subscribe('user.register'); -$client->subscribe('user.join'); -$client->subscribe('user.leave'); -``` - -Similarly, the [`PSUBSCRIBE` command](https://redis.io/commands/psubscribe) can -be used to subscribe to all channels matching a given pattern and then receive -all incoming PubSub messages with the `pmessage` event: - - -```php -$pattern = 'user.*'; -$client->psubscribe($pattern); - -$client->on('pmessage', function ($pattern, $channel, $payload) { - // pubsub message received matching given $pattern - var_dump($channel, json_decode($payload)); -}); -``` - -Once you're in a subscribed state, Redis no longer allows executing any other -commands on the same client connection. This is commonly worked around by simply -creating a second client connection and dedicating one client connection solely -for PubSub subscriptions and the other for all other commands. - -The [`UNSUBSCRIBE` command](https://redis.io/commands/unsubscribe) and -[`PUNSUBSCRIBE` command](https://redis.io/commands/punsubscribe) can be used to -unsubscribe from active subscriptions if you're no longer interested in -receiving any further events for the given channel and pattern subscriptions -respectively: - -```php -$client->subscribe('user'); - -Loop::addTimer(60.0, function () use ($client) { - $client->unsubscribe('user'); -}); -``` - -Likewise, once you've unsubscribed the last channel and pattern, the client -connection is no longer in a subscribed state and you can issue any other -command over this client connection again. - -Each of the above methods follows normal request-response semantics and return -a [`Promise`](#promises) to await successful subscriptions. Note that while -Redis allows a variable number of arguments for each of these commands, this -library is currently limited to single arguments for each of these methods in -order to match exactly one response to each command request. As an alternative, -the methods can simply be invoked multiple times with one argument each. - -Additionally, can listen for the following PubSub events to get notifications -about subscribed/unsubscribed channels and patterns: - -```php -$client->on('subscribe', function ($channel, $total) { - // subscribed to given $channel -}); -$client->on('psubscribe', function ($pattern, $total) { - // subscribed to matching given $pattern -}); -$client->on('unsubscribe', function ($channel, $total) { - // unsubscribed from given $channel -}); -$client->on('punsubscribe', function ($pattern, $total) { - // unsubscribed from matching given $pattern -}); -``` +#### end() -When using the [`createLazyClient()`](#createlazyclient) method, the `unsubscribe` -and `punsubscribe` events will be invoked automatically when the underlying -connection is lost. This gives you control over re-subscribing to the channels -and patterns as appropriate. +The `end():void` method can be used to +soft-close the Redis connection once all pending commands are completed. #### close() The `close():void` method can be used to force-close the Redis connection and reject all pending commands. -#### end() - -The `end():void` method can be used to -soft-close the Redis connection once all pending commands are completed. - #### error event The `error` event will be emitted once a fatal error occurs, such as From 92d3208dc9c3f8fe3089a325706f5e1e846c6f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 19 Apr 2021 08:53:41 +0200 Subject: [PATCH 21/65] Add documentation for magic `__call()` method --- README.md | 77 +++++++++++++++++++++++++++++++++++++++++--------- src/Client.php | 2 +- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c808f47..cc374da 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ It enables you to set and query its data or use its PubSub topics to react to in * [createClient()](#createclient) * [createLazyClient()](#createlazyclient) * [Client](#client) + * [__call()](#__call) * [end()](#end) * [close()](#close) * [error event](#error-event) @@ -86,7 +87,8 @@ See also the [examples](examples). ### Commands -All [Redis commands](https://redis.io/commands) are automatically available as public methods like this: +Most importantly, this project provides a [`Client`](#client) instance that +can be used to invoke all [Redis commands](https://redis.io/commands) (such as `GET`, `SET`, etc.). ```php $client->get($key); @@ -107,26 +109,41 @@ $client->select($database); // many more… ``` -Listing all available commands is out of scope here, please refer to the [Redis command reference](https://redis.io/commands). -All [Redis commands](https://redis.io/commands) are automatically available as public methods via the magic `__call()` method. +Each method call matches the respective [Redis command](https://redis.io/commands). +For example, the `$redis->get()` method will invoke the [`GET` command](https://redis.io/commands/get). -Each of these commands supports async operation and either *resolves* with -its *results* or *rejects* with an `Exception`. -Please see the following section about [promises](#promises) for more details. +All [Redis commands](https://redis.io/commands) are automatically available as +public methods via the magic [`__call()` method](#__call). +Listing all available commands is out of scope here, please refer to the +[Redis command reference](https://redis.io/commands). + +Any arguments passed to the method call will be forwarded as command arguments. +For example, the `$redis->set('name', 'Alice')` call will perform the equivalent of a +`SET name Alice` command. It's safe to pass integer arguments where applicable (for +example `$redis->expire($key, 60)`), but internally Redis requires all arguments to +always be coerced to string values. + +Each of these commands supports async operation and returns a [Promise](#promises) +that eventually *fulfills* with its *results* on success or *rejects* with an +`Exception` on error. See also the following section about [promises](#promises) +for more details. ### Promises -Sending commands is async (non-blocking), so you can actually send multiple commands in parallel. -Redis will respond to each command request with a response message, pending commands will be pipelined automatically. +Sending commands is async (non-blocking), so you can actually send multiple +commands in parallel. +Redis will respond to each command request with a response message, pending +commands will be pipelined automatically. -Sending commands uses a [Promise](https://github.com/reactphp/promise)-based interface that makes it easy to react to when a command is *fulfilled* -(i.e. either successfully resolved or rejected with an error): +Sending commands uses a [Promise](https://github.com/reactphp/promise)-based +interface that makes it easy to react to when a command is completed +(i.e. either successfully fulfilled or rejected with an error): ```php -$client->set('hello', 'world'); -$client->get('hello')->then(function ($response) { - // response received for GET command - echo 'hello ' . $response; +$redis->get($key)->then(function (string $value) { + var_dump($value); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -510,6 +527,38 @@ and keeps track of pending commands. Besides defining a few methods, this interface also implements the `EventEmitterInterface` which allows you to react to certain events as documented below. +#### __call() + +The `__call(string $name, string[] $args): PromiseInterface` method can be used to +invoke the given command. + +This is a magic method that will be invoked when calling any Redis command on this instance. +Each method call matches the respective [Redis command](https://redis.io/commands). +For example, the `$redis->get()` method will invoke the [`GET` command](https://redis.io/commands/get). + +```php +$redis->get($key)->then(function (string $value) { + var_dump($value); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +All [Redis commands](https://redis.io/commands) are automatically available as +public methods via this magic `__call()` method. +Listing all available commands is out of scope here, please refer to the +[Redis command reference](https://redis.io/commands). + +Any arguments passed to the method call will be forwarded as command arguments. +For example, the `$redis->set('name', 'Alice')` call will perform the equivalent of a +`SET name Alice` command. It's safe to pass integer arguments where applicable (for +example `$redis->expire($key, 60)`), but internally Redis requires all arguments to +always be coerced to string values. + +Each of these commands supports async operation and returns a [Promise](#promises) +that eventually *fulfills* with its *results* on success or *rejects* with an +`Exception` on error. See also [promises](#promises) for more details. + #### end() The `end():void` method can be used to diff --git a/src/Client.php b/src/Client.php index 1b514df..ec54229 100644 --- a/src/Client.php +++ b/src/Client.php @@ -22,7 +22,7 @@ interface Client extends EventEmitterInterface { /** - * Invoke the given command and return a Promise that will be resolved when the request has been replied to + * Invoke the given command and return a Promise that will be fulfilled when the request has been replied to * * This is a magic method that will be invoked when calling any redis * command on this instance. From a753f969b77516aaceb185d5142ba61231379884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 29 Aug 2021 15:19:06 +0200 Subject: [PATCH 22/65] Consistently use `$redis` and fully qualified class names for `Client` --- README.md | 76 +++++++++--------- examples/cli.php | 17 ++-- examples/incr.php | 12 ++- examples/publish.php | 10 +-- examples/subscribe.php | 19 +++-- src/Factory.php | 24 +++--- src/LazyClient.php | 34 ++++---- tests/FactoryLazyClientTest.php | 12 +-- tests/FunctionalTest.php | 90 ++++++++++----------- tests/LazyClientTest.php | 138 ++++++++++++++++---------------- tests/StreamingClientTest.php | 98 +++++++++++------------ 11 files changed, 261 insertions(+), 269 deletions(-) diff --git a/README.md b/README.md index cc374da..3b4a5e9 100644 --- a/README.md +++ b/README.md @@ -63,22 +63,22 @@ local Redis server and send some requests: ```php $factory = new Clue\React\Redis\Factory(); -$client = $factory->createLazyClient('localhost:6379'); +$redis = $factory->createLazyClient('localhost:6379'); -$client->set('greeting', 'Hello world'); -$client->append('greeting', '!'); +$redis->set('greeting', 'Hello world'); +$redis->append('greeting', '!'); -$client->get('greeting')->then(function ($greeting) { +$redis->get('greeting')->then(function ($greeting) { // Hello world! echo $greeting . PHP_EOL; }); -$client->incr('invocation')->then(function ($n) { +$redis->incr('invocation')->then(function ($n) { echo 'This is invocation #' . $n . PHP_EOL; }); // end connection once all pending requests have been resolved -$client->end(); +$redis->end(); ``` See also the [examples](examples). @@ -91,20 +91,20 @@ Most importantly, this project provides a [`Client`](#client) instance that can be used to invoke all [Redis commands](https://redis.io/commands) (such as `GET`, `SET`, etc.). ```php -$client->get($key); -$client->set($key, $value); -$client->exists($key); -$client->expire($key, $seconds); -$client->mget($key1, $key2, $key3); +$redis->get($key); +$redis->set($key, $value); +$redis->exists($key); +$redis->expire($key, $seconds); +$redis->mget($key1, $key2, $key3); -$client->multi(); -$client->exec(); +$redis->multi(); +$redis->exec(); -$client->publish($channel, $payload); -$client->subscribe($channel); +$redis->publish($channel, $payload); +$redis->subscribe($channel); -$client->ping(); -$client->select($database); +$redis->ping(); +$redis->select($database); // many more… ``` @@ -161,7 +161,7 @@ send a message to all clients currently subscribed to a given channel: ```php $channel = 'user'; $message = json_encode(array('id' => 10)); -$client->publish($channel, $message); +$redis->publish($channel, $message); ``` The [`SUBSCRIBE` command](https://redis.io/commands/subscribe) can be used to @@ -169,9 +169,9 @@ subscribe to a channel and then receive incoming PubSub `message` events: ```php $channel = 'user'; -$client->subscribe($channel); +$redis->subscribe($channel); -$client->on('message', function ($channel, $payload) { +$redis->on('message', function ($channel, $payload) { // pubsub message received on given $channel var_dump($channel, json_decode($payload)); }); @@ -181,9 +181,9 @@ Likewise, you can use the same client connection to subscribe to multiple channels by simply executing this command multiple times: ```php -$client->subscribe('user.register'); -$client->subscribe('user.join'); -$client->subscribe('user.leave'); +$redis->subscribe('user.register'); +$redis->subscribe('user.join'); +$redis->subscribe('user.leave'); ``` Similarly, the [`PSUBSCRIBE` command](https://redis.io/commands/psubscribe) can @@ -193,9 +193,9 @@ all incoming PubSub messages with the `pmessage` event: ```php $pattern = 'user.*'; -$client->psubscribe($pattern); +$redis->psubscribe($pattern); -$client->on('pmessage', function ($pattern, $channel, $payload) { +$redis->on('pmessage', function ($pattern, $channel, $payload) { // pubsub message received matching given $pattern var_dump($channel, json_decode($payload)); }); @@ -213,10 +213,10 @@ receiving any further events for the given channel and pattern subscriptions respectively: ```php -$client->subscribe('user'); +$redis->subscribe('user'); -Loop::addTimer(60.0, function () use ($client) { - $client->unsubscribe('user'); +Loop::addTimer(60.0, function () use ($redis) { + $redis->unsubscribe('user'); }); ``` @@ -235,16 +235,16 @@ Additionally, can listen for the following PubSub events to get notifications about subscribed/unsubscribed channels and patterns: ```php -$client->on('subscribe', function ($channel, $total) { +$redis->on('subscribe', function ($channel, $total) { // subscribed to given $channel }); -$client->on('psubscribe', function ($pattern, $total) { +$redis->on('psubscribe', function ($pattern, $total) { // subscribed to matching given $pattern }); -$client->on('unsubscribe', function ($channel, $total) { +$redis->on('unsubscribe', function ($channel, $total) { // unsubscribed from given $channel }); -$client->on('punsubscribe', function ($pattern, $total) { +$redis->on('punsubscribe', function ($pattern, $total) { // unsubscribed from matching given $pattern }); ``` @@ -299,7 +299,7 @@ and optionally authenticating (AUTH) and selecting the right database (SELECT). ```php $factory->createClient('localhost:6379')->then( - function (Client $client) { + function (Client $redis) { // client connected (and authenticated) }, function (Exception $e) { @@ -395,10 +395,10 @@ It helps with establishing a plain TCP/IP or secure TLS connection to Redis and optionally authenticating (AUTH) and selecting the right database (SELECT). ```php -$client = $factory->createLazyClient('localhost:6379'); +$redis = $factory->createLazyClient('localhost:6379'); -$client->incr('hello'); -$client->end(); +$redis->incr('hello'); +$redis->end(); ``` This method immediately returns a "virtual" connection implementing the @@ -576,7 +576,7 @@ when the client connection is lost or is invalid. The event receives a single `Exception` argument for the error instance. ```php -$client->on('error', function (Exception $e) { +$redis->on('error', function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; }); ``` @@ -590,7 +590,7 @@ errors caused by invalid commands. The `close` event will be emitted once the client connection closes (terminates). ```php -$client->on('close', function () { +$redis->on('close', function () { echo 'Connection closed' . PHP_EOL; }); ``` diff --git a/examples/cli.php b/examples/cli.php index 8b7fa03..c1c629f 100644 --- a/examples/cli.php +++ b/examples/cli.php @@ -3,26 +3,23 @@ // $ php examples/cli.php // $ REDIS_URI=localhost:6379 php examples/cli.php -use Clue\React\Redis\Client; -use Clue\React\Redis\Factory; use React\EventLoop\Loop; -use React\Promise\PromiseInterface; require __DIR__ . '/../vendor/autoload.php'; -$factory = new Factory(); +$factory = new Clue\React\Redis\Factory(); echo '# connecting to redis...' . PHP_EOL; -$factory->createClient(getenv('REDIS_URI') ?: 'localhost:6379')->then(function (Client $client) { +$factory->createClient(getenv('REDIS_URI') ?: 'localhost:6379')->then(function (Clue\React\Redis\Client $redis) { echo '# connected! Entering interactive mode, hit CTRL-D to quit' . PHP_EOL; - Loop::addReadStream(STDIN, function () use ($client) { + Loop::addReadStream(STDIN, function () use ($redis) { $line = fgets(STDIN); if ($line === false || $line === '') { echo '# CTRL-D -> Ending connection...' . PHP_EOL; Loop::removeReadStream(STDIN); - return $client->end(); + return $redis->end(); } $line = rtrim($line); @@ -32,10 +29,10 @@ $params = explode(' ', $line); $method = array_shift($params); - $promise = call_user_func_array(array($client, $method), $params); + $promise = call_user_func_array(array($redis, $method), $params); // special method such as end() / close() called - if (!$promise instanceof PromiseInterface) { + if (!$promise instanceof React\Promise\PromiseInterface) { return; } @@ -46,7 +43,7 @@ }); }); - $client->on('close', function() { + $redis->on('close', function() { echo '## DISCONNECTED' . PHP_EOL; Loop::removeReadStream(STDIN); diff --git a/examples/incr.php b/examples/incr.php index 36a24d2..71887e4 100644 --- a/examples/incr.php +++ b/examples/incr.php @@ -3,20 +3,18 @@ // $ php examples/incr.php // $ REDIS_URI=localhost:6379 php examples/incr.php -use Clue\React\Redis\Factory; - require __DIR__ . '/../vendor/autoload.php'; -$factory = new Factory(); -$client = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); +$factory = new Clue\React\Redis\Factory(); +$redis = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); -$client->incr('test'); +$redis->incr('test'); -$client->get('test')->then(function ($result) { +$redis->get('test')->then(function ($result) { var_dump($result); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; exit(1); }); -//$client->end(); +//$redis->end(); diff --git a/examples/publish.php b/examples/publish.php index 5353301..6eb2f9d 100644 --- a/examples/publish.php +++ b/examples/publish.php @@ -3,21 +3,19 @@ // $ php examples/publish.php // $ REDIS_URI=localhost:6379 php examples/publish.php channel message -use Clue\React\Redis\Factory; - require __DIR__ . '/../vendor/autoload.php'; -$factory = new Factory(); -$client = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); +$factory = new Clue\React\Redis\Factory(); +$redis = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); $channel = isset($argv[1]) ? $argv[1] : 'channel'; $message = isset($argv[2]) ? $argv[2] : 'message'; -$client->publish($channel, $message)->then(function ($received) { +$redis->publish($channel, $message)->then(function ($received) { echo 'Successfully published. Received by ' . $received . PHP_EOL; }, function (Exception $e) { echo 'Unable to publish: ' . $e->getMessage() . PHP_EOL; exit(1); }); -$client->end(); +$redis->end(); diff --git a/examples/subscribe.php b/examples/subscribe.php index 4e8c6fe..9f93f2e 100644 --- a/examples/subscribe.php +++ b/examples/subscribe.php @@ -3,33 +3,32 @@ // $ php examples/subscribe.php // $ REDIS_URI=localhost:6379 php examples/subscribe.php channel -use Clue\React\Redis\Factory; use React\EventLoop\Loop; require __DIR__ . '/../vendor/autoload.php'; -$factory = new Factory(); -$client = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); +$factory = new Clue\React\Redis\Factory(); +$redis = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); $channel = isset($argv[1]) ? $argv[1] : 'channel'; -$client->subscribe($channel)->then(function () { +$redis->subscribe($channel)->then(function () { echo 'Now subscribed to channel ' . PHP_EOL; -}, function (Exception $e) use ($client) { - $client->close(); +}, function (Exception $e) use ($redis) { + $redis->close(); echo 'Unable to subscribe: ' . $e->getMessage() . PHP_EOL; }); -$client->on('message', function ($channel, $message) { +$redis->on('message', function ($channel, $message) { echo 'Message on ' . $channel . ': ' . $message . PHP_EOL; }); // automatically re-subscribe to channel on connection issues -$client->on('unsubscribe', function ($channel) use ($client) { +$redis->on('unsubscribe', function ($channel) use ($redis) { echo 'Unsubscribed from ' . $channel . PHP_EOL; - Loop::addPeriodicTimer(2.0, function ($timer) use ($client, $channel){ - $client->subscribe($channel)->then(function () use ($timer) { + Loop::addPeriodicTimer(2.0, function ($timer) use ($redis, $channel){ + $redis->subscribe($channel)->then(function () use ($timer) { echo 'Now subscribed again' . PHP_EOL; Loop::cancelTimer($timer); }, function (Exception $e) { diff --git a/src/Factory.php b/src/Factory.php index 171523c..4e94905 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -103,13 +103,13 @@ public function createClient($uri) $pass = isset($args['password']) ? $args['password'] : (isset($parts['pass']) ? rawurldecode($parts['pass']) : null); if (isset($args['password']) || isset($parts['pass'])) { $pass = isset($args['password']) ? $args['password'] : rawurldecode($parts['pass']); - $promise = $promise->then(function (StreamingClient $client) use ($pass, $uri) { - return $client->auth($pass)->then( - function () use ($client) { - return $client; + $promise = $promise->then(function (StreamingClient $redis) use ($pass, $uri) { + return $redis->auth($pass)->then( + function () use ($redis) { + return $redis; }, - function (\Exception $e) use ($client, $uri) { - $client->close(); + function (\Exception $e) use ($redis, $uri) { + $redis->close(); $const = ''; $errno = $e->getCode(); @@ -131,13 +131,13 @@ function (\Exception $e) use ($client, $uri) { // use `?db=1` query or `/1` path (skip first slash) if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) { $db = isset($args['db']) ? $args['db'] : substr($parts['path'], 1); - $promise = $promise->then(function (StreamingClient $client) use ($db, $uri) { - return $client->select($db)->then( - function () use ($client) { - return $client; + $promise = $promise->then(function (StreamingClient $redis) use ($db, $uri) { + return $redis->select($db)->then( + function () use ($redis) { + return $redis; }, - function (\Exception $e) use ($client, $uri) { - $client->close(); + function (\Exception $e) use ($redis, $uri) { + $redis->close(); $const = ''; $errno = $e->getCode(); diff --git a/src/LazyClient.php b/src/LazyClient.php index c542f6b..42aceca 100644 --- a/src/LazyClient.php +++ b/src/LazyClient.php @@ -53,9 +53,9 @@ private function client() $subscribed =& $this->subscribed; $psubscribed =& $this->psubscribed; $loop = $this->loop; - return $pending = $this->factory->createClient($this->target)->then(function (Client $client) use ($self, &$pending, &$idleTimer, &$subscribed, &$psubscribed, $loop) { + return $pending = $this->factory->createClient($this->target)->then(function (Client $redis) use ($self, &$pending, &$idleTimer, &$subscribed, &$psubscribed, $loop) { // connection completed => remember only until closed - $client->on('close', function () use (&$pending, $self, &$subscribed, &$psubscribed, &$idleTimer, $loop) { + $redis->on('close', function () use (&$pending, $self, &$subscribed, &$psubscribed, &$idleTimer, $loop) { $pending = null; // foward unsubscribe/punsubscribe events when underlying connection closes @@ -77,21 +77,21 @@ private function client() }); // keep track of all channels and patterns this connection is subscribed to - $client->on('subscribe', function ($channel) use (&$subscribed) { + $redis->on('subscribe', function ($channel) use (&$subscribed) { $subscribed[$channel] = true; }); - $client->on('psubscribe', function ($pattern) use (&$psubscribed) { + $redis->on('psubscribe', function ($pattern) use (&$psubscribed) { $psubscribed[$pattern] = true; }); - $client->on('unsubscribe', function ($channel) use (&$subscribed) { + $redis->on('unsubscribe', function ($channel) use (&$subscribed) { unset($subscribed[$channel]); }); - $client->on('punsubscribe', function ($pattern) use (&$psubscribed) { + $redis->on('punsubscribe', function ($pattern) use (&$psubscribed) { unset($psubscribed[$pattern]); }); Util::forwardEvents( - $client, + $redis, $self, array( 'message', @@ -103,7 +103,7 @@ private function client() ) ); - return $client; + return $redis; }, function (\Exception $e) use (&$pending) { // connection failed => discard connection attempt $pending = null; @@ -122,9 +122,9 @@ public function __call($name, $args) } $that = $this; - return $this->client()->then(function (Client $client) use ($name, $args, $that) { + return $this->client()->then(function (Client $redis) use ($name, $args, $that) { $that->awake(); - return \call_user_func_array(array($client, $name), $args)->then( + return \call_user_func_array(array($redis, $name), $args)->then( function ($result) use ($that) { $that->idle(); return $result; @@ -148,11 +148,11 @@ public function end() } $that = $this; - return $this->client()->then(function (Client $client) use ($that) { - $client->on('close', function () use ($that) { + return $this->client()->then(function (Client $redis) use ($that) { + $redis->on('close', function () use ($that) { $that->close(); }); - $client->end(); + $redis->end(); }); } @@ -166,8 +166,8 @@ public function close() // either close active connection or cancel pending connection attempt if ($this->promise !== null) { - $this->promise->then(function (Client $client) { - $client->close(); + $this->promise->then(function (Client $redis) { + $redis->close(); }); if ($this->promise !== null) { $this->promise->cancel(); @@ -208,8 +208,8 @@ public function idle() $idleTimer =& $this->idleTimer; $promise =& $this->promise; $idleTimer = $this->loop->addTimer($this->idlePeriod, function () use (&$idleTimer, &$promise) { - $promise->then(function (Client $client) { - $client->close(); + $promise->then(function (Client $redis) { + $redis->close(); }); $promise = null; $idleTimer = null; diff --git a/tests/FactoryLazyClientTest.php b/tests/FactoryLazyClientTest.php index 33a216c..8b5005b 100644 --- a/tests/FactoryLazyClientTest.php +++ b/tests/FactoryLazyClientTest.php @@ -50,9 +50,9 @@ public function testWillResolveIfConnectorResolves() $stream->expects($this->never())->method('write'); $this->connector->expects($this->never())->method('connect')->willReturn(Promise\resolve($stream)); - $client = $this->factory->createLazyClient('localhost'); + $redis = $this->factory->createLazyClient('localhost'); - $this->assertInstanceOf('Clue\React\Redis\Client', $client); + $this->assertInstanceOf('Clue\React\Redis\Client', $redis); } public function testWillWriteSelectCommandIfTargetContainsPath() @@ -148,15 +148,15 @@ public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter public function testWillRejectIfConnectorRejects() { $this->connector->expects($this->never())->method('connect')->with('127.0.0.1:2')->willReturn(Promise\reject(new \RuntimeException())); - $client = $this->factory->createLazyClient('redis://127.0.0.1:2'); + $redis = $this->factory->createLazyClient('redis://127.0.0.1:2'); - $this->assertInstanceOf('Clue\React\Redis\Client', $client); + $this->assertInstanceOf('Clue\React\Redis\Client', $redis); } public function testWillRejectIfTargetIsInvalid() { - $client = $this->factory->createLazyClient('http://invalid target'); + $redis = $this->factory->createLazyClient('http://invalid target'); - $this->assertInstanceOf('Clue\React\Redis\Client', $client); + $this->assertInstanceOf('Clue\React\Redis\Client', $redis); } } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 7e70b71..45a9fcd 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -32,9 +32,9 @@ public function setUpFactory() public function testPing() { - $client = $this->createClient($this->uri); + $redis = $this->createClient($this->uri); - $promise = $client->ping(); + $promise = $redis->ping(); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); $ret = Block\await($promise, $this->loop); @@ -44,9 +44,9 @@ public function testPing() public function testPingLazy() { - $client = $this->factory->createLazyClient($this->uri); + $redis = $this->factory->createLazyClient($this->uri); - $promise = $client->ping(); + $promise = $redis->ping(); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); $ret = Block\await($promise, $this->loop); @@ -59,9 +59,9 @@ public function testPingLazy() */ public function testPingLazyWillNotBlockLoopWhenIdleTimeIsSmall() { - $client = $this->factory->createLazyClient($this->uri . '?idle=0'); + $redis = $this->factory->createLazyClient($this->uri . '?idle=0'); - $client->ping(); + $redis->ping(); $this->loop->run(); } @@ -71,41 +71,41 @@ public function testPingLazyWillNotBlockLoopWhenIdleTimeIsSmall() */ public function testLazyClientWithoutCommandsWillNotBlockLoop() { - $client = $this->factory->createLazyClient($this->uri); + $redis = $this->factory->createLazyClient($this->uri); $this->loop->run(); - unset($client); + unset($redis); } public function testMgetIsNotInterpretedAsSubMessage() { - $client = $this->createClient($this->uri); + $redis = $this->createClient($this->uri); - $client->mset('message', 'message', 'channel', 'channel', 'payload', 'payload'); + $redis->mset('message', 'message', 'channel', 'channel', 'payload', 'payload'); - $promise = $client->mget('message', 'channel', 'payload')->then($this->expectCallableOnce()); - $client->on('message', $this->expectCallableNever()); + $promise = $redis->mget('message', 'channel', 'payload')->then($this->expectCallableOnce()); + $redis->on('message', $this->expectCallableNever()); Block\await($promise, $this->loop); } public function testPipeline() { - $client = $this->createClient($this->uri); + $redis = $this->createClient($this->uri); - $client->set('a', 1)->then($this->expectCallableOnceWith('OK')); - $client->incr('a')->then($this->expectCallableOnceWith(2)); - $client->incr('a')->then($this->expectCallableOnceWith(3)); - $promise = $client->get('a')->then($this->expectCallableOnceWith('3')); + $redis->set('a', 1)->then($this->expectCallableOnceWith('OK')); + $redis->incr('a')->then($this->expectCallableOnceWith(2)); + $redis->incr('a')->then($this->expectCallableOnceWith(3)); + $promise = $redis->get('a')->then($this->expectCallableOnceWith('3')); Block\await($promise, $this->loop); } public function testInvalidCommand() { - $client = $this->createClient($this->uri); - $promise = $client->doesnotexist(1, 2, 3); + $redis = $this->createClient($this->uri); + $promise = $redis->doesnotexist(1, 2, 3); if (method_exists($this, 'expectException')) { $this->expectException('Exception'); @@ -117,23 +117,23 @@ public function testInvalidCommand() public function testMultiExecEmpty() { - $client = $this->createClient($this->uri); - $client->multi()->then($this->expectCallableOnceWith('OK')); - $promise = $client->exec()->then($this->expectCallableOnceWith(array())); + $redis = $this->createClient($this->uri); + $redis->multi()->then($this->expectCallableOnceWith('OK')); + $promise = $redis->exec()->then($this->expectCallableOnceWith(array())); Block\await($promise, $this->loop); } public function testMultiExecQueuedExecHasValues() { - $client = $this->createClient($this->uri); + $redis = $this->createClient($this->uri); - $client->multi()->then($this->expectCallableOnceWith('OK')); - $client->set('b', 10)->then($this->expectCallableOnceWith('QUEUED')); - $client->expire('b', 20)->then($this->expectCallableOnceWith('QUEUED')); - $client->incrBy('b', 2)->then($this->expectCallableOnceWith('QUEUED')); - $client->ttl('b')->then($this->expectCallableOnceWith('QUEUED')); - $promise = $client->exec()->then($this->expectCallableOnceWith(array('OK', 1, 12, 20))); + $redis->multi()->then($this->expectCallableOnceWith('OK')); + $redis->set('b', 10)->then($this->expectCallableOnceWith('QUEUED')); + $redis->expire('b', 20)->then($this->expectCallableOnceWith('QUEUED')); + $redis->incrBy('b', 2)->then($this->expectCallableOnceWith('QUEUED')); + $redis->ttl('b')->then($this->expectCallableOnceWith('QUEUED')); + $promise = $redis->exec()->then($this->expectCallableOnceWith(array('OK', 1, 12, 20))); Block\await($promise, $this->loop); } @@ -161,34 +161,34 @@ public function testPubSub() public function testClose() { - $client = $this->createClient($this->uri); + $redis = $this->createClient($this->uri); - $client->get('willBeCanceledAnyway')->then(null, $this->expectCallableOnce()); + $redis->get('willBeCanceledAnyway')->then(null, $this->expectCallableOnce()); - $client->close(); + $redis->close(); - $client->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce()); + $redis->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce()); } public function testCloseLazy() { - $client = $this->factory->createLazyClient($this->uri); + $redis = $this->factory->createLazyClient($this->uri); - $client->get('willBeCanceledAnyway')->then(null, $this->expectCallableOnce()); + $redis->get('willBeCanceledAnyway')->then(null, $this->expectCallableOnce()); - $client->close(); + $redis->close(); - $client->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce()); + $redis->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce()); } public function testInvalidProtocol() { - $client = $this->createClientResponse("communication does not conform to protocol\r\n"); + $redis = $this->createClientResponse("communication does not conform to protocol\r\n"); - $client->on('error', $this->expectCallableOnce()); - $client->on('close', $this->expectCallableOnce()); + $redis->on('error', $this->expectCallableOnce()); + $redis->on('close', $this->expectCallableOnce()); - $promise = $client->get('willBeRejectedDueToClosing'); + $promise = $redis->get('willBeRejectedDueToClosing'); if (method_exists($this, 'expectException')) { $this->expectException('Exception'); @@ -200,12 +200,12 @@ public function testInvalidProtocol() public function testInvalidServerRepliesWithDuplicateMessages() { - $client = $this->createClientResponse("+OK\r\n-ERR invalid\r\n"); + $redis = $this->createClientResponse("+OK\r\n-ERR invalid\r\n"); - $client->on('error', $this->expectCallableOnce()); - $client->on('close', $this->expectCallableOnce()); + $redis->on('error', $this->expectCallableOnce()); + $redis->on('close', $this->expectCallableOnce()); - $promise = $client->set('a', 0)->then($this->expectCallableOnceWith('OK')); + $promise = $redis->set('a', 0)->then($this->expectCallableOnceWith('OK')); Block\await($promise, $this->loop); } diff --git a/tests/LazyClientTest.php b/tests/LazyClientTest.php index 5510ec9..2ad644e 100644 --- a/tests/LazyClientTest.php +++ b/tests/LazyClientTest.php @@ -19,7 +19,7 @@ class LazyClientTest extends TestCase { private $factory; private $loop; - private $client; + private $redis; /** * @before @@ -29,7 +29,7 @@ public function setUpClient() $this->factory = $this->getMockBuilder('Clue\React\Redis\Factory')->disableOriginalConstructor()->getMock(); $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $this->client = new LazyClient('localhost', $this->factory, $this->loop); + $this->redis = new LazyClient('localhost', $this->factory, $this->loop); } public function testPingWillCreateUnderlyingClientAndReturnPendingPromise() @@ -39,7 +39,7 @@ public function testPingWillCreateUnderlyingClientAndReturnPendingPromise() $this->loop->expects($this->never())->method('addTimer'); - $promise = $this->client->ping(); + $promise = $this->redis->ping(); $promise->then($this->expectCallableNever()); } @@ -49,8 +49,8 @@ public function testPingTwiceWillCreateOnceUnderlyingClient() $promise = new Promise(function () { }); $this->factory->expects($this->once())->method('createClient')->willReturn($promise); - $this->client->ping(); - $this->client->ping(); + $this->redis->ping(); + $this->redis->ping(); } public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimer() @@ -63,7 +63,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT $this->loop->expects($this->once())->method('addTimer')->with(60.0, $this->anything()); - $promise = $this->client->ping(); + $promise = $this->redis->ping(); $deferred->resolve($client); $promise->then($this->expectCallableOnceWith('PONG')); @@ -71,7 +71,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimerWithIdleTimeFromQueryParam() { - $this->client = new LazyClient('localhost?idle=10', $this->factory, $this->loop); + $this->redis = new LazyClient('localhost?idle=10', $this->factory, $this->loop); $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); @@ -81,7 +81,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT $this->loop->expects($this->once())->method('addTimer')->with(10.0, $this->anything()); - $promise = $this->client->ping(); + $promise = $this->redis->ping(); $deferred->resolve($client); $promise->then($this->expectCallableOnceWith('PONG')); @@ -89,7 +89,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartIdleTimerWhenIdleParamIsNegative() { - $this->client = new LazyClient('localhost?idle=-1', $this->factory, $this->loop); + $this->redis = new LazyClient('localhost?idle=-1', $this->factory, $this->loop); $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); @@ -99,7 +99,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartId $this->loop->expects($this->never())->method('addTimer'); - $promise = $this->client->ping(); + $promise = $this->redis->ping(); $deferred->resolve($client); $promise->then($this->expectCallableOnceWith('PONG')); @@ -116,7 +116,7 @@ public function testPingWillRejectWhenUnderlyingClientRejectsPingAndStartIdleTim $this->loop->expects($this->once())->method('addTimer'); - $promise = $this->client->ping(); + $promise = $this->redis->ping(); $deferred->resolve($client); $promise->then(null, $this->expectCallableOnceWith($error)); @@ -129,10 +129,10 @@ public function testPingWillRejectAndNotEmitErrorOrCloseWhenFactoryRejectsUnderl $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->client->on('error', $this->expectCallableNever()); - $this->client->on('close', $this->expectCallableNever()); + $this->redis->on('error', $this->expectCallableNever()); + $this->redis->on('close', $this->expectCallableNever()); - $promise = $this->client->ping(); + $promise = $this->redis->ping(); $deferred->reject($error); $promise->then(null, $this->expectCallableOnceWith($error)); @@ -148,10 +148,10 @@ public function testPingAfterPreviousFactoryRejectsUnderlyingClientWillCreateNew new Promise(function () { }) ); - $this->client->ping(); + $this->redis->ping(); $deferred->reject($error); - $this->client->ping(); + $this->redis->ping(); } public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewUnderlyingConnection() @@ -171,19 +171,19 @@ public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewU new Promise(function () { }) ); - $this->client->ping(); + $this->redis->ping(); $this->assertTrue(is_callable($closeHandler)); $closeHandler(); - $this->client->ping(); + $this->redis->ping(); } public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection() { $this->factory->expects($this->never())->method('createClient'); - $this->client->close(); - $promise = $this->client->ping(); + $this->redis->close(); + $promise = $this->redis->ping(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -211,8 +211,8 @@ public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() $this->loop->expects($this->never())->method('addTimer'); - $this->client->ping(); - $this->client->ping(); + $this->redis->ping(); + $this->redis->ping(); $deferred->resolve(); } @@ -231,9 +231,9 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); $this->loop->expects($this->once())->method('cancelTimer')->with($timer); - $this->client->ping(); + $this->redis->ping(); $deferred->resolve(); - $this->client->ping(); + $this->redis->ping(); } public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent() @@ -251,9 +251,9 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutC return true; }))->willReturn($timer); - $this->client->on('close', $this->expectCallableNever()); + $this->redis->on('close', $this->expectCallableNever()); - $this->client->ping(); + $this->redis->ping(); $this->assertNotNull($timeout); $timeout(); @@ -263,17 +263,17 @@ public function testCloseWillEmitCloseEventWithoutCreatingUnderlyingClient() { $this->factory->expects($this->never())->method('createClient'); - $this->client->on('close', $this->expectCallableOnce()); + $this->redis->on('close', $this->expectCallableOnce()); - $this->client->close(); + $this->redis->close(); } public function testCloseTwiceWillEmitCloseEventOnce() { - $this->client->on('close', $this->expectCallableOnce()); + $this->redis->on('close', $this->expectCallableOnce()); - $this->client->close(); - $this->client->close(); + $this->redis->close(); + $this->redis->close(); } public function testCloseAfterPingWillCancelUnderlyingClientConnectionWhenStillPending() @@ -281,8 +281,8 @@ public function testCloseAfterPingWillCancelUnderlyingClientConnectionWhenStillP $promise = new Promise(function () { }, $this->expectCallableOnce()); $this->factory->expects($this->once())->method('createClient')->willReturn($promise); - $this->client->ping(); - $this->client->close(); + $this->redis->ping(); + $this->redis->close(); } public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientConnectionThrowsDueToCancellation() @@ -292,11 +292,11 @@ public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientC }); $this->factory->expects($this->once())->method('createClient')->willReturn($promise); - $this->client->on('error', $this->expectCallableNever()); - $this->client->on('close', $this->expectCallableOnce()); + $this->redis->on('error', $this->expectCallableNever()); + $this->redis->on('close', $this->expectCallableOnce()); - $this->client->ping(); - $this->client->close(); + $this->redis->ping(); + $this->redis->close(); } public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlreadyResolved() @@ -308,9 +308,9 @@ public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlready $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->client->ping(); + $this->redis->ping(); $deferred->resolve($client); - $this->client->close(); + $this->redis->close(); } public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() @@ -326,9 +326,9 @@ public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); $this->loop->expects($this->once())->method('cancelTimer')->with($timer); - $this->client->ping(); + $this->redis->ping(); $deferred->resolve(); - $this->client->close(); + $this->redis->close(); } public function testCloseAfterPingRejectsWillEmitClose() @@ -346,7 +346,7 @@ public function testCloseAfterPingRejectsWillEmitClose() $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); $this->loop->expects($this->once())->method('cancelTimer')->with($timer); - $ref = $this->client; + $ref = $this->redis; $ref->ping()->then(null, function () use ($ref, $client) { $ref->close(); }); @@ -356,8 +356,8 @@ public function testCloseAfterPingRejectsWillEmitClose() public function testEndWillCloseClientIfUnderlyingConnectionIsNotPending() { - $this->client->on('close', $this->expectCallableOnce()); - $this->client->end(); + $this->redis->on('close', $this->expectCallableOnce()); + $this->redis->end(); } public function testEndAfterPingWillEndUnderlyingClient() @@ -369,9 +369,9 @@ public function testEndAfterPingWillEndUnderlyingClient() $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->client->ping(); + $this->redis->ping(); $deferred->resolve($client); - $this->client->end(); + $this->redis->end(); } public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose() @@ -389,11 +389,11 @@ public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose() $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->client->ping(); + $this->redis->ping(); $deferred->resolve($client); - $this->client->on('close', $this->expectCallableOnce()); - $this->client->end(); + $this->redis->on('close', $this->expectCallableOnce()); + $this->redis->end(); $this->assertTrue(is_callable($closeHandler)); $closeHandler(); @@ -409,10 +409,10 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->client->ping(); + $this->redis->ping(); $deferred->resolve($client); - $this->client->on('error', $this->expectCallableNever()); + $this->redis->on('error', $this->expectCallableNever()); $client->emit('error', array($error)); } @@ -424,10 +424,10 @@ public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->client->ping(); + $this->redis->ping(); $deferred->resolve($client); - $this->client->on('close', $this->expectCallableNever()); + $this->redis->on('close', $this->expectCallableNever()); $client->emit('close'); } @@ -450,9 +450,9 @@ public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnect $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); $this->loop->expects($this->once())->method('cancelTimer')->with($timer); - $this->client->on('close', $this->expectCallableNever()); + $this->redis->on('close', $this->expectCallableNever()); - $this->client->ping(); + $this->redis->ping(); $deferred->resolve(); $this->assertTrue(is_callable($closeHandler)); @@ -473,10 +473,10 @@ public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubCh $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->client->subscribe('foo'); + $this->redis->subscribe('foo'); $deferred->resolve($client); - $this->client->on('message', $this->expectCallableOnce()); + $this->redis->on('message', $this->expectCallableOnce()); $this->assertTrue(is_callable($messageHandler)); $messageHandler('foo', 'bar'); } @@ -494,32 +494,32 @@ public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClo $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); - $this->client->subscribe('foo'); + $this->redis->subscribe('foo'); $this->assertTrue(is_callable($allHandler['subscribe'])); $allHandler['subscribe']('foo', 1); - $this->client->subscribe('bar'); + $this->redis->subscribe('bar'); $this->assertTrue(is_callable($allHandler['subscribe'])); $allHandler['subscribe']('bar', 2); - $this->client->unsubscribe('bar'); + $this->redis->unsubscribe('bar'); $this->assertTrue(is_callable($allHandler['unsubscribe'])); $allHandler['unsubscribe']('bar', 1); - $this->client->psubscribe('foo*'); + $this->redis->psubscribe('foo*'); $this->assertTrue(is_callable($allHandler['psubscribe'])); $allHandler['psubscribe']('foo*', 1); - $this->client->psubscribe('bar*'); + $this->redis->psubscribe('bar*'); $this->assertTrue(is_callable($allHandler['psubscribe'])); $allHandler['psubscribe']('bar*', 2); - $this->client->punsubscribe('bar*'); + $this->redis->punsubscribe('bar*'); $this->assertTrue(is_callable($allHandler['punsubscribe'])); $allHandler['punsubscribe']('bar*', 1); - $this->client->on('unsubscribe', $this->expectCallableOnce()); - $this->client->on('punsubscribe', $this->expectCallableOnce()); + $this->redis->on('unsubscribe', $this->expectCallableOnce()); + $this->redis->on('punsubscribe', $this->expectCallableOnce()); $this->assertTrue(is_callable($allHandler['close'])); $allHandler['close'](); @@ -541,7 +541,7 @@ public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAnd $this->loop->expects($this->never())->method('addTimer'); - $promise = $this->client->subscribe('foo'); + $promise = $this->redis->subscribe('foo'); $this->assertTrue(is_callable($subscribeHandler)); $subscribeHandler('foo', 1); $deferred->resolve(array('subscribe', 'foo', 1)); @@ -570,13 +570,13 @@ public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientReso $this->loop->expects($this->once())->method('addTimer'); - $promise = $this->client->subscribe('foo'); + $promise = $this->redis->subscribe('foo'); $this->assertTrue(is_callable($subscribeHandler)); $subscribeHandler('foo', 1); $deferredSubscribe->resolve(array('subscribe', 'foo', 1)); $promise->then($this->expectCallableOnceWith(array('subscribe', 'foo', 1))); - $promise = $this->client->unsubscribe('foo'); + $promise = $this->redis->unsubscribe('foo'); $this->assertTrue(is_callable($unsubscribeHandler)); $unsubscribeHandler('foo', 0); $deferredUnsubscribe->resolve(array('unsubscribe', 'foo', 0)); @@ -600,7 +600,7 @@ public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResp $this->loop->expects($this->never())->method('addTimer'); - $promise = $this->client->blpop('list'); + $promise = $this->redis->blpop('list'); $this->assertTrue(is_callable($closeHandler)); $closeHandler(); diff --git a/tests/StreamingClientTest.php b/tests/StreamingClientTest.php index af54942..bff6ca1 100644 --- a/tests/StreamingClientTest.php +++ b/tests/StreamingClientTest.php @@ -16,7 +16,7 @@ class StreamingClientTest extends TestCase private $stream; private $parser; private $serializer; - private $client; + private $redis; /** * @before @@ -27,27 +27,27 @@ public function setUpClient() $this->parser = $this->getMockBuilder('Clue\Redis\Protocol\Parser\ParserInterface')->getMock(); $this->serializer = $this->getMockBuilder('Clue\Redis\Protocol\Serializer\SerializerInterface')->getMock(); - $this->client = new StreamingClient($this->stream, $this->parser, $this->serializer); + $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); } public function testConstructWithoutParserAssignsParserAutomatically() { - $this->client = new StreamingClient($this->stream, null, $this->serializer); + $this->redis = new StreamingClient($this->stream, null, $this->serializer); - $ref = new \ReflectionProperty($this->client, 'parser'); + $ref = new \ReflectionProperty($this->redis, 'parser'); $ref->setAccessible(true); - $parser = $ref->getValue($this->client); + $parser = $ref->getValue($this->redis); $this->assertInstanceOf('Clue\Redis\Protocol\Parser\ParserInterface', $parser); } public function testConstructWithoutParserAndSerializerAssignsParserAndSerializerAutomatically() { - $this->client = new StreamingClient($this->stream, $this->parser); + $this->redis = new StreamingClient($this->stream, $this->parser); - $ref = new \ReflectionProperty($this->client, 'serializer'); + $ref = new \ReflectionProperty($this->redis, 'serializer'); $ref->setAccessible(true); - $serializer = $ref->getValue($this->client); + $serializer = $ref->getValue($this->redis); $this->assertInstanceOf('Clue\Redis\Protocol\Serializer\SerializerInterface', $serializer); } @@ -57,22 +57,22 @@ public function testSending() $this->serializer->expects($this->once())->method('getRequestMessage')->with($this->equalTo('ping'))->will($this->returnValue('message')); $this->stream->expects($this->once())->method('write')->with($this->equalTo('message')); - $this->client->ping(); + $this->redis->ping(); } public function testClosingClientEmitsEvent() { - $this->client->on('close', $this->expectCallableOnce()); + $this->redis->on('close', $this->expectCallableOnce()); - $this->client->close(); + $this->redis->close(); } public function testClosingStreamClosesClient() { $this->stream = new ThroughStream(); - $this->client = new StreamingClient($this->stream, $this->parser, $this->serializer); + $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); - $this->client->on('close', $this->expectCallableOnce()); + $this->redis->on('close', $this->expectCallableOnce()); $this->stream->emit('close'); } @@ -80,9 +80,9 @@ public function testClosingStreamClosesClient() public function testReceiveParseErrorEmitsErrorEvent() { $this->stream = new ThroughStream(); - $this->client = new StreamingClient($this->stream, $this->parser, $this->serializer); + $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); - $this->client->on('error', $this->expectCallableOnceWith( + $this->redis->on('error', $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('UnexpectedValueException'), $this->callback(function (\UnexpectedValueException $e) { @@ -93,7 +93,7 @@ public function testReceiveParseErrorEmitsErrorEvent() }) ) )); - $this->client->on('close', $this->expectCallableOnce()); + $this->redis->on('close', $this->expectCallableOnce()); $this->parser->expects($this->once())->method('pushIncoming')->with('message')->willThrowException(new ParserException('Foo')); $this->stream->emit('data', array('message')); @@ -102,10 +102,10 @@ public function testReceiveParseErrorEmitsErrorEvent() public function testReceiveUnexpectedReplyEmitsErrorEvent() { $this->stream = new ThroughStream(); - $this->client = new StreamingClient($this->stream, $this->parser, $this->serializer); + $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); - $this->client->on('error', $this->expectCallableOnce()); - $this->client->on('error', $this->expectCallableOnceWith( + $this->redis->on('error', $this->expectCallableOnce()); + $this->redis->on('error', $this->expectCallableOnceWith( $this->logicalAnd( $this->isInstanceOf('UnderflowException'), $this->callback(function (\UnderflowException $e) { @@ -134,9 +134,9 @@ public function testPingPong() { $this->serializer->expects($this->once())->method('getRequestMessage')->with($this->equalTo('ping')); - $promise = $this->client->ping(); + $promise = $this->redis->ping(); - $this->client->handleMessage(new BulkReply('PONG')); + $this->redis->handleMessage(new BulkReply('PONG')); $this->expectPromiseResolve($promise); $promise->then($this->expectCallableOnceWith('PONG')); @@ -144,7 +144,7 @@ public function testPingPong() public function testMonitorCommandIsNotSupported() { - $promise = $this->client->monitor(); + $promise = $this->redis->monitor(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -161,18 +161,18 @@ public function testMonitorCommandIsNotSupported() public function testErrorReply() { - $promise = $this->client->invalid(); + $promise = $this->redis->invalid(); $err = new ErrorReply("ERR unknown command 'invalid'"); - $this->client->handleMessage($err); + $this->redis->handleMessage($err); $promise->then(null, $this->expectCallableOnceWith($err)); } public function testClosingClientRejectsAllRemainingRequests() { - $promise = $this->client->ping(); - $this->client->close(); + $promise = $this->redis->ping(); + $this->redis->close(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -191,9 +191,9 @@ public function testClosingStreamRejectsAllRemainingRequests() { $this->stream = new ThroughStream(); $this->parser->expects($this->once())->method('pushIncoming')->willReturn(array()); - $this->client = new StreamingClient($this->stream, $this->parser, $this->serializer); + $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); - $promise = $this->client->ping(); + $promise = $this->redis->ping(); $this->stream->close(); $promise->then(null, $this->expectCallableOnceWith( @@ -211,9 +211,9 @@ public function testClosingStreamRejectsAllRemainingRequests() public function testEndingClientRejectsAllNewRequests() { - $this->client->ping(); - $this->client->end(); - $promise = $this->client->ping(); + $this->redis->ping(); + $this->redis->end(); + $promise = $this->redis->ping(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -230,8 +230,8 @@ public function testEndingClientRejectsAllNewRequests() public function testClosedClientRejectsAllNewRequests() { - $this->client->close(); - $promise = $this->client->ping(); + $this->redis->close(); + $promise = $this->redis->ping(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -248,35 +248,35 @@ public function testClosedClientRejectsAllNewRequests() public function testEndingNonBusyClosesClient() { - $this->client->on('close', $this->expectCallableOnce()); - $this->client->end(); + $this->redis->on('close', $this->expectCallableOnce()); + $this->redis->end(); } public function testEndingBusyClosesClientWhenNotBusyAnymore() { // count how often the "close" method has been called $closed = 0; - $this->client->on('close', function() use (&$closed) { + $this->redis->on('close', function() use (&$closed) { ++$closed; }); - $promise = $this->client->ping(); + $promise = $this->redis->ping(); $this->assertEquals(0, $closed); - $this->client->end(); + $this->redis->end(); $this->assertEquals(0, $closed); - $this->client->handleMessage(new BulkReply('PONG')); + $this->redis->handleMessage(new BulkReply('PONG')); $promise->then($this->expectCallableOnceWith('PONG')); $this->assertEquals(1, $closed); } public function testClosingMultipleTimesEmitsOnce() { - $this->client->on('close', $this->expectCallableOnce()); + $this->redis->on('close', $this->expectCallableOnce()); - $this->client->close(); - $this->client->close(); + $this->redis->close(); + $this->redis->close(); } public function testReceivingUnexpectedMessageThrowsException() @@ -286,18 +286,18 @@ public function testReceivingUnexpectedMessageThrowsException() } else { $this->setExpectedException('UnderflowException'); } - $this->client->handleMessage(new BulkReply('PONG')); + $this->redis->handleMessage(new BulkReply('PONG')); } public function testPubsubSubscribe() { - $promise = $this->client->subscribe('test'); + $promise = $this->redis->subscribe('test'); $this->expectPromiseResolve($promise); - $this->client->on('subscribe', $this->expectCallableOnce()); - $this->client->handleMessage(new MultiBulkReply(array(new BulkReply('subscribe'), new BulkReply('test'), new IntegerReply(1)))); + $this->redis->on('subscribe', $this->expectCallableOnce()); + $this->redis->handleMessage(new MultiBulkReply(array(new BulkReply('subscribe'), new BulkReply('test'), new IntegerReply(1)))); - return $this->client; + return $this->redis; } /** @@ -327,7 +327,7 @@ public function testPubsubMessage(Client $client) public function testSubscribeWithMultipleArgumentsRejects() { - $promise = $this->client->subscribe('a', 'b'); + $promise = $this->redis->subscribe('a', 'b'); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -344,7 +344,7 @@ public function testSubscribeWithMultipleArgumentsRejects() public function testUnsubscribeWithoutArgumentsRejects() { - $promise = $this->client->unsubscribe(); + $promise = $this->redis->unsubscribe(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( From bd3dba15f807f185571b97f0d1e0e14b8fac3266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 13 Mar 2021 11:29:04 +0100 Subject: [PATCH 23/65] Minor documentation improvements --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3b4a5e9..1b3d113 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,10 @@ Once [installed](#install), you can use the following code to connect to your local Redis server and send some requests: ```php +createLazyClient('localhost:6379'); @@ -506,7 +510,7 @@ in seconds (or use a negative number to not apply a timeout) like this: $factory->createLazyClient('localhost?timeout=0.5'); ``` -By default, this method will keep "idle" connection open for 60s and will +By default, this method will keep "idle" connections 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 @@ -599,7 +603,7 @@ 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 project follows [SemVer](https://semver.org/). @@ -619,7 +623,7 @@ It's *highly recommended to use PHP 7+* for this project. ## 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 @@ -628,7 +632,7 @@ $ composer install To run the test suite, go to the project root and run: ```bash -$ php vendor/bin/phpunit +$ vendor/bin/phpunit ``` The test suite contains both unit tests and functional integration tests. @@ -645,7 +649,7 @@ To now run the functional tests, you need to supply *your* login details in an environment variable like this: ```bash -$ REDIS_URI=localhost:6379 php vendor/bin/phpunit +$ REDIS_URI=localhost:6379 vendor/bin/phpunit ``` ## License From 61a979ff491d9e2492d65a3f65638905e2a49dec Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 31 Aug 2021 10:39:31 +0200 Subject: [PATCH 24/65] Prepare v2.5.0 release --- CHANGELOG.md | 22 ++++++++++++++++++++++ README.md | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e53491f..1492177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 2.5.0 (2021-08-31) + +* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop) and new Socket API. + (#114 and #115 by @SimonFrings) + + ```php + // old (still supported) + $factory = new Clue\React\Redis\Factory($loop); + + // new (using default loop) + $factory = new Clue\React\Redis\Factory(); + ``` + +* Feature: Improve error reporting, include Redis URI and socket error codes in all connection errors. + (#116 by @clue) + +* Documentation improvements and updated examples. + (#117 by @clue, #112 by @Nyholm and #113 by @PaulRotmann) + +* Improve test suite and use GitHub actions for continuous integration (CI). + (#111 by @SimonFrings) + ## 2.4.0 (2020-09-25) * Fix: Fix dangling timer when lazy connection closes with pending commands. diff --git a/README.md b/README.md index 1b3d113..9d33d21 100644 --- a/README.md +++ b/README.md @@ -610,7 +610,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require clue/redis-react:^2.4 +$ composer require clue/redis-react:^2.5 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -618,7 +618,7 @@ See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. -It's *highly recommended to use PHP 7+* for this project. +It's *highly recommended to use the latest supported PHP version* for this project. ## Tests From 3a091b8785792f0fbd943804095692a4ba5fb0c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Dec 2021 11:44:18 +0100 Subject: [PATCH 25/65] Support PHP 8.1 --- .github/workflows/ci.yml | 1 + composer.json | 2 +- phpunit.xml.dist | 3 ++- src/LazyClient.php | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a9676b..10341d1 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 3f8c7af..c1752cc 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/event-loop": "^1.2", "react/promise": "^2.0 || ^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 e19a12c..5093fa5 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,8 +4,9 @@ + convertDeprecationsToExceptions="true"> ./tests/ diff --git a/src/LazyClient.php b/src/LazyClient.php index 42aceca..d82b257 100644 --- a/src/LazyClient.php +++ b/src/LazyClient.php @@ -31,7 +31,7 @@ class LazyClient extends EventEmitter implements Client public function __construct($target, Factory $factory, LoopInterface $loop) { $args = array(); - \parse_str(\parse_url($target, \PHP_URL_QUERY), $args); + \parse_str((string) \parse_url($target, \PHP_URL_QUERY), $args); if (isset($args['idle'])) { $this->idlePeriod = (float)$args['idle']; } From c9f6ca47a4a64635957935f7c84c068fa66aa2bd Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Tue, 26 Apr 2022 10:59:22 +0200 Subject: [PATCH 26/65] 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 10341d1..79e1ca3 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) install - run: docker run --net=host -d redis - run: REDIS_URI=localhost:6379 hhvm vendor/bin/phpunit diff --git a/README.md b/README.md index 9d33d21..b728334 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # clue/reactphp-redis -[![CI status](https://github.com/clue/reactphp-redis/workflows/CI/badge.svg)](https://github.com/clue/reactphp-redis/actions) +[![CI status](https://github.com/clue/reactphp-redis/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-redis/actions) [![installs on Packagist](https://img.shields.io/packagist/dt/clue/redis-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/redis-react) Async [Redis](https://redis.io/) client implementation, built on top of [ReactPHP](https://reactphp.org/). From a1070d3c657e834f69451ec807ef8c0b95390b99 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Mon, 9 May 2022 11:37:22 +0200 Subject: [PATCH 27/65] Fix code examples in documentation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b728334..8f29cc5 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ interface that makes it easy to react to when a command is completed (i.e. either successfully fulfilled or rejected with an error): ```php -$redis->get($key)->then(function (string $value) { +$redis->get($key)->then(function (?string $value) { var_dump($value); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; @@ -541,7 +541,7 @@ Each method call matches the respective [Redis command](https://redis.io/command For example, the `$redis->get()` method will invoke the [`GET` command](https://redis.io/commands/get). ```php -$redis->get($key)->then(function (string $value) { +$redis->get($key)->then(function (?string $value) { var_dump($value); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; From f911455f9d7a77dd6f39c22548ddff521544b291 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Mon, 9 May 2022 11:50:02 +0200 Subject: [PATCH 28/65] Prepare v2.6.0 release --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1492177..c5e8e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.6.0 (2022-05-09) + +* Feature: Support PHP 8.1 release. + (#119 by @clue) + +* Improve documentation and CI configuration. + (#123 and #125 by @SimonFrings) + ## 2.5.0 (2021-08-31) * Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop) and new Socket API. diff --git a/README.md b/README.md index 8f29cc5..5492572 100644 --- a/README.md +++ b/README.md @@ -610,7 +610,7 @@ This project follows [SemVer](https://semver.org/). This will install the latest supported version: ```bash -$ composer require clue/redis-react:^2.5 +$ composer require clue/redis-react:^2.6 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. From edd918f3750d2cb84acd7b0a5b0ca9ab6485f8eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 12 May 2022 13:29:41 +0200 Subject: [PATCH 29/65] Hello `3.x` development branch --- README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5492572..aef11c1 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,15 @@ Async [Redis](https://redis.io/) client implementation, built on top of [ReactPHP](https://reactphp.org/). +> **Development version:** This branch contains the code for the upcoming 3.0 release. +> For the code of the current stable 2.x release, check out the +> [`2.x` branch](https://github.com/reactphp/promise/tree/2.x). +> +> The upcoming 3.0 release will be the way forward for this package. +> However, we will still actively support 2.x for those not yet +> on the latest version. +> See also [installation instructions](#install) for more details. + [Redis](https://redis.io/) is an open source, advanced, in-memory key-value database. It offers a set of simple, atomic operations in order to work with its primitive data types. Its lightweight design and fast operation makes it an ideal candidate for modern application stacks. @@ -606,11 +615,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 clue/redis-react:^2.6 +$ composer require clue/redis-react:^3@dev ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -620,6 +629,14 @@ extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. It's *highly recommended to use the latest supported PHP version* for this project. +We're committed to providing long-term support (LTS) options and to provide a +smooth upgrade path. You may target multiple versions at the same time to +support a wider range of PHP versions like this: + +```bash +$ composer require "clue/redis-react:^3@dev || ^2" +``` + ## Tests To run the test suite, you first need to clone this repo and then install all From 97e0478326d60bcf15fdd90dce564a9afa1db323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 17 May 2022 20:41:56 +0200 Subject: [PATCH 30/65] Update to require PHP 7.1+ --- .github/workflows/ci.yml | 19 ------------------- README.md | 5 ++--- composer.json | 4 ++-- phpunit.xml.legacy | 2 +- 4 files changed, 5 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79e1ca3..ff52770 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,11 +17,6 @@ jobs: - 7.3 - 7.2 - 7.1 - - 7.0 - - 5.6 - - 5.5 - - 5.4 - - 5.3 steps: - uses: actions/checkout@v2 - uses: shivammathur/setup-php@v2 @@ -34,17 +29,3 @@ jobs: if: ${{ matrix.php >= 7.3 }} - run: REDIS_URI=localhost:6379 vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy if: ${{ matrix.php < 7.3 }} - - PHPUnit-hhvm: - name: PHPUnit (HHVM) - runs-on: ubuntu-18.04 - continue-on-error: true - steps: - - uses: actions/checkout@v2 - - uses: azjezz/setup-hhvm@v1 - with: - version: lts-3.30 - - run: composer self-update --2.2 # downgrade Composer for HHVM - - run: hhvm $(which composer) install - - run: docker run --net=host -d redis - - run: REDIS_URI=localhost:6379 hhvm vendor/bin/phpunit diff --git a/README.md b/README.md index aef11c1..2d66d05 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Async [Redis](https://redis.io/) client implementation, built on top of [ReactPH > **Development version:** This branch contains the code for the upcoming 3.0 release. > For the code of the current stable 2.x release, check out the -> [`2.x` branch](https://github.com/reactphp/promise/tree/2.x). +> [`2.x` branch](https://github.com/clue/reactphp-redis/tree/2.x). > > The upcoming 3.0 release will be the way forward for this package. > However, we will still actively support 2.x for those not yet @@ -625,8 +625,7 @@ $ composer require clue/redis-react:^3@dev See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP -extensions and supports running on legacy PHP 5.3 through current PHP 8+ and -HHVM. +extensions and supports running on PHP 7.1 through current PHP 8+. It's *highly recommended to use the latest supported PHP version* for this project. We're committed to providing long-term support (LTS) options and to provide a diff --git a/composer.json b/composer.json index c1752cc..55e91e6 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "php": ">=5.3", + "php": ">=7.1", "clue/redis-protocol": "0.3.*", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/event-loop": "^1.2", @@ -21,7 +21,7 @@ }, "require-dev": { "clue/block-react": "^1.1", - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + "phpunit/phpunit": "^9.3 || ^5.7" }, "autoload": { "psr-4": { "Clue\\React\\Redis\\": "src/" } diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index 8d93c4f..a853976 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -2,7 +2,7 @@ From 15ac1e98ae95e855adf6e6b9746daa102837486c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 20 May 2022 13:56:23 +0200 Subject: [PATCH 31/65] Update PHP lanuage syntax --- README.md | 14 +++--- examples/cli.php | 2 +- examples/publish.php | 4 +- examples/subscribe.php | 2 +- src/Factory.php | 30 ++++++------ src/LazyClient.php | 100 ++++++++++++++++++---------------------- src/StreamingClient.php | 35 ++++++-------- 7 files changed, 86 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 2d66d05..8d75af6 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ send a message to all clients currently subscribed to a given channel: ```php $channel = 'user'; -$message = json_encode(array('id' => 10)); +$message = json_encode(['id' => 10]); $redis->publish($channel, $message); ``` @@ -288,16 +288,16 @@ 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(array( +$connector = new React\Socket\Connector([ 'dns' => '127.0.0.1', - 'tcp' => array( + 'tcp' => [ 'bindto' => '192.168.10.1:0' - ), - 'tls' => array( + ], + 'tls' => [ 'verify_peer' => false, 'verify_peer_name' => false - ) -)); + ] +]); $factory = new Clue\React\Redis\Factory(null, $connector); ``` diff --git a/examples/cli.php b/examples/cli.php index c1c629f..d0b41f8 100644 --- a/examples/cli.php +++ b/examples/cli.php @@ -29,7 +29,7 @@ $params = explode(' ', $line); $method = array_shift($params); - $promise = call_user_func_array(array($redis, $method), $params); + $promise = call_user_func_array([$redis, $method], $params); // special method such as end() / close() called if (!$promise instanceof React\Promise\PromiseInterface) { diff --git a/examples/publish.php b/examples/publish.php index 6eb2f9d..70a8bb0 100644 --- a/examples/publish.php +++ b/examples/publish.php @@ -8,8 +8,8 @@ $factory = new Clue\React\Redis\Factory(); $redis = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); -$channel = isset($argv[1]) ? $argv[1] : 'channel'; -$message = isset($argv[2]) ? $argv[2] : 'message'; +$channel = $argv[1] ?? 'channel'; +$message = $argv[2] ?? 'message'; $redis->publish($channel, $message)->then(function ($received) { echo 'Successfully published. Received by ' . $received . PHP_EOL; diff --git a/examples/subscribe.php b/examples/subscribe.php index 9f93f2e..b7871f5 100644 --- a/examples/subscribe.php +++ b/examples/subscribe.php @@ -10,7 +10,7 @@ $factory = new Clue\React\Redis\Factory(); $redis = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); -$channel = isset($argv[1]) ? $argv[1] : 'channel'; +$channel = $argv[1] ?? 'channel'; $redis->subscribe($channel)->then(function () { echo 'Now subscribed to channel ' . PHP_EOL; diff --git a/src/Factory.php b/src/Factory.php index 4e94905..e79abac 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -10,6 +10,8 @@ use React\Socket\ConnectionInterface; use React\Socket\Connector; use React\Socket\ConnectorInterface; +use function React\Promise\reject; +use function React\Promise\Timer\timeout; class Factory { @@ -30,7 +32,7 @@ class Factory public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null, ProtocolFactory $protocol = null) { $this->loop = $loop ?: Loop::get(); - $this->connector = $connector ?: new Connector(array(), $this->loop); + $this->connector = $connector ?: new Connector([], $this->loop); $this->protocol = $protocol ?: new ProtocolFactory(); } @@ -54,18 +56,18 @@ public function createClient($uri) $parts = parse_url($uri); } - $uri = preg_replace(array('/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'), '$1***$2', $uri); - if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss', 'redis+unix'))) { - return \React\Promise\reject(new \InvalidArgumentException( + $uri = preg_replace(['/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'], '$1***$2', $uri); + if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], ['redis', 'rediss', 'redis+unix'])) { + return reject(new \InvalidArgumentException( 'Invalid Redis URI given (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); } - $args = array(); - parse_str(isset($parts['query']) ? $parts['query'] : '', $args); + $args = []; + parse_str($parts['query'] ?? '', $args); - $authority = $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 6379); + $authority = $parts['host'] . ':' . ($parts['port'] ?? 6379); if ($parts['scheme'] === 'rediss') { $authority = 'tls://' . $authority; } elseif ($parts['scheme'] === 'redis+unix') { @@ -88,9 +90,8 @@ public function createClient($uri) $connecting->cancel(); }); - $protocol = $this->protocol; - $promise = $connecting->then(function (ConnectionInterface $stream) use ($protocol) { - return new StreamingClient($stream, $protocol->createResponseParser(), $protocol->createSerializer()); + $promise = $connecting->then(function (ConnectionInterface $stream) { + return new StreamingClient($stream, $this->protocol->createResponseParser(), $this->protocol->createSerializer()); }, function (\Exception $e) use ($uri) { throw new \RuntimeException( 'Connection to ' . $uri . ' failed: ' . $e->getMessage(), @@ -100,9 +101,8 @@ public function createClient($uri) }); // use `?password=secret` query or `user:secret@host` password form URL - $pass = isset($args['password']) ? $args['password'] : (isset($parts['pass']) ? rawurldecode($parts['pass']) : null); if (isset($args['password']) || isset($parts['pass'])) { - $pass = isset($args['password']) ? $args['password'] : rawurldecode($parts['pass']); + $pass = $args['password'] ?? rawurldecode($parts['pass']); $promise = $promise->then(function (StreamingClient $redis) use ($pass, $uri) { return $redis->auth($pass)->then( function () use ($redis) { @@ -130,7 +130,7 @@ function (\Exception $e) use ($redis, $uri) { // use `?db=1` query or `/1` path (skip first slash) if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) { - $db = isset($args['db']) ? $args['db'] : substr($parts['path'], 1); + $db = $args['db'] ?? substr($parts['path'], 1); $promise = $promise->then(function (StreamingClient $redis) use ($db, $uri) { return $redis->select($db)->then( function () use ($redis) { @@ -159,7 +159,7 @@ function (\Exception $e) use ($redis, $uri) { }); } - $promise->then(array($deferred, 'resolve'), array($deferred, 'reject')); + $promise->then([$deferred, 'resolve'], [$deferred, 'reject']); // use timeout from explicit ?timeout=x parameter or default to PHP's default_socket_timeout (60) $timeout = isset($args['timeout']) ? (float) $args['timeout'] : (int) ini_get("default_socket_timeout"); @@ -167,7 +167,7 @@ function (\Exception $e) use ($redis, $uri) { return $deferred->promise(); } - return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) { + return timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) { if ($e instanceof TimeoutException) { throw new \RuntimeException( 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)', diff --git a/src/LazyClient.php b/src/LazyClient.php index d82b257..291bb27 100644 --- a/src/LazyClient.php +++ b/src/LazyClient.php @@ -5,6 +5,7 @@ use Evenement\EventEmitter; use React\Stream\Util; use React\EventLoop\LoopInterface; +use function React\Promise\reject; /** * @internal @@ -22,15 +23,15 @@ class LazyClient extends EventEmitter implements Client private $idleTimer; private $pending = 0; - private $subscribed = array(); - private $psubscribed = array(); + private $subscribed = []; + private $psubscribed = []; /** * @param $target */ public function __construct($target, Factory $factory, LoopInterface $loop) { - $args = array(); + $args = []; \parse_str((string) \parse_url($target, \PHP_URL_QUERY), $args); if (isset($args['idle'])) { $this->idlePeriod = (float)$args['idle']; @@ -47,66 +48,59 @@ private function client() return $this->promise; } - $self = $this; - $pending =& $this->promise; - $idleTimer=& $this->idleTimer; - $subscribed =& $this->subscribed; - $psubscribed =& $this->psubscribed; - $loop = $this->loop; - return $pending = $this->factory->createClient($this->target)->then(function (Client $redis) use ($self, &$pending, &$idleTimer, &$subscribed, &$psubscribed, $loop) { + return $this->promise = $this->factory->createClient($this->target)->then(function (Client $redis) { // connection completed => remember only until closed - $redis->on('close', function () use (&$pending, $self, &$subscribed, &$psubscribed, &$idleTimer, $loop) { - $pending = null; + $redis->on('close', function () { + $this->promise = null; // foward unsubscribe/punsubscribe events when underlying connection closes - $n = count($subscribed); - foreach ($subscribed as $channel => $_) { - $self->emit('unsubscribe', array($channel, --$n)); + $n = count($this->subscribed); + foreach ($this->subscribed as $channel => $_) { + $this->emit('unsubscribe', [$channel, --$n]); } - $n = count($psubscribed); - foreach ($psubscribed as $pattern => $_) { - $self->emit('punsubscribe', array($pattern, --$n)); + $n = count($this->psubscribed); + foreach ($this->psubscribed as $pattern => $_) { + $this->emit('punsubscribe', [$pattern, --$n]); } - $subscribed = array(); - $psubscribed = array(); + $this->subscribed = $this->psubscribed = []; - if ($idleTimer !== null) { - $loop->cancelTimer($idleTimer); - $idleTimer = null; + if ($this->idleTimer !== null) { + $this->loop->cancelTimer($this->idleTimer); + $this->idleTimer = null; } }); // keep track of all channels and patterns this connection is subscribed to - $redis->on('subscribe', function ($channel) use (&$subscribed) { - $subscribed[$channel] = true; + $redis->on('subscribe', function ($channel) { + $this->subscribed[$channel] = true; }); - $redis->on('psubscribe', function ($pattern) use (&$psubscribed) { - $psubscribed[$pattern] = true; + $redis->on('psubscribe', function ($pattern) { + $this->psubscribed[$pattern] = true; }); - $redis->on('unsubscribe', function ($channel) use (&$subscribed) { - unset($subscribed[$channel]); + $redis->on('unsubscribe', function ($channel) { + unset($this->subscribed[$channel]); }); - $redis->on('punsubscribe', function ($pattern) use (&$psubscribed) { - unset($psubscribed[$pattern]); + $redis->on('punsubscribe', function ($pattern) { + unset($this->psubscribed[$pattern]); }); Util::forwardEvents( $redis, - $self, - array( + $this, + [ 'message', 'subscribe', 'unsubscribe', 'pmessage', 'psubscribe', 'punsubscribe', - ) + ] ); return $redis; - }, function (\Exception $e) use (&$pending) { + }, function (\Exception $e) { // connection failed => discard connection attempt - $pending = null; + $this->promise = null; throw $e; }); @@ -115,22 +109,21 @@ private function client() public function __call($name, $args) { if ($this->closed) { - return \React\Promise\reject(new \RuntimeException( + return reject(new \RuntimeException( 'Connection closed (ENOTCONN)', defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107 )); } - $that = $this; - return $this->client()->then(function (Client $redis) use ($name, $args, $that) { - $that->awake(); - return \call_user_func_array(array($redis, $name), $args)->then( - function ($result) use ($that) { - $that->idle(); + return $this->client()->then(function (Client $redis) use ($name, $args) { + $this->awake(); + return \call_user_func_array([$redis, $name], $args)->then( + function ($result) { + $this->idle(); return $result; }, - function ($error) use ($that) { - $that->idle(); + function ($error) { + $this->idle(); throw $error; } ); @@ -147,10 +140,9 @@ public function end() return; } - $that = $this; - return $this->client()->then(function (Client $redis) use ($that) { - $redis->on('close', function () use ($that) { - $that->close(); + return $this->client()->then(function (Client $redis) { + $redis->on('close', function () { + $this->close(); }); $redis->end(); }); @@ -205,14 +197,12 @@ public function idle() --$this->pending; if ($this->pending < 1 && $this->idlePeriod >= 0 && !$this->subscribed && !$this->psubscribed && $this->promise !== null) { - $idleTimer =& $this->idleTimer; - $promise =& $this->promise; - $idleTimer = $this->loop->addTimer($this->idlePeriod, function () use (&$idleTimer, &$promise) { - $promise->then(function (Client $redis) { + $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () { + $this->promise->then(function (Client $redis) { $redis->close(); }); - $promise = null; - $idleTimer = null; + $this->promise = null; + $this->idleTimer = null; }); } } diff --git a/src/StreamingClient.php b/src/StreamingClient.php index 8afd84d..5ad14a9 100644 --- a/src/StreamingClient.php +++ b/src/StreamingClient.php @@ -21,7 +21,7 @@ class StreamingClient extends EventEmitter implements Client private $stream; private $parser; private $serializer; - private $requests = array(); + private $requests = []; private $ending = false; private $closed = false; @@ -40,32 +40,31 @@ public function __construct(DuplexStreamInterface $stream, ParserInterface $pars } } - $that = $this; - $stream->on('data', function($chunk) use ($parser, $that) { + $stream->on('data', function($chunk) use ($parser) { try { $models = $parser->pushIncoming($chunk); } catch (ParserException $error) { - $that->emit('error', array(new \UnexpectedValueException( + $this->emit('error', [new \UnexpectedValueException( 'Invalid data received: ' . $error->getMessage() . ' (EBADMSG)', defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG : 77, $error - ))); - $that->close(); + )]); + $this->close(); return; } foreach ($models as $data) { try { - $that->handleMessage($data); + $this->handleMessage($data); } catch (\UnderflowException $error) { - $that->emit('error', array($error)); - $that->close(); + $this->emit('error', [$error]); + $this->close(); return; } } }); - $stream->on('close', array($this, 'close')); + $stream->on('close', [$this, 'close']); $this->stream = $stream; $this->parser = $parser; @@ -80,7 +79,7 @@ public function __call($name, $args) $name = strtolower($name); // special (p)(un)subscribe commands only accept a single parameter and have custom response logic applied - static $pubsubs = array('subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe'); + static $pubsubs = ['subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe']; if ($this->ending) { $request->reject(new \RuntimeException( @@ -103,21 +102,17 @@ public function __call($name, $args) } if (in_array($name, $pubsubs)) { - $that = $this; - $subscribed =& $this->subscribed; - $psubscribed =& $this->psubscribed; - - $promise->then(function ($array) use ($that, &$subscribed, &$psubscribed) { + $promise->then(function ($array) { $first = array_shift($array); // (p)(un)subscribe messages are to be forwarded - $that->emit($first, $array); + $this->emit($first, $array); // remember number of (p)subscribe topics if ($first === 'subscribe' || $first === 'unsubscribe') { - $subscribed = $array[1]; + $this->subscribed = $array[1]; } else { - $psubscribed = $array[1]; + $this->psubscribed = $array[1]; } }); } @@ -132,7 +127,7 @@ public function handleMessage(ModelInterface $message) $first = array_shift($array); // pub/sub messages are to be forwarded and should not be processed as request responses - if (in_array($first, array('message', 'pmessage'))) { + if (in_array($first, ['message', 'pmessage'])) { $this->emit($first, $array); return; } From 73b9460b33bc11b6b2f63068dac4de27eecc0beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 20 May 2022 14:30:39 +0200 Subject: [PATCH 32/65] Update PHPUnit and PHP syntax for tests --- composer.json | 2 +- phpunit.xml.legacy | 4 +- tests/FactoryLazyClientTest.php | 69 ++++---- tests/FactoryStreamingClientTest.php | 231 ++++++++++++++------------- tests/FunctionalTest.php | 41 +++-- tests/LazyClientTest.php | 108 ++++++------- tests/StreamingClientTest.php | 53 +++--- tests/TestCase.php | 28 ++-- 8 files changed, 263 insertions(+), 273 deletions(-) diff --git a/composer.json b/composer.json index 55e91e6..9dff4b7 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ }, "require-dev": { "clue/block-react": "^1.1", - "phpunit/phpunit": "^9.3 || ^5.7" + "phpunit/phpunit": "^9.3 || ^7.5" }, "autoload": { "psr-4": { "Clue\\React\\Redis\\": "src/" } diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index a853976..b325858 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -1,8 +1,8 @@ - + diff --git a/tests/FactoryLazyClientTest.php b/tests/FactoryLazyClientTest.php index 8b5005b..e6b6730 100644 --- a/tests/FactoryLazyClientTest.php +++ b/tests/FactoryLazyClientTest.php @@ -2,8 +2,13 @@ namespace Clue\Tests\React\Redis; +use Clue\React\Redis\Client; use Clue\React\Redis\Factory; -use React\Promise; +use React\EventLoop\LoopInterface; +use React\Socket\ConnectionInterface; +use React\Socket\ConnectorInterface; +use function React\Promise\reject; +use function React\Promise\resolve; class FactoryLazyClientTest extends TestCase { @@ -16,8 +21,8 @@ class FactoryLazyClientTest extends TestCase */ public function setUpFactory() { - $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $this->loop = $this->createMock(LoopInterface::class); + $this->connector = $this->createMock(ConnectorInterface::class); $this->factory = new Factory($this->loop, $this->connector); } @@ -29,134 +34,134 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $ref->setAccessible(true); $loop = $ref->getValue($factory); - $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + $this->assertInstanceOf(LoopInterface::class, $loop); } public function testWillConnectWithDefaultPort() { - $this->connector->expects($this->never())->method('connect')->with('redis.example.com:6379')->willReturn(Promise\reject(new \RuntimeException())); + $this->connector->expects($this->never())->method('connect')->with('redis.example.com:6379')->willReturn(reject(new \RuntimeException())); $this->factory->createLazyClient('redis.example.com'); } public function testWillConnectToLocalhost() { - $this->connector->expects($this->never())->method('connect')->with('localhost:1337')->willReturn(Promise\reject(new \RuntimeException())); + $this->connector->expects($this->never())->method('connect')->with('localhost:1337')->willReturn(reject(new \RuntimeException())); $this->factory->createLazyClient('localhost:1337'); } public function testWillResolveIfConnectorResolves() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write'); - $this->connector->expects($this->never())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->willReturn(resolve($stream)); $redis = $this->factory->createLazyClient('localhost'); - $this->assertInstanceOf('Clue\React\Redis\Client', $redis); + $this->assertInstanceOf(Client::class, $redis); } public function testWillWriteSelectCommandIfTargetContainsPath() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n"); - $this->connector->expects($this->never())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->willReturn(resolve($stream)); $this->factory->createLazyClient('redis://127.0.0.1/demo'); } public function testWillWriteSelectCommandIfTargetContainsDbQueryParameter() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write')->with("*2\r\n$6\r\nselect\r\n$1\r\n4\r\n"); - $this->connector->expects($this->never())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->willReturn(resolve($stream)); $this->factory->createLazyClient('redis://127.0.0.1?db=4'); } public function testWillWriteAuthCommandIfRedisUriContainsUserInfo() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createLazyClient('redis://hello:world@example.com'); } public function testWillWriteAuthCommandIfRedisUriContainsEncodedUserInfo() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n"); - $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createLazyClient('redis://:h%40llo@example.com'); } public function testWillWriteAuthCommandIfTargetContainsPasswordQueryParameter() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$6\r\nsecret\r\n"); - $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createLazyClient('redis://example.com?password=secret'); } public function testWillWriteAuthCommandIfTargetContainsEncodedPasswordQueryParameter() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n"); - $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createLazyClient('redis://example.com?password=h%40llo'); } public function testWillWriteAuthCommandIfRedissUriContainsUserInfo() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - $this->connector->expects($this->never())->method('connect')->with('tls://example.com:6379')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->with('tls://example.com:6379')->willReturn(resolve($stream)); $this->factory->createLazyClient('rediss://hello:world@example.com'); } public function testWillWriteAuthCommandIfRedisUnixUriContainsPasswordQueryParameter() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - $this->connector->expects($this->never())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); $this->factory->createLazyClient('redis+unix:///tmp/redis.sock?password=world'); } public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - $this->connector->expects($this->never())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); $this->factory->createLazyClient('redis+unix://hello:world@/tmp/redis.sock'); } public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n"); - $this->connector->expects($this->never())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->never())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); $this->factory->createLazyClient('redis+unix:///tmp/redis.sock?db=demo'); } public function testWillRejectIfConnectorRejects() { - $this->connector->expects($this->never())->method('connect')->with('127.0.0.1:2')->willReturn(Promise\reject(new \RuntimeException())); + $this->connector->expects($this->never())->method('connect')->with('127.0.0.1:2')->willReturn(reject(new \RuntimeException())); $redis = $this->factory->createLazyClient('redis://127.0.0.1:2'); - $this->assertInstanceOf('Clue\React\Redis\Client', $redis); + $this->assertInstanceOf(Client::class, $redis); } public function testWillRejectIfTargetIsInvalid() { $redis = $this->factory->createLazyClient('http://invalid target'); - $this->assertInstanceOf('Clue\React\Redis\Client', $redis); + $this->assertInstanceOf(Client::class, $redis); } } diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php index 882af76..938f4e7 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/FactoryStreamingClientTest.php @@ -2,9 +2,14 @@ namespace Clue\Tests\React\Redis; +use Clue\React\Redis\Client; use Clue\React\Redis\Factory; -use React\Promise; +use React\EventLoop\LoopInterface; use React\Promise\Deferred; +use React\Socket\ConnectionInterface; +use React\Socket\ConnectorInterface; +use function React\Promise\reject; +use function React\Promise\resolve; class FactoryStreamingClientTest extends TestCase { @@ -17,8 +22,8 @@ class FactoryStreamingClientTest extends TestCase */ public function setUpFactory() { - $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); - $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface')->getMock(); + $this->loop = $this->createMock(LoopInterface::class); + $this->connector = $this->createMock(ConnectorInterface::class); $this->factory = new Factory($this->loop, $this->connector); } @@ -30,7 +35,7 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() $ref->setAccessible(true); $loop = $ref->getValue($factory); - $this->assertInstanceOf('React\EventLoop\LoopInterface', $loop); + $this->assertInstanceOf(LoopInterface::class, $loop); } /** @@ -43,22 +48,22 @@ public function testCtor() public function testWillConnectWithDefaultPort() { - $this->connector->expects($this->once())->method('connect')->with('redis.example.com:6379')->willReturn(Promise\reject(new \RuntimeException())); + $this->connector->expects($this->once())->method('connect')->with('redis.example.com:6379')->willReturn(reject(new \RuntimeException())); $this->factory->createClient('redis.example.com'); } public function testWillConnectToLocalhost() { - $this->connector->expects($this->once())->method('connect')->with('localhost:1337')->willReturn(Promise\reject(new \RuntimeException())); + $this->connector->expects($this->once())->method('connect')->with('localhost:1337')->willReturn(reject(new \RuntimeException())); $this->factory->createClient('localhost:1337'); } public function testWillResolveIfConnectorResolves() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write'); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $promise = $this->factory->createClient('localhost'); $this->expectPromiseResolve($promise); @@ -66,131 +71,131 @@ public function testWillResolveIfConnectorResolves() public function testWillWriteSelectCommandIfTargetContainsPath() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n"); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $this->factory->createClient('redis://127.0.0.1/demo'); } public function testWillWriteSelectCommandIfTargetContainsDbQueryParameter() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$1\r\n4\r\n"); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $this->factory->createClient('redis://127.0.0.1?db=4'); } public function testWillWriteAuthCommandIfRedisUriContainsUserInfo() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createClient('redis://hello:world@example.com'); } public function testWillWriteAuthCommandIfRedisUriContainsEncodedUserInfo() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n"); - $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createClient('redis://:h%40llo@example.com'); } public function testWillWriteAuthCommandIfTargetContainsPasswordQueryParameter() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$6\r\nsecret\r\n"); - $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createClient('redis://example.com?password=secret'); } public function testWillWriteAuthCommandIfTargetContainsEncodedPasswordQueryParameter() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n"); - $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createClient('redis://example.com?password=h%40llo'); } public function testWillWriteAuthCommandIfRedissUriContainsUserInfo() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - $this->connector->expects($this->once())->method('connect')->with('tls://example.com:6379')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->with('tls://example.com:6379')->willReturn(resolve($stream)); $this->factory->createClient('rediss://hello:world@example.com'); } public function testWillWriteAuthCommandIfRedisUnixUriContainsPasswordQueryParameter() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); $this->factory->createClient('redis+unix:///tmp/redis.sock?password=world'); } public function testWillNotWriteAnyCommandIfRedisUnixUriContainsNoPasswordOrDb() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write'); - $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); $this->factory->createClient('redis+unix:///tmp/redis.sock'); } public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); $this->factory->createClient('redis+unix://hello:world@/tmp/redis.sock'); } public function testWillResolveWhenAuthCommandReceivesOkResponseIfRedisUriContainsUserInfo() { $dataHandler = null; - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); $stream->expects($this->exactly(2))->method('on')->withConsecutive( - array('data', $this->callback(function ($arg) use (&$dataHandler) { + ['data', $this->callback(function ($arg) use (&$dataHandler) { $dataHandler = $arg; return true; - })), - array('close', $this->anything()) + })], + ['close', $this->anything()] ); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $promise = $this->factory->createClient('redis://:world@localhost'); $this->assertTrue(is_callable($dataHandler)); $dataHandler("+OK\r\n"); - $promise->then($this->expectCallableOnceWith($this->isInstanceOf('Clue\React\Redis\Client'))); + $promise->then($this->expectCallableOnceWith($this->isInstanceOf(Client::class))); } public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorResponseIfRedisUriContainsUserInfo() { $dataHandler = null; - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); $stream->expects($this->once())->method('close'); $stream->expects($this->exactly(2))->method('on')->withConsecutive( - array('data', $this->callback(function ($arg) use (&$dataHandler) { + ['data', $this->callback(function ($arg) use (&$dataHandler) { $dataHandler = $arg; return true; - })), - array('close', $this->anything()) + })], + ['close', $this->anything()] ); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $promise = $this->factory->createClient('redis://:world@localhost'); $this->assertTrue(is_callable($dataHandler)); @@ -198,7 +203,7 @@ public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorR $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection to redis://:***@localhost failed during AUTH command: ERR invalid password (EACCES)'; }), @@ -215,18 +220,18 @@ public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorR public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWaitingForAuthCommand() { $closeHandler = null; - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); $stream->expects($this->once())->method('close'); $stream->expects($this->exactly(2))->method('on')->withConsecutive( - array('data', $this->anything()), - array('close', $this->callback(function ($arg) use (&$closeHandler) { + ['data', $this->anything()], + ['close', $this->callback(function ($arg) use (&$closeHandler) { $closeHandler = $arg; return true; - })) + })] ); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $promise = $this->factory->createClient('redis://:world@localhost'); $this->assertTrue(is_callable($closeHandler)); @@ -236,7 +241,7 @@ public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWa $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\Exception $e) { return $e->getMessage() === 'Connection to redis://:***@localhost failed during AUTH command: Connection closed by peer (ECONNRESET)'; }), @@ -252,50 +257,50 @@ public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWa public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n"); - $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); $this->factory->createClient('redis+unix:///tmp/redis.sock?db=demo'); } public function testWillResolveWhenSelectCommandReceivesOkResponseIfRedisUriContainsPath() { $dataHandler = null; - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$3\r\n123\r\n"); $stream->expects($this->exactly(2))->method('on')->withConsecutive( - array('data', $this->callback(function ($arg) use (&$dataHandler) { + ['data', $this->callback(function ($arg) use (&$dataHandler) { $dataHandler = $arg; return true; - })), - array('close', $this->anything()) + })], + ['close', $this->anything()] ); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $promise = $this->factory->createClient('redis://localhost/123'); $this->assertTrue(is_callable($dataHandler)); $dataHandler("+OK\r\n"); - $promise->then($this->expectCallableOnceWith($this->isInstanceOf('Clue\React\Redis\Client'))); + $promise->then($this->expectCallableOnceWith($this->isInstanceOf(Client::class))); } public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErrorResponseIfRedisUriContainsPath() { $dataHandler = null; - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$3\r\n123\r\n"); $stream->expects($this->once())->method('close'); $stream->expects($this->exactly(2))->method('on')->withConsecutive( - array('data', $this->callback(function ($arg) use (&$dataHandler) { + ['data', $this->callback(function ($arg) use (&$dataHandler) { $dataHandler = $arg; return true; - })), - array('close', $this->anything()) + })], + ['close', $this->anything()] ); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $promise = $this->factory->createClient('redis://localhost/123'); $this->assertTrue(is_callable($dataHandler)); @@ -303,7 +308,7 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErro $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection to redis://localhost/123 failed during SELECT command: ERR DB index is out of range (ENOENT)'; }), @@ -320,18 +325,18 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErro public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesAuthErrorResponseIfRedisUriContainsPath() { $dataHandler = null; - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$3\r\n123\r\n"); $stream->expects($this->once())->method('close'); $stream->expects($this->exactly(2))->method('on')->withConsecutive( - array('data', $this->callback(function ($arg) use (&$dataHandler) { + ['data', $this->callback(function ($arg) use (&$dataHandler) { $dataHandler = $arg; return true; - })), - array('close', $this->anything()) + })], + ['close', $this->anything()] ); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $promise = $this->factory->createClient('redis://localhost/123'); $this->assertTrue(is_callable($dataHandler)); @@ -339,7 +344,7 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesAuth $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\Exception $e) { return $e->getMessage() === 'Connection to redis://localhost/123 failed during SELECT command: NOAUTH Authentication required. (EACCES)'; }), @@ -356,18 +361,18 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesAuth public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWaitingForSelectCommand() { $closeHandler = null; - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$3\r\n123\r\n"); $stream->expects($this->once())->method('close'); $stream->expects($this->exactly(2))->method('on')->withConsecutive( - array('data', $this->anything()), - array('close', $this->callback(function ($arg) use (&$closeHandler) { + ['data', $this->anything()], + ['close', $this->callback(function ($arg) use (&$closeHandler) { $closeHandler = $arg; return true; - })) + })] ); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $promise = $this->factory->createClient('redis://localhost/123'); $this->assertTrue(is_callable($closeHandler)); @@ -377,7 +382,7 @@ public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWa $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\Exception $e) { return $e->getMessage() === 'Connection to redis://localhost/123 failed during SELECT command: Connection closed by peer (ECONNRESET)'; }), @@ -393,12 +398,12 @@ public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWa public function testWillRejectIfConnectorRejects() { - $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn(Promise\reject(new \RuntimeException('Foo', 42))); + $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn(reject(new \RuntimeException('Foo', 42))); $promise = $this->factory->createClient('redis://127.0.0.1:2'); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection to redis://127.0.0.1:2 failed: Foo'; }), @@ -418,7 +423,7 @@ public function testWillRejectIfTargetIsInvalid() $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('InvalidArgumentException'), + $this->isInstanceOf(\InvalidArgumentException::class), $this->callback(function (\InvalidArgumentException $e) { return $e->getMessage() === 'Invalid Redis URI given (EINVAL)'; }), @@ -437,73 +442,73 @@ public function testCancelWillRejectPromise() $promise = $this->factory->createClient('redis://127.0.0.1:2'); $promise->cancel(); - $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf(\RuntimeException::class))); } public function provideUris() { - return array( - array( + return [ + [ 'localhost', 'redis://localhost' - ), - array( + ], + [ 'redis://localhost', 'redis://localhost' - ), - array( + ], + [ 'redis://localhost:6379', 'redis://localhost:6379' - ), - array( + ], + [ 'redis://localhost/0', 'redis://localhost/0' - ), - array( + ], + [ 'redis://user@localhost', 'redis://user@localhost' - ), - array( + ], + [ 'redis://:secret@localhost', 'redis://:***@localhost' - ), - array( + ], + [ 'redis://user:secret@localhost', 'redis://user:***@localhost' - ), - array( + ], + [ 'redis://:@localhost', 'redis://:***@localhost' - ), - array( + ], + [ 'redis://localhost?password=secret', 'redis://localhost?password=***' - ), - array( + ], + [ 'redis://localhost/0?password=secret', 'redis://localhost/0?password=***' - ), - array( + ], + [ 'redis://localhost?password=', 'redis://localhost?password=***' - ), - array( + ], + [ 'redis://localhost?foo=1&password=secret&bar=2', 'redis://localhost?foo=1&password=***&bar=2' - ), - array( + ], + [ 'rediss://localhost', 'rediss://localhost' - ), - array( + ], + [ 'redis+unix://:secret@/tmp/redis.sock', 'redis+unix://:***@/tmp/redis.sock' - ), - array( + ], + [ 'redis+unix:///tmp/redis.sock?password=secret', 'redis+unix:///tmp/redis.sock?password=***' - ) - ); + ] + ]; } /** @@ -521,7 +526,7 @@ public function testCancelWillRejectWithUriInMessageAndCancelConnectorWhenConnec $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\RuntimeException $e) use ($safe) { return $e->getMessage() === 'Connection to ' . $safe . ' cancelled (ECONNABORTED)'; }), @@ -534,18 +539,18 @@ public function testCancelWillRejectWithUriInMessageAndCancelConnectorWhenConnec public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect() { - $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write'); $stream->expects($this->once())->method('close'); - $this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream)); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $promise = $this->factory->createClient('redis://127.0.0.1:2/123'); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection to redis://127.0.0.1:2/123 cancelled (ECONNABORTED)'; }), @@ -574,7 +579,7 @@ public function testCreateClientWithTimeoutParameterWillStartTimerAndRejectOnExp $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\Exception $e) { return $e->getMessage() === 'Connection to redis://127.0.0.1:2?timeout=0 timed out after 0 seconds (ETIMEDOUT)'; }), diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 45a9fcd..74aa600 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -2,13 +2,14 @@ namespace Clue\Tests\React\Redis; -use Clue\React\Block; use Clue\React\Redis\Client; use Clue\React\Redis\Factory; use Clue\React\Redis\StreamingClient; use React\EventLoop\StreamSelectLoop; use React\Promise\Deferred; +use React\Promise\PromiseInterface; use React\Stream\DuplexResourceStream; +use function Clue\React\Block\await; class FunctionalTest extends TestCase { @@ -35,9 +36,9 @@ public function testPing() $redis = $this->createClient($this->uri); $promise = $redis->ping(); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); - $ret = Block\await($promise, $this->loop); + $ret = await($promise, $this->loop); $this->assertEquals('PONG', $ret); } @@ -47,9 +48,9 @@ public function testPingLazy() $redis = $this->factory->createLazyClient($this->uri); $promise = $redis->ping(); - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); - $ret = Block\await($promise, $this->loop); + $ret = await($promise, $this->loop); $this->assertEquals('PONG', $ret); } @@ -87,7 +88,7 @@ public function testMgetIsNotInterpretedAsSubMessage() $promise = $redis->mget('message', 'channel', 'payload')->then($this->expectCallableOnce()); $redis->on('message', $this->expectCallableNever()); - Block\await($promise, $this->loop); + await($promise, $this->loop); } public function testPipeline() @@ -99,7 +100,7 @@ public function testPipeline() $redis->incr('a')->then($this->expectCallableOnceWith(3)); $promise = $redis->get('a')->then($this->expectCallableOnceWith('3')); - Block\await($promise, $this->loop); + await($promise, $this->loop); } public function testInvalidCommand() @@ -112,16 +113,16 @@ public function testInvalidCommand() } else { $this->setExpectedException('Exception'); } - Block\await($promise, $this->loop); + await($promise, $this->loop); } public function testMultiExecEmpty() { $redis = $this->createClient($this->uri); $redis->multi()->then($this->expectCallableOnceWith('OK')); - $promise = $redis->exec()->then($this->expectCallableOnceWith(array())); + $promise = $redis->exec()->then($this->expectCallableOnceWith([])); - Block\await($promise, $this->loop); + await($promise, $this->loop); } public function testMultiExecQueuedExecHasValues() @@ -133,9 +134,9 @@ public function testMultiExecQueuedExecHasValues() $redis->expire('b', 20)->then($this->expectCallableOnceWith('QUEUED')); $redis->incrBy('b', 2)->then($this->expectCallableOnceWith('QUEUED')); $redis->ttl('b')->then($this->expectCallableOnceWith('QUEUED')); - $promise = $redis->exec()->then($this->expectCallableOnceWith(array('OK', 1, 12, 20))); + $promise = $redis->exec()->then($this->expectCallableOnceWith(['OK', 1, 12, 20])); - Block\await($promise, $this->loop); + await($promise, $this->loop); } public function testPubSub() @@ -148,7 +149,7 @@ public function testPubSub() // consumer receives a single message $deferred = new Deferred(); $consumer->on('message', $this->expectCallableOnce()); - $consumer->on('message', array($deferred, 'resolve')); + $consumer->on('message', [$deferred, 'resolve']); $once = $this->expectCallableOnceWith(1); $consumer->subscribe($channel)->then(function() use ($producer, $channel, $once){ // producer sends a single message @@ -156,7 +157,7 @@ public function testPubSub() })->then($this->expectCallableOnce()); // expect "message" event to take no longer than 0.1s - Block\await($deferred->promise(), $this->loop, 0.1); + await($deferred->promise(), $this->loop, 0.1); } public function testClose() @@ -190,12 +191,8 @@ public function testInvalidProtocol() $promise = $redis->get('willBeRejectedDueToClosing'); - if (method_exists($this, 'expectException')) { - $this->expectException('Exception'); - } else { - $this->setExpectedException('Exception'); - } - Block\await($promise, $this->loop); + $this->expectException(\Exception::class); + await($promise, $this->loop); } public function testInvalidServerRepliesWithDuplicateMessages() @@ -207,7 +204,7 @@ public function testInvalidServerRepliesWithDuplicateMessages() $promise = $redis->set('a', 0)->then($this->expectCallableOnceWith('OK')); - Block\await($promise, $this->loop); + await($promise, $this->loop); } /** @@ -216,7 +213,7 @@ public function testInvalidServerRepliesWithDuplicateMessages() */ protected function createClient($uri) { - return Block\await($this->factory->createClient($uri), $this->loop); + return await($this->factory->createClient($uri), $this->loop); } protected function createClientResponse($response) diff --git a/tests/LazyClientTest.php b/tests/LazyClientTest.php index 2ad644e..f1cd894 100644 --- a/tests/LazyClientTest.php +++ b/tests/LazyClientTest.php @@ -2,16 +2,11 @@ namespace Clue\Tests\React\Redis; -use Clue\React\Redis\LazyClient; -use Clue\React\Redis\StreamingClient; -use Clue\Redis\Protocol\Parser\ParserException; -use Clue\Redis\Protocol\Model\IntegerReply; -use Clue\Redis\Protocol\Model\BulkReply; -use Clue\Redis\Protocol\Model\ErrorReply; -use Clue\Redis\Protocol\Model\MultiBulkReply; use Clue\React\Redis\Client; -use React\EventLoop\Factory; -use React\Stream\ThroughStream; +use Clue\React\Redis\Factory; +use Clue\React\Redis\LazyClient; +use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use React\Promise\Promise; use React\Promise\Deferred; @@ -26,8 +21,8 @@ class LazyClientTest extends TestCase */ public function setUpClient() { - $this->factory = $this->getMockBuilder('Clue\React\Redis\Factory')->disableOriginalConstructor()->getMock(); - $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $this->factory = $this->createMock(Factory::class); + $this->loop = $this->createMock(LoopInterface::class); $this->redis = new LazyClient('localhost', $this->factory, $this->loop); } @@ -55,7 +50,7 @@ public function testPingTwiceWillCreateOnceUnderlyingClient() public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimer() { - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $deferred = new Deferred(); @@ -73,7 +68,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT { $this->redis = new LazyClient('localhost?idle=10', $this->factory, $this->loop); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $deferred = new Deferred(); @@ -91,7 +86,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartId { $this->redis = new LazyClient('localhost?idle=-1', $this->factory, $this->loop); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $deferred = new Deferred(); @@ -108,7 +103,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartId public function testPingWillRejectWhenUnderlyingClientRejectsPingAndStartIdleTimer() { $error = new \RuntimeException(); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\reject($error)); $deferred = new Deferred(); @@ -157,13 +152,13 @@ public function testPingAfterPreviousFactoryRejectsUnderlyingClientWillCreateNew public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewUnderlyingConnection() { $closeHandler = null; - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->any())->method('on')->withConsecutive( - array('close', $this->callback(function ($arg) use (&$closeHandler) { + ['close', $this->callback(function ($arg) use (&$closeHandler) { $closeHandler = $arg; return true; - })) + })] ); $this->factory->expects($this->exactly(2))->method('createClient')->willReturnOnConsecutiveCalls( @@ -187,7 +182,7 @@ public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection( $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection closed (ENOTCONN)'; }), @@ -201,7 +196,7 @@ public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection( public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() { $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) @@ -219,7 +214,7 @@ public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStartsAfterFirstResolves() { $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) @@ -227,7 +222,7 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); $this->loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -238,14 +233,14 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent() { - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $client->expects($this->once())->method('close')->willReturn(\React\Promise\resolve()); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); $timeout = null; - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); $this->loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { $timeout = $cb; return true; @@ -301,7 +296,7 @@ public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientC public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlreadyResolved() { - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $client->expects($this->once())->method('close'); @@ -316,13 +311,13 @@ public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlready public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() { $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->once())->method('close'); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); $this->loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -334,7 +329,7 @@ public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() public function testCloseAfterPingRejectsWillEmitClose() { $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->once())->method('close')->willReturnCallback(function () use ($client) { $client->emit('close'); @@ -342,7 +337,7 @@ public function testCloseAfterPingRejectsWillEmitClose() $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); $this->loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -362,7 +357,7 @@ public function testEndWillCloseClientIfUnderlyingConnectionIsNotPending() public function testEndAfterPingWillEndUnderlyingClient() { - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->once())->method('end'); @@ -377,7 +372,7 @@ public function testEndAfterPingWillEndUnderlyingClient() public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose() { $closeHandler = null; - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->once())->method('end'); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$closeHandler) { @@ -403,7 +398,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() { $error = new \RuntimeException(); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $deferred = new Deferred(); @@ -413,12 +408,12 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() $deferred->resolve($client); $this->redis->on('error', $this->expectCallableNever()); - $client->emit('error', array($error)); + $client->emit('error', [$error]); } public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() { - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $deferred = new Deferred(); @@ -434,19 +429,19 @@ public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnectionEmitsCloseAfterPingIsAlreadyResolved() { $closeHandler = null; - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $deferred = new Deferred(); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->any())->method('on')->withConsecutive( - array('close', $this->callback(function ($arg) use (&$closeHandler) { + ['close', $this->callback(function ($arg) use (&$closeHandler) { $closeHandler = $arg; return true; - })) + })] ); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); - $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); + $timer = $this->createMock(TimerInterface::class); $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); $this->loop->expects($this->once())->method('cancelTimer')->with($timer); @@ -462,7 +457,7 @@ public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnect public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubChannel() { $messageHandler = null; - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$messageHandler) { if ($event === 'message') { @@ -484,7 +479,7 @@ public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubCh public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClosesWhileUsingPubSubChannel() { $allHandler = null; - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->exactly(6))->method('__call')->willReturn(\React\Promise\resolve()); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$allHandler) { if (!isset($allHandler[$event])) { @@ -529,7 +524,7 @@ public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAnd { $subscribeHandler = null; $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->with('subscribe')->willReturn($deferred->promise()); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$subscribeHandler) { if ($event === 'subscribe' && $subscribeHandler === null) { @@ -544,9 +539,9 @@ public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAnd $promise = $this->redis->subscribe('foo'); $this->assertTrue(is_callable($subscribeHandler)); $subscribeHandler('foo', 1); - $deferred->resolve(array('subscribe', 'foo', 1)); + $deferred->resolve(['subscribe', 'foo', 1]); - $promise->then($this->expectCallableOnceWith(array('subscribe', 'foo', 1))); + $promise->then($this->expectCallableOnceWith(['subscribe', 'foo', 1])); } public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientResolvesUnsubscribeAndStartIdleTimerWhenSubscriptionStopped() @@ -555,7 +550,7 @@ public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientReso $unsubscribeHandler = null; $deferredSubscribe = new Deferred(); $deferredUnsubscribe = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls($deferredSubscribe->promise(), $deferredUnsubscribe->promise()); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$subscribeHandler, &$unsubscribeHandler) { if ($event === 'subscribe' && $subscribeHandler === null) { @@ -573,27 +568,27 @@ public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientReso $promise = $this->redis->subscribe('foo'); $this->assertTrue(is_callable($subscribeHandler)); $subscribeHandler('foo', 1); - $deferredSubscribe->resolve(array('subscribe', 'foo', 1)); - $promise->then($this->expectCallableOnceWith(array('subscribe', 'foo', 1))); + $deferredSubscribe->resolve(['subscribe', 'foo', 1]); + $promise->then($this->expectCallableOnceWith(['subscribe', 'foo', 1])); $promise = $this->redis->unsubscribe('foo'); $this->assertTrue(is_callable($unsubscribeHandler)); $unsubscribeHandler('foo', 0); - $deferredUnsubscribe->resolve(array('unsubscribe', 'foo', 0)); - $promise->then($this->expectCallableOnceWith(array('unsubscribe', 'foo', 0))); + $deferredUnsubscribe->resolve(['unsubscribe', 'foo', 0]); + $promise->then($this->expectCallableOnceWith(['unsubscribe', 'foo', 0])); } public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResponse() { $closeHandler = null; $deferred = new Deferred(); - $client = $this->getMockBuilder('Clue\React\Redis\Client')->getMock(); + $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->with('blpop')->willReturn($deferred->promise()); $client->expects($this->any())->method('on')->withConsecutive( - array('close', $this->callback(function ($arg) use (&$closeHandler) { + ['close', $this->callback(function ($arg) use (&$closeHandler) { $closeHandler = $arg; return true; - })) + })] ); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); @@ -609,15 +604,4 @@ public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResp $promise->then(null, $this->expectCallableOnceWith($e)); } - - public function createCallableMockWithOriginalConstructorDisabled($array) - { - if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { - // PHPUnit 9+ - return $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->onlyMethods($array)->getMock(); - } else { - // legacy PHPUnit 4 - PHPUnit 8 - return $this->getMockBuilder('Clue\React\Redis\StreamingClient')->disableOriginalConstructor()->setMethods($array)->getMock(); - } - } } diff --git a/tests/StreamingClientTest.php b/tests/StreamingClientTest.php index bff6ca1..00f50d4 100644 --- a/tests/StreamingClientTest.php +++ b/tests/StreamingClientTest.php @@ -7,9 +7,12 @@ use Clue\Redis\Protocol\Model\IntegerReply; use Clue\Redis\Protocol\Model\MultiBulkReply; use Clue\Redis\Protocol\Parser\ParserException; +use Clue\Redis\Protocol\Parser\ParserInterface; +use Clue\Redis\Protocol\Serializer\SerializerInterface; use Clue\React\Redis\Client; use Clue\React\Redis\StreamingClient; use React\Stream\ThroughStream; +use React\Stream\DuplexStreamInterface; class StreamingClientTest extends TestCase { @@ -23,9 +26,9 @@ class StreamingClientTest extends TestCase */ public function setUpClient() { - $this->stream = $this->getMockBuilder('React\Stream\DuplexStreamInterface')->getMock(); - $this->parser = $this->getMockBuilder('Clue\Redis\Protocol\Parser\ParserInterface')->getMock(); - $this->serializer = $this->getMockBuilder('Clue\Redis\Protocol\Serializer\SerializerInterface')->getMock(); + $this->stream = $this->createMock(DuplexStreamInterface::class); + $this->parser = $this->createMock(ParserInterface::class); + $this->serializer = $this->createMock(SerializerInterface::class); $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); } @@ -38,7 +41,7 @@ public function testConstructWithoutParserAssignsParserAutomatically() $ref->setAccessible(true); $parser = $ref->getValue($this->redis); - $this->assertInstanceOf('Clue\Redis\Protocol\Parser\ParserInterface', $parser); + $this->assertInstanceOf(ParserInterface::class, $parser); } public function testConstructWithoutParserAndSerializerAssignsParserAndSerializerAutomatically() @@ -49,7 +52,7 @@ public function testConstructWithoutParserAndSerializerAssignsParserAndSerialize $ref->setAccessible(true); $serializer = $ref->getValue($this->redis); - $this->assertInstanceOf('Clue\Redis\Protocol\Serializer\SerializerInterface', $serializer); + $this->assertInstanceOf(SerializerInterface::class, $serializer); } public function testSending() @@ -84,7 +87,7 @@ public function testReceiveParseErrorEmitsErrorEvent() $this->redis->on('error', $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('UnexpectedValueException'), + $this->isInstanceOf(\UnexpectedValueException::class), $this->callback(function (\UnexpectedValueException $e) { return $e->getMessage() === 'Invalid data received: Foo (EBADMSG)'; }), @@ -96,7 +99,7 @@ public function testReceiveParseErrorEmitsErrorEvent() $this->redis->on('close', $this->expectCallableOnce()); $this->parser->expects($this->once())->method('pushIncoming')->with('message')->willThrowException(new ParserException('Foo')); - $this->stream->emit('data', array('message')); + $this->stream->emit('data', ['message']); } public function testReceiveUnexpectedReplyEmitsErrorEvent() @@ -107,7 +110,7 @@ public function testReceiveUnexpectedReplyEmitsErrorEvent() $this->redis->on('error', $this->expectCallableOnce()); $this->redis->on('error', $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('UnderflowException'), + $this->isInstanceOf(\UnderflowException::class), $this->callback(function (\UnderflowException $e) { return $e->getMessage() === 'Unexpected reply received, no matching request found (ENOMSG)'; }), @@ -118,8 +121,8 @@ public function testReceiveUnexpectedReplyEmitsErrorEvent() )); - $this->parser->expects($this->once())->method('pushIncoming')->with('message')->willReturn(array(new IntegerReply(2))); - $this->stream->emit('data', array('message')); + $this->parser->expects($this->once())->method('pushIncoming')->with('message')->willReturn([new IntegerReply(2)]); + $this->stream->emit('data', ['message']); } /** @@ -127,7 +130,7 @@ public function testReceiveUnexpectedReplyEmitsErrorEvent() */ public function testDefaultCtor() { - $client = new StreamingClient($this->stream); + new StreamingClient($this->stream); } public function testPingPong() @@ -148,7 +151,7 @@ public function testMonitorCommandIsNotSupported() $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('BadMethodCallException'), + $this->isInstanceOf(\BadMethodCallException::class), $this->callback(function (\BadMethodCallException $e) { return $e->getMessage() === 'MONITOR command explicitly not supported (ENOTSUP)'; }), @@ -176,7 +179,7 @@ public function testClosingClientRejectsAllRemainingRequests() $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection closing (ECONNABORTED)'; }), @@ -190,7 +193,7 @@ public function testClosingClientRejectsAllRemainingRequests() public function testClosingStreamRejectsAllRemainingRequests() { $this->stream = new ThroughStream(); - $this->parser->expects($this->once())->method('pushIncoming')->willReturn(array()); + $this->parser->expects($this->once())->method('pushIncoming')->willReturn([]); $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); $promise = $this->redis->ping(); @@ -198,7 +201,7 @@ public function testClosingStreamRejectsAllRemainingRequests() $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection closed by peer (ECONNRESET)'; }), @@ -217,7 +220,7 @@ public function testEndingClientRejectsAllNewRequests() $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection closing (ENOTCONN)'; }), @@ -235,7 +238,7 @@ public function testClosedClientRejectsAllNewRequests() $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('RuntimeException'), + $this->isInstanceOf(\RuntimeException::class), $this->callback(function (\RuntimeException $e) { return $e->getMessage() === 'Connection closed (ENOTCONN)'; }), @@ -281,11 +284,7 @@ public function testClosingMultipleTimesEmitsOnce() public function testReceivingUnexpectedMessageThrowsException() { - if (method_exists($this, 'expectException')) { - $this->expectException('UnderflowException'); - } else { - $this->setExpectedException('UnderflowException'); - } + $this->expectException(\UnderflowException::class); $this->redis->handleMessage(new BulkReply('PONG')); } @@ -295,7 +294,7 @@ public function testPubsubSubscribe() $this->expectPromiseResolve($promise); $this->redis->on('subscribe', $this->expectCallableOnce()); - $this->redis->handleMessage(new MultiBulkReply(array(new BulkReply('subscribe'), new BulkReply('test'), new IntegerReply(1)))); + $this->redis->handleMessage(new MultiBulkReply([new BulkReply('subscribe'), new BulkReply('test'), new IntegerReply(1)])); return $this->redis; } @@ -310,7 +309,7 @@ public function testPubsubPatternSubscribe(Client $client) $this->expectPromiseResolve($promise); $client->on('psubscribe', $this->expectCallableOnce()); - $client->handleMessage(new MultiBulkReply(array(new BulkReply('psubscribe'), new BulkReply('demo_*'), new IntegerReply(1)))); + $client->handleMessage(new MultiBulkReply([new BulkReply('psubscribe'), new BulkReply('demo_*'), new IntegerReply(1)])); return $client; } @@ -322,7 +321,7 @@ public function testPubsubPatternSubscribe(Client $client) public function testPubsubMessage(Client $client) { $client->on('message', $this->expectCallableOnce()); - $client->handleMessage(new MultiBulkReply(array(new BulkReply('message'), new BulkReply('test'), new BulkReply('payload')))); + $client->handleMessage(new MultiBulkReply([new BulkReply('message'), new BulkReply('test'), new BulkReply('payload')])); } public function testSubscribeWithMultipleArgumentsRejects() @@ -331,7 +330,7 @@ public function testSubscribeWithMultipleArgumentsRejects() $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('InvalidArgumentException'), + $this->isInstanceOf(\InvalidArgumentException::class), $this->callback(function (\InvalidArgumentException $e) { return $e->getMessage() === 'PubSub commands limited to single argument (EINVAL)'; }), @@ -348,7 +347,7 @@ public function testUnsubscribeWithoutArgumentsRejects() $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( - $this->isInstanceOf('InvalidArgumentException'), + $this->isInstanceOf(\InvalidArgumentException::class), $this->callback(function (\InvalidArgumentException $e) { return $e->getMessage() === 'PubSub commands limited to single argument (EINVAL)'; }), diff --git a/tests/TestCase.php b/tests/TestCase.php index 013aa7a..99189ef 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,9 @@ namespace Clue\Tests\React\Redis; +use PHPUnit\Framework\MockObject\MockBuilder; use PHPUnit\Framework\TestCase as BaseTestCase; +use React\Promise\PromiseInterface; class TestCase extends BaseTestCase { @@ -32,23 +34,22 @@ protected function expectCallableNever() protected function createCallableMock() { - if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + if (method_exists(MockBuilder::class, 'addMethods')) { // PHPUnit 9+ - return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); + return $this->getMockBuilder(\stdClass::class)->addMethods(['__invoke'])->getMock(); } else { - // legacy PHPUnit 4 - PHPUnit 8 - return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + // legacy PHPUnit < 9 + return $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); } } protected function expectPromiseResolve($promise) { - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); - $that = $this; - $promise->then(null, function($error) use ($that) { - $that->assertNull($error); - $that->fail('promise rejected'); + $promise->then(null, function($error) { + $this->assertNull($error); + $this->fail('promise rejected'); }); $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); @@ -57,12 +58,11 @@ protected function expectPromiseResolve($promise) protected function expectPromiseReject($promise) { - $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); + $this->assertInstanceOf(PromiseInterface::class, $promise); - $that = $this; - $promise->then(function($value) use ($that) { - $that->assertNull($value); - $that->fail('promise resolved'); + $promise->then(function($value) { + $this->assertNull($value); + $this->fail('promise resolved'); }); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); From 1e34a7a29aca13086630499353456f8572cf0ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 22 May 2022 15:13:05 +0200 Subject: [PATCH 33/65] Add type definitions for all APIs and tests --- README.md | 16 ++++---- examples/incr.php | 2 +- examples/publish.php | 2 +- examples/subscribe.php | 6 +-- src/Client.php | 6 +-- src/Factory.php | 7 ++-- src/LazyClient.php | 59 ++++++++++++++++------------ src/StreamingClient.php | 27 ++++++++++--- tests/FactoryLazyClientTest.php | 11 ++++-- tests/FactoryStreamingClientTest.php | 11 ++++-- tests/FunctionalTest.php | 14 ++++--- tests/LazyClientTest.php | 13 +++--- tests/StreamingClientTest.php | 37 +++++++++-------- tests/TestCase.php | 15 +++---- 14 files changed, 134 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 8d75af6..4a76831 100644 --- a/README.md +++ b/README.md @@ -81,12 +81,12 @@ $redis = $factory->createLazyClient('localhost:6379'); $redis->set('greeting', 'Hello world'); $redis->append('greeting', '!'); -$redis->get('greeting')->then(function ($greeting) { +$redis->get('greeting')->then(function (string $greeting) { // Hello world! echo $greeting . PHP_EOL; }); -$redis->incr('invocation')->then(function ($n) { +$redis->incr('invocation')->then(function (int $n) { echo 'This is invocation #' . $n . PHP_EOL; }); @@ -184,7 +184,7 @@ subscribe to a channel and then receive incoming PubSub `message` events: $channel = 'user'; $redis->subscribe($channel); -$redis->on('message', function ($channel, $payload) { +$redis->on('message', function (string $channel, string $payload) { // pubsub message received on given $channel var_dump($channel, json_decode($payload)); }); @@ -208,7 +208,7 @@ all incoming PubSub messages with the `pmessage` event: $pattern = 'user.*'; $redis->psubscribe($pattern); -$redis->on('pmessage', function ($pattern, $channel, $payload) { +$redis->on('pmessage', function (string $pattern, string $channel, string $payload) { // pubsub message received matching given $pattern var_dump($channel, json_decode($payload)); }); @@ -248,16 +248,16 @@ Additionally, can listen for the following PubSub events to get notifications about subscribed/unsubscribed channels and patterns: ```php -$redis->on('subscribe', function ($channel, $total) { +$redis->on('subscribe', function (string $channel, int $total) { // subscribed to given $channel }); -$redis->on('psubscribe', function ($pattern, $total) { +$redis->on('psubscribe', function (string $pattern, int $total) { // subscribed to matching given $pattern }); -$redis->on('unsubscribe', function ($channel, $total) { +$redis->on('unsubscribe', function (string $channel, int $total) { // unsubscribed from given $channel }); -$redis->on('punsubscribe', function ($pattern, $total) { +$redis->on('punsubscribe', function (string $pattern, int $total) { // unsubscribed from matching given $pattern }); ``` diff --git a/examples/incr.php b/examples/incr.php index 71887e4..1d28524 100644 --- a/examples/incr.php +++ b/examples/incr.php @@ -10,7 +10,7 @@ $redis->incr('test'); -$redis->get('test')->then(function ($result) { +$redis->get('test')->then(function (string $result) { var_dump($result); }, function (Exception $e) { echo 'Error: ' . $e->getMessage() . PHP_EOL; diff --git a/examples/publish.php b/examples/publish.php index 70a8bb0..90e6a48 100644 --- a/examples/publish.php +++ b/examples/publish.php @@ -11,7 +11,7 @@ $channel = $argv[1] ?? 'channel'; $message = $argv[2] ?? 'message'; -$redis->publish($channel, $message)->then(function ($received) { +$redis->publish($channel, $message)->then(function (int $received) { echo 'Successfully published. Received by ' . $received . PHP_EOL; }, function (Exception $e) { echo 'Unable to publish: ' . $e->getMessage() . PHP_EOL; diff --git a/examples/subscribe.php b/examples/subscribe.php index b7871f5..1270b7c 100644 --- a/examples/subscribe.php +++ b/examples/subscribe.php @@ -19,15 +19,15 @@ echo 'Unable to subscribe: ' . $e->getMessage() . PHP_EOL; }); -$redis->on('message', function ($channel, $message) { +$redis->on('message', function (string $channel, string $message) { echo 'Message on ' . $channel . ': ' . $message . PHP_EOL; }); // automatically re-subscribe to channel on connection issues -$redis->on('unsubscribe', function ($channel) use ($redis) { +$redis->on('unsubscribe', function (string $channel) use ($redis) { echo 'Unsubscribed from ' . $channel . PHP_EOL; - Loop::addPeriodicTimer(2.0, function ($timer) use ($redis, $channel){ + Loop::addPeriodicTimer(2.0, function (React\EventLoop\TimerInterface $timer) use ($redis, $channel){ $redis->subscribe($channel)->then(function () use ($timer) { echo 'Now subscribed again' . PHP_EOL; Loop::cancelTimer($timer); diff --git a/src/Client.php b/src/Client.php index ec54229..714ac88 100644 --- a/src/Client.php +++ b/src/Client.php @@ -31,7 +31,7 @@ interface Client extends EventEmitterInterface * @param string[] $args * @return PromiseInterface Promise */ - public function __call($name, $args); + public function __call(string $name, array $args): PromiseInterface; /** * end connection once all pending requests have been replied to @@ -40,7 +40,7 @@ public function __call($name, $args); * @uses self::close() once all replies have been received * @see self::close() for closing the connection immediately */ - public function end(); + public function end(): void; /** * close connection immediately @@ -50,5 +50,5 @@ public function end(); * @return void * @see self::end() for closing the connection once the client is idle */ - public function close(); + public function close(): void; } diff --git a/src/Factory.php b/src/Factory.php index e79abac..3b77c92 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -6,6 +6,7 @@ use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Promise\Deferred; +use React\Promise\PromiseInterface; use React\Promise\Timer\TimeoutException; use React\Socket\ConnectionInterface; use React\Socket\Connector; @@ -40,10 +41,10 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn * Create Redis client connected to address of given redis instance * * @param string $uri Redis server URI to connect to - * @return \React\Promise\PromiseInterface Promise that will + * @return PromiseInterface Promise that will * be fulfilled with `Client` on success or rejects with `\Exception` on error. */ - public function createClient($uri) + public function createClient(string $uri): PromiseInterface { // support `redis+unix://` scheme for Unix domain socket (UDS) paths if (preg_match('/^(redis\+unix:\/\/(?:[^:]*:[^@]*@)?)(.+?)?$/', $uri, $match)) { @@ -184,7 +185,7 @@ function (\Exception $e) use ($redis, $uri) { * @param string $target * @return Client */ - public function createLazyClient($target) + public function createLazyClient($target): Client { return new LazyClient($target, $this, $this->loop); } diff --git a/src/LazyClient.php b/src/LazyClient.php index 291bb27..7f83ac7 100644 --- a/src/LazyClient.php +++ b/src/LazyClient.php @@ -3,8 +3,10 @@ namespace Clue\React\Redis; use Evenement\EventEmitter; -use React\Stream\Util; use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; +use React\Promise\PromiseInterface; +use React\Stream\Util; use function React\Promise\reject; /** @@ -12,24 +14,37 @@ */ class LazyClient extends EventEmitter implements Client { + /** @var string */ private $target; + /** @var Factory */ private $factory; + + /** @var bool */ private $closed = false; - private $promise; + /** @var ?PromiseInterface */ + private $promise = null; + + /** @var LoopInterface */ private $loop; + + /** @var float */ private $idlePeriod = 60.0; - private $idleTimer; + + /** @var ?TimerInterface */ + private $idleTimer = null; + + /** @var int */ private $pending = 0; + /** @var array */ private $subscribed = []; + + /** @var array */ private $psubscribed = []; - /** - * @param $target - */ - public function __construct($target, Factory $factory, LoopInterface $loop) + public function __construct(string $target, Factory $factory, LoopInterface $loop) { $args = []; \parse_str((string) \parse_url($target, \PHP_URL_QUERY), $args); @@ -42,7 +57,7 @@ public function __construct($target, Factory $factory, LoopInterface $loop) $this->loop = $loop; } - private function client() + private function client(): PromiseInterface { if ($this->promise !== null) { return $this->promise; @@ -71,16 +86,16 @@ private function client() }); // keep track of all channels and patterns this connection is subscribed to - $redis->on('subscribe', function ($channel) { + $redis->on('subscribe', function (string $channel) { $this->subscribed[$channel] = true; }); - $redis->on('psubscribe', function ($pattern) { + $redis->on('psubscribe', function (string $pattern) { $this->psubscribed[$pattern] = true; }); - $redis->on('unsubscribe', function ($channel) { + $redis->on('unsubscribe', function (string $channel) { unset($this->subscribed[$channel]); }); - $redis->on('punsubscribe', function ($pattern) { + $redis->on('punsubscribe', function (string $pattern) { unset($this->psubscribed[$pattern]); }); @@ -106,7 +121,7 @@ private function client() }); } - public function __call($name, $args) + public function __call(string $name, array $args): PromiseInterface { if ($this->closed) { return reject(new \RuntimeException( @@ -122,7 +137,7 @@ function ($result) { $this->idle(); return $result; }, - function ($error) { + function (\Exception $error) { $this->idle(); throw $error; } @@ -130,7 +145,7 @@ function ($error) { }); } - public function end() + public function end(): void { if ($this->promise === null) { $this->close(); @@ -140,7 +155,7 @@ public function end() return; } - return $this->client()->then(function (Client $redis) { + $this->client()->then(function (Client $redis) { $redis->on('close', function () { $this->close(); }); @@ -148,7 +163,7 @@ public function end() }); } - public function close() + public function close(): void { if ($this->closed) { return; @@ -176,10 +191,7 @@ public function close() $this->removeAllListeners(); } - /** - * @internal - */ - public function awake() + private function awake(): void { ++$this->pending; @@ -189,10 +201,7 @@ public function awake() } } - /** - * @internal - */ - public function idle() + private function idle(): void { --$this->pending; diff --git a/src/StreamingClient.php b/src/StreamingClient.php index 5ad14a9..91467d6 100644 --- a/src/StreamingClient.php +++ b/src/StreamingClient.php @@ -11,6 +11,7 @@ use Clue\Redis\Protocol\Serializer\SerializerInterface; use Evenement\EventEmitter; use React\Promise\Deferred; +use React\Promise\PromiseInterface; use React\Stream\DuplexStreamInterface; /** @@ -18,14 +19,28 @@ */ class StreamingClient extends EventEmitter implements Client { + /** @var DuplexStreamInterface */ private $stream; + + /** @var ParserInterface */ private $parser; + + /** @var SerializerInterface */ private $serializer; + + /** @var Deferred[] */ private $requests = []; + + /** @var bool */ private $ending = false; + + /** @var bool */ private $closed = false; + /** @var int */ private $subscribed = 0; + + /** @var int */ private $psubscribed = 0; public function __construct(DuplexStreamInterface $stream, ParserInterface $parser = null, SerializerInterface $serializer = null) @@ -40,7 +55,7 @@ public function __construct(DuplexStreamInterface $stream, ParserInterface $pars } } - $stream->on('data', function($chunk) use ($parser) { + $stream->on('data', function (string $chunk) use ($parser) { try { $models = $parser->pushIncoming($chunk); } catch (ParserException $error) { @@ -71,7 +86,7 @@ public function __construct(DuplexStreamInterface $stream, ParserInterface $pars $this->serializer = $serializer; } - public function __call($name, $args) + public function __call(string $name, array $args): PromiseInterface { $request = new Deferred(); $promise = $request->promise(); @@ -102,7 +117,7 @@ public function __call($name, $args) } if (in_array($name, $pubsubs)) { - $promise->then(function ($array) { + $promise->then(function (array $array) { $first = array_shift($array); // (p)(un)subscribe messages are to be forwarded @@ -120,7 +135,7 @@ public function __call($name, $args) return $promise; } - public function handleMessage(ModelInterface $message) + public function handleMessage(ModelInterface $message): void { if (($this->subscribed !== 0 || $this->psubscribed !== 0) && $message instanceof MultiBulkReply) { $array = $message->getValueNative(); @@ -154,7 +169,7 @@ public function handleMessage(ModelInterface $message) } } - public function end() + public function end(): void { $this->ending = true; @@ -163,7 +178,7 @@ public function end() } } - public function close() + public function close(): void { if ($this->closed) { return; diff --git a/tests/FactoryLazyClientTest.php b/tests/FactoryLazyClientTest.php index e6b6730..5b8ef60 100644 --- a/tests/FactoryLazyClientTest.php +++ b/tests/FactoryLazyClientTest.php @@ -4,6 +4,7 @@ use Clue\React\Redis\Client; use Clue\React\Redis\Factory; +use PHPUnit\Framework\MockObject\MockObject; use React\EventLoop\LoopInterface; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; @@ -12,14 +13,16 @@ class FactoryLazyClientTest extends TestCase { + /** @var MockObject */ private $loop; + + /** @var MockObject */ private $connector; + + /** @var Factory */ private $factory; - /** - * @before - */ - public function setUpFactory() + public function setUp(): void { $this->loop = $this->createMock(LoopInterface::class); $this->connector = $this->createMock(ConnectorInterface::class); diff --git a/tests/FactoryStreamingClientTest.php b/tests/FactoryStreamingClientTest.php index 938f4e7..a490cb3 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/FactoryStreamingClientTest.php @@ -4,6 +4,7 @@ use Clue\React\Redis\Client; use Clue\React\Redis\Factory; +use PHPUnit\Framework\MockObject\MockObject; use React\EventLoop\LoopInterface; use React\Promise\Deferred; use React\Socket\ConnectionInterface; @@ -13,14 +14,16 @@ class FactoryStreamingClientTest extends TestCase { + /** @var MockObject */ private $loop; + + /** @var MockObject */ private $connector; + + /** @var Factory */ private $factory; - /** - * @before - */ - public function setUpFactory() + public function setUp(): void { $this->loop = $this->createMock(LoopInterface::class); $this->connector = $this->createMock(ConnectorInterface::class); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 74aa600..22586c2 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -13,17 +13,19 @@ class FunctionalTest extends TestCase { + /** @var StreamSelectLoop */ private $loop; + + /** @var Factory */ private $factory; + + /** @var string */ private $uri; - /** - * @before - */ - public function setUpFactory() + public function setUp(): void { - $this->uri = getenv('REDIS_URI'); - if ($this->uri === false) { + $this->uri = getenv('REDIS_URI') ?: ''; + if ($this->uri === '') { $this->markTestSkipped('No REDIS_URI environment variable given'); } diff --git a/tests/LazyClientTest.php b/tests/LazyClientTest.php index f1cd894..fd61c6e 100644 --- a/tests/LazyClientTest.php +++ b/tests/LazyClientTest.php @@ -5,6 +5,7 @@ use Clue\React\Redis\Client; use Clue\React\Redis\Factory; use Clue\React\Redis\LazyClient; +use PHPUnit\Framework\MockObject\MockObject; use React\EventLoop\LoopInterface; use React\EventLoop\TimerInterface; use React\Promise\Promise; @@ -12,14 +13,16 @@ class LazyClientTest extends TestCase { + /** @var MockObject */ private $factory; + + /** @var MockObject */ private $loop; + + /** @var LazyClient */ private $redis; - /** - * @before - */ - public function setUpClient() + public function setUp(): void { $this->factory = $this->createMock(Factory::class); $this->loop = $this->createMock(LoopInterface::class); @@ -235,7 +238,7 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutC { $client = $this->createMock(Client::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); - $client->expects($this->once())->method('close')->willReturn(\React\Promise\resolve()); + $client->expects($this->once())->method('close'); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); diff --git a/tests/StreamingClientTest.php b/tests/StreamingClientTest.php index 00f50d4..bed0ebf 100644 --- a/tests/StreamingClientTest.php +++ b/tests/StreamingClientTest.php @@ -11,20 +11,25 @@ use Clue\Redis\Protocol\Serializer\SerializerInterface; use Clue\React\Redis\Client; use Clue\React\Redis\StreamingClient; +use PHPUnit\Framework\MockObject\MockObject; use React\Stream\ThroughStream; use React\Stream\DuplexStreamInterface; class StreamingClientTest extends TestCase { + /** @var MockObject */ private $stream; + + /** @var MockObject */ private $parser; + + /** @var MockObject */ private $serializer; + + /** @var StreamingClient */ private $redis; - /** - * @before - */ - public function setUpClient() + public function setUp(): void { $this->stream = $this->createMock(DuplexStreamInterface::class); $this->parser = $this->createMock(ParserInterface::class); @@ -72,18 +77,18 @@ public function testClosingClientEmitsEvent() public function testClosingStreamClosesClient() { - $this->stream = new ThroughStream(); - $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); + $stream = new ThroughStream(); + $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); $this->redis->on('close', $this->expectCallableOnce()); - $this->stream->emit('close'); + $stream->emit('close'); } public function testReceiveParseErrorEmitsErrorEvent() { - $this->stream = new ThroughStream(); - $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); + $stream = new ThroughStream(); + $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); $this->redis->on('error', $this->expectCallableOnceWith( $this->logicalAnd( @@ -99,13 +104,13 @@ public function testReceiveParseErrorEmitsErrorEvent() $this->redis->on('close', $this->expectCallableOnce()); $this->parser->expects($this->once())->method('pushIncoming')->with('message')->willThrowException(new ParserException('Foo')); - $this->stream->emit('data', ['message']); + $stream->emit('data', ['message']); } public function testReceiveUnexpectedReplyEmitsErrorEvent() { - $this->stream = new ThroughStream(); - $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); + $stream = new ThroughStream(); + $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); $this->redis->on('error', $this->expectCallableOnce()); $this->redis->on('error', $this->expectCallableOnceWith( @@ -122,7 +127,7 @@ public function testReceiveUnexpectedReplyEmitsErrorEvent() $this->parser->expects($this->once())->method('pushIncoming')->with('message')->willReturn([new IntegerReply(2)]); - $this->stream->emit('data', ['message']); + $stream->emit('data', ['message']); } /** @@ -192,12 +197,12 @@ public function testClosingClientRejectsAllRemainingRequests() public function testClosingStreamRejectsAllRemainingRequests() { - $this->stream = new ThroughStream(); + $stream = new ThroughStream(function () { return ''; }); $this->parser->expects($this->once())->method('pushIncoming')->willReturn([]); - $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); + $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); $promise = $this->redis->ping(); - $this->stream->close(); + $stream->close(); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( diff --git a/tests/TestCase.php b/tests/TestCase.php index 99189ef..4a73762 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,12 +3,13 @@ namespace Clue\Tests\React\Redis; use PHPUnit\Framework\MockObject\MockBuilder; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase as BaseTestCase; use React\Promise\PromiseInterface; class TestCase extends BaseTestCase { - protected function expectCallableOnce() + protected function expectCallableOnce(): callable { $mock = $this->createCallableMock(); $mock->expects($this->once())->method('__invoke'); @@ -16,7 +17,7 @@ protected function expectCallableOnce() return $mock; } - protected function expectCallableOnceWith($argument) + protected function expectCallableOnceWith($argument): callable { $mock = $this->createCallableMock(); $mock->expects($this->once())->method('__invoke')->with($argument); @@ -24,7 +25,7 @@ protected function expectCallableOnceWith($argument) return $mock; } - protected function expectCallableNever() + protected function expectCallableNever(): callable { $mock = $this->createCallableMock(); $mock->expects($this->never())->method('__invoke'); @@ -32,7 +33,7 @@ protected function expectCallableNever() return $mock; } - protected function createCallableMock() + protected function createCallableMock(): MockObject { if (method_exists(MockBuilder::class, 'addMethods')) { // PHPUnit 9+ @@ -43,11 +44,11 @@ protected function createCallableMock() } } - protected function expectPromiseResolve($promise) + protected function expectPromiseResolve(PromiseInterface $promise): PromiseInterface { $this->assertInstanceOf(PromiseInterface::class, $promise); - $promise->then(null, function($error) { + $promise->then(null, function(\Exception $error) { $this->assertNull($error); $this->fail('promise rejected'); }); @@ -56,7 +57,7 @@ protected function expectPromiseResolve($promise) return $promise; } - protected function expectPromiseReject($promise) + protected function expectPromiseReject(PromiseInterface $promise): PromiseInterface { $this->assertInstanceOf(PromiseInterface::class, $promise); From b9d436246915750aca6dc245a6d3a9e2e13f728a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 24 May 2022 10:51:41 +0200 Subject: [PATCH 34/65] Simplify API, add new `RedisClient` and remove `Factory` --- README.md | 234 +++++------------- examples/cli.php | 76 +++--- examples/incr.php | 5 +- examples/publish.php | 5 +- examples/subscribe.php | 5 +- src/Client.php | 54 ---- src/{ => Io}/Factory.php | 20 +- src/{ => Io}/StreamingClient.php | 4 +- src/{LazyClient.php => RedisClient.php} | 70 +++++- tests/FactoryLazyClientTest.php | 170 ------------- tests/FunctionalTest.php | 80 ++---- tests/{ => Io}/FactoryStreamingClientTest.php | 11 +- tests/{ => Io}/StreamingClientTest.php | 14 +- ...LazyClientTest.php => RedisClientTest.php} | 72 +++--- 14 files changed, 232 insertions(+), 588 deletions(-) delete mode 100644 src/Client.php rename src/{ => Io}/Factory.php (93%) rename src/{ => Io}/StreamingClient.php (98%) rename src/{LazyClient.php => RedisClient.php} (71%) delete mode 100644 tests/FactoryLazyClientTest.php rename tests/{ => Io}/FactoryStreamingClientTest.php (99%) rename tests/{ => Io}/StreamingClientTest.php (97%) rename tests/{LazyClientTest.php => RedisClientTest.php} (91%) diff --git a/README.md b/README.md index 4a76831..7bbcfed 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,8 @@ It enables you to set and query its data or use its PubSub topics to react to in * [Promises](#promises) * [PubSub](#pubsub) * [API](#api) - * [Factory](#factory) - * [createClient()](#createclient) - * [createLazyClient()](#createlazyclient) - * [Client](#client) + * [RedisClient](#redisclient) + * [__construct()](#__construct) * [__call()](#__call) * [end()](#end) * [close()](#close) @@ -75,8 +73,7 @@ local Redis server and send some requests: require __DIR__ . '/vendor/autoload.php'; -$factory = new Clue\React\Redis\Factory(); -$redis = $factory->createLazyClient('localhost:6379'); +$redis = new Clue\React\Redis\RedisClient('localhost:6379'); $redis->set('greeting', 'Hello world'); $redis->append('greeting', '!'); @@ -100,10 +97,12 @@ See also the [examples](examples). ### Commands -Most importantly, this project provides a [`Client`](#client) instance that +Most importantly, this project provides a [`RedisClient`](#redisclient) instance that can be used to invoke all [Redis commands](https://redis.io/commands) (such as `GET`, `SET`, etc.). ```php +$redis = new Clue\React\Redis\RedisClient('localhost:6379'); + $redis->get($key); $redis->set($key, $value); $redis->exists($key); @@ -262,161 +261,28 @@ $redis->on('punsubscribe', function (string $pattern, int $total) { }); ``` -When using the [`createLazyClient()`](#createlazyclient) method, the `unsubscribe` -and `punsubscribe` events will be invoked automatically when the underlying -connection is lost. This gives you control over re-subscribing to the channels -and patterns as appropriate. +When the underlying connection is lost, the `unsubscribe` and `punsubscribe` events +will be invoked automatically. This gives you control over re-subscribing to the +channels and patterns as appropriate. ## API -### Factory - -The `Factory` is responsible for creating your [`Client`](#client) instance. - -```php -$factory = new Clue\React\Redis\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 Clue\React\Redis\Factory(null, $connector); -``` - -#### createClient() - -The `createClient(string $uri): PromiseInterface` method can be used to -create a new [`Client`](#client). +### RedisClient -It helps with establishing a plain TCP/IP or secure TLS connection to Redis -and optionally authenticating (AUTH) and selecting the right database (SELECT). - -```php -$factory->createClient('localhost:6379')->then( - function (Client $redis) { - // client connected (and authenticated) - }, - function (Exception $e) { - // an error occurred while trying to connect (or authenticate) client - } -); -``` - -The method returns a [Promise](https://github.com/reactphp/promise) that -will resolve with a [`Client`](#client) -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 Redis authentication. - -```php -$promise = $factory->createClient($uri); - -Loop::addTimer(3.0, function () use ($promise) { - $promise->cancel(); -}); -``` - -The `$redisUri` can be given in the -[standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form -`[redis[s]://][:auth@]host[:port][/db]`. -You can omit the URI scheme and port if you're connecting to the default port 6379: - -```php -// both are equivalent due to defaults being applied -$factory->createClient('localhost'); -$factory->createClient('redis://localhost:6379'); -``` - -Redis supports password-based authentication (`AUTH` command). Note that Redis' -authentication mechanism does not employ a username, so you can pass the -password `h@llo` URL-encoded (percent-encoded) as part of the URI like this: - -```php -// all forms are equivalent -$factory->createClient('redis://:h%40llo@localhost'); -$factory->createClient('redis://ignored:h%40llo@localhost'); -$factory->createClient('redis://localhost?password=h%40llo'); -``` - -You can optionally include a path that will be used to select (SELECT command) the right database: - -```php -// both forms are equivalent -$factory->createClient('redis://localhost/2'); -$factory->createClient('redis://localhost?db=2'); -``` - -You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/rediss) -`rediss://` URI scheme if you're using a secure TLS proxy in front of Redis: - -```php -$factory->createClient('rediss://redis.example.com:6340'); -``` - -You can use the `redis+unix://` URI scheme if your Redis instance is listening -on a Unix domain socket (UDS) path: - -```php -$factory->createClient('redis+unix:///tmp/redis.sock'); - -// the URI MAY contain `password` and `db` query parameters as seen above -$factory->createClient('redis+unix:///tmp/redis.sock?password=secret&db=2'); - -// the URI MAY contain authentication details as userinfo as seen above -// should be used with care, also note that database can not be passed as path -$factory->createClient('redis+unix://:secret@/tmp/redis.sock'); -``` - -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->createClient('localhost?timeout=0.5'); -``` - -#### createLazyClient() - -The `createLazyClient(string $uri): Client` method can be used to -create a new [`Client`](#client). - -It helps with establishing a plain TCP/IP or secure TLS connection to Redis -and optionally authenticating (AUTH) and selecting the right database (SELECT). +The `RedisClient` is responsible for exchanging messages with your Redis server +and keeps track of pending commands. ```php -$redis = $factory->createLazyClient('localhost:6379'); +$redis = new Clue\React\Redis\RedisClient('localhost:6379'); $redis->incr('hello'); $redis->end(); ``` -This method immediately returns a "virtual" connection implementing the -[`Client`](#client) that can be used to interface with your Redis database. -Internally, it lazily creates the underlying database connection only on +Besides defining a few methods, this interface also implements the +`EventEmitterInterface` which allows you to react to certain events as documented below. + +Internally, this class 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 @@ -428,9 +294,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 oustanding 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 `Client` interface 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 @@ -450,24 +313,25 @@ creating a new underlying connection repeating the above commands again. 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 -`end()` method to ensure that the "virtual" connection will be soft-closed +`end()` method to ensure that the connection will be soft-closed and no further commands can be enqueued. Similarly, calling `end()` 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 `createClient()` which resolves with a promise. For many -simple use cases it may be easier to create a lazy connection. +#### __construct() + +The `new RedisClient(string $url, ConnectorInterface $connector = null, LoopInterface $loop = null)` constructor can be used to +create a new `RedisClient` instance. -The `$redisUri` can be given in the +The `$url` can be given in the [standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form `[redis[s]://][:auth@]host[:port][/db]`. You can omit the URI scheme and port if you're connecting to the default port 6379: ```php // both are equivalent due to defaults being applied -$factory->createLazyClient('localhost'); -$factory->createLazyClient('redis://localhost:6379'); +$redis = new Clue\React\Redis\RedisClient('localhost'); +$redis = new Clue\React\Redis\RedisClient('redis://localhost:6379'); ``` Redis supports password-based authentication (`AUTH` command). Note that Redis' @@ -476,38 +340,38 @@ password `h@llo` URL-encoded (percent-encoded) as part of the URI like this: ```php // all forms are equivalent -$factory->createLazyClient('redis://:h%40llo@localhost'); -$factory->createLazyClient('redis://ignored:h%40llo@localhost'); -$factory->createLazyClient('redis://localhost?password=h%40llo'); +$redis = new Clue\React\Redis\RedisClient('redis://:h%40llo@localhost'); +$redis = new Clue\React\Redis\RedisClient('redis://ignored:h%40llo@localhost'); +$redis = new Clue\React\Redis\RedisClient('redis://localhost?password=h%40llo'); ``` You can optionally include a path that will be used to select (SELECT command) the right database: ```php // both forms are equivalent -$factory->createLazyClient('redis://localhost/2'); -$factory->createLazyClient('redis://localhost?db=2'); +$redis = new Clue\React\Redis\RedisClient('redis://localhost/2'); +$redis = new Clue\React\Redis\RedisClient('redis://localhost?db=2'); ``` You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/rediss) `rediss://` URI scheme if you're using a secure TLS proxy in front of Redis: ```php -$factory->createLazyClient('rediss://redis.example.com:6340'); +$redis = new Clue\React\Redis\RedisClient('rediss://redis.example.com:6340'); ``` You can use the `redis+unix://` URI scheme if your Redis instance is listening on a Unix domain socket (UDS) path: ```php -$factory->createLazyClient('redis+unix:///tmp/redis.sock'); +$redis = new Clue\React\Redis\RedisClient('redis+unix:///tmp/redis.sock'); // the URI MAY contain `password` and `db` query parameters as seen above -$factory->createLazyClient('redis+unix:///tmp/redis.sock?password=secret&db=2'); +$redis = new Clue\React\Redis\RedisClient('redis+unix:///tmp/redis.sock?password=secret&db=2'); // the URI MAY contain authentication details as userinfo as seen above // should be used with care, also note that database can not be passed as path -$factory->createLazyClient('redis+unix://:secret@/tmp/redis.sock'); +$redis = new Clue\React\Redis\RedisClient('redis+unix://:secret@/tmp/redis.sock'); ``` This method respects PHP's `default_socket_timeout` setting (default 60s) @@ -516,7 +380,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->createLazyClient('localhost?timeout=0.5'); +$redis = new Clue\React\Redis\RedisClient('localhost?timeout=0.5'); ``` By default, this method will keep "idle" connections open for 60s and will @@ -529,16 +393,32 @@ idle timeout value in seconds (or use a negative number to not apply a timeout) like this: ```php -$factory->createLazyClient('localhost?idle=0.1'); +$redis = new Clue\React\Redis\RedisClient('localhost?idle=0.1'); ``` -### Client +If you need custom DNS, proxy or TLS settings, you can explicitly pass a +custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): -The `Client` is responsible for exchanging messages with Redis -and keeps track of pending commands. +```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 + ] +]); -Besides defining a few methods, this interface also implements the -`EventEmitterInterface` which allows you to react to certain events as documented below. +$redis = new Clue\React\Redis\RedisClient('localhost', $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. #### __call() diff --git a/examples/cli.php b/examples/cli.php index d0b41f8..0eb8b65 100644 --- a/examples/cli.php +++ b/examples/cli.php @@ -7,48 +7,42 @@ require __DIR__ . '/../vendor/autoload.php'; -$factory = new Clue\React\Redis\Factory(); - -echo '# connecting to redis...' . PHP_EOL; - -$factory->createClient(getenv('REDIS_URI') ?: 'localhost:6379')->then(function (Clue\React\Redis\Client $redis) { - echo '# connected! Entering interactive mode, hit CTRL-D to quit' . PHP_EOL; - - Loop::addReadStream(STDIN, function () use ($redis) { - $line = fgets(STDIN); - if ($line === false || $line === '') { - echo '# CTRL-D -> Ending connection...' . PHP_EOL; - Loop::removeReadStream(STDIN); - return $redis->end(); - } - - $line = rtrim($line); - if ($line === '') { - return; - } - - $params = explode(' ', $line); - $method = array_shift($params); - $promise = call_user_func_array([$redis, $method], $params); - - // special method such as end() / close() called - if (!$promise instanceof React\Promise\PromiseInterface) { - return; - } - - $promise->then(function ($data) { - echo '# reply: ' . json_encode($data) . PHP_EOL; - }, function (Exception $e) { - echo '# error reply: ' . $e->getMessage() . PHP_EOL; - }); - }); - - $redis->on('close', function() { - echo '## DISCONNECTED' . PHP_EOL; +$redis = new Clue\React\Redis\RedisClient(getenv('REDIS_URI') ?: 'localhost:6379'); +Loop::addReadStream(STDIN, function () use ($redis) { + $line = fgets(STDIN); + if ($line === false || $line === '') { + echo '# CTRL-D -> Ending connection...' . PHP_EOL; Loop::removeReadStream(STDIN); + $redis->end(); + return; + } + + $line = rtrim($line); + if ($line === '') { + return; + } + + $params = explode(' ', $line); + $method = array_shift($params); + $promise = call_user_func_array([$redis, $method], $params); + + // special method such as end() / close() called + if (!$promise instanceof React\Promise\PromiseInterface) { + return; + } + + $promise->then(function ($data) { + echo '# reply: ' . json_encode($data) . PHP_EOL; + }, function ($e) { + echo '# error reply: ' . $e->getMessage() . PHP_EOL; }); -}, function (Exception $e) { - echo 'Error: ' . $e->getMessage() . PHP_EOL; - exit(1); }); + +$redis->on('close', function() { + echo '## DISCONNECTED' . PHP_EOL; + + Loop::removeReadStream(STDIN); +}); + +echo '# Entering interactive mode ready, hit CTRL-D to quit' . PHP_EOL; diff --git a/examples/incr.php b/examples/incr.php index 1d28524..6744da5 100644 --- a/examples/incr.php +++ b/examples/incr.php @@ -5,8 +5,7 @@ require __DIR__ . '/../vendor/autoload.php'; -$factory = new Clue\React\Redis\Factory(); -$redis = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); +$redis = new Clue\React\Redis\RedisClient(getenv('REDIS_URI') ?: 'localhost:6379'); $redis->incr('test'); @@ -17,4 +16,4 @@ exit(1); }); -//$redis->end(); +$redis->end(); diff --git a/examples/publish.php b/examples/publish.php index 90e6a48..06bcd9b 100644 --- a/examples/publish.php +++ b/examples/publish.php @@ -5,12 +5,11 @@ require __DIR__ . '/../vendor/autoload.php'; -$factory = new Clue\React\Redis\Factory(); -$redis = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); - $channel = $argv[1] ?? 'channel'; $message = $argv[2] ?? 'message'; +$redis = new Clue\React\Redis\RedisClient(getenv('REDIS_URI') ?: 'localhost:6379'); + $redis->publish($channel, $message)->then(function (int $received) { echo 'Successfully published. Received by ' . $received . PHP_EOL; }, function (Exception $e) { diff --git a/examples/subscribe.php b/examples/subscribe.php index 1270b7c..673b565 100644 --- a/examples/subscribe.php +++ b/examples/subscribe.php @@ -7,11 +7,10 @@ require __DIR__ . '/../vendor/autoload.php'; -$factory = new Clue\React\Redis\Factory(); -$redis = $factory->createLazyClient(getenv('REDIS_URI') ?: 'localhost:6379'); - $channel = $argv[1] ?? 'channel'; +$redis = new Clue\React\Redis\RedisClient(getenv('REDIS_URI') ?: 'localhost:6379'); + $redis->subscribe($channel)->then(function () { echo 'Now subscribed to channel ' . PHP_EOL; }, function (Exception $e) use ($redis) { diff --git a/src/Client.php b/src/Client.php deleted file mode 100644 index 714ac88..0000000 --- a/src/Client.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ - public function __call(string $name, array $args): PromiseInterface; - - /** - * end connection once all pending requests have been replied to - * - * @return void - * @uses self::close() once all replies have been received - * @see self::close() for closing the connection immediately - */ - public function end(): void; - - /** - * close connection immediately - * - * This will emit the "close" event. - * - * @return void - * @see self::end() for closing the connection once the client is idle - */ - public function close(): void; -} diff --git a/src/Factory.php b/src/Io/Factory.php similarity index 93% rename from src/Factory.php rename to src/Io/Factory.php index 3b77c92..0f88825 100644 --- a/src/Factory.php +++ b/src/Io/Factory.php @@ -1,6 +1,6 @@ Promise that will - * be fulfilled with `Client` on success or rejects with `\Exception` on error. + * @return PromiseInterface Promise that will + * be fulfilled with `StreamingClient` on success or rejects with `\Exception` on error. */ public function createClient(string $uri): PromiseInterface { @@ -178,15 +181,4 @@ function (\Exception $e) use ($redis, $uri) { throw $e; }); } - - /** - * Create Redis client connected to address of given redis instance - * - * @param string $target - * @return Client - */ - public function createLazyClient($target): Client - { - return new LazyClient($target, $this, $this->loop); - } } diff --git a/src/StreamingClient.php b/src/Io/StreamingClient.php similarity index 98% rename from src/StreamingClient.php rename to src/Io/StreamingClient.php index 91467d6..126bff5 100644 --- a/src/StreamingClient.php +++ b/src/Io/StreamingClient.php @@ -1,6 +1,6 @@ */ private $psubscribed = []; - public function __construct(string $target, Factory $factory, LoopInterface $loop) + /** + * @param string $url + * @param ?ConnectorInterface $connector + * @param ?LoopInterface $loop + */ + public function __construct($url, ConnectorInterface $connector = null, LoopInterface $loop = null) { $args = []; - \parse_str((string) \parse_url($target, \PHP_URL_QUERY), $args); + \parse_str((string) \parse_url($url, \PHP_URL_QUERY), $args); if (isset($args['idle'])) { $this->idlePeriod = (float)$args['idle']; } - $this->target = $target; - $this->factory = $factory; - $this->loop = $loop; + $this->target = $url; + $this->loop = $loop ?: Loop::get(); + $this->factory = new Factory($this->loop, $connector); } private function client(): PromiseInterface @@ -63,7 +82,7 @@ private function client(): PromiseInterface return $this->promise; } - return $this->promise = $this->factory->createClient($this->target)->then(function (Client $redis) { + return $this->promise = $this->factory->createClient($this->target)->then(function (StreamingClient $redis) { // connection completed => remember only until closed $redis->on('close', function () { $this->promise = null; @@ -121,6 +140,16 @@ private function client(): PromiseInterface }); } + /** + * Invoke the given command and return a Promise that will be resolved when the request has been replied to + * + * This is a magic method that will be invoked when calling any redis + * command on this instance. + * + * @param string $name + * @param string[] $args + * @return PromiseInterface Promise + */ public function __call(string $name, array $args): PromiseInterface { if ($this->closed) { @@ -130,7 +159,7 @@ public function __call(string $name, array $args): PromiseInterface )); } - return $this->client()->then(function (Client $redis) use ($name, $args) { + return $this->client()->then(function (StreamingClient $redis) use ($name, $args) { $this->awake(); return \call_user_func_array([$redis, $name], $args)->then( function ($result) { @@ -145,6 +174,13 @@ function (\Exception $error) { }); } + /** + * end connection once all pending requests have been replied to + * + * @return void + * @uses self::close() once all replies have been received + * @see self::close() for closing the connection immediately + */ public function end(): void { if ($this->promise === null) { @@ -155,7 +191,7 @@ public function end(): void return; } - $this->client()->then(function (Client $redis) { + $this->client()->then(function (StreamingClient $redis) { $redis->on('close', function () { $this->close(); }); @@ -163,6 +199,14 @@ public function end(): void }); } + /** + * close connection immediately + * + * This will emit the "close" event. + * + * @return void + * @see self::end() for closing the connection once the client is idle + */ public function close(): void { if ($this->closed) { @@ -173,7 +217,7 @@ public function close(): void // either close active connection or cancel pending connection attempt if ($this->promise !== null) { - $this->promise->then(function (Client $redis) { + $this->promise->then(function (StreamingClient $redis) { $redis->close(); }); if ($this->promise !== null) { @@ -207,7 +251,7 @@ private function idle(): void if ($this->pending < 1 && $this->idlePeriod >= 0 && !$this->subscribed && !$this->psubscribed && $this->promise !== null) { $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () { - $this->promise->then(function (Client $redis) { + $this->promise->then(function (StreamingClient $redis) { $redis->close(); }); $this->promise = null; diff --git a/tests/FactoryLazyClientTest.php b/tests/FactoryLazyClientTest.php deleted file mode 100644 index 5b8ef60..0000000 --- a/tests/FactoryLazyClientTest.php +++ /dev/null @@ -1,170 +0,0 @@ -loop = $this->createMock(LoopInterface::class); - $this->connector = $this->createMock(ConnectorInterface::class); - $this->factory = new Factory($this->loop, $this->connector); - } - - public function testConstructWithoutLoopAssignsLoopAutomatically() - { - $factory = new Factory(); - - $ref = new \ReflectionProperty($factory, 'loop'); - $ref->setAccessible(true); - $loop = $ref->getValue($factory); - - $this->assertInstanceOf(LoopInterface::class, $loop); - } - - public function testWillConnectWithDefaultPort() - { - $this->connector->expects($this->never())->method('connect')->with('redis.example.com:6379')->willReturn(reject(new \RuntimeException())); - $this->factory->createLazyClient('redis.example.com'); - } - - public function testWillConnectToLocalhost() - { - $this->connector->expects($this->never())->method('connect')->with('localhost:1337')->willReturn(reject(new \RuntimeException())); - $this->factory->createLazyClient('localhost:1337'); - } - - public function testWillResolveIfConnectorResolves() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write'); - - $this->connector->expects($this->never())->method('connect')->willReturn(resolve($stream)); - $redis = $this->factory->createLazyClient('localhost'); - - $this->assertInstanceOf(Client::class, $redis); - } - - public function testWillWriteSelectCommandIfTargetContainsPath() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n"); - - $this->connector->expects($this->never())->method('connect')->willReturn(resolve($stream)); - $this->factory->createLazyClient('redis://127.0.0.1/demo'); - } - - public function testWillWriteSelectCommandIfTargetContainsDbQueryParameter() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write')->with("*2\r\n$6\r\nselect\r\n$1\r\n4\r\n"); - - $this->connector->expects($this->never())->method('connect')->willReturn(resolve($stream)); - $this->factory->createLazyClient('redis://127.0.0.1?db=4'); - } - - public function testWillWriteAuthCommandIfRedisUriContainsUserInfo() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - - $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); - $this->factory->createLazyClient('redis://hello:world@example.com'); - } - - public function testWillWriteAuthCommandIfRedisUriContainsEncodedUserInfo() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n"); - - $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); - $this->factory->createLazyClient('redis://:h%40llo@example.com'); - } - - public function testWillWriteAuthCommandIfTargetContainsPasswordQueryParameter() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$6\r\nsecret\r\n"); - - $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); - $this->factory->createLazyClient('redis://example.com?password=secret'); - } - - public function testWillWriteAuthCommandIfTargetContainsEncodedPasswordQueryParameter() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n"); - - $this->connector->expects($this->never())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); - $this->factory->createLazyClient('redis://example.com?password=h%40llo'); - } - - public function testWillWriteAuthCommandIfRedissUriContainsUserInfo() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - - $this->connector->expects($this->never())->method('connect')->with('tls://example.com:6379')->willReturn(resolve($stream)); - $this->factory->createLazyClient('rediss://hello:world@example.com'); - } - - public function testWillWriteAuthCommandIfRedisUnixUriContainsPasswordQueryParameter() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - - $this->connector->expects($this->never())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); - $this->factory->createLazyClient('redis+unix:///tmp/redis.sock?password=world'); - } - - public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); - - $this->connector->expects($this->never())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); - $this->factory->createLazyClient('redis+unix://hello:world@/tmp/redis.sock'); - } - - public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter() - { - $stream = $this->createMock(ConnectionInterface::class); - $stream->expects($this->never())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n"); - - $this->connector->expects($this->never())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); - $this->factory->createLazyClient('redis+unix:///tmp/redis.sock?db=demo'); - } - - public function testWillRejectIfConnectorRejects() - { - $this->connector->expects($this->never())->method('connect')->with('127.0.0.1:2')->willReturn(reject(new \RuntimeException())); - $redis = $this->factory->createLazyClient('redis://127.0.0.1:2'); - - $this->assertInstanceOf(Client::class, $redis); - } - - public function testWillRejectIfTargetIsInvalid() - { - $redis = $this->factory->createLazyClient('http://invalid target'); - - $this->assertInstanceOf(Client::class, $redis); - } -} diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 22586c2..c81cd85 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -2,13 +2,10 @@ namespace Clue\Tests\React\Redis; -use Clue\React\Redis\Client; -use Clue\React\Redis\Factory; -use Clue\React\Redis\StreamingClient; +use Clue\React\Redis\RedisClient; use React\EventLoop\StreamSelectLoop; use React\Promise\Deferred; use React\Promise\PromiseInterface; -use React\Stream\DuplexResourceStream; use function Clue\React\Block\await; class FunctionalTest extends TestCase @@ -16,9 +13,6 @@ class FunctionalTest extends TestCase /** @var StreamSelectLoop */ private $loop; - /** @var Factory */ - private $factory; - /** @var string */ private $uri; @@ -30,12 +24,11 @@ public function setUp(): void } $this->loop = new StreamSelectLoop(); - $this->factory = new Factory($this->loop); } public function testPing() { - $redis = $this->createClient($this->uri); + $redis = new RedisClient($this->uri, null, $this->loop); $promise = $redis->ping(); $this->assertInstanceOf(PromiseInterface::class, $promise); @@ -47,7 +40,7 @@ public function testPing() public function testPingLazy() { - $redis = $this->factory->createLazyClient($this->uri); + $redis = new RedisClient($this->uri, null, $this->loop); $promise = $redis->ping(); $this->assertInstanceOf(PromiseInterface::class, $promise); @@ -62,7 +55,7 @@ public function testPingLazy() */ public function testPingLazyWillNotBlockLoopWhenIdleTimeIsSmall() { - $redis = $this->factory->createLazyClient($this->uri . '?idle=0'); + $redis = new RedisClient($this->uri . '?idle=0', null, $this->loop); $redis->ping(); @@ -74,7 +67,7 @@ public function testPingLazyWillNotBlockLoopWhenIdleTimeIsSmall() */ public function testLazyClientWithoutCommandsWillNotBlockLoop() { - $redis = $this->factory->createLazyClient($this->uri); + $redis = new RedisClient($this->uri, null, $this->loop); $this->loop->run(); @@ -83,7 +76,7 @@ public function testLazyClientWithoutCommandsWillNotBlockLoop() public function testMgetIsNotInterpretedAsSubMessage() { - $redis = $this->createClient($this->uri); + $redis = new RedisClient($this->uri, null, $this->loop); $redis->mset('message', 'message', 'channel', 'channel', 'payload', 'payload'); @@ -95,7 +88,7 @@ public function testMgetIsNotInterpretedAsSubMessage() public function testPipeline() { - $redis = $this->createClient($this->uri); + $redis = new RedisClient($this->uri, null, $this->loop); $redis->set('a', 1)->then($this->expectCallableOnceWith('OK')); $redis->incr('a')->then($this->expectCallableOnceWith(2)); @@ -107,7 +100,7 @@ public function testPipeline() public function testInvalidCommand() { - $redis = $this->createClient($this->uri); + $redis = new RedisClient($this->uri, null, $this->loop); $promise = $redis->doesnotexist(1, 2, 3); if (method_exists($this, 'expectException')) { @@ -120,7 +113,7 @@ public function testInvalidCommand() public function testMultiExecEmpty() { - $redis = $this->createClient($this->uri); + $redis = new RedisClient($this->uri, null, $this->loop); $redis->multi()->then($this->expectCallableOnceWith('OK')); $promise = $redis->exec()->then($this->expectCallableOnceWith([])); @@ -129,7 +122,7 @@ public function testMultiExecEmpty() public function testMultiExecQueuedExecHasValues() { - $redis = $this->createClient($this->uri); + $redis = new RedisClient($this->uri, null, $this->loop); $redis->multi()->then($this->expectCallableOnceWith('OK')); $redis->set('b', 10)->then($this->expectCallableOnceWith('QUEUED')); @@ -143,8 +136,8 @@ public function testMultiExecQueuedExecHasValues() public function testPubSub() { - $consumer = $this->createClient($this->uri); - $producer = $this->createClient($this->uri); + $consumer = new RedisClient($this->uri, null, $this->loop); + $producer = new RedisClient($this->uri, null, $this->loop); $channel = 'channel:test:' . mt_rand(); @@ -164,7 +157,7 @@ public function testPubSub() public function testClose() { - $redis = $this->createClient($this->uri); + $redis = new RedisClient($this->uri, null, $this->loop); $redis->get('willBeCanceledAnyway')->then(null, $this->expectCallableOnce()); @@ -175,7 +168,7 @@ public function testClose() public function testCloseLazy() { - $redis = $this->factory->createLazyClient($this->uri); + $redis = new RedisClient($this->uri, null, $this->loop); $redis->get('willBeCanceledAnyway')->then(null, $this->expectCallableOnce()); @@ -183,49 +176,4 @@ public function testCloseLazy() $redis->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce()); } - - public function testInvalidProtocol() - { - $redis = $this->createClientResponse("communication does not conform to protocol\r\n"); - - $redis->on('error', $this->expectCallableOnce()); - $redis->on('close', $this->expectCallableOnce()); - - $promise = $redis->get('willBeRejectedDueToClosing'); - - $this->expectException(\Exception::class); - await($promise, $this->loop); - } - - public function testInvalidServerRepliesWithDuplicateMessages() - { - $redis = $this->createClientResponse("+OK\r\n-ERR invalid\r\n"); - - $redis->on('error', $this->expectCallableOnce()); - $redis->on('close', $this->expectCallableOnce()); - - $promise = $redis->set('a', 0)->then($this->expectCallableOnceWith('OK')); - - await($promise, $this->loop); - } - - /** - * @param string $uri - * @return Client - */ - protected function createClient($uri) - { - return await($this->factory->createClient($uri), $this->loop); - } - - protected function createClientResponse($response) - { - $fp = fopen('php://temp', 'r+'); - fwrite($fp, $response); - fseek($fp, 0); - - $stream = new DuplexResourceStream($fp, $this->loop); - - return new StreamingClient($stream); - } } diff --git a/tests/FactoryStreamingClientTest.php b/tests/Io/FactoryStreamingClientTest.php similarity index 99% rename from tests/FactoryStreamingClientTest.php rename to tests/Io/FactoryStreamingClientTest.php index a490cb3..9941b07 100644 --- a/tests/FactoryStreamingClientTest.php +++ b/tests/Io/FactoryStreamingClientTest.php @@ -1,9 +1,10 @@ assertTrue(is_callable($dataHandler)); $dataHandler("+OK\r\n"); - $promise->then($this->expectCallableOnceWith($this->isInstanceOf(Client::class))); + $promise->then($this->expectCallableOnceWith($this->isInstanceOf(StreamingClient::class))); } public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorResponseIfRedisUriContainsUserInfo() @@ -286,7 +287,7 @@ public function testWillResolveWhenSelectCommandReceivesOkResponseIfRedisUriCont $this->assertTrue(is_callable($dataHandler)); $dataHandler("+OK\r\n"); - $promise->then($this->expectCallableOnceWith($this->isInstanceOf(Client::class))); + $promise->then($this->expectCallableOnceWith($this->isInstanceOf(StreamingClient::class))); } public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErrorResponseIfRedisUriContainsPath() diff --git a/tests/StreamingClientTest.php b/tests/Io/StreamingClientTest.php similarity index 97% rename from tests/StreamingClientTest.php rename to tests/Io/StreamingClientTest.php index bed0ebf..b0274f8 100644 --- a/tests/StreamingClientTest.php +++ b/tests/Io/StreamingClientTest.php @@ -1,7 +1,8 @@ psubscribe('demo_*'); $this->expectPromiseResolve($promise); @@ -321,9 +321,9 @@ public function testPubsubPatternSubscribe(Client $client) /** * @depends testPubsubPatternSubscribe - * @param Client $client + * @param StreamingClient $client */ - public function testPubsubMessage(Client $client) + public function testPubsubMessage(StreamingClient $client) { $client->on('message', $this->expectCallableOnce()); $client->handleMessage(new MultiBulkReply([new BulkReply('message'), new BulkReply('test'), new BulkReply('payload')])); diff --git a/tests/LazyClientTest.php b/tests/RedisClientTest.php similarity index 91% rename from tests/LazyClientTest.php rename to tests/RedisClientTest.php index fd61c6e..c1f8d65 100644 --- a/tests/LazyClientTest.php +++ b/tests/RedisClientTest.php @@ -2,16 +2,16 @@ namespace Clue\Tests\React\Redis; -use Clue\React\Redis\Client; -use Clue\React\Redis\Factory; -use Clue\React\Redis\LazyClient; +use Clue\React\Redis\RedisClient; +use Clue\React\Redis\Io\Factory; +use Clue\React\Redis\Io\StreamingClient; use PHPUnit\Framework\MockObject\MockObject; use React\EventLoop\LoopInterface; use React\EventLoop\TimerInterface; -use React\Promise\Promise; use React\Promise\Deferred; +use React\Promise\Promise; -class LazyClientTest extends TestCase +class RedisClientTest extends TestCase { /** @var MockObject */ private $factory; @@ -19,7 +19,7 @@ class LazyClientTest extends TestCase /** @var MockObject */ private $loop; - /** @var LazyClient */ + /** @var RedisClient */ private $redis; public function setUp(): void @@ -27,7 +27,11 @@ public function setUp(): void $this->factory = $this->createMock(Factory::class); $this->loop = $this->createMock(LoopInterface::class); - $this->redis = new LazyClient('localhost', $this->factory, $this->loop); + $this->redis = new RedisClient('localhost', null, $this->loop); + + $ref = new \ReflectionProperty($this->redis, 'factory'); + $ref->setAccessible(true); + $ref->setValue($this->redis, $this->factory); } public function testPingWillCreateUnderlyingClientAndReturnPendingPromise() @@ -53,7 +57,7 @@ public function testPingTwiceWillCreateOnceUnderlyingClient() public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimer() { - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $deferred = new Deferred(); @@ -69,9 +73,13 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimerWithIdleTimeFromQueryParam() { - $this->redis = new LazyClient('localhost?idle=10', $this->factory, $this->loop); + $this->redis = new RedisClient('localhost?idle=10', null, $this->loop); - $client = $this->createMock(Client::class); + $ref = new \ReflectionProperty($this->redis, 'factory'); + $ref->setAccessible(true); + $ref->setValue($this->redis, $this->factory); + + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $deferred = new Deferred(); @@ -87,9 +95,13 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartIdleTimerWhenIdleParamIsNegative() { - $this->redis = new LazyClient('localhost?idle=-1', $this->factory, $this->loop); + $this->redis = new RedisClient('localhost?idle=-1', null, $this->loop); + + $ref = new \ReflectionProperty($this->redis, 'factory'); + $ref->setAccessible(true); + $ref->setValue($this->redis, $this->factory); - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $deferred = new Deferred(); @@ -106,7 +118,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartId public function testPingWillRejectWhenUnderlyingClientRejectsPingAndStartIdleTimer() { $error = new \RuntimeException(); - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\reject($error)); $deferred = new Deferred(); @@ -155,7 +167,7 @@ public function testPingAfterPreviousFactoryRejectsUnderlyingClientWillCreateNew public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewUnderlyingConnection() { $closeHandler = null; - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->any())->method('on')->withConsecutive( ['close', $this->callback(function ($arg) use (&$closeHandler) { @@ -199,7 +211,7 @@ public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection( public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() { $deferred = new Deferred(); - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) @@ -217,7 +229,7 @@ public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStartsAfterFirstResolves() { $deferred = new Deferred(); - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) @@ -236,7 +248,7 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent() { - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $client->expects($this->once())->method('close'); @@ -299,7 +311,7 @@ public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientC public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlreadyResolved() { - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $client->expects($this->once())->method('close'); @@ -314,7 +326,7 @@ public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlready public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() { $deferred = new Deferred(); - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->once())->method('close'); @@ -332,7 +344,7 @@ public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() public function testCloseAfterPingRejectsWillEmitClose() { $deferred = new Deferred(); - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->once())->method('close')->willReturnCallback(function () use ($client) { $client->emit('close'); @@ -360,7 +372,7 @@ public function testEndWillCloseClientIfUnderlyingConnectionIsNotPending() public function testEndAfterPingWillEndUnderlyingClient() { - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->once())->method('end'); @@ -375,7 +387,7 @@ public function testEndAfterPingWillEndUnderlyingClient() public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose() { $closeHandler = null; - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->once())->method('end'); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$closeHandler) { @@ -401,7 +413,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() { $error = new \RuntimeException(); - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $deferred = new Deferred(); @@ -416,7 +428,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() { - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $deferred = new Deferred(); @@ -432,7 +444,7 @@ public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnectionEmitsCloseAfterPingIsAlreadyResolved() { $closeHandler = null; - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $deferred = new Deferred(); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->any())->method('on')->withConsecutive( @@ -460,7 +472,7 @@ public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnect public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubChannel() { $messageHandler = null; - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$messageHandler) { if ($event === 'message') { @@ -482,7 +494,7 @@ public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubCh public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClosesWhileUsingPubSubChannel() { $allHandler = null; - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->exactly(6))->method('__call')->willReturn(\React\Promise\resolve()); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$allHandler) { if (!isset($allHandler[$event])) { @@ -527,7 +539,7 @@ public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAnd { $subscribeHandler = null; $deferred = new Deferred(); - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('subscribe')->willReturn($deferred->promise()); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$subscribeHandler) { if ($event === 'subscribe' && $subscribeHandler === null) { @@ -553,7 +565,7 @@ public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientReso $unsubscribeHandler = null; $deferredSubscribe = new Deferred(); $deferredUnsubscribe = new Deferred(); - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls($deferredSubscribe->promise(), $deferredUnsubscribe->promise()); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$subscribeHandler, &$unsubscribeHandler) { if ($event === 'subscribe' && $subscribeHandler === null) { @@ -585,7 +597,7 @@ public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResp { $closeHandler = null; $deferred = new Deferred(); - $client = $this->createMock(Client::class); + $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('blpop')->willReturn($deferred->promise()); $client->expects($this->any())->method('on')->withConsecutive( ['close', $this->callback(function ($arg) use (&$closeHandler) { From 7e5dc68cf162a0083469aa925cb2801bebb09a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 26 May 2022 14:39:17 +0200 Subject: [PATCH 35/65] Reduce default idle time to 1ms --- README.md | 23 ++++++++++------------- examples/incr.php | 2 -- examples/publish.php | 2 -- src/RedisClient.php | 2 +- tests/FunctionalTest.php | 4 ++-- tests/RedisClientTest.php | 2 +- 6 files changed, 14 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 7bbcfed..94d301a 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,6 @@ $redis->get('greeting')->then(function (string $greeting) { $redis->incr('invocation')->then(function (int $n) { echo 'This is invocation #' . $n . PHP_EOL; }); - -// end connection once all pending requests have been resolved -$redis->end(); ``` See also the [examples](examples). @@ -276,18 +273,18 @@ and keeps track of pending commands. $redis = new Clue\React\Redis\RedisClient('localhost:6379'); $redis->incr('hello'); -$redis->end(); ``` Besides defining a few methods, this interface also implements the `EventEmitterInterface` which allows you to react to certain events as documented below. -Internally, this class creates the underlying database connection only on +Internally, this class creates the underlying connection to Redis 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 close the underlying connection when -it is no longer needed. +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 commands to the database right away while the underlying connection may still be @@ -383,17 +380,17 @@ in seconds (or use a negative number to not apply a timeout) like this: $redis = new Clue\React\Redis\RedisClient('localhost?timeout=0.5'); ``` -By default, this method will keep "idle" connections 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 +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 -$redis = new Clue\React\Redis\RedisClient('localhost?idle=0.1'); +$redis = new Clue\React\Redis\RedisClient('localhost?idle=10.0'); ``` If you need custom DNS, proxy or TLS settings, you can explicitly pass a diff --git a/examples/incr.php b/examples/incr.php index 6744da5..6c0a218 100644 --- a/examples/incr.php +++ b/examples/incr.php @@ -15,5 +15,3 @@ echo 'Error: ' . $e->getMessage() . PHP_EOL; exit(1); }); - -$redis->end(); diff --git a/examples/publish.php b/examples/publish.php index 06bcd9b..5082943 100644 --- a/examples/publish.php +++ b/examples/publish.php @@ -16,5 +16,3 @@ echo 'Unable to publish: ' . $e->getMessage() . PHP_EOL; exit(1); }); - -$redis->end(); diff --git a/src/RedisClient.php b/src/RedisClient.php index 06b1e6c..0761454 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -44,7 +44,7 @@ class RedisClient extends EventEmitter private $loop; /** @var float */ - private $idlePeriod = 60.0; + private $idlePeriod = 0.001; /** @var ?TimerInterface */ private $idleTimer = null; diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index c81cd85..4dd0936 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -53,9 +53,9 @@ public function testPingLazy() /** * @doesNotPerformAssertions */ - public function testPingLazyWillNotBlockLoopWhenIdleTimeIsSmall() + public function testPingLazyWillNotBlockLoop() { - $redis = new RedisClient($this->uri . '?idle=0', null, $this->loop); + $redis = new RedisClient($this->uri, null, $this->loop); $redis->ping(); diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index c1f8d65..647f017 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -63,7 +63,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->loop->expects($this->once())->method('addTimer')->with(60.0, $this->anything()); + $this->loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); $promise = $this->redis->ping(); $deferred->resolve($client); From 9ba4e584b5bbe5d83e05d888e4ee8b5528325f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 4 Sep 2022 11:00:27 +0200 Subject: [PATCH 36/65] Forward compatibility with upcoming Promise v3 --- composer.json | 8 ++++---- tests/RedisClientTest.php | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index 9dff4b7..7c0c178 100644 --- a/composer.json +++ b/composer.json @@ -15,12 +15,12 @@ "clue/redis-protocol": "0.3.*", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/event-loop": "^1.2", - "react/promise": "^2.0 || ^1.1", - "react/promise-timer": "^1.8", - "react/socket": "^1.9" + "react/promise": "^3 || ^2.0 || ^1.1", + "react/promise-timer": "^1.9", + "react/socket": "^1.12" }, "require-dev": { - "clue/block-react": "^1.1", + "clue/block-react": "^1.5", "phpunit/phpunit": "^9.3 || ^7.5" }, "autoload": { diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 647f017..54b8e97 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -223,7 +223,7 @@ public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() $this->redis->ping(); $this->redis->ping(); - $deferred->resolve(); + $deferred->resolve(null); } public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStartsAfterFirstResolves() @@ -242,14 +242,14 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts $this->loop->expects($this->once())->method('cancelTimer')->with($timer); $this->redis->ping(); - $deferred->resolve(); + $deferred->resolve(null); $this->redis->ping(); } public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent() { $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); + $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); $client->expects($this->once())->method('close'); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); @@ -312,7 +312,7 @@ public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientC public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlreadyResolved() { $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); + $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); $client->expects($this->once())->method('close'); $deferred = new Deferred(); @@ -337,7 +337,7 @@ public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() $this->loop->expects($this->once())->method('cancelTimer')->with($timer); $this->redis->ping(); - $deferred->resolve(); + $deferred->resolve(null); $this->redis->close(); } @@ -414,7 +414,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() $error = new \RuntimeException(); $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); + $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); @@ -429,7 +429,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() { $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); + $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); @@ -463,7 +463,7 @@ public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnect $this->redis->on('close', $this->expectCallableNever()); $this->redis->ping(); - $deferred->resolve(); + $deferred->resolve(null); $this->assertTrue(is_callable($closeHandler)); $closeHandler(); @@ -473,7 +473,7 @@ public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubCh { $messageHandler = null; $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve()); + $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$messageHandler) { if ($event === 'message') { $messageHandler = $callback; @@ -495,7 +495,7 @@ public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClo { $allHandler = null; $client = $this->createMock(StreamingClient::class); - $client->expects($this->exactly(6))->method('__call')->willReturn(\React\Promise\resolve()); + $client->expects($this->exactly(6))->method('__call')->willReturn(\React\Promise\resolve(null)); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$allHandler) { if (!isset($allHandler[$event])) { $allHandler[$event] = $callback; From cff1bca5d509a700f13ae14c4926b645b0ea8fac Mon Sep 17 00:00:00 2001 From: Fabian Meyer Date: Sat, 1 Oct 2022 02:34:18 +0200 Subject: [PATCH 37/65] Use newer PHP version in CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff52770..18fe8bf 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 ea801e610bd388fa51487cc2390f5471e2c60e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 28 Oct 2022 03:56:18 +0200 Subject: [PATCH 38/65] Update test suite and report failed assertions --- .github/workflows/ci.yml | 5 +++-- composer.json | 2 +- phpunit.xml.dist | 14 +++++++++++--- phpunit.xml.legacy | 8 ++++++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18fe8bf..2ce8eef 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: @@ -19,11 +19,12 @@ jobs: - 7.2 - 7.1 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 --net=host -d redis - run: REDIS_URI=localhost:6379 vendor/bin/phpunit --coverage-text diff --git a/composer.json b/composer.json index 7c0c178..151a433 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ }, "require-dev": { "clue/block-react": "^1.5", - "phpunit/phpunit": "^9.3 || ^7.5" + "phpunit/phpunit": "^9.5 || ^7.5" }, "autoload": { "psr-4": { "Clue\\React\\Redis\\": "src/" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5093fa5..bd23341 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,8 @@ - - +./src/ + + + + + + + + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index b325858..5ce58af 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -15,4 +15,12 @@ ./src/ + + + + + + + + From f3e020149a22edfe1e7de3d1d46c5afed7c2df65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 28 Oct 2022 04:08:57 +0200 Subject: [PATCH 39/65] Update CI setup to ensure 100% code coverage --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18fe8bf..d03b493 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,13 @@ jobs: coverage: xdebug - run: composer install - run: docker run --net=host -d redis - - run: REDIS_URI=localhost:6379 vendor/bin/phpunit --coverage-text + - run: REDIS_URI=localhost:6379 vendor/bin/phpunit --coverage-text --coverage-clover=clover.xml if: ${{ matrix.php >= 7.3 }} - - run: REDIS_URI=localhost:6379 vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy + - run: REDIS_URI=localhost:6379 vendor/bin/phpunit --coverage-text --coverage-clover=clover.xml -c phpunit.xml.legacy if: ${{ matrix.php < 7.3 }} + - name: Check 100% code coverage + shell: php {0} + run: | + project->metrics; + exit((int) $metrics['statements'] === (int) $metrics['coveredstatements'] ? 0 : 1); From 1fc247d729a58ab945e2862351ae64385941a014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 28 Oct 2022 04:32:11 +0200 Subject: [PATCH 40/65] Add PHPStan to test environment --- .gitattributes | 1 + .github/workflows/ci.yml | 22 ++++++++++++++++++++++ composer.json | 1 + phpstan.neon.dist | 15 +++++++++++++++ src/Io/Factory.php | 1 + src/RedisClient.php | 3 ++- tests/FunctionalTest.php | 1 + tests/Io/FactoryStreamingClientTest.php | 6 ++++++ tests/RedisClientTest.php | 10 ++++++---- tests/TestCase.php | 2 +- 10 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/.gitattributes b/.gitattributes index da20d18..0373309 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ /.github/workflows/ export-ignore /.gitignore export-ignore /examples/ export-ignore +/phpstan.neon.dist export-ignore /phpunit.xml.dist export-ignore /phpunit.xml.legacy export-ignore /tests/ export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 368a0aa..21293f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,3 +37,25 @@ jobs: project->metrics; exit((int) $metrics['statements'] === (int) $metrics['coveredstatements'] ? 0 : 1); + + PHPStan: + name: PHPStan (PHP ${{ matrix.php }}) + runs-on: ubuntu-22.04 + strategy: + matrix: + php: + - 8.2 + - 8.1 + - 8.0 + - 7.4 + - 7.3 + - 7.2 + - 7.1 + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + - run: composer install + - run: vendor/bin/phpstan diff --git a/composer.json b/composer.json index 151a433..3f933de 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ }, "require-dev": { "clue/block-react": "^1.5", + "phpstan/phpstan": "1.8.11 || 1.4.10", "phpunit/phpunit": "^9.5 || ^7.5" }, "autoload": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..753ec6d --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,15 @@ +parameters: + level: 3 + + paths: + - examples/ + - src/ + - tests/ + + reportUnmatchedIgnoredErrors: false + ignoreErrors: + # ignore generic usage like `PromiseInterface` until fixed upstream + - '/^PHPDoc tag @return contains generic type React\\Promise\\PromiseInterface<.+> but interface React\\Promise\\PromiseInterface is not generic\.$/' + # ignore undefined methods due to magic `__call()` method + - '/^Call to an undefined method Clue\\React\\Redis\\RedisClient::.+\(\)\.$/' + - '/^Call to an undefined method Clue\\React\\Redis\\Io\\StreamingClient::.+\(\)\.$/' diff --git a/src/Io/Factory.php b/src/Io/Factory.php index 0f88825..7daec61 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -91,6 +91,7 @@ public function createClient(string $uri): PromiseInterface $connecting->then(function (ConnectionInterface $connection) { $connection->close(); }); + assert(\method_exists($connecting, 'cancel')); $connecting->cancel(); }); diff --git a/src/RedisClient.php b/src/RedisClient.php index 0761454..769f050 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -46,7 +46,7 @@ class RedisClient extends EventEmitter /** @var float */ private $idlePeriod = 0.001; - /** @var ?TimerInterface */ + /** @var ?\React\EventLoop\TimerInterface */ private $idleTimer = null; /** @var int */ @@ -221,6 +221,7 @@ public function close(): void $redis->close(); }); if ($this->promise !== null) { + assert(\method_exists($this->promise, 'cancel')); $this->promise->cancel(); $this->promise = null; } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 4dd0936..d40c1d5 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -106,6 +106,7 @@ public function testInvalidCommand() if (method_exists($this, 'expectException')) { $this->expectException('Exception'); } else { + assert(method_exists($this, 'setExpectedException')); $this->setExpectedException('Exception'); } await($promise, $this->loop); diff --git a/tests/Io/FactoryStreamingClientTest.php b/tests/Io/FactoryStreamingClientTest.php index 9941b07..d72c29c 100644 --- a/tests/Io/FactoryStreamingClientTest.php +++ b/tests/Io/FactoryStreamingClientTest.php @@ -444,6 +444,8 @@ public function testCancelWillRejectPromise() $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn($promise); $promise = $this->factory->createClient('redis://127.0.0.1:2'); + + assert(method_exists($promise, 'cancel')); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf(\RuntimeException::class))); @@ -526,6 +528,8 @@ public function testCancelWillRejectWithUriInMessageAndCancelConnectorWhenConnec $this->connector->expects($this->once())->method('connect')->willReturn($deferred->promise()); $promise = $this->factory->createClient($uri); + + assert(method_exists($promise, 'cancel')); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith( @@ -550,6 +554,8 @@ public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect() $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $promise = $this->factory->createClient('redis://127.0.0.1:2/123'); + + assert(method_exists($promise, 'cancel')); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith( diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 54b8e97..9733a3f 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -347,6 +347,7 @@ public function testCloseAfterPingRejectsWillEmitClose() $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); $client->expects($this->once())->method('close')->willReturnCallback(function () use ($client) { + assert($client instanceof StreamingClient); $client->emit('close'); }); @@ -356,11 +357,10 @@ public function testCloseAfterPingRejectsWillEmitClose() $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); $this->loop->expects($this->once())->method('cancelTimer')->with($timer); - $ref = $this->redis; - $ref->ping()->then(null, function () use ($ref, $client) { - $ref->close(); + $this->redis->ping()->then(null, function () { + $this->redis->close(); }); - $ref->on('close', $this->expectCallableOnce()); + $this->redis->on('close', $this->expectCallableOnce()); $deferred->reject(new \RuntimeException()); } @@ -423,6 +423,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() $deferred->resolve($client); $this->redis->on('error', $this->expectCallableNever()); + assert($client instanceof StreamingClient); $client->emit('error', [$error]); } @@ -438,6 +439,7 @@ public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() $deferred->resolve($client); $this->redis->on('close', $this->expectCallableNever()); + assert($client instanceof StreamingClient); $client->emit('close'); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 4a73762..0da36f4 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -36,7 +36,7 @@ protected function expectCallableNever(): callable protected function createCallableMock(): MockObject { if (method_exists(MockBuilder::class, 'addMethods')) { - // PHPUnit 9+ + // @phpstan-ignore-next-line requires PHPUnit 9+ return $this->getMockBuilder(\stdClass::class)->addMethods(['__invoke'])->getMock(); } else { // legacy PHPUnit < 9 From 6e335214dc8265baa9e7bf5ce2b783f2e59af88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 28 Oct 2022 05:14:30 +0200 Subject: [PATCH 41/65] Improve type definitions and update to PHPStan level `max` --- examples/cli.php | 4 +- phpstan.neon.dist | 4 +- src/Io/Factory.php | 5 +- src/Io/StreamingClient.php | 19 +------ src/RedisClient.php | 2 + tests/FunctionalTest.php | 24 ++++---- tests/Io/FactoryStreamingClientTest.php | 74 +++++++++++++------------ tests/Io/StreamingClientTest.php | 74 +++++++------------------ tests/RedisClientTest.php | 72 ++++++++++++------------ tests/TestCase.php | 4 ++ 10 files changed, 126 insertions(+), 156 deletions(-) diff --git a/examples/cli.php b/examples/cli.php index 0eb8b65..23c9e55 100644 --- a/examples/cli.php +++ b/examples/cli.php @@ -25,7 +25,9 @@ $params = explode(' ', $line); $method = array_shift($params); - $promise = call_user_func_array([$redis, $method], $params); + + assert(is_callable([$redis, $method])); + $promise = $redis->$method(...$params); // special method such as end() / close() called if (!$promise instanceof React\Promise\PromiseInterface) { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 753ec6d..aa2f1e3 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: - level: 3 + level: max paths: - examples/ @@ -13,3 +13,5 @@ parameters: # ignore undefined methods due to magic `__call()` method - '/^Call to an undefined method Clue\\React\\Redis\\RedisClient::.+\(\)\.$/' - '/^Call to an undefined method Clue\\React\\Redis\\Io\\StreamingClient::.+\(\)\.$/' + # ignore incomplete type information for mocks in legacy PHPUnit 7.5 + - '/^Parameter #\d+ .+ of .+ expects .+, PHPUnit\\Framework\\MockObject\\MockObject given\.$/' diff --git a/src/Io/Factory.php b/src/Io/Factory.php index 7daec61..a760832 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -75,6 +75,7 @@ public function createClient(string $uri): PromiseInterface if ($parts['scheme'] === 'rediss') { $authority = 'tls://' . $authority; } elseif ($parts['scheme'] === 'redis+unix') { + assert(isset($parts['path'])); $authority = 'unix://' . substr($parts['path'], 1); unset($parts['path']); } @@ -107,7 +108,7 @@ public function createClient(string $uri): PromiseInterface // use `?password=secret` query or `user:secret@host` password form URL if (isset($args['password']) || isset($parts['pass'])) { - $pass = $args['password'] ?? rawurldecode($parts['pass']); + $pass = $args['password'] ?? rawurldecode($parts['pass']); // @phpstan-ignore-line $promise = $promise->then(function (StreamingClient $redis) use ($pass, $uri) { return $redis->auth($pass)->then( function () use ($redis) { @@ -135,7 +136,7 @@ function (\Exception $e) use ($redis, $uri) { // use `?db=1` query or `/1` path (skip first slash) if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) { - $db = $args['db'] ?? substr($parts['path'], 1); + $db = $args['db'] ?? substr($parts['path'], 1); // @phpstan-ignore-line $promise = $promise->then(function (StreamingClient $redis) use ($db, $uri) { return $redis->select($db)->then( function () use ($redis) { diff --git a/src/Io/StreamingClient.php b/src/Io/StreamingClient.php index 126bff5..1dc2fde 100644 --- a/src/Io/StreamingClient.php +++ b/src/Io/StreamingClient.php @@ -2,7 +2,6 @@ namespace Clue\React\Redis\Io; -use Clue\Redis\Protocol\Factory as ProtocolFactory; use Clue\Redis\Protocol\Model\ErrorReply; use Clue\Redis\Protocol\Model\ModelInterface; use Clue\Redis\Protocol\Model\MultiBulkReply; @@ -22,9 +21,6 @@ class StreamingClient extends EventEmitter /** @var DuplexStreamInterface */ private $stream; - /** @var ParserInterface */ - private $parser; - /** @var SerializerInterface */ private $serializer; @@ -43,18 +39,8 @@ class StreamingClient extends EventEmitter /** @var int */ private $psubscribed = 0; - public function __construct(DuplexStreamInterface $stream, ParserInterface $parser = null, SerializerInterface $serializer = null) + public function __construct(DuplexStreamInterface $stream, ParserInterface $parser, SerializerInterface $serializer) { - if ($parser === null || $serializer === null) { - $factory = new ProtocolFactory(); - if ($parser === null) { - $parser = $factory->createResponseParser(); - } - if ($serializer === null) { - $serializer = $factory->createSerializer(); - } - } - $stream->on('data', function (string $chunk) use ($parser) { try { $models = $parser->pushIncoming($chunk); @@ -82,10 +68,10 @@ public function __construct(DuplexStreamInterface $stream, ParserInterface $pars $stream->on('close', [$this, 'close']); $this->stream = $stream; - $this->parser = $parser; $this->serializer = $serializer; } + /** @param string[] $args */ public function __call(string $name, array $args): PromiseInterface { $request = new Deferred(); @@ -139,6 +125,7 @@ public function handleMessage(ModelInterface $message): void { if (($this->subscribed !== 0 || $this->psubscribed !== 0) && $message instanceof MultiBulkReply) { $array = $message->getValueNative(); + assert(\is_array($array)); $first = array_shift($array); // pub/sub messages are to be forwarded and should not be processed as request responses diff --git a/src/RedisClient.php b/src/RedisClient.php index 769f050..8230294 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -161,6 +161,7 @@ public function __call(string $name, array $args): PromiseInterface return $this->client()->then(function (StreamingClient $redis) use ($name, $args) { $this->awake(); + assert(\is_callable([$redis, $name])); // @phpstan-ignore-next-line return \call_user_func_array([$redis, $name], $args)->then( function ($result) { $this->idle(); @@ -252,6 +253,7 @@ private function idle(): void if ($this->pending < 1 && $this->idlePeriod >= 0 && !$this->subscribed && !$this->psubscribed && $this->promise !== null) { $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () { + assert($this->promise instanceof PromiseInterface); $this->promise->then(function (StreamingClient $redis) { $redis->close(); }); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index d40c1d5..673ab73 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -26,7 +26,7 @@ public function setUp(): void $this->loop = new StreamSelectLoop(); } - public function testPing() + public function testPing(): void { $redis = new RedisClient($this->uri, null, $this->loop); @@ -38,7 +38,7 @@ public function testPing() $this->assertEquals('PONG', $ret); } - public function testPingLazy() + public function testPingLazy(): void { $redis = new RedisClient($this->uri, null, $this->loop); @@ -53,7 +53,7 @@ public function testPingLazy() /** * @doesNotPerformAssertions */ - public function testPingLazyWillNotBlockLoop() + public function testPingLazyWillNotBlockLoop(): void { $redis = new RedisClient($this->uri, null, $this->loop); @@ -65,7 +65,7 @@ public function testPingLazyWillNotBlockLoop() /** * @doesNotPerformAssertions */ - public function testLazyClientWithoutCommandsWillNotBlockLoop() + public function testLazyClientWithoutCommandsWillNotBlockLoop(): void { $redis = new RedisClient($this->uri, null, $this->loop); @@ -74,7 +74,7 @@ public function testLazyClientWithoutCommandsWillNotBlockLoop() unset($redis); } - public function testMgetIsNotInterpretedAsSubMessage() + public function testMgetIsNotInterpretedAsSubMessage(): void { $redis = new RedisClient($this->uri, null, $this->loop); @@ -86,7 +86,7 @@ public function testMgetIsNotInterpretedAsSubMessage() await($promise, $this->loop); } - public function testPipeline() + public function testPipeline(): void { $redis = new RedisClient($this->uri, null, $this->loop); @@ -98,7 +98,7 @@ public function testPipeline() await($promise, $this->loop); } - public function testInvalidCommand() + public function testInvalidCommand(): void { $redis = new RedisClient($this->uri, null, $this->loop); $promise = $redis->doesnotexist(1, 2, 3); @@ -112,7 +112,7 @@ public function testInvalidCommand() await($promise, $this->loop); } - public function testMultiExecEmpty() + public function testMultiExecEmpty(): void { $redis = new RedisClient($this->uri, null, $this->loop); $redis->multi()->then($this->expectCallableOnceWith('OK')); @@ -121,7 +121,7 @@ public function testMultiExecEmpty() await($promise, $this->loop); } - public function testMultiExecQueuedExecHasValues() + public function testMultiExecQueuedExecHasValues(): void { $redis = new RedisClient($this->uri, null, $this->loop); @@ -135,7 +135,7 @@ public function testMultiExecQueuedExecHasValues() await($promise, $this->loop); } - public function testPubSub() + public function testPubSub(): void { $consumer = new RedisClient($this->uri, null, $this->loop); $producer = new RedisClient($this->uri, null, $this->loop); @@ -156,7 +156,7 @@ public function testPubSub() await($deferred->promise(), $this->loop, 0.1); } - public function testClose() + public function testClose(): void { $redis = new RedisClient($this->uri, null, $this->loop); @@ -167,7 +167,7 @@ public function testClose() $redis->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce()); } - public function testCloseLazy() + public function testCloseLazy(): void { $redis = new RedisClient($this->uri, null, $this->loop); diff --git a/tests/Io/FactoryStreamingClientTest.php b/tests/Io/FactoryStreamingClientTest.php index d72c29c..3cb09c6 100644 --- a/tests/Io/FactoryStreamingClientTest.php +++ b/tests/Io/FactoryStreamingClientTest.php @@ -31,7 +31,7 @@ public function setUp(): void $this->factory = new Factory($this->loop, $this->connector); } - public function testConstructWithoutLoopAssignsLoopAutomatically() + public function testConstructWithoutLoopAssignsLoopAutomatically(): void { $factory = new Factory(); @@ -45,24 +45,24 @@ public function testConstructWithoutLoopAssignsLoopAutomatically() /** * @doesNotPerformAssertions */ - public function testCtor() + public function testCtor(): void { $this->factory = new Factory($this->loop); } - public function testWillConnectWithDefaultPort() + public function testWillConnectWithDefaultPort(): void { $this->connector->expects($this->once())->method('connect')->with('redis.example.com:6379')->willReturn(reject(new \RuntimeException())); $this->factory->createClient('redis.example.com'); } - public function testWillConnectToLocalhost() + public function testWillConnectToLocalhost(): void { $this->connector->expects($this->once())->method('connect')->with('localhost:1337')->willReturn(reject(new \RuntimeException())); $this->factory->createClient('localhost:1337'); } - public function testWillResolveIfConnectorResolves() + public function testWillResolveIfConnectorResolves(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write'); @@ -73,7 +73,7 @@ public function testWillResolveIfConnectorResolves() $this->expectPromiseResolve($promise); } - public function testWillWriteSelectCommandIfTargetContainsPath() + public function testWillWriteSelectCommandIfTargetContainsPath(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n"); @@ -82,7 +82,7 @@ public function testWillWriteSelectCommandIfTargetContainsPath() $this->factory->createClient('redis://127.0.0.1/demo'); } - public function testWillWriteSelectCommandIfTargetContainsDbQueryParameter() + public function testWillWriteSelectCommandIfTargetContainsDbQueryParameter(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$1\r\n4\r\n"); @@ -91,7 +91,7 @@ public function testWillWriteSelectCommandIfTargetContainsDbQueryParameter() $this->factory->createClient('redis://127.0.0.1?db=4'); } - public function testWillWriteAuthCommandIfRedisUriContainsUserInfo() + public function testWillWriteAuthCommandIfRedisUriContainsUserInfo(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); @@ -100,7 +100,7 @@ public function testWillWriteAuthCommandIfRedisUriContainsUserInfo() $this->factory->createClient('redis://hello:world@example.com'); } - public function testWillWriteAuthCommandIfRedisUriContainsEncodedUserInfo() + public function testWillWriteAuthCommandIfRedisUriContainsEncodedUserInfo(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n"); @@ -109,7 +109,7 @@ public function testWillWriteAuthCommandIfRedisUriContainsEncodedUserInfo() $this->factory->createClient('redis://:h%40llo@example.com'); } - public function testWillWriteAuthCommandIfTargetContainsPasswordQueryParameter() + public function testWillWriteAuthCommandIfTargetContainsPasswordQueryParameter(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$6\r\nsecret\r\n"); @@ -118,7 +118,7 @@ public function testWillWriteAuthCommandIfTargetContainsPasswordQueryParameter() $this->factory->createClient('redis://example.com?password=secret'); } - public function testWillWriteAuthCommandIfTargetContainsEncodedPasswordQueryParameter() + public function testWillWriteAuthCommandIfTargetContainsEncodedPasswordQueryParameter(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n"); @@ -127,7 +127,7 @@ public function testWillWriteAuthCommandIfTargetContainsEncodedPasswordQueryPara $this->factory->createClient('redis://example.com?password=h%40llo'); } - public function testWillWriteAuthCommandIfRedissUriContainsUserInfo() + public function testWillWriteAuthCommandIfRedissUriContainsUserInfo(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); @@ -136,7 +136,7 @@ public function testWillWriteAuthCommandIfRedissUriContainsUserInfo() $this->factory->createClient('rediss://hello:world@example.com'); } - public function testWillWriteAuthCommandIfRedisUnixUriContainsPasswordQueryParameter() + public function testWillWriteAuthCommandIfRedisUnixUriContainsPasswordQueryParameter(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); @@ -145,7 +145,7 @@ public function testWillWriteAuthCommandIfRedisUnixUriContainsPasswordQueryParam $this->factory->createClient('redis+unix:///tmp/redis.sock?password=world'); } - public function testWillNotWriteAnyCommandIfRedisUnixUriContainsNoPasswordOrDb() + public function testWillNotWriteAnyCommandIfRedisUnixUriContainsNoPasswordOrDb(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->never())->method('write'); @@ -154,7 +154,7 @@ public function testWillNotWriteAnyCommandIfRedisUnixUriContainsNoPasswordOrDb() $this->factory->createClient('redis+unix:///tmp/redis.sock'); } - public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo() + public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); @@ -163,7 +163,7 @@ public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo() $this->factory->createClient('redis+unix://hello:world@/tmp/redis.sock'); } - public function testWillResolveWhenAuthCommandReceivesOkResponseIfRedisUriContainsUserInfo() + public function testWillResolveWhenAuthCommandReceivesOkResponseIfRedisUriContainsUserInfo(): void { $dataHandler = null; $stream = $this->createMock(ConnectionInterface::class); @@ -185,7 +185,7 @@ public function testWillResolveWhenAuthCommandReceivesOkResponseIfRedisUriContai $promise->then($this->expectCallableOnceWith($this->isInstanceOf(StreamingClient::class))); } - public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorResponseIfRedisUriContainsUserInfo() + public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorResponseIfRedisUriContainsUserInfo(): void { $dataHandler = null; $stream = $this->createMock(ConnectionInterface::class); @@ -215,13 +215,14 @@ public function testWillRejectAndCloseAutomaticallyWhenAuthCommandReceivesErrorR return $e->getCode() === (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13); }), $this->callback(function (\RuntimeException $e) { + assert($e->getPrevious() !== null); return $e->getPrevious()->getMessage() === 'ERR invalid password'; }) ) )); } - public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWaitingForAuthCommand() + public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWaitingForAuthCommand(): void { $closeHandler = null; $stream = $this->createMock(ConnectionInterface::class); @@ -253,13 +254,14 @@ public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWa return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104); }), $this->callback(function (\Exception $e) { + assert($e->getPrevious() !== null); return $e->getPrevious()->getMessage() === 'Connection closed by peer (ECONNRESET)'; }) ) )); } - public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter() + public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n"); @@ -268,7 +270,7 @@ public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter $this->factory->createClient('redis+unix:///tmp/redis.sock?db=demo'); } - public function testWillResolveWhenSelectCommandReceivesOkResponseIfRedisUriContainsPath() + public function testWillResolveWhenSelectCommandReceivesOkResponseIfRedisUriContainsPath(): void { $dataHandler = null; $stream = $this->createMock(ConnectionInterface::class); @@ -290,7 +292,7 @@ public function testWillResolveWhenSelectCommandReceivesOkResponseIfRedisUriCont $promise->then($this->expectCallableOnceWith($this->isInstanceOf(StreamingClient::class))); } - public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErrorResponseIfRedisUriContainsPath() + public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErrorResponseIfRedisUriContainsPath(): void { $dataHandler = null; $stream = $this->createMock(ConnectionInterface::class); @@ -320,13 +322,14 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesErro return $e->getCode() === (defined('SOCKET_ENOENT') ? SOCKET_ENOENT : 2); }), $this->callback(function (\RuntimeException $e) { + assert($e->getPrevious() !== null); return $e->getPrevious()->getMessage() === 'ERR DB index is out of range'; }) ) )); } - public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesAuthErrorResponseIfRedisUriContainsPath() + public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesAuthErrorResponseIfRedisUriContainsPath(): void { $dataHandler = null; $stream = $this->createMock(ConnectionInterface::class); @@ -356,13 +359,14 @@ public function testWillRejectAndCloseAutomaticallyWhenSelectCommandReceivesAuth return $e->getCode() === (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13); }), $this->callback(function (\Exception $e) { + assert($e->getPrevious() !== null); return $e->getPrevious()->getMessage() === 'NOAUTH Authentication required.'; }) ) )); } - public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWaitingForSelectCommand() + public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWaitingForSelectCommand(): void { $closeHandler = null; $stream = $this->createMock(ConnectionInterface::class); @@ -394,13 +398,14 @@ public function testWillRejectAndCloseAutomaticallyWhenConnectionIsClosedWhileWa return $e->getCode() === (defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104); }), $this->callback(function (\Exception $e) { + assert($e->getPrevious() !== null); return $e->getPrevious()->getMessage() === 'Connection closed by peer (ECONNRESET)'; }) ) )); } - public function testWillRejectIfConnectorRejects() + public function testWillRejectIfConnectorRejects(): void { $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn(reject(new \RuntimeException('Foo', 42))); $promise = $this->factory->createClient('redis://127.0.0.1:2'); @@ -415,13 +420,14 @@ public function testWillRejectIfConnectorRejects() return $e->getCode() === 42; }), $this->callback(function (\RuntimeException $e) { + assert($e->getPrevious() !== null); return $e->getPrevious()->getMessage() === 'Foo'; }) ) )); } - public function testWillRejectIfTargetIsInvalid() + public function testWillRejectIfTargetIsInvalid(): void { $promise = $this->factory->createClient('http://invalid target'); @@ -438,7 +444,7 @@ public function testWillRejectIfTargetIsInvalid() )); } - public function testCancelWillRejectPromise() + public function testCancelWillRejectPromise(): void { $promise = new \React\Promise\Promise(function () { }); $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn($promise); @@ -451,7 +457,8 @@ public function testCancelWillRejectPromise() $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf(\RuntimeException::class))); } - public function provideUris() + /** @return list> */ + public function provideUris(): array { return [ [ @@ -519,10 +526,8 @@ public function provideUris() /** * @dataProvider provideUris - * @param string $uri - * @param string $safe */ - public function testCancelWillRejectWithUriInMessageAndCancelConnectorWhenConnectionIsPending($uri, $safe) + public function testCancelWillRejectWithUriInMessageAndCancelConnectorWhenConnectionIsPending(string $uri, string $safe): void { $deferred = new Deferred($this->expectCallableOnce()); $this->connector->expects($this->once())->method('connect')->willReturn($deferred->promise()); @@ -545,7 +550,7 @@ public function testCancelWillRejectWithUriInMessageAndCancelConnectorWhenConnec )); } - public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect() + public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect(): void { $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write'); @@ -571,7 +576,7 @@ public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect() )); } - public function testCreateClientWithTimeoutParameterWillStartTimerAndRejectOnExplicitTimeout() + public function testCreateClientWithTimeoutParameterWillStartTimerAndRejectOnExplicitTimeout(): void { $timeout = null; $this->loop->expects($this->once())->method('addTimer')->with(0, $this->callback(function ($cb) use (&$timeout) { @@ -600,7 +605,7 @@ public function testCreateClientWithTimeoutParameterWillStartTimerAndRejectOnExp )); } - public function testCreateClientWithNegativeTimeoutParameterWillNotStartTimer() + public function testCreateClientWithNegativeTimeoutParameterWillNotStartTimer(): void { $this->loop->expects($this->never())->method('addTimer'); @@ -610,7 +615,7 @@ public function testCreateClientWithNegativeTimeoutParameterWillNotStartTimer() $this->factory->createClient('redis://127.0.0.1:2?timeout=-1'); } - public function testCreateClientWithoutTimeoutParameterWillStartTimerWithDefaultTimeoutFromIni() + public function testCreateClientWithoutTimeoutParameterWillStartTimerWithDefaultTimeoutFromIni(): void { $this->loop->expects($this->once())->method('addTimer')->with(42, $this->anything()); @@ -618,6 +623,7 @@ public function testCreateClientWithoutTimeoutParameterWillStartTimerWithDefault $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn($deferred->promise()); $old = ini_get('default_socket_timeout'); + assert(is_string($old)); ini_set('default_socket_timeout', '42'); $this->factory->createClient('redis://127.0.0.1:2'); ini_set('default_socket_timeout', $old); diff --git a/tests/Io/StreamingClientTest.php b/tests/Io/StreamingClientTest.php index b0274f8..dc47ad7 100644 --- a/tests/Io/StreamingClientTest.php +++ b/tests/Io/StreamingClientTest.php @@ -38,29 +38,7 @@ public function setUp(): void $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); } - public function testConstructWithoutParserAssignsParserAutomatically() - { - $this->redis = new StreamingClient($this->stream, null, $this->serializer); - - $ref = new \ReflectionProperty($this->redis, 'parser'); - $ref->setAccessible(true); - $parser = $ref->getValue($this->redis); - - $this->assertInstanceOf(ParserInterface::class, $parser); - } - - public function testConstructWithoutParserAndSerializerAssignsParserAndSerializerAutomatically() - { - $this->redis = new StreamingClient($this->stream, $this->parser); - - $ref = new \ReflectionProperty($this->redis, 'serializer'); - $ref->setAccessible(true); - $serializer = $ref->getValue($this->redis); - - $this->assertInstanceOf(SerializerInterface::class, $serializer); - } - - public function testSending() + public function testSending(): void { $this->serializer->expects($this->once())->method('getRequestMessage')->with($this->equalTo('ping'))->will($this->returnValue('message')); $this->stream->expects($this->once())->method('write')->with($this->equalTo('message')); @@ -68,14 +46,14 @@ public function testSending() $this->redis->ping(); } - public function testClosingClientEmitsEvent() + public function testClosingClientEmitsEvent(): void { $this->redis->on('close', $this->expectCallableOnce()); $this->redis->close(); } - public function testClosingStreamClosesClient() + public function testClosingStreamClosesClient(): void { $stream = new ThroughStream(); $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); @@ -85,7 +63,7 @@ public function testClosingStreamClosesClient() $stream->emit('close'); } - public function testReceiveParseErrorEmitsErrorEvent() + public function testReceiveParseErrorEmitsErrorEvent(): void { $stream = new ThroughStream(); $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); @@ -107,7 +85,7 @@ public function testReceiveParseErrorEmitsErrorEvent() $stream->emit('data', ['message']); } - public function testReceiveUnexpectedReplyEmitsErrorEvent() + public function testReceiveUnexpectedReplyEmitsErrorEvent(): void { $stream = new ThroughStream(); $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); @@ -130,15 +108,7 @@ public function testReceiveUnexpectedReplyEmitsErrorEvent() $stream->emit('data', ['message']); } - /** - * @doesNotPerformAssertions - */ - public function testDefaultCtor() - { - new StreamingClient($this->stream); - } - - public function testPingPong() + public function testPingPong(): void { $this->serializer->expects($this->once())->method('getRequestMessage')->with($this->equalTo('ping')); @@ -150,7 +120,7 @@ public function testPingPong() $promise->then($this->expectCallableOnceWith('PONG')); } - public function testMonitorCommandIsNotSupported() + public function testMonitorCommandIsNotSupported(): void { $promise = $this->redis->monitor(); @@ -167,7 +137,7 @@ public function testMonitorCommandIsNotSupported() )); } - public function testErrorReply() + public function testErrorReply(): void { $promise = $this->redis->invalid(); @@ -177,7 +147,7 @@ public function testErrorReply() $promise->then(null, $this->expectCallableOnceWith($err)); } - public function testClosingClientRejectsAllRemainingRequests() + public function testClosingClientRejectsAllRemainingRequests(): void { $promise = $this->redis->ping(); $this->redis->close(); @@ -195,7 +165,7 @@ public function testClosingClientRejectsAllRemainingRequests() )); } - public function testClosingStreamRejectsAllRemainingRequests() + public function testClosingStreamRejectsAllRemainingRequests(): void { $stream = new ThroughStream(function () { return ''; }); $this->parser->expects($this->once())->method('pushIncoming')->willReturn([]); @@ -217,7 +187,7 @@ public function testClosingStreamRejectsAllRemainingRequests() )); } - public function testEndingClientRejectsAllNewRequests() + public function testEndingClientRejectsAllNewRequests(): void { $this->redis->ping(); $this->redis->end(); @@ -236,7 +206,7 @@ public function testEndingClientRejectsAllNewRequests() )); } - public function testClosedClientRejectsAllNewRequests() + public function testClosedClientRejectsAllNewRequests(): void { $this->redis->close(); $promise = $this->redis->ping(); @@ -254,13 +224,13 @@ public function testClosedClientRejectsAllNewRequests() )); } - public function testEndingNonBusyClosesClient() + public function testEndingNonBusyClosesClient(): void { $this->redis->on('close', $this->expectCallableOnce()); $this->redis->end(); } - public function testEndingBusyClosesClientWhenNotBusyAnymore() + public function testEndingBusyClosesClientWhenNotBusyAnymore(): void { // count how often the "close" method has been called $closed = 0; @@ -279,7 +249,7 @@ public function testEndingBusyClosesClientWhenNotBusyAnymore() $this->assertEquals(1, $closed); } - public function testClosingMultipleTimesEmitsOnce() + public function testClosingMultipleTimesEmitsOnce(): void { $this->redis->on('close', $this->expectCallableOnce()); @@ -287,13 +257,13 @@ public function testClosingMultipleTimesEmitsOnce() $this->redis->close(); } - public function testReceivingUnexpectedMessageThrowsException() + public function testReceivingUnexpectedMessageThrowsException(): void { $this->expectException(\UnderflowException::class); $this->redis->handleMessage(new BulkReply('PONG')); } - public function testPubsubSubscribe() + public function testPubsubSubscribe(): StreamingClient { $promise = $this->redis->subscribe('test'); $this->expectPromiseResolve($promise); @@ -306,9 +276,8 @@ public function testPubsubSubscribe() /** * @depends testPubsubSubscribe - * @param StreamingClient $client */ - public function testPubsubPatternSubscribe(StreamingClient $client) + public function testPubsubPatternSubscribe(StreamingClient $client): StreamingClient { $promise = $client->psubscribe('demo_*'); $this->expectPromiseResolve($promise); @@ -321,15 +290,14 @@ public function testPubsubPatternSubscribe(StreamingClient $client) /** * @depends testPubsubPatternSubscribe - * @param StreamingClient $client */ - public function testPubsubMessage(StreamingClient $client) + public function testPubsubMessage(StreamingClient $client): void { $client->on('message', $this->expectCallableOnce()); $client->handleMessage(new MultiBulkReply([new BulkReply('message'), new BulkReply('test'), new BulkReply('payload')])); } - public function testSubscribeWithMultipleArgumentsRejects() + public function testSubscribeWithMultipleArgumentsRejects(): void { $promise = $this->redis->subscribe('a', 'b'); @@ -346,7 +314,7 @@ public function testSubscribeWithMultipleArgumentsRejects() )); } - public function testUnsubscribeWithoutArgumentsRejects() + public function testUnsubscribeWithoutArgumentsRejects(): void { $promise = $this->redis->unsubscribe(); diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 9733a3f..813523a 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -34,7 +34,7 @@ public function setUp(): void $ref->setValue($this->redis, $this->factory); } - public function testPingWillCreateUnderlyingClientAndReturnPendingPromise() + public function testPingWillCreateUnderlyingClientAndReturnPendingPromise(): void { $promise = new Promise(function () { }); $this->factory->expects($this->once())->method('createClient')->willReturn($promise); @@ -46,7 +46,7 @@ public function testPingWillCreateUnderlyingClientAndReturnPendingPromise() $promise->then($this->expectCallableNever()); } - public function testPingTwiceWillCreateOnceUnderlyingClient() + public function testPingTwiceWillCreateOnceUnderlyingClient(): void { $promise = new Promise(function () { }); $this->factory->expects($this->once())->method('createClient')->willReturn($promise); @@ -55,7 +55,7 @@ public function testPingTwiceWillCreateOnceUnderlyingClient() $this->redis->ping(); } - public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimer() + public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimer(): void { $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); @@ -71,7 +71,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT $promise->then($this->expectCallableOnceWith('PONG')); } - public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimerWithIdleTimeFromQueryParam() + public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimerWithIdleTimeFromQueryParam(): void { $this->redis = new RedisClient('localhost?idle=10', null, $this->loop); @@ -93,7 +93,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT $promise->then($this->expectCallableOnceWith('PONG')); } - public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartIdleTimerWhenIdleParamIsNegative() + public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartIdleTimerWhenIdleParamIsNegative(): void { $this->redis = new RedisClient('localhost?idle=-1', null, $this->loop); @@ -115,7 +115,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartId $promise->then($this->expectCallableOnceWith('PONG')); } - public function testPingWillRejectWhenUnderlyingClientRejectsPingAndStartIdleTimer() + public function testPingWillRejectWhenUnderlyingClientRejectsPingAndStartIdleTimer(): void { $error = new \RuntimeException(); $client = $this->createMock(StreamingClient::class); @@ -132,7 +132,7 @@ public function testPingWillRejectWhenUnderlyingClientRejectsPingAndStartIdleTim $promise->then(null, $this->expectCallableOnceWith($error)); } - public function testPingWillRejectAndNotEmitErrorOrCloseWhenFactoryRejectsUnderlyingClient() + public function testPingWillRejectAndNotEmitErrorOrCloseWhenFactoryRejectsUnderlyingClient(): void { $error = new \RuntimeException(); @@ -148,7 +148,7 @@ public function testPingWillRejectAndNotEmitErrorOrCloseWhenFactoryRejectsUnderl $promise->then(null, $this->expectCallableOnceWith($error)); } - public function testPingAfterPreviousFactoryRejectsUnderlyingClientWillCreateNewUnderlyingConnection() + public function testPingAfterPreviousFactoryRejectsUnderlyingClientWillCreateNewUnderlyingConnection(): void { $error = new \RuntimeException(); @@ -164,7 +164,7 @@ public function testPingAfterPreviousFactoryRejectsUnderlyingClientWillCreateNew $this->redis->ping(); } - public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewUnderlyingConnection() + public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewUnderlyingConnection(): void { $closeHandler = null; $client = $this->createMock(StreamingClient::class); @@ -188,7 +188,7 @@ public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewU $this->redis->ping(); } - public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection() + public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection(): void { $this->factory->expects($this->never())->method('createClient'); @@ -208,7 +208,7 @@ public function testPingAfterCloseWillRejectWithoutCreatingUnderlyingConnection( )); } - public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() + public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves(): void { $deferred = new Deferred(); $client = $this->createMock(StreamingClient::class); @@ -226,7 +226,7 @@ public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves() $deferred->resolve(null); } - public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStartsAfterFirstResolves() + public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStartsAfterFirstResolves(): void { $deferred = new Deferred(); $client = $this->createMock(StreamingClient::class); @@ -246,7 +246,7 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts $this->redis->ping(); } - public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent() + public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent(): void { $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); @@ -269,7 +269,7 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutC $timeout(); } - public function testCloseWillEmitCloseEventWithoutCreatingUnderlyingClient() + public function testCloseWillEmitCloseEventWithoutCreatingUnderlyingClient(): void { $this->factory->expects($this->never())->method('createClient'); @@ -278,7 +278,7 @@ public function testCloseWillEmitCloseEventWithoutCreatingUnderlyingClient() $this->redis->close(); } - public function testCloseTwiceWillEmitCloseEventOnce() + public function testCloseTwiceWillEmitCloseEventOnce(): void { $this->redis->on('close', $this->expectCallableOnce()); @@ -286,7 +286,7 @@ public function testCloseTwiceWillEmitCloseEventOnce() $this->redis->close(); } - public function testCloseAfterPingWillCancelUnderlyingClientConnectionWhenStillPending() + public function testCloseAfterPingWillCancelUnderlyingClientConnectionWhenStillPending(): void { $promise = new Promise(function () { }, $this->expectCallableOnce()); $this->factory->expects($this->once())->method('createClient')->willReturn($promise); @@ -295,7 +295,7 @@ public function testCloseAfterPingWillCancelUnderlyingClientConnectionWhenStillP $this->redis->close(); } - public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientConnectionThrowsDueToCancellation() + public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientConnectionThrowsDueToCancellation(): void { $promise = new Promise(function () { }, function () { throw new \RuntimeException('Discarded'); @@ -309,7 +309,7 @@ public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientC $this->redis->close(); } - public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlreadyResolved() + public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlreadyResolved(): void { $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); @@ -323,7 +323,7 @@ public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlready $this->redis->close(); } - public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() + public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved(): void { $deferred = new Deferred(); $client = $this->createMock(StreamingClient::class); @@ -341,7 +341,7 @@ public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() $this->redis->close(); } - public function testCloseAfterPingRejectsWillEmitClose() + public function testCloseAfterPingRejectsWillEmitClose(): void { $deferred = new Deferred(); $client = $this->createMock(StreamingClient::class); @@ -364,13 +364,13 @@ public function testCloseAfterPingRejectsWillEmitClose() $deferred->reject(new \RuntimeException()); } - public function testEndWillCloseClientIfUnderlyingConnectionIsNotPending() + public function testEndWillCloseClientIfUnderlyingConnectionIsNotPending(): void { $this->redis->on('close', $this->expectCallableOnce()); $this->redis->end(); } - public function testEndAfterPingWillEndUnderlyingClient() + public function testEndAfterPingWillEndUnderlyingClient(): void { $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); @@ -384,7 +384,7 @@ public function testEndAfterPingWillEndUnderlyingClient() $this->redis->end(); } - public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose() + public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose(): void { $closeHandler = null; $client = $this->createMock(StreamingClient::class); @@ -409,7 +409,7 @@ public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose() $closeHandler(); } - public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() + public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError(): void { $error = new \RuntimeException(); @@ -427,7 +427,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError() $client->emit('error', [$error]); } - public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() + public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose(): void { $client = $this->createMock(StreamingClient::class); $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); @@ -443,7 +443,7 @@ public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose() $client->emit('close'); } - public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnectionEmitsCloseAfterPingIsAlreadyResolved() + public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnectionEmitsCloseAfterPingIsAlreadyResolved(): void { $closeHandler = null; $client = $this->createMock(StreamingClient::class); @@ -471,7 +471,7 @@ public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnect $closeHandler(); } - public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubChannel() + public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubChannel(): void { $messageHandler = null; $client = $this->createMock(StreamingClient::class); @@ -493,7 +493,7 @@ public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubCh $messageHandler('foo', 'bar'); } - public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClosesWhileUsingPubSubChannel() + public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClosesWhileUsingPubSubChannel(): void { $allHandler = null; $client = $this->createMock(StreamingClient::class); @@ -507,27 +507,25 @@ public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClo $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); $this->redis->subscribe('foo'); - $this->assertTrue(is_callable($allHandler['subscribe'])); + assert(isset($allHandler['subscribe']) && is_callable($allHandler['subscribe'])); $allHandler['subscribe']('foo', 1); $this->redis->subscribe('bar'); - $this->assertTrue(is_callable($allHandler['subscribe'])); $allHandler['subscribe']('bar', 2); $this->redis->unsubscribe('bar'); - $this->assertTrue(is_callable($allHandler['unsubscribe'])); + assert(isset($allHandler['unsubscribe']) && is_callable($allHandler['unsubscribe'])); $allHandler['unsubscribe']('bar', 1); $this->redis->psubscribe('foo*'); - $this->assertTrue(is_callable($allHandler['psubscribe'])); + assert(isset($allHandler['psubscribe']) && is_callable($allHandler['psubscribe'])); $allHandler['psubscribe']('foo*', 1); $this->redis->psubscribe('bar*'); - $this->assertTrue(is_callable($allHandler['psubscribe'])); $allHandler['psubscribe']('bar*', 2); $this->redis->punsubscribe('bar*'); - $this->assertTrue(is_callable($allHandler['punsubscribe'])); + assert(isset($allHandler['punsubscribe']) && is_callable($allHandler['punsubscribe'])); $allHandler['punsubscribe']('bar*', 1); $this->redis->on('unsubscribe', $this->expectCallableOnce()); @@ -537,7 +535,7 @@ public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClo $allHandler['close'](); } - public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAndNotStartIdleTimerWithIdleDueToSubscription() + public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAndNotStartIdleTimerWithIdleDueToSubscription(): void { $subscribeHandler = null; $deferred = new Deferred(); @@ -561,7 +559,7 @@ public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAnd $promise->then($this->expectCallableOnceWith(['subscribe', 'foo', 1])); } - public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientResolvesUnsubscribeAndStartIdleTimerWhenSubscriptionStopped() + public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientResolvesUnsubscribeAndStartIdleTimerWhenSubscriptionStopped(): void { $subscribeHandler = null; $unsubscribeHandler = null; @@ -595,7 +593,7 @@ public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientReso $promise->then($this->expectCallableOnceWith(['unsubscribe', 'foo', 0])); } - public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResponse() + public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResponse(): void { $closeHandler = null; $deferred = new Deferred(); diff --git a/tests/TestCase.php b/tests/TestCase.php index 0da36f4..b79f7ab 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -13,14 +13,17 @@ protected function expectCallableOnce(): callable { $mock = $this->createCallableMock(); $mock->expects($this->once())->method('__invoke'); + assert(is_callable($mock)); return $mock; } + /** @param mixed $argument */ protected function expectCallableOnceWith($argument): callable { $mock = $this->createCallableMock(); $mock->expects($this->once())->method('__invoke')->with($argument); + assert(is_callable($mock)); return $mock; } @@ -29,6 +32,7 @@ protected function expectCallableNever(): callable { $mock = $this->createCallableMock(); $mock->expects($this->never())->method('__invoke'); + assert(is_callable($mock)); return $mock; } From 7299866ddb8ca5e63bff7d728a027b39591d55bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 11 Nov 2022 12:32:47 +0100 Subject: [PATCH 42/65] Update PHPStan and stricter types for PHPUnit --- composer.json | 2 +- phpstan.neon.dist | 2 -- tests/FunctionalTest.php | 7 +------ tests/Io/FactoryStreamingClientTest.php | 4 ++++ tests/Io/StreamingClientTest.php | 12 ++++++++++++ tests/RedisClientTest.php | 3 +++ 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 3f933de..ab034dd 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ }, "require-dev": { "clue/block-react": "^1.5", - "phpstan/phpstan": "1.8.11 || 1.4.10", + "phpstan/phpstan": "1.9.2 || 1.4.10", "phpunit/phpunit": "^9.5 || ^7.5" }, "autoload": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index aa2f1e3..f9fd7fb 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -13,5 +13,3 @@ parameters: # ignore undefined methods due to magic `__call()` method - '/^Call to an undefined method Clue\\React\\Redis\\RedisClient::.+\(\)\.$/' - '/^Call to an undefined method Clue\\React\\Redis\\Io\\StreamingClient::.+\(\)\.$/' - # ignore incomplete type information for mocks in legacy PHPUnit 7.5 - - '/^Parameter #\d+ .+ of .+ expects .+, PHPUnit\\Framework\\MockObject\\MockObject given\.$/' diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 673ab73..b55309b 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -103,12 +103,7 @@ public function testInvalidCommand(): void $redis = new RedisClient($this->uri, null, $this->loop); $promise = $redis->doesnotexist(1, 2, 3); - if (method_exists($this, 'expectException')) { - $this->expectException('Exception'); - } else { - assert(method_exists($this, 'setExpectedException')); - $this->setExpectedException('Exception'); - } + $this->expectException(\Exception::class); await($promise, $this->loop); } diff --git a/tests/Io/FactoryStreamingClientTest.php b/tests/Io/FactoryStreamingClientTest.php index 3cb09c6..da83302 100644 --- a/tests/Io/FactoryStreamingClientTest.php +++ b/tests/Io/FactoryStreamingClientTest.php @@ -28,6 +28,9 @@ public function setUp(): void { $this->loop = $this->createMock(LoopInterface::class); $this->connector = $this->createMock(ConnectorInterface::class); + + assert($this->loop instanceof LoopInterface); + assert($this->connector instanceof ConnectorInterface); $this->factory = new Factory($this->loop, $this->connector); } @@ -47,6 +50,7 @@ public function testConstructWithoutLoopAssignsLoopAutomatically(): void */ public function testCtor(): void { + assert($this->loop instanceof LoopInterface); $this->factory = new Factory($this->loop); } diff --git a/tests/Io/StreamingClientTest.php b/tests/Io/StreamingClientTest.php index dc47ad7..241a76d 100644 --- a/tests/Io/StreamingClientTest.php +++ b/tests/Io/StreamingClientTest.php @@ -35,6 +35,9 @@ public function setUp(): void $this->parser = $this->createMock(ParserInterface::class); $this->serializer = $this->createMock(SerializerInterface::class); + assert($this->stream instanceof DuplexStreamInterface); + assert($this->parser instanceof ParserInterface); + assert($this->serializer instanceof SerializerInterface); $this->redis = new StreamingClient($this->stream, $this->parser, $this->serializer); } @@ -56,6 +59,8 @@ public function testClosingClientEmitsEvent(): void public function testClosingStreamClosesClient(): void { $stream = new ThroughStream(); + assert($this->parser instanceof ParserInterface); + assert($this->serializer instanceof SerializerInterface); $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); $this->redis->on('close', $this->expectCallableOnce()); @@ -66,6 +71,8 @@ public function testClosingStreamClosesClient(): void public function testReceiveParseErrorEmitsErrorEvent(): void { $stream = new ThroughStream(); + assert($this->parser instanceof ParserInterface); + assert($this->serializer instanceof SerializerInterface); $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); $this->redis->on('error', $this->expectCallableOnceWith( @@ -88,6 +95,8 @@ public function testReceiveParseErrorEmitsErrorEvent(): void public function testReceiveUnexpectedReplyEmitsErrorEvent(): void { $stream = new ThroughStream(); + assert($this->parser instanceof ParserInterface); + assert($this->serializer instanceof SerializerInterface); $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); $this->redis->on('error', $this->expectCallableOnce()); @@ -169,6 +178,9 @@ public function testClosingStreamRejectsAllRemainingRequests(): void { $stream = new ThroughStream(function () { return ''; }); $this->parser->expects($this->once())->method('pushIncoming')->willReturn([]); + + assert($this->parser instanceof ParserInterface); + assert($this->serializer instanceof SerializerInterface); $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); $promise = $this->redis->ping(); diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 813523a..323b3f8 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -27,6 +27,7 @@ public function setUp(): void $this->factory = $this->createMock(Factory::class); $this->loop = $this->createMock(LoopInterface::class); + assert($this->loop instanceof LoopInterface); $this->redis = new RedisClient('localhost', null, $this->loop); $ref = new \ReflectionProperty($this->redis, 'factory'); @@ -73,6 +74,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimerWithIdleTimeFromQueryParam(): void { + assert($this->loop instanceof LoopInterface); $this->redis = new RedisClient('localhost?idle=10', null, $this->loop); $ref = new \ReflectionProperty($this->redis, 'factory'); @@ -95,6 +97,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartIdleTimerWhenIdleParamIsNegative(): void { + assert($this->loop instanceof LoopInterface); $this->redis = new RedisClient('localhost?idle=-1', null, $this->loop); $ref = new \ReflectionProperty($this->redis, 'factory'); From 33ed63df1999bb7d3a3780c85bd2bac1c039dafa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 3 Jun 2023 12:04:29 +0200 Subject: [PATCH 43/65] Update test environment and dev dependencies --- README.md | 28 ++++++++++++++++++++++------ composer.json | 4 ++-- phpunit.xml.dist | 6 +++--- phpunit.xml.legacy | 2 +- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 94d301a..8e439fa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # clue/reactphp-redis [![CI status](https://github.com/clue/reactphp-redis/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-redis/actions) +[![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests) +[![PHPStan level](https://img.shields.io/badge/PHPStan%20level-max-success)](#tests) [![installs on Packagist](https://img.shields.io/packagist/dt/clue/redis-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/redis-react) Async [Redis](https://redis.io/) client implementation, built on top of [ReactPHP](https://reactphp.org/). @@ -496,7 +498,7 @@ Once released, this project will follow [SemVer](https://semver.org/). At the moment, this will install the latest development version: ```bash -$ composer require clue/redis-react:^3@dev +composer require clue/redis-react:^3@dev ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. @@ -510,7 +512,7 @@ smooth upgrade path. You may target multiple versions at the same time to support a wider range of PHP versions like this: ```bash -$ composer require "clue/redis-react:^3@dev || ^2" +composer require "clue/redis-react:^3@dev || ^2" ``` ## Tests @@ -519,13 +521,21 @@ 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 ``` To run the test suite, go to the project root and run: ```bash -$ vendor/bin/phpunit +vendor/bin/phpunit +``` + +The test suite is set up to always ensure 100% code coverage across all +supported environments. If you have the Xdebug extension installed, you can also +generate a code coverage report locally like this: + +```bash +XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text ``` The test suite contains both unit tests and functional integration tests. @@ -535,14 +545,20 @@ and will be skipped by default. If you don't have access to a running Redis server, you can also use a temporary `Redis` Docker image: ```bash -$ docker run --net=host redis +docker run --net=host redis ``` To now run the functional tests, you need to supply *your* login details in an environment variable like this: ```bash -$ REDIS_URI=localhost:6379 vendor/bin/phpunit +REDIS_URI=localhost:6379 vendor/bin/phpunit +``` + +On top of this, we use PHPStan on max level to ensure type safety across the project: + +```bash +vendor/bin/phpstan ``` ## License diff --git a/composer.json b/composer.json index ab034dd..a0cb588 100644 --- a/composer.json +++ b/composer.json @@ -21,8 +21,8 @@ }, "require-dev": { "clue/block-react": "^1.5", - "phpstan/phpstan": "1.9.2 || 1.4.10", - "phpunit/phpunit": "^9.5 || ^7.5" + "phpstan/phpstan": "1.10.15 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" }, "autoload": { "psr-4": { "Clue\\React\\Redis\\": "src/" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bd23341..a743d84 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,8 +1,8 @@ - + - + diff --git a/phpunit.xml.legacy b/phpunit.xml.legacy index 5ce58af..5c226c9 100644 --- a/phpunit.xml.legacy +++ b/phpunit.xml.legacy @@ -18,7 +18,7 @@ - + From 741175db98e057f912eb2a56a88b3737db733a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 9 Jul 2023 19:21:35 +0200 Subject: [PATCH 44/65] Update close handler to avoid unhandled promise rejections --- src/Io/Factory.php | 2 ++ src/RedisClient.php | 2 ++ tests/FunctionalTest.php | 11 ----------- tests/Io/FactoryStreamingClientTest.php | 8 ++++++-- tests/RedisClientTest.php | 10 ++++++++-- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Io/Factory.php b/src/Io/Factory.php index a760832..c8b2b97 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -91,6 +91,8 @@ public function createClient(string $uri): PromiseInterface // either close successful connection or cancel pending connection attempt $connecting->then(function (ConnectionInterface $connection) { $connection->close(); + }, function () { + // ignore to avoid reporting unhandled rejection }); assert(\method_exists($connecting, 'cancel')); $connecting->cancel(); diff --git a/src/RedisClient.php b/src/RedisClient.php index 8230294..9ad7027 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -220,6 +220,8 @@ public function close(): void if ($this->promise !== null) { $this->promise->then(function (StreamingClient $redis) { $redis->close(); + }, function () { + // ignore to avoid reporting unhandled rejection }); if ($this->promise !== null) { assert(\method_exists($this->promise, 'cancel')); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index b55309b..be36eda 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -161,15 +161,4 @@ public function testClose(): void $redis->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce()); } - - public function testCloseLazy(): void - { - $redis = new RedisClient($this->uri, null, $this->loop); - - $redis->get('willBeCanceledAnyway')->then(null, $this->expectCallableOnce()); - - $redis->close(); - - $redis->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce()); - } } diff --git a/tests/Io/FactoryStreamingClientTest.php b/tests/Io/FactoryStreamingClientTest.php index da83302..4df8589 100644 --- a/tests/Io/FactoryStreamingClientTest.php +++ b/tests/Io/FactoryStreamingClientTest.php @@ -57,13 +57,17 @@ public function testCtor(): void public function testWillConnectWithDefaultPort(): void { $this->connector->expects($this->once())->method('connect')->with('redis.example.com:6379')->willReturn(reject(new \RuntimeException())); - $this->factory->createClient('redis.example.com'); + $promise = $this->factory->createClient('redis.example.com'); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection } public function testWillConnectToLocalhost(): void { $this->connector->expects($this->once())->method('connect')->with('localhost:1337')->willReturn(reject(new \RuntimeException())); - $this->factory->createClient('localhost:1337'); + $promise = $this->factory->createClient('localhost:1337'); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection } public function testWillResolveIfConnectorResolves(): void diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 323b3f8..8fc8bab 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -161,7 +161,10 @@ public function testPingAfterPreviousFactoryRejectsUnderlyingClientWillCreateNew new Promise(function () { }) ); - $this->redis->ping(); + $promise = $this->redis->ping(); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $deferred->reject($error); $this->redis->ping(); @@ -308,7 +311,10 @@ public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientC $this->redis->on('error', $this->expectCallableNever()); $this->redis->on('close', $this->expectCallableOnce()); - $this->redis->ping(); + $promise = $this->redis->ping(); + + $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection + $this->redis->close(); } From d4bd58eb3ea77c9da4edc210938ad6b3ac67a7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 14 Jul 2023 11:42:16 +0200 Subject: [PATCH 45/65] Use Promise v3 template types --- README.md | 2 +- composer.json | 2 +- phpstan.neon.dist | 2 +- src/Io/Factory.php | 8 +++++--- src/Io/StreamingClient.php | 7 +++++-- src/RedisClient.php | 11 ++++++++--- tests/TestCase.php | 23 +++++------------------ 7 files changed, 26 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 8e439fa..b143811 100644 --- a/README.md +++ b/README.md @@ -421,7 +421,7 @@ given event loop instance. #### __call() -The `__call(string $name, string[] $args): PromiseInterface` method can be used to +The `__call(string $name, string[] $args): PromiseInterface` method can be used to invoke the given command. This is a magic method that will be invoked when calling any Redis command on this instance. diff --git a/composer.json b/composer.json index a0cb588..c1414b3 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/event-loop": "^1.2", "react/promise": "^3 || ^2.0 || ^1.1", - "react/promise-timer": "^1.9", + "react/promise-timer": "^1.10", "react/socket": "^1.12" }, "require-dev": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f9fd7fb..fb725aa 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,7 +8,7 @@ parameters: reportUnmatchedIgnoredErrors: false ignoreErrors: - # ignore generic usage like `PromiseInterface` until fixed upstream + # ignore generic usage like `PromiseInterface` for Promise v2/v1 - '/^PHPDoc tag @return contains generic type React\\Promise\\PromiseInterface<.+> but interface React\\Promise\\PromiseInterface is not generic\.$/' # ignore undefined methods due to magic `__call()` method - '/^Call to an undefined method Clue\\React\\Redis\\RedisClient::.+\(\)\.$/' diff --git a/src/Io/Factory.php b/src/Io/Factory.php index c8b2b97..d70a08c 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -44,7 +44,7 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn * Create Redis client connected to address of given redis instance * * @param string $uri Redis server URI to connect to - * @return PromiseInterface Promise that will + * @return PromiseInterface Promise that will * be fulfilled with `StreamingClient` on success or rejects with `\Exception` on error. */ public function createClient(string $uri): PromiseInterface @@ -79,6 +79,8 @@ public function createClient(string $uri): PromiseInterface $authority = 'unix://' . substr($parts['path'], 1); unset($parts['path']); } + + /** @var PromiseInterface $connecting */ $connecting = $this->connector->connect($authority); $deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) { @@ -100,7 +102,7 @@ public function createClient(string $uri): PromiseInterface $promise = $connecting->then(function (ConnectionInterface $stream) { return new StreamingClient($stream, $this->protocol->createResponseParser(), $this->protocol->createSerializer()); - }, function (\Exception $e) use ($uri) { + }, function (\Throwable $e) use ($uri) { throw new \RuntimeException( 'Connection to ' . $uri . ' failed: ' . $e->getMessage(), $e->getCode(), @@ -175,7 +177,7 @@ function (\Exception $e) use ($redis, $uri) { return $deferred->promise(); } - return timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) { + return timeout($deferred->promise(), $timeout, $this->loop)->then(null, function (\Throwable $e) use ($uri) { if ($e instanceof TimeoutException) { throw new \RuntimeException( 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)', diff --git a/src/Io/StreamingClient.php b/src/Io/StreamingClient.php index 1dc2fde..bb7689f 100644 --- a/src/Io/StreamingClient.php +++ b/src/Io/StreamingClient.php @@ -24,7 +24,7 @@ class StreamingClient extends EventEmitter /** @var SerializerInterface */ private $serializer; - /** @var Deferred[] */ + /** @var Deferred[] */ private $requests = []; /** @var bool */ @@ -71,7 +71,10 @@ public function __construct(DuplexStreamInterface $stream, ParserInterface $pars $this->serializer = $serializer; } - /** @param string[] $args */ + /** + * @param string[] $args + * @return PromiseInterface + */ public function __call(string $name, array $args): PromiseInterface { $request = new Deferred(); diff --git a/src/RedisClient.php b/src/RedisClient.php index 9ad7027..6fa991c 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -37,7 +37,7 @@ class RedisClient extends EventEmitter /** @var bool */ private $closed = false; - /** @var ?PromiseInterface */ + /** @var ?PromiseInterface */ private $promise = null; /** @var LoopInterface */ @@ -76,6 +76,9 @@ public function __construct($url, ConnectorInterface $connector = null, LoopInte $this->factory = new Factory($this->loop, $connector); } + /** + * @return PromiseInterface + */ private function client(): PromiseInterface { if ($this->promise !== null) { @@ -132,7 +135,9 @@ private function client(): PromiseInterface ); return $redis; - }, function (\Exception $e) { + }, function (\Throwable $e) { + assert($e instanceof \Exception); + // connection failed => discard connection attempt $this->promise = null; @@ -148,7 +153,7 @@ private function client(): PromiseInterface * * @param string $name * @param string[] $args - * @return PromiseInterface Promise + * @return PromiseInterface */ public function __call(string $name, array $args): PromiseInterface { diff --git a/tests/TestCase.php b/tests/TestCase.php index b79f7ab..483864d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -48,30 +48,17 @@ protected function createCallableMock(): MockObject } } - protected function expectPromiseResolve(PromiseInterface $promise): PromiseInterface + /** + * @param PromiseInterface $promise + */ + protected function expectPromiseResolve(PromiseInterface $promise): void { $this->assertInstanceOf(PromiseInterface::class, $promise); - $promise->then(null, function(\Exception $error) { + $promise->then(null, function(\Throwable $error) { $this->assertNull($error); $this->fail('promise rejected'); }); $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); - - return $promise; - } - - protected function expectPromiseReject(PromiseInterface $promise): PromiseInterface - { - $this->assertInstanceOf(PromiseInterface::class, $promise); - - $promise->then(function($value) { - $this->assertNull($value); - $this->fail('promise resolved'); - }); - - $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); - - return $promise; } } From e38538046feff3935a67c14facb6556dee542f20 Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Thu, 2 Nov 2023 08:28:53 +0100 Subject: [PATCH 46/65] Fix typos in documentation --- CHANGELOG.md | 2 +- README.md | 2 +- src/RedisClient.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5e8e66..6d2d33a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,7 +139,7 @@ affected by any BC breaks, see below for more details. `ConnectorInterface` and then update to this version without causing a BC break. -* BC break: Remove uneeded `data` event and support for advanced `MONITOR` +* BC break: Remove unneeded `data` event and support for advanced `MONITOR` command for performance and consistency reasons and remove underdocumented `isBusy()` method. (#62, #63 and #64 by @clue) diff --git a/README.md b/README.md index b143811..98be7a5 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,7 @@ create a new underlying connection if this idle time is expired. From a consumer side this means that you can start sending commands 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. If the underlying database connection fails, it will reject all diff --git a/src/RedisClient.php b/src/RedisClient.php index 6fa991c..bfe9ac8 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -90,7 +90,7 @@ private function client(): PromiseInterface $redis->on('close', function () { $this->promise = null; - // foward unsubscribe/punsubscribe events when underlying connection closes + // forward unsubscribe/punsubscribe events when underlying connection closes $n = count($this->subscribed); foreach ($this->subscribed as $channel => $_) { $this->emit('unsubscribe', [$channel, --$n]); From 15f43bf3e50a373a6c4431796aeca90841a429d5 Mon Sep 17 00:00:00 2001 From: Yada Clintjens Date: Fri, 24 Nov 2023 12:56:49 +0100 Subject: [PATCH 47/65] Run tests on PHP 8.3 and update test suite --- .github/workflows/ci.yml | 6 ++++-- composer.json | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21293f7..cf56001 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 @@ -19,7 +20,7 @@ jobs: - 7.2 - 7.1 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} @@ -44,6 +45,7 @@ jobs: strategy: matrix: php: + - 8.3 - 8.2 - 8.1 - 8.0 @@ -52,7 +54,7 @@ jobs: - 7.2 - 7.1 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} diff --git a/composer.json b/composer.json index c1414b3..8dea32e 100644 --- a/composer.json +++ b/composer.json @@ -25,9 +25,13 @@ "phpunit/phpunit": "^9.6 || ^7.5" }, "autoload": { - "psr-4": { "Clue\\React\\Redis\\": "src/" } + "psr-4": { + "Clue\\React\\Redis\\": "src/" + } }, "autoload-dev": { - "psr-4": { "Clue\\Tests\\React\\Redis\\": "tests/" } + "psr-4": { + "Clue\\Tests\\React\\Redis\\": "tests/" + } } } From 579169d5308141cb3e7fab90f0aaf9a9a16ee653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 8 Dec 2023 16:32:50 +0100 Subject: [PATCH 48/65] Avoid referencing unneeded explicit loop instance --- composer.json | 8 +++++- tests/FunctionalTest.php | 54 ++++++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/composer.json b/composer.json index c1414b3..5601a46 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "react/event-loop": "^1.2", "react/promise": "^3 || ^2.0 || ^1.1", "react/promise-timer": "^1.10", - "react/socket": "^1.12" + "react/socket": "dev-cancel-happy as 1.15.0" }, "require-dev": { "clue/block-react": "^1.5", @@ -29,5 +29,11 @@ }, "autoload-dev": { "psr-4": { "Clue\\Tests\\React\\Redis\\": "tests/" } + }, + "repositories": { + "clue": { + "type": "vcs", + "url": "https://github.com/clue-labs/socket" + } } } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index be36eda..d66e80b 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -3,15 +3,14 @@ namespace Clue\Tests\React\Redis; use Clue\React\Redis\RedisClient; -use React\EventLoop\StreamSelectLoop; +use React\EventLoop\Loop; use React\Promise\Deferred; use React\Promise\PromiseInterface; use function Clue\React\Block\await; +use function React\Promise\Timer\timeout; class FunctionalTest extends TestCase { - /** @var StreamSelectLoop */ - private $loop; /** @var string */ private $uri; @@ -22,30 +21,28 @@ public function setUp(): void if ($this->uri === '') { $this->markTestSkipped('No REDIS_URI environment variable given'); } - - $this->loop = new StreamSelectLoop(); } public function testPing(): void { - $redis = new RedisClient($this->uri, null, $this->loop); + $redis = new RedisClient($this->uri); $promise = $redis->ping(); $this->assertInstanceOf(PromiseInterface::class, $promise); - $ret = await($promise, $this->loop); + $ret = await($promise); $this->assertEquals('PONG', $ret); } public function testPingLazy(): void { - $redis = new RedisClient($this->uri, null, $this->loop); + $redis = new RedisClient($this->uri); $promise = $redis->ping(); $this->assertInstanceOf(PromiseInterface::class, $promise); - $ret = await($promise, $this->loop); + $ret = await($promise); $this->assertEquals('PONG', $ret); } @@ -55,11 +52,11 @@ public function testPingLazy(): void */ public function testPingLazyWillNotBlockLoop(): void { - $redis = new RedisClient($this->uri, null, $this->loop); + $redis = new RedisClient($this->uri); $redis->ping(); - $this->loop->run(); + Loop::run(); } /** @@ -67,58 +64,58 @@ public function testPingLazyWillNotBlockLoop(): void */ public function testLazyClientWithoutCommandsWillNotBlockLoop(): void { - $redis = new RedisClient($this->uri, null, $this->loop); + $redis = new RedisClient($this->uri); - $this->loop->run(); + Loop::run(); unset($redis); } public function testMgetIsNotInterpretedAsSubMessage(): void { - $redis = new RedisClient($this->uri, null, $this->loop); + $redis = new RedisClient($this->uri); $redis->mset('message', 'message', 'channel', 'channel', 'payload', 'payload'); $promise = $redis->mget('message', 'channel', 'payload')->then($this->expectCallableOnce()); $redis->on('message', $this->expectCallableNever()); - await($promise, $this->loop); + await($promise); } public function testPipeline(): void { - $redis = new RedisClient($this->uri, null, $this->loop); + $redis = new RedisClient($this->uri); $redis->set('a', 1)->then($this->expectCallableOnceWith('OK')); $redis->incr('a')->then($this->expectCallableOnceWith(2)); $redis->incr('a')->then($this->expectCallableOnceWith(3)); $promise = $redis->get('a')->then($this->expectCallableOnceWith('3')); - await($promise, $this->loop); + await($promise); } public function testInvalidCommand(): void { - $redis = new RedisClient($this->uri, null, $this->loop); + $redis = new RedisClient($this->uri); $promise = $redis->doesnotexist(1, 2, 3); $this->expectException(\Exception::class); - await($promise, $this->loop); + await($promise); } public function testMultiExecEmpty(): void { - $redis = new RedisClient($this->uri, null, $this->loop); + $redis = new RedisClient($this->uri); $redis->multi()->then($this->expectCallableOnceWith('OK')); $promise = $redis->exec()->then($this->expectCallableOnceWith([])); - await($promise, $this->loop); + await($promise); } public function testMultiExecQueuedExecHasValues(): void { - $redis = new RedisClient($this->uri, null, $this->loop); + $redis = new RedisClient($this->uri); $redis->multi()->then($this->expectCallableOnceWith('OK')); $redis->set('b', 10)->then($this->expectCallableOnceWith('QUEUED')); @@ -127,13 +124,13 @@ public function testMultiExecQueuedExecHasValues(): void $redis->ttl('b')->then($this->expectCallableOnceWith('QUEUED')); $promise = $redis->exec()->then($this->expectCallableOnceWith(['OK', 1, 12, 20])); - await($promise, $this->loop); + await($promise); } public function testPubSub(): void { - $consumer = new RedisClient($this->uri, null, $this->loop); - $producer = new RedisClient($this->uri, null, $this->loop); + $consumer = new RedisClient($this->uri); + $producer = new RedisClient($this->uri); $channel = 'channel:test:' . mt_rand(); @@ -148,12 +145,15 @@ public function testPubSub(): void })->then($this->expectCallableOnce()); // expect "message" event to take no longer than 0.1s - await($deferred->promise(), $this->loop, 0.1); + + await(timeout($deferred->promise(), 0.1)); + + await($consumer->unsubscribe($channel)); } public function testClose(): void { - $redis = new RedisClient($this->uri, null, $this->loop); + $redis = new RedisClient($this->uri); $redis->get('willBeCanceledAnyway')->then(null, $this->expectCallableOnce()); From 9cae74a00a6597068b89701105be4201441aa4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 10 Dec 2023 17:58:50 +0100 Subject: [PATCH 49/65] Update to use reactphp/async instead of clue/reactphp-block --- composer.json | 4 ++-- tests/FunctionalTest.php | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 5601a46..f0eda34 100644 --- a/composer.json +++ b/composer.json @@ -20,9 +20,9 @@ "react/socket": "dev-cancel-happy as 1.15.0" }, "require-dev": { - "clue/block-react": "^1.5", "phpstan/phpstan": "1.10.15 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" + "phpunit/phpunit": "^9.6 || ^7.5", + "react/async": "^4.2 || ^3.2" }, "autoload": { "psr-4": { "Clue\\React\\Redis\\": "src/" } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index d66e80b..767c530 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -6,7 +6,7 @@ use React\EventLoop\Loop; use React\Promise\Deferred; use React\Promise\PromiseInterface; -use function Clue\React\Block\await; +use function React\Async\await; use function React\Promise\Timer\timeout; class FunctionalTest extends TestCase @@ -27,8 +27,8 @@ public function testPing(): void { $redis = new RedisClient($this->uri); + /** @var PromiseInterface */ $promise = $redis->ping(); - $this->assertInstanceOf(PromiseInterface::class, $promise); $ret = await($promise); @@ -39,8 +39,8 @@ public function testPingLazy(): void { $redis = new RedisClient($this->uri); + /** @var PromiseInterface */ $promise = $redis->ping(); - $this->assertInstanceOf(PromiseInterface::class, $promise); $ret = await($promise); @@ -77,6 +77,7 @@ public function testMgetIsNotInterpretedAsSubMessage(): void $redis->mset('message', 'message', 'channel', 'channel', 'payload', 'payload'); + /** @var PromiseInterface */ $promise = $redis->mget('message', 'channel', 'payload')->then($this->expectCallableOnce()); $redis->on('message', $this->expectCallableNever()); @@ -90,6 +91,8 @@ public function testPipeline(): void $redis->set('a', 1)->then($this->expectCallableOnceWith('OK')); $redis->incr('a')->then($this->expectCallableOnceWith(2)); $redis->incr('a')->then($this->expectCallableOnceWith(3)); + + /** @var PromiseInterface */ $promise = $redis->get('a')->then($this->expectCallableOnceWith('3')); await($promise); @@ -98,6 +101,8 @@ public function testPipeline(): void public function testInvalidCommand(): void { $redis = new RedisClient($this->uri); + + /** @var PromiseInterface */ $promise = $redis->doesnotexist(1, 2, 3); $this->expectException(\Exception::class); @@ -108,6 +113,8 @@ public function testMultiExecEmpty(): void { $redis = new RedisClient($this->uri); $redis->multi()->then($this->expectCallableOnceWith('OK')); + + /** @var PromiseInterface */ $promise = $redis->exec()->then($this->expectCallableOnceWith([])); await($promise); @@ -122,6 +129,8 @@ public function testMultiExecQueuedExecHasValues(): void $redis->expire('b', 20)->then($this->expectCallableOnceWith('QUEUED')); $redis->incrBy('b', 2)->then($this->expectCallableOnceWith('QUEUED')); $redis->ttl('b')->then($this->expectCallableOnceWith('QUEUED')); + + /** @var PromiseInterface */ $promise = $redis->exec()->then($this->expectCallableOnceWith(['OK', 1, 12, 20])); await($promise); @@ -135,6 +144,7 @@ public function testPubSub(): void $channel = 'channel:test:' . mt_rand(); // consumer receives a single message + /** @var Deferred */ $deferred = new Deferred(); $consumer->on('message', $this->expectCallableOnce()); $consumer->on('message', [$deferred, 'resolve']); @@ -148,7 +158,9 @@ public function testPubSub(): void await(timeout($deferred->promise(), 0.1)); - await($consumer->unsubscribe($channel)); + /** @var PromiseInterface */ + $promise = $consumer->unsubscribe($channel); + await($promise); } public function testClose(): void From d1b2e0fd6b88b3a9127bb5d591b38aae215d104b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 15 Dec 2023 23:15:08 +0100 Subject: [PATCH 50/65] Update to stable Socket v1.15.0 release --- composer.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/composer.json b/composer.json index f0eda34..72642e9 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "react/event-loop": "^1.2", "react/promise": "^3 || ^2.0 || ^1.1", "react/promise-timer": "^1.10", - "react/socket": "dev-cancel-happy as 1.15.0" + "react/socket": "^1.15" }, "require-dev": { "phpstan/phpstan": "1.10.15 || 1.4.10", @@ -29,11 +29,5 @@ }, "autoload-dev": { "psr-4": { "Clue\\Tests\\React\\Redis\\": "tests/" } - }, - "repositories": { - "clue": { - "type": "vcs", - "url": "https://github.com/clue-labs/socket" - } } } From def15d7e869068aae7361842a8ea990ad735cf14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 30 Jul 2023 12:55:37 +0200 Subject: [PATCH 51/65] Update to require Promise v3 --- composer.json | 2 +- phpstan.neon.dist | 3 --- src/Io/Factory.php | 2 -- src/RedisClient.php | 1 - tests/Io/FactoryStreamingClientTest.php | 3 --- tests/TestCase.php | 10 +++++----- 6 files changed, 6 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 242b91f..335b526 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "clue/redis-protocol": "0.3.*", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/event-loop": "^1.2", - "react/promise": "^3 || ^2.0 || ^1.1", + "react/promise": "^3", "react/promise-timer": "^1.10", "react/socket": "^1.15" }, diff --git a/phpstan.neon.dist b/phpstan.neon.dist index fb725aa..be4a66a 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,10 +6,7 @@ parameters: - src/ - tests/ - reportUnmatchedIgnoredErrors: false ignoreErrors: - # ignore generic usage like `PromiseInterface` for Promise v2/v1 - - '/^PHPDoc tag @return contains generic type React\\Promise\\PromiseInterface<.+> but interface React\\Promise\\PromiseInterface is not generic\.$/' # ignore undefined methods due to magic `__call()` method - '/^Call to an undefined method Clue\\React\\Redis\\RedisClient::.+\(\)\.$/' - '/^Call to an undefined method Clue\\React\\Redis\\Io\\StreamingClient::.+\(\)\.$/' diff --git a/src/Io/Factory.php b/src/Io/Factory.php index d70a08c..b6778b9 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -80,7 +80,6 @@ public function createClient(string $uri): PromiseInterface unset($parts['path']); } - /** @var PromiseInterface $connecting */ $connecting = $this->connector->connect($authority); $deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) { @@ -96,7 +95,6 @@ public function createClient(string $uri): PromiseInterface }, function () { // ignore to avoid reporting unhandled rejection }); - assert(\method_exists($connecting, 'cancel')); $connecting->cancel(); }); diff --git a/src/RedisClient.php b/src/RedisClient.php index bfe9ac8..f923d0f 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -229,7 +229,6 @@ public function close(): void // ignore to avoid reporting unhandled rejection }); if ($this->promise !== null) { - assert(\method_exists($this->promise, 'cancel')); $this->promise->cancel(); $this->promise = null; } diff --git a/tests/Io/FactoryStreamingClientTest.php b/tests/Io/FactoryStreamingClientTest.php index 4df8589..fba36e8 100644 --- a/tests/Io/FactoryStreamingClientTest.php +++ b/tests/Io/FactoryStreamingClientTest.php @@ -459,7 +459,6 @@ public function testCancelWillRejectPromise(): void $promise = $this->factory->createClient('redis://127.0.0.1:2'); - assert(method_exists($promise, 'cancel')); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf(\RuntimeException::class))); @@ -542,7 +541,6 @@ public function testCancelWillRejectWithUriInMessageAndCancelConnectorWhenConnec $promise = $this->factory->createClient($uri); - assert(method_exists($promise, 'cancel')); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith( @@ -568,7 +566,6 @@ public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect(): voi $promise = $this->factory->createClient('redis://127.0.0.1:2/123'); - assert(method_exists($promise, 'cancel')); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith( diff --git a/tests/TestCase.php b/tests/TestCase.php index 483864d..57e4a6d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,6 @@ namespace Clue\Tests\React\Redis; -use PHPUnit\Framework\MockObject\MockBuilder; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase as BaseTestCase; use React\Promise\PromiseInterface; @@ -39,12 +38,13 @@ protected function expectCallableNever(): callable protected function createCallableMock(): MockObject { - if (method_exists(MockBuilder::class, 'addMethods')) { - // @phpstan-ignore-next-line requires PHPUnit 9+ - return $this->getMockBuilder(\stdClass::class)->addMethods(['__invoke'])->getMock(); + $builder = $this->getMockBuilder(\stdClass::class); + if (method_exists($builder, 'addMethods')) { + // PHPUnit 9+ + return $builder->addMethods(['__invoke'])->getMock(); } else { // legacy PHPUnit < 9 - return $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock(); + return $builder->setMethods(['__invoke'])->getMock(); } } From ab315007e8fedb0709c6ab9357a7e54c2a9bb9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 18 Jun 2023 16:55:44 +0200 Subject: [PATCH 52/65] Include timeout logic to avoid dependency on reactphp/promise-timer --- composer.json | 1 - src/Io/Factory.php | 56 +++++++++++++++++++++---- tests/FunctionalTest.php | 18 ++++---- tests/Io/FactoryStreamingClientTest.php | 32 ++++++++++++++ 4 files changed, 90 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 335b526..007b36f 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/event-loop": "^1.2", "react/promise": "^3", - "react/promise-timer": "^1.10", "react/socket": "^1.15" }, "require-dev": { diff --git a/src/Io/Factory.php b/src/Io/Factory.php index b6778b9..a669793 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -6,13 +6,12 @@ use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Promise\Deferred; +use React\Promise\Promise; use React\Promise\PromiseInterface; -use React\Promise\Timer\TimeoutException; use React\Socket\ConnectionInterface; use React\Socket\Connector; use React\Socket\ConnectorInterface; use function React\Promise\reject; -use function React\Promise\Timer\timeout; /** * @internal @@ -175,14 +174,53 @@ function (\Exception $e) use ($redis, $uri) { return $deferred->promise(); } - return timeout($deferred->promise(), $timeout, $this->loop)->then(null, function (\Throwable $e) use ($uri) { - if ($e instanceof TimeoutException) { - throw new \RuntimeException( - 'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)', - defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110 - ); + $promise = $deferred->promise(); + + /** @var Promise */ + $ret = new Promise(function (callable $resolve, callable $reject) use ($timeout, $promise, $uri): void { + /** @var ?\React\EventLoop\TimerInterface */ + $timer = null; + $promise = $promise->then(function (StreamingClient $v) use (&$timer, $resolve): void { + if ($timer) { + $this->loop->cancelTimer($timer); + } + $timer = false; + $resolve($v); + }, function (\Throwable $e) use (&$timer, $reject): void { + if ($timer) { + $this->loop->cancelTimer($timer); + } + $timer = false; + $reject($e); + }); + + // promise already settled => no need to start timer + if ($timer === false) { + return; } - throw $e; + + // start timeout timer which will cancel the pending promise + $timer = $this->loop->addTimer($timeout, function () use ($timeout, &$promise, $reject, $uri): void { + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' timed out after ' . $timeout . ' seconds (ETIMEDOUT)', + \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110 + )); + + // Cancel pending connection to clean up any underlying resources and references. + // Avoid garbage references in call stack by passing pending promise by reference. + \assert($promise instanceof PromiseInterface); + $promise->cancel(); + $promise = null; + }); + }, function () use (&$promise): void { + // Cancelling this promise will cancel the pending connection, thus triggering the rejection logic above. + // Avoid garbage references in call stack by passing pending promise by reference. + \assert($promise instanceof PromiseInterface); + $promise->cancel(); + $promise = null; }); + + // variable assignment needed for legacy PHPStan on PHP 7.1 only + return $ret; } } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 767c530..c1fe42b 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -4,10 +4,9 @@ use Clue\React\Redis\RedisClient; use React\EventLoop\Loop; -use React\Promise\Deferred; +use React\Promise\Promise; use React\Promise\PromiseInterface; use function React\Async\await; -use function React\Promise\Timer\timeout; class FunctionalTest extends TestCase { @@ -144,10 +143,7 @@ public function testPubSub(): void $channel = 'channel:test:' . mt_rand(); // consumer receives a single message - /** @var Deferred */ - $deferred = new Deferred(); $consumer->on('message', $this->expectCallableOnce()); - $consumer->on('message', [$deferred, 'resolve']); $once = $this->expectCallableOnceWith(1); $consumer->subscribe($channel)->then(function() use ($producer, $channel, $once){ // producer sends a single message @@ -155,8 +151,16 @@ public function testPubSub(): void })->then($this->expectCallableOnce()); // expect "message" event to take no longer than 0.1s - - await(timeout($deferred->promise(), 0.1)); + await(new Promise(function (callable $resolve, callable $reject) use ($consumer): void { + $timeout = Loop::addTimer(0.1, function () use ($consumer, $reject): void { + $consumer->close(); + $reject(new \RuntimeException('Timed out')); + }); + $consumer->on('message', function () use ($timeout, $resolve): void { + Loop::cancelTimer($timeout); + $resolve(null); + }); + })); /** @var PromiseInterface */ $promise = $consumer->unsubscribe($channel); diff --git a/tests/Io/FactoryStreamingClientTest.php b/tests/Io/FactoryStreamingClientTest.php index fba36e8..669b069 100644 --- a/tests/Io/FactoryStreamingClientTest.php +++ b/tests/Io/FactoryStreamingClientTest.php @@ -7,6 +7,7 @@ use Clue\Tests\React\Redis\TestCase; use PHPUnit\Framework\MockObject\MockObject; use React\EventLoop\LoopInterface; +use React\EventLoop\TimerInterface; use React\Promise\Deferred; use React\Socket\ConnectionInterface; use React\Socket\ConnectorInterface; @@ -633,4 +634,35 @@ public function testCreateClientWithoutTimeoutParameterWillStartTimerWithDefault $this->factory->createClient('redis://127.0.0.1:2'); ini_set('default_socket_timeout', $old); } + + public function testCreateClientWillCancelTimerWhenConnectionResolves(): void + { + $timer = $this->createMock(TimerInterface::class); + $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); + $this->loop->expects($this->once())->method('cancelTimer')->with($timer); + + $deferred = new Deferred(); + $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:6379')->willReturn($deferred->promise()); + + $promise = $this->factory->createClient('127.0.0.1'); + $promise->then($this->expectCallableOnce()); + + $deferred->resolve($this->createMock(ConnectionInterface::class)); + } + + public function testCreateClientWillCancelTimerWhenConnectionRejects(): void + { + $timer = $this->createMock(TimerInterface::class); + $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); + $this->loop->expects($this->once())->method('cancelTimer')->with($timer); + + $deferred = new Deferred(); + $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:6379')->willReturn($deferred->promise()); + + $promise = $this->factory->createClient('127.0.0.1'); + + $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); + + $deferred->reject(new \RuntimeException()); + } } From 16859a7fa8560302f964220256d51b2db8261e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 28 Oct 2022 16:01:45 +0200 Subject: [PATCH 53/65] Remove optional `$loop` constructor argument, always use default loop --- README.md | 6 -- src/Io/Factory.php | 16 ++-- src/RedisClient.php | 22 ++--- tests/FunctionalTest.php | 1 - tests/Io/FactoryStreamingClientTest.php | 114 ++++++++++++++++++------ tests/RedisClientTest.php | 105 ++++++++++++++++------ 6 files changed, 177 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 98be7a5..0bf684a 100644 --- a/README.md +++ b/README.md @@ -413,12 +413,6 @@ $connector = new React\Socket\Connector([ $redis = new Clue\React\Redis\RedisClient('localhost', $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. - #### __call() The `__call(string $name, string[] $args): PromiseInterface` method can be used to diff --git a/src/Io/Factory.php b/src/Io/Factory.php index a669793..196ee0d 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -4,7 +4,6 @@ use Clue\Redis\Protocol\Factory as ProtocolFactory; use React\EventLoop\Loop; -use React\EventLoop\LoopInterface; use React\Promise\Deferred; use React\Promise\Promise; use React\Promise\PromiseInterface; @@ -18,9 +17,6 @@ */ class Factory { - /** @var LoopInterface */ - private $loop; - /** @var ConnectorInterface */ private $connector; @@ -28,14 +24,12 @@ class Factory private $protocol; /** - * @param ?LoopInterface $loop * @param ?ConnectorInterface $connector * @param ?ProtocolFactory $protocol */ - public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null, ProtocolFactory $protocol = null) + public function __construct(ConnectorInterface $connector = null, ProtocolFactory $protocol = null) { - $this->loop = $loop ?: Loop::get(); - $this->connector = $connector ?: new Connector([], $this->loop); + $this->connector = $connector ?: new Connector(); $this->protocol = $protocol ?: new ProtocolFactory(); } @@ -182,13 +176,13 @@ function (\Exception $e) use ($redis, $uri) { $timer = null; $promise = $promise->then(function (StreamingClient $v) use (&$timer, $resolve): void { if ($timer) { - $this->loop->cancelTimer($timer); + Loop::cancelTimer($timer); } $timer = false; $resolve($v); }, function (\Throwable $e) use (&$timer, $reject): void { if ($timer) { - $this->loop->cancelTimer($timer); + Loop::cancelTimer($timer); } $timer = false; $reject($e); @@ -200,7 +194,7 @@ function (\Exception $e) use ($redis, $uri) { } // start timeout timer which will cancel the pending promise - $timer = $this->loop->addTimer($timeout, function () use ($timeout, &$promise, $reject, $uri): void { + $timer = Loop::addTimer($timeout, function () use ($timeout, &$promise, $reject, $uri): void { $reject(new \RuntimeException( 'Connection to ' . $uri . ' timed out after ' . $timeout . ' seconds (ETIMEDOUT)', \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110 diff --git a/src/RedisClient.php b/src/RedisClient.php index f923d0f..1710d11 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -6,7 +6,6 @@ use Clue\React\Redis\Io\StreamingClient; use Evenement\EventEmitter; use React\EventLoop\Loop; -use React\EventLoop\LoopInterface; use React\Promise\PromiseInterface; use React\Socket\ConnectorInterface; use React\Stream\Util; @@ -40,9 +39,6 @@ class RedisClient extends EventEmitter /** @var ?PromiseInterface */ private $promise = null; - /** @var LoopInterface */ - private $loop; - /** @var float */ private $idlePeriod = 0.001; @@ -58,12 +54,7 @@ class RedisClient extends EventEmitter /** @var array */ private $psubscribed = []; - /** - * @param string $url - * @param ?ConnectorInterface $connector - * @param ?LoopInterface $loop - */ - public function __construct($url, ConnectorInterface $connector = null, LoopInterface $loop = null) + public function __construct(string $url, ConnectorInterface $connector = null) { $args = []; \parse_str((string) \parse_url($url, \PHP_URL_QUERY), $args); @@ -72,8 +63,7 @@ public function __construct($url, ConnectorInterface $connector = null, LoopInte } $this->target = $url; - $this->loop = $loop ?: Loop::get(); - $this->factory = new Factory($this->loop, $connector); + $this->factory = new Factory($connector); } /** @@ -102,7 +92,7 @@ private function client(): PromiseInterface $this->subscribed = $this->psubscribed = []; if ($this->idleTimer !== null) { - $this->loop->cancelTimer($this->idleTimer); + Loop::cancelTimer($this->idleTimer); $this->idleTimer = null; } }); @@ -235,7 +225,7 @@ public function close(): void } if ($this->idleTimer !== null) { - $this->loop->cancelTimer($this->idleTimer); + Loop::cancelTimer($this->idleTimer); $this->idleTimer = null; } @@ -248,7 +238,7 @@ private function awake(): void ++$this->pending; if ($this->idleTimer !== null) { - $this->loop->cancelTimer($this->idleTimer); + Loop::cancelTimer($this->idleTimer); $this->idleTimer = null; } } @@ -258,7 +248,7 @@ private function idle(): void --$this->pending; if ($this->pending < 1 && $this->idlePeriod >= 0 && !$this->subscribed && !$this->psubscribed && $this->promise !== null) { - $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () { + $this->idleTimer = Loop::addTimer($this->idlePeriod, function () { assert($this->promise instanceof PromiseInterface); $this->promise->then(function (StreamingClient $redis) { $redis->close(); diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index c1fe42b..25bf829 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -10,7 +10,6 @@ class FunctionalTest extends TestCase { - /** @var string */ private $uri; diff --git a/tests/Io/FactoryStreamingClientTest.php b/tests/Io/FactoryStreamingClientTest.php index 669b069..fbc274a 100644 --- a/tests/Io/FactoryStreamingClientTest.php +++ b/tests/Io/FactoryStreamingClientTest.php @@ -6,6 +6,7 @@ use Clue\React\Redis\Io\StreamingClient; use Clue\Tests\React\Redis\TestCase; use PHPUnit\Framework\MockObject\MockObject; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\EventLoop\TimerInterface; use React\Promise\Deferred; @@ -16,34 +17,31 @@ class FactoryStreamingClientTest extends TestCase { - /** @var MockObject */ - private $loop; - /** @var MockObject */ private $connector; /** @var Factory */ private $factory; - public function setUp(): void - { - $this->loop = $this->createMock(LoopInterface::class); - $this->connector = $this->createMock(ConnectorInterface::class); + /** @var LoopInterface */ + public static $loop; - assert($this->loop instanceof LoopInterface); - assert($this->connector instanceof ConnectorInterface); - $this->factory = new Factory($this->loop, $this->connector); + public static function setUpBeforeClass(): void + { + self::$loop = Loop::get(); } - public function testConstructWithoutLoopAssignsLoopAutomatically(): void + public static function tearDownAfterClass(): void { - $factory = new Factory(); + Loop::set(self::$loop); + } - $ref = new \ReflectionProperty($factory, 'loop'); - $ref->setAccessible(true); - $loop = $ref->getValue($factory); + public function setUp(): void + { + $this->connector = $this->createMock(ConnectorInterface::class); - $this->assertInstanceOf(LoopInterface::class, $loop); + assert($this->connector instanceof ConnectorInterface); + $this->factory = new Factory($this->connector); } /** @@ -51,8 +49,7 @@ public function testConstructWithoutLoopAssignsLoopAutomatically(): void */ public function testCtor(): void { - assert($this->loop instanceof LoopInterface); - $this->factory = new Factory($this->loop); + $this->factory = new Factory(); } public function testWillConnectWithDefaultPort(): void @@ -87,6 +84,11 @@ public function testWillWriteSelectCommandIfTargetContainsPath(): void $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n"); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $this->factory->createClient('redis://127.0.0.1/demo'); } @@ -96,6 +98,11 @@ public function testWillWriteSelectCommandIfTargetContainsDbQueryParameter(): vo $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$1\r\n4\r\n"); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); $this->factory->createClient('redis://127.0.0.1?db=4'); } @@ -105,6 +112,11 @@ public function testWillWriteAuthCommandIfRedisUriContainsUserInfo(): void $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createClient('redis://hello:world@example.com'); } @@ -114,6 +126,11 @@ public function testWillWriteAuthCommandIfRedisUriContainsEncodedUserInfo(): voi $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n"); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createClient('redis://:h%40llo@example.com'); } @@ -123,6 +140,11 @@ public function testWillWriteAuthCommandIfTargetContainsPasswordQueryParameter() $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$6\r\nsecret\r\n"); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createClient('redis://example.com?password=secret'); } @@ -132,6 +154,11 @@ public function testWillWriteAuthCommandIfTargetContainsEncodedPasswordQueryPara $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n"); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(resolve($stream)); $this->factory->createClient('redis://example.com?password=h%40llo'); } @@ -141,6 +168,11 @@ public function testWillWriteAuthCommandIfRedissUriContainsUserInfo(): void $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $this->connector->expects($this->once())->method('connect')->with('tls://example.com:6379')->willReturn(resolve($stream)); $this->factory->createClient('rediss://hello:world@example.com'); } @@ -150,6 +182,11 @@ public function testWillWriteAuthCommandIfRedisUnixUriContainsPasswordQueryParam $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); $this->factory->createClient('redis+unix:///tmp/redis.sock?password=world'); } @@ -168,6 +205,11 @@ public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo(): void $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n"); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); $this->factory->createClient('redis+unix://hello:world@/tmp/redis.sock'); } @@ -275,6 +317,11 @@ public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter $stream = $this->createMock(ConnectionInterface::class); $stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n"); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(resolve($stream)); $this->factory->createClient('redis+unix:///tmp/redis.sock?db=demo'); } @@ -584,8 +631,12 @@ public function testCancelWillCloseConnectionWhenConnectionWaitsForSelect(): voi public function testCreateClientWithTimeoutParameterWillStartTimerAndRejectOnExplicitTimeout(): void { + $loop = $this->createMock(LoopInterface::class); + assert($loop instanceof LoopInterface); + Loop::set($loop); + $timeout = null; - $this->loop->expects($this->once())->method('addTimer')->with(0, $this->callback(function ($cb) use (&$timeout) { + $loop->expects($this->once())->method('addTimer')->with(0, $this->callback(function ($cb) use (&$timeout) { $timeout = $cb; return true; })); @@ -613,7 +664,11 @@ public function testCreateClientWithTimeoutParameterWillStartTimerAndRejectOnExp public function testCreateClientWithNegativeTimeoutParameterWillNotStartTimer(): void { - $this->loop->expects($this->never())->method('addTimer'); + $loop = $this->createMock(LoopInterface::class); + assert($loop instanceof LoopInterface); + Loop::set($loop); + + $loop->expects($this->never())->method('addTimer'); $deferred = new Deferred(); $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn($deferred->promise()); @@ -623,7 +678,10 @@ public function testCreateClientWithNegativeTimeoutParameterWillNotStartTimer(): public function testCreateClientWithoutTimeoutParameterWillStartTimerWithDefaultTimeoutFromIni(): void { - $this->loop->expects($this->once())->method('addTimer')->with(42, $this->anything()); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer')->with(42, $this->anything()); + assert($loop instanceof LoopInterface); + Loop::set($loop); $deferred = new Deferred(); $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn($deferred->promise()); @@ -637,9 +695,12 @@ public function testCreateClientWithoutTimeoutParameterWillStartTimerWithDefault public function testCreateClientWillCancelTimerWhenConnectionResolves(): void { + $loop = $this->createMock(LoopInterface::class); $timer = $this->createMock(TimerInterface::class); - $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); - $this->loop->expects($this->once())->method('cancelTimer')->with($timer); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + assert($loop instanceof LoopInterface); + Loop::set($loop); $deferred = new Deferred(); $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:6379')->willReturn($deferred->promise()); @@ -652,9 +713,12 @@ public function testCreateClientWillCancelTimerWhenConnectionResolves(): void public function testCreateClientWillCancelTimerWhenConnectionRejects(): void { + $loop = $this->createMock(LoopInterface::class); $timer = $this->createMock(TimerInterface::class); - $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); - $this->loop->expects($this->once())->method('cancelTimer')->with($timer); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + assert($loop instanceof LoopInterface); + Loop::set($loop); $deferred = new Deferred(); $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:6379')->willReturn($deferred->promise()); diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 8fc8bab..6bfdb7e 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -6,6 +6,7 @@ use Clue\React\Redis\Io\Factory; use Clue\React\Redis\Io\StreamingClient; use PHPUnit\Framework\MockObject\MockObject; +use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\EventLoop\TimerInterface; use React\Promise\Deferred; @@ -16,19 +17,27 @@ class RedisClientTest extends TestCase /** @var MockObject */ private $factory; - /** @var MockObject */ - private $loop; - /** @var RedisClient */ private $redis; + /** @var Loopinterface */ + public static $loop; + + public static function setUpBeforeClass(): void + { + self::$loop = Loop::get(); + } + + public static function tearDownAfterClass(): void + { + Loop::set(self::$loop); + } + public function setUp(): void { $this->factory = $this->createMock(Factory::class); - $this->loop = $this->createMock(LoopInterface::class); - assert($this->loop instanceof LoopInterface); - $this->redis = new RedisClient('localhost', null, $this->loop); + $this->redis = new RedisClient('localhost'); $ref = new \ReflectionProperty($this->redis, 'factory'); $ref->setAccessible(true); @@ -40,7 +49,10 @@ public function testPingWillCreateUnderlyingClientAndReturnPendingPromise(): voi $promise = new Promise(function () { }); $this->factory->expects($this->once())->method('createClient')->willReturn($promise); - $this->loop->expects($this->never())->method('addTimer'); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->never())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); $promise = $this->redis->ping(); @@ -64,7 +76,10 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); + assert($loop instanceof LoopInterface); + Loop::set($loop); $promise = $this->redis->ping(); $deferred->resolve($client); @@ -74,8 +89,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimerWithIdleTimeFromQueryParam(): void { - assert($this->loop instanceof LoopInterface); - $this->redis = new RedisClient('localhost?idle=10', null, $this->loop); + $this->redis = new RedisClient('localhost?idle=10'); $ref = new \ReflectionProperty($this->redis, 'factory'); $ref->setAccessible(true); @@ -87,7 +101,10 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->loop->expects($this->once())->method('addTimer')->with(10.0, $this->anything()); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer')->with(10.0, $this->anything()); + assert($loop instanceof LoopInterface); + Loop::set($loop); $promise = $this->redis->ping(); $deferred->resolve($client); @@ -97,8 +114,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartIdleTimerWhenIdleParamIsNegative(): void { - assert($this->loop instanceof LoopInterface); - $this->redis = new RedisClient('localhost?idle=-1', null, $this->loop); + $this->redis = new RedisClient('localhost?idle=-1'); $ref = new \ReflectionProperty($this->redis, 'factory'); $ref->setAccessible(true); @@ -110,7 +126,10 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartId $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->loop->expects($this->never())->method('addTimer'); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->never())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); $promise = $this->redis->ping(); $deferred->resolve($client); @@ -127,7 +146,10 @@ public function testPingWillRejectWhenUnderlyingClientRejectsPingAndStartIdleTim $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); - $this->loop->expects($this->once())->method('addTimer'); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); $promise = $this->redis->ping(); $deferred->resolve($client); @@ -225,7 +247,10 @@ public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves(): v $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); - $this->loop->expects($this->never())->method('addTimer'); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->never())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); $this->redis->ping(); $this->redis->ping(); @@ -243,9 +268,12 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); + $loop = $this->createMock(LoopInterface::class); $timer = $this->createMock(TimerInterface::class); - $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); - $this->loop->expects($this->once())->method('cancelTimer')->with($timer); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + assert($loop instanceof LoopInterface); + Loop::set($loop); $this->redis->ping(); $deferred->resolve(null); @@ -260,12 +288,15 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutC $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); + $loop = $this->createMock(LoopInterface::class); $timeout = null; $timer = $this->createMock(TimerInterface::class); - $this->loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { + $loop->expects($this->once())->method('addTimer')->with($this->anything(), $this->callback(function ($cb) use (&$timeout) { $timeout = $cb; return true; }))->willReturn($timer); + assert($loop instanceof LoopInterface); + Loop::set($loop); $this->redis->on('close', $this->expectCallableNever()); @@ -341,9 +372,12 @@ public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); + $loop = $this->createMock(LoopInterface::class); $timer = $this->createMock(TimerInterface::class); - $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); - $this->loop->expects($this->once())->method('cancelTimer')->with($timer); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + assert($loop instanceof LoopInterface); + Loop::set($loop); $this->redis->ping(); $deferred->resolve(null); @@ -362,9 +396,12 @@ public function testCloseAfterPingRejectsWillEmitClose(): void $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); + $loop = $this->createMock(LoopInterface::class); $timer = $this->createMock(TimerInterface::class); - $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); - $this->loop->expects($this->once())->method('cancelTimer')->with($timer); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + assert($loop instanceof LoopInterface); + Loop::set($loop); $this->redis->ping()->then(null, function () { $this->redis->close(); @@ -467,9 +504,12 @@ public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnect $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); + $loop = $this->createMock(LoopInterface::class); $timer = $this->createMock(TimerInterface::class); - $this->loop->expects($this->once())->method('addTimer')->willReturn($timer); - $this->loop->expects($this->once())->method('cancelTimer')->with($timer); + $loop->expects($this->once())->method('addTimer')->willReturn($timer); + $loop->expects($this->once())->method('cancelTimer')->with($timer); + assert($loop instanceof LoopInterface); + Loop::set($loop); $this->redis->on('close', $this->expectCallableNever()); @@ -558,7 +598,10 @@ public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAnd $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); - $this->loop->expects($this->never())->method('addTimer'); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->never())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); $promise = $this->redis->subscribe('foo'); $this->assertTrue(is_callable($subscribeHandler)); @@ -587,7 +630,10 @@ public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientReso $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); - $this->loop->expects($this->once())->method('addTimer'); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->once())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); $promise = $this->redis->subscribe('foo'); $this->assertTrue(is_callable($subscribeHandler)); @@ -617,7 +663,10 @@ public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResp $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); - $this->loop->expects($this->never())->method('addTimer'); + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->never())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); $promise = $this->redis->blpop('list'); From a4d4773e01f0d68549c75871e18e2b42ca2188da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 30 Jun 2024 23:45:57 +0200 Subject: [PATCH 54/65] Improve PHP 8.4+ support by avoiding implicitly nullable types --- composer.json | 8 ++++---- src/Io/Factory.php | 2 +- src/RedisClient.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 007b36f..f2d104f 100644 --- a/composer.json +++ b/composer.json @@ -12,16 +12,16 @@ ], "require": { "php": ">=7.1", - "clue/redis-protocol": "0.3.*", + "clue/redis-protocol": "^0.3.2", "evenement/evenement": "^3.0 || ^2.0 || ^1.0", "react/event-loop": "^1.2", - "react/promise": "^3", - "react/socket": "^1.15" + "react/promise": "^3.2", + "react/socket": "^1.16" }, "require-dev": { "phpstan/phpstan": "1.10.15 || 1.4.10", "phpunit/phpunit": "^9.6 || ^7.5", - "react/async": "^4.2 || ^3.2" + "react/async": "^4.3 || ^3.2" }, "autoload": { "psr-4": { diff --git a/src/Io/Factory.php b/src/Io/Factory.php index 196ee0d..d920fd6 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -27,7 +27,7 @@ class Factory * @param ?ConnectorInterface $connector * @param ?ProtocolFactory $protocol */ - public function __construct(ConnectorInterface $connector = null, ProtocolFactory $protocol = null) + public function __construct(?ConnectorInterface $connector = null, ?ProtocolFactory $protocol = null) { $this->connector = $connector ?: new Connector(); $this->protocol = $protocol ?: new ProtocolFactory(); diff --git a/src/RedisClient.php b/src/RedisClient.php index 1710d11..8f984b0 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -54,7 +54,7 @@ class RedisClient extends EventEmitter /** @var array */ private $psubscribed = []; - public function __construct(string $url, ConnectorInterface $connector = null) + public function __construct(string $url, ?ConnectorInterface $connector = null) { $args = []; \parse_str((string) \parse_url($url, \PHP_URL_QUERY), $args); From c3a869c528e107259bf5d1055f2f80b124f0d31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 26 Nov 2024 10:12:52 +0100 Subject: [PATCH 55/65] Minor documentation improvements --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0bf684a..bc30f7b 100644 --- a/README.md +++ b/README.md @@ -242,8 +242,8 @@ library is currently limited to single arguments for each of these methods in order to match exactly one response to each command request. As an alternative, the methods can simply be invoked multiple times with one argument each. -Additionally, can listen for the following PubSub events to get notifications -about subscribed/unsubscribed channels and patterns: +Additionally, you can listen for the following PubSub events to get +notifications about subscribed/unsubscribed channels and patterns: ```php $redis->on('subscribe', function (string $channel, int $total) { @@ -261,7 +261,7 @@ $redis->on('punsubscribe', function (string $pattern, int $total) { ``` When the underlying connection is lost, the `unsubscribe` and `punsubscribe` events -will be invoked automatically. This gives you control over re-subscribing to the +will be invoked automatically. This gives you control over re-subscribing to any channels and patterns as appropriate. ## API @@ -319,7 +319,7 @@ will not have to wait for an actual underlying connection. #### __construct() -The `new RedisClient(string $url, ConnectorInterface $connector = null, LoopInterface $loop = null)` constructor can be used to +The `new RedisClient(string $url, ConnectorInterface $connector = null)` constructor can be used to create a new `RedisClient` instance. The `$url` can be given in the From 299dd93d790ba837e1fd1071fc6eb71c47a33533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Fri, 27 Dec 2024 16:34:40 +0100 Subject: [PATCH 56/65] Make `RedisClient` constructor throw if given `$uri` is invalid --- README.md | 4 +- src/Io/Factory.php | 14 +---- src/RedisClient.php | 34 +++++++++-- tests/Io/FactoryStreamingClientTest.php | 31 ++-------- tests/RedisClientTest.php | 81 +++++++++++++++++++++++++ 5 files changed, 120 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index bc30f7b..b04b423 100644 --- a/README.md +++ b/README.md @@ -319,10 +319,10 @@ will not have to wait for an actual underlying connection. #### __construct() -The `new RedisClient(string $url, ConnectorInterface $connector = null)` constructor can be used to +The `new RedisClient(string $uri, ConnectorInterface $connector = null)` constructor can be used to create a new `RedisClient` instance. -The `$url` can be given in the +The `$uri` can be given in the [standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form `[redis[s]://][:auth@]host[:port][/db]`. You can omit the URI scheme and port if you're connecting to the default port 6379: diff --git a/src/Io/Factory.php b/src/Io/Factory.php index d920fd6..b66e30d 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -43,23 +43,15 @@ public function __construct(?ConnectorInterface $connector = null, ?ProtocolFact public function createClient(string $uri): PromiseInterface { // support `redis+unix://` scheme for Unix domain socket (UDS) paths - if (preg_match('/^(redis\+unix:\/\/(?:[^:]*:[^@]*@)?)(.+?)?$/', $uri, $match)) { + if (preg_match('/^(redis\+unix:\/\/(?:[^@]*@)?)(.+)$/', $uri, $match)) { $parts = parse_url($match[1] . 'localhost/' . $match[2]); } else { - if (strpos($uri, '://') === false) { - $uri = 'redis://' . $uri; - } - $parts = parse_url($uri); } $uri = preg_replace(['/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'], '$1***$2', $uri); - if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], ['redis', 'rediss', 'redis+unix'])) { - return reject(new \InvalidArgumentException( - 'Invalid Redis URI given (EINVAL)', - defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 - )); - } + assert(is_array($parts) && isset($parts['scheme'], $parts['host'])); + assert(in_array($parts['scheme'], ['redis', 'rediss', 'redis+unix'])); $args = []; parse_str($parts['query'] ?? '', $args); diff --git a/src/RedisClient.php b/src/RedisClient.php index 8f984b0..df4ebf5 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -28,7 +28,7 @@ class RedisClient extends EventEmitter { /** @var string */ - private $target; + private $uri; /** @var Factory */ private $factory; @@ -54,15 +54,39 @@ class RedisClient extends EventEmitter /** @var array */ private $psubscribed = []; - public function __construct(string $url, ?ConnectorInterface $connector = null) + /** + * @param string $uri + * @param ?ConnectorInterface $connector + * @throws \InvalidArgumentException if $uri is not a valid Redis URI + */ + public function __construct(string $uri, ?ConnectorInterface $connector = null) { + // support `redis+unix://` scheme for Unix domain socket (UDS) paths + if (preg_match('/^(redis\+unix:\/\/(?:[^@]*@)?)(.+)$/', $uri, $match)) { + $parts = parse_url($match[1] . 'localhost/' . $match[2]); + } else { + if (strpos($uri, '://') === false) { + $uri = 'redis://' . $uri; + } + + $parts = parse_url($uri); + } + + $uri = (string) preg_replace(['/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'], '$1***$2', $uri); + if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], ['redis', 'rediss', 'redis+unix'])) { + throw new \InvalidArgumentException( + 'Invalid Redis URI "' . $uri . '" (EINVAL)', + defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 + ); + } + $args = []; - \parse_str((string) \parse_url($url, \PHP_URL_QUERY), $args); + \parse_str($parts['query'] ?? '', $args); if (isset($args['idle'])) { $this->idlePeriod = (float)$args['idle']; } - $this->target = $url; + $this->uri = $uri; $this->factory = new Factory($connector); } @@ -75,7 +99,7 @@ private function client(): PromiseInterface return $this->promise; } - return $this->promise = $this->factory->createClient($this->target)->then(function (StreamingClient $redis) { + return $this->promise = $this->factory->createClient($this->uri)->then(function (StreamingClient $redis) { // connection completed => remember only until closed $redis->on('close', function () { $this->promise = null; diff --git a/tests/Io/FactoryStreamingClientTest.php b/tests/Io/FactoryStreamingClientTest.php index fbc274a..4a5ee19 100644 --- a/tests/Io/FactoryStreamingClientTest.php +++ b/tests/Io/FactoryStreamingClientTest.php @@ -55,7 +55,7 @@ public function testCtor(): void public function testWillConnectWithDefaultPort(): void { $this->connector->expects($this->once())->method('connect')->with('redis.example.com:6379')->willReturn(reject(new \RuntimeException())); - $promise = $this->factory->createClient('redis.example.com'); + $promise = $this->factory->createClient('redis://redis.example.com'); $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection } @@ -63,7 +63,7 @@ public function testWillConnectWithDefaultPort(): void public function testWillConnectToLocalhost(): void { $this->connector->expects($this->once())->method('connect')->with('localhost:1337')->willReturn(reject(new \RuntimeException())); - $promise = $this->factory->createClient('localhost:1337'); + $promise = $this->factory->createClient('redis://localhost:1337'); $promise->then(null, $this->expectCallableOnce()); // avoid reporting unhandled rejection } @@ -74,7 +74,7 @@ public function testWillResolveIfConnectorResolves(): void $stream->expects($this->never())->method('write'); $this->connector->expects($this->once())->method('connect')->willReturn(resolve($stream)); - $promise = $this->factory->createClient('localhost'); + $promise = $this->factory->createClient('redis://localhost'); $this->expectPromiseResolve($promise); } @@ -483,23 +483,6 @@ public function testWillRejectIfConnectorRejects(): void )); } - public function testWillRejectIfTargetIsInvalid(): void - { - $promise = $this->factory->createClient('http://invalid target'); - - $promise->then(null, $this->expectCallableOnceWith( - $this->logicalAnd( - $this->isInstanceOf(\InvalidArgumentException::class), - $this->callback(function (\InvalidArgumentException $e) { - return $e->getMessage() === 'Invalid Redis URI given (EINVAL)'; - }), - $this->callback(function (\InvalidArgumentException $e) { - return $e->getCode() === (defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22); - }) - ) - )); - } - public function testCancelWillRejectPromise(): void { $promise = new \React\Promise\Promise(function () { }); @@ -516,10 +499,6 @@ public function testCancelWillRejectPromise(): void public function provideUris(): array { return [ - [ - 'localhost', - 'redis://localhost' - ], [ 'redis://localhost', 'redis://localhost' @@ -705,7 +684,7 @@ public function testCreateClientWillCancelTimerWhenConnectionResolves(): void $deferred = new Deferred(); $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:6379')->willReturn($deferred->promise()); - $promise = $this->factory->createClient('127.0.0.1'); + $promise = $this->factory->createClient('redis://127.0.0.1'); $promise->then($this->expectCallableOnce()); $deferred->resolve($this->createMock(ConnectionInterface::class)); @@ -723,7 +702,7 @@ public function testCreateClientWillCancelTimerWhenConnectionRejects(): void $deferred = new Deferred(); $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:6379')->willReturn($deferred->promise()); - $promise = $this->factory->createClient('127.0.0.1'); + $promise = $this->factory->createClient('redis://127.0.0.1'); $promise->then(null, $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 6bfdb7e..273c655 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -44,6 +44,67 @@ public function setUp(): void $ref->setValue($this->redis, $this->factory); } + public static function provideInvalidUris(): \Generator + { + yield [ + '', + 'redis://' + ]; + yield [ + 'localhost:100000', + 'redis://localhost:100000' + ]; + yield [ + 'tcp://localhost', + 'tcp://localhost' + ]; + yield [ + 'redis://', + 'redis://' + ]; + yield [ + 'redis+unix://', + 'redis+unix://' + ]; + yield [ + 'user@localhost:100000', + 'redis://user@localhost:100000' + ]; + yield [ + ':pass@localhost:100000', + 'redis://:***@localhost:100000' + ]; + yield [ + 'user:@localhost:100000', + 'redis://user:***@localhost:100000' + ]; + yield [ + 'user:pass@localhost:100000', + 'redis://user:***@localhost:100000' + ]; + yield [ + 'localhost:100000?password=secret', + 'redis://localhost:100000?password=***' + ]; + yield [ + 'user@', + 'redis://user@' + ]; + yield [ + 'user:pass@', + 'redis://user:***@' + ]; + } + + /** @dataProvider provideInvalidUris() */ + public function testCtorWithInvalidUriThrows(string $uri, string $message): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid Redis URI "' . $message . '" (EINVAL)'); + $this->expectExceptionCode(defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22); + new RedisClient($uri); + } + public function testPingWillCreateUnderlyingClientAndReturnPendingPromise(): void { $promise = new Promise(function () { }); @@ -59,6 +120,26 @@ public function testPingWillCreateUnderlyingClientAndReturnPendingPromise(): voi $promise->then($this->expectCallableNever()); } + public function testPingWithUnixUriWillCreateUnderlyingClientAndReturnPendingPromise(): void + { + $this->redis = new RedisClient('redis+unix:///tmp/redis.sock'); + $ref = new \ReflectionProperty($this->redis, 'factory'); + $ref->setAccessible(true); + $ref->setValue($this->redis, $this->factory); + + $promise = new Promise(function () { }); + $this->factory->expects($this->once())->method('createClient')->with('redis+unix:///tmp/redis.sock')->willReturn($promise); + + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->never())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + + $promise = $this->redis->ping(); + + $promise->then($this->expectCallableNever()); + } + public function testPingTwiceWillCreateOnceUnderlyingClient(): void { $promise = new Promise(function () { }); From 1c1e0bab913377c79b9317d8ea2b8b9891e9945b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 22 Dec 2024 13:11:12 +0100 Subject: [PATCH 57/65] Run tests on PHP 8.4 and update test environment --- .github/workflows/ci.yml | 6 ++++-- composer.json | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf56001..6365a34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,10 +7,11 @@ on: jobs: PHPUnit: name: PHPUnit (PHP ${{ matrix.php }}) - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: php: + - 8.4 - 8.3 - 8.2 - 8.1 @@ -41,10 +42,11 @@ jobs: PHPStan: name: PHPStan (PHP ${{ matrix.php }}) - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: php: + - 8.4 - 8.3 - 8.2 - 8.1 diff --git a/composer.json b/composer.json index f2d104f..d14527c 100644 --- a/composer.json +++ b/composer.json @@ -19,17 +19,17 @@ "react/socket": "^1.16" }, "require-dev": { - "phpstan/phpstan": "1.10.15 || 1.4.10", + "phpstan/phpstan": "1.12.13 || 1.4.10", "phpunit/phpunit": "^9.6 || ^7.5", "react/async": "^4.3 || ^3.2" }, "autoload": { - "psr-4": { + "psr-4": { "Clue\\React\\Redis\\": "src/" } }, "autoload-dev": { - "psr-4": { + "psr-4": { "Clue\\Tests\\React\\Redis\\": "tests/" } } From 194370df1fbe4629848b3047900cff4d9754a19d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 2 Jan 2024 19:27:09 +0100 Subject: [PATCH 58/65] Add new internal `callAsync()` API --- phpstan.neon.dist | 1 - src/Io/Factory.php | 10 +++++--- src/Io/StreamingClient.php | 13 +++++----- src/RedisClient.php | 10 ++++---- tests/Io/StreamingClientTest.php | 28 ++++++++++----------- tests/RedisClientTest.php | 42 ++++++++++++++++---------------- 6 files changed, 52 insertions(+), 52 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index be4a66a..f61b9e6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,4 +9,3 @@ parameters: ignoreErrors: # ignore undefined methods due to magic `__call()` method - '/^Call to an undefined method Clue\\React\\Redis\\RedisClient::.+\(\)\.$/' - - '/^Call to an undefined method Clue\\React\\Redis\\Io\\StreamingClient::.+\(\)\.$/' diff --git a/src/Io/Factory.php b/src/Io/Factory.php index b66e30d..5a3d692 100644 --- a/src/Io/Factory.php +++ b/src/Io/Factory.php @@ -96,12 +96,13 @@ public function createClient(string $uri): PromiseInterface // use `?password=secret` query or `user:secret@host` password form URL if (isset($args['password']) || isset($parts['pass'])) { $pass = $args['password'] ?? rawurldecode($parts['pass']); // @phpstan-ignore-line + \assert(\is_string($pass)); $promise = $promise->then(function (StreamingClient $redis) use ($pass, $uri) { - return $redis->auth($pass)->then( + return $redis->callAsync('auth', $pass)->then( function () use ($redis) { return $redis; }, - function (\Exception $e) use ($redis, $uri) { + function (\Throwable $e) use ($redis, $uri) { $redis->close(); $const = ''; @@ -124,12 +125,13 @@ function (\Exception $e) use ($redis, $uri) { // use `?db=1` query or `/1` path (skip first slash) if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) { $db = $args['db'] ?? substr($parts['path'], 1); // @phpstan-ignore-line + \assert(\is_string($db)); $promise = $promise->then(function (StreamingClient $redis) use ($db, $uri) { - return $redis->select($db)->then( + return $redis->callAsync('select', $db)->then( function () use ($redis) { return $redis; }, - function (\Exception $e) use ($redis, $uri) { + function (\Throwable $e) use ($redis, $uri) { $redis->close(); $const = ''; diff --git a/src/Io/StreamingClient.php b/src/Io/StreamingClient.php index bb7689f..0d5b2f7 100644 --- a/src/Io/StreamingClient.php +++ b/src/Io/StreamingClient.php @@ -72,15 +72,14 @@ public function __construct(DuplexStreamInterface $stream, ParserInterface $pars } /** - * @param string[] $args * @return PromiseInterface */ - public function __call(string $name, array $args): PromiseInterface + public function callAsync(string $command, string ...$args): PromiseInterface { $request = new Deferred(); $promise = $request->promise(); - $name = strtolower($name); + $command = strtolower($command); // special (p)(un)subscribe commands only accept a single parameter and have custom response logic applied static $pubsubs = ['subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe']; @@ -90,22 +89,22 @@ public function __call(string $name, array $args): PromiseInterface 'Connection ' . ($this->closed ? 'closed' : 'closing'). ' (ENOTCONN)', defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107 )); - } elseif (count($args) !== 1 && in_array($name, $pubsubs)) { + } elseif (count($args) !== 1 && in_array($command, $pubsubs)) { $request->reject(new \InvalidArgumentException( 'PubSub commands limited to single argument (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 )); - } elseif ($name === 'monitor') { + } elseif ($command === 'monitor') { $request->reject(new \BadMethodCallException( 'MONITOR command explicitly not supported (ENOTSUP)', defined('SOCKET_ENOTSUP') ? SOCKET_ENOTSUP : (defined('SOCKET_EOPNOTSUPP') ? SOCKET_EOPNOTSUPP : 95) )); } else { - $this->stream->write($this->serializer->getRequestMessage($name, $args)); + $this->stream->write($this->serializer->getRequestMessage($command, $args)); $this->requests []= $request; } - if (in_array($name, $pubsubs)) { + if (in_array($command, $pubsubs)) { $promise->then(function (array $array) { $first = array_shift($array); diff --git a/src/RedisClient.php b/src/RedisClient.php index df4ebf5..5cedb30 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -178,17 +178,17 @@ public function __call(string $name, array $args): PromiseInterface )); } - return $this->client()->then(function (StreamingClient $redis) use ($name, $args) { + return $this->client()->then(function (StreamingClient $redis) use ($name, $args): PromiseInterface { $this->awake(); - assert(\is_callable([$redis, $name])); // @phpstan-ignore-next-line - return \call_user_func_array([$redis, $name], $args)->then( + return $redis->callAsync($name, ...$args)->then( function ($result) { $this->idle(); return $result; }, - function (\Exception $error) { + function (\Throwable $e) { + \assert($e instanceof \Exception); $this->idle(); - throw $error; + throw $e; } ); }); diff --git a/tests/Io/StreamingClientTest.php b/tests/Io/StreamingClientTest.php index 241a76d..3c550eb 100644 --- a/tests/Io/StreamingClientTest.php +++ b/tests/Io/StreamingClientTest.php @@ -46,7 +46,7 @@ public function testSending(): void $this->serializer->expects($this->once())->method('getRequestMessage')->with($this->equalTo('ping'))->will($this->returnValue('message')); $this->stream->expects($this->once())->method('write')->with($this->equalTo('message')); - $this->redis->ping(); + $this->redis->callAsync('ping'); } public function testClosingClientEmitsEvent(): void @@ -121,7 +121,7 @@ public function testPingPong(): void { $this->serializer->expects($this->once())->method('getRequestMessage')->with($this->equalTo('ping')); - $promise = $this->redis->ping(); + $promise = $this->redis->callAsync('ping'); $this->redis->handleMessage(new BulkReply('PONG')); @@ -131,7 +131,7 @@ public function testPingPong(): void public function testMonitorCommandIsNotSupported(): void { - $promise = $this->redis->monitor(); + $promise = $this->redis->callAsync('monitor'); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -148,7 +148,7 @@ public function testMonitorCommandIsNotSupported(): void public function testErrorReply(): void { - $promise = $this->redis->invalid(); + $promise = $this->redis->callAsync('invalid'); $err = new ErrorReply("ERR unknown command 'invalid'"); $this->redis->handleMessage($err); @@ -158,7 +158,7 @@ public function testErrorReply(): void public function testClosingClientRejectsAllRemainingRequests(): void { - $promise = $this->redis->ping(); + $promise = $this->redis->callAsync('ping'); $this->redis->close(); $promise->then(null, $this->expectCallableOnceWith( @@ -183,7 +183,7 @@ public function testClosingStreamRejectsAllRemainingRequests(): void assert($this->serializer instanceof SerializerInterface); $this->redis = new StreamingClient($stream, $this->parser, $this->serializer); - $promise = $this->redis->ping(); + $promise = $this->redis->callAsync('ping'); $stream->close(); $promise->then(null, $this->expectCallableOnceWith( @@ -201,9 +201,9 @@ public function testClosingStreamRejectsAllRemainingRequests(): void public function testEndingClientRejectsAllNewRequests(): void { - $this->redis->ping(); + $this->redis->callAsync('ping'); $this->redis->end(); - $promise = $this->redis->ping(); + $promise = $this->redis->callAsync('ping'); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -221,7 +221,7 @@ public function testEndingClientRejectsAllNewRequests(): void public function testClosedClientRejectsAllNewRequests(): void { $this->redis->close(); - $promise = $this->redis->ping(); + $promise = $this->redis->callAsync('ping'); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -250,7 +250,7 @@ public function testEndingBusyClosesClientWhenNotBusyAnymore(): void ++$closed; }); - $promise = $this->redis->ping(); + $promise = $this->redis->callAsync('ping'); $this->assertEquals(0, $closed); $this->redis->end(); @@ -277,7 +277,7 @@ public function testReceivingUnexpectedMessageThrowsException(): void public function testPubsubSubscribe(): StreamingClient { - $promise = $this->redis->subscribe('test'); + $promise = $this->redis->callAsync('subscribe', 'test'); $this->expectPromiseResolve($promise); $this->redis->on('subscribe', $this->expectCallableOnce()); @@ -291,7 +291,7 @@ public function testPubsubSubscribe(): StreamingClient */ public function testPubsubPatternSubscribe(StreamingClient $client): StreamingClient { - $promise = $client->psubscribe('demo_*'); + $promise = $client->callAsync('psubscribe', 'demo_*'); $this->expectPromiseResolve($promise); $client->on('psubscribe', $this->expectCallableOnce()); @@ -311,7 +311,7 @@ public function testPubsubMessage(StreamingClient $client): void public function testSubscribeWithMultipleArgumentsRejects(): void { - $promise = $this->redis->subscribe('a', 'b'); + $promise = $this->redis->callAsync('subscribe', 'a', 'b'); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( @@ -328,7 +328,7 @@ public function testSubscribeWithMultipleArgumentsRejects(): void public function testUnsubscribeWithoutArgumentsRejects(): void { - $promise = $this->redis->unsubscribe(); + $promise = $this->redis->callAsync('unsubscribe'); $promise->then(null, $this->expectCallableOnceWith( $this->logicalAnd( diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 273c655..4fd77a6 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -152,7 +152,7 @@ public function testPingTwiceWillCreateOnceUnderlyingClient(): void public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleTimer(): void { $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); + $client->expects($this->once())->method('callAsync')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); @@ -177,7 +177,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndStartIdleT $ref->setValue($this->redis, $this->factory); $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); + $client->expects($this->once())->method('callAsync')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); @@ -202,7 +202,7 @@ public function testPingWillResolveWhenUnderlyingClientResolvesPingAndNotStartId $ref->setValue($this->redis, $this->factory); $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); + $client->expects($this->once())->method('callAsync')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); @@ -222,7 +222,7 @@ public function testPingWillRejectWhenUnderlyingClientRejectsPingAndStartIdleTim { $error = new \RuntimeException(); $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\reject($error)); + $client->expects($this->once())->method('callAsync')->with('ping')->willReturn(\React\Promise\reject($error)); $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); @@ -277,7 +277,7 @@ public function testPingAfterPreviousUnderlyingClientAlreadyClosedWillCreateNewU { $closeHandler = null; $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); + $client->expects($this->once())->method('callAsync')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->any())->method('on')->withConsecutive( ['close', $this->callback(function ($arg) use (&$closeHandler) { $closeHandler = $arg; @@ -321,7 +321,7 @@ public function testPingAfterPingWillNotStartIdleTimerWhenFirstPingResolves(): v { $deferred = new Deferred(); $client = $this->createMock(StreamingClient::class); - $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls( + $client->expects($this->exactly(2))->method('callAsync')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) ); @@ -342,7 +342,7 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts { $deferred = new Deferred(); $client = $this->createMock(StreamingClient::class); - $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls( + $client->expects($this->exactly(2))->method('callAsync')->willReturnOnConsecutiveCalls( $deferred->promise(), new Promise(function () { }) ); @@ -364,7 +364,7 @@ public function testPingAfterPingWillStartAndCancelIdleTimerWhenSecondPingStarts public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutCloseEvent(): void { $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); + $client->expects($this->once())->method('callAsync')->willReturn(\React\Promise\resolve(null)); $client->expects($this->once())->method('close'); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); @@ -433,7 +433,7 @@ public function testCloseAfterPingWillEmitCloseWithoutErrorWhenUnderlyingClientC public function testCloseAfterPingWillCloseUnderlyingClientConnectionWhenAlreadyResolved(): void { $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); + $client->expects($this->once())->method('callAsync')->willReturn(\React\Promise\resolve(null)); $client->expects($this->once())->method('close'); $deferred = new Deferred(); @@ -448,7 +448,7 @@ public function testCloseAfterPingWillCancelIdleTimerWhenPingIsAlreadyResolved() { $deferred = new Deferred(); $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); + $client->expects($this->once())->method('callAsync')->willReturn($deferred->promise()); $client->expects($this->once())->method('close'); $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); @@ -469,7 +469,7 @@ public function testCloseAfterPingRejectsWillEmitClose(): void { $deferred = new Deferred(); $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); + $client->expects($this->once())->method('callAsync')->willReturn($deferred->promise()); $client->expects($this->once())->method('close')->willReturnCallback(function () use ($client) { assert($client instanceof StreamingClient); $client->emit('close'); @@ -500,7 +500,7 @@ public function testEndWillCloseClientIfUnderlyingConnectionIsNotPending(): void public function testEndAfterPingWillEndUnderlyingClient(): void { $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); + $client->expects($this->once())->method('callAsync')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->once())->method('end'); $deferred = new Deferred(); @@ -515,7 +515,7 @@ public function testEndAfterPingWillCloseClientWhenUnderlyingClientEmitsClose(): { $closeHandler = null; $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->with('ping')->willReturn(\React\Promise\resolve('PONG')); + $client->expects($this->once())->method('callAsync')->with('ping')->willReturn(\React\Promise\resolve('PONG')); $client->expects($this->once())->method('end'); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$closeHandler) { if ($event === 'close') { @@ -541,7 +541,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError(): void $error = new \RuntimeException(); $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); + $client->expects($this->once())->method('callAsync')->willReturn(\React\Promise\resolve(null)); $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); @@ -557,7 +557,7 @@ public function testEmitsNoErrorEventWhenUnderlyingClientEmitsError(): void public function testEmitsNoCloseEventWhenUnderlyingClientEmitsClose(): void { $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); + $client->expects($this->once())->method('callAsync')->willReturn(\React\Promise\resolve(null)); $deferred = new Deferred(); $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); @@ -575,7 +575,7 @@ public function testEmitsNoCloseEventButWillCancelIdleTimerWhenUnderlyingConnect $closeHandler = null; $client = $this->createMock(StreamingClient::class); $deferred = new Deferred(); - $client->expects($this->once())->method('__call')->willReturn($deferred->promise()); + $client->expects($this->once())->method('callAsync')->willReturn($deferred->promise()); $client->expects($this->any())->method('on')->withConsecutive( ['close', $this->callback(function ($arg) use (&$closeHandler) { $closeHandler = $arg; @@ -605,7 +605,7 @@ public function testEmitsMessageEventWhenUnderlyingClientEmitsMessageForPubSubCh { $messageHandler = null; $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->willReturn(\React\Promise\resolve(null)); + $client->expects($this->once())->method('callAsync')->willReturn(\React\Promise\resolve(null)); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$messageHandler) { if ($event === 'message') { $messageHandler = $callback; @@ -627,7 +627,7 @@ public function testEmitsUnsubscribeAndPunsubscribeEventsWhenUnderlyingClientClo { $allHandler = null; $client = $this->createMock(StreamingClient::class); - $client->expects($this->exactly(6))->method('__call')->willReturn(\React\Promise\resolve(null)); + $client->expects($this->exactly(6))->method('callAsync')->willReturn(\React\Promise\resolve(null)); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$allHandler) { if (!isset($allHandler[$event])) { $allHandler[$event] = $callback; @@ -670,7 +670,7 @@ public function testSubscribeWillResolveWhenUnderlyingClientResolvesSubscribeAnd $subscribeHandler = null; $deferred = new Deferred(); $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->with('subscribe')->willReturn($deferred->promise()); + $client->expects($this->once())->method('callAsync')->with('subscribe')->willReturn($deferred->promise()); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$subscribeHandler) { if ($event === 'subscribe' && $subscribeHandler === null) { $subscribeHandler = $callback; @@ -699,7 +699,7 @@ public function testUnsubscribeAfterSubscribeWillResolveWhenUnderlyingClientReso $deferredSubscribe = new Deferred(); $deferredUnsubscribe = new Deferred(); $client = $this->createMock(StreamingClient::class); - $client->expects($this->exactly(2))->method('__call')->willReturnOnConsecutiveCalls($deferredSubscribe->promise(), $deferredUnsubscribe->promise()); + $client->expects($this->exactly(2))->method('callAsync')->willReturnOnConsecutiveCalls($deferredSubscribe->promise(), $deferredUnsubscribe->promise()); $client->expects($this->any())->method('on')->willReturnCallback(function ($event, $callback) use (&$subscribeHandler, &$unsubscribeHandler) { if ($event === 'subscribe' && $subscribeHandler === null) { $subscribeHandler = $callback; @@ -734,7 +734,7 @@ public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResp $closeHandler = null; $deferred = new Deferred(); $client = $this->createMock(StreamingClient::class); - $client->expects($this->once())->method('__call')->with('blpop')->willReturn($deferred->promise()); + $client->expects($this->once())->method('callAsync')->with('blpop')->willReturn($deferred->promise()); $client->expects($this->any())->method('on')->withConsecutive( ['close', $this->callback(function ($arg) use (&$closeHandler) { $closeHandler = $arg; From e89ae33c277c6ae5e3ce60ee994761e1009f758e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Jan 2024 15:09:23 +0100 Subject: [PATCH 59/65] Add new public `callAsync()` method --- README.md | 43 +++++++++++++++++++++++++++++++++- examples/cli.php | 16 ++++++------- src/RedisClient.php | 57 +++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b04b423..b1c24fe 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ It enables you to set and query its data or use its PubSub topics to react to in * [RedisClient](#redisclient) * [__construct()](#__construct) * [__call()](#__call) + * [callAsync()](#callasync) * [end()](#end) * [close()](#close) * [error event](#error-event) @@ -124,7 +125,8 @@ Each method call matches the respective [Redis command](https://redis.io/command For example, the `$redis->get()` method will invoke the [`GET` command](https://redis.io/commands/get). All [Redis commands](https://redis.io/commands) are automatically available as -public methods via the magic [`__call()` method](#__call). +public methods via the magic [`__call()` method](#__call) or through the more +explicit [`callAsync()` method]. Listing all available commands is out of scope here, please refer to the [Redis command reference](https://redis.io/commands). @@ -432,6 +434,8 @@ $redis->get($key)->then(function (?string $value) { All [Redis commands](https://redis.io/commands) are automatically available as public methods via this magic `__call()` method. +Note that some static analysis tools may not understand this magic method, so +you may also the [`callAsync()` method](#callasync) as a more explicit alternative. Listing all available commands is out of scope here, please refer to the [Redis command reference](https://redis.io/commands). @@ -445,6 +449,43 @@ Each of these commands supports async operation and returns a [Promise](#promise that eventually *fulfills* with its *results* on success or *rejects* with an `Exception` on error. See also [promises](#promises) for more details. +#### callAsync() + +The `callAsync(string $command, string ...$args): PromiseInterface` method can be used to +invoke a Redis command. + +```php +$redis->callAsync('GET', 'name')->then(function (?string $name): void { + echo 'Name: ' . ($name ?? 'Unknown') . PHP_EOL; +}, function (Throwable $e): void { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +The `string $command` parameter can be any valid Redis command. All +[Redis commands](https://redis.io/commands/) are available through this +method. As an alternative, you may also use the magic +[`__call()` method](#__call), but note that not all static analysis tools +may understand this magic method. Listing all available commands is out +of scope here, please refer to the +[Redis command reference](https://redis.io/commands). + +The optional `string ...$args` parameter can be used to pass any +additional arguments to the Redis command. Some commands may require or +support additional arguments that this method will simply forward as is. +Internally, Redis requires all arguments to be coerced to `string` values, +but you may also rely on PHP's type-juggling semantics and pass `int` or +`float` values: + +```php +$redis->callAsync('SET', 'name', 'Alice', 'EX', 600); +``` + +This method supports async operation and returns a [Promise](#promises) +that eventually *fulfills* with its *results* on success or *rejects* +with an `Exception` on error. See also [promises](#promises) for more +details. + #### end() The `end():void` method can be used to diff --git a/examples/cli.php b/examples/cli.php index 23c9e55..195c737 100644 --- a/examples/cli.php +++ b/examples/cli.php @@ -23,20 +23,20 @@ return; } - $params = explode(' ', $line); - $method = array_shift($params); - - assert(is_callable([$redis, $method])); - $promise = $redis->$method(...$params); + $args = explode(' ', $line); + $command = strtolower(array_shift($args)); // special method such as end() / close() called - if (!$promise instanceof React\Promise\PromiseInterface) { + if (in_array($command, ['end', 'close'])) { + $redis->$command(); return; } - $promise->then(function ($data) { + $promise = $redis->callAsync($command, ...$args); + + $promise->then(function ($data): void { echo '# reply: ' . json_encode($data) . PHP_EOL; - }, function ($e) { + }, function (Throwable $e): void { echo '# error reply: ' . $e->getMessage() . PHP_EOL; }); }); diff --git a/src/RedisClient.php b/src/RedisClient.php index 5cedb30..29d4854 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -160,16 +160,65 @@ private function client(): PromiseInterface } /** - * Invoke the given command and return a Promise that will be resolved when the request has been replied to + * Invoke the given command and return a Promise that will be resolved when the command has been replied to * * This is a magic method that will be invoked when calling any redis - * command on this instance. + * command on this instance. See also `RedisClient::callAsync()`. * * @param string $name * @param string[] $args * @return PromiseInterface + * @see self::callAsync() */ public function __call(string $name, array $args): PromiseInterface + { + return $this->callAsync($name, ...$args); + } + + /** + * Invoke a Redis command. + * + * For example, the [`GET` command](https://redis.io/commands/get) can be invoked + * like this: + * + * ```php + * $redis->callAsync('GET', 'name')->then(function (?string $name): void { + * echo 'Name: ' . ($name ?? 'Unknown') . PHP_EOL; + * }, function (Throwable $e): void { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * The `string $command` parameter can be any valid Redis command. All + * [Redis commands](https://redis.io/commands/) are available through this + * method. As an alternative, you may also use the magic + * [`__call()` method](#__call), but note that not all static analysis tools + * may understand this magic method. Listing all available commands is out + * of scope here, please refer to the + * [Redis command reference](https://redis.io/commands). + * + * The optional `string ...$args` parameter can be used to pass any + * additional arguments to the Redis command. Some commands may require or + * support additional arguments that this method will simply forward as is. + * Internally, Redis requires all arguments to be coerced to `string` values, + * but you may also rely on PHP's type-juggling semantics and pass `int` or + * `float` values: + * + * ```php + * $redis->callAsync('SET', 'name', 'Alice', 'EX', 600); + * ``` + * + * This method supports async operation and returns a [Promise](#promises) + * that eventually *fulfills* with its *results* on success or *rejects* + * with an `Exception` on error. See also [promises](#promises) for more + * details. + * + * @param string $command + * @param string ...$args + * @return PromiseInterface + * @throws void + */ + public function callAsync(string $command, string ...$args): PromiseInterface { if ($this->closed) { return reject(new \RuntimeException( @@ -178,9 +227,9 @@ public function __call(string $name, array $args): PromiseInterface )); } - return $this->client()->then(function (StreamingClient $redis) use ($name, $args): PromiseInterface { + return $this->client()->then(function (StreamingClient $redis) use ($command, $args): PromiseInterface { $this->awake(); - return $redis->callAsync($name, ...$args)->then( + return $redis->callAsync($command, ...$args)->then( function ($result) { $this->idle(); return $result; From fb494d107005cc488865db5e25868216b51615b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 30 Dec 2024 16:02:09 +0100 Subject: [PATCH 60/65] Fix `RedisClient` to pass correct password to internal `Factory` --- src/RedisClient.php | 2 +- tests/RedisClientTest.php | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/RedisClient.php b/src/RedisClient.php index 29d4854..688e584 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -72,8 +72,8 @@ public function __construct(string $uri, ?ConnectorInterface $connector = null) $parts = parse_url($uri); } - $uri = (string) preg_replace(['/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'], '$1***$2', $uri); if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], ['redis', 'rediss', 'redis+unix'])) { + $uri = (string) preg_replace(['/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'], '$1***$2', $uri); throw new \InvalidArgumentException( 'Invalid Redis URI "' . $uri . '" (EINVAL)', defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22 diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 4fd77a6..99d6b63 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -108,7 +108,27 @@ public function testCtorWithInvalidUriThrows(string $uri, string $message): void public function testPingWillCreateUnderlyingClientAndReturnPendingPromise(): void { $promise = new Promise(function () { }); - $this->factory->expects($this->once())->method('createClient')->willReturn($promise); + $this->factory->expects($this->once())->method('createClient')->with('redis://localhost')->willReturn($promise); + + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->never())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + + $promise = $this->redis->ping(); + + $promise->then($this->expectCallableNever()); + } + + public function testPingWithAuthWillCreateUnderlyingClientWithAuthAndReturnPendingPromise(): void + { + $this->redis = new RedisClient('user:pass@localhost'); + $ref = new \ReflectionProperty($this->redis, 'factory'); + $ref->setAccessible(true); + $ref->setValue($this->redis, $this->factory); + + $promise = new Promise(function () { }); + $this->factory->expects($this->once())->method('createClient')->with('redis://user:pass@localhost')->willReturn($promise); $loop = $this->createMock(LoopInterface::class); $loop->expects($this->never())->method('addTimer'); From 4ffcfafc4b179c4f7b8590949e0f1d41c01aae6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 2 Feb 2025 21:46:07 +0100 Subject: [PATCH 61/65] Automatically convert numeric arguments passed to any Redis commands --- README.md | 21 +++++++------- src/RedisClient.php | 31 +++++++++++++-------- tests/RedisClientTest.php | 58 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b1c24fe..6652011 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ Listing all available commands is out of scope here, please refer to the Any arguments passed to the method call will be forwarded as command arguments. For example, the `$redis->set('name', 'Alice')` call will perform the equivalent of a -`SET name Alice` command. It's safe to pass integer arguments where applicable (for +`SET name Alice` command. It's safe to pass numeric arguments where applicable (for example `$redis->expire($key, 60)`), but internally Redis requires all arguments to always be coerced to string values. @@ -417,7 +417,7 @@ $redis = new Clue\React\Redis\RedisClient('localhost', $connector); #### __call() -The `__call(string $name, string[] $args): PromiseInterface` method can be used to +The `__call(string $name, list $args): PromiseInterface` method can be used to invoke the given command. This is a magic method that will be invoked when calling any Redis command on this instance. @@ -441,7 +441,7 @@ Listing all available commands is out of scope here, please refer to the Any arguments passed to the method call will be forwarded as command arguments. For example, the `$redis->set('name', 'Alice')` call will perform the equivalent of a -`SET name Alice` command. It's safe to pass integer arguments where applicable (for +`SET name Alice` command. It's safe to pass numeric arguments where applicable (for example `$redis->expire($key, 60)`), but internally Redis requires all arguments to always be coerced to string values. @@ -451,9 +451,12 @@ that eventually *fulfills* with its *results* on success or *rejects* with an #### callAsync() -The `callAsync(string $command, string ...$args): PromiseInterface` method can be used to +The `callAsync(string $command, string|int|float ...$args): PromiseInterface` method can be used to invoke a Redis command. +For example, the [`GET` command](https://redis.io/commands/get) can be invoked +like this: + ```php $redis->callAsync('GET', 'name')->then(function (?string $name): void { echo 'Name: ' . ($name ?? 'Unknown') . PHP_EOL; @@ -470,12 +473,10 @@ may understand this magic method. Listing all available commands is out of scope here, please refer to the [Redis command reference](https://redis.io/commands). -The optional `string ...$args` parameter can be used to pass any -additional arguments to the Redis command. Some commands may require or -support additional arguments that this method will simply forward as is. -Internally, Redis requires all arguments to be coerced to `string` values, -but you may also rely on PHP's type-juggling semantics and pass `int` or -`float` values: +The optional `string|int|float ...$args` parameter can be used to pass +any additional arguments that some Redis commands may require or support. +Values get passed directly to Redis, with any numeric values converted +automatically since Redis only works with `string` arguments internally: ```php $redis->callAsync('SET', 'name', 'Alice', 'EX', 600); diff --git a/src/RedisClient.php b/src/RedisClient.php index 688e584..32ced43 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -165,8 +165,8 @@ private function client(): PromiseInterface * This is a magic method that will be invoked when calling any redis * command on this instance. See also `RedisClient::callAsync()`. * - * @param string $name - * @param string[] $args + * @param string $name + * @param list $args * @return PromiseInterface * @see self::callAsync() */ @@ -197,12 +197,10 @@ public function __call(string $name, array $args): PromiseInterface * of scope here, please refer to the * [Redis command reference](https://redis.io/commands). * - * The optional `string ...$args` parameter can be used to pass any - * additional arguments to the Redis command. Some commands may require or - * support additional arguments that this method will simply forward as is. - * Internally, Redis requires all arguments to be coerced to `string` values, - * but you may also rely on PHP's type-juggling semantics and pass `int` or - * `float` values: + * The optional `string|int|float ...$args` parameter can be used to pass + * any additional arguments that some Redis commands may require or support. + * Values get passed directly to Redis, with any numeric values converted + * automatically since Redis only works with `string` arguments internally: * * ```php * $redis->callAsync('SET', 'name', 'Alice', 'EX', 600); @@ -214,12 +212,23 @@ public function __call(string $name, array $args): PromiseInterface * details. * * @param string $command - * @param string ...$args + * @param string|int|float ...$args * @return PromiseInterface - * @throws void + * @throws \TypeError if given $args are invalid */ - public function callAsync(string $command, string ...$args): PromiseInterface + public function callAsync(string $command, ...$args): PromiseInterface { + $args = \array_map(function ($value): string { + /** @var mixed $value */ + if (\is_string($value)) { + return $value; + } elseif (\is_int($value) || \is_float($value)) { + return \var_export($value, true); + } else { + throw new \TypeError('Argument must be of type string|int|float, ' . (\is_object($value) ? \get_class($value) : \gettype($value)) . ' given'); + } + }, $args); + if ($this->closed) { return reject(new \RuntimeException( 'Connection closed (ENOTCONN)', diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 99d6b63..785bfbc 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -407,6 +407,64 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutC $timeout(); } + public function testBlpopWillForwardArgumentsAsStringToUnderlyingClient(): void + { + $client = $this->createMock(StreamingClient::class); + $client->expects($this->once())->method('callAsync')->with('BLPOP', 'foo', 'bar', '10.0')->willReturn(new Promise(function () { })); + + $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); + + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->never())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + + $this->redis->callAsync('BLPOP', 'foo', 'bar', 10.0); + } + + public function testCallAsyncWillForwardArgumentsAsStringToUnderlyingClient(): void + { + $client = $this->createMock(StreamingClient::class); + $client->expects($this->once())->method('callAsync')->with('ZCOUNT', 'foo', '-INF', 'INF')->willReturn(new Promise(function () { })); + + $this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client)); + + $loop = $this->createMock(LoopInterface::class); + $loop->expects($this->never())->method('addTimer'); + assert($loop instanceof LoopInterface); + Loop::set($loop); + + $this->redis->callAsync('ZCOUNT', 'foo', -INF, INF); + } + + public function testSetWithInvalidBoolArgumentThrows(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument must be of type string|int|float, boolean given'); + $this->redis->set('foo', true); + } + + public function testSetWithInvalidObjectArgumentThrows(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument must be of type string|int|float, stdClass given'); + $this->redis->set('foo', (object) []); + } + + public function testCallAsyncWithInvalidBoolArgumentThrows(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument must be of type string|int|float, boolean given'); + $this->redis->callAsync('SET', 'foo', true); // @phpstan-ignore-line + } + + public function testCallAsyncWithInvalidObjectArgumentThrows(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('Argument must be of type string|int|float, stdClass given'); + $this->redis->callAsync('SET', 'foo', (object) []); // @phpstan-ignore-line + } + public function testCloseWillEmitCloseEventWithoutCreatingUnderlyingClient(): void { $this->factory->expects($this->never())->method('createClient'); From fe2ab078066fe085c7103c798a661e200995966c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 22 Feb 2025 10:12:34 +0100 Subject: [PATCH 62/65] Update tests to work around segfault on PHP 8.4.4 with Xdebug 3.5.0-dev --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6365a34..769c3ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} + extensions: xdebug-stable # temporarily force stable Xdebug due to segfault on PHP 8.4.4 with 3.5.0-dev coverage: xdebug ini-file: development - run: composer install From b2da0c345d1b96c4327630ee9755d953efa36f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 15 Feb 2025 12:02:43 +0100 Subject: [PATCH 63/65] Support cloning `RedisClient` instance --- README.md | 37 +++++++++++++++++++++++++++++ src/RedisClient.php | 49 +++++++++++++++++++++++++++++++++++++++ tests/FunctionalTest.php | 42 +++++++++++++++++++++++++++++++++ tests/RedisClientTest.php | 32 +++++++++++++++++++++++++ 4 files changed, 160 insertions(+) diff --git a/README.md b/README.md index 6652011..94416ff 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ It enables you to set and query its data or use its PubSub topics to react to in * [API](#api) * [RedisClient](#redisclient) * [__construct()](#__construct) + * [__clone()](#__clone) * [__call()](#__call) * [callAsync()](#callasync) * [end()](#end) @@ -415,6 +416,42 @@ $connector = new React\Socket\Connector([ $redis = new Clue\React\Redis\RedisClient('localhost', $connector); ``` +#### __clone() + +The `__clone()` method is a magic method in PHP that is called +automatically when a `RedisClient` instance is being cloned: + +```php +$original = new Clue\React\Redis\RedisClient($uri); +$redis = clone $original; +``` + +This method ensures the cloned client is created in a "fresh" state and +any connection state is reset on the clone, matching how a new instance +would start after returning from its constructor. Accordingly, the clone +will always start in an unconnected and unclosed state, with no event +listeners attached and ready to accept commands. Invoking any of the +[commands](#commands) will establish a new connection as usual: + +```php +$redis = clone $original; +$redis->set('name', 'Alice'); +``` + +This can be especially useful if the original connection is used for a +[PubSub subscription](#pubsub) or when using blocking commands or similar +and you need a control connection that is not affected by any of this. +Both instances will not be directly affected by any operations performed, +for example you can [`close()`](#close) either instance without also +closing the other. Similarly, you can also clone a fresh instance from a +closed state or overwrite a dead connection: + +```php +$redis->close(); +$redis = clone $redis; +$redis->set('name', 'Alice'); +``` + #### __call() The `__call(string $name, list $args): PromiseInterface` method can be used to diff --git a/src/RedisClient.php b/src/RedisClient.php index 32ced43..1c1bf87 100644 --- a/src/RedisClient.php +++ b/src/RedisClient.php @@ -90,6 +90,55 @@ public function __construct(string $uri, ?ConnectorInterface $connector = null) $this->factory = new Factory($connector); } + /** + * The `__clone()` method is a magic method in PHP that is called + * automatically when a `RedisClient` instance is being cloned: + * + * ```php + * $original = new Clue\React\Redis\RedisClient($uri); + * $redis = clone $original; + * ``` + * + * This method ensures the cloned client is created in a "fresh" state and + * any connection state is reset on the clone, matching how a new instance + * would start after returning from its constructor. Accordingly, the clone + * will always start in an unconnected and unclosed state, with no event + * listeners attached and ready to accept commands. Invoking any of the + * [commands](#commands) will establish a new connection as usual: + * + * ```php + * $redis = clone $original; + * $redis->set('name', 'Alice'); + * ``` + * + * This can be especially useful if the original connection is used for a + * [PubSub subscription](#pubsub) or when using blocking commands or similar + * and you need a control connection that is not affected by any of this. + * Both instances will not be directly affected by any operations performed, + * for example you can [`close()`](#close) either instance without also + * closing the other. Similarly, you can also clone a fresh instance from a + * closed state or overwrite a dead connection: + * + * ```php + * $redis->close(); + * $redis = clone $redis; + * $redis->set('name', 'Alice'); + * ``` + * + * @return void + * @throws void + */ + public function __clone() + { + $this->closed = false; + $this->promise = null; + $this->idleTimer = null; + $this->pending = 0; + $this->subscribed = []; + $this->psubscribed = []; + $this->removeAllListeners(); + } + /** * @return PromiseInterface */ diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 25bf829..5757eac 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -176,4 +176,46 @@ public function testClose(): void $redis->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce()); } + + public function testCloneWhenOriginalIsIdleReturnsClientThatWillCloseIndependently(): void + { + $prefix = 'test:' . mt_rand() . ':'; + $original = new RedisClient($this->uri); + + $this->assertNull(await($original->callAsync('GET', $prefix . 'doesnotexist'))); + + $redis = clone $original; + + $this->assertNull(await($redis->callAsync('GET', $prefix . 'doesnotexist'))); + } + + public function testCloneWhenOriginalIsPendingReturnsClientThatWillCloseIndependently(): void + { + $prefix = 'test:' . mt_rand() . ':'; + $original = new RedisClient($this->uri); + + $this->assertNull(await($original->callAsync('GET', $prefix . 'doesnotexist'))); + $promise = $original->callAsync('GET', $prefix . 'doesnotexist'); + + $redis = clone $original; + + $this->assertNull(await($redis->callAsync('GET', $prefix . 'doesnotexist'))); + $this->assertNull(await($promise)); + } + + public function testCloneReturnsClientNotAffectedByPubSubSubscriptions(): void + { + $prefix = 'test:' . mt_rand() . ':'; + $consumer = new RedisClient($this->uri); + + $consumer->on('message', $this->expectCallableNever()); + $consumer->on('pmessage', $this->expectCallableNever()); + await($consumer->callAsync('SUBSCRIBE', $prefix . 'demo')); + await($consumer->callAsync('PSUBSCRIBE', $prefix . '*')); + + $redis = clone $consumer; + $consumer->close(); + + $this->assertNull(await($redis->callAsync('GET', $prefix . 'doesnotexist'))); + } } diff --git a/tests/RedisClientTest.php b/tests/RedisClientTest.php index 785bfbc..b17e148 100644 --- a/tests/RedisClientTest.php +++ b/tests/RedisClientTest.php @@ -836,4 +836,36 @@ public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResp $promise->then(null, $this->expectCallableOnceWith($e)); } + + public function testCloneClosedClientReturnsClientThatWillCreateNewConnectionForFirstCommand(): void + { + $this->redis->close(); + + $redis = clone $this->redis; + + $deferred = new Deferred($this->expectCallableNever()); + $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); + + $promise = $redis->callAsync('PING'); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } + + public function testCloneClientReturnsClientThatWillNotBeAffectedByOldClientClosing(): void + { + $this->redis->on('close', $this->expectCallableOnce()); + + $redis = clone $this->redis; + + $this->assertEquals([], $redis->listeners()); + + $deferred = new Deferred($this->expectCallableNever()); + $this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise()); + + $promise = $redis->callAsync('PING'); + + $this->redis->close(); + + $promise->then($this->expectCallableNever(), $this->expectCallableNever()); + } } From b3e226576a44655f3aa8b1a94e4c493e80c44872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Wed, 5 Mar 2025 17:02:41 +0100 Subject: [PATCH 64/65] Revert "Update tests to work around segfault on PHP 8.4.4 with Xdebug 3.5.0-dev" This reverts commit fe2ab078066fe085c7103c798a661e200995966c. --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 769c3ac..6365a34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,6 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: xdebug-stable # temporarily force stable Xdebug due to segfault on PHP 8.4.4 with 3.5.0-dev coverage: xdebug ini-file: development - run: composer install From 9f6051bda9e2af423b55dee64ceefee584bc3191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 6 Apr 2025 12:02:26 +0200 Subject: [PATCH 65/65] Update test suite to use PCOV to avoid segfault with Xdebug 3.4.2 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6365a34..2166300 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - coverage: xdebug + coverage: ${{ matrix.php < 8.0 && 'xdebug' || 'pcov' }} ini-file: development - run: composer install - run: docker run --net=host -d redis