*/
class MemcachedAdapter extends AbstractAdapter
{
+ private static $defaultClientOptions = array(
+ 'persistent_id' => null,
+ 'username' => null,
+ 'password' => null,
+ );
+
+ protected $maxIdLength = 250;
+
private $client;
+ public static function isSupported()
+ {
+ return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
+ }
+
public function __construct(\Memcached $client, $namespace = '', $defaultLifetime = 0)
{
+ if (!static::isSupported()) {
+ throw new CacheException('Memcached >= 2.2.0 is required');
+ }
+ $opt = $client->getOption(\Memcached::OPT_SERIALIZER);
+ if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
+ throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
+ }
+ if (!$client->getOption(\Memcached::OPT_BINARY_PROTOCOL)) {
+ throw new CacheException('MemcachedAdapter: "binary_protocol" option must be enabled.');
+ }
+ $this->maxIdLength -= strlen($client->getOption(\Memcached::OPT_PREFIX_KEY));
+
parent::__construct($namespace, $defaultLifetime);
$this->client = $client;
}
- public static function isSupported()
+ /**
+ * 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.
+ */
+ public static function createConnection($servers, array $options = array())
{
- return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
+ 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)));
+ }
+ set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
+ try {
+ if (!static::isSupported()) {
+ throw new trigger_error('Memcached >= 2.2.0 is required');
+ }
+ $options += static::$defaultClientOptions;
+ $client = new \Memcached($options['persistent_id']);
+ $username = $options['username'];
+ $password = $options['password'];
+ unset($options['persistent_id'], $options['username'], $options['password']);
+ $options = array_change_key_case($options, CASE_UPPER);
+
+ // set client's options
+ $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);
+
+ // 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;
+ }
+
+ $servers[$i] = array($params['host'], $params['port'], $params['weight']);
+ }
+
+ // 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();
+ }
}
/**
@@ -34,7 +189,7 @@ public static function isSupported()
*/
protected function doSave(array $values, $lifetime)
{
- return $this->client->setMulti($values, $lifetime) && $this->client->getResultCode() === \Memcached::RES_SUCCESS;
+ return $this->checkResultCode($this->client->setMulti($values, $lifetime));
}
/**
@@ -42,7 +197,7 @@ protected function doSave(array $values, $lifetime)
*/
protected function doFetch(array $ids)
{
- return $this->client->getMulti($ids);
+ return $this->checkResultCode($this->client->getMulti($ids));
}
/**
@@ -50,7 +205,7 @@ protected function doFetch(array $ids)
*/
protected function doHave($id)
{
- return $this->client->get($id) !== false || $this->client->getResultCode() === \Memcached::RES_SUCCESS;
+ return false !== $this->client->get($id) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode());
}
/**
@@ -58,14 +213,14 @@ protected function doHave($id)
*/
protected function doDelete(array $ids)
{
- $toDelete = count($ids);
- foreach ($this->client->deleteMulti($ids) as $result) {
- if (\Memcached::RES_SUCCESS === $result || \Memcached::RES_NOTFOUND === $result) {
- --$toDelete;
+ $ok = true;
+ foreach ($this->checkResultCode($this->client->deleteMulti($ids)) as $result) {
+ if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) {
+ $ok = false;
}
}
- return 0 === $toDelete;
+ return $ok;
}
/**
@@ -73,6 +228,17 @@ protected function doDelete(array $ids)
*/
protected function doClear($namespace)
{
- return $this->client->flush();
+ return $this->checkResultCode($this->client->flush());
+ }
+
+ private function checkResultCode($result)
+ {
+ $code = $this->client->getResultCode();
+
+ if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) {
+ return $result;
+ }
+
+ throw new CacheException(sprintf('MemcachedAdapter client error: %s.', strtolower($this->client->getResultMessage())));
}
}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php
index 55b4dc19b145d..7a766e073f60c 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Tests\Adapter;
+use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
class MemcachedAdapterTest extends AdapterTestCase
@@ -28,14 +29,13 @@ public static function setupBeforeClass()
if (!MemcachedAdapter::isSupported()) {
self::markTestSkipped('Extension memcached >=2.2.0 required.');
}
+ self::$client = AbstractAdapter::createConnection('memcached://'.getenv('MEMCACHED_HOST'));
+ self::$client->get('foo');
+ $code = self::$client->getResultCode();
- self::$client = new \Memcached();
- self::$client->addServers(array(array(
- getenv('MEMCACHED_HOST') ?: '127.0.0.1',
- getenv('MEMCACHED_PORT') ?: 11211,
- )));
-
- parent::setupBeforeClass();
+ if (\Memcached::RES_SUCCESS !== $code && \Memcached::RES_NOTFOUND !== $code) {
+ self::markTestSkipped('Memcached error: '.strtolower(self::$client->getResultMessage()));
+ }
}
public function createCachePool($defaultLifetime = 0)
@@ -43,8 +43,130 @@ public function createCachePool($defaultLifetime = 0)
return new MemcachedAdapter(self::$client, str_replace('\\', '.', __CLASS__), $defaultLifetime);
}
- public function testIsSupported()
+ public function testOptions()
+ {
+ $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));
+ }
+
+ /**
+ * @dataProvider provideBadOptions
+ * @expectedException \ErrorException
+ * @expectedExceptionMessage constant(): Couldn't find constant Memcached::
+ */
+ public function testBadOptions($name, $value)
+ {
+ MemcachedAdapter::createConnection(array(), array($name => $value));
+ }
+
+ public function provideBadOptions()
+ {
+ return array(
+ array('foo', 'bar'),
+ array('hash', 'zyx'),
+ array('serializer', 'zyx'),
+ array('distribution', 'zyx'),
+ );
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Cache\Exception\CacheException
+ * @expectedExceptionMessage MemcachedAdapter: "binary_protocol" option must be enabled.
+ */
+ public function testBinaryProtocol()
+ {
+ new MemcachedAdapter(MemcachedAdapter::createConnection(array(), array('binary_protocol' => false)));
+ }
+
+ public function testDefaultOptions()
{
$this->assertTrue(MemcachedAdapter::isSupported());
+
+ $client = MemcachedAdapter::createConnection(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));
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Cache\Exception\CacheException
+ * @expectedExceptionMessage MemcachedAdapter: "serializer" option must be "php" or "igbinary".
+ */
+ public function testOptionSerializer()
+ {
+ if (!\Memcached::HAVE_JSON) {
+ $this->markTestSkipped('Memcached::HAVE_JSON required');
+ }
+
+ new MemcachedAdapter(MemcachedAdapter::createConnection(array(), array('serializer' => 'json')));
+ }
+
+ /**
+ * @dataProvider provideServersSetting
+ */
+ public function testServersSetting($dsn, $host, $port)
+ {
+ $client1 = MemcachedAdapter::createConnection($dsn);
+ $client2 = MemcachedAdapter::createConnection(array($dsn));
+ $client3 = MemcachedAdapter::createConnection(array(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, $client2->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,
+ );
+ }
}
}
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php
index 301d531ac3530..a8f7a673f8b87 100644
--- a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php
+++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Tests\Adapter;
+use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;
class RedisAdapterTest extends AbstractRedisAdapterTest
@@ -18,8 +19,7 @@ class RedisAdapterTest extends AbstractRedisAdapterTest
public static function setupBeforeClass()
{
parent::setupBeforeClass();
- self::$redis = new \Redis();
- self::$redis->connect(getenv('REDIS_HOST'));
+ self::$redis = AbstractAdapter::createConnection('redis://'.getenv('REDIS_HOST'));
}
public function testCreateConnection()
diff --git a/src/Symfony/Component/Cache/phpunit.xml.dist b/src/Symfony/Component/Cache/phpunit.xml.dist
index 3e164e64b50d9..19b5496277219 100644
--- a/src/Symfony/Component/Cache/phpunit.xml.dist
+++ b/src/Symfony/Component/Cache/phpunit.xml.dist
@@ -9,6 +9,7 @@