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 @@ -22,13 +23,6 @@ */ trait MemcachedTrait { - private static $defaultClientOptions = array( - 'persistent_id' => null, - 'username' => null, - 'password' => null, - 'serializer' => 'php', - ); - private $client; private $lazyClient; @@ -58,134 +52,38 @@ private function init(\Memcached $client, $namespace, $defaultLifetime) } /** - * Creates a Memcached instance. - * - * By default, the binary protocol, no block, and libketama compatible options are enabled. - * - * Examples for servers: - * - 'memcached://user:pass@localhost?weight=33' - * - array(array('localhost', 11211, 33)) - * - * @param array[]|string|string[] An array of servers, a DSN, or an array of DSNs - * @param array An array of options - * - * @return \Memcached - * - * @throws \ErrorEception When invalid options or servers are provided + * @see MemcachedFactory::create() */ public static function createConnection($servers, array $options = array()) { - if (is_string($servers)) { - $servers = array($servers); - } elseif (!is_array($servers)) { - throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, %s given.', gettype($servers))); + @trigger_error(sprintf('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.', __METHOD__), E_USER_DEPRECATED); + + foreach ((array) $servers as $i => $dsn) { + if (is_array($dsn)) { + @trigger_error(sprintf('Passing an array of array to 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 with an array of Dsn instead.', __METHOD__), E_USER_DEPRECATED); + + $scheme = 'memcached://'; + $host = isset($dsn['host']) ? $dsn['host'] : ''; + $port = isset($dsn['port']) ? ':'.$dsn['port'] : ''; + $user = isset($dsn['user']) ? $dsn['user'] : ''; + $pass = isset($dsn['pass']) ? ':'.$dsn['pass'] : ''; + $pass = ($user || $pass) ? "$pass@" : ''; + $path = isset($dsn['path']) ? $dsn['path'] : ''; + $query = isset($dsn['query']) ? '?'.$dsn['query'] : ''; + $fragment = isset($dsn['fragment']) ? '#'.$dsn['fragment'] : ''; + + $servers[$i] = "$scheme$user$pass$host$port$path$query$fragment"; + } } + if (!static::isSupported()) { throw new CacheException('Memcached >= 2.2.0 is required'); } - set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); }); - try { - $options += static::$defaultClientOptions; - $client = new \Memcached($options['persistent_id']); - $username = $options['username']; - $password = $options['password']; - - // parse any DSN in $servers - foreach ($servers as $i => $dsn) { - if (is_array($dsn)) { - continue; - } - 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[$i] = 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(); + try { + return MemcachedFactory::create($servers, $options); + } catch (\Symfony\Component\Dsn\Exception\InvalidArgumentException $e) { + throw new InvalidArgumentException($e->getMessage(), 0, $e); } } diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index 467e602070c7a..c36462de64236 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -11,12 +11,12 @@ namespace Symfony\Component\Cache\Traits; -use Predis\Connection\Factory; use Predis\Connection\Aggregate\ClusterInterface; use Predis\Connection\Aggregate\PredisCluster; use Predis\Connection\Aggregate\RedisCluster; use Predis\Response\Status; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\Dsn\Factory\RedisFactory; /** * @author Aurimas Niekis @@ -26,14 +26,6 @@ */ trait RedisTrait { - private static $defaultConnectionOptions = array( - 'class' => null, - 'persistent' => 0, - 'persistent_id' => null, - 'timeout' => 30, - 'read_timeout' => 0, - 'retry_interval' => 0, - ); private $redis; /** @@ -55,90 +47,17 @@ public function init($redisClient, $namespace = '', $defaultLifetime = 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 + * @see RedisFactory::create() */ public static function createConnection($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]; - } + @trigger_error(sprintf('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.', __METHOD__), E_USER_DEPRECATED); - 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])); + try { + return RedisFactory::create($dsn, $options); + } catch (\Symfony\Component\Dsn\Exception\InvalidArgumentException $e) { + throw new InvalidArgumentException($e->getMessage(), 0, $e); } - 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/Cache/composer.json b/src/Symfony/Component/Cache/composer.json index e13cd9675160f..80aa0c275a978 100644 --- a/src/Symfony/Component/Cache/composer.json +++ b/src/Symfony/Component/Cache/composer.json @@ -30,7 +30,11 @@ "cache/integration-tests": "dev-master", "doctrine/cache": "~1.6", "doctrine/dbal": "~2.4", - "predis/predis": "~1.0" + "predis/predis": "~1.0", + "symfony/dsn": "3.4" + }, + "suggest": { + "symfony/dsn": "For configuring redis and memcached adapters with Dsn" }, "conflict": { "symfony/var-dumper": "<3.3" diff --git a/src/Symfony/Component/Dsn/.gitignore b/src/Symfony/Component/Dsn/.gitignore new file mode 100644 index 0000000000000..5414c2c655e72 --- /dev/null +++ b/src/Symfony/Component/Dsn/.gitignore @@ -0,0 +1,3 @@ +composer.lock +phpunit.xml +vendor/ diff --git a/src/Symfony/Component/Dsn/CHANGELOG.md b/src/Symfony/Component/Dsn/CHANGELOG.md new file mode 100644 index 0000000000000..8e992b982a9b9 --- /dev/null +++ b/src/Symfony/Component/Dsn/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +3.4.0 +----- + + * added the component diff --git a/src/Symfony/Component/Dsn/ConnectionFactory.php b/src/Symfony/Component/Dsn/ConnectionFactory.php new file mode 100644 index 0000000000000..ccaa0195b3b1b --- /dev/null +++ b/src/Symfony/Component/Dsn/ConnectionFactory.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Dsn; + +use Symfony\Component\Dsn\Exception\InvalidArgumentException; +use Symfony\Component\Dsn\Factory\MemcachedFactory; +use Symfony\Component\Dsn\Factory\RedisFactory; + +/** + * Factory for undetermined DSN. + * + * @author Jérémy Derussé + */ +final class ConnectionFactory +{ + const TYPE_REDIS = 'redis'; + const TYPE_MEMCACHED = 'memcached'; + + /** + * @param string $dsn + * + * @return string + */ + public static function getType($dsn) + { + 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 static::TYPE_REDIS; + } + if (0 === strpos($dsn, 'memcached://')) { + return static::TYPE_MEMCACHED; + } + + throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn)); + } + + /** + * Create a connection for a given DSN. + * + * @param string $dsn + * @param array $options + * + * @return mixed + */ + public static function create($dsn, array $options = array()) + { + switch (static::getType($dsn)) { + case static::TYPE_REDIS: + return RedisFactory::create($dsn, $options); + case static::TYPE_MEMCACHED: + return MemcachedFactory::create($dsn, $options); + default: + throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn)); + } + } +} diff --git a/src/Symfony/Component/Dsn/Exception/ExceptionInterface.php b/src/Symfony/Component/Dsn/Exception/ExceptionInterface.php new file mode 100644 index 0000000000000..06e9634fc0882 --- /dev/null +++ b/src/Symfony/Component/Dsn/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Dsn\Exception; + +/** + * Base ExceptionInterface for the Dsn Component. + * + * @author Jérémy Derussé + */ +interface ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Dsn/Exception/InvalidArgumentException.php b/src/Symfony/Component/Dsn/Exception/InvalidArgumentException.php new file mode 100644 index 0000000000000..99e4c53177f0f --- /dev/null +++ b/src/Symfony/Component/Dsn/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Dsn\Exception; + +/** + * Base InvalidArgumentException for the Dsn component. + * + * @author Jérémy Derussé + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Dsn/Factory/MemcachedFactory.php b/src/Symfony/Component/Dsn/Factory/MemcachedFactory.php new file mode 100644 index 0000000000000..02ffe18584daa --- /dev/null +++ b/src/Symfony/Component/Dsn/Factory/MemcachedFactory.php @@ -0,0 +1,167 @@ + + * + * 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 Symfony\Component\Dsn\Exception\InvalidArgumentException; + +/** + * Factory for Memcached connections. + * + * @author Nicolas Grekas + */ +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 @@ + + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + diff --git a/src/Symfony/Component/Lock/Store/MemcachedStore.php b/src/Symfony/Component/Lock/Store/MemcachedStore.php index 4a2ffa3e02042..beaad69962084 100644 --- a/src/Symfony/Component/Lock/Store/MemcachedStore.php +++ b/src/Symfony/Component/Lock/Store/MemcachedStore.php @@ -58,128 +58,6 @@ public function __construct(\Memcached $memcached, $initialTtl = 300) $this->initialTtl = $initialTtl; } - /** - * Creates a Memcached instance. - * - * By default, the binary protocol, block, and libketama compatible options are enabled. - * - * Example DSN: - * - 'memcached://user:pass@localhost?weight=33' - * - array(array('localhost', 11211, 33)) - * - * @param string $dsn A server or A DSN - * @param array $options An array of options - * - * @return \Memcached - * - * @throws \ErrorEception When invalid options or server are provided - */ - public static function createConnection($server, array $options = array()) - { - if (!static::isSupported()) { - throw new InvalidArgumentException('Memcached extension is required'); - } - set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); }); - try { - $options += static::$defaultClientOptions; - $client = new \Memcached($options['persistent_id']); - $username = $options['username']; - $password = $options['password']; - - // parse any DSN in $server - if (is_string($server)) { - if (0 !== strpos($server, 'memcached://')) { - throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s does not start with "memcached://"', $server)); - } - $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://'; - }, $server); - if (false === $params = parse_url($params)) { - throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $server)); - } - if (!isset($params['host']) && !isset($params['path'])) { - throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $server)); - } - 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; - } - - $server = 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, false); - 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(); - 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(array($server)); - $client->resetServerList(); - } - } - $client->addServers(array($server)); - - 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(); - } - } - /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Lock/Store/RedisStore.php b/src/Symfony/Component/Lock/Store/RedisStore.php index 66a067dfb0f9d..9f17e49b78668 100644 --- a/src/Symfony/Component/Lock/Store/RedisStore.php +++ b/src/Symfony/Component/Lock/Store/RedisStore.php @@ -53,88 +53,6 @@ public function __construct($redisClient, $initialTtl = 300.0) $this->initialTtl = $initialTtl; } - /** - * 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 createConnection($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])); - } - $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'] = isset($params['host']) ? 'tcp' : 'unix'; - $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; - } - /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Lock/Store/StoreFactory.php b/src/Symfony/Component/Lock/Store/StoreFactory.php index 9a23cf6472dea..eccaeaf97473c 100644 --- a/src/Symfony/Component/Lock/Store/StoreFactory.php +++ b/src/Symfony/Component/Lock/Store/StoreFactory.php @@ -20,21 +20,6 @@ */ class StoreFactory { - public static function createConnection($dsn, array $options = array()) - { - if (!is_string($dsn)) { - throw new InvalidArgumentException(sprintf('The %s() method expects argument #1 to be string, %s given.', __METHOD__, gettype($dsn))); - } - if (0 === strpos($dsn, 'redis://')) { - return RedisStore::createConnection($dsn, $options); - } - if (0 === strpos($dsn, 'memcached://')) { - return MemcachedStore::createConnection($dsn, $options); - } - - throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn)); - } - /** * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|\Memcached $connection * diff --git a/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php index cfe03b25e2c34..eb030fba0f9de 100644 --- a/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php @@ -54,100 +54,4 @@ public function testAbortAfterExpiration() { $this->markTestSkipped('Memcached expects a TTL greater than 1 sec. Simulating a slow network is too hard'); } - - public function testDefaultOptions() - { - $this->assertTrue(MemcachedStore::isSupported()); - - $client = MemcachedStore::createConnection('memcached://127.0.0.1'); - - $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 = MemcachedStore::createConnection($dsn); - $client3 = MemcachedStore::createConnection(array($host, $port)); - $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, $client3->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 = MemcachedStore::createConnection($dsn, $options); - - foreach ($expectedOptions as $option => $expect) { - $this->assertSame($expect, $client->getOption($option)); - } - } - - public function provideDsnWithOptions() - { - if (!class_exists('\Memcached')) { - self::markTestSkipped('Extension memcached required.'); - } - - 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/Lock/composer.json b/src/Symfony/Component/Lock/composer.json index 9a6e768723bb9..e333e93670a98 100644 --- a/src/Symfony/Component/Lock/composer.json +++ b/src/Symfony/Component/Lock/composer.json @@ -23,6 +23,9 @@ "require-dev": { "predis/predis": "~1.0" }, + "suggest": { + "symfony/dsn": "For configuring redis and memcached stores with Dsn" + }, "autoload": { "psr-4": { "Symfony\\Component\\Lock\\": "" }, "exclude-from-classmap": [