Skip to content

Commit 3947b38

Browse files
committed
memcached cache adapter configurable with dsn/options
1 parent 51bc35c commit 3947b38

File tree

2 files changed

+405
-0
lines changed

2 files changed

+405
-0
lines changed
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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;
13+
14+
use Symfony\Component\Cache\Exception\InvalidArgumentException;
15+
16+
/**
17+
* @author Rob Frawley 2nd <rmf@src.run>
18+
*/
19+
class MemcachedAdapter extends AbstractAdapter
20+
{
21+
private static $defaultClientServer = array(
22+
'host' => '127.0.0.1',
23+
'port' => 11211,
24+
'weight' => 100,
25+
);
26+
27+
private $client;
28+
29+
public static function isSupported()
30+
{
31+
return extension_loaded('memcached') && version_compare(phpversion('memcached'), '2.2.0', '>=');
32+
}
33+
34+
public function __construct(\Memcached $client, $namespace = '', $defaultLifetime = 0)
35+
{
36+
parent::__construct($namespace, $defaultLifetime);
37+
$this->client = $client;
38+
}
39+
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 client constants, as described in the PHP manual:
50+
* - http://php.net/manual/en/memcached.constants.php
51+
*
52+
* @param string|null $dsn
53+
* @param array $opts
54+
* @param string|null $persistentId
55+
*
56+
* @return MemcachedAdapter
57+
*/
58+
public static function create($dsn = null, array $opts = array(), $persistentId = null)
59+
{
60+
$adapter = new static(new \Memcached($persistentId));
61+
$adapter->setup($dsn ? array($dsn) : array(), $opts);
62+
63+
return $adapter;
64+
}
65+
66+
/**
67+
* @param string[] $dsns
68+
* @param mixed[] $opts
69+
*
70+
* @return $this
71+
*/
72+
public function setup(array $dsns = array(), array $opts = array())
73+
{
74+
foreach ($opts as $opt => $val) {
75+
$this->setOption($opt, $val);
76+
}
77+
foreach ($dsns as $dsn) {
78+
$this->addServer($dsn);
79+
}
80+
81+
return $this;
82+
}
83+
84+
/**
85+
* {@inheritdoc}
86+
*/
87+
protected function doSave(array $values, $lifetime)
88+
{
89+
return $this->client->setMulti($values, $lifetime)
90+
&& $this->client->getResultCode() === \Memcached::RES_SUCCESS;
91+
}
92+
93+
/**
94+
* {@inheritdoc}
95+
*/
96+
protected function doFetch(array $ids)
97+
{
98+
return $this->client->getMulti($ids);
99+
}
100+
101+
/**
102+
* {@inheritdoc}
103+
*/
104+
protected function doHave($id)
105+
{
106+
return $this->client->get($id) !== false
107+
|| $this->client->getResultCode() === \Memcached::RES_SUCCESS;
108+
}
109+
/**
110+
* {@inheritdoc}
111+
*/
112+
protected function doDelete(array $ids)
113+
{
114+
$toDelete = count($ids);
115+
foreach ((array) $this->client->deleteMulti($ids) as $result) {
116+
if (true === $result || \Memcached::RES_NOTFOUND === $result) {
117+
--$toDelete;
118+
}
119+
}
120+
121+
return 0 === $toDelete;
122+
}
123+
124+
/**
125+
* {@inheritdoc}
126+
*/
127+
protected function doClear($namespace)
128+
{
129+
if (!isset($namespace[0]) || false === $ids = $this->getIdsByPrefix($namespace)) {
130+
return $this->client->flush();
131+
}
132+
133+
$isCleared = true;
134+
do {
135+
$isCleared = $this->doDelete($ids) && $isCleared;
136+
} while ($ids = $this->getIdsByPrefix($namespace));
137+
138+
return $isCleared;
139+
}
140+
141+
private function getIdsByPrefix($namespace)
142+
{
143+
if (false === $ids = $this->client->getAllKeys()) {
144+
return false;
145+
}
146+
147+
return array_filter((array) $ids, function ($id) use ($namespace) {
148+
return 0 === strpos($id, $namespace);
149+
});
150+
}
151+
152+
private function setOption($opt, $val)
153+
{
154+
return $this->client->setOption(
155+
$this->resolveOption($opt),
156+
$this->resolveOption($val)
157+
);
158+
}
159+
160+
private function resolveOption($val)
161+
{
162+
return defined($constant = '\Memcached::'.strtoupper($val)) ? constant($constant) : $val;
163+
}
164+
165+
private function addServer($dsn)
166+
{
167+
list($host, $port, $weight) = $this->parseServerDSN($dsn);
168+
169+
return $this->isServerInClientPool($host, $port)
170+
|| ($this->client->addServer($host, $port, $weight)
171+
&& $this->client->getResultCode() === \Memcached::RES_SUCCESS);
172+
}
173+
174+
private function parseServerDSN($dsn)
175+
{
176+
if (false === ($srv = parse_url($dsn)) || $srv['scheme'] !== 'memcached' || count($srv) > 4) {
177+
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s (expected "memcached://[<string>[:<int>]][?weight=<int>]")', $dsn));
178+
}
179+
180+
unset($srv['scheme']);
181+
$srv += self::$defaultClientServer;
182+
183+
if (isset($srv['query']) && 1 === preg_match('{weight=([^&]{1,})}', $srv['query'], $weight)) {
184+
unset($srv['query']);
185+
$srv['weight'] = (int) $weight[1];
186+
}
187+
188+
return array_values($srv);
189+
}
190+
191+
private function isServerInClientPool($host, $port)
192+
{
193+
foreach ($this->client->getServerList() as $srv) {
194+
if ($host === $srv['host'] && $port === $srv['port']) {
195+
return true;
196+
}
197+
}
198+
199+
return false;
200+
}
201+
}

0 commit comments

Comments
 (0)