8000 New DSN component by Nyholm · Pull Request #36999 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

New DSN component #36999

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Fixed tests
  • Loading branch information
Nyholm committed Jun 13, 2020
commit 10427c7773ae457eec0958a8fbd0a55ed0d9ce77
23 changes: 23 additions & 0 deletions src/Symfony/Component/Dsn/Exception/FailedToConnectException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Symfony\Component\Dsn\Exception;

/**
* When we cannot connect to Redis, Memcached etc.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class FailedToConnectException extends InvalidArgumentException
{
}
19 changes: 10 additions & 9 deletions src/Symfony/Component/Dsn/Factory/MemcachedFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,23 +93,24 @@ public static function create(string $dsnString): object
} elseif ($dsn instanceof Path) {
$params['host'] = $path;
$servers[] = [$path, null, $params['weight']];
} else {
foreach ($dsn->getParameter('hosts', []) as $host => $weight) {
if (false === $port = strrpos($host, ':')) {
$hosts[$host] = [$host, 11211, (int) $weight];
} else {
$hosts[$host] = [substr($host, 0, $port), (int) substr($host, 1 + $port), (int) $weight];
}
}

$hosts = [];
foreach ($dsn->getParameter('host', []) as $host => $weight) {
if (false === $port = strrpos($host, ':')) {
$hosts[$host] = [$host, 11211, (int) $weight];
} else {
$hosts[$host] = [substr($host, 0, $port), (int) substr($host, 1 + $port), (int) $weight];
}
$servers = array_merge($servers, array_values($hosts));
}
$servers = array_merge($servers, array_values($hosts));

$params += $dsn->getParameters();
$options = $dsn->getParameters() + $options;
}

// set client's options
unset($options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']);
unset($options['host'], $options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']);
$options = array_change_key_case($options, CASE_UPPER);
$client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
$client->setOption(\Memcached::OPT_NO_BLOCK, true);
Expand Down
52 changes: 32 additions & 20 deletions src/Symfony/Component/Dsn/Factory/RedisFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
use Symfony\Component\Dsn\Configuration\Url;
use Symfony\Component\Dsn\ConnectionFactoryInterface;
use Symfony\Component\Dsn\DsnParser;
use Symfony\Component\Dsn\Exception\FailedToConnectException;
use Symfony\Component\Dsn\Exception\FunctionNotSupportedException;
use Symfony\Component\Dsn\Exception\InvalidArgumentException;
use Symfony\Component\Dsn\Exception\InvalidDsnException;

/**
* @author Nicolas Grekas <p@tchwork.com>
Expand All @@ -44,10 +46,17 @@ class RedisFactory implements ConnectionFactoryInterface
'failover' => 'none',
];

/**
* Example DSN strings.
*
* - redis://localhost:6379?timeout=10
* - redis(redis://127.0.0.1)?persistent_id=foobar
* - redis(redis://127.0.0.1/20 redis://127.0.0.2?timeout=10)?lazy=1
*/
public static function create(string $dsnString): object
{
$rootDsn = DsnParser::parseFunc($dsnString);
if ('dsn' !== $rootDsn->getName() && 'memcached' !== $rootDsn->getName()) {
if ('dsn' !== $rootDsn->getName() && 'redis' !== $rootDsn->getName()) {
throw new FunctionNotSupportedException($dsnString, $rootDsn->getName());
}
$params = $rootDsn->getParameters() + self::$defaultConnectionOptions;
Expand All @@ -59,7 +68,7 @@ public static function create(string $dsnString): object
throw new InvalidArgumentException('Only one DSN function is allowed.');
}
if ('redis' !== $dsn->getScheme() && 'rediss' !== $dsn->getScheme()) {
throw new InvalidArgumentException(sprintf('Invalid Redis DSN: "%s" does not start with "redis:" or "rediss".', $dsn));
throw new InvalidDsnException($dsn->__toString(), 'Invalid Redis DSN: The scheme must be "redis:" or "rediss".');
}

$auth = $dsn->getPassword() ?? $dsn->getUser();
Expand All @@ -74,27 +83,30 @@ public static function create(string $dsnString): object
array_unshift($hosts, ['scheme' => 'tcp', 'host' => $dsn->getHost(), 'port' => $dsn->getPort() ?? 6379]);
} elseif ($dsn instanceof Path) {
array_unshift($hosts, ['scheme' => 'unix', 'path' => $path]);
} else {
foreach ($dsn->getParameter('hosts', []) as $host => $parameters) {
if (\is_string($parameters)) {
parse_str($parameters, $parameters);
}
if (false === $i = strrpos($host, ':')) {
$hosts[$host] = ['scheme' => 'tcp', 'host' => $host, 'port' => 6379] + $parameters;
} elseif ($port = (int) substr($host, 1 + $i)) {
$hosts[$host] = ['scheme' => 'tcp', 'host' => substr($host, 0, $i), 'port' => $port] + $parameters;
} else {
$hosts[$host] = ['scheme' => 'unix', 'path' => substr($host, 0, $i)] + $parameters;
}
}
$hosts = array_values($hosts);
}

