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);