Skip to content

Commit 10ea36a

Browse files
committed
Add the DSN component
1 parent 55a7691 commit 10ea36a

13 files changed

+719
-0
lines changed

src/Symfony/Component/Dsn/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
composer.lock
2+
phpunit.xml
3+
vendor/
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
3.4.0
5+
-----
6+
7+
* added the component
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Dsn;
13+
14+
use Symfony\Component\Dsn\Exception\InvalidArgumentException;
15+
use Symfony\Component\Dsn\Factory\MemcachedConnectionFactory;
16+
use Symfony\Component\Dsn\Factory\RedisConnectionFactory;
17+
18+
/**
19+
* Factory for undetermined Dsn.
20+
*
21+
* @author Jérémy Derussé <jeremy@derusse.com>
22+
*/
23+
class ConnectionFactory
24+
{
25+
const TYPE_REDIS = 1;
26+
const TYPE_MEMCACHED = 2;
27+
28+
/**
29+
* @param string $dsn
30+
*
31+
* @return int
32+
*/
33+
public static function getType($dsn)
34+
{
35+
if (!is_string($dsn)) {
36+
throw new InvalidArgumentException(sprintf('The %s() method expect argument #1 to be string, %s given.', __METHOD__, gettype($dsn)));
37+
}
38+
if (0 === strpos($dsn, 'redis://')) {
39+
return static::TYPE_REDIS;
40+
}
41+
if (0 === strpos($dsn, 'memcached://')) {
42+
return static::TYPE_MEMCACHED;
43+
}
44+
45+
throw new InvalidArgumentException(sprintf('Unsupported Dsn: %s.', $dsn));
46+
}
47+
48+
/**
49+
* Create a connection for a redis Dsn.
50+
*
51+
* @param string $dsn
52+
* @param array $options
53+
*
54+
* @return mixed
55+
*/
56+
public static function createConnection($dsn, array $options = array())
57+
{
58+
switch (static::getType($dsn)) {
59+
case static::TYPE_REDIS:
60+
return RedisConnectionFactory::createConnection($dsn, $options);
61+
case static::TYPE_MEMCACHED:
62+
return MemcachedConnectionFactory::createConnection($dsn, $options);
63+
default:
64+
throw new InvalidArgumentException(sprintf('Unsupported Dsn: %s.', $dsn));
65+
}
66+
}
67+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Dsn\Exception;
13+
14+
/**
15+
* Interface for exceptions.
16+
*
17+
* @author Jérémy Derussé <jeremy@derusse.com>
18+
*/
19+
interface ExceptionInterface
20+
{
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Dsn\Exception;
13+
14+
/**
15+
* Base InvalidArgumentException for the Dsn component.
16+
*
17+
* @author Jérémy Derussé <jeremy@derusse.com>
18+
*/
19+
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
20+
{
21+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Dsn\Factory;
13+
14+
use Symfony\Component\Dsn\Exception\InvalidArgumentException;
15+
16+
/**
17+
* Factory for Memcached connections.
18+
*
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
class MemcachedConnectionFactory
22+
{
23+
private static $defaultClientOptions = array(
24+
'persistent_id' => null,
25+
'username' => null,
26+
'password' => null,
27+
'serializer' => 'php',
28+
);
29+
30+
/**
31+
* Creates a Memcached instance.
32+
*
33+
* By default, the binary protocol, no block, and libketama compatible options are enabled.
34+
*
35+
* Examples for servers:
36+
* - memcached://localhost
37+
* - memcached://example.com:1234
38+
* - memcached://user:pass@example.com
39+
* - memcached://localhost?weight=25
40+
* - memcached:///var/run/memcached.sock?weight=25
41+
* - memcached://user:password@/var/run/memcached.socket?weight=25
42+
* - array('memcached://server1', 'memcached://server2')
43+
*
44+
* @param string|string[] A DSN, or an array of DSNs
45+
* @param array An array of options
46+
*
47+
* @return \Memcached
48+
*
49+
* @throws \ErrorException When invalid options or servers are provided
50+
*/
51+
public static function createConnection($dsns, array $options = array())
52+
{
53+
set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
54+
try {
55+
$options += static::$defaultClientOptions;
56+
$client = new \Memcached($options['persistent_id']);
57+
$username = $options['username'];
58+
$password = $options['password'];
59+
60+
// parse any DSN in $dsns
61+
$servers=[];
62+
foreach ((array) $dsns as $dsn) {
63+
if (0 !== strpos($dsn, 'memcached://')) {
64+
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s does not start with "memcached://"', $dsn));
65+
}
66+
$params = preg_replace_callback('#^memcached://(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
67+
if (!empty($m[1])) {
68+
list($username, $password) = explode(':', $m[1], 2) + array(1 => null);
69+
}
70+
71+
return 'file://';
72+
}, $dsn);
73+
if (false === $params = parse_url($params)) {
74+
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
75+
}
76+
if (!isset($params['host']) && !isset($params['path'])) {
77+
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
78+
}
79+
if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
80+
$params['weight'] = $m[1];
81+
$params['path'] = substr($params['path'], 0, -strlen($m[0]));
82+
}
83+
$params += array(
84+
'host' => isset($params['host']) ? $params['host'] : $params['path'],
85+
'port' => isset($params['host']) ? 11211 : null,
86+
'weight' => 0,
87+
);
88+
if (isset($params['query'])) {
89+
parse_str($params['query'], $query);
90+
$params += $query;
91+
$options = $query + $options;
92+
}
93+
94+
$servers[] = array($params['host'], $params['port'], $params['weight']);
95+
}
96+
97+
// set client's options
98+
unset($options['persistent_id'], $options['username'], $options['password'], $options['weight']);
99+
$options = array_change_key_case($options, CASE_UPPER);
100+
$client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
101+
$client->setOption(\Memcached::OPT_NO_BLOCK, true);
102+
if (!array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) {
103+
$client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
104+
}
105+
foreach ($options as $name => $value) {
106+
if (is_int($name)) {
107+
continue;
108+
}
109+
if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
110+
$value = constant('Memcached::'.$name.'_'.strtoupper($value));
111+
}
112+
$opt = constant('Memcached::OPT_'.$name);
113+
114+
unset($options[$name]);
115+
$options[$opt] = $value;
116+
}
117+
$client->setOptions($options);
118+
119+
// set client's servers, taking care of persistent connections
120+
if (!$client->isPristine()) {
121+
$oldServers = array();
122+
foreach ($client->getServerList() as $server) {
123+
$oldServers[] = array($server['host'], $server['port']);
124+
}
125+
126+
$newServers = array();
127+
foreach ($servers as $server) {
128+
if (1 < count($server)) {
129+
$server = array_values($server);
130+
unset($server[2]);
131+
$server[1] = (int) $server[1];
132+
}
133+
$newServers[] = $server;
134+
}
135+
136+
if ($oldServers !== $newServers) {
137+
// before resetting, ensure $servers is valid
138+
$client->addServers($servers);
139+
$client->resetServerList();
140+
}
141+
}
142+
$client->addServers($servers);
143+
144+
if (null !== $username || null !== $password) {
145+
if (!method_exists($client, 'setSaslAuthData')) {
146+
trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
147+
}
148+
$client->setSaslAuthData($username, $password);
149+
}
150+
151+
return $client;
152+
} finally {
153+
restore_error_handler();
154+
}
155+
}
156+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Dsn\Factory;
13+
14+
use Symfony\Component\Dsn\Exception\InvalidArgumentException;
15+
16+
/**
17+
* Factory for Redis Dsn.
18+
*
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
class RedisConnectionFactory
22+
{
23+
private static $defaultConnectionOptions = array(
24+
'class' => null,
25+
'persistent' => 0,
26+
'persistent_id' => null,
27+
'timeout' => 30,
28+
'read_timeout' => 0,
29+
'retry_interval' => 0,
30+
);
31+
32+
/**
33+
* Creates a Redis connection using a DSN configuration.
34+
*
35+
* Example DSN:
36+
* - redis://localhost
37+
* - redis://example.com:1234
38+
* - redis://secret@example.com/13
39+
* - redis:///var/run/redis.sock
40+
* - redis://secret@/var/run/redis.sock/13
41+
*
42+
* @param string $dsn
43+
* @param array $options See self::$defaultConnectionOptions
44+
*
45+
* @throws InvalidArgumentException when the DSN is invalid
46+
*
47+
* @return \Redis|\Predis\Client According to the "class" option
48+
*/
49+
public static function createConnection($dsn, array $options = array())
50+
{
51+
if (0 !== strpos($dsn, 'redis://')) {
52+
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://"', $dsn));
53+
}
54+
$params = preg_replace_callback('#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
55+
if (isset($m[1])) {
56+
$auth = $m[1];
57+
}
58+
59+
return 'file://';
60+
}, $dsn);
61+
if (false === $params = parse_url($params)) {
62+
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
63+
}
64+
if (!isset($params['host']) && !isset($params['path'])) {
65+
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
66+
}
67+
if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
68+
$params['dbindex'] = $m[1];
69+
$params['path'] = substr($params['path'], 0, -strlen($m[0]));
70+
}
71+
if (isset($params['host'])) {
72+
$scheme = 'tcp';
73+
} else {
74+
$scheme = 'unix';
75+
}
76+
$params += array(
77+
'host' => isset($params['host']) ? $params['host'] : $params['path'],
78+
'port' => isset($params['host']) ? 6379 : null,
79+
'dbindex' => 0,
80+
);
81+
if (isset($params['query'])) {
82+
parse_str($params['query'], $query);
83+
$params += $query;
84+
}
85+
$params += $options + self::$defaultConnectionOptions;
86+
$class = null === $params['class'] ? (extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class'];
87+
88+
if (is_a($class, \Redis::class, true)) {
89+
$connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect';
90+
$redis = new $class();
91+
@$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']);
92+
93+
if (@!$redis->isConnected()) {
94+
$e = ($e = error_get_last()) && preg_match('/^Redis::p?connect\(\): (.*)/', $e['message'], $e) ? sprintf(' (%s)', $e[1]) : '';
95+
throw new InvalidArgumentException(sprintf('Redis connection failed%s: %s', $e, $dsn));
96+
}
97+
98+
if ((null !== $auth && !$redis->auth($auth))
99+
|| ($params['dbindex'] && !$redis->select($params['dbindex']))
100+
|| ($params['read_timeout'] && !$redis->setOption(\Redis::OPT_READ_TIMEOUT, $params['read_timeout']))
101+
) {
102+
$e = preg_replace('/^ERR /', '', $redis->getLastError());
103+
throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e, $dsn));
104+
}
105+
} elseif (is_a($class, \Predis\Client::class, true)) {
106+
$params['scheme'] = $scheme;
107+
$params['database'] = $params['dbindex'] ?: null;
108+
$params['password'] = $auth;
109+
$redis = new $class((new Factory())->create($params));
110+
} elseif (class_exists($class, false)) {
111+
throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis" or "Predis\Client"', $class));
112+
} else {
113+
throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $class));
114+
}
115+
116+
return $redis;
117+
}
118+
}

0 commit comments

Comments
 (0)