Skip to content

Commit 652d56e

Browse files
authored
Merge pull request #166 from clue-labs/callasync
Add new public `callAsync()` method
2 parents 41e54b7 + e89ae33 commit 652d56e

8 files changed

+153
-63
lines changed

README.md

+42-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ It enables you to set and query its data or use its PubSub topics to react to in
4747
* [RedisClient](#redisclient)
4848
* [__construct()](#__construct)
4949
* [__call()](#__call)
50+
* [callAsync()](#callasync)
5051
* [end()](#end)
5152
* [close()](#close)
5253
* [error event](#error-event)
@@ -124,7 +125,8 @@ Each method call matches the respective [Redis command](https://redis.io/command
124125
For example, the `$redis->get()` method will invoke the [`GET` command](https://redis.io/commands/get).
125126

126127
All [Redis commands](https://redis.io/commands) are automatically available as
127-
public methods via the magic [`__call()` method](#__call).
128+
public methods via the magic [`__call()` method](#__call) or through the more
129+
explicit [`callAsync()` method].
128130
Listing all available commands is out of scope here, please refer to the
129131
[Redis command reference](https://redis.io/commands).
130132

@@ -432,6 +434,8 @@ $redis->get($key)->then(function (?string $value) {
432434

433435
All [Redis commands](https://redis.io/commands) are automatically available as
434436
public methods via this magic `__call()` method.
437+
Note that some static analysis tools may not understand this magic method, so
438+
you may also the [`callAsync()` method](#callasync) as a more explicit alternative.
435439
Listing all available commands is out of scope here, please refer to the
436440
[Redis command reference](https://redis.io/commands).
437441

@@ -445,6 +449,43 @@ Each of these commands supports async operation and returns a [Promise](#promise
445449
that eventually *fulfills* with its *results* on success or *rejects* with an
446450
`Exception` on error. See also [promises](#promises) for more details.
447451

452+
#### callAsync()
453+
454+
The `callAsync(string $command, string ...$args): PromiseInterface<mixed>` method can be used to
455+
invoke a Redis command.
456+
457+
```php
458+
$redis->callAsync('GET', 'name')->then(function (?string $name): void {
459+
echo 'Name: ' . ($name ?? 'Unknown') . PHP_EOL;
460+
}, function (Throwable $e): void {
461+
echo 'Error: ' . $e->getMessage() . PHP_EOL;
462+
});
463+
```
464+
465+
The `string $command` parameter can be any valid Redis command. All
466+
[Redis commands](https://redis.io/commands/) are available through this
467+
method. As an alternative, you may also use the magic
468+
[`__call()` method](#__call), but note that not all static analysis tools
469+
may understand this magic method. Listing all available commands is out
470+
of scope here, please refer to the
471+
[Redis command reference](https://redis.io/commands).
472+
473+
The optional `string ...$args` parameter can be used to pass any
474+
additional arguments to the Redis command. Some commands may require or
475+
support additional arguments that this method will simply forward as is.
476+
Internally, Redis requires all arguments to be coerced to `string` values,
477+
but you may also rely on PHP's type-juggling semantics and pass `int` or
478+
`float` values:
479+
480+
```php
481+
$redis->callAsync('SET', 'name', 'Alice', 'EX', 600);
482+
```
483+
484+
This method supports async operation and returns a [Promise](#promises)
485+
that eventually *fulfills* with its *results* on success or *rejects*
486+
with an `Exception` on error. See also [promises](#promises) for more
487+
details.
488+
448489
#### end()
449490

450491
The `end():void` method can be used to

examples/cli.php

+8-8
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,20 @@
2323
return;
2424
}
2525

26-
$params = explode(' ', $line);
27-
$method = array_shift($params);
28-
29-
assert(is_callable([$redis, $method]));
30-
$promise = $redis->$method(...$params);
26+
$args = explode(' ', $line);
27+
$command = strtolower(array_shift($args));
3128

3229
// special method such as end() / close() called
33-
if (!$promise instanceof React\Promise\PromiseInterface) {
30+
if (in_array($command, ['end', 'close'])) {
31+
$redis->$command();
3432
return;
3533
}
3634

37-
$promise->then(function ($data) {
35+
$promise = $redis->callAsync($command, ...$args);
36+
37+
$promise->then(function ($data): void {
3838
echo '# reply: ' . json_encode($data) . PHP_EOL;
39-
}, function ($e) {
39+
}, function (Throwable $e): void {
4040
echo '# error reply: ' . $e->getMessage() . PHP_EOL;
4141
});
4242
});

phpstan.neon.dist

-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ parameters:
99
ignoreErrors:
1010
# ignore undefined methods due to magic `__call()` method
1111
- '/^Call to an undefined method Clue\\React\\Redis\\RedisClient::.+\(\)\.$/'
12-
- '/^Call to an undefined method Clue\\React\\Redis\\Io\\StreamingClient::.+\(\)\.$/'

src/Io/Factory.php

+6-4
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,13 @@ public function createClient(string $uri): PromiseInterface
9696
// use `?password=secret` query or `user:secret@host` password form URL
9797
if (isset($args['password']) || isset($parts['pass'])) {
9898
$pass = $args['password'] ?? rawurldecode($parts['pass']); // @phpstan-ignore-line
99+
\assert(\is_string($pass));
99100
$promise = $promise->then(function (StreamingClient $redis) use ($pass, $uri) {
100-
return $redis->auth($pass)->then(
101+
return $redis->callAsync('auth', $pass)->then(
101102
function () use ($redis) {
102103
return $redis;
103104
},
104-
function (\Exception $e) use ($redis, $uri) {
105+
function (\Throwable $e) use ($redis, $uri) {
105106
$redis->close();
106107

107108
$const = '';
@@ -124,12 +125,13 @@ function (\Exception $e) use ($redis, $uri) {
124125
// use `?db=1` query or `/1` path (skip first slash)
125126
if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) {
126127
$db = $args['db'] ?? substr($parts['path'], 1); // @phpstan-ignore-line
128+
\assert(\is_string($db));
127129
$promise = $promise->then(function (StreamingClient $redis) use ($db, $uri) {
128-
return $redis->select($db)->then(
130+
return $redis->callAsync('select', $db)->then(
129131
function () use ($redis) {
130132
return $redis;
131133
},
132-
function (\Exception $e) use ($redis, $uri) {
134+
function (\Throwable $e) use ($redis, $uri) {
133135
$redis->close();
134136

135137
$const = '';

src/Io/StreamingClient.php

+6-7
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,14 @@ public function __construct(DuplexStreamInterface $stream, ParserInterface $pars
7272
}
7373

7474
/**
75-
* @param string[] $args
7675
* @return PromiseInterface<mixed>
7776
*/
78-
public function __call(string $name, array $args): PromiseInterface
77+
public function callAsync(string $command, string ...$args): PromiseInterface
7978
{
8079
$request = new Deferred();
8180
$promise = $request->promise();
8281

83-
$name = strtolower($name);
82+
$command = strtolower($command);
8483

8584
// special (p)(un)subscribe commands only accept a single parameter and have custom response logic applied
8685
static $pubsubs = ['subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe'];
@@ -90,22 +89,22 @@ public function __call(string $name, array $args): PromiseInterface
9089
'Connection ' . ($this->closed ? 'closed' : 'closing'). ' (ENOTCONN)',
9190
defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107
9291
));
93-
} elseif (count($args) !== 1 && in_array($name, $pubsubs)) {
92+
} elseif (count($args) !== 1 && in_array($command, $pubsubs)) {
9493
$request->reject(new \InvalidArgumentException(
9594
'PubSub commands limited to single argument (EINVAL)',
9695
defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
9796
));
98-
} elseif ($name === 'monitor') {
97+
} elseif ($command === 'monitor') {
9998
$request->reject(new \BadMethodCallException(
10099
'MONITOR command explicitly not supported (ENOTSUP)',
101100
defined('SOCKET_ENOTSUP') ? SOCKET_ENOTSUP : (defined('SOCKET_EOPNOTSUPP') ? SOCKET_EOPNOTSUPP : 95)
102101
));
103102
} else {
104-
$this->stream->write($this->serializer->getRequestMessage($name, $args));
103+
$this->stream->write($this->serializer->getRequestMessage($command, $args));
105104
$this->requests []= $request;
106105
}
107106

108-
if (in_array($name, $pubsubs)) {
107+
if (in_array($command, $pubsubs)) {
109108
$promise->then(function (array $array) {
110109
$first = array_shift($array);
111110

src/RedisClient.php

+56-7
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,65 @@ private function client(): PromiseInterface
160160
}
161161

162162
/**
163-
* Invoke the given command and return a Promise that will be resolved when the request has been replied to
163+
* Invoke the given command and return a Promise that will be resolved when the command has been replied to
164164
*
165165
* This is a magic method that will be invoked when calling any redis
166-
* command on this instance.
166+
* command on this instance. See also `RedisClient::callAsync()`.
167167
*
168168
* @param string $name
169169
* @param string[] $args
170170
* @return PromiseInterface<mixed>
171+
* @see self::callAsync()
171172
*/
172173
public function __call(string $name, array $args): PromiseInterface
174+
{
175+
return $this->callAsync($name, ...$args);
176+
}
177+
178+
/**
179+
* Invoke a Redis command.
180+
*
181+
* For example, the [`GET` command](https://redis.io/commands/get) can be invoked
182+
* like this:
183+
*
184+
* ```php
185+
* $redis->callAsync('GET', 'name')->then(function (?string $name): void {
186+
* echo 'Name: ' . ($name ?? 'Unknown') . PHP_EOL;
187+
* }, function (Throwable $e): void {
188+
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
189+
* });
190+
* ```
191+
*
192+
* The `string $command` parameter can be any valid Redis command. All
193+
* [Redis commands](https://redis.io/commands/) are available through this
194+
* method. As an alternative, you may also use the magic
195+
* [`__call()` method](#__call), but note that not all static analysis tools
196+
* may understand this magic method. Listing all available commands is out
197+
* of scope here, please refer to the
198+
* [Redis command reference](https://redis.io/commands).
199+
*
200+
* The optional `string ...$args` parameter can be used to pass any
201+
* additional arguments to the Redis command. Some commands may require or
202+
* support additional arguments that this method will simply forward as is.
203+
* Internally, Redis requires all arguments to be coerced to `string` values,
204+
* but you may also rely on PHP's type-juggling semantics and pass `int` or
205+
* `float` values:
206+
*
207+
* ```php
208+
* $redis->callAsync('SET', 'name', 'Alice', 'EX', 600);
209+
* ```
210+
*
211+
* This method supports async operation and returns a [Promise](#promises)
212+
* that eventually *fulfills* with its *results* on success or *rejects*
213+
* with an `Exception` on error. See also [promises](#promises) for more
214+
* details.
215+
*
216+
* @param string $command
217+
* @param string ...$args
218+
* @return PromiseInterface<mixed>
219+
* @throws void
220+
*/
221+
public function callAsync(string $command, string ...$args): PromiseInterface
173222
{
174223
if ($this->closed) {
175224
return reject(new \RuntimeException(
@@ -178,17 +227,17 @@ public function __call(string $name, array $args): PromiseInterface
178227
));
179228
}
180229

181-
return $this->client()->then(function (StreamingClient $redis) use ($name, $args) {
230+
return $this->client()->then(function (StreamingClient $redis) use ($command, $args): PromiseInterface {
182231
$this->awake();
183-
assert(\is_callable([$redis, $name])); // @phpstan-ignore-next-line
184-
return \call_user_func_array([$redis, $name], $args)->then(
232+
return $redis->callAsync($command, ...$args)->then(
185233
function ($result) {
186234
$this->idle();
187235
return $result;
188236
},
189-
function (\Exception $error) {
237+
function (\Throwable $e) {
238+
\assert($e instanceof \Exception);
190239
$this->idle();
191-
throw $error;
240+
throw $e;
192241
}
193242
);
194243
});

tests/Io/StreamingClientTest.php

+14-14
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function testSending(): void
4646
$this->serializer->expects($this->once())->method('getRequestMessage')->with($this->equalTo('ping'))->will($this->returnValue('message'));
4747
$this->stream->expects($this->once())->method('write')->with($this->equalTo('message'));
4848

49-
$this->redis->ping();
49+
$this->redis->callAsync('ping');
5050
}
5151

5252
public function testClosingClientEmitsEvent(): void
@@ -121,7 +121,7 @@ public function testPingPong(): void
121121
{
122122
$this->serializer->expects($this->once())->method('getRequestMessage')->with($this->equalTo('ping'));
123123

124-
$promise = $this->redis->ping();
124+
$promise = $this->redis->callAsync('ping');
125125

126126
$this->redis->handleMessage(new BulkReply('PONG'));
127127

@@ -131,7 +131,7 @@ public function testPingPong(): void
131131

132132
public function testMonitorCommandIsNotSupported(): void
133133
{
134-
$promise = $this->redis->monitor();
134+
$promise = $this->redis->callAsync('monitor');
135135

136136
$promise->then(null, $this->expectCallableOnceWith(
137137
$this->logicalAnd(
@@ -148,7 +148,7 @@ public function testMonitorCommandIsNotSupported(): void
148148

149149
public function testErrorReply(): void
150150
{
151-
$promise = $this->redis->invalid();
151+
$promise = $this->redis->callAsync('invalid');
152152

153153
$err = new ErrorReply("ERR unknown command 'invalid'");
154154
$this->redis->handleMessage($err);
@@ -158,7 +158,7 @@ public function testErrorReply(): void
158158

159159
public function testClosingClientRejectsAllRemainingRequests(): void
160160
{
161-
$promise = $this->redis->ping();
161+
$promise = $this->redis->callAsync('ping');
162162
$this->redis->close();
163163

164164
$promise->then(null, $this->expectCallableOnceWith(
@@ -183,7 +183,7 @@ public function testClosingStreamRejectsAllRemainingRequests(): void
183183
assert($this->serializer instanceof SerializerInterface);
184184
$this->redis = new StreamingClient($stream, $this->parser, $this->serializer);
185185

186-
$promise = $this->redis->ping();
186+
$promise = $this->redis->callAsync('ping');
187187
$stream->close();
188188

189189
$promise->then(null, $this->expectCallableOnceWith(
@@ -201,9 +201,9 @@ public function testClosingStreamRejectsAllRemainingRequests(): void
201201

202202
public function testEndingClientRejectsAllNewRequests(): void
203203
{
204-
$this->redis->ping();
204+
$this->redis->callAsync('ping');
205205
$this->redis->end();
206-
$promise = $this->redis->ping();
206+
$promise = $this->redis->callAsync('ping');
207207

208208
$promise->then(null, $this->expectCallableOnceWith(
209209
$this->logicalAnd(
@@ -221,7 +221,7 @@ public function testEndingClientRejectsAllNewRequests(): void
221221
public function testClosedClientRejectsAllNewRequests(): void
222222
{
223223
$this->redis->close();
224-
$promise = $this->redis->ping();
224+
$promise = $this->redis->callAsync('ping');
225225

226226
$promise->then(null, $this->expectCallableOnceWith(
227227
$this->logicalAnd(
@@ -250,7 +250,7 @@ public function testEndingBusyClosesClientWhenNotBusyAnymore(): void
250250
++$closed;
251251
});
252252

253-
$promise = $this->redis->ping();
253+
$promise = $this->redis->callAsync('ping');
254254
$this->assertEquals(0, $closed);
255255

256256
$this->redis->end();
@@ -277,7 +277,7 @@ public function testReceivingUnexpectedMessageThrowsException(): void
277277

278278
public function testPubsubSubscribe(): StreamingClient
279279
{
280-
$promise = $this->redis->subscribe('test');
280+
$promise = $this->redis->callAsync('subscribe', 'test');
281281
$this->expectPromiseResolve($promise);
282282

283283
$this->redis->on('subscribe', $this->expectCallableOnce());
@@ -291,7 +291,7 @@ public function testPubsubSubscribe(): StreamingClient
291291
*/
292292
public function testPubsubPatternSubscribe(StreamingClient $client): StreamingClient
293293
{
294-
$promise = $client->psubscribe('demo_*');
294+
$promise = $client->callAsync('psubscribe', 'demo_*');
295295
$this->expectPromiseResolve($promise);
296296

297297
$client->on('psubscribe', $this->expectCallableOnce());
@@ -311,7 +311,7 @@ public function testPubsubMessage(StreamingClient $client): void
311311

312312
public function testSubscribeWithMultipleArgumentsRejects(): void
313313
{
314-
$promise = $this->redis->subscribe('a', 'b');
314+
$promise = $this->redis->callAsync('subscribe', 'a', 'b');
315315

316316
$promise->then(null, $this->expectCallableOnceWith(
317317
$this->logicalAnd(
@@ -328,7 +328,7 @@ public function testSubscribeWithMultipleArgumentsRejects(): void
328328

329329
public function testUnsubscribeWithoutArgumentsRejects(): void
330330
{
331-
$promise = $this->redis->unsubscribe();
331+
$promise = $this->redis->callAsync('unsubscribe');
332332

333333
$promise->then(null, $this->expectCallableOnceWith(
334334
$this->logicalAnd(

0 commit comments

Comments
 (0)