Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 43a4559

Browse files
committed
memcached cache adapter configurable with dsn/options
1 parent 69dcf41 commit 43a4559

File tree

2 files changed

+276
-6
lines changed

2 files changed

+276
-6
lines changed

src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@
1111

1212
namespace Symfony\Component\Cache\Adapter;
1313

14+
use Symfony\Component\Cache\Exception\InvalidArgumentException;
15+
1416
/**
1517
* @author Rob Frawley 2nd <[email protected]>
1618
*/
1719
class MemcachedAdapter extends AbstractAdapter
1820
{
21+
private static $clientDefaultServer = array(
22+
'host' => '127.0.0.1',
23+
'port' => 11211,
24+
'weight' => 100,
25+
);
26+
1927
private $client;
2028

2129
public function __construct(\Memcached $client, $namespace = '', $defaultLifetime = 0)
@@ -29,6 +37,51 @@ public static function isSupported()
2937
return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
3038
}
3139

40+
/**
41+
* Factory method to create adapter setup with configured \Memcache client instance.
42+
*
43+
* Valid DSN values include the following:
44+
* - memcached://localhost : Specifies only the host (defaults used for port and weight)
45+
* - memcached://example.com:1234 : Specifies host and port (defaults weight)
46+
* - memcached://example.com:1234?weight=50 : Specifies host, port, and weight (no defaults used)
47+
*
48+
* Options are expected to be passed as an associative array with indexes of the option type with corresponding
49+
* values as the option assignment. Valid options include any \Memcached constants (either as their resolved value
50+
* or as their relative constant name string). Reference the PHP manual for available option values:
51+
* - http://php.net/manual/en/memcached.constants.php
52+
*
53+
* @param string|null $dsn
54+
* @param array $opts
55+
* @param string|null $persistentId
56+
*
57+
* @return MemcachedAdapter
58+
*/
59+
public static function create($dsn = null, array $opts = array(), $persistentId = null)
60+
{
61+
$adapter = new static(new \Memcached($persistentId));
62+
$adapter->setup($dsn ? array($dsn) : array(), $opts);
63+
64+
return $adapter;
65+
}
66+
67+
/**
68+
* @param string[] $dsns
69+
* @param mixed[] $opts
70+
*
71+
* @return $this
72+
*/
73+
public function setup(array $dsns = array(), array $opts = array())
74+
{
75+
foreach ($opts as $opt => $val) {
76+
$this->setOption($opt, $val);
77+
}
78+
foreach ($dsns as $dsn) {
79+
$this->addServer($dsn);
80+
}
81+
82+
return $this;
83+
}
84+
3285
/**
3386
* {@inheritdoc}
3487
*/
@@ -75,4 +128,50 @@ protected function doClear($namespace)
75128
{
76129
return $this->client->flush();
77130
}
131+
132+
private function setOption($opt, $val)
133+
{
134+
return $this->client->setOption($this->resolveOption($opt), $this->resolveOption($val));
135+
}
136+
137+
private function resolveOption($val)
138+
{
139+
return defined($constant = '\Memcached::'.strtoupper($val)) ? constant($constant) : $val;
140+
}
141+
142+
private function addServer($dsn)
143+
{
144+
list($host, $port, $weight) = $this->parseServerDSN($dsn);
145+
146+
return $this->isServerInClientPool($host, $port)
147+
|| ($this->client->addServer($host, $port, $weight) && $this->client->getResultCode() === \Memcached::RES_SUCCESS);
148+
}
149+
150+
private function parseServerDSN($dsn)
151+
{
152+
if (false === ($srv = parse_url($dsn)) || $srv['scheme'] !== 'memcached' || count($srv) > 4) {
153+
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s (expected "memcached://[<string>[:<int>]][?weight=<int>]")', $dsn));
154+
}
155+
156+
unset($srv['scheme']);
157+
$srv += self::$clientDefaultServer;
158+
159+
if (isset($srv['query']) && 1 === preg_match('{weight=([^&]{1,})}', $srv['query'], $weight)) {
160+
unset($srv['query']);
161+
$srv['weight'] = (int) $weight[1];
162+
}
163+
164+
return array_values($srv);
165+
}
166+
167+
private function isServerInClientPool($host, $port)
168+
{
169+
foreach ($this->client->getServerList() as $srv) {
170+
if ($host === $srv['host'] && $port === $srv['port']) {
171+
return true;
172+
}
173+
}
174+
175+
return false;
176+
}
78177
}

src/Symfony/Component/Cache/Tests/Adapter/MemcachedAdapterTest.php

Lines changed: 177 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,24 @@ class MemcachedAdapterTest extends AdapterTestCase
2121
'testDefaultLifeTime' => 'Testing expiration slows down the test suite',
2222
);
2323

