Skip to content

Commit 28d7747

Browse files
committed
Automatically convert numeric arguments passed to any Redis command
1 parent 424df04 commit 28d7747

File tree

3 files changed

+89
-21
lines changed

3 files changed

+89
-21
lines changed

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ Listing all available commands is out of scope here, please refer to the
132132

133133
Any arguments passed to the method call will be forwarded as command arguments.
134134
For example, the `$redis->set('name', 'Alice')` call will perform the equivalent of a
135-
`SET name Alice` command. It's safe to pass integer arguments where applicable (for
135+
`SET name Alice` command. It's safe to pass numeric arguments where applicable (for
136136
example `$redis->expire($key, 60)`), but internally Redis requires all arguments to
137137
always be coerced to string values.
138138

@@ -417,7 +417,7 @@ $redis = new Clue\React\Redis\RedisClient('localhost', $connector);
417417

418418
#### __call()
419419

420-
The `__call(string $name, string[] $args): PromiseInterface<mixed>` method can be used to
420+
The `__call(string $name, list<string|int|float> $args): PromiseInterface<mixed>` method can be used to
421421
invoke the given command.
422422

423423
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
441441

442442
Any arguments passed to the method call will be forwarded as command arguments.
443443
For example, the `$redis->set('name', 'Alice')` call will perform the equivalent of a
444-
`SET name Alice` command. It's safe to pass integer arguments where applicable (for
444+
`SET name Alice` command. It's safe to pass numeric arguments where applicable (for
445445
example `$redis->expire($key, 60)`), but internally Redis requires all arguments to
446446
always be coerced to string values.
447447

@@ -451,9 +451,12 @@ that eventually *fulfills* with its *results* on success or *rejects* with an
451451

452452
#### callAsync()
453453

454-
The `callAsync(string $command, string ...$args): PromiseInterface<mixed>` method can be used to
454+
The `callAsync(string $command, string|int|float ...$args): PromiseInterface<mixed>` method can be used to
455455
invoke a Redis command.
456456

