Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d35b594

Browse filesBrowse files
committed
support memcached dsn using new memcacheclient factory helper
1 parent 9f95654 commit d35b594
Copy full SHA for d35b594

File tree

4 files changed

+388
-11
lines changed
Filter options

4 files changed

+388
-11
lines changed
+189Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Cache\Adapter\Client;
13+
14+
use Symfony\Component\Cache\Exception\InvalidArgumentException;
15+
16+
/**
17+
* @author Rob Frawley 2nd <rmf@src.run>
18+
*/
19+
class MemcachedClient
20+
{
21+
private static $serverDefaults = array(
22+
'host' => 'localhost',
23+
'port' => 11211,
24+
'weight' => 100,
25+
);
26+
27+
private static $optionDefaults = array(
28+
'compression' => true,
29+
'libketama_compatible' => true,
30+
);
31+
32+
private $client;
33+
34+
public function __construct(array $servers = array(), array $options = array())
35+
{
36+
$this->client = new \Memcached(isset($options['persistent_id']) ? $options['persistent_id'] : null);
37+
$this->setOptions($options);
38+
$this->setServers($servers);
39+
}
40+
41+
/**
42+
* @return \Memcached
43+
*/
44+
public static function create($servers = array(), array $options = array())
45+
{
46+
return (new static(is_array($servers) ? $servers : array($servers), $options))->getClient();
47+
}
48+
49+
public static function isSupported()
50+
{
51+
return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
52+
}
53+
54+
public function getClient()
55+
{
56+
return $this->client;
57+
}
58+
59+
private function setOptions(array $options)
60+
{
61+
unset($options['persistent_id']);
62+
$options += static::$optionDefaults;
63+
64+
foreach ($options as $named => $value) {
65+
$this->addOption($named, $value);
66+
}
67+
}
68+
69+
private function addOption($named, $value)
70+
{
71+
$toRestore = error_reporting(~E_ALL);
72+
$isSuccess = $this->client->setOption(
73+
$this->resolveOptionNamed($named),
74+
$this->resolveOptionValue($named, $value)
75+
);
76+
error_reporting($toRestore);
77+
78+
if (!$isSuccess) {
79+
$errLast = error_get_last();
80+
$msgLast = isset($errLast['message']) ? $errLast['message'] : $this->client->getResultMessage();
81+
82+
throw new InvalidArgumentException(
83+
sprintf('Invalid option: %s=%s (%s)', var_export($named, true), var_export($value, true), $msgLast)
84+
);
85+
}
86+
}
87+
88+
private function resolveOptionNamed($named)
89+
{
90+
if (!defined($constant = sprintf('\Memcached::OPT_%s', strtoupper($named)))) {
91+
throw new InvalidArgumentException(sprintf('Invalid option named: %s', $named));
92+
}
93+
94+
return constant($constant);
95+
}
96+
97+
private function resolveOptionValue($named, $value)
98+
{
99+
$typed = preg_replace('{_.*$}', '', $named);
100+
101+
if (defined($constant = sprintf('\Memcached::%s_%s', strtoupper($typed), strtoupper($value)))
102+
|| defined($constant = sprintf('\Memcached::%s', strtoupper($value)))) {
103+
return constant($constant);
104+
}
105+
106+
return $value;
107+
}
108+
109+
private function setServers(array $dsns)
110+
{
111+
foreach ($dsns as $i => $dsn) {
112+
$this->addServer($i, $dsn);
113+
}
114+
}
115+
116+
private function addServer($i, $dsn)
117+
{
118+
if (false === $server = $this->resolveServer($dsn)) {
119+
throw new InvalidArgumentException(sprintf('Invalid server %d DSN: %s', $i, $dsn));
120+
}
121+
122+
if ($this->hasServer($server['host'], $server['port'])) {
123+
return;
124+
}
125+
126+
$this->client->addServer($server['host'], $server['port'], $server['weight']);
127+
}
128+
129+
private function hasServer($host, $port)
130+
{
131+
foreach ($this->client->getServerList() as $server) {
132+
if ($server['host'] === $host && $server['port'] === $port) {
133+
return true;
134+
}
135+
}
136+
137+
return false;
138+
}
139+
140+
private function resolveServer($dsn)
141+
{
142+
if (0 !== strpos($dsn, 'memcached')) {
143+
return false;
144+
}
145+
146+
if (false !== $server = $this->resolveServerAsHost($dsn)) {
147+
return $server;
148+
}
149+
150+
return $this->resolveServerAsSock($dsn);
151+
}
152+
153+
private function resolveServerAsHost($dsn)
154+
{
155+
if (false === $server = parse_url($dsn)) {
156+
return false;
157+
}
158+
159+
return $this->resolveServerReturn($server);
160+
}
161+
162+
private function resolveServerAsSock($dsn)
163+
{
164+
if (1 !== preg_match('{memcached:\/\/(?<host>\/[^?]+)(?:\?)?(?<query>.+)?}', $dsn, $matches)) {
165+
return false;
166+
}
167+
168+
return $this->resolveServerReturn(array(
169+
'host' => 0 === strpos(strrev($matches['host']), '/') ? substr($matches['host'], 0, -1) : $matches['host'],
170+
'query' => isset($matches['query']) ? $matches['query'] : null,
171+
));
172+
}
173+
174+
private function resolveServerReturn($server)
175+
{
176+
parse_str(isset($server['query']) ? $server['query'] : '', $query);
177+
178+
$query = array_filter($query, function ($index) {
179+
return in_array($index, array('weight'));
180+
}, ARRAY_FILTER_USE_KEY);
181+
182+
$server += $query;
183+
$server += static::$serverDefaults;
184+
185+
return array_filter($server, function ($index) {
186+
return in_array($index, array('host', 'port', 'weight'));
187+
}, ARRAY_FILTER_USE_KEY);
188+
}
189+
}

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php
+19-2Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Cache\Adapter;
1313

