[Lock] Include lock component in framework bundle
This commit is contained in:
parent
084e49f2ef
commit
b4b00c9c6f
@ -18,6 +18,8 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
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 @@ class Configuration implements ConfigurationInterface
|
||||
$this->addCacheSection($rootNode);
|
||||
$this->addPhpErrorsSection($rootNode);
|
||||
$this->addWebLinkSection($rootNode);
|
||||
$this->addLockSection($rootNode);
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
@ -875,6 +878,49 @@ class Configuration implements ConfigurationInterface
|
||||
;
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -38,6 +38,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
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\CacheWarmer\CacheWarmerInterface;
|
||||
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 @@ class FrameworkExtension extends Extension
|
||||
$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 @@ class FrameworkExtension extends Extension
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
38
src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml
Normal file
38
src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" ?>
|
||||
|
||||
<container xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
|
||||
<services>
|
||||
<defaults public="false" />
|
||||
|
||||
<service id="lock.store.flock" class="Symfony\Component\Lock\Store\FlockStore" />
|
||||
|
||||
<service id="lock.store.semaphore" class="Symfony\Component\Lock\Store\SemaphoreStore" />
|
||||
|
||||
<service id="lock.store.memcached.abstract" class="Symfony\Component\Lock\Store\MemcachedStore" abstract="true">
|
||||
<argument /> <!-- Memcached connection service -->
|
||||
</service>
|
||||
|
||||
<service id="lock.store.redis.abstract" class="Symfony\Component\Lock\Store\RedisStore" abstract="true">
|
||||
<argument /> <!-- Redis connection service -->
|
||||
</service>
|
||||
|
||||
<service id="lock.store.combined.abstract" class="Symfony\Component\Lock\Store\CombinedStore" abstract="true">
|
||||
<argument /> <!-- List of stores -->
|
||||
<argument type="service" id="lock.strategy.majority" /> <!-- Strategy -->
|
||||
</service>
|
||||
|
||||
<service id="lock.strategy.majority" class="Symfony\Component\Lock\Strategy\ConsensusStrategy" />
|
||||
|
||||
<service id="lock.factory.abstract" class="Symfony\Component\Lock\Factory" abstract="true">
|
||||
<tag name="monolog.logger" channel="lock" />
|
||||
<argument /> <!-- Store -->
|
||||
<call method="setLogger">
|
||||
<argument type="service" id="logger" on-invalid="ignore" />
|
||||
</call>
|
||||
</service>
|
||||
|
||||
</services>
|
||||
</container>
|
@ -29,6 +29,7 @@
|
||||
<xsd:element name="property-info" type="property_info" minOccurs="0" maxOccurs="1" />
|
||||
<xsd:element name="cache" type="cache" minOccurs="0" maxOccurs="1" />
|
||||
<xsd:element name="workflow" type="workflow" minOccurs="0" maxOccurs="unbounded" />
|
||||
<xsd:element name="lock" type="lock" minOccurs="0" maxOccurs="1" />
|
||||
</xsd:choice>
|
||||
|
||||
<xsd:attribute name="http-method-override" type="xsd:boolean" />
|
||||
@ -296,4 +297,19 @@
|
||||
<xsd:enumeration value="workflow" />
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
|
||||
<xsd:complexType name="lock">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="resource" type="lock_resource" minOccurs="1" maxOccurs="unbounded" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="enabled" type="xsd:boolean" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="lock_resource">
|
||||
<xsd:simpleContent>
|
||||
<xsd:extension base="xsd:string">
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:extension>
|
||||
</xsd:simpleContent>
|
||||
</xsd:complexType>
|
||||
</xsd:schema>
|
||||
|
@ -16,6 +16,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Configuration;
|
||||
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 @@ class ConfigurationTest extends TestCase
|
||||
'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',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" ?>
|
||||
<container xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:framework="http://symfony.com/schema/dic/symfony"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
|
||||
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
|
||||
|
||||
<framework:config>
|
||||
<framework:lock/>
|
||||
</framework:config>
|
||||
</container>
|
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" ?>
|
||||
<container xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:framework="http://symfony.com/schema/dic/symfony"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
|
||||
http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
|
||||
|
||||
|
||||
<parameters>
|
||||
<parameter key="env(REDIS_URL)">redis://paas.com</parameter>
|
||||
</parameters>
|
||||
|
||||
<framework:config>
|
||||
<framework:lock>
|
||||
<framework:resource name="foo">semaphore</framework:resource>
|
||||
<framework:resource name="bar">flock</framework:resource>
|
||||
<framework:resource name="baz">semaphore</framework:resource>
|
||||
<framework:resource name="baz">flock</framework:resource>
|
||||
<framework:resource name="qux">%env(REDIS_URL)%</framework:resource>
|
||||
</framework:lock>
|
||||
</framework:config>
|
||||
</container>
|
@ -0,0 +1,2 @@
|
||||
framework:
|
||||
lock: ~
|
@ -0,0 +1,9 @@
|
||||
parameters:
|
||||
env(REDIS_DSN): redis://paas.com
|
||||
|
||||
framework:
|
||||
lock:
|
||||
foo: semaphore
|
||||
bar: flock
|
||||
baz: [semaphore, flock]
|
||||
qux: "%env(REDIS_DSN)%"
|
@ -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",
|
||||
|
@ -46,7 +46,7 @@ trait LockableTrait
|
||||
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());
|
||||
|
@ -44,7 +44,7 @@ class LockableTraitTest extends TestCase
|
||||
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());
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -24,6 +24,12 @@ use Symfony\Component\Lock\StoreInterface;
|
||||
*/
|
||||
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 @@ class MemcachedStore implements StoreInterface
|
||||
$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}
|
||||
*/
|
||||
|
@ -24,6 +24,14 @@ use Symfony\Component\Lock\StoreInterface;
|
||||
*/
|
||||
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 @@ class RedisStore implements StoreInterface
|
||||
$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}
|
||||
*/
|
||||
|
54
src/Symfony/Component/Lock/Store/StoreFactory.php
Normal file
54
src/Symfony/Component/Lock/Store/StoreFactory.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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é <jeremy@derusse.com>
|
||||
*/
|
||||
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)));
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ class FlockStoreTest extends AbstractStoreTest
|
||||
*/
|
||||
protected function getStore()
|
||||
{
|
||||
return new FlockStore(sys_get_temp_dir());
|
||||
return new FlockStore();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,4 +54,100 @@ class MemcachedStoreTest extends AbstractStoreTest
|
||||
{
|
||||
$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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user