457+
For example, the [`GET` command](https://redis.io/commands/get) can be invoked
458+
like this:
459+
457460
```php
458461
$redis->callAsync('GET', 'name')->then(function (?string $name): void {
459462
echo 'Name: ' . ($name ?? 'Unknown') . PHP_EOL;
@@ -470,12 +473,10 @@ may understand this magic method. Listing all available commands is out
470473
of scope here, please refer to the
471474
[Redis command reference](https://redis.io/commands).
472475

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:
476+
The optional `string|int|float ...$args` parameter can be used to pass
477+
any additional arguments that some Redis commands may require or support.
478+
Values get passed directly to Redis, with any numeric values converted
479+
automatically since Redis only works with `string` arguments internally:
479480

480481
```php
481482
$redis->callAsync('SET', 'name', 'Alice', 'EX', 600);

src/RedisClient.php

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,8 @@ private function client(): PromiseInterface
165165
* This is a magic method that will be invoked when calling any redis
166166
* command on this instance. See also `RedisClient::callAsync()`.
167167
*
168-
* @param string $name
169-
* @param string[] $args
168+
* @param string $name
169+
* @param list<string|int|float> $args
170170
* @return PromiseInterface<mixed>
171171
* @see self::callAsync()
172172
*/
@@ -197,12 +197,10 @@ public function __call(string $name, array $args): PromiseInterface
197197
* of scope here, please refer to the
198198
* [Redis command reference](https://redis.io/commands).
199199
*
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:
200+
* The optional `string|int|float ...$args` parameter can be used to pass
201+
* any additional arguments that some Redis commands may require or support.
202+
* Values get passed directly to Redis, with any numeric values converted
203+
* automatically since Redis only works with `string` arguments internally:
206204
*
207205
* ```php
208206
* $redis->callAsync('SET', 'name', 'Alice', 'EX', 600);
@@ -214,12 +212,23 @@ public function __call(string $name, array $args): PromiseInterface
214212
* details.
215213
*
216214
* @param string $command
217-
* @param string ...$args
215+
* @param string|int|float ...$args
218216
* @return PromiseInterface<mixed>
219-
* @throws void
217+
* @throws \TypeError if given $args are invalid
220218
*/
221-
public function callAsync(string $command, string ...$args): PromiseInterface
219+
public function callAsync(string $command, ...$args): PromiseInterface
222220
{
221+
$args = \array_map(function ($value): string {
222+
/** @var mixed $value */
223+
if (\is_string($value)) {
224+
return $value;
225+
} elseif (\is_int($value) || \is_float($value)) {
226+
return \var_export($value, true);
227+
} else {
228+
throw new \TypeError('Argument must be of type string|int|float, ' . (\is_object($value) ? \get_class($value) : \gettype($value)) . ' given');
229+
}
230+
}, $args);
231+
223232
if ($this->closed) {
224233
return reject(new \RuntimeException(
225234
'Connection closed (ENOTCONN)',

tests/RedisClientTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,64 @@ public function testPingFollowedByIdleTimerWillCloseUnderlyingConnectionWithoutC
407407
$timeout();
408408
}
409409

410+
public function testBlpopWillForwardArgumentsAsStringToUnderlyingClient(): void
411+
{
412+
$client = $this->createMock(StreamingClient::class);
413+
$client->expects($this->once())->method('callAsync')->with('BLPOP', 'foo', 'bar', '10.0')->willReturn(new Promise(function () { }));
414+
415+
$this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client));
416+
417+
$loop = $this->createMock(LoopInterface::class);
418+
$loop->expects($this->never())->method('addTimer');
419+
assert($loop instanceof LoopInterface);
420+
Loop::set($loop);
421+
422+
$this->redis->callAsync('BLPOP', 'foo', 'bar', 10.0);
423+
}
424+
425+
public function testCallAsyncWillForwardArgumentsAsStringToUnderlyingClient(): void
426+
{
427+
$client = $this->createMock(StreamingClient::class);
428+
$client->expects($this->once())->method('callAsync')->with('ZCOUNT', 'foo', '-INF', 'INF')->willReturn(new Promise(function () { }));
429+
430+
$this->factory->expects($this->once())->method('createClient')->willReturn(\React\Promise\resolve($client));
431+
432+
$loop = $this->createMock(LoopInterface::class);
433+
$loop->expects($this->never())->method('addTimer');
434+
assert($loop instanceof LoopInterface);
435+
Loop::set($loop);
436+
437+
$this->redis->callAsync('ZCOUNT', 'foo', -INF, INF);
438+
}
439+
440+
public function testSetWithInvalidBoolArgumentThrows(): void
441+
{
442+
$this->expectException(\TypeError::class);
443+
$this->expectExceptionMessage('Argument must be of type string|int|float, boolean given');
444+
$this->redis->set('foo', true);
445+
}
446+
447+
public function testSetWithInvalidObjectArgumentThrows(): void
448+
{
449+
$this->expectException(\TypeError::class);
450+
$this->expectExceptionMessage('Argument must be of type string|int|float, stdClass given');
451+
$this->redis->set('foo', (object) []);
452+
}
453+
454+
public function testCallAsyncWithInvalidBoolArgumentThrows(): void
455+
{
456+
$this->expectException(\TypeError::class);
457+
$this->expectExceptionMessage('Argument must be of type string|int|float, boolean given');
458+
$this->redis->callAsync('SET', 'foo', true); // @phpstan-ignore-line
459+
}
460+
461+
public function testCallAsyncWithInvalidObjectArgumentThrows(): void
462+
{
463+
$this->expectException(\TypeError::class);
464+
$this->expectExceptionMessage('Argument must be of type string|int|float, stdClass given');
465+
$this->redis->callAsync('SET', 'foo', (object) []); // @phpstan-ignore-line
466+
}
467+
410468
public function testCloseWillEmitCloseEventWithoutCreatingUnderlyingClient(): void
411469
{
412470
$this->factory->expects($this->never())->method('createClient');

0 commit comments

Comments
 (0)