14+
use Symfony\Component\Cache\Adapter\Client\MemcachedClient;
15+
1416
/**
1517
* @author Rob Frawley 2nd <rmf@src.run>
1618
*/
@@ -24,9 +26,15 @@ public function __construct(\Memcached $client, $namespace = '', $defaultLifetim
2426
$this->client = $client;
2527
}
2628

27-
public static function isSupported()
29+
/**
30+
* @param string[] $servers
31+
* @param mixed[] $options
32+
*
33+
* @return \Memcached
34+
*/
35+
public static function createConnection($servers = array(), array $options = array())
2836
{
29-
return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
37+
return MemcachedClient::create($servers, $options);
3038
}
3139

3240
/**
@@ -75,4 +83,13 @@ protected function doClear($namespace)
7583
{
7684
return $this->client->flush();
7785
}
86+
87+
public function __destruct()
88+
{
89+
if (!$this->client->isPersistent() && method_exists($this->client, 'flushBuffers')) {
90+
$this->client->flushBuffers();
91+
}
92+
93+
parent::__destruct();
94+
}
7895
}
+155Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Cache\Tests\Adapter\Client;
13+
14+
use Symfony\Component\Cache\Adapter\Client\MemcachedClient;
15+
16+
class MemcachedClientTest extends \PHPUnit_Framework_TestCase
17+
{
18+
public static function setupBeforeClass()
19+
{
20+
if (!MemcachedClient::isSupported()) {
21+
self::markTestSkipped('Memcached extension >= 2.2.0 required for test.');
22+
}
23+
24+
parent::setupBeforeClass();
25+
}
26+
27+
public function testIsSupported()
28+
{
29+
$this->assertTrue(MemcachedClient::isSupported());
30+
}
31+
32+
public function testServersNoDuplicates()
33+
{
34+
$dsns = array(
35+
'memcached://127.0.0.1:11211',
36+
'memcached://127.0.0.1:11211',
37+
'memcached://127.0.0.1:11211',
38+
'memcached://127.0.0.1:11211',
39+
);
40+
41+
$this->assertCount(1, MemcachedClient::create($dsns)->getServerList());
42+
}
43+
44+
/**
45+
* @dataProvider provideServersSetting
46+
*/
47+
public function testServersSetting($dsn, $host, $port, $type)
48+
{
49+
$client1 = MemcachedClient::create($dsn);
50+
$client2 = MemcachedClient::create(array($dsn));
51+
$expect = array(
52+
'host' => $host,
53+
'port' => $port,
54+
'type' => $type,
55+
);
56+
57+
$this->assertSame(array($expect), $client1->getServerList());
58+
$this->assertSame(array($expect), $client2->getServerList());
59+
}
60+
61+
public function provideServersSetting()
62+
{
63+
return array(
64+
array(
65+
'memcached://127.0.0.1?weight=50',
66+
'127.0.0.1',
67+
11211,
68+
'TCP',
69+
),
70+
array(
71+
'memcached://localhost:11222?weight=25',
72+
'localhost',
73+
11222,
74+
'TCP',
75+
),
76+
array(
77+
'memcached:///var/run/memcached.sock?weight=25',
78+
'/var/run/memcached.sock',
79+
11211,
80+
'SOCKET',
81+
),
82+
array(
83+
'memcached:///var/local/run/memcached.socket/?weight=25',
84+
'/var/local/run/memcached.socket',
85+
11211,
86+
'SOCKET',
87+
),
88+
);
89+
}
90+
91+
/**
92+
* @dataProvider provideServersInvalid
93+
* @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
94+
* @expectedExceptionMessageRegExp {Invalid server ([0-9]+ )?DSN:}
95+
*/
96+
public function testServersInvalid($server)
97+
{
98+
MemcachedClient::create(array($server));
99+
}
100+
101+
public function provideServersInvalid()
102+
{
103+
return array(
104+
array('redis://127.0.0.1'),
105+
array('memcached://localhost:bad-port'),
106+
);
107+
}
108+
109+
/**
110+
* @dataProvider provideOptionsSetting
111+
*/
112+
public function testOptionsSetting($named, $value, $resolvedNamed, $resolvedValue)
113+
{
114+
$client = MemcachedClient::create(array(), array($named => $value));
115+
116+
$this->assertSame($resolvedValue, $client->getOption($resolvedNamed));
117+
}
118+
119+
public function provideOptionsSetting()
120+
{
121+
return array(
122+
array('serializer', 'igbinary', \Memcached::OPT_SERIALIZER, \Memcached::SERIALIZER_IGBINARY),
123+
array('hash', 'md5', \Memcached::OPT_HASH, \Memcached::HASH_MD5),
124+
array('compression', true, \Memcached::OPT_COMPRESSION, true),
125+
array('compression_type', 'fastlz', \Memcached::OPT_COMPRESSION_TYPE, \Memcached::COMPRESSION_FASTLZ),
126+
array('libketama_compatible', false, \Memcached::OPT_LIBKETAMA_COMPATIBLE, 0),
127+
);
128+
}
129+
130+
/**
131+
* @dataProvider provideOptionsInvalid
132+
* @expectedException \Symfony\Component\Cache\Exception\InvalidArgumentException
133+
* @expectedExceptionMessageRegExp {Invalid option( named)?: ([a-z']+)(=[a-z']+)?}
134+
*/
135+
public function testOptionsInvalid($named, $value)
136+
{
137+
MemcachedClient::create(array(), array($named => $value));
138+
}
139+
140+
public function provideOptionsInvalid()
141+
{
142+
return array(
143+
array('invalid_named', 'hash_md5'),
144+
array('prefix_key', str_repeat('abcdef', 128)),
145+
);
146+
}
147+
148+
public function testOptionsDefault()
149+
{
150+
$client = MemcachedClient::create();
151+
152+
$this->assertSame(1, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE));
153+
$this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION));
154+
}
155+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.