8000 [Cache] Add DSN, createClient & better error reporting to MemcachedAd… · symfony/symfony@e109438 · GitHub
[go: up one dir, main page]

Skip to content

Commit e109438

Browse files
[Cache] Add DSN, createClient & better error reporting to MemcachedAdapter
1 parent 0d2dfa7 commit e109438

File tree

10 files changed

+213
-17
lines changed

10 files changed

+213
-17
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
1313

14-
use Symfony\Component\Cache\Adapter\RedisAdapter;
14+
use Symfony\Component\Cache\Adapter\AbstractAdapter;
1515
use Symfony\Component\DependencyInjection\ChildDefinition;
1616
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1717
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -108,13 +108,13 @@ public static function getServiceProvider(ContainerBuilder $container, $name)
108108
{
109109
$container->resolveEnvPlaceholders($name, null, $usedEnvs);
110110

111-
if (0 === strpos($name, 'redis://') || $usedEnvs) {
111+
if ($usedEnvs || preg_match('#^[a-z]++://#', $name)) {
112112
$dsn = $name;
113113

114114
if (!$container->hasDefinition($name = md5($dsn))) {
115-
$definition = new Definition(\Redis::class);
115+
$definition = new Definition(AbstractAdapter::class);
116116
$definition->setPublic(false);
117-
$definition->setFactory(array(RedisAdapter::class, 'createConnection'));
117+
$definition->setFactory(array(AbstractAdapter::class, 'createConnection'));
118118
$definition->setArguments(array($dsn));
119119
$container->setDefinition($name, $definition);
120120
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,7 @@ private function addCacheSection(ArrayNodeDefinition $rootNode)
706706
->scalarNode('default_doctrine_provider')->end()
707707
->scalarNode('default_psr6_provider')->end()
708708
->scalarNode('default_redis_provider')->defaultValue('redis://localhost')->end()
709+
->scalarNode('default_memcached_provider')->defaultValue('memcached://localhost')->end()
709710
->arrayNode('pools')
710711
->useAttributeAsKey('name')
711712
->prototype('array')

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1256,7 +1256,7 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
12561256
// Inline any env vars referenced in the parameter
12571257
$container->setParameter('cache.prefix.seed', $container->resolveEnvPlaceholders($container->getParameter('cache.prefix.seed'), true));
12581258
}
1259-
foreach (array('doctrine', 'psr6', 'redis') as $name) {
1259+
foreach (array('doctrine', 'psr6', 'redis', 'memcached') as $name) {
12601260
if (isset($config[$name = 'default_'.$name.'_provider'])) {
12611261
$container->setAlias('cache.'.$name, new Alias(Compiler\CachePoolPass::getServiceProvider($container, $config[$name]), false));
12621262
}

src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@
9393
</call>
9494
</service>
9595

96+
<service id="cache.adapter.memcached" class="Symfony\Component\Cache\Adapter\MemcachedAdapter" abstract="true">
97+
<tag name="cache.pool" provider="cache.default_memcached_provider" clearer="cache.default_clearer" />
98+
<tag name="monolog.logger" channel="cache" />
99+
<argument /> <!-- Memcached connection service -->
100+
<argument /> <!-- namespace -->
101+
<argument>0</argument> <!-- default lifetime -->
102+
<call method="setLogger">
103+
<argument type="service" id="logger" on-invalid="ignore" />
104+
</call>
105+
</service>
106+
96107
<service id="cache.default_clearer" class="Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer">
97108
<tag name="kernel.cache_clearer" />
98109
</service>

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@
215215
<xsd:element name="default-doctrine-provider" type="xsd:string" minOccurs="0" maxOccurs="1" />
216216
<xsd:element name="default-psr6-provider" type="xsd:string" minOccurs="0" maxOccurs="1" />
217217
<xsd:element name="default-redis-provider" type="xsd:string" minOccurs="0" maxOccurs="1" />
218+
<xsd:element name="default-memcached-provider" type="xsd:string" minOccurs="0" maxOccurs="1" />
218219
<xsd:element name="pool" type="cache_pool" minOccurs="0" maxOccurs="unbounded" />
219220
</xsd:sequence>
220221
</xsd:complexType>

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ protected static function getBundleDefaultConfig()
272272
'system' => 'cache.adapter.system',
273273
'directory' => '%kernel.cache_dir%/pools',
274274
'default_redis_provider' => 'redis://localhost',
275+
'default_memcached_provider' => 'memcached://localhost',
275276
),
276277
'workflows' => array(),
277278
'php_errors' => array(

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
],
1818
"require": {
1919
"php": ">=5.5.9",
20-
"symfony/cache": "~3.2",
20+
"symfony/cache": "~3.3",
2121
"symfony/class-loader": "~3.2",
2222
"symfony/dependency-injection": "~3.3",
2323
"symfony/config": "~2.8|~3.0",

src/Symfony/Bundle/FrameworkBundle/phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<php>
1010
<ini name="error_reporting" value="-1" />
1111
<env name="REDIS_HOST" value="localhost" />
12+
<env name="MEMCACHED_HOST" value="localhost" />
1213
</php>
1314

1415
<testsuites>

src/Symfony/Component/Cache/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
*

src/Symfony/Component/Cache/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)
0