Skip to content

Commit 8d1ff7c

Browse files
committed
memcached cache adapter configurable with dsn/options
1 parent 25bf3c5 commit 8d1ff7c

File tree

2 files changed

+415
-0
lines changed

2 files changed

+415
-0
lines changed
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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 bool
71+
*/
72+
public function setup(array $dsns = array(), array $opts = array())
73+
{
74+
$isSuccess = true;
75+
foreach ($opts as $opt => $val) {
76+
$isSuccess = $this->setOption($opt, $val) && $isSuccess;
77+
}
78+
foreach ($dsns as $dsn) {
79+
$isSuccess = $this->addServer($dsn) && $isSuccess;
80+
}
81+
82+
return $isSuccess;
83+
}
84+
85+
/**
86+
* {@inheritdoc}
87+
*/
88+
protected function doSave(array $values, $lifetime)
89+
{
90+
return $this->client->setMulti($values, $lifetime)
91+
&& $this->client->getResultCode() === \Memcached::RES_SUCCESS;
92+
}
93+
94+
/**
95+
* {@inheritdoc}
96+
*/
97+
protected function doFetch(array $ids)
98+
{
99+
return $this->client->getMulti($ids);
100+
}
101+
102+
/**
103+
* {@inheritdoc}
104+
*/
105+
protected function doHave($id)
106+
{
107+
return $this->client->get($id) !== false
108+
|| $this->client->getResultCode() === \Memcached::RES_SUCCESS;
109+
}
110+
/**
111+
* {@inheritdoc}
112+
*/
113+
protected function doDelete(array $ids)
114+
{
115+
$toDelete = count($ids);
116+
foreach ((array) $this->client->deleteMulti($ids) as $result) {
117+
if (true === $result || \Memcached::RES_NOTFOUND === $result) {
118+
--$toDelete;
119+
}
120+
}
121+
122+
return 0 === $toDelete;
123+
}
124+
125+
/**
126+
* {@inheritdoc}
127+
*/
128+
protected function doClear($namespace)
129+
{
130+
if (!isset($namespace[0]) || false === $ids = $this->getIdsByPrefix($namespace)) {
131+
return $this->client->flush();
132+
}
133+
134+
$isCleared = true;
135+
do {
136+
$isCleared = $this->doDelete($ids) && $isCleared;
137+
} while ($ids = $this->getIdsByPrefix($namespace));
138+
139+
return $isCleared;
140+
}
141+
142+
private function getIdsByPrefix($namespace)
143+
{
144+
if (false === $ids = $this->client->getAllKeys()) {
145+
return false;
146+
}
147+
148+
return array_filter((array) $ids, function ($id) use ($namespace) {
149+
return 0 === strpos($id, $namespace);
150+
});
151+
}
152+
153+
private function setOption($opt, $val)
154+
{
155+
$toRestore = error_reporting(~E_ALL);
156+
$isSuccess = $this->client->setOption($this->resolveOption($opt), $this->resolveOption($val));
157+
error_reporting($toRestore);
158+
159+
return $isSuccess;
160+
}
161+
162+
private function resolveOption($val)
163+
{
164+
return defined($constant = '\Memcached::'.strtoupper($val)) ? constant($constant) : $val;
165+
}
166+
167+
private function addServer($dsn)
168+
{
169+
list($host, $port, $weight) = $this->parseServerDSN($dsn);
170+
171+
return $this->isServerInClientPool($host, $port)
172+
|| ($this->client->addServer($host, $port, $weight)
173+
&& $this->client->getResultCode() === \Memcached::RES_SUCCESS);
174+
}
175+
176+
private function parseServerDSN($dsn)
177+
{
178+
if (false === ($srv = parse_url($dsn)) || $srv['scheme'] !== 'memcached' || count($srv) > 4) {
179+
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s (expected "memcached://[<string>[:<int>]][?weight=<int>]")', $dsn));
180+
}
181+
182+
array_shift($srv);
183+
$srv += self::$defaultClientServer;
184+
185+
if (isset($srv['query']) && 1 === preg_match('{weight=([^&]{1,})}', $srv['query'], $weight)) {
186+
unset($srv['query']);
187+
$srv['weight'] = (int) $weight[1];
188+
}
189+
190+
return array_values($srv);
191+
}
192+
193+
protected function isServerInClientPool($host, $port)
194+
{
195+
return (bool) array_filter($this->client->getServerList(), function ($srv) use ($host, $port) {
196+
return $host === array_shift($srv) && $port === array_shift($srv);
197+
});
198+
}
199+
}

0 commit comments

Comments
 (0)