Skip to content

Commit 264cad5

Browse files
committed
Support cloning RedisClient instance
1 parent 39aa211 commit 264cad5

File tree

4 files changed

+160
-0
lines changed

4 files changed

+160
-0
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ It enables you to set and query its data or use its PubSub topics to react to in
4646
* [API](#api)
4747
* [RedisClient](#redisclient)
4848
* [__construct()](#__construct)
49+
* [__clone()](#__clone)
4950
* [__call()](#__call)
5051
* [callAsync()](#callasync)
5152
* [end()](#end)
@@ -415,6 +416,42 @@ $connector = new React\Socket\Connector([
415416
$redis = new Clue\React\Redis\RedisClient('localhost', $connector);
416417
```
417418

419+
#### __clone()
420+
421+
The `__clone()` method is a magic method in PHP that is called
422+
automatically when a `RedisClient` instance is being cloned:
423+
424+
```php
425+
$original = new Clue\React\Redis\RedisClient($uri);
426+
$redis = clone $original;
427+
```
428+
429+
This method ensures the cloned client is created in a "fresh" state and
430+
any connection state is reset on the clone, matching how a new instance
431+
would start after returning from its constructor. Accordingly, the clone
432+
will always start in an unconnected and unclosed state, with no event
433+
listeners attached and ready to accept commands. Invoking any of the
434+
[commands](#commands) will establish a new connection as usual:
435+
436+
```php
437+
$redis = clone $original;
438+
$redis->set('name', 'Alice');
439+
```
440+
441+
This can be especially useful if the original connection is used for a
442+
[PubSub subscription](#pubsub) or when using blocking commands or similar
443+
and you need a control connection that is not affected by any of this.
444+
Both instances will not be directly affected by any operations performed,
445+
for example you can [`close()`](#close) either instance without also
446+
closing the other. Similarly, you can also clone a fresh instance from a
447+
closed state or overwrite a dead connection:
448+
449+
```php
450+
$redis->close();
451+
$redis = clone $redis;
452+
$redis->set('name', 'Alice');
453+
```
454+
418455
#### __call()
419456

420457
The `__call(string $name, list<string|int|float> $args): PromiseInterface<mixed>` method can be used to

src/RedisClient.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,55 @@ public function __construct(string $uri, ?ConnectorInterface $connector = null)
9090
$this->factory = new Factory($connector);
9191
}
9292

93+
/**
94+
* The `__clone()` method is a magic method in PHP that is called
95+
* automatically when a `RedisClient` instance is being cloned:
96+
*
97+
* ```php
98+
* $original = new Clue\React\Redis\RedisClient($uri);
99+
* $redis = clone $original;
100+
* ```
101+
*
102+
* This method ensures the cloned client is created in a "fresh" state and
103+
* any connection state is reset on the clone, matching how a new instance
104+
* would start after returning from its constructor. Accordingly, the clone
105+
* will always start in an unconnected and unclosed state, with no event
106+
* listeners attached and ready to accept commands. Invoking any of the
107+
* [commands](#commands) will establish a new connection as usual:
108+
*
109+
* ```php
110+
* $redis = clone $original;
111+
* $redis->set('name', 'Alice');
112+
* ```
113+
*
114+
* This can be especially useful if the original connection is used for a
115+
* [PubSub subscription](#pubsub) or when using blocking commands or similar
116+
* and you need a control connection that is not affected by any of this.
117+
* Both instances will not be directly affected by any operations performed,
118+
* for example you can [`close()`](#close) either instance without also
119+
* closing the other. Similarly, you can also clone a fresh instance from a
120+
* closed state or overwrite a dead connection:
121+
*
122+
* ```php
123+
* $redis->close();
124+
* $redis = clone $redis;
125+
* $redis->set('name', 'Alice');
126+
* ```
127+
*
128+
* @return void
129+
* @throws void
130+
*/
131+
public function __clone()
132+
{
133+
$this->closed = false;
134+
$this->promise = null;
135+
$this->idleTimer = null;
136+
$this->pending = 0;
137+
$this->subscribed = [];
138+
$this->psubscribed = [];
139+
$this->removeAllListeners();
140+
}
141+
93142
/**
94143
* @return PromiseInterface<StreamingClient>
95144
*/

tests/FunctionalTest.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,46 @@ public function testClose(): void
176176

177177
$redis->get('willBeRejectedRightAway')->then(null, $this->expectCallableOnce());
178178
}
179+
180+
public function testCloneWhenOriginalIsIdleReturnsClientThatWillCloseIndependently(): void
181+
{
182+
$prefix = 'test:' . mt_rand() . ':';
183+
$original = new RedisClient($this->uri);
184+
185+
$this->assertNull(await($original->callAsync('GET', $prefix . 'doesnotexist')));
186+
187+
$redis = clone $original;
188+
189+
$this->assertNull(await($redis->callAsync('GET', $prefix . 'doesnotexist')));
190+
}
191+
192+
public function testCloneWhenOriginalIsPendingReturnsClientThatWillCloseIndependently(): void
193+
{
194+
$prefix = 'test:' . mt_rand() . ':';
195+
$original = new RedisClient($this->uri);
196+
197+
$this->assertNull(await($original->callAsync('GET', $prefix . 'doesnotexist')));
198+
$promise = $original->callAsync('GET', $prefix . 'doesnotexist');
199+
200+
$redis = clone $original;
201+
202+
$this->assertNull(await($redis->callAsync('GET', $prefix . 'doesnotexist')));
203+
$this->assertNull(await($promise));
204+
}
205+
206+
public function testCloneReturnsClientNotAffectedByPubSubSubscriptions(): void
207+
{
208+
$prefix = 'test:' . mt_rand() . ':';
209+
$consumer = new RedisClient($this->uri);
210+
211+
$consumer->on('message', $this->expectCallableNever());
212+
$consumer->on('pmessage', $this->expectCallableNever());
213+
await($consumer->callAsync('SUBSCRIBE', $prefix . 'demo'));
214+
await($consumer->callAsync('PSUBSCRIBE', $prefix . '*'));
215+
216+
$redis = clone $consumer;
217+
$consumer->close();
218+
219+
$this->assertNull(await($redis->callAsync('GET', $prefix . 'doesnotexist')));
220+
}
179221
}

tests/RedisClientTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,4 +836,36 @@ public function testBlpopWillRejectWhenUnderlyingClientClosesWhileWaitingForResp
836836

837837
$promise->then(null, $this->expectCallableOnceWith($e));
838838
}
839+
840+
public function testCloneClosedClientReturnsClientThatWillCreateNewConnectionForFirstCommand(): void
841+
{
842+
$this->redis->close();
843+
844+
$redis = clone $this->redis;
845+
846+
$deferred = new Deferred($this->expectCallableNever());
847+
$this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise());
848+
849+
$promise = $redis->callAsync('PING');
850+
851+
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
852+
}
853+
854+
public function testCloneClientReturnsClientThatWillNotBeAffectedByOldClientClosing(): void
855+
{
856+
$this->redis->on('close', $this->expectCallableOnce());
857+
858+
$redis = clone $this->redis;
859+
860+
$this->assertEquals([], $redis->listeners());
861+
862+
$deferred = new Deferred($this->expectCallableNever());
863+
$this->factory->expects($this->once())->method('createClient')->willReturn($deferred->promise());
864+
865+
$promise = $redis->callAsync('PING');
866+
867+
$this->redis->close();
868+
869+
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
870+
}
839871
}

0 commit comments

Comments
 (0)