foreach ($dsn->getParameter('host', []) as $host => $parameters) {
if (\is_string($parameters)) {
parse_str($parameters, $parameters);
}
if (false === $i = strrpos($host, ':')) {
$hosts[$host] = ['scheme' => 'tcp', 'host' => $host, 'port' => 6379] + $parameters;
} elseif ($port = (int) substr($host, 1 + $i)) {
$hosts[$host] = ['scheme' => 'tcp', 'host' => substr($host, 0, $i), 'port' => $port] + $parameters;
} else {
$hosts[$host] = ['scheme' => 'unix', 'path' => substr($host, 0, $i)] + $parameters;
}
}
$hosts = array_values($hosts);
$params = $dsn->getParameters() + $params;
}

if (empty($hosts)) {
throw new InvalidDsnException($dsnString, 'Invalid Redis DSN: The DSN does not contain any hosts.');
}

if (isset($params['redis_sentinel']) && !class_exists(\Predis\Client::class)) {
throw new CacheException(sprintf('Redis Sentinel support requires the "predis/predis" package: "%s".', $dsn));
throw new InvalidArgumentException(sprintf('Redis Sentinel support requires the "predis/predis" package: "%s".', $dsn));
}

if (null === $params['class'] && !isset($params['redis_sentinel']) && \extension_loaded('redis')) {
Expand All @@ -111,23 +123,23 @@ public static function create(string $dsnString): object
try {
@$redis->{$connect}($hosts[0]['host'] ?? $hosts[0]['path'], $hosts[0]['port'] ?? null, (float) $params['timeout'], (string) $params['persistent_id'], $params['retry_interval']);
} catch (\RedisException $e) {
throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: ', $dsn).$e->getMessage());
throw new FailedToConnectException(sprintf('Redis connection "%s" failed: ', $dsn).$e->getMessage(), 0, $e);
}

set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; });
$isConnected = $redis->isConnected();
restore_error_handler();
if (!$isConnected) {
$error = preg_match('/^Redis::p?connect\(\): (.*)/', $error, $error) ? sprintf(' (%s)', $error[1]) : '';
throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: ', $dsn).$error.'.');
throw new FailedToConnectException(sprintf('Redis connection "%s" failed: ', $dsn).$error.'.');
}

if ((null !== $auth && !$redis->auth($auth))
|| ($params['dbindex'] && !$redis->select($params['dbindex']))
|| ($params['read_timeout'] && !$redis->setOption(\Redis::OPT_READ_TIMEOUT, $params['read_timeout']))
) {
$e = preg_replace('/^ERR /', '', $redis->getLastError());
throw new InvalidArgumentException(sprintf('Redis connection "%s" failed: ', $dsn).$e.'.');
throw new FailedToConnectException(sprintf('Redis connection "%s" failed: ', $dsn).$e.'.');
}