24-
private static $client;
24+
protected static $client;
25+
26+
protected static function getClientDefaultServer()
27+
{
28+
return sprintf(
29+
'memcached://%s:%d',
30+
getenv('MEMCACHED_HOST') ?: '127.0.0.1',
31+
getenv('MEMCACHED_PORT') ?: 11211
32+
);
33+
}
2534

2635
public static function setupBeforeClass()
2736
{
2837
if (!MemcachedAdapter::isSupported()) {
2938
self::markTestSkipped('Extension memcached >=2.2.0 required.');
3039
}
3140

32-
self::$client = new \Memcached();
33-
self::$client->addServers(array(array(
34-
getenv('MEMCACHED_HOST') ?: '127.0.0.1',
35-
getenv('MEMCACHED_PORT') ?: 11211,
36-
)));
41+
static::$client = static::getClientFromAdapter(MemcachedAdapter::create(static::getClientDefaultServer()));
3742

3843
parent::setupBeforeClass();
3944
}
@@ -47,4 +52,170 @@ public function testIsSupported()
4752
{
4853
$this->assertTrue(MemcachedAdapter::isSupported());
4954
}
55+
56+
public function testIgnoreDuplicateServerRegistrations()
57+
{
58+
$client = static::getClientFromAdapter($adapter = MemcachedAdapter::create(null));
59+
60+
$this->assertCount(0, $client->getServerList(), 'Passing "null" DSN should result in no registered servers.');
61+
62+
for ($i = 0; $i < 5; ++$i) {
63+
$adapter->setup(array(static::getClientDefaultServer()));
64+
$this->assertCount(1, $client->getServerList(), 'Duplicate server registrations should be ignored.');
65+
}
66+
}
67+
68+
/**
69+
* @dataProvider provideValidServerConfigData
70+
*/
71+
public function testValidServerConfig($dsn)
72+
{
73+
$servers = static::getClientFromAdapter(MemcachedAdapter::create($dsn))->getServerList();
74+
75+
$this->assertCount(1, $servers,
76+
sprintf('Passed DSN should result in single server registration matching "%s".', $dsn));
77+
78+
$params = parse_url($dsn);
79+
$server = $servers[0];
80+
81+
$this->assertSame($server['host'], isset($params['host']) ? $params['host'] : '127.0.0.1',
82+
sprintf('Registered server should match host from passed DSN "%s".', $dsn));
83+
$this->assertSame($server['port'], isset($params['port']) ? $params['port'] : 11211,
84+
sprintf('Registered server should match port from passed DSN "%s".', $dsn));
85+
}
86+
87+
public function provideValidServerConfigData()
88+
{
89+
$data = array(
90+
array('memcached:'),
91+
array('memcached:?weight=50'),
92+
array('memcached://127.0.0.1'),
93+
array('memcached://127.0.0.1:11211'),
94+
array('memcached://127.0.0.1?weight=50'),
95+
array('memcached://127.0.0.1:11211?weight=50'),
96+
array('memcached://127.0.0.1:11211?weight=50&extra-query=is-ignored'),
97+
);
98+
99+
for ($i = 1; $i <= 65535; $i = $i + random_int(8000, 12000)) {
100+
$data[] = array(sprintf('memcached://127.0.0.1:%d', $i));
101+
$data[] = array(sprintf('memcached://127.0.0.1:11211?weight=%d', max(floor(100 * $i / 65535), 1)));
102+
}
103+
104+
return $data;
105+
}
106+
107+
/**
108+
* @dataProvider provideNotValidServerConfigData
109+
* @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
110+
* @expectedExceptionMessageRegExp {Invalid Memcached DSN:}
111+
*/
112+
public function testNotValidServerConfig($dsn)
113+
{
114+
MemcachedAdapter::create($dsn);
115+
}
116+
117+
public function provideNotValidServerConfigData()
118+
{
119+
$data = array(
120+
array('http://google.com/?bad-scheme=wont+work'),
121+
array('redis://[email protected]/13'),
122+
);
123+
124+
for ($i = 1; $i <= 5; ++$i) {
125+
$data[] = array(sprintf('http://%1$d.%1$d.%1$d.%1$d', random_int(255, 2550)));
126+
$data[] = array(sprintf('http://%1$d.%1$d.%1$d.%1$d', random_int(-2550, 0)));
127+
$data[] = array(sprintf('%s://127.0.0.1', str_repeat(chr(random_int(97, 122)), random_int(1, 10))));
128+
}
129+
130+
for ($i = 65536; $i < 65535 * 2; $i = $i + random_int(8000, 12000)) {
131+
$data[] = array(sprintf('memcached://127.0.0.1:%d', $i));
132+
}
133+
134+
return $data;
135+
}
136+
137+
/**
138+
* @dataProvider provideValidServerOptionsData
139+
*/
140+
public function testValidServerOptions($assignments, $constants, $expectations, $messages)
141+
{
142+
$client = static::getClientFromAdapter(MemcachedAdapter::create(null, $assignments));
143+
144+
for ($i = 0; $i < count($assignments); ++$i) {
145+
$this->assertSame($expectations[$i], $client->getOption($constants[$i]), $messages[$i]);
146+
}
147+
}
148+
149+
public function provideValidServerOptionsData()
150+
{
151+
$expectationCollection = array(1, true, \Memcached::SERIALIZER_IGBINARY, 30);
152+
$expectsAltsCollection = array(true, 1, 'SERIALIZER_IGBINARY', '30');
153+
154+
$constantStrCollection = array('OPT_LIBKETAMA_COMPATIBLE', 'OPT_COMPRESSION', 'OPT_SERIALIZER', 'OPT_RETRY_TIMEOUT');
155+
$constantIntCollection = array(\Memcached::OPT_LIBKETAMA_COMPATIBLE, \Memcached::OPT_COMPRESSION, \Memcached::OPT_SERIALIZER, \Memcached::OPT_RETRY_TIMEOUT);
156+
157+
$messages = array();
158+
for ($i = 0; $i < count($expectationCollection); ++$i) {
159+
$messages[] = sprintf('Memcached client option "%s:%d" expected value "%s:%s".', $constantStrCollection[$i],
160+
$constantIntCollection[$i], gettype($expectationCollection[$i]), var_export($expectationCollection[$i], true));
161+
}
162+
163+
$assignmentSet = array(
164+
array(
165+
$constantStrCollection[0] => $expectationCollection[0], // unresolved constant type string and resolved constant value
166+
$constantStrCollection[1] => $expectationCollection[1], // unresolved constant type string and resolved constant value
167+
$constantStrCollection[2] => $expectationCollection[2], // unresolved constant type string and resolved constant value
168+
$constantStrCollection[3] => $expectationCollection[3], // unresolved constant type string and resolved constant value
169+
),
170+
array(
171+
$constantIntCollection[0] => $expectationCollection[0], // resolved constant type and resolved constant value
172+
$constantIntCollection[1] => $expectationCollection[1], // resolved constant type and resolved constant value
173+
$constantIntCollection[2] => $expectationCollection[2], // resolved constant type and resolved constant value
174+
$constantIntCollection[3] => $expectationCollection[3], // resolved constant type and resolved constant value
175+
),
176+
array(
177+
$constantIntCollection[0] => $expectsAltsCollection[0], // resolved constant type and equivalent constant value "true" to expected "1"
178+
$constantStrCollection[1] => $expectsAltsCollection[1], // unresolved constant type string and equivalent constant value "1" to expected "true"
179+
$constantIntCollection[2] => $expectsAltsCollection[2], // resolved constant type and unresolved constant value string
180+
$constantStrCollection[3] => $expectsAltsCollection[3], // unresolved constant type string and equivalent constant value "string:30" to expected "int:30"
181+
),
182+
);
183+
184+
return array_map(function ($assignments) use ($constantIntCollection, $expectationCollection, $messages) {
185+
return array($assignments, $constantIntCollection, $expectationCollection, $messages);
186+
}, $assignmentSet);
187+
}
188+
189+
/**
190+
* @dataProvider provideInvalidServerOptionsData
191+
* @expectedException \PHPUnit_Framework_Error_Warning
192+
*/
193+
public function testInvalidServerOptions(array $options)
194+
{
195+
if (defined('HHVM_VERSION')) {
196+
$this->markTestSkipped('HHVM ext-memcached silently ignores invalid passed options.');
197+
}
198+
199+
MemcachedAdapter::create(null, $options);
200+
}
201+
202+
public function provideInvalidServerOptionsData()
203+
{
204+
return array(
205+
array(array(-10000000 => 'OPT_DISTRIBUTION')),
206+
array(array('OPT_SERIALIZER' => 'OPT_DISTRIBUTION')),
207+
);
208+
}
209+
210+
/**
211+
* @return \Memcached
212+
*/
213+
private static function getClientFromAdapter(MemcachedAdapter $adapter)
214+
{
215+
$r = new \ReflectionObject($adapter);
216+
$p = $r->getProperty('client');
217+
$p->setAccessible(true);
218+
219+
return $p->getValue($adapter);
220+
}
50221
}

0 commit comments

Comments
 (0)