diff --git a/UPGRADE-3.4.md b/UPGRADE-3.4.md index f6d137842b400..394af68d0b2a4 100644 --- a/UPGRADE-3.4.md +++ b/UPGRADE-3.4.md @@ -1,6 +1,13 @@ UPGRADE FROM 3.3 to 3.4 ======================= +Cache +----- + +* The `AbstractAdapter::createConnection()`, `RedisTrait::createConnection()` + and `MemcachedTrait::createConnection()` methods have been deprecated and + will be removed in 4.0. Use the Dsn component instead. + DependencyInjection ------------------- diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index 8174f1a4d266d..40e2e6c136b58 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -1,6 +1,13 @@ UPGRADE FROM 3.x to 4.0 ======================= +Cache +----- + + * The `AbstractAdapter::createConnection()`, `RedisTrait::createConnection()` + and `MemcachedTrait::createConnection()` methods have been removed. Use the + Dsn component instead. + ClassLoader ----------- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php index dafe0b7b8fce4..ed0d1a4d7b229 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php @@ -17,8 +17,10 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\Dsn\ConnectionFactory; /** * @author Nicolas Grekas
@@ -133,7 +135,7 @@ public static function getServiceProvider(ContainerBuilder $container, $name) if (!$container->hasDefinition($name = 'cache_connection.'.ContainerBuilder::hash($dsn))) { $definition = new Definition(AbstractAdapter::class); $definition->setPublic(false); - $definition->setFactory(array(AbstractAdapter::class, 'createConnection')); + $definition->setFactory(array(ConnectionFactory::class, 'createConnection')); $definition->setArguments(array($dsn)); $container->setDefinition($name, $definition); } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 4c7b4dfe67716..b1df7aeca7e70 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -43,6 +43,7 @@ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; +use Symfony\Component\Dsn\ConnectionFactory; use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -1706,7 +1707,7 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont if (!$container->hasDefinition($connectionDefinitionId = $container->hash($storeDsn))) { $connectionDefinition = new Definition(\stdClass::class); $connectionDefinition->setPublic(false); - $connectionDefinition->setFactory(array(StoreFactory::class, 'createConnection')); + $connectionDefinition->setFactory(array(ConnectionFactory::class, 'createConnection')); $connectionDefinition->setArguments(array($storeDsn)); $container->setDefinition($connectionDefinitionId, $connectionDefinition); } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 18065e6782306..4fc7b87c4987c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -37,6 +37,7 @@ "symfony/browser-kit": "~2.8|~3.0|~4.0", "symfony/console": "~3.4|~4.0", "symfony/css-selector": "~2.8|~3.0|~4.0", + "symfony/dsn": "~3.4", "symfony/dom-crawler": "~2.8|~3.0|~4.0", "symfony/polyfill-intl-icu": "~1.0", "symfony/security": "~2.8|~3.0|~4.0", diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index a2fae2fc2f548..122668f9b5c06 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -19,6 +19,10 @@ use Symfony\Component\Cache\Exception\InvalidArgumentException; use Symfony\Component\Cache\ResettableInterface; use Symfony\Component\Cache\Traits\AbstractTrait; +use Symfony\Component\Dsn\ConnectionFactory; +use Symfony\Component\Dsn\Exception\InvalidArgumentException as DsnInvalidArgumentException; +use Symfony\Component\Dsn\Factory\MemcachedFactory; +use Symfony\Component\Dsn\Factory\RedisFactory; /** * @author Nicolas Grekas
@@ -128,14 +132,19 @@ public static function createSystemCache($namespace, $defaultLifetime, $version,
public static function createConnection($dsn, array $options = array())
{
- if (!is_string($dsn)) {
- throw new InvalidArgumentException(sprintf('The %s() method expect argument #1 to be string, %s given.', __METHOD__, gettype($dsn)));
- }
- if (0 === strpos($dsn, 'redis://')) {
- return RedisAdapter::createConnection($dsn, $options);
+ @trigger_error(sprintf('The %s() method is deprecated since version 3.4 and will be removed in 4.0. Use the ConnectionFactory::create() method from Dsn component instead.', __METHOD__), E_USER_DEPRECATED);
+
+ try {
+ $type = ConnectionFactory::getType($dsn);
+ } catch (DsnInvalidArgumentException $e) {
+ throw new InvalidArgumentException($e->getMessage(), 0, $e);
}
- if (0 === strpos($dsn, 'memcached://')) {
- return MemcachedAdapter::createConnection($dsn, $options);
+
+ switch ($type) {
+ case ConnectionFactory::TYPE_MEMCACHED:
+ return MemcachedFactory::create($dsn, $options);
+ case ConnectionFactory::TYPE_REDIS:
+ return RedisFactory::create($dsn, $options);
}
throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
diff --git a/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php b/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php
index 3acf8bdb86c6a..f9f35e46a8b38 100644
--- a/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php
+++ b/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php
@@ -25,7 +25,7 @@ class MemcachedAdapter extends AbstractAdapter
* Using a MemcachedAdapter with a TagAwareAdapter for storing tags is discouraged.
* Using a RedisAdapter is recommended instead. If you cannot do otherwise, be aware that:
* - the Memcached::OPT_BINARY_PROTOCOL must be enabled
- * (that's the default when using MemcachedAdapter::createConnection());
+ * (that's the default when using MemcachedFactory::create());
* - tags eviction by Memcached's LRU algorithm will break by-tags invalidation;
* your Memcached memory should be large enough to never trigger LRU.
*
diff --git a/src/Symfony/Component/Cache/CHANGELOG.md b/src/Symfony/Component/Cache/CHANGELOG.md
index 11c1b9364ebd5..514b1bbc975aa 100644
--- a/src/Symfony/Component/Cache/CHANGELOG.md
+++ b/src/Symfony/Component/Cache/CHANGELOG.md
@@ -9,6 +9,8 @@ CHANGELOG
* added prune logic to FilesystemTrait, PhpFilesTrait, PdoTrait, TagAwareAdapter and ChainTrait
* now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, PhpFilesCache, PdoAdapter, PdoCache, ChainAdapter, and
ChainCache implement PruneableInterface and support manual stale cache pruning
+ * deprecated `AbstractAdapter::createConnection()`, `RedisTrait::createConnection()` and
+ `MemcachedTrait::createConnection()`
3.3.0
-----
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php
index 76e0608006173..5632ec9ea623b 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php
@@ -11,8 +11,8 @@
namespace Symfony\Component\Cache\Tests\Adapter;
-use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
+use Symfony\Component\Dsn\Factory\MemcachedFactory;
class MemcachedAdapterTest extends AdapterTestCase
{
@@ -28,7 +28,7 @@ public static function setupBeforeClass()
if (!MemcachedAdapter::isSupported()) {
self::markTestSkipped('Extension memcached >=2.2.0 required.');
}
- self::$client = AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST'), array('binary_protocol' => false));
+ self::$client = MemcachedFactory::create('memcached://'.getenv('MEMCACHED_HOST'), array('binary_protocol' => false));
self::$client->get('foo');
$code = self::$client->getResultCode();
@@ -39,29 +39,24 @@ public static function setupBeforeClass()
public function createCachePool($defaultLifetime = 0)
{
- $client = $defaultLifetime ? AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST')) : self::$client;
+ $client = $defaultLifetime ? MemcachedFactory::create('memcached://'.getenv('MEMCACHED_HOST')) : self::$client;
return new MemcachedAdapter($client, str_replace('\\', '.', __CLASS__), $defaultLifetime);
}
- public function testOptions()
+ /**
+ * @group legacy
+ * @expectedDeprecation The %s() method is deprecated since version 3.4 and will be removed in 4.0. Use the MemcachedFactory::create() method from Dsn component instead.
+ */
+ public function testCreateConnectionDeprecated()
{
- $client = MemcachedAdapter::createConnection(array(), array(
- 'libketama_compatible' => false,
- 'distribution' => 'modula',
- 'compression' => true,
- 'serializer' => 'php',
- 'hash' => 'md5',
- ));
-
- $this->assertSame(\Memcached::SERIALIZER_PHP, $client->getOption(\Memcached::OPT_SERIALIZER));
- $this->assertSame(\Memcached::HASH_MD5, $client->getOption(\Memcached::OPT_HASH));
- $this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION));
- $this->assertSame(0, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE));
- $this->assertSame(\Memcached::DISTRIBUTION_MODULA, $client->getOption(\Memcached::OPT_DISTRIBUTION));
+ $client = MemcachedAdapter::createConnection('memcached://localhost');
+
+ $this->assertInstanceOf(\Memcached::class, $client);
}
/**
+ * @group legacy
* @dataProvider provideBadOptions
* @expectedException \ErrorException
* @expectedExceptionMessage constant(): Couldn't find constant Memcached::
@@ -81,6 +76,9 @@ public function provideBadOptions()
);
}
+ /**
+ * @group legacy
+ */
public function testDefaultOptions()
{
$this->assertTrue(MemcachedAdapter::isSupported());
@@ -93,6 +91,7 @@ public function testDefaultOptions()
}
/**
+ * @group legacy
* @expectedException \Symfony\Component\Cache\Exception\CacheException
* @expectedExceptionMessage MemcachedAdapter: "serializer" option must be "php" or "igbinary".
*/
@@ -106,6 +105,7 @@ public function testOptionSerializer()
}
/**
+ * @group legacy
* @dataProvider provideServersSetting
*/
public function testServersSetting($dsn, $host, $port)
@@ -163,6 +163,7 @@ public function provideServersSetting()
}
/**
+ * @group legacy
* @dataProvider provideDsnWithOptions
*/
public function testDsnWithOptions($dsn, array $options, array $expectedOptions)
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php
index 85ca36c9ef263..928a6eac9f8d1 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php
@@ -22,6 +22,18 @@ public static function setupBeforeClass()
self::$redis = new \Predis\Client(array('host' => getenv('REDIS_HOST')));
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The %s() method is deprecated since version 3.4 and will be removed in 4.0. Use the RedisFactory::create() method from Dsn component instead.
+ */
+ public function testCreateConnectionDeprecated()
+ {
+ RedisAdapter::createConnection('redis://'.getenv('REDIS_HOST'));
+ }
+
+ /**
+ * @group legacy
+ */
public function testCreateConnection()
{
$redisHost = getenv('REDIS_HOST');
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php
index a8f7a673f8b87..9c559fec50f92 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php
@@ -13,15 +13,30 @@
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;
+use Symfony\Component\Dsn\Factory\RedisFactory;
class RedisAdapterTest extends AbstractRedisAdapterTest
{
public static function setupBeforeClass()
{
parent::setupBeforeClass();
- self::$redis = AbstractAdapter::createConnection('redis://'.getenv('REDIS_HOST'));
+ self::$redis = RedisFactory::create('redis://'.getenv('REDIS_HOST'));
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The %s() method is deprecated since version 3.4 and will be removed in 4.0. Use the RedisFactory::create() method from Dsn component instead.
+ */
+ public function testCreateConnectionDeprecated()
+ {
+ $client = RedisAdapter::createConnection('redis://'.getenv('REDIS_HOST'));
+
+ $this->assertInstanceOf(\Redis::class, $client);
+ }
+
+ /**
+ * @group legacy
+ */
public function testCreateConnection()
{
$redisHost = getenv('REDIS_HOST');
@@ -45,6 +60,7 @@ public function testCreateConnection()
}
/**
+ * @group legacy
* @dataProvider provideFailedCreateConnection
* @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
* @expectedExceptionMessage Redis connection failed
@@ -64,6 +80,7 @@ public function provideFailedCreateConnection()
}
/**
+ * @group legacy
* @dataProvider provideInvalidCreateConnection
* @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
* @expectedExceptionMessage Invalid Redis DSN
diff --git a/src/Symfony/Component/Cache/Tests/Simple/MemcachedCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/MemcachedCacheTest.php
index c4af891af7ba7..8fefa1ba7c23a 100644
--- a/src/Symfony/Component/Cache/Tests/Simple/MemcachedCacheTest.php
+++ b/src/Symfony/Component/Cache/Tests/Simple/MemcachedCacheTest.php
@@ -11,8 +11,8 @@
namespace Symfony\Component\Cache\Tests\Simple;
-use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Simple\MemcachedCache;
+use Symfony\Component\Dsn\Factory\MemcachedFactory;
class MemcachedCacheTest extends CacheTestCase
{
@@ -29,7 +29,7 @@ public static function setupBeforeClass()
if (!MemcachedCache::isSupported()) {
self::markTestSkipped('Extension memcached >=2.2.0 required.');
}
- self::$client = AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST'));
+ self::$client = MemcachedFactory::create('memcached://'.getenv('MEMCACHED_HOST'));
self::$client->get('foo');
$code = self::$client->getResultCode();
@@ -40,11 +40,25 @@ public static function setupBeforeClass()
public function createSimpleCache($defaultLifetime = 0)
{
- $client = $defaultLifetime ? AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST'), array('binary_protocol' => false)) : self::$client;
+ $client = $defaultLifetime ? MemcachedFactory::create('memcached://'.getenv('MEMCACHED_HOST'), array('binary_protocol' => false)) : self::$client;
return new MemcachedCache($client, str_replace('\\', '.', __CLASS__), $defaultLifetime);
}
+ /**
+ * @group legacy
+ * @expectedDeprecation This "%s" method is deprecated.
+ */
+ public function testCreateConnection()
+ {
+ $client = MemcachedCache::createConnection('memcached://localhost');
+
+ $this->assertInstanceOf(\Memcached::class, $client);
+ }
+
+ /**
+ * @group legacy
+ */
public function testOptions()
{
$client = MemcachedCache::createConnection(array(), array(
@@ -63,6 +77,7 @@ public function testOptions()
}
/**
+ * @group legacy
* @dataProvider provideBadOptions
* @expectedException \ErrorException
* @expectedExceptionMessage constant(): Couldn't find constant Memcached::
@@ -82,6 +97,9 @@ public function provideBadOptions()
);
}
+ /**
+ * @group legacy
+ */
public function testDefaultOptions()
{
$this->assertTrue(MemcachedCache::isSupported());
@@ -94,6 +112,7 @@ public function testDefaultOptions()
}
/**
+ * @group legacy
* @expectedException \Symfony\Component\Cache\Exception\CacheException
* @expectedExceptionMessage MemcachedAdapter: "serializer" option must be "php" or "igbinary".
*/
@@ -107,6 +126,7 @@ public function testOptionSerializer()
}
/**
+ * @group legacy
* @dataProvider provideServersSetting
*/
public function testServersSetting($dsn, $host, $port)
diff --git a/src/Symfony/Component/Cache/Tests/Simple/RedisCacheTest.php b/src/Symfony/Component/Cache/Tests/Simple/RedisCacheTest.php
index d33421f9aae46..f6cd14821a28b 100644
--- a/src/Symfony/Component/Cache/Tests/Simple/RedisCacheTest.php
+++ b/src/Symfony/Component/Cache/Tests/Simple/RedisCacheTest.php
@@ -12,15 +12,28 @@
namespace Symfony\Component\Cache\Tests\Simple;
use Symfony\Component\Cache\Simple\RedisCache;
+use Symfony\Component\Dsn\Factory\RedisFactory;
class RedisCacheTest extends AbstractRedisCacheTest
{
public static function setupBeforeClass()
{
parent::setupBeforeClass();
- self::$redis = RedisCache::createConnection('redis://'.getenv('REDIS_HOST'));
+ self::$redis = RedisFactory::create('redis://'.getenv('REDIS_HOST'));
}
+ /**
+ * @group legacy
+ * @expectedDeprecation The %s() method is deprecated since version 3.4 and will be removed in 4.0. Use the RedisFactory::create() method from Dsn component instead.
+ */
+ public function testCreateConnectionDeprecated()
+ {
+ RedisCache::createConnection('redis://'.getenv('REDIS_HOST'));
+ }
+
+ /**
+ * @group legacy
+ */
public function testCreateConnection()
{
$redisHost = getenv('REDIS_HOST');
@@ -44,6 +57,7 @@ public function testCreateConnection()
}
/**
+ * @group legacy
* @dataProvider provideFailedCreateConnection
* @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
* @expectedExceptionMessage Redis connection failed
@@ -63,6 +77,7 @@ public function provideFailedCreateConnection()
}
/**
+ * @group legacy
* @dataProvider provideInvalidCreateConnection
* @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
* @expectedExceptionMessage Invalid Redis DSN
diff --git a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php
index 29b92647c9bde..bdd1c77753a0e 100644
--- a/src/Symfony/Component/Cache/Traits/MemcachedTrait.php
+++ b/src/Symfony/Component/Cache/Traits/MemcachedTrait.php
@@ -13,6 +13,7 @@
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
+use Symfony\Component\Dsn\Factory\MemcachedFactory;
/**
* @author Rob Frawley 2nd
+ */
+class MemcachedFactory
+{
+ private static $defaultClientOptions = array(
+ 'class' => null,
+ 'persistent_id' => null,
+ 'username' => null,
+ 'password' => null,
+ 'serializer' => 'php',
+ );
+
+ /**
+ * Creates a Memcached instance.
+ *
+ * By default, the binary protocol, no block, and libketama compatible options are enabled.
+ *
+ * Examples for servers:
+ * - memcached://localhost
+ * - memcached://example.com:1234
+ * - memcached://user:pass@example.com
+ * - memcached://localhost?weight=25
+ * - memcached:///var/run/memcached.sock?weight=25
+ * - memcached://user:password@/var/run/memcached.socket?weight=25
+ * - array('memcached://server1', 'memcached://server2')
+ *
+ * @param string|string[] A DSN, or an array of DSNs
+ * @param array An array of options
+ *
+ * @return \Memcached According to the "class" option
+ *
+ * @throws \ErrorException When invalid options or servers are provided
+ */
+ public static function create($dsns, array $options = array())
+ {
+ set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
+ try {
+ $options += static::$defaultClientOptions;
+
+ $class = null === $options['class'] ? \Memcached::class : $options['class'];
+ unset($options['class']);
+ if (is_a($class, \Memcached::class, true)) {
+ $client = new \Memcached($options['persistent_id']);
+ } elseif (class_exists($class, false)) {
+ throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Memcached"', $class));
+ } else {
+ throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $class));
+ }
+
+ $username = $options['username'];
+ $password = $options['password'];
+
+ // parse any DSN in $dsns
+ $servers = array();
+ foreach ((array) $dsns as $dsn) {
+ if (0 !== strpos($dsn, 'memcached://')) {
+ throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s does not start with "memcached://"', $dsn));
+ }
+ $params = preg_replace_callback('#^memcached://(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
+ if (!empty($m[1])) {
+ list($username, $password) = explode(':', $m[1], 2) + array(1 => null);
+ }
+
+ return 'file://';
+ }, $dsn);
+ if (false === $params = parse_url($params)) {
+ throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
+ }
+ if (!isset($params['host']) && !isset($params['path'])) {
+ throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
+ }
+ if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
+ $params['weight'] = $m[1];
+ $params['path'] = substr($params['path'], 0, -strlen($m[0]));
+ }
+ $params += array(
+ 'host' => isset($params['host']) ? $params['host'] : $params['path'],
+ 'port' => isset($params['host']) ? 11211 : null,
+ 'weight' => 0,
+ );
+ if (isset($params['query'])) {
+ parse_str($params['query'], $query);
+ $params += $query;
+ $options = $query + $options;
+ }
+
+ $servers[] = array($params['host'], $params['port'], $params['weight']);
+ }
+
+ // set client's options
+ unset($options['persistent_id'], $options['username'], $options['password'], $options['weight']);
+ $options = array_change_key_case($options, CASE_UPPER);
+ $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
+ $client->setOption(\Memcached::OPT_NO_BLOCK, true);
+ if (!array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) {
+ $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
+ }
+ foreach ($options as $name => $value) {
+ if (is_int($name)) {
+ continue;
+ }
+ if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
+ $value = constant('Memcached::'.$name.'_'.strtoupper($value));
+ }
+ $opt = constant('Memcached::OPT_'.$name);
+
+ unset($options[$name]);
+ $options[$opt] = $value;
+ }
+ $client->setOptions($options);
+
+ // set client's servers, taking care of persistent connections
+ if (!$client->isPristine()) {
+ $oldServers = array();
+ foreach ($client->getServerList() as $server) {
+ $oldServers[] = array($server['host'], $server['port']);
+ }
+
+ $newServers = array();
+ foreach ($servers as $server) {
+ if (1 < count($server)) {
+ $server = array_values($server);
+ unset($server[2]);
+ $server[1] = (int) $server[1];
+ }
+ $newServers[] = $server;
+ }
+
+ if ($oldServers !== $newServers) {
+ // before resetting, ensure $servers is valid
+ $client->addServers($servers);
+ $client->resetServerList();
+ }
+ }
+ $client->addServers($servers);
+
+ if (null !== $username || null !== $password) {
+ if (!method_exists($client, 'setSaslAuthData')) {
+ trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
+ }
+ $client->setSaslAuthData($username, $password);
+ }
+
+ return $client;
+ } finally {
+ restore_error_handler();
+ }
+ }
+}
diff --git a/src/Symfony/Component/Dsn/Factory/RedisFactory.php b/src/Symfony/Component/Dsn/Factory/RedisFactory.php
new file mode 100644
index 0000000000000..c9d22f5e72e23
--- /dev/null
+++ b/src/Symfony/Component/Dsn/Factory/RedisFactory.php
@@ -0,0 +1,119 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Dsn\Factory;
+
+use Predis\Connection\Factory;
+use Symfony\Component\Dsn\Exception\InvalidArgumentException;
+
+/**
+ * Factory for Redis connections.
+ *
+ * @author Nicolas Grekas
+ */
+class RedisFactory
+{
+ private static $defaultConnectionOptions = array(
+ 'class' => null,
+ 'persistent' => 0,
+ 'persistent_id' => null,
+ 'timeout' => 30,
+ 'read_timeout' => 0,
+ 'retry_interval' => 0,
+ );
+
+ /**
+ * Creates a Redis connection using a DSN configuration.
+ *
+ * Example DSN:
+ * - redis://localhost
+ * - redis://example.com:1234
+ * - redis://secret@example.com/13
+ * - redis:///var/run/redis.sock
+ * - redis://secret@/var/run/redis.sock/13
+ *
+ * @param string $dsn
+ * @param array $options See self::$defaultConnectionOptions
+ *
+ * @throws InvalidArgumentException when the DSN is invalid
+ *
+ * @return \Redis|\Predis\Client According to the "class" option
+ */
+ public static function create($dsn, array $options = array())
+ {
+ if (0 !== strpos($dsn, 'redis://')) {
+ throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://"', $dsn));
+ }
+ $params = preg_replace_callback('#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
+ if (isset($m[1])) {
+ $auth = $m[1];
+ }
+
+ return 'file://';
+ }, $dsn);
+ if (false === $params = parse_url($params)) {
+ throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
+ }
+ if (!isset($params['host']) && !isset($params['path'])) {
+ throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
+ }
+ if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
+ $params['dbindex'] = $m[1];
+ $params['path'] = substr($params['path'], 0, -strlen($m[0]));
+ }
+ if (isset($params['host'])) {
+ $scheme = 'tcp';
+ } else {
+ $scheme = 'unix';
+ }
+ $params += array(
+ 'host' => isset($params['host']) ? $params['host'] : $params['path'],
+ 'port' => isset($params['host']) ? 6379 : null,
+ 'dbindex' => 0,
+ );
+ if (isset($params['query'])) {
+ parse_str($params['query'], $query);
+ $params += $query;
+ }
+ $params += $options + self::$defaultConnectionOptions;
+ $class = null === $params['class'] ? (extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class'];
+
+ if (is_a($class, \Redis::class, true)) {
+ $connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect';
+ $redis = new $class();
+ @$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']);
+
+ if (@!$redis->isConnected()) {
+ $e = ($e = error_get_last()) && preg_match('/^Redis::p?connect\(\): (.*)/', $e['message'], $e) ? sprintf(' (%s)', $e[1]) : '';
+ throw new InvalidArgumentException(sprintf('Redis connection failed%s: %s', $e, $dsn));
+ }
+
+ 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 failed (%s): %s', $e, $dsn));
+ }
+ } elseif (is_a($class, \Predis\Client::class, true)) {
+ $params['scheme'] = $scheme;
+ $params['database'] = $params['dbindex'] ?: null;
+ $params['password'] = $auth;
+ $redis = new $class((new Factory())->create($params));
+ } elseif (class_exists($class, false)) {
+ throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis" or "Predis\Client"', $class));
+ } else {
+ throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $class));
+ }
+
+ return $redis;
+ }
+}
diff --git a/src/Symfony/Component/Dsn/LICENSE b/src/Symfony/Component/Dsn/LICENSE
new file mode 100644
index 0000000000000..ce39894f6a9a2
--- /dev/null
+++ b/src/Symfony/Component/Dsn/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2016-2017 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/src/Symfony/Component/Dsn/README.md b/src/Symfony/Component/Dsn/README.md
new file mode 100644
index 0000000000000..be5ccc8fbf1b6
--- /dev/null
+++ b/src/Symfony/Component/Dsn/README.md
@@ -0,0 +1,14 @@
+Dsn Component
+=============
+
+Provides utilities for creating connections using DSN (Data Source Names)
+strings.
+
+Resources
+---------
+
+ * [Documentation](https://symfony.com/doc/master/components/dsn.html)
+ * [Contributing](https://symfony.com/doc/current/contributing/index.html)
+ * [Report issues](https://github.com/symfony/symfony/issues) and
+ [send Pull Requests](https://github.com/symfony/symfony/pulls)
+ in the [main Symfony repository](https://github.com/symfony/symfony)
diff --git a/src/Symfony/Component/Dsn/Tests/ConnectionFactoryTest.php b/src/Symfony/Component/Dsn/Tests/ConnectionFactoryTest.php
new file mode 100644
index 0000000000000..fd5a60521c36f
--- /dev/null
+++ b/src/Symfony/Component/Dsn/Tests/ConnectionFactoryTest.php
@@ -0,0 +1,69 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Dsn\Tests\Adapter;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Dsn\ConnectionFactory;
+
+/**
+ * @requires extension redis
+ * @requires extension memcached
+ */
+class ConnectionFactoryTest extends TestCase
+{
+ /**
+ * @dataProvider provideValidDsn
+ */
+ public function testGetType($dsn, $type)
+ {
+ $this->assertSame($type, ConnectionFactory::getType($dsn));
+ }
+
+ /**
+ * @dataProvider provideInvalidDsn
+ * @expectedException \Symfony\Component\Dsn\Exception\InvalidArgumentException
+ */
+ public function testGetTypeInvalid($dsn)
+ {
+ ConnectionFactory::getType($dsn);
+ }
+
+ /**
+ * @dataProvider provideValidDsn
+ */
+ public function testCreate($dsn, $type, $objectClass)
+ {
+ $this->assertInstanceOf($objectClass, ConnectionFactory::create($dsn));
+ }
+
+ /**
+ * @dataProvider provideInvalidDsn
+ * @expectedException \Symfony\Component\Dsn\Exception\InvalidArgumentException
+ */
+ public function testCreateInvalid($dsn)
+ {
+ ConnectionFactory::create($dsn);
+ }
+
+ public function provideValidDsn()
+ {
+ yield array('redis://localhost', ConnectionFactory::TYPE_REDIS, \Redis::class);
+ yield array('memcached://localhost', ConnectionFactory::TYPE_MEMCACHED, \Memcached::class);
+ }
+
+ public function provideInvalidDsn()
+ {
+ yield array(array('http://localhost'));
+ yield array('http://localhost');
+ yield array('mysql://localhost');
+ }
+}
diff --git a/src/Symfony/Component/Dsn/Tests/Factory/MemcachedFactoryTest.php b/src/Symfony/Component/Dsn/Tests/Factory/MemcachedFactoryTest.php
new file mode 100644
index 0000000000000..3636301022f66
--- /dev/null
+++ b/src/Symfony/Component/Dsn/Tests/Factory/MemcachedFactoryTest.php
@@ -0,0 +1,148 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Adapter;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Dsn\Factory\MemcachedFactory;
+
+/**
+ * @requires extension memcached
+ */
+class MemcachedFactoryTest extends TestCase
+{
+ public function testOptions()
+ {
+ $client = MemcachedFactory::create(array(), array(
+ 'libketama_compatible' => false,
+ 'distribution' => 'modula',
+ 'compression' => true,
+ 'serializer' => 'php',
+ 'hash' => 'md5',
+ ));
+
+ $this->assertSame(\Memcached::SERIALIZER_PHP, $client->getOption(\Memcached::OPT_SERIALIZER));
+ $this->assertSame(\Memcached::HASH_MD5, $client->getOption(\Memcached::OPT_HASH));
+ $this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION));
+ if (version_compare(phpversion('memcached'), '2.2.0', '>=')) {
+ $this->assertSame(0, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE));
+ }
+ $this->assertSame(\Memcached::DISTRIBUTION_MODULA, $client->getOption(\Memcached::OPT_DISTRIBUTION));
+ }
+
+ /**
+ * @dataProvider provideBadOptions
+ * @expectedException \ErrorException
+ * @expectedExceptionMessage constant(): Couldn't find constant Memcached::
+ */
+ public function testBadOptions($name, $value)
+ {
+ MemcachedFactory::create(array(), array($name => $value));
+ }
+
+ public function provideBadOptions()
+ {
+ yield array('foo', 'bar');
+ yield array('hash', 'zyx');
+ yield array('serializer', 'zyx');
+ yield array('distribution', 'zyx');
+ }
+
+ public function testDefaultOptions()
+ {
+ $client = MemcachedFactory::create(array());
+
+ $this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION));
+ $this->assertSame(1, $client->getOption(\Memcached::OPT_BINARY_PROTOCOL));
+ $this->assertSame(1, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE));
+ }
+
+ /**
+ * @dataProvider provideServersSetting
+ */
+ public function testServersSetting($dsn, $host, $port)
+ {
+ $client1 = MemcachedFactory::create($dsn);
+ $client2 = MemcachedFactory::create(array($dsn));
+ $expect = array(
+ 'host' => $host,
+ 'port' => $port,
+ );
+
+ $f = function ($s) { return array('host' => $s['host'], 'port' => $s['port']); };
+ $this->assertSame(array($expect), array_map($f, $client1->getServerList()));
+ $this->assertSame(array($expect), array_map($f, $client2->getServerList()));
+ }
+
+ public function provideServersSetting()
+ {
+ yield array(
+ 'memcached://127.0.0.1/50',
+ '127.0.0.1',
+ 11211,
+ );
+ yield array(
+ 'memcached://localhost:11222?weight=25',
+ 'localhost',
+ 11222,
+ );
+ if (ini_get('memcached.use_sasl')) {
+ yield array(
+ 'memcached://user:password@127.0.0.1?weight=50',
+ '127.0.0.1',
+ 11211,
+ );
+ }
+ yield array(
+ 'memcached:///var/run/memcached.sock?weight=25',
+ '/var/run/memcached.sock',
+ 0,
+ );
+ yield array(
+ 'memcached:///var/local/run/memcached.socket?weight=25',
+ '/var/local/run/memcached.socket',
+ 0,
+ );
+ if (ini_get('memcached.use_sasl')) {
+ yield array(
+ 'memcached://user:password@/var/local/run/memcached.socket?weight=25',
+ '/var/local/run/memcached.socket',
+ 0,
+ );
+ }
+ }
+
+ /**
+ * @dataProvider provideDsnWithOptions
+ */
+ public function testDsnWithOptions($dsn, array $options, array $expectedOptions)
+ {
+ $client = MemcachedFactory::create($dsn, $options);
+
+ foreach ($expectedOptions as $option => $expect) {
+ $this->assertSame($expect, $client->getOption($option));
+ }
+ }
+
+ public function provideDsnWithOptions()
+ {
+ yield array(
+ 'memcached://localhost:11222?retry_timeout=10',
+ array(\Memcached::OPT_RETRY_TIMEOUT => 8),
+ array(\Memcached::OPT_RETRY_TIMEOUT => 10),
+ );
+ yield array(
+ 'memcached://localhost:11222?socket_recv_size=1&socket_send_size=2',
+ array(\Memcached::OPT_RETRY_TIMEOUT => 8),
+ array(\Memcached::OPT_SOCKET_RECV_SIZE => 1, \Memcached::OPT_SOCKET_SEND_SIZE => 2, \Memcached::OPT_RETRY_TIMEOUT => 8),
+ );
+ }
+}
diff --git a/src/Symfony/Component/Dsn/Tests/Factory/RedisFactoryTest.php b/src/Symfony/Component/Dsn/Tests/Factory/RedisFactoryTest.php
new file mode 100644
index 0000000000000..af5cb644bbe88
--- /dev/null
+++ b/src/Symfony/Component/Dsn/Tests/Factory/RedisFactoryTest.php
@@ -0,0 +1,76 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Dsn\Tests\Adapter;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Dsn\Factory\RedisFactory;
+
+/**
+ * @requires extension redis
+ */
+class RedisFactoryTest extends TestCase
+{
+ public function testCreate()
+ {
+ $redisHost = getenv('REDIS_HOST');
+
+ $redis = RedisFactory::create('redis://'.$redisHost);
+ $this->assertInstanceOf(\Redis::class, $redis);
+ $this->assertTrue($redis->isConnected());
+ $this->assertSame(0, $redis->getDbNum());
+
+ $redis = RedisFactory::create('redis://'.$redisHost.'/2');
+ $this->assertSame(2, $redis->getDbNum());
+
+ $redis = RedisFactory::create('redis://'.$redisHost, array('timeout' => 3));
+ $this->assertEquals(3, $redis->getTimeout());
+
+ $redis = RedisFactory::create('redis://'.$redisHost.'?timeout=4');
+ $this->assertEquals(4, $redis->getTimeout());
+
+ $redis = RedisFactory::create('redis://'.$redisHost, array('read_timeout' => 5));
+ $this->assertEquals(5, $redis->getReadTimeout());
+ }
+
+ /**
+ * @dataProvider provideFailedCreate
+ * @expectedException \Symfony\Component\Dsn\Exception\InvalidArgumentException
+ * @expectedExceptionMessage Redis connection failed
+ */
+ public function testFailedCreate($dsn)
+ {
+ RedisFactory::create($dsn);
+ }
+
+ public function provideFailedCreate()
+ {
+ yield array('redis://localhost:1234');
+ yield array('redis://foo@localhost');
+ yield array('redis://localhost/123');
+ }
+
+ /**
+ * @dataProvider provideInvalidCreate
+ * @expectedException \Symfony\Component\Dsn\Exception\InvalidArgumentException
+ * @expectedExceptionMessage Invalid Redis DSN
+ */
+ public function testInvalidCreate($dsn)
+ {
+ RedisFactory::create($dsn);
+ }
+
+ public function provideInvalidCreate()
+ {
+ yield array('foo://localhost');
+ yield array('redis://');
+ }
+}
diff --git a/src/Symfony/Component/Dsn/composer.json b/src/Symfony/Component/Dsn/composer.json
new file mode 100644
index 0000000000000..1697134a93270
--- /dev/null
+++ b/src/Symfony/Component/Dsn/composer.json
@@ -0,0 +1,36 @@
+{
+ "name": "symfony/dsn",
+ "type": "library",
+ "description": "Symfony Dsn Component",
+ "keywords": ["dsn", "redis", "memcached"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": "^5.5.9|>=7.0.8"
+ },
+ "require-dev": {
+ "predis/predis": "~1.0"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\Dsn\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.4-dev"
+ }
+ }
+}
diff --git a/src/Symfony/Component/Dsn/phpunit.xml.dist b/src/Symfony/Component/Dsn/phpunit.xml.dist
new file mode 100644
index 0000000000000..fec89ae6cdf42
--- /dev/null
+++ b/src/Symfony/Component/Dsn/phpunit.xml.dist
@@ -0,0 +1,31 @@
+
+
+