Skip to content

Commit 9150b26

Browse files
feature #21108 [Cache] Add DSN, createClient & better error reporting to MemcachedAdapter (nicolas-grekas, robfrawley)
This PR was merged into the 3.3-dev branch. Discussion ---------- [Cache] Add DSN, createClient & better error reporting to MemcachedAdapter | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | symfony/symfony-docs#7265 Replaces #20863 ping @robfrawley: would you mind opening a doc PR for this? Commits ------- 87030b4 [cache] Add tests for MemcachedAdapter::createClient() e109438 [Cache] Add DSN, createClient & better error reporting to MemcachedAdapter
2 parents 3d50aee + 239e11a commit 9150b26

File tree

5 files changed

+325
-21
lines changed

5 files changed

+325
-21
lines changed

Adapter/AbstractAdapter.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ public static function createSystemCache($namespace, $defaultLifetime, $version,
115115
return new ChainAdapter(array($apcu, $fs));
116116
}
117117

118+
public static function createConnection($dsn, array $options = array())
119+
{
120+
if (!is_string($dsn)) {
121+
throw new InvalidArgumentException(sprintf('The %s() method expect argument #1 to be string, %s given.', __METHOD__, gettype($dsn)));
122+
}
123+
if (0 === strpos($dsn, 'redis://')) {
124+
return RedisAdapter::createConnection($dsn, $options);
125+
}
126+
if (0 === strpos($dsn, 'memcached://')) {
127+
return MemcachedAdapter::createConnection($dsn, $options);
128+
}
129+
130+
throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
131+
}
132+
118133
/**
119134
* Fetches several cache items.
120135
*

Adapter/MemcachedAdapter.php

Lines changed: 177 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,68 +11,234 @@
1111

1212
namespace Symfony\Component\Cache\Adapter;
1313

14+
use Symfony\Component\Cache\Exception\CacheException;
15+
use Symfony\Component\Cache\Exception\InvalidArgumentException;
16+
1417
/**
1518
* @author Rob Frawley 2nd <rmf@src.run>
19+
* @author Nicolas Grekas <p@tchwork.com>
1620
*/
1721
class MemcachedAdapter extends AbstractAdapter
1822
{
23+
private static $defaultClientOptions = array(
24+
'persistent_id' => null,
25+
'username' => null,
26+
'password' => null,
27+
);
28+
29+
protected $maxIdLength = 250;
30+
1931
private $client;
2032

33+
public static function isSupported()
34+
{
35+
return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
36+
}
37+
2138
public function __construct(\Memcached $client, $namespace = '', $defaultLifetime = 0)
2239
{
40+
if (!static::isSupported()) {
41+
throw new CacheException('Memcached >= 2.2.0 is required');
42+
}
43+
$opt = $client->getOption(\Memcached::OPT_SERIALIZER);
44+
if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
45+
throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
46+
}
47+
if (!$client->getOption(\Memcached::OPT_BINARY_PROTOCOL)) {
48+
throw new CacheException('MemcachedAdapter: "binary_protocol" option must be enabled.');
49+
}
50+
$this->maxIdLength -= strlen($client->getOption(\Memcached::OPT_PREFIX_KEY));
51+
2352
parent::__construct($namespace, $defaultLifetime);
2453
$this->client = $client;
2554
}
2655

