Skip to content

Commit b2da0c3

Browse files
committed
Support cloning RedisClient instance
1 parent fe2ab07 commit b2da0c3

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy