diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index fab5ac344600e..bc7026bd61275 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -18,6 +18,8 @@
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Form\Form;
+use Symfony\Component\Lock\Lock;
+use Symfony\Component\Lock\Store\SemaphoreStore;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Validator\Validation;
@@ -129,6 +131,7 @@ public function getConfigTreeBuilder()
$this->addCacheSection($rootNode);
$this->addPhpErrorsSection($rootNode);
$this->addWebLinkSection($rootNode);
+ $this->addLockSection($rootNode);
return $treeBuilder;
}
@@ -875,6 +878,49 @@ private function addPhpErrorsSection(ArrayNodeDefinition $rootNode)
;
}
+ private function addLockSection(ArrayNodeDefinition $rootNode)
+ {
+ $rootNode
+ ->children()
+ ->arrayNode('lock')
+ ->info('Lock configuration')
+ ->{!class_exists(FullStack::class) && class_exists(Lock::class) ? 'canBeDisabled' : 'canBeEnabled'}()
+ ->beforeNormalization()
+ ->ifString()->then(function ($v) { return array('enabled' => true, 'resources' => $v); })
+ ->end()
+ ->beforeNormalization()
+ ->ifTrue(function ($v) { return is_array($v) && !isset($v['resources']); })
+ ->then(function ($v) {
+ $e = $v['enabled'];
+ unset($v['enabled']);
+
+ return array('enabled' => $e, 'resources' => $v);
+ })
+ ->end()
+ ->addDefaultsIfNotSet()
+ ->fixXmlConfig('resource')
+ ->children()
+ ->arrayNode('resources')
+ ->requiresAtLeastOneElement()
+ ->defaultValue(array('default' => array(class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphore' : 'flock')))
+ ->beforeNormalization()
+ ->ifString()->then(function ($v) { return array('default' => $v); })
+ ->end()
+ ->beforeNormalization()
+ ->ifTrue(function ($v) { return is_array($v) && array_keys($v) === range(0, count($v) - 1); })
+ ->then(function ($v) { return array('default' => $v); })
+ ->end()
+ ->prototype('array')
+ ->beforeNormalization()->ifString()->then(function ($v) { return array($v); })->end()
+ ->prototype('scalar')->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ;
+ }
+
private function addWebLinkSection(ArrayNodeDefinition $rootNode)
{
$rootNode
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index ebb594fac1590..4c7b4dfe67716 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -38,6 +38,7 @@
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\EnvVarProcessorInterface;
+use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
@@ -54,6 +55,11 @@
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+use Symfony\Component\Lock\Factory;
+use Symfony\Component\Lock\Lock;
+use Symfony\Component\Lock\LockInterface;
+use Symfony\Component\Lock\Store\StoreFactory;
+use Symfony\Component\Lock\StoreInterface;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface;
@@ -298,6 +304,10 @@ public function load(array $configs, ContainerBuilder $container)
$this->registerPropertyInfoConfiguration($config['property_info'], $container, $loader);
}
+ if ($this->isConfigEnabled($container, $config['lock'])) {
+ $this->registerLockConfiguration($config['lock'], $container, $loader);
+ }
+
if ($this->isConfigEnabled($container, $config['web_link'])) {
if (!class_exists(HttpHeaderSerializer::class)) {
throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed.');
@@ -1672,6 +1682,84 @@ private function registerPropertyInfoConfiguration(array $config, ContainerBuild
}
}
+ private function registerLockConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
+ {
+ $loader->load('lock.xml');
+
+ foreach ($config['resources'] as $resourceName => $resourceStores) {
+ if (0 === count($resourceStores)) {
+ continue;
+ }
+
+ // Generate stores
+ $storeDefinitions = array();
+ foreach ($resourceStores as $storeDsn) {
+ $storeDsn = $container->resolveEnvPlaceholders($storeDsn, null, $usedEnvs);
+ switch (true) {
+ case 'flock' === $storeDsn:
+ $storeDefinition = new Reference('lock.store.flock');
+ break;
+ case 'semaphore' === $storeDsn:
+ $storeDefinition = new Reference('lock.store.semaphore');
+ break;
+ case $usedEnvs || preg_match('#^[a-z]++://#', $storeDsn):
+ if (!$container->hasDefinition($connectionDefinitionId = $container->hash($storeDsn))) {
+ $connectionDefinition = new Definition(\stdClass::class);
+ $connectionDefinition->setPublic(false);
+ $connectionDefinition->setFactory(array(StoreFactory::class, 'createConnection'));
+ $connectionDefinition->setArguments(array($storeDsn));
+ $container->setDefinition($connectionDefinitionId, $connectionDefinition);
+ }
+
+ $storeDefinition = new Definition(StoreInterface::class);
+ $storeDefinition->setPublic(false);
+ $storeDefinition->setFactory(array(StoreFactory::class, 'createStore'));
+ $storeDefinition->setArguments(array(new Reference($connectionDefinitionId)));
+
+ $container->setDefinition($storeDefinitionId = 'lock.'.$resourceName.'.store.'.$container->hash($storeDsn), $storeDefinition);
+
+ $storeDefinition = new Reference($storeDefinitionId);
+ break;
+ default:
+ throw new InvalidArgumentException(sprintf('Lock store DSN "%s" is not valid in resource "%s"', $storeDsn, $resourceName));
+ }
+
+ $storeDefinitions[] = $storeDefinition;
+ }
+
+ // Wrap array of stores with CombinedStore
+ if (count($storeDefinitions) > 1) {
+ $combinedDefinition = new ChildDefinition('lock.store.combined.abstract');
+ $combinedDefinition->replaceArgument(0, $storeDefinitions);
+ $container->setDefinition('lock.'.$resourceName.'.store', $combinedDefinition);
+ } else {
+ $container->setAlias('lock.'.$resourceName.'.store', new Alias((string) $storeDefinitions[0], false));
+ }
+
+ // Generate factories for each resource
+ $factoryDefinition = new ChildDefinition('lock.factory.abstract');
+ $factoryDefinition->replaceArgument(0, new Reference('lock.'.$resourceName.'.store'));
+ $container->setDefinition('lock.'.$resourceName.'.factory', $factoryDefinition);
+
+ // Generate services for lock instances
+ $lockDefinition = new Definition(Lock::class);
+ $lockDefinition->setPublic(false);
+ $lockDefinition->setFactory(array(new Reference('lock.'.$resourceName.'.factory'), 'createLock'));
+ $lockDefinition->setArguments(array($resourceName));
+ $container->setDefinition('lock.'.$resourceName, $lockDefinition);
+
+ // provide alias for default resource
+ if ('default' === $resourceName) {
+ $container->setAlias('lock.store', new Alias('lock.'.$resourceName.'.store', false));
+ $container->setAlias('lock.factory', new Alias('lock.'.$resourceName.'.factory', false));
+ $container->setAlias('lock', new Alias('lock.'.$resourceName, false));
+ $container->setAlias(StoreInterface::class, new Alias('lock.store', false));
+ $container->setAlias(Factory::class, new Alias('lock.factory', false));
+ $container->setAlias(LockInterface::class, new Alias('lock', false));
+ }
+ }
+ }
+
private function registerCacheConfiguration(array $config, ContainerBuilder $container)
{
$version = substr(str_replace('/', '-', base64_encode(hash('sha256', uniqid(mt_rand(), true), true))), 0, 22);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml
new file mode 100644
index 0000000000000..e4c2231c1c155
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
index f63f93723029d..181dd9334a80b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
@@ -29,6 +29,7 @@
+
@@ -296,4 +297,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
index e4e6f0f2a7bea..e6e83d40b538d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
@@ -16,6 +16,7 @@
use Symfony\Bundle\FullStack;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Processor;
+use Symfony\Component\Lock\Store\SemaphoreStore;
class ConfigurationTest extends TestCase
{
@@ -343,6 +344,14 @@ protected static function getBundleDefaultConfig()
'web_link' => array(
'enabled' => !class_exists(FullStack::class),
),
+ 'lock' => array(
+ 'enabled' => !class_exists(FullStack::class),
+ 'resources' => array(
+ 'default' => array(
+ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphore' : 'flock',
+ ),
+ ),
+ ),
);
}
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/lock.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/lock.xml
new file mode 100644
index 0000000000000..fc2bf0657a615
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/lock.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/lock_named.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/lock_named.xml
new file mode 100644
index 0000000000000..d36c482de62ea
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/lock_named.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ redis://paas.com
+
+
+
+
+ semaphore
+ flock
+ semaphore
+ flock
+ %env(REDIS_URL)%
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/lock.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/lock.yml
new file mode 100644
index 0000000000000..70f578a143a56
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/lock.yml
@@ -0,0 +1,2 @@
+framework:
+ lock: ~
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/lock_named.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/lock_named.yml
new file mode 100644
index 0000000000000..6d0cb5ca638bd
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/lock_named.yml
@@ -0,0 +1,9 @@
+parameters:
+ env(REDIS_DSN): redis://paas.com
+
+framework:
+ lock:
+ foo: semaphore
+ bar: flock
+ baz: [semaphore, flock]
+ qux: "%env(REDIS_DSN)%"
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index 9b16b9417950c..18065e6782306 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -54,6 +54,7 @@
"symfony/workflow": "~3.3|~4.0",
"symfony/yaml": "~3.2|~4.0",
"symfony/property-info": "~3.3|~4.0",
+ "symfony/lock": "~3.4|~4.0",
"symfony/web-link": "~3.3|~4.0",
"doctrine/annotations": "~1.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0",
diff --git a/src/Symfony/Component/Console/Command/LockableTrait.php b/src/Symfony/Component/Console/Command/LockableTrait.php
index b521f3b7708b9..308ebf28c045a 100644
--- a/src/Symfony/Component/Console/Command/LockableTrait.php
+++ b/src/Symfony/Component/Console/Command/LockableTrait.php
@@ -46,7 +46,7 @@ private function lock($name = null, $blocking = false)
if (SemaphoreStore::isSupported($blocking)) {
$store = new SemaphoreStore();
} else {
- $store = new FlockStore(sys_get_temp_dir());
+ $store = new FlockStore();
}
$this->lock = (new Factory($store))->createLock($name ?: $this->getName());
diff --git a/src/Symfony/Component/Console/Tests/Command/LockableTraitTest.php b/src/Symfony/Component/Console/Tests/Command/LockableTraitTest.php
index 401ff823a7761..a622d1b4895f5 100644
--- a/src/Symfony/Component/Console/Tests/Command/LockableTraitTest.php
+++ b/src/Symfony/Component/Console/Tests/Command/LockableTraitTest.php
@@ -44,7 +44,7 @@ public function testLockReturnsFalseIfAlreadyLockedByAnotherCommand()
if (SemaphoreStore::isSupported(false)) {
$store = new SemaphoreStore();
} else {
- $store = new FlockStore(sys_get_temp_dir());
+ $store = new FlockStore();
}
$lock = (new Factory($store))->createLock($command->getName());
diff --git a/src/Symfony/Component/Lock/Store/FlockStore.php b/src/Symfony/Component/Lock/Store/FlockStore.php
index cd0a276de4a38..5babc7f610bce 100644
--- a/src/Symfony/Component/Lock/Store/FlockStore.php
+++ b/src/Symfony/Component/Lock/Store/FlockStore.php
@@ -32,12 +32,15 @@ class FlockStore implements StoreInterface
private $lockPath;
/**
- * @param string $lockPath the directory to store the lock
+ * @param string|null $lockPath the directory to store the lock, defaults to the system's temporary directory
*
* @throws LockStorageException If the lock directory could not be created or is not writable
*/
- public function __construct($lockPath)
+ public function __construct($lockPath = null)
{
+ if (null === $lockPath) {
+ $lockPath = sys_get_temp_dir();
+ }
if (!is_dir($lockPath) || !is_writable($lockPath)) {
throw new InvalidArgumentException(sprintf('The directory "%s" is not writable.', $lockPath));
}
diff --git a/src/Symfony/Component/Lock/Store/MemcachedStore.php b/src/Symfony/Component/Lock/Store/MemcachedStore.php
index 8e9db10cd036f..4a2ffa3e02042 100644
--- a/src/Symfony/Component/Lock/Store/MemcachedStore.php
+++ b/src/Symfony/Component/Lock/Store/MemcachedStore.php
@@ -24,6 +24,12 @@
*/
class MemcachedStore implements StoreInterface
{
+ private static $defaultClientOptions = array(
+ 'persistent_id' => null,
+ 'username' => null,
+ 'password' => null,
+ );
+
private $memcached;
private $initialTtl;
/** @var bool */
@@ -52,6 +58,128 @@ public function __construct(\Memcached $memcached, $initialTtl = 300)
$this->initialTtl = $initialTtl;
}
+ /**
+ * Creates a Memcached instance.
+ *
+ * By default, the binary protocol, block, and libketama compatible options are enabled.
+ *
+ * Example DSN:
+ * - 'memcached://user:pass@localhost?weight=33'
+ * - array(array('localhost', 11211, 33))
+ *
+ * @param string $dsn A server or A DSN
+ * @param array $options An array of options
+ *
+ * @return \Memcached
+ *
+ * @throws \ErrorEception When invalid options or server are provided
+ */
+ public static function createConnection($server, array $options = array())
+ {
+ if (!static::isSupported()) {
+ throw new InvalidArgumentException('Memcached extension is required');
+ }
+ set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
+ try {
+ $options += static::$defaultClientOptions;
+ $client = new \Memcached($options['persistent_id']);
+ $username = $options['username'];
+ $password = $options['password'];
+
+ // parse any DSN in $server
+ if (is_string($server)) {
+ if (0 !== strpos($server, 'memcached://')) {
+ throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s does not start with "memcached://"', $server));
+ }
+ $params = preg_replace_callback('#^memcached://(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
+ if (!empty($m[1])) {
+ list($username, $password) = explode(':', $m[1], 2) + array(1 => null);
+ }
+
+ return 'file://';
+ }, $server);
+ if (false === $params = parse_url($params)) {
+ throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $server));
+ }
+ if (!isset($params['host']) && !isset($params['path'])) {
+ throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $server));
+ }
+ if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
+ $params['weight'] = $m[1];
+ $params['path'] = substr($params['path'], 0, -strlen($m[0]));
+ }
+ $params += array(
+ 'host' => isset($params['host']) ? $params['host'] : $params['path'],
+ 'port' => isset($params['host']) ? 11211 : null,
+ 'weight' => 0,
+ );
+ if (isset($params['query'])) {
+ parse_str($params['query'], $query);
+ $params += $query;
+ $options = $query + $options;
+ }
+
+ $server = array($params['host'], $params['port'], $params['weight']);
+ }
+
+ // set client's options
+ unset($options['persistent_id'], $options['username'], $options['password'], $options['weight']);
+ $options = array_change_key_case($options, CASE_UPPER);
+ $client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
+ $client->setOption(\Memcached::OPT_NO_BLOCK, false);
+ if (!array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) {
+ $client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
+ }
+ foreach ($options as $name => $value) {
+ if (is_int($name)) {
+ continue;
+ }
+ if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
+ $value = constant('Memcached::'.$name.'_'.strtoupper($value));
+ }
+ $opt = constant('Memcached::OPT_'.$name);
+
+ unset($options[$name]);
+ $options[$opt] = $value;
+ }
+ $client->setOptions($options);
+
+ // set client's servers, taking care of persistent connections
+ if (!$client->isPristine()) {
+ $oldServers = array();
+ foreach ($client->getServerList() as $server) {
+ $oldServers[] = array($server['host'], $server['port']);
+ }
+
+ $newServers = array();
+ if (1 < count($server)) {
+ $server = array_values($server);
+ unset($server[2]);
+ $server[1] = (int) $server[1];
+ }
+ $newServers[] = $server;
+
+ if ($oldServers !== $newServers) {
+ // before resetting, ensure $servers is valid
+ $client->addServers(array($server));
+ $client->resetServerList();
+ }
+ }
+ $client->addServers(array($server));
+
+ if (null !== $username || null !== $password) {
+ if (!method_exists($client, 'setSaslAuthData')) {
+ trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
+ }
+ $client->setSaslAuthData($username, $password);
+ }
+
+ return $client;
+ } finally {
+ restore_error_handler();
+ }
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Symfony/Component/Lock/Store/RedisStore.php b/src/Symfony/Component/Lock/Store/RedisStore.php
index 88b15938997c9..66a067dfb0f9d 100644
--- a/src/Symfony/Component/Lock/Store/RedisStore.php
+++ b/src/Symfony/Component/Lock/Store/RedisStore.php
@@ -24,6 +24,14 @@
*/
class RedisStore implements StoreInterface
{
+ private static $defaultConnectionOptions = array(
+ 'class' => null,
+ 'persistent' => 0,
+ 'persistent_id' => null,
+ 'timeout' => 30,
+ 'read_timeout' => 0,
+ 'retry_interval' => 0,
+ );
private $redis;
private $initialTtl;
@@ -45,6 +53,88 @@ public function __construct($redisClient, $initialTtl = 300.0)
$this->initialTtl = $initialTtl;
}
+ /**
+ * Creates a Redis connection using a DSN configuration.
+ *
+ * Example DSN:
+ * - redis://localhost
+ * - redis://example.com:1234
+ * - redis://secret@example.com/13
+ * - redis:///var/run/redis.sock
+ * - redis://secret@/var/run/redis.sock/13
+ *
+ * @param string $dsn
+ * @param array $options See self::$defaultConnectionOptions
+ *
+ * @throws InvalidArgumentException When the DSN is invalid
+ *
+ * @return \Redis|\Predis\Client According to the "class" option
+ */
+ public static function createConnection($dsn, array $options = array())
+ {
+ if (0 !== strpos($dsn, 'redis://')) {
+ throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s does not start with "redis://"', $dsn));
+ }
+ $params = preg_replace_callback('#^redis://(?:(?:[^:@]*+:)?([^@]*+)@)?#', function ($m) use (&$auth) {
+ if (isset($m[1])) {
+ $auth = $m[1];
+ }
+
+ return 'file://';
+ }, $dsn);
+ if (false === $params = parse_url($params)) {
+ throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
+ }
+ if (!isset($params['host']) && !isset($params['path'])) {
+ throw new InvalidArgumentException(sprintf('Invalid Redis DSN: %s', $dsn));
+ }
+ if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
+ $params['dbindex'] = $m[1];
+ $params['path'] = substr($params['path'], 0, -strlen($m[0]));
+ }
+ $params += array(
+ 'host' => isset($params['host']) ? $params['host'] : $params['path'],
+ 'port' => isset($params['host']) ? 6379 : null,
+ 'dbindex' => 0,
+ );
+ if (isset($params['query'])) {
+ parse_str($params['query'], $query);
+ $params += $query;
+ }
+ $params += $options + self::$defaultConnectionOptions;
+ $class = null === $params['class'] ? (extension_loaded('redis') ? \Redis::class : \Predis\Client::class) : $params['class'];
+
+ if (is_a($class, \Redis::class, true)) {
+ $connect = $params['persistent'] || $params['persistent_id'] ? 'pconnect' : 'connect';
+ $redis = new $class();
+ @$redis->{$connect}($params['host'], $params['port'], $params['timeout'], $params['persistent_id'], $params['retry_interval']);
+
+ if (@!$redis->isConnected()) {
+ $e = ($e = error_get_last()) && preg_match('/^Redis::p?connect\(\): (.*)/', $e['message'], $e) ? sprintf(' (%s)', $e[1]) : '';
+ throw new InvalidArgumentException(sprintf('Redis connection failed%s: %s', $e, $dsn));
+ }
+
+ if ((null !== $auth && !$redis->auth($auth))
+ || ($params['dbindex'] && !$redis->select($params['dbindex']))
+ || ($params['read_timeout'] && !$redis->setOption(\Redis::OPT_READ_TIMEOUT, $params['read_timeout']))
+ ) {
+ $e = preg_replace('/^ERR /', '', $redis->getLastError());
+ throw new InvalidArgumentException(sprintf('Redis connection failed (%s): %s', $e, $dsn));
+ }
+ } elseif (is_a($class, \Predis\Client::class, true)) {
+ $params['scheme'] = isset($params['host']) ? 'tcp' : 'unix';
+ $params['database'] = $params['dbindex'] ?: null;
+ $params['password'] = $auth;
+ $redis = new $class((new Factory())->create($params));
+ } elseif (class_exists($class, false)) {
+ throw new InvalidArgumentException(sprintf('"%s" is not a subclass of "Redis" or "Predis\Client"', $class));
+ } else {
+ throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $class));
+ }
+
+ return $redis;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/Symfony/Component/Lock/Store/StoreFactory.php b/src/Symfony/Component/Lock/Store/StoreFactory.php
new file mode 100644
index 0000000000000..9a23cf6472dea
--- /dev/null
+++ b/src/Symfony/Component/Lock/Store/StoreFactory.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Store;
+
+use Symfony\Component\Lock\Exception\InvalidArgumentException;
+
+/**
+ * StoreFactory create stores and connections.
+ *
+ * @author Jérémy Derussé
+ */
+class StoreFactory
+{
+ public static function createConnection($dsn, array $options = array())
+ {
+ if (!is_string($dsn)) {
+ throw new InvalidArgumentException(sprintf('The %s() method expects argument #1 to be string, %s given.', __METHOD__, gettype($dsn)));
+ }
+ if (0 === strpos($dsn, 'redis://')) {
+ return RedisStore::createConnection($dsn, $options);
+ }
+ if (0 === strpos($dsn, 'memcached://')) {
+ return MemcachedStore::createConnection($dsn, $options);
+ }
+
+ throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
+ }
+
+ /**
+ * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|\Memcached $connection
+ *
+ * @return RedisStore|MemcachedStore
+ */
+ public static function createStore($connection)
+ {
+ if ($connection instanceof \Redis || $connection instanceof \RedisArray || $connection instanceof \RedisCluster || $connection instanceof \Predis\Client) {
+ return new RedisStore($connection);
+ }
+ if ($connection instanceof \Memcached) {
+ return new MemcachedStore($connection);
+ }
+
+ throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', get_class($connection)));
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/FlockStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/FlockStoreTest.php
index 53d2ae78dc78a..ef3650c3124b5 100644
--- a/src/Symfony/Component/Lock/Tests/Store/FlockStoreTest.php
+++ b/src/Symfony/Component/Lock/Tests/Store/FlockStoreTest.php
@@ -26,7 +26,7 @@ class FlockStoreTest extends AbstractStoreTest
*/
protected function getStore()
{
- return new FlockStore(sys_get_temp_dir());
+ return new FlockStore();
}
/**
diff --git a/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php
index eb030fba0f9de..cfe03b25e2c34 100644
--- a/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php
+++ b/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php
@@ -54,4 +54,100 @@ public function testAbortAfterExpiration()
{
$this->markTestSkipped('Memcached expects a TTL greater than 1 sec. Simulating a slow network is too hard');
}
+
+ public function testDefaultOptions()
+ {
+ $this->assertTrue(MemcachedStore::isSupported());
+
+ $client = MemcachedStore::createConnection('memcached://127.0.0.1');
+
+ $this->assertTrue($client->getOption(\Memcached::OPT_COMPRESSION));
+ $this->assertSame(1, $client->getOption(\Memcached::OPT_BINARY_PROTOCOL));
+ $this->assertSame(1, $client->getOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE));
+ }
+
+ /**
+ * @dataProvider provideServersSetting
+ */
+ public function testServersSetting($dsn, $host, $port)
+ {
+ $client1 = MemcachedStore::createConnection($dsn);
+ $client3 = MemcachedStore::createConnection(array($host, $port));
+ $expect = array(
+ 'host' => $host,
+ 'port' => $port,
+ );
+
+ $f = function ($s) { return array('host' => $s['host'], 'port' => $s['port']); };
+ $this->assertSame(array($expect), array_map($f, $client1->getServerList()));
+ $this->assertSame(array($expect), array_map($f, $client3->getServerList()));
+ }
+
+ public function provideServersSetting()
+ {
+ yield array(
+ 'memcached://127.0.0.1/50',
+ '127.0.0.1',
+ 11211,
+ );
+ yield array(
+ 'memcached://localhost:11222?weight=25',
+ 'localhost',
+ 11222,
+ );
+ if (ini_get('memcached.use_sasl')) {
+ yield array(
+ 'memcached://user:password@127.0.0.1?weight=50',
+ '127.0.0.1',
+ 11211,
+ );
+ }
+ yield array(
+ 'memcached:///var/run/memcached.sock?weight=25',
+ '/var/run/memcached.sock',
+ 0,
+ );
+ yield array(
+ 'memcached:///var/local/run/memcached.socket?weight=25',
+ '/var/local/run/memcached.socket',
+ 0,
+ );
+ if (ini_get('memcached.use_sasl')) {
+ yield array(
+ 'memcached://user:password@/var/local/run/memcached.socket?weight=25',
+ '/var/local/run/memcached.socket',
+ 0,
+ );
+ }
+ }
+
+ /**
+ * @dataProvider provideDsnWithOptions
+ */
+ public function testDsnWithOptions($dsn, array $options, array $expectedOptions)
+ {
+ $client = MemcachedStore::createConnection($dsn, $options);
+
+ foreach ($expectedOptions as $option => $expect) {
+ $this->assertSame($expect, $client->getOption($option));
+ }
+ }
+
+ public function provideDsnWithOptions()
+ {
+ if (!class_exists('\Memcached')) {
+ self::markTestSkipped('Extension memcached required.');
+ }
+
+ yield array(
+ 'memcached://localhost:11222?retry_timeout=10',
+ array(\Memcached::OPT_RETRY_TIMEOUT => 8),
+ array(\Memcached::OPT_RETRY_TIMEOUT => 10),
+ );
+ yield array(
+ 'memcached://localhost:11222?socket_recv_size=1&socket_send_size=2',
+ array(\Memcached::OPT_RETRY_TIMEOUT => 8),
+ array(\Memcached::OPT_SOCKET_RECV_SIZE => 1, \Memcached::OPT_SOCKET_SEND_SIZE => 2, \Memcached::OPT_RETRY_TIMEOUT => 8),
+ );
+ }
}