27-
public static function isSupported()
56+
/**
57+
* Creates a Memcached instance.
58+
*
59+
* By default, the binary protocol, no block, and libketama compatible options are enabled.
60+
*
61+
* Examples for servers:
62+
* - 'memcached://user:pass@localhost?weight=33'
63+
* - array(array('localhost', 11211, 33))
64+
*
65+
* @param array[]|string|string[] An array of servers, a DSN, or an array of DSNs
66+
* @param array An array of options
67+
*
68+
* @return \Memcached
69+
*
70+
* @throws \ErrorEception When invalid options or servers are provided.
71+
*/
72+
public static function createConnection($servers, array $options = array())
2873
{
29-
return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
74+
if (is_string($servers)) {
75+
$servers = array($servers);
76+
} elseif (!is_array($servers)) {
77+
throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, %s given.', gettype($servers)));
78+
}
79+
set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
80+
try {
81+
if (!static::isSupported()) {
82+
throw new trigger_error('Memcached >= 2.2.0 is required');
83+
}
84+
$options += static::$defaultClientOptions;
85+
$client = new \Memcached($options['persistent_id']);
86+
$username = $options['username'];
87+
$password = $options['password'];
88+
unset($options['persistent_id'], $options['username'], $options['password']);
89+
$options = array_change_key_case($options, CASE_UPPER);
90+
91+
// set client's options
92+
$client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
93+
$client->setOption(\Memcached::OPT_NO_BLOCK, true);
94+
if (!array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) {
95+
$client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
96+
}
97+
foreach ($options as $name => $value) {
98+
if (is_int($name)) {
99+
continue;
100+
}
101+
if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
102+
$value = constant('Memcached::'.$name.'_'.strtoupper($value));
103+
}
104+
$opt = constant('Memcached::OPT_'.$name);
105+
106+
unset($options[$name]);
107+
$options[$opt] = $value;
108+
}
109+
$client->setOptions($options);
110+
111+
// parse any DSN in $servers
112+
foreach ($servers as $i => $dsn) {
113+
if (is_array($dsn)) {
114+
continue;
115+
}
116+
if (0 !== strpos($dsn, 'memcached://')) {
117+
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s does not start with "memcached://"', $dsn));
118+
}
119+
$params = preg_replace_callback('#^memcached://(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
120+
if (!empty($m[1])) {
121+
list($username, $password) = explode(':', $m[1], 2) + array(1 => null);
122+
}
123+
124+
return 'file://';
125+
}, $dsn);
126+
if (false === $params = parse_url($params)) {
127+
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
128+
}
129+
if (!isset($params['host']) && !isset($params['path'])) {
130+
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
131+
}
132+
if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
133+
$params['weight'] = $m[1];
134+
$params['path'] = substr($params['path'], 0, -strlen($m[0]));
135+
}
136+
$params += array(
137+
'host' => isset($params['host']) ? $params['host'] : $params['path'],
138+
'port' => isset($params['host']) ? 11211 : null,
139+
'weight' => 0,
140+
);
141+
if (isset($params['query'])) {
142+
parse_str($params['query'], $query);
143+
$params += $query;
144+
}
145+
146+
$servers[$i] = array($params['host'], $params['port'], $params['weight']);
147+
}
148+
149+
// set client's servers, taking care of persistent connections
150+
if (!$client->isPristine()) {
151+
$oldServers = array();
152+
foreach ($client->getServerList() as $server) {
153+
$oldServers[] = array($server['host'], $server['port']);
154+
}
155+
156+
$newServers = array();
157+
foreach ($servers as $server) {
158+
if (1 < count($server)) {
159+
$server = array_values($server);
160+
unset($server[2]);
161+
$server[1] = (int) $server[1];
162+
}
163+
$newServers[] = $server;
164+
}
165+
166+
if ($oldServers !== $newServers) {
167+
// before resetting, ensure $servers is valid
168+
$client->addServers($servers);
169+
$client->resetServerList();
170+
}
171+
}
172+
$client->addServers($servers);
173+
174+
if (null !== $username || null !== $password) {
175+
if (!method_exists($client, 'setSaslAuthData')) {
176+
trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
177+
}
178+
$client->setSaslAuthData($username, $password);
179+
}
180+
181+
return $client;
182+
} finally {
183+
restore_error_handler();
184+
}
30185
}
31186

32187
/**
33188
* {@inheritdoc}
34189
*/
35190
protected function doSave(array $values, $lifetime)
36191
{
37-
return $this->client->setMulti($values, $lifetime) && $this->client->getResultCode() === \Memcached::RES_SUCCESS;
192+
return $this->checkResultCode($this->client->setMulti($values, $lifetime));
38193
}
39194

40195
/**
41196
* {@inheritdoc}
42197
*/
43198
protected function doFetch(array $ids)
44199
{
45-
return $this->client->getMulti($ids);
200+
return $this->checkResultCode($this->client->getMulti($ids));
46201
}
47202

48203
/**
49204
* {@inheritdoc}
50205
*/
51206
protected function doHave($id)
52207
{
53-
return $this->client->get($id) !== false || $this->client->getResultCode() === \Memcached::RES_SUCCESS;
208+
return false !== $this->client->get($id) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode());
54209
}
55210

56211
/**
57212
* {@inheritdoc}
58213
*/
59214
protected function doDelete(array $ids)
60215
{
61-
$toDelete = count($ids);
62-
foreach ($this->client->deleteMulti($ids) as $result) {
63-
if (\Memcached::RES_SUCCESS === $result || \Memcached::RES_NOTFOUND === $result) {
64-
--$toDelete;
216+
$ok = true;
217+
foreach ($this->checkResultCode($this->client->deleteMulti($ids)) as $result) {
218+
if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) {
219+
$ok = false;
65220
}
66221
}
67222

68-
return 0 === $toDelete;
223+
return $ok;
69224
}
70225

71226
/**
72227
* {@inheritdoc}
73228
*/
74229
protected function doClear($namespace)
75230
{
76-
return $this->client->flush();
231+
return $this->checkResultCode($this->client->flush());
232+
}
233+
234+
private function checkResultCode($result)
235+
{
236+
$code = $this->client->getResultCode();
237+
238+
if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) {
239+
return $result;
240+
}
241+
242+
throw new CacheException(sprintf('MemcachedAdapter client error: %s.', strtolower($this->client->getResultMessage())));
77243
}
78244
}

0 commit comments

Comments
 (0)