if (0 < $params['tcp_keepalive'] && \defined('Redis::OPT_TCP_KEEPALIVE')) {
Expand Down
67 changes: 65 additions & 2 deletions src/Symfony/Component/Dsn/Tests/Factory/MemcachedFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function provideServersSetting()
'localhost',
11222,
];
if (ini_get('memcached.use_sasl')) {
if (filter_var(ini_get('memcached.use_sasl'), FILTER_VALIDATE_BOOLEAN)) {
yield [
'memcached://user:password@127.0.0.1?weight=50',
'127.0.0.1',
Expand All @@ -92,7 +92,7 @@ public function provideServersSetting()
'/var/local/run/memcached.socket',
0,
];
if (ini_get('memcached.use_sasl')) {
if (filter_var(ini_get('memcached.use_sasl'), FILTER_VALIDATE_BOOLEAN)) {
yield [
'memcached://user:password@/var/local/run/memcached.socket?weight=25',
'/var/local/run/memcached.socket',
Expand Down Expand Up @@ -124,4 +124,67 @@ public function provideDsnWithOptions()
[\Memcached::OPT_SOCKET_RECV_SIZE => 1, \Memcached::OPT_SOCKET_SEND_SIZE => 2, \Memcached::OPT_RETRY_TIMEOUT => 8],
];
}

public function testMultiServerDsn()
{
$dsn = 'memcached:?host[localhost]&host[localhost:12345]&host[/some/memcached.sock:]=3';
$client = MemcachedFactory::create($dsn);

$expected = [
0 => [
'host' => 'localhost',
'port' => 11211,
'type' => 'TCP',
],
1 => [
'host' => 'localhost',
'port' => 12345,
'type' => 'TCP',
],
2 => [
'host' => '/some/memcached.sock',
'port' => 0,
'type' => 'SOCKET',
],
];
$this->assertSame($expected, $client->getServerList());

$dsn = 'memcached://localhost?host[foo.bar]=3';
$client = MemcachedFactory::create($dsn);

$expected = [
0 => [
'host' => 'localhost',
'port' => 11211,
'type' => 'TCP',
],
1 => [
'host' => 'foo.bar',
'port' => 11211,
'type' => 'TCP',
],
];
$this->assertSame($expected, $client->getServerList());

$dsn = 'memcached(memcached://localhost memcached://localhost:12345 memcached:///some/memcached.sock?weight=3)';
$client = MemcachedFactory::create($dsn);
$expected = [
0 => [
'host' => 'localhost',
'port' => 11211,
'type' => 'TCP',
],
1 => [
'host' => 'localhost',
'port' => 12345,
'type' => 'TCP',
],
2 => [
'host' => '/some/memcached.sock',
'port' => 0,
'type' => 'SOCKET',
],
];
$this->assertSame($expected, $client->getServerList());
}
}
37 changes: 29 additions & 8 deletions src/Symfony/Component/Dsn/Tests/Factory/RedisFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
namespace Symfony\Component\Dsn\Tests\Adapter;

use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Dsn\Exception\FailedToConnectException;
use Symfony\Component\Dsn\Exception\InvalidArgumentException;
use Symfony\Component\Dsn\Exception\InvalidDsnException;
use Symfony\Component\Dsn\Factory\RedisFactory;

/**
Expand All @@ -22,22 +25,30 @@
*/
class RedisFactoryTest extends TestCase
{
public function testCreate()
/**
* @dataProvider provideValidSchemes
*/
public function testCreate(string $dsnScheme)
{
$redis = RedisFactory::create($dsnScheme.':?host[h1]&host[h2]&host[/foo:]');
$this->assertInstanceOf(\RedisArray::class, $redis);
$this->assertSame(['h1:6379', 'h2:6379', '/foo'], $redis->_hosts());
@$redis = null; // some versions of phpredis connect on destruct, let's silence the warning

$redisHost = getenv('REDIS_HOST');

$redis = RedisFactory::create('redis://'.$redisHost);
$redis = RedisFactory::create($dsnScheme.'://'.$redisHost);
$this->assertInstanceOf(\Redis::class, $redis);
$this->assertTrue($redis->isConnected());
$this->assertSame(0, $redis->getDbNum());

$redis = RedisFactory::create('redis://'.$redisHost.'/2');
$redis = RedisFactory::create($dsnScheme.'://'.$redisHost.'/2');
$this->assertSame(2, $redis->getDbNum());

$redis = RedisFactory::create('redis://'.$redisHost.'?timeout=4');
$redis = RedisFactory::create($dsnScheme.'://'.$redisHost.'?timeout=4');
$this->assertEquals(4, $redis->getTimeout());

$redis = RedisFactory::create('redis://'.$redisHost.'?read_timeout=5');
$redis = RedisFactory::create($dsnScheme.'://'.$redisHost.'?read_timeout=5');
$this->assertEquals(5, $redis->getReadTimeout());
}

Expand All @@ -46,8 +57,8 @@ public function testCreate()
*/
public function testFailedCreate($dsn)
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Redis connection failed');
$this->expectException(FailedToConnectException::class);
$this->expectExceptionMessage('Redis connection "'.$dsn.'" failed');
RedisFactory::create($dsn);
}

Expand All @@ -63,7 +74,7 @@ public function provideFailedCreate()
*/
public function testInvalidCreate($dsn)
{
$this->expectException(InvalidArgumentException::class);
$this->expectException(InvalidDsnException::class);
$this->expectExceptionMessage('Invalid Redis DSN');
RedisFactory::create($dsn);
}
Expand All @@ -73,4 +84,14 @@ public function provideInvalidCreate()
yield ['foo://localhost'];
yield ['redis://'];
}


public function provideValidSchemes(): array
{
return [
['redis'],
['rediss'],
];
}

}
0