Skip to content

Commit 0c6a9b3

Browse files
committed
Refactor to simplify parsing Redis URI
1 parent 408a248 commit 0c6a9b3

File tree

3 files changed

+52
-77
lines changed

3 files changed

+52
-77
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ $factory = new Clue\React\Redis\Factory(null, $connector);
118118

119119
#### createClient()
120120

121-
The `createClient(string $redisUri): PromiseInterface<Client,Exception>` method can be used to
121+
The `createClient(string $uri): PromiseInterface<Client,Exception>` method can be used to
122122
create a new [`Client`](#client).
123123

124124
It helps with establishing a plain TCP/IP or secure TLS connection to Redis
@@ -215,7 +215,7 @@ $factory->createClient('localhost?timeout=0.5');
215215

216216
#### createLazyClient()
217217

218-
The `createLazyClient(string $redisUri): Client` method can be used to
218+
The `createLazyClient(string $uri): Client` method can be used to
219219
create a new [`Client`](#client).
220220

221221
It helps with establishing a plain TCP/IP or secure TLS connection to Redis

src/Factory.php

Lines changed: 41 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use React\Socket\ConnectionInterface;
1111
use React\Socket\Connector;
1212
use React\Socket\ConnectorInterface;
13-
use InvalidArgumentException;
1413

1514
class Factory
1615
{
@@ -38,18 +37,39 @@ public function __construct(LoopInterface $loop = null, ConnectorInterface $conn
3837
/**
3938
* Create Redis client connected to address of given redis instance
4039
*
41-
* @param string $target Redis server URI to connect to
42-
* @return \React\Promise\PromiseInterface<Client> resolves with Client or rejects with \Exception
40+
* @param string $uri Redis server URI to connect to
41+
* @return \React\Promise\PromiseInterface<Client,\Exception> Promise that will
42+
* be fulfilled with `Client` on success or rejects with `\Exception` on error.
4343
*/
44-
public function createClient($target)
44+
public function createClient($uri)
4545
{
46-
try {
47-
$parts = $this->parseUrl($target);
48-
} catch (InvalidArgumentException $e) {
49-
return \React\Promise\reject($e);
46+
// support `redis+unix://` scheme for Unix domain socket (UDS) paths
47+
if (preg_match('/^(redis\+unix:\/\/(?:[^:]*:[^@]*@)?)(.+?)?$/', $uri, $match)) {
48+
$parts = parse_url($match[1] . 'localhost/' . $match[2]);
49+
} else {
50+
if (strpos($uri, '://') === false) {
51+
$uri = 'redis://' . $uri;
52+
}
53+
54+
$parts = parse_url($uri);
55+
}
56+
57+
if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss', 'redis+unix'))) {
58+
return \React\Promise\reject(new \InvalidArgumentException('Given URL can not be parsed'));
5059
}
5160

52-
$connecting = $this->connector->connect($parts['authority']);
61+
$args = array();
62+
parse_str(isset($parts['query']) ? $parts['query'] : '', $args);
63+
64+
$authority = $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 6379);
65+
if ($parts['scheme'] === 'rediss') {
66+
$authority = 'tls://' . $authority;
67+
} elseif ($parts['scheme'] === 'redis+unix') {
68+
$authority = 'unix://' . substr($parts['path'], 1);
69+
unset($parts['path']);
70+
}
71+
$connecting = $this->connector->connect($authority);
72+
5373
$deferred = new Deferred(function ($_, $reject) use ($connecting) {
5474
// connection cancelled, start with rejecting attempt, then clean up
5575
$reject(new \RuntimeException('Connection to Redis server cancelled'));
@@ -72,9 +92,12 @@ public function createClient($target)
7292
);
7393
});
7494

75-
if (isset($parts['auth'])) {
76-
$promise = $promise->then(function (StreamingClient $client) use ($parts) {
77-
return $client->auth($parts['auth'])->then(
95+
// use `?password=secret` query or `user:secret@host` password form URL
96+
$pass = isset($args['password']) ? $args['password'] : (isset($parts['pass']) ? rawurldecode($parts['pass']) : null);
97+
if (isset($args['password']) || isset($parts['pass'])) {
98+
$pass = isset($args['password']) ? $args['password'] : rawurldecode($parts['pass']);
99+
$promise = $promise->then(function (StreamingClient $client) use ($pass) {
100+
return $client->auth($pass)->then(
78101
function () use ($client) {
79102
return $client;
80103
},
@@ -91,9 +114,11 @@ function ($error) use ($client) {
91114
});
92115
}
93116

94-
if (isset($parts['db'])) {
95-
$promise = $promise->then(function (StreamingClient $client) use ($parts) {
96-
return $client->select($parts['db'])->then(
117+
// use `?db=1` query or `/1` path (skip first slash)
118+
if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) {
119+
$db = isset($args['db']) ? $args['db'] : substr($parts['path'], 1);
120+
$promise = $promise->then(function (StreamingClient $client) use ($db) {
121+
return $client->select($db)->then(
97122
function () use ($client) {
98123
return $client;
99124
},
@@ -113,7 +138,7 @@ function ($error) use ($client) {
113138
$promise->then(array($deferred, 'resolve'), array($deferred, 'reject'));
114139

115140
// use timeout from explicit ?timeout=x parameter or default to PHP's default_socket_timeout (60)
116-
$timeout = isset($parts['timeout']) ? $parts['timeout'] : (int) ini_get("default_socket_timeout");
141+
$timeout = isset($args['timeout']) ? (float) $args['timeout'] : (int) ini_get("default_socket_timeout");
117142
if ($timeout < 0) {
118143
return $deferred->promise();
119144
}
@@ -138,63 +163,4 @@ public function createLazyClient($target)
138163
{
139164
return new LazyClient($target, $this, $this->loop);
140165
}
141-
142-
/**
143-
* @param string $target
144-
* @return array with keys authority, auth and db
145-
* @throws InvalidArgumentException
146-
*/
147-
private function parseUrl($target)
148-
{
149-
$ret = array();
150-
// support `redis+unix://` scheme for Unix domain socket (UDS) paths
151-
if (preg_match('/^redis\+unix:\/\/([^:]*:[^@]*@)?(.+?)(\?.*)?$/', $target, $match)) {
152-
$ret['authority'] = 'unix://' . $match[2];
153-
$target = 'redis://' . (isset($match[1]) ? $match[1] : '') . 'localhost' . (isset($match[3]) ? $match[3] : '');
154-
}
155-
156-
if (strpos($target, '://') === false) {
157-
$target = 'redis://' . $target;
158-
}
159-
160-
$parts = parse_url($target);
161-
if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss'))) {
162-
throw new InvalidArgumentException('Given URL can not be parsed');
163-
}
164-
165-
if (isset($parts['pass'])) {
166-
$ret['auth'] = rawurldecode($parts['pass']);
167-
}
168-
169-
if (isset($parts['path']) && $parts['path'] !== '') {
170-
// skip first slash
171-
$ret['db'] = substr($parts['path'], 1);
172-
}
173-
174-
if (!isset($ret['authority'])) {
175-
$ret['authority'] =
176-
($parts['scheme'] === 'rediss' ? 'tls://' : '') .
177-
$parts['host'] . ':' .
178-
(isset($parts['port']) ? $parts['port'] : 6379);
179-
}
180-
181-
if (isset($parts['query'])) {
182-
$args = array();
183-
parse_str($parts['query'], $args);
184-
185-
if (isset($args['password'])) {
186-
$ret['auth'] = $args['password'];
187-
}
188-
189-
if (isset($args['db'])) {
190-
$ret['db'] = $args['db'];
191-
}
192-
193-
if (isset($args['timeout'])) {
194-
$ret['timeout'] = (float) $args['timeout'];
195-
}
196-
}
197-
198-
return $ret;
199-
}
200166
}

tests/FactoryStreamingClientTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@ public function testWillWriteAuthCommandIfRedisUnixUriContainsPasswordQueryParam
136136
$this->factory->createClient('redis+unix:///tmp/redis.sock?password=world');
137137
}
138138

139+
public function testWillNotWriteAnyCommandIfRedisUnixUriContainsNoPasswordOrDb()
140+
{
141+
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
142+
$stream->expects($this->never())->method('write');
143+
144+
$this->connector->expects($this->once())->method('connect')->with('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream));
145+
$this->factory->createClient('redis+unix:///tmp/redis.sock');
146+
}
147+
139148
public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo()
140149
{
141150
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();

0 commit comments

Comments